From f8d82724067fd464d65ba5e07c6ef01fb848beee Mon Sep 17 00:00:00 2001 From: Robin Ward <robin.ward@gmail.com> Date: Wed, 6 Mar 2013 15:17:07 -0500 Subject: [PATCH] Cleaned up TopicUserSpec, introduces clearing of pinned topics --- Gemfile | 2 +- Gemfile.lock | 2 +- .../discourse/controllers/topic_controller.js | 9 + .../javascripts/discourse/models/topic.js | 21 + .../views/topic_footer_buttons_view.js | 24 + app/controllers/topics_controller.rb | 23 +- app/models/post.rb | 2 +- app/models/post_alert_observer.rb | 4 +- app/models/topic.rb | 41 +- app/models/topic_user.rb | 314 ++- app/models/user.rb | 32 +- app/serializers/category_topic_serializer.rb | 1 - app/serializers/topic_list_item_serializer.rb | 6 + app/serializers/topic_view_serializer.rb | 10 +- config/locales/client.en.yml | 4 + config/routes.rb | 1 + ...80148_add_cleared_pinned_to_topic_users.rb | 9 + db/structure.sql | 2049 ++++++++++++++++- lib/pinned_check.rb | 24 + lib/topic_query.rb | 97 +- lib/unread.rb | 2 +- spec/components/pinned_check_spec.rb | 58 + spec/components/topic_query_spec.rb | 16 +- spec/components/unread_spec.rb | 6 +- spec/controllers/topics_controller_spec.rb | 31 + spec/models/topic_spec.rb | 18 +- spec/models/topic_user_spec.rb | 199 +- 27 files changed, 2663 insertions(+), 342 deletions(-) create mode 100644 db/migrate/20130306180148_add_cleared_pinned_to_topic_users.rb create mode 100644 lib/pinned_check.rb create mode 100644 spec/components/pinned_check_spec.rb diff --git a/Gemfile b/Gemfile index 0c1ade108..fac8f5ac7 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,4 @@ -source 'https://rubygems.org' +source 'http://rubygems.org' gem 'active_model_serializers', git: 'git://github.com/rails-api/active_model_serializers.git' gem 'ember-rails', git: 'git://github.com/emberjs/ember-rails.git' # so we get the pre version diff --git a/Gemfile.lock b/Gemfile.lock index 3684482f2..15802713a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -71,7 +71,7 @@ PATH rails (~> 3.1) GEM - remote: https://rubygems.org/ + remote: http://rubygems.org/ specs: actionmailer (3.2.12) actionpack (= 3.2.12) diff --git a/app/assets/javascripts/discourse/controllers/topic_controller.js b/app/assets/javascripts/discourse/controllers/topic_controller.js index f1c503b0d..cd0e48f4b 100644 --- a/app/assets/javascripts/discourse/controllers/topic_controller.js +++ b/app/assets/javascripts/discourse/controllers/topic_controller.js @@ -255,6 +255,15 @@ Discourse.TopicController = Discourse.ObjectController.extend({ this.get('content').toggleStar(); }, + /** + Clears the pin from a topic for the currentUser + + @method clearPin + **/ + clearPin: function() { + this.get('content').clearPin(); + }, + // Receive notifications for this topic subscribe: function() { var bus, diff --git a/app/assets/javascripts/discourse/models/topic.js b/app/assets/javascripts/discourse/models/topic.js index 5dc912a8e..937ab5824 100644 --- a/app/assets/javascripts/discourse/models/topic.js +++ b/app/assets/javascripts/discourse/models/topic.js @@ -329,6 +329,27 @@ Discourse.Topic = Discourse.Model.extend({ }); }, + /** + Clears the pin from a topic for the currentUser + + @method clearPin + **/ + clearPin: function() { + + var topic = this; + + // Clear the pin optimistically from the object + topic.set('pinned', false); + + $.ajax("/t/" + this.get('id') + "/clear-pin", { + type: 'PUT', + error: function() { + // On error, put the pin back + topic.set('pinned', true); + } + }); + }, + // Is the reply to a post directly below it? isReplyDirectlyBelow: function(post) { var postBelow, posts; diff --git a/app/assets/javascripts/discourse/views/topic_footer_buttons_view.js b/app/assets/javascripts/discourse/views/topic_footer_buttons_view.js index c2fe4e777..a68ee7ca4 100644 --- a/app/assets/javascripts/discourse/views/topic_footer_buttons_view.js +++ b/app/assets/javascripts/discourse/views/topic_footer_buttons_view.js @@ -68,6 +68,30 @@ Discourse.TopicFooterButtonsView = Ember.ContainerView.extend({ buffer.push("<i class='icon icon-share'></i>"); } })); + + // Add our clear pin button + this.addObject(Discourse.ButtonView.createWithMixins({ + textKey: 'topic.clear_pin.title', + helpKey: 'topic.clear_pin.help', + classNameBindings: ['unpinned'], + + // Hide the button if it becomes unpinned + unpinned: function() { + // When not logged in don't show the button + if (!Discourse.get('currentUser')) return 'hidden' + + return this.get('controller.pinned') ? null : 'hidden'; + }.property('controller.pinned'), + + click: function(buffer) { + this.get('controller').clearPin(); + }, + + renderIcon: function(buffer) { + buffer.push("<i class='icon icon-pushpin'></i>"); + } + })); + } this.addObject(Discourse.ButtonView.createWithMixins({ diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 8381ed13d..2ddb7868d 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -14,7 +14,9 @@ class TopicsController < ApplicationController :mute, :unmute, :set_notifications, - :move_posts] + :move_posts, + :clear_pin] + before_filter :consider_user_for_promotion, only: :show skip_before_filter :check_xhr, only: [:avatar, :show, :feed] @@ -127,16 +129,21 @@ class TopicsController < ApplicationController end end + def clear_pin + topic = Topic.where(id: params[:topic_id].to_i).first + guardian.ensure_can_see!(topic) + topic.clear_pin_for(current_user) + render nothing: true + end + def timings - PostTiming.process_timings( - current_user, - params[:topic_id].to_i, - params[:highest_seen].to_i, - params[:topic_time].to_i, - (params[:timings] || []).map{|post_number, t| [post_number.to_i, t.to_i]} + current_user, + params[:topic_id].to_i, + params[:highest_seen].to_i, + params[:topic_time].to_i, + (params[:timings] || []).map{|post_number, t| [post_number.to_i, t.to_i]} ) - render nothing: true end diff --git a/app/models/post.rb b/app/models/post.rb index fc42d8d21..82cf60d74 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -54,7 +54,7 @@ class Post < ActiveRecord::Base after_commit :store_unique_post_key, on: :create after_create do - TopicUser.auto_track(user_id, topic_id, TopicUser::NotificationReasons::CREATED_POST) + TopicUser.auto_track(user_id, topic_id, TopicUser.notification_reasons[:created_post]) end scope :by_newest, order('created_at desc, id desc') diff --git a/app/models/post_alert_observer.rb b/app/models/post_alert_observer.rb index c421122d8..88c7bca90 100644 --- a/app/models/post_alert_observer.rb +++ b/app/models/post_alert_observer.rb @@ -80,7 +80,7 @@ class PostAlertObserver < ActiveRecord::Observer return unless Guardian.new(user).can_see?(post) # skip if muted on the topic - return if TopicUser.get(post.topic, user).try(:notification_level) == TopicUser::NotificationLevel::MUTED + return if TopicUser.get(post.topic, user).try(:notification_level) == TopicUser.notification_levels[:muted] # Don't notify the same user about the same notification on the same post return if user.notifications.exists?(notification_type: type, topic_id: post.topic_id, post_number: post.post_number) @@ -132,7 +132,7 @@ class PostAlertObserver < ActiveRecord::Observer exclude_user_ids << extract_mentioned_users(post).map(&:id) exclude_user_ids << extract_quoted_users(post).map(&:id) exclude_user_ids.flatten! - TopicUser.where(topic_id: post.topic_id, notification_level: TopicUser::NotificationLevel::WATCHING).includes(:user).each do |tu| + TopicUser.where(topic_id: post.topic_id, notification_level: TopicUser.notification_levels[:watching]).includes(:user).each do |tu| create_notification(tu.user, Notification.types[:posted], post) unless exclude_user_ids.include?(tu.user_id) end end diff --git a/app/models/topic.rb b/app/models/topic.rb index 9c9f85cc6..4b0f2ce42 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -83,11 +83,9 @@ class Topic < ActiveRecord::Base after_create do changed_to_category(category) - TopicUser.change( - user_id, id, - notification_level: TopicUser::NotificationLevel::WATCHING, - notifications_reason_id: TopicUser::NotificationReasons::CREATED_TOPIC - ) + TopicUser.change(user_id, id, + notification_level: TopicUser.notification_levels[:watching], + notifications_reason_id: TopicUser.notification_reasons[:created_topic]) if archetype == Archetype.private_message DraftSequence.next!(user, Draft::NEW_PRIVATE_MESSAGE) else @@ -206,8 +204,17 @@ class Topic < ActiveRecord::Base end def update_status(property, status, user) + Topic.transaction do - update_column(property, status) + + # Special case: if it's pinned, update that + if property.to_sym == :pinned + update_pinned(status) + else + # otherwise update the column + update_column(property, status) + end + key = "topic_statuses.#{property}_" key << (status ? 'enabled' : 'disabled') @@ -506,7 +513,7 @@ class Topic < ActiveRecord::Base # Enable/disable the mute on the topic def toggle_mute(user, muted) - TopicUser.change(user, self.id, notification_level: muted?(user) ? TopicUser::NotificationLevel::REGULAR : TopicUser::NotificationLevel::MUTED ) + TopicUser.change(user, self.id, notification_level: muted?(user) ? TopicUser.notification_levels[:regular] : TopicUser.notification_levels[:muted] ) end def slug @@ -526,7 +533,17 @@ class Topic < ActiveRecord::Base def muted?(user) return false unless user && user.id tu = topic_users.where(user_id: user.id).first - tu && tu.notification_level == TopicUser::NotificationLevel::MUTED + tu && tu.notification_level == TopicUser.notification_levels[:muted] + end + + def clear_pin_for(user) + return unless user.present? + + TopicUser.change(user.id, id, cleared_pinned_at: Time.now) + end + + def update_pinned(status) + update_column(:pinned_at, status ? Time.now : nil) end def draft_key @@ -535,18 +552,18 @@ class Topic < ActiveRecord::Base # notification stuff def notify_watch!(user) - TopicUser.change(user, id, notification_level: TopicUser::NotificationLevel::WATCHING) + TopicUser.change(user, id, notification_level: TopicUser.notification_levels[:watching]) end def notify_tracking!(user) - TopicUser.change(user, id, notification_level: TopicUser::NotificationLevel::TRACKING) + TopicUser.change(user, id, notification_level: TopicUser.notification_levels[:tracking]) end def notify_regular!(user) - TopicUser.change(user, id, notification_level: TopicUser::NotificationLevel::REGULAR) + TopicUser.change(user, id, notification_level: TopicUser.notification_levels[:regular]) end def notify_muted!(user) - TopicUser.change(user, id, notification_level: TopicUser::NotificationLevel::MUTED) + TopicUser.change(user, id, notification_level: TopicUser.notification_levels[:muted]) end end diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb index b92e7bec1..e3d29d0a8 100644 --- a/app/models/topic_user.rb +++ b/app/models/topic_user.rb @@ -2,185 +2,173 @@ class TopicUser < ActiveRecord::Base belongs_to :user belongs_to :topic - module NotificationLevel - WATCHING = 3 - TRACKING = 2 - REGULAR = 1 - MUTED = 0 - end + # Class methods + class << self - module NotificationReasons - CREATED_TOPIC = 1 - USER_CHANGED = 2 - USER_INTERACTED = 3 - CREATED_POST = 4 - end + # Enums + def notification_levels + @notification_levels ||= Enum.new(:muted, :regular, :tracking, :watching, start: 0) + end - def self.auto_track(user_id, topic_id, reason) - if TopicUser.where(user_id: user_id, topic_id: topic_id, notifications_reason_id: nil).exists? - change(user_id, topic_id, - notification_level: NotificationLevel::TRACKING, + def notification_reasons + @notification_reasons ||= Enum.new(:created_topic, :user_changed, :user_interacted, :created_post) + end + + def auto_track(user_id, topic_id, reason) + if TopicUser.where(user_id: user_id, topic_id: topic_id, notifications_reason_id: nil).exists? + change(user_id, topic_id, + notification_level: notification_levels[:tracking], notifications_reason_id: reason - ) + ) - MessageBus.publish("/topic/#{topic_id}", { - notification_level_change: NotificationLevel::TRACKING, - notifications_reason_id: reason - }, user_ids: [user_id]) - end - end - - - # Find the information specific to a user in a forum topic - def self.lookup_for(user, topics) - # If the user isn't logged in, there's no last read posts - return {} if user.blank? || topics.blank? - - topic_ids = topics.map(&:id) - create_lookup(TopicUser.where(topic_id: topic_ids, user_id: user.id)) - end - - def self.create_lookup(topic_users) - topic_users = topic_users.to_a - - result = {} - return result if topic_users.blank? - - topic_users.each do |ftu| - result[ftu.topic_id] = ftu - end - result - end - - def self.get(topic,user) - if Topic === topic - topic = topic.id - end - if User === user - user = user.id - end - - TopicUser.where('topic_id = ? and user_id = ?', topic, user).first - end - - # Change attributes for a user (creates a record when none is present). First it tries an update - # since there's more likely to be an existing record than not. If the update returns 0 rows affected - # it then creates the row instead. - def self.change(user_id, topic_id, attrs) - # Sometimes people pass objs instead of the ids. We can handle that. - topic_id = topic_id.id if topic_id.is_a?(Topic) - user_id = user_id.id if user_id.is_a?(User) - - TopicUser.transaction do - attrs = attrs.dup - attrs[:starred_at] = DateTime.now if attrs[:starred_at].nil? && attrs[:starred] - - if attrs[:notification_level] - attrs[:notifications_changed_at] ||= DateTime.now - attrs[:notifications_reason_id] ||= TopicUser::NotificationReasons::USER_CHANGED + MessageBus.publish("/topic/#{topic_id}", { + notification_level_change: notification_levels[:tracking], + notifications_reason_id: reason + }, user_ids: [user_id]) end - attrs_array = attrs.to_a + end - attrs_sql = attrs_array.map { |t| "#{t[0]} = ?" }.join(", ") - vals = attrs_array.map { |t| t[1] } - rows = TopicUser.update_all([attrs_sql, *vals], topic_id: topic_id.to_i, user_id: user_id) + # Find the information specific to a user in a forum topic + def lookup_for(user, topics) + # If the user isn't logged in, there's no last read posts + return {} if user.blank? || topics.blank? - if rows == 0 - now = DateTime.now - auto_track_after = self.exec_sql("select auto_track_topics_after_msecs from users where id = ?", user_id).values[0][0] - auto_track_after ||= SiteSetting.auto_track_topics_after - auto_track_after = auto_track_after.to_i + topic_ids = topics.map(&:id) + create_lookup(TopicUser.where(topic_id: topic_ids, user_id: user.id)) + end - if auto_track_after >= 0 && auto_track_after <= (attrs[:total_msecs_viewed] || 0) - attrs[:notification_level] ||= TopicUser::NotificationLevel::TRACKING + def create_lookup(topic_users) + topic_users = topic_users.to_a + + result = {} + return result if topic_users.blank? + + topic_users.each do |ftu| + result[ftu.topic_id] = ftu + end + result + end + + def get(topic,user) + topic = topic.id if Topic === topic + user = user.id if User === user + TopicUser.where('topic_id = ? and user_id = ?', topic, user).first + end + + # Change attributes for a user (creates a record when none is present). First it tries an update + # since there's more likely to be an existing record than not. If the update returns 0 rows affected + # it then creates the row instead. + def change(user_id, topic_id, attrs) + # Sometimes people pass objs instead of the ids. We can handle that. + topic_id = topic_id.id if topic_id.is_a?(Topic) + user_id = user_id.id if user_id.is_a?(User) + + TopicUser.transaction do + attrs = attrs.dup + attrs[:starred_at] = DateTime.now if attrs[:starred_at].nil? && attrs[:starred] + + if attrs[:notification_level] + attrs[:notifications_changed_at] ||= DateTime.now + attrs[:notifications_reason_id] ||= TopicUser.notification_reasons[:user_changed] end + attrs_array = attrs.to_a - TopicUser.create(attrs.merge!(user_id: user_id, topic_id: topic_id.to_i, first_visited_at: now ,last_visited_at: now)) + attrs_sql = attrs_array.map { |t| "#{t[0]} = ?" }.join(", ") + vals = attrs_array.map { |t| t[1] } + rows = TopicUser.update_all([attrs_sql, *vals], topic_id: topic_id.to_i, user_id: user_id) + + if rows == 0 + now = DateTime.now + auto_track_after = User.select(:auto_track_topics_after_msecs).where(id: user_id).first.auto_track_topics_after_msecs + auto_track_after ||= SiteSetting.auto_track_topics_after + + if auto_track_after >= 0 && auto_track_after <= (attrs[:total_msecs_viewed] || 0) + attrs[:notification_level] ||= notification_levels[:tracking] + end + + TopicUser.create(attrs.merge!(user_id: user_id, topic_id: topic_id.to_i, first_visited_at: now ,last_visited_at: now)) + end end + rescue ActiveRecord::RecordNotUnique + # In case of a race condition to insert, do nothing end - rescue ActiveRecord::RecordNotUnique - # In case of a race condition to insert, do nothing - end - def self.track_visit!(topic,user) - now = DateTime.now - rows = exec_sql_row_count( - "update topic_users set last_visited_at=? where topic_id=? and user_id=?", - now, topic.id, user.id - ) - - if rows == 0 - exec_sql('insert into topic_users(topic_id, user_id, last_visited_at, first_visited_at) - values(?,?,?,?)', - topic.id, user.id, now, now) - end - end - - # Update the last read and the last seen post count, but only if it doesn't exist. - # This would be a lot easier if psql supported some kind of upsert - def self.update_last_read(user, topic_id, post_number, msecs) - return if post_number.blank? - msecs = 0 if msecs.to_i < 0 - - args = { - user_id: user.id, - topic_id: topic_id, - post_number: post_number, - now: DateTime.now, - msecs: msecs, - tracking: TopicUser::NotificationLevel::TRACKING, - threshold: SiteSetting.auto_track_topics_after - } - - rows = exec_sql("UPDATE topic_users - SET - last_read_post_number = greatest(:post_number, tu.last_read_post_number), - seen_post_count = t.highest_post_number, - total_msecs_viewed = tu.total_msecs_viewed + :msecs, - notification_level = - case when tu.notifications_reason_id is null and (tu.total_msecs_viewed + :msecs) > - coalesce(u.auto_track_topics_after_msecs,:threshold) and - coalesce(u.auto_track_topics_after_msecs, :threshold) >= 0 then - :tracking - else - tu.notification_level - end - FROM topic_users tu - join topics t on t.id = tu.topic_id - join users u on u.id = :user_id - WHERE - tu.topic_id = topic_users.topic_id AND - tu.user_id = topic_users.user_id AND - tu.topic_id = :topic_id AND - tu.user_id = :user_id - RETURNING - topic_users.notification_level, tu.notification_level old_level - ", - args).values - - if rows.length == 1 - before = rows[0][1].to_i - after = rows[0][0].to_i - - if before != after - MessageBus.publish("/topic/#{topic_id}", {notification_level_change: after}, user_ids: [user.id]) + def track_visit!(topic,user) + now = DateTime.now + rows = TopicUser.update_all({last_visited_at: now}, {topic_id: topic.id, user_id: user.id}) + if rows == 0 + TopicUser.create(topic_id: topic.id, user_id: user.id, last_visited_at: now, first_visited_at: now) end end - if rows.length == 0 - args[:tracking] = TopicUser::NotificationLevel::TRACKING - args[:regular] = TopicUser::NotificationLevel::REGULAR - args[:site_setting] = SiteSetting.auto_track_topics_after - exec_sql("INSERT INTO topic_users (user_id, topic_id, last_read_post_number, seen_post_count, last_visited_at, first_visited_at, notification_level) - SELECT :user_id, :topic_id, :post_number, ft.highest_post_number, :now, :now, - case when coalesce(u.auto_track_topics_after_msecs, :site_setting) = 0 then :tracking else :regular end - FROM topics AS ft - JOIN users u on u.id = :user_id - WHERE ft.id = :topic_id - AND NOT EXISTS(SELECT 1 - FROM topic_users AS ftu - WHERE ftu.user_id = :user_id and ftu.topic_id = :topic_id)", - args) + # Update the last read and the last seen post count, but only if it doesn't exist. + # This would be a lot easier if psql supported some kind of upsert + def update_last_read(user, topic_id, post_number, msecs) + return if post_number.blank? + msecs = 0 if msecs.to_i < 0 + + args = { + user_id: user.id, + topic_id: topic_id, + post_number: post_number, + now: DateTime.now, + msecs: msecs, + tracking: notification_levels[:tracking], + threshold: SiteSetting.auto_track_topics_after + } + + rows = exec_sql("UPDATE topic_users + SET + last_read_post_number = greatest(:post_number, tu.last_read_post_number), + seen_post_count = t.highest_post_number, + total_msecs_viewed = tu.total_msecs_viewed + :msecs, + notification_level = + case when tu.notifications_reason_id is null and (tu.total_msecs_viewed + :msecs) > + coalesce(u.auto_track_topics_after_msecs,:threshold) and + coalesce(u.auto_track_topics_after_msecs, :threshold) >= 0 then + :tracking + else + tu.notification_level + end + FROM topic_users tu + join topics t on t.id = tu.topic_id + join users u on u.id = :user_id + WHERE + tu.topic_id = topic_users.topic_id AND + tu.user_id = topic_users.user_id AND + tu.topic_id = :topic_id AND + tu.user_id = :user_id + RETURNING + topic_users.notification_level, tu.notification_level old_level + ", + args).values + + if rows.length == 1 + before = rows[0][1].to_i + after = rows[0][0].to_i + + if before != after + MessageBus.publish("/topic/#{topic_id}", {notification_level_change: after}, user_ids: [user.id]) + end + end + + if rows.length == 0 + args[:tracking] = notification_levels[:tracking] + args[:regular] = notification_levels[:regular] + args[:site_setting] = SiteSetting.auto_track_topics_after + exec_sql("INSERT INTO topic_users (user_id, topic_id, last_read_post_number, seen_post_count, last_visited_at, first_visited_at, notification_level) + SELECT :user_id, :topic_id, :post_number, ft.highest_post_number, :now, :now, + case when coalesce(u.auto_track_topics_after_msecs, :site_setting) = 0 then :tracking else :regular end + FROM topics AS ft + JOIN users u on u.id = :user_id + WHERE ft.id = :topic_id + AND NOT EXISTS(SELECT 1 + FROM topic_users AS ftu + WHERE ftu.user_id = :user_id and ftu.topic_id = :topic_id)", + args) + end end + end + end diff --git a/app/models/user.rb b/app/models/user.rb index e14b0916b..33b703a1c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -443,11 +443,8 @@ class User < ActiveRecord::Base end def readable_name - if name.present? && name != username - "#{name} (#{username})" - else - username - end + return "#{name} (#{username})" if name.present? && name != username + username end protected @@ -461,25 +458,14 @@ class User < ActiveRecord::Base end def update_tracked_topics - if auto_track_topics_after_msecs_changed? + return unless auto_track_topics_after_msecs_changed? - if auto_track_topics_after_msecs < 0 - - User.exec_sql('update topic_users set notification_level = ? - where notifications_reason_id is null and - user_id = ?' , TopicUser::NotificationLevel::REGULAR , id) - else - - User.exec_sql('update topic_users set notification_level = ? - where notifications_reason_id is null and - user_id = ? and - total_msecs_viewed < ?' , TopicUser::NotificationLevel::REGULAR , id, auto_track_topics_after_msecs) - - User.exec_sql('update topic_users set notification_level = ? - where notifications_reason_id is null and - user_id = ? and - total_msecs_viewed >= ?' , TopicUser::NotificationLevel::TRACKING , id, auto_track_topics_after_msecs) - end + where_conditions = {notifications_reason_id: nil, user_id: id} + if auto_track_topics_after_msecs < 0 + TopicUser.update_all({notification_level: TopicUser.notification_levels[:regular]}, where_conditions) + else + TopicUser.update_all(["notification_level = CASE WHEN total_msecs_viewed < ? THEN ? ELSE ? END", + auto_track_topics_after_msecs, TopicUser.notification_levels[:regular], TopicUser.notification_levels[:tracking]], where_conditions) end end diff --git a/app/serializers/category_topic_serializer.rb b/app/serializers/category_topic_serializer.rb index d17fc548a..0df31095c 100644 --- a/app/serializers/category_topic_serializer.rb +++ b/app/serializers/category_topic_serializer.rb @@ -2,7 +2,6 @@ class CategoryTopicSerializer < BasicTopicSerializer attributes :slug, :visible, - :pinned, :closed, :archived diff --git a/app/serializers/topic_list_item_serializer.rb b/app/serializers/topic_list_item_serializer.rb index 49ea8ebaf..e5466abf0 100644 --- a/app/serializers/topic_list_item_serializer.rb +++ b/app/serializers/topic_list_item_serializer.rb @@ -1,3 +1,5 @@ +require_dependency 'pinned_check' + class TopicListItemSerializer < BasicTopicSerializer attributes :views, @@ -29,4 +31,8 @@ class TopicListItemSerializer < BasicTopicSerializer object.posters || [] end + def pinned + PinnedCheck.new(object, object.user_data).pinned? + end + end diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index 337dd11ff..9e7fa4078 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -1,3 +1,5 @@ +require_dependency 'pinned_check' + class TopicViewSerializer < ApplicationSerializer # These attributes will be delegated to the topic @@ -12,7 +14,6 @@ class TopicViewSerializer < ApplicationSerializer :last_posted_at, :visible, :closed, - :pinned, :archived, :moderator_posts_count, :has_best_of, @@ -42,7 +43,8 @@ class TopicViewSerializer < ApplicationSerializer :notifications_reason_id, :posts, :at_bottom, - :highest_post_number + :highest_post_number, + :pinned has_one :created_by, serializer: BasicUserSerializer, embed: :objects has_one :last_poster, serializer: BasicUserSerializer, embed: :objects @@ -193,6 +195,10 @@ class TopicViewSerializer < ApplicationSerializer object.highest_post_number end + def pinned + PinnedCheck.new(object.topic, object.topic_user).pinned? + end + def posts return @posts if @posts.present? @posts = [] diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index ff27b03c3..102d0970a 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -452,6 +452,10 @@ en: title: 'Reply' help: 'begin composing a reply to this topic' + clear_pin: + title: "Clear pin" + help: "Clear the pinned status of this topic so it no longer appears at the top of your topic list" + share: title: 'Share' help: 'share a link to this topic' diff --git a/config/routes.rb b/config/routes.rb index 3693bf644..eb6271a44 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -175,6 +175,7 @@ Discourse::Application.routes.draw do put 't/:topic_id/star' => 'topics#star', :constraints => {:topic_id => /\d+/} put 't/:slug/:topic_id/status' => 'topics#status', :constraints => {:topic_id => /\d+/} put 't/:topic_id/status' => 'topics#status', :constraints => {:topic_id => /\d+/} + put 't/:topic_id/clear-pin' => 'topics#clear_pin', :constraints => {:topic_id => /\d+/} put 't/:topic_id/mute' => 'topics#mute', :constraints => {:topic_id => /\d+/} put 't/:topic_id/unmute' => 'topics#unmute', :constraints => {:topic_id => /\d+/} diff --git a/db/migrate/20130306180148_add_cleared_pinned_to_topic_users.rb b/db/migrate/20130306180148_add_cleared_pinned_to_topic_users.rb new file mode 100644 index 000000000..9cc4af5d9 --- /dev/null +++ b/db/migrate/20130306180148_add_cleared_pinned_to_topic_users.rb @@ -0,0 +1,9 @@ +class AddClearedPinnedToTopicUsers < ActiveRecord::Migration + def change + add_column :topic_users, :cleared_pinned_at, :datetime, null: true + + add_column :topics, :pinned_at, :datetime, null: true + execute "UPDATE topics SET pinned_at = created_at WHERE pinned" + remove_column :topics, :pinned + end +end diff --git a/db/structure.sql b/db/structure.sql index fa70f2c10..09a4e77d2 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -8,6 +8,13 @@ SET standard_conforming_strings = on; SET check_function_bodies = false; SET client_min_messages = warning; +-- +-- Name: backup; Type: SCHEMA; Schema: -; Owner: - +-- + +CREATE SCHEMA backup; + + -- -- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: - -- @@ -36,12 +43,1176 @@ CREATE EXTENSION IF NOT EXISTS hstore WITH SCHEMA public; COMMENT ON EXTENSION hstore IS 'data type for storing sets of (key, value) pairs'; -SET search_path = public, pg_catalog; +SET search_path = backup, pg_catalog; SET default_tablespace = ''; SET default_with_oids = false; +-- +-- Name: categories; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE categories ( + id integer NOT NULL, + name character varying(50) NOT NULL, + color character varying(6) DEFAULT 'AB9364'::character varying NOT NULL, + topic_id integer, + top1_topic_id integer, + top2_topic_id integer, + top1_user_id integer, + top2_user_id integer, + topic_count integer DEFAULT 0 NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + user_id integer NOT NULL, + topics_year integer, + topics_month integer, + topics_week integer, + slug character varying(255) NOT NULL +); + + +-- +-- Name: categories_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE categories_id_seq + START WITH 5 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: categories_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE categories_id_seq OWNED BY categories.id; + + +-- +-- Name: category_featured_topics; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE category_featured_topics ( + category_id integer NOT NULL, + topic_id integer NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: category_featured_users; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE category_featured_users ( + id integer NOT NULL, + category_id integer, + user_id integer, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: category_featured_users_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE category_featured_users_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: category_featured_users_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE category_featured_users_id_seq OWNED BY category_featured_users.id; + + +-- +-- Name: draft_sequences; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE draft_sequences ( + id integer NOT NULL, + user_id integer NOT NULL, + draft_key character varying(255) NOT NULL, + sequence integer NOT NULL +); + + +-- +-- Name: draft_sequences_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE draft_sequences_id_seq + START WITH 20 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: draft_sequences_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE draft_sequences_id_seq OWNED BY draft_sequences.id; + + +-- +-- Name: drafts; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE drafts ( + id integer NOT NULL, + user_id integer NOT NULL, + draft_key character varying(255) NOT NULL, + data text NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + sequence integer DEFAULT 0 NOT NULL +); + + +-- +-- Name: drafts_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE drafts_id_seq + START WITH 2 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: drafts_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE drafts_id_seq OWNED BY drafts.id; + + +-- +-- Name: email_logs; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE email_logs ( + id integer NOT NULL, + to_address character varying(255) NOT NULL, + email_type character varying(255) NOT NULL, + user_id integer, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: email_logs_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE email_logs_id_seq + START WITH 3 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: email_logs_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE email_logs_id_seq OWNED BY email_logs.id; + + +-- +-- Name: email_tokens; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE email_tokens ( + id integer NOT NULL, + user_id integer NOT NULL, + email character varying(255) NOT NULL, + token character varying(255) NOT NULL, + confirmed boolean DEFAULT false NOT NULL, + expired boolean DEFAULT false NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: email_tokens_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE email_tokens_id_seq + START WITH 3 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: email_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE email_tokens_id_seq OWNED BY email_tokens.id; + + +-- +-- Name: facebook_user_infos; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE facebook_user_infos ( + id integer NOT NULL, + user_id integer NOT NULL, + facebook_user_id integer NOT NULL, + username character varying(255) NOT NULL, + first_name character varying(255), + last_name character varying(255), + email character varying(255), + gender character varying(255), + name character varying(255), + link character varying(255), + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: facebook_user_infos_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE facebook_user_infos_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: facebook_user_infos_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE facebook_user_infos_id_seq OWNED BY facebook_user_infos.id; + + +-- +-- Name: incoming_links; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE incoming_links ( + id integer NOT NULL, + url character varying(1000) NOT NULL, + referer character varying(1000) NOT NULL, + domain character varying(100) NOT NULL, + topic_id integer, + post_number integer, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: incoming_links_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE incoming_links_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: incoming_links_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE incoming_links_id_seq OWNED BY incoming_links.id; + + +-- +-- Name: invites; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE invites ( + id integer NOT NULL, + invite_key character varying(32) NOT NULL, + email character varying(255) NOT NULL, + invited_by_id integer NOT NULL, + user_id integer, + redeemed_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + deleted_at timestamp without time zone +); + + +-- +-- Name: invites_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE invites_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: invites_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE invites_id_seq OWNED BY invites.id; + + +-- +-- Name: notifications; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE notifications ( + id integer NOT NULL, + notification_type integer NOT NULL, + user_id integer NOT NULL, + data character varying(255) NOT NULL, + read boolean DEFAULT false NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + topic_id integer, + post_number integer, + post_action_id integer +); + + +-- +-- Name: notifications_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE notifications_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: notifications_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE notifications_id_seq OWNED BY notifications.id; + + +-- +-- Name: onebox_renders; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE onebox_renders ( + id integer NOT NULL, + url character varying(255) NOT NULL, + cooked text NOT NULL, + expires_at timestamp without time zone NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + preview text +); + + +-- +-- Name: onebox_renders_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE onebox_renders_id_seq + START WITH 2 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: onebox_renders_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE onebox_renders_id_seq OWNED BY onebox_renders.id; + + +-- +-- Name: post_action_types; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE post_action_types ( + name_key character varying(50) NOT NULL, + is_flag boolean DEFAULT false NOT NULL, + icon character varying(20), + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + id integer NOT NULL +); + + +-- +-- Name: post_action_types_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE post_action_types_id_seq + START WITH 6 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: post_action_types_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE post_action_types_id_seq OWNED BY post_action_types.id; + + +-- +-- Name: post_actions; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE post_actions ( + id integer NOT NULL, + post_id integer NOT NULL, + user_id integer NOT NULL, + post_action_type_id integer NOT NULL, + deleted_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: post_actions_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE post_actions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: post_actions_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE post_actions_id_seq OWNED BY post_actions.id; + + +-- +-- Name: post_onebox_renders; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE post_onebox_renders ( + post_id integer NOT NULL, + onebox_render_id integer NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: post_replies; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE post_replies ( + post_id integer, + reply_id integer, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: post_timings; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE post_timings ( + topic_id integer NOT NULL, + post_number integer NOT NULL, + user_id integer NOT NULL, + msecs integer NOT NULL +); + + +-- +-- Name: posts; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE posts ( + id integer NOT NULL, + user_id integer NOT NULL, + topic_id integer NOT NULL, + post_number integer NOT NULL, + raw text NOT NULL, + cooked text NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + reply_to_post_number integer, + cached_version integer DEFAULT 1 NOT NULL, + reply_count integer DEFAULT 0 NOT NULL, + quote_count integer DEFAULT 0 NOT NULL, + reply_below_post_number integer, + deleted_at timestamp without time zone, + off_topic_count integer DEFAULT 0 NOT NULL, + offensive_count integer DEFAULT 0 NOT NULL, + like_count integer DEFAULT 0 NOT NULL, + incoming_link_count integer DEFAULT 0 NOT NULL, + bookmark_count integer DEFAULT 0 NOT NULL, + avg_time integer, + score double precision, + reads integer DEFAULT 0 NOT NULL, + post_type integer DEFAULT 1 NOT NULL, + vote_count integer DEFAULT 0 NOT NULL, + sort_order integer, + last_editor_id integer +); + + +-- +-- Name: posts_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE posts_id_seq + START WITH 16 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: posts_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE posts_id_seq OWNED BY posts.id; + + +-- +-- Name: site_customizations; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE site_customizations ( + id integer NOT NULL, + name character varying(255) NOT NULL, + stylesheet text, + header text, + "position" integer NOT NULL, + user_id integer NOT NULL, + enabled boolean NOT NULL, + key character varying(255) NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + override_default_style boolean DEFAULT false NOT NULL, + stylesheet_baked text DEFAULT ''::text NOT NULL +); + + +-- +-- Name: site_customizations_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE site_customizations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: site_customizations_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE site_customizations_id_seq OWNED BY site_customizations.id; + + +-- +-- Name: site_settings; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE site_settings ( + id integer NOT NULL, + name character varying(255) NOT NULL, + data_type integer NOT NULL, + value text, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: site_settings_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE site_settings_id_seq + START WITH 4 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: site_settings_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE site_settings_id_seq OWNED BY site_settings.id; + + +-- +-- Name: topic_allowed_users; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE topic_allowed_users ( + id integer NOT NULL, + user_id integer NOT NULL, + topic_id integer NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: topic_allowed_users_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE topic_allowed_users_id_seq + START WITH 3 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: topic_allowed_users_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE topic_allowed_users_id_seq OWNED BY topic_allowed_users.id; + + +-- +-- Name: topic_invites; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE topic_invites ( + id integer NOT NULL, + topic_id integer NOT NULL, + invite_id integer NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: topic_invites_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE topic_invites_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: topic_invites_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE topic_invites_id_seq OWNED BY topic_invites.id; + + +-- +-- Name: topic_link_clicks; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE topic_link_clicks ( + id integer NOT NULL, + topic_link_id integer NOT NULL, + user_id integer, + ip bigint NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: topic_link_clicks_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE topic_link_clicks_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: topic_link_clicks_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE topic_link_clicks_id_seq OWNED BY topic_link_clicks.id; + + +-- +-- Name: topic_links; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE topic_links ( + id integer NOT NULL, + topic_id integer NOT NULL, + post_id integer, + user_id integer NOT NULL, + url character varying(500) NOT NULL, + domain character varying(100) NOT NULL, + internal boolean DEFAULT false NOT NULL, + link_topic_id integer, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + reflection boolean DEFAULT false, + clicks integer DEFAULT 0 NOT NULL, + link_post_id integer +); + + +-- +-- Name: topic_links_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE topic_links_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: topic_links_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE topic_links_id_seq OWNED BY topic_links.id; + + +-- +-- Name: topic_users; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE topic_users ( + user_id integer NOT NULL, + topic_id integer NOT NULL, + starred boolean DEFAULT false NOT NULL, + posted boolean DEFAULT false NOT NULL, + last_read_post_number integer, + seen_post_count integer, + starred_at timestamp without time zone, + muted_at timestamp without time zone, + last_visited_at timestamp without time zone, + first_visited_at timestamp without time zone, + notifications integer DEFAULT 2, + notifications_changed_at timestamp without time zone, + notifications_reason_id integer, + CONSTRAINT test_starred_at CHECK (((starred = false) OR (starred_at IS NOT NULL))) +); + + +-- +-- Name: topics; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE topics ( + id integer NOT NULL, + title character varying(255) NOT NULL, + last_posted_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + views integer DEFAULT 0 NOT NULL, + posts_count integer DEFAULT 0 NOT NULL, + user_id integer NOT NULL, + last_post_user_id integer NOT NULL, + reply_count integer DEFAULT 0 NOT NULL, + featured_user1_id integer, + featured_user2_id integer, + featured_user3_id integer, + avg_time integer, + deleted_at timestamp without time zone, + highest_post_number integer DEFAULT 0 NOT NULL, + image_url character varying(255), + off_topic_count integer DEFAULT 0 NOT NULL, + offensive_count integer DEFAULT 0 NOT NULL, + like_count integer DEFAULT 0 NOT NULL, + incoming_link_count integer DEFAULT 0 NOT NULL, + bookmark_count integer DEFAULT 0 NOT NULL, + star_count integer DEFAULT 0 NOT NULL, + category_id integer, + visible boolean DEFAULT true NOT NULL, + moderator_posts_count integer DEFAULT 0 NOT NULL, + closed boolean DEFAULT false NOT NULL, + pinned boolean DEFAULT false NOT NULL, + archived boolean DEFAULT false NOT NULL, + bumped_at timestamp without time zone NOT NULL, + sub_tag character varying(255), + has_best_of boolean DEFAULT false NOT NULL, + meta_data public.hstore, + vote_count integer DEFAULT 0 NOT NULL, + archetype character varying(255) DEFAULT 'regular'::character varying NOT NULL, + featured_user4_id integer +); + + +-- +-- Name: topics_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE topics_id_seq + START WITH 16 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: topics_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE topics_id_seq OWNED BY topics.id; + + +-- +-- Name: trust_levels; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE trust_levels ( + id integer NOT NULL, + name_key character varying(255) NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: trust_levels_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE trust_levels_id_seq + START WITH 3 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: trust_levels_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE trust_levels_id_seq OWNED BY trust_levels.id; + + +-- +-- Name: twitter_user_infos; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE twitter_user_infos ( + id integer NOT NULL, + user_id integer NOT NULL, + screen_name character varying(255) NOT NULL, + twitter_user_id integer NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: twitter_user_infos_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE twitter_user_infos_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: twitter_user_infos_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE twitter_user_infos_id_seq OWNED BY twitter_user_infos.id; + + +-- +-- Name: uploads; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE uploads ( + id integer NOT NULL, + user_id integer NOT NULL, + topic_id integer NOT NULL, + original_filename character varying(255) NOT NULL, + filesize integer NOT NULL, + width integer, + height integer, + url character varying(255) NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: uploads_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE uploads_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: uploads_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE uploads_id_seq OWNED BY uploads.id; + + +-- +-- Name: user_actions; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE user_actions ( + id integer NOT NULL, + action_type integer NOT NULL, + user_id integer NOT NULL, + target_topic_id integer, + target_post_id integer, + target_user_id integer, + acting_user_id integer, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: user_actions_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE user_actions_id_seq + START WITH 40 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: user_actions_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE user_actions_id_seq OWNED BY user_actions.id; + + +-- +-- Name: user_open_ids; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE user_open_ids ( + id integer NOT NULL, + user_id integer NOT NULL, + email character varying(255) NOT NULL, + url character varying(255) NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + active boolean NOT NULL +); + + +-- +-- Name: user_open_ids_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE user_open_ids_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: user_open_ids_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE user_open_ids_id_seq OWNED BY user_open_ids.id; + + +-- +-- Name: user_visits; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE user_visits ( + id integer NOT NULL, + user_id integer NOT NULL, + visited_at date NOT NULL +); + + +-- +-- Name: user_visits_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE user_visits_id_seq + START WITH 4 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: user_visits_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE user_visits_id_seq OWNED BY user_visits.id; + + +-- +-- Name: users; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE users ( + id integer NOT NULL, + username character varying(20) NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + name character varying(255), + bio_raw text, + seen_notification_id integer DEFAULT 0 NOT NULL, + last_posted_at timestamp without time zone, + email character varying(256) NOT NULL, + password_hash character varying(64), + salt character varying(32), + active boolean, + username_lower character varying(20) NOT NULL, + auth_token character varying(32), + last_seen_at timestamp without time zone, + website character varying(255), + admin boolean DEFAULT false NOT NULL, + moderator boolean DEFAULT false NOT NULL, + last_emailed_at timestamp without time zone, + email_digests boolean DEFAULT true NOT NULL, + trust_level_id integer DEFAULT 1 NOT NULL, + bio_cooked text, + email_private_messages boolean DEFAULT true, + email_direct boolean DEFAULT true NOT NULL, + approved boolean DEFAULT false NOT NULL, + approved_by_id integer, + approved_at timestamp without time zone, + topics_entered integer DEFAULT 0 NOT NULL, + posts_read_count integer DEFAULT 0 NOT NULL, + digest_after_days integer DEFAULT 7 NOT NULL, + previous_visit_at timestamp without time zone +); + + +-- +-- Name: users_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE users_id_seq + START WITH 3 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE users_id_seq OWNED BY users.id; + + +-- +-- Name: versions; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE versions ( + id integer NOT NULL, + versioned_id integer, + versioned_type character varying(255), + user_id integer, + user_type character varying(255), + user_name character varying(255), + modifications text, + number integer, + reverted_from integer, + tag character varying(255), + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: versions_id_seq; Type: SEQUENCE; Schema: backup; Owner: - +-- + +CREATE SEQUENCE versions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: versions_id_seq; Type: SEQUENCE OWNED BY; Schema: backup; Owner: - +-- + +ALTER SEQUENCE versions_id_seq OWNED BY versions.id; + + +-- +-- Name: views; Type: TABLE; Schema: backup; Owner: -; Tablespace: +-- + +CREATE TABLE views ( + parent_id integer NOT NULL, + parent_type character varying(50) NOT NULL, + ip bigint NOT NULL, + viewed_at timestamp without time zone NOT NULL, + user_id integer +); + + +SET search_path = public, pg_catalog; + -- -- Name: categories; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -926,6 +2097,7 @@ CREATE TABLE topic_users ( notifications_changed_at timestamp without time zone, notifications_reason_id integer, total_msecs_viewed integer DEFAULT 0 NOT NULL, + cleared_pinned_at timestamp without time zone, CONSTRAINT test_starred_at CHECK (((starred = false) OR (starred_at IS NOT NULL))) ); @@ -961,7 +2133,6 @@ CREATE TABLE topics ( visible boolean DEFAULT true NOT NULL, moderator_posts_count integer DEFAULT 0 NOT NULL, closed boolean DEFAULT false NOT NULL, - pinned boolean DEFAULT false NOT NULL, archived boolean DEFAULT false NOT NULL, bumped_at timestamp without time zone NOT NULL, has_best_of boolean DEFAULT false NOT NULL, @@ -972,7 +2143,8 @@ CREATE TABLE topics ( custom_flag_count integer DEFAULT 0 NOT NULL, spam_count integer DEFAULT 0 NOT NULL, illegal_count integer DEFAULT 0 NOT NULL, - inappropriate_count integer DEFAULT 0 NOT NULL + inappropriate_count integer DEFAULT 0 NOT NULL, + pinned_at timestamp without time zone ); @@ -1294,6 +2466,213 @@ CREATE TABLE views ( ); +SET search_path = backup, pg_catalog; + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY categories ALTER COLUMN id SET DEFAULT nextval('categories_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY category_featured_users ALTER COLUMN id SET DEFAULT nextval('category_featured_users_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY draft_sequences ALTER COLUMN id SET DEFAULT nextval('draft_sequences_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY drafts ALTER COLUMN id SET DEFAULT nextval('drafts_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY email_logs ALTER COLUMN id SET DEFAULT nextval('email_logs_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY email_tokens ALTER COLUMN id SET DEFAULT nextval('email_tokens_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY facebook_user_infos ALTER COLUMN id SET DEFAULT nextval('facebook_user_infos_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY incoming_links ALTER COLUMN id SET DEFAULT nextval('incoming_links_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY invites ALTER COLUMN id SET DEFAULT nextval('invites_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY notifications ALTER COLUMN id SET DEFAULT nextval('notifications_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY onebox_renders ALTER COLUMN id SET DEFAULT nextval('onebox_renders_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY post_action_types ALTER COLUMN id SET DEFAULT nextval('post_action_types_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY post_actions ALTER COLUMN id SET DEFAULT nextval('post_actions_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY posts ALTER COLUMN id SET DEFAULT nextval('posts_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY site_customizations ALTER COLUMN id SET DEFAULT nextval('site_customizations_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY site_settings ALTER COLUMN id SET DEFAULT nextval('site_settings_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY topic_allowed_users ALTER COLUMN id SET DEFAULT nextval('topic_allowed_users_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY topic_invites ALTER COLUMN id SET DEFAULT nextval('topic_invites_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY topic_link_clicks ALTER COLUMN id SET DEFAULT nextval('topic_link_clicks_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY topic_links ALTER COLUMN id SET DEFAULT nextval('topic_links_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY topics ALTER COLUMN id SET DEFAULT nextval('topics_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY trust_levels ALTER COLUMN id SET DEFAULT nextval('trust_levels_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY twitter_user_infos ALTER COLUMN id SET DEFAULT nextval('twitter_user_infos_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY uploads ALTER COLUMN id SET DEFAULT nextval('uploads_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY user_actions ALTER COLUMN id SET DEFAULT nextval('user_actions_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY user_open_ids ALTER COLUMN id SET DEFAULT nextval('user_open_ids_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY user_visits ALTER COLUMN id SET DEFAULT nextval('user_visits_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY users ALTER COLUMN id SET DEFAULT nextval('users_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: backup; Owner: - +-- + +ALTER TABLE ONLY versions ALTER COLUMN id SET DEFAULT nextval('versions_id_seq'::regclass); + + +SET search_path = public, pg_catalog; + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1504,6 +2883,242 @@ ALTER TABLE ONLY users ALTER COLUMN id SET DEFAULT nextval('users_id_seq'::regcl ALTER TABLE ONLY versions ALTER COLUMN id SET DEFAULT nextval('versions_id_seq'::regclass); +SET search_path = backup, pg_catalog; + +-- +-- Name: actions_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY user_actions + ADD CONSTRAINT actions_pkey PRIMARY KEY (id); + + +-- +-- Name: categories_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY categories + ADD CONSTRAINT categories_pkey PRIMARY KEY (id); + + +-- +-- Name: category_featured_users_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY category_featured_users + ADD CONSTRAINT category_featured_users_pkey PRIMARY KEY (id); + + +-- +-- Name: draft_sequences_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY draft_sequences + ADD CONSTRAINT draft_sequences_pkey PRIMARY KEY (id); + + +-- +-- Name: drafts_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY drafts + ADD CONSTRAINT drafts_pkey PRIMARY KEY (id); + + +-- +-- Name: email_logs_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY email_logs + ADD CONSTRAINT email_logs_pkey PRIMARY KEY (id); + + +-- +-- Name: email_tokens_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY email_tokens + ADD CONSTRAINT email_tokens_pkey PRIMARY KEY (id); + + +-- +-- Name: facebook_user_infos_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY facebook_user_infos + ADD CONSTRAINT facebook_user_infos_pkey PRIMARY KEY (id); + + +-- +-- Name: forum_thread_link_clicks_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY topic_link_clicks + ADD CONSTRAINT forum_thread_link_clicks_pkey PRIMARY KEY (id); + + +-- +-- Name: forum_thread_links_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY topic_links + ADD CONSTRAINT forum_thread_links_pkey PRIMARY KEY (id); + + +-- +-- Name: forum_threads_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY topics + ADD CONSTRAINT forum_threads_pkey PRIMARY KEY (id); + + +-- +-- Name: incoming_links_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY incoming_links + ADD CONSTRAINT incoming_links_pkey PRIMARY KEY (id); + + +-- +-- Name: invites_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY invites + ADD CONSTRAINT invites_pkey PRIMARY KEY (id); + + +-- +-- Name: notifications_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY notifications + ADD CONSTRAINT notifications_pkey PRIMARY KEY (id); + + +-- +-- Name: onebox_renders_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY onebox_renders + ADD CONSTRAINT onebox_renders_pkey PRIMARY KEY (id); + + +-- +-- Name: post_action_types_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY post_action_types + ADD CONSTRAINT post_action_types_pkey PRIMARY KEY (id); + + +-- +-- Name: post_actions_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY post_actions + ADD CONSTRAINT post_actions_pkey PRIMARY KEY (id); + + +-- +-- Name: posts_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY posts + ADD CONSTRAINT posts_pkey PRIMARY KEY (id); + + +-- +-- Name: site_customizations_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY site_customizations + ADD CONSTRAINT site_customizations_pkey PRIMARY KEY (id); + + +-- +-- Name: site_settings_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY site_settings + ADD CONSTRAINT site_settings_pkey PRIMARY KEY (id); + + +-- +-- Name: topic_allowed_users_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY topic_allowed_users + ADD CONSTRAINT topic_allowed_users_pkey PRIMARY KEY (id); + + +-- +-- Name: topic_invites_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY topic_invites + ADD CONSTRAINT topic_invites_pkey PRIMARY KEY (id); + + +-- +-- Name: trust_levels_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY trust_levels + ADD CONSTRAINT trust_levels_pkey PRIMARY KEY (id); + + +-- +-- Name: twitter_user_infos_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY twitter_user_infos + ADD CONSTRAINT twitter_user_infos_pkey PRIMARY KEY (id); + + +-- +-- Name: uploads_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY uploads + ADD CONSTRAINT uploads_pkey PRIMARY KEY (id); + + +-- +-- Name: user_open_ids_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY user_open_ids + ADD CONSTRAINT user_open_ids_pkey PRIMARY KEY (id); + + +-- +-- Name: user_visits_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY user_visits + ADD CONSTRAINT user_visits_pkey PRIMARY KEY (id); + + +-- +-- Name: users_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + + +-- +-- Name: versions_pkey; Type: CONSTRAINT; Schema: backup; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY versions + ADD CONSTRAINT versions_pkey PRIMARY KEY (id); + + +SET search_path = public, pg_catalog; + -- -- Name: actions_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -1768,6 +3383,430 @@ ALTER TABLE ONLY versions ADD CONSTRAINT versions_pkey PRIMARY KEY (id); +SET search_path = backup, pg_catalog; + +-- +-- Name: cat_featured_threads; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX cat_featured_threads ON category_featured_topics USING btree (category_id, topic_id); + + +-- +-- Name: idx_search_thread; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX idx_search_thread ON topics USING gin (to_tsvector('english'::regconfig, (title)::text)); + + +-- +-- Name: idx_search_user; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX idx_search_user ON users USING gin (to_tsvector('english'::regconfig, (username)::text)); + + +-- +-- Name: idx_unique_actions; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX idx_unique_actions ON post_actions USING btree (user_id, post_action_type_id, post_id) WHERE (deleted_at IS NULL); + + +-- +-- Name: idx_unique_rows; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX idx_unique_rows ON user_actions USING btree (action_type, user_id, target_topic_id, target_post_id, acting_user_id); + + +-- +-- Name: incoming_index; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX incoming_index ON incoming_links USING btree (topic_id, post_number); + + +-- +-- Name: index_actions_on_acting_user_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_actions_on_acting_user_id ON user_actions USING btree (acting_user_id); + + +-- +-- Name: index_actions_on_user_id_and_action_type; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_actions_on_user_id_and_action_type ON user_actions USING btree (user_id, action_type); + + +-- +-- Name: index_categories_on_forum_thread_count; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_categories_on_forum_thread_count ON categories USING btree (topic_count); + + +-- +-- Name: index_categories_on_name; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_categories_on_name ON categories USING btree (name); + + +-- +-- Name: index_category_featured_users_on_category_id_and_user_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_category_featured_users_on_category_id_and_user_id ON category_featured_users USING btree (category_id, user_id); + + +-- +-- Name: index_draft_sequences_on_user_id_and_draft_key; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_draft_sequences_on_user_id_and_draft_key ON draft_sequences USING btree (user_id, draft_key); + + +-- +-- Name: index_drafts_on_user_id_and_draft_key; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_drafts_on_user_id_and_draft_key ON drafts USING btree (user_id, draft_key); + + +-- +-- Name: index_email_logs_on_created_at; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_email_logs_on_created_at ON email_logs USING btree (created_at DESC); + + +-- +-- Name: index_email_logs_on_user_id_and_created_at; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_email_logs_on_user_id_and_created_at ON email_logs USING btree (user_id, created_at DESC); + + +-- +-- Name: index_email_tokens_on_token; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_email_tokens_on_token ON email_tokens USING btree (token); + + +-- +-- Name: index_facebook_user_infos_on_facebook_user_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_facebook_user_infos_on_facebook_user_id ON facebook_user_infos USING btree (facebook_user_id); + + +-- +-- Name: index_facebook_user_infos_on_user_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_facebook_user_infos_on_user_id ON facebook_user_infos USING btree (user_id); + + +-- +-- Name: index_forum_thread_link_clicks_on_forum_thread_link_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_forum_thread_link_clicks_on_forum_thread_link_id ON topic_link_clicks USING btree (topic_link_id); + + +-- +-- Name: index_forum_thread_links_on_forum_thread_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_forum_thread_links_on_forum_thread_id ON topic_links USING btree (topic_id); + + +-- +-- Name: index_forum_thread_links_on_forum_thread_id_and_post_id_and_url; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_forum_thread_links_on_forum_thread_id_and_post_id_and_url ON topic_links USING btree (topic_id, post_id, url); + + +-- +-- Name: index_forum_thread_users_on_forum_thread_id_and_user_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_forum_thread_users_on_forum_thread_id_and_user_id ON topic_users USING btree (topic_id, user_id); + + +-- +-- Name: index_forum_threads_on_bumped_at; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_forum_threads_on_bumped_at ON topics USING btree (bumped_at DESC); + + +-- +-- Name: index_forum_threads_on_category_id_and_sub_tag_and_bumped_at; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_forum_threads_on_category_id_and_sub_tag_and_bumped_at ON topics USING btree (category_id, sub_tag, bumped_at); + + +-- +-- Name: index_invites_on_email_and_invited_by_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_invites_on_email_and_invited_by_id ON invites USING btree (email, invited_by_id); + + +-- +-- Name: index_invites_on_invite_key; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_invites_on_invite_key ON invites USING btree (invite_key); + + +-- +-- Name: index_notifications_on_post_action_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_notifications_on_post_action_id ON notifications USING btree (post_action_id); + + +-- +-- Name: index_notifications_on_user_id_and_created_at; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_notifications_on_user_id_and_created_at ON notifications USING btree (user_id, created_at); + + +-- +-- Name: index_onebox_renders_on_url; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_onebox_renders_on_url ON onebox_renders USING btree (url); + + +-- +-- Name: index_post_actions_on_post_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_post_actions_on_post_id ON post_actions USING btree (post_id); + + +-- +-- Name: index_post_onebox_renders_on_post_id_and_onebox_render_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_post_onebox_renders_on_post_id_and_onebox_render_id ON post_onebox_renders USING btree (post_id, onebox_render_id); + + +-- +-- Name: index_post_replies_on_post_id_and_reply_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_post_replies_on_post_id_and_reply_id ON post_replies USING btree (post_id, reply_id); + + +-- +-- Name: index_posts_on_reply_to_post_number; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_posts_on_reply_to_post_number ON posts USING btree (reply_to_post_number); + + +-- +-- Name: index_posts_on_topic_id_and_post_number; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_posts_on_topic_id_and_post_number ON posts USING btree (topic_id, post_number); + + +-- +-- Name: index_site_customizations_on_key; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_site_customizations_on_key ON site_customizations USING btree (key); + + +-- +-- Name: index_topic_allowed_users_on_topic_id_and_user_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_topic_allowed_users_on_topic_id_and_user_id ON topic_allowed_users USING btree (topic_id, user_id); + + +-- +-- Name: index_topic_allowed_users_on_user_id_and_topic_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_topic_allowed_users_on_user_id_and_topic_id ON topic_allowed_users USING btree (user_id, topic_id); + + +-- +-- Name: index_topic_invites_on_invite_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_topic_invites_on_invite_id ON topic_invites USING btree (invite_id); + + +-- +-- Name: index_topic_invites_on_topic_id_and_invite_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_topic_invites_on_topic_id_and_invite_id ON topic_invites USING btree (topic_id, invite_id); + + +-- +-- Name: index_twitter_user_infos_on_twitter_user_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_twitter_user_infos_on_twitter_user_id ON twitter_user_infos USING btree (twitter_user_id); + + +-- +-- Name: index_twitter_user_infos_on_user_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_twitter_user_infos_on_user_id ON twitter_user_infos USING btree (user_id); + + +-- +-- Name: index_uploads_on_forum_thread_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_uploads_on_forum_thread_id ON uploads USING btree (topic_id); + + +-- +-- Name: index_uploads_on_user_id; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_uploads_on_user_id ON uploads USING btree (user_id); + + +-- +-- Name: index_user_open_ids_on_url; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_user_open_ids_on_url ON user_open_ids USING btree (url); + + +-- +-- Name: index_user_visits_on_user_id_and_visited_at; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_user_visits_on_user_id_and_visited_at ON user_visits USING btree (user_id, visited_at); + + +-- +-- Name: index_users_on_auth_token; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_users_on_auth_token ON users USING btree (auth_token); + + +-- +-- Name: index_users_on_email; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_users_on_email ON users USING btree (email); + + +-- +-- Name: index_users_on_last_posted_at; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_users_on_last_posted_at ON users USING btree (last_posted_at); + + +-- +-- Name: index_users_on_username; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_users_on_username ON users USING btree (username); + + +-- +-- Name: index_users_on_username_lower; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_users_on_username_lower ON users USING btree (username_lower); + + +-- +-- Name: index_versions_on_created_at; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_versions_on_created_at ON versions USING btree (created_at); + + +-- +-- Name: index_versions_on_number; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_versions_on_number ON versions USING btree (number); + + +-- +-- Name: index_versions_on_tag; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_versions_on_tag ON versions USING btree (tag); + + +-- +-- Name: index_versions_on_user_id_and_user_type; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_versions_on_user_id_and_user_type ON versions USING btree (user_id, user_type); + + +-- +-- Name: index_versions_on_user_name; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_versions_on_user_name ON versions USING btree (user_name); + + +-- +-- Name: index_versions_on_versioned_id_and_versioned_type; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_versions_on_versioned_id_and_versioned_type ON versions USING btree (versioned_id, versioned_type); + + +-- +-- Name: index_views_on_parent_id_and_parent_type; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX index_views_on_parent_id_and_parent_type ON views USING btree (parent_id, parent_type); + + +-- +-- Name: post_timings_summary; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE INDEX post_timings_summary ON post_timings USING btree (topic_id, post_number); + + +-- +-- Name: post_timings_unique; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX post_timings_unique ON post_timings USING btree (topic_id, post_number, user_id); + + +-- +-- Name: unique_views; Type: INDEX; Schema: backup; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX unique_views ON views USING btree (parent_id, parent_type, ip, viewed_at); + + +SET search_path = public, pg_catalog; + -- -- Name: cat_featured_threads; Type: INDEX; Schema: public; Owner: -; Tablespace: -- @@ -2603,4 +4642,6 @@ INSERT INTO schema_migrations (version) VALUES ('20130213203300'); INSERT INTO schema_migrations (version) VALUES ('20130221215017'); -INSERT INTO schema_migrations (version) VALUES ('20130226015336'); \ No newline at end of file +INSERT INTO schema_migrations (version) VALUES ('20130226015336'); + +INSERT INTO schema_migrations (version) VALUES ('20130306180148'); \ No newline at end of file diff --git a/lib/pinned_check.rb b/lib/pinned_check.rb new file mode 100644 index 000000000..d1890c58b --- /dev/null +++ b/lib/pinned_check.rb @@ -0,0 +1,24 @@ +# Helps us determine whether a topic should be displayed as pinned or not, +# taking into account anonymous users and users who have dismissed it +class PinnedCheck + + def initialize(topic, topic_user=nil) + @topic, @topic_user = topic, topic_user + end + + def pinned? + + # If the topic isn't pinned the answer is false + return false if @topic.pinned_at.blank? + + # If the user is anonymous or hasn't entered the topic, the value is always true + return true if @topic_user.blank? + + # If the user hasn't cleared the pin, it's true + return true if @topic_user.cleared_pinned_at.blank? + + # The final check is to see whether the cleared the pin before or after it was last pinned + @topic_user.cleared_pinned_at < @topic.pinned_at + end + +end \ No newline at end of file diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 468f975ee..f6a75dcaa 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -6,6 +6,47 @@ require_dependency 'topic_list' class TopicQuery + class << self + # use the constants in conjuction with COALESCE to determine the order with regard to pinned + # topics that have been cleared by the user. There + # might be a cleaner way to do this. + def lowest_date + "2010-01-01" + end + + def highest_date + "3000-01-01" + end + + # If you've clearned the pin, use bumped_at, otherwise put it at the top + def order_with_pinned_sql + "CASE + WHEN (COALESCE(topics.pinned_at, '#{lowest_date}') > COALESCE(tu.cleared_pinned_at, '#{lowest_date}')) + THEN '#{highest_date}' + ELSE topics.bumped_at + END DESC" + end + + # If you've clearned the pin, use bumped_at, otherwise put it at the top + def order_nocategory_with_pinned_sql + "CASE + WHEN topics.category_id IS NULL and (COALESCE(topics.pinned_at, '#{lowest_date}') > COALESCE(tu.cleared_pinned_at, '#{lowest_date}')) + THEN '#{highest_date}' + ELSE topics.bumped_at + END DESC" + end + + # For anonymous users + def order_nocategory_basic_bumped + "CASE WHEN topics.category_id IS NULL and (topics.pinned_at IS NOT NULL) THEN 0 ELSE 1 END, topics.bumped_at DESC" + end + + def order_basic_bumped + "CASE WHEN (topics.pinned_at IS NOT NULL) THEN 0 ELSE 1 END, topics.bumped_at DESC" + end + + end + def initialize(user=nil, opts={}) @user = user @@ -64,23 +105,19 @@ class TopicQuery # The popular view of topics def list_popular - return_list(unordered: true) do |list| - list.order('CASE WHEN topics.category_id IS NULL and topics.pinned THEN 0 ELSE 1 END, topics.bumped_at DESC') - end + TopicList.new(@user, default_list) end # The favorited topics def list_favorited return_list do |list| - list.joins("INNER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.starred AND tu.user_id = #{@user_id})") + list.where('tu.starred') end end def list_read return_list(unordered: true) do |list| - list - .joins("INNER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{@user_id})") - .order('COALESCE(tu.last_visited_at, topics.bumped_at) DESC') + list.order('COALESCE(tu.last_visited_at, topics.bumped_at) DESC') end end @@ -93,17 +130,30 @@ class TopicQuery end def list_posted - return_list do |list| - list.joins("INNER JOIN topic_users AS tu ON (tu.topic_id = topics.id AND tu.posted AND tu.user_id = #{@user_id})") - end + return_list {|l| l.where('tu.user_id IS NOT NULL') } end def list_uncategorized - return_list {|l| l.where(category_id: nil).order('topics.pinned desc')} + return_list(unordered: true) do |list| + list = list.where(category_id: nil) + + if @user_id.present? + list.order(TopicQuery.order_with_pinned_sql) + else + list.order(TopicQuery.order_nocategory_basic_bumped) + end + end end def list_category(category) - return_list {|l| l.where(category_id: category.id).order('topics.pinned desc')} + return_list(unordered: true) do |list| + list = list.where(category_id: category.id) + if @user_id.present? + list.order(TopicQuery.order_with_pinned_sql) + else + list.order(TopicQuery.order_basic_bumped) + end + end end def unread_count @@ -130,8 +180,22 @@ class TopicQuery query_opts = @opts.merge(list_opts) page_size = query_opts[:per_page] || SiteSetting.topics_per_page + # Start with a list of all topics result = Topic - result = result.topic_list_order unless query_opts[:unordered] + + if @user_id.present? + result = result.joins("LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{@user_id})") + end + + unless query_opts[:unordered] + # If we're logged in, we have to pay attention to our pinned settings + if @user_id.present? + result = result.order(TopicQuery.order_nocategory_with_pinned_sql) + else + result = result.order(TopicQuery.order_basic_bumped) + end + end + result = result.listable_topics.includes(:category) result = result.where('categories.name is null or categories.name <> ?', query_opts[:exclude_category]) if query_opts[:exclude_category] result = result.where('categories.name = ?', query_opts[:only_category]) if query_opts[:only_category] @@ -145,16 +209,15 @@ class TopicQuery def new_results(list_opts={}) default_list(list_opts) - .joins("LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{@user_id})") .where("topics.created_at >= :created_at", created_at: @user.treat_as_new_topic_start_date) .where("tu.last_read_post_number IS NULL") - .where("COALESCE(tu.notification_level, :tracking) >= :tracking", tracking: TopicUser::NotificationLevel::TRACKING) + .where("COALESCE(tu.notification_level, :tracking) >= :tracking", tracking: TopicUser.notification_levels[:tracking]) end def unread_results(list_opts={}) default_list(list_opts) - .joins("INNER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{@user_id} AND tu.last_read_post_number < topics.highest_post_number)") - .where("COALESCE(tu.notification_level, :regular) >= :tracking", regular: TopicUser::NotificationLevel::REGULAR, tracking: TopicUser::NotificationLevel::TRACKING) + .where("tu.last_read_post_number < topics.highest_post_number") + .where("COALESCE(tu.notification_level, :regular) >= :tracking", regular: TopicUser.notification_levels[:regular], tracking: TopicUser.notification_levels[:tracking]) end def random_suggested_results_for(topic, count, exclude_topic_ids) diff --git a/lib/unread.rb b/lib/unread.rb index 5aed0f251..e41d37e4e 100644 --- a/lib/unread.rb +++ b/lib/unread.rb @@ -27,7 +27,7 @@ class Unread protected def do_not_notify?(notification_level) - [TopicUser::NotificationLevel::MUTED, TopicUser::NotificationLevel::REGULAR].include?(notification_level) + [TopicUser.notification_levels[:muted], TopicUser.notification_levels[:regular]].include?(notification_level) end end diff --git a/spec/components/pinned_check_spec.rb b/spec/components/pinned_check_spec.rb new file mode 100644 index 000000000..2cab45ae9 --- /dev/null +++ b/spec/components/pinned_check_spec.rb @@ -0,0 +1,58 @@ +require 'pinned_check' + +describe PinnedCheck do + + #let(:topic) { Fabricate.build(:topic) } + + let(:pinned_at) { 12.hours.ago } + let(:unpinned_topic) { Fabricate.build(:topic) } + let(:pinned_topic) { Fabricate.build(:topic, pinned_at: pinned_at) } + + context "without a topic_user record (either anonymous or never been in the topic)" do + + it "returns false if the topic is not pinned" do + PinnedCheck.new(unpinned_topic).should_not be_pinned + end + + it "returns true if the topic is pinned" do + PinnedCheck.new(unpinned_topic).should_not be_pinned + end + + end + + context "with a topic_user record" do + let(:user) { Fabricate.build(:user) } + let(:unpinned_topic_user) { Fabricate.build(:topic_user, user: user, topic: unpinned_topic) } + + describe "unpinned topic" do + let(:topic_user) { TopicUser.new(topic: unpinned_topic, user: user) } + + it "returns false" do + PinnedCheck.new(unpinned_topic, topic_user).should_not be_pinned + end + + end + + describe "pinned topic" do + let(:topic_user) { TopicUser.new(topic: pinned_topic, user: user) } + + it "is pinned if the topic_user's cleared_pinned_at is blank" do + PinnedCheck.new(pinned_topic, topic_user).should be_pinned + end + + it "is not pinned if the topic_user's cleared_pinned_at is later than when it was pinned_at" do + topic_user.cleared_pinned_at = (pinned_at + 1.hour) + PinnedCheck.new(pinned_topic, topic_user).should_not be_pinned + end + + it "is pinned if the topic_user's cleared_pinned_at is earlier than when it was pinned_at" do + topic_user.cleared_pinned_at = (pinned_at - 3.hours) + PinnedCheck.new(pinned_topic, topic_user).should be_pinned + end + end + + + end + +end + diff --git a/spec/components/topic_query_spec.rb b/spec/components/topic_query_spec.rb index 2f5ea43e1..4536cec36 100644 --- a/spec/components/topic_query_spec.rb +++ b/spec/components/topic_query_spec.rb @@ -12,14 +12,13 @@ describe TopicQuery do context 'a bunch of topics' do let!(:regular_topic) { Fabricate(:topic, title: 'this is a regular topic', user: creator, bumped_at: 15.minutes.ago) } - let!(:pinned_topic) { Fabricate(:topic, title: 'this is a pinned topic', user: creator, pinned: true, bumped_at: 10.minutes.ago) } + let!(:pinned_topic) { Fabricate(:topic, title: 'this is a pinned topic', user: creator, pinned_at: 10.minutes.ago, bumped_at: 10.minutes.ago) } let!(:archived_topic) { Fabricate(:topic, title: 'this is an archived topic', user: creator, archived: true, bumped_at: 6.minutes.ago) } let!(:invisible_topic) { Fabricate(:topic, title: 'this is an invisible topic', user: creator, visible: false, bumped_at: 5.minutes.ago) } let!(:closed_topic) { Fabricate(:topic, title: 'this is a closed topic', user: creator, closed: true, bumped_at: 1.minute.ago) } + let(:topics) { topic_query.list_popular.topics } context 'list_popular' do - let(:topics) { topic_query.list_popular.topics } - it "returns the topics in the correct order" do topics.should == [pinned_topic, closed_topic, archived_topic, regular_topic] end @@ -33,6 +32,17 @@ describe TopicQuery do end end + context 'after clearring a pinned topic' do + before do + pinned_topic.clear_pin_for(user) + end + + it "no longer shows the pinned topic at the top" do + topics.should == [closed_topic, archived_topic, pinned_topic, regular_topic] + end + + end + end context 'categorized' do diff --git a/spec/components/unread_spec.rb b/spec/components/unread_spec.rb index c567ee9bd..e40b9d2a2 100644 --- a/spec/components/unread_spec.rb +++ b/spec/components/unread_spec.rb @@ -7,8 +7,8 @@ describe Unread do before do @topic = Fabricate(:topic, posts_count: 13, highest_post_number: 13) @topic_user = TopicUser.get(@topic, @topic.user) - @topic_user.stubs(:notification_level).returns(TopicUser::NotificationLevel::TRACKING) - @topic_user.notification_level = TopicUser::NotificationLevel::TRACKING + @topic_user.stubs(:notification_level).returns(TopicUser.notification_levels[:tracking]) + @topic_user.notification_level = TopicUser.notification_levels[:tracking] @unread = Unread.new(@topic, @topic_user) end @@ -51,7 +51,7 @@ describe Unread do it 'has 0 new posts if the user has read 10 posts but is not tracking' do @topic_user.stubs(:seen_post_count).returns(10) - @topic_user.stubs(:notification_level).returns(TopicUser::NotificationLevel::REGULAR) + @topic_user.stubs(:notification_level).returns(TopicUser.notification_levels[:regular]) @unread.new_posts.should == 0 end diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index d7a463ff2..5c78aa7ac 100644 --- a/spec/controllers/topics_controller_spec.rb +++ b/spec/controllers/topics_controller_spec.rb @@ -74,6 +74,37 @@ describe TopicsController do end + context 'clear_pin' do + it 'needs you to be logged in' do + lambda { xhr :put, :clear_pin, topic_id: 1 }.should raise_error(Discourse::NotLoggedIn) + end + + context 'when logged in' do + let(:topic) { Fabricate(:topic) } + let!(:user) { log_in } + + it "fails when the user can't see the topic" do + Guardian.any_instance.expects(:can_see?).with(topic).returns(false) + xhr :put, :clear_pin, topic_id: topic.id + response.should_not be_success + end + + describe 'when the user can see the topic' do + it "calls clear_pin_for if the user can see the topic" do + Topic.any_instance.expects(:clear_pin_for).with(user).once + xhr :put, :clear_pin, topic_id: topic.id + end + + it "succeeds" do + xhr :put, :clear_pin, topic_id: topic.id + response.should be_success + end + end + + end + + end + context 'status' do it 'needs you to be logged in' do lambda { xhr :put, :status, topic_id: 1, status: 'visible', enabled: true }.should raise_error(Discourse::NotLoggedIn) diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index 1991ff712..f8f669e08 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -547,8 +547,12 @@ describe Topic do @topic.reload end + it "doesn't have a pinned_at" do + @topic.pinned_at.should be_blank + end + it 'should not be pinned' do - @topic.should_not be_pinned + @topic.pinned_at.should be_blank end it 'adds a moderator post' do @@ -562,13 +566,13 @@ describe Topic do context 'enable' do before do - @topic.update_attribute :pinned, false + @topic.update_attribute :pinned_at, nil @topic.update_status('pinned', true, @user) @topic.reload end it 'should be pinned' do - @topic.should be_pinned + @topic.pinned_at.should be_present end it 'adds a moderator post' do @@ -588,7 +592,7 @@ describe Topic do @topic.reload end - it 'should not be pinned' do + it 'should not be archived' do @topic.should_not be_archived end @@ -866,8 +870,12 @@ describe Topic do topic.should be_visible end + it "has an empty pinned_at" do + topic.pinned_at.should be_blank + end + it 'is not pinned' do - topic.should_not be_pinned + topic.pinned_at.should be_blank end it 'is not closed' do diff --git a/spec/models/topic_user_spec.rb b/spec/models/topic_user_spec.rb index 41daf770e..60a7e61a3 100644 --- a/spec/models/topic_user_spec.rb +++ b/spec/models/topic_user_spec.rb @@ -5,155 +5,165 @@ describe TopicUser do it { should belong_to :user } it { should belong_to :topic } + let!(:yesterday) { DateTime.now.yesterday } + before do - #mock time so we can test dates - @now = DateTime.now.yesterday - DateTime.expects(:now).at_least_once.returns(@now) - @topic = Fabricate(:topic) - @user = Fabricate(:coding_horror) + DateTime.expects(:now).at_least_once.returns(yesterday) + end + + let!(:topic) { Fabricate(:topic) } + let!(:user) { Fabricate(:coding_horror) } + let(:topic_user) { TopicUser.get(topic,user) } + let(:topic_creator_user) { TopicUser.get(topic, topic.user) } + + let(:post) { Fabricate(:post, topic: topic, user: user) } + let(:new_user) { Fabricate(:user, auto_track_topics_after_msecs: 1000) } + let(:topic_new_user) { TopicUser.get(topic, new_user)} + + + describe "unpinned" do + + before do + TopicUser.change(user, topic, {:starred_at => yesterday}) + end + + it "defaults to blank" do + topic_user.cleared_pinned_at.should be_blank + end + end describe 'notifications' do it 'should be set to tracking if auto_track_topics is enabled' do - @user.auto_track_topics_after_msecs = 0 - @user.save - TopicUser.change(@user, @topic, {:starred_at => DateTime.now}) - TopicUser.get(@topic,@user).notification_level.should == TopicUser::NotificationLevel::TRACKING + user.update_column(:auto_track_topics_after_msecs, 0) + TopicUser.change(user, topic, {:starred_at => yesterday}) + TopicUser.get(topic, user).notification_level.should == TopicUser.notification_levels[:tracking] end it 'should reset regular topics to tracking topics if auto track is changed' do - TopicUser.change(@user, @topic, {:starred_at => DateTime.now}) - @user.auto_track_topics_after_msecs = 0 - @user.save - TopicUser.get(@topic,@user).notification_level.should == TopicUser::NotificationLevel::TRACKING + TopicUser.change(user, topic, {:starred_at => yesterday}) + user.auto_track_topics_after_msecs = 0 + user.save + topic_user.notification_level.should == TopicUser.notification_levels[:tracking] end it 'should be set to "regular" notifications, by default on non creators' do - TopicUser.change(@user, @topic, {:starred_at => DateTime.now}) - TopicUser.get(@topic,@user).notification_level.should == TopicUser::NotificationLevel::REGULAR + TopicUser.change(user, topic, {:starred_at => yesterday}) + TopicUser.get(topic,user).notification_level.should == TopicUser.notification_levels[:regular] end it 'reason should reset when changed' do - @topic.notify_muted!(@topic.user) - TopicUser.get(@topic,@topic.user).notifications_reason_id.should == TopicUser::NotificationReasons::USER_CHANGED + topic.notify_muted!(topic.user) + TopicUser.get(topic,topic.user).notifications_reason_id.should == TopicUser.notification_reasons[:user_changed] end it 'should have the correct reason for a user change when watched' do - @topic.notify_watch!(@user) - tu = TopicUser.get(@topic,@user) - tu.notification_level.should == TopicUser::NotificationLevel::WATCHING - tu.notifications_reason_id.should == TopicUser::NotificationReasons::USER_CHANGED - tu.notifications_changed_at.should_not be_nil + topic.notify_watch!(user) + topic_user.notification_level.should == TopicUser.notification_levels[:watching] + topic_user.notifications_reason_id.should == TopicUser.notification_reasons[:user_changed] + topic_user.notifications_changed_at.should_not be_nil end it 'should have the correct reason for a user change when set to regular' do - @topic.notify_regular!(@user) - tu = TopicUser.get(@topic,@user) - tu.notification_level.should == TopicUser::NotificationLevel::REGULAR - tu.notifications_reason_id.should == TopicUser::NotificationReasons::USER_CHANGED - tu.notifications_changed_at.should_not be_nil + topic.notify_regular!(user) + topic_user.notification_level.should == TopicUser.notification_levels[:regular] + topic_user.notifications_reason_id.should == TopicUser.notification_reasons[:user_changed] + topic_user.notifications_changed_at.should_not be_nil end it 'should have the correct reason for a user change when set to regular' do - @topic.notify_muted!(@user) - tu = TopicUser.get(@topic,@user) - tu.notification_level.should == TopicUser::NotificationLevel::MUTED - tu.notifications_reason_id.should == TopicUser::NotificationReasons::USER_CHANGED - tu.notifications_changed_at.should_not be_nil + topic.notify_muted!(user) + topic_user.notification_level.should == TopicUser.notification_levels[:muted] + topic_user.notifications_reason_id.should == TopicUser.notification_reasons[:user_changed] + topic_user.notifications_changed_at.should_not be_nil end it 'should watch topics a user created' do - tu = TopicUser.get(@topic,@topic.user) - tu.notification_level.should == TopicUser::NotificationLevel::WATCHING - tu.notifications_reason_id.should == TopicUser::NotificationReasons::CREATED_TOPIC + topic_creator_user.notification_level.should == TopicUser.notification_levels[:watching] + topic_creator_user.notifications_reason_id.should == TopicUser.notification_reasons[:created_topic] end end describe 'visited at' do - before do - TopicUser.track_visit!(@topic, @user) - @topic_user = TopicUser.get(@topic,@user) + before do + TopicUser.track_visit!(topic, user) end it 'set upon initial visit' do - @topic_user.first_visited_at.to_i.should == @now.to_i - @topic_user.last_visited_at.to_i.should == @now.to_i + topic_user.first_visited_at.to_i.should == yesterday.to_i + topic_user.last_visited_at.to_i.should == yesterday.to_i end it 'updates upon repeat visit' do - tomorrow = @now.tomorrow - DateTime.expects(:now).returns(tomorrow) + today = yesterday.tomorrow + DateTime.expects(:now).returns(today) - TopicUser.track_visit!(@topic,@user) + TopicUser.track_visit!(topic,user) # reload is a no go - @topic_user = TopicUser.get(@topic,@user) - @topic_user.first_visited_at.to_i.should == @now.to_i - @topic_user.last_visited_at.to_i.should == tomorrow.to_i + topic_user = TopicUser.get(topic,user) + topic_user.first_visited_at.to_i.should == yesterday.to_i + topic_user.last_visited_at.to_i.should == today.to_i end end describe 'read tracking' do - before do - @post = Fabricate(:post, topic: @topic, user: @topic.user) - TopicUser.update_last_read(@user, @topic.id, 1, 0) - @topic_user = TopicUser.get(@topic,@user) - end - it 'should create a new record for a visit' do - @topic_user.last_read_post_number.should == 1 - @topic_user.last_visited_at.to_i.should == @now.to_i - @topic_user.first_visited_at.to_i.should == @now.to_i - end + context "without auto tracking" do - it 'should update the record for repeat visit' do - Fabricate(:post, topic: @topic, user: @user) - TopicUser.update_last_read(@user, @topic.id, 2, 0) - @topic_user = TopicUser.get(@topic,@user) - @topic_user.last_read_post_number.should == 2 - @topic_user.last_visited_at.to_i.should == @now.to_i - @topic_user.first_visited_at.to_i.should == @now.to_i + before do + TopicUser.update_last_read(user, topic.id, 1, 0) + end + + let(:topic_user) { TopicUser.get(topic,user) } + + it 'should create a new record for a visit' do + topic_user.last_read_post_number.should == 1 + topic_user.last_visited_at.to_i.should == yesterday.to_i + topic_user.first_visited_at.to_i.should == yesterday.to_i + end + + it 'should update the record for repeat visit' do + Fabricate(:post, topic: topic, user: user) + TopicUser.update_last_read(user, topic.id, 2, 0) + topic_user = TopicUser.get(topic,user) + topic_user.last_read_post_number.should == 2 + topic_user.last_visited_at.to_i.should == yesterday.to_i + topic_user.first_visited_at.to_i.should == yesterday.to_i + end end context 'auto tracking' do + before do - Fabricate(:post, topic: @topic, user: @user) - @new_user = Fabricate(:user, auto_track_topics_after_msecs: 1000) - TopicUser.update_last_read(@new_user, @topic.id, 2, 0) - @topic_user = TopicUser.get(@topic,@new_user) + TopicUser.update_last_read(new_user, topic.id, 2, 0) end it 'should automatically track topics you reply to' do - post = Fabricate(:post, topic: @topic, user: @new_user) - @topic_user = TopicUser.get(@topic,@new_user) - @topic_user.notification_level.should == TopicUser::NotificationLevel::TRACKING - @topic_user.notifications_reason_id.should == TopicUser::NotificationReasons::CREATED_POST + post = Fabricate(:post, topic: topic, user: new_user) + topic_new_user.notification_level.should == TopicUser.notification_levels[:tracking] + topic_new_user.notifications_reason_id.should == TopicUser.notification_reasons[:created_post] end it 'should not automatically track topics you reply to and have set state manually' do - Fabricate(:post, topic: @topic, user: @new_user) - TopicUser.change(@new_user, @topic, notification_level: TopicUser::NotificationLevel::REGULAR) - @topic_user = TopicUser.get(@topic,@new_user) - @topic_user.notification_level.should == TopicUser::NotificationLevel::REGULAR - @topic_user.notifications_reason_id.should == TopicUser::NotificationReasons::USER_CHANGED + Fabricate(:post, topic: topic, user: new_user) + TopicUser.change(new_user, topic, notification_level: TopicUser.notification_levels[:regular]) + topic_new_user.notification_level.should == TopicUser.notification_levels[:regular] + topic_new_user.notifications_reason_id.should == TopicUser.notification_reasons[:user_changed] end it 'should automatically track topics after they are read for long enough' do - @topic_user.notification_level.should == TopicUser::NotificationLevel::REGULAR - TopicUser.update_last_read(@new_user, @topic.id, 2, 1001) - @topic_user = TopicUser.get(@topic,@new_user) - @topic_user.notification_level.should == TopicUser::NotificationLevel::TRACKING + topic_new_user.notification_level.should ==TopicUser.notification_levels[:regular] + TopicUser.update_last_read(new_user, topic.id, 2, 1001) + TopicUser.get(topic, new_user).notification_level.should == TopicUser.notification_levels[:tracking] end it 'should not automatically track topics after they are read for long enough if changed manually' do - TopicUser.change(@new_user, @topic, notification_level: TopicUser::NotificationLevel::REGULAR) - @topic_user = TopicUser.get(@topic,@new_user) - - TopicUser.update_last_read(@new_user, @topic, 2, 1001) - @topic_user = TopicUser.get(@topic,@new_user) - @topic_user.notification_level.should == TopicUser::NotificationLevel::REGULAR + TopicUser.change(new_user, topic, notification_level: TopicUser.notification_levels[:regular]) + TopicUser.update_last_read(new_user, topic, 2, 1001) + topic_new_user.notification_level.should == TopicUser.notification_levels[:regular] end end end @@ -162,34 +172,33 @@ describe TopicUser do it 'creates a forum topic user record' do lambda { - TopicUser.change(@user, @topic.id, starred: true) + TopicUser.change(user, topic.id, starred: true) }.should change(TopicUser, :count).by(1) end it "only inserts a row once, even on repeated calls" do lambda { - TopicUser.change(@user, @topic.id, starred: true) - TopicUser.change(@user, @topic.id, starred: false) - TopicUser.change(@user, @topic.id, starred: true) + TopicUser.change(user, topic.id, starred: true) + TopicUser.change(user, topic.id, starred: false) + TopicUser.change(user, topic.id, starred: true) }.should change(TopicUser, :count).by(1) end describe 'after creating a row' do before do - TopicUser.change(@user, @topic.id, starred: true) - @topic_user = TopicUser.where(user_id: @user.id, topic_id: @topic.id).first + TopicUser.change(user, topic.id, starred: true) end it 'has the correct starred value' do - @topic_user.should be_starred + TopicUser.get(topic, user).should be_starred end it 'has a lookup' do - TopicUser.lookup_for(@user, [@topic]).should be_present + TopicUser.lookup_for(user, [topic]).should be_present end it 'has a key in the lookup for this forum topic' do - TopicUser.lookup_for(@user, [@topic]).has_key?(@topic.id).should be_true + TopicUser.lookup_for(user, [topic]).has_key?(topic.id).should be_true end end