This commit is contained in:
Kira 2019-03-20 05:14:08 -07:00
parent fa619e3e1e
commit 45394916d8
40 changed files with 1503 additions and 18 deletions

View File

@ -85,6 +85,10 @@ class ApplicationController < ActionController::Base
puts "---"
end
if Rails.env.development?
Rails.logger.error("<<< \n #{exception.class} exception thrown: #{exception.message}\n#{exception.backtrace.join("\n")}\n<<<")
end
if exception.is_a?(::ActiveRecord::StatementInvalid) && exception.to_s =~ /statement timeout/
if Rails.env.production?
NewRelic::Agent.notice_error(exception, :uri => request.original_url, :referer => request.referer, :request_params => params, :custom_params => {:user_id => CurrentUser.user.id, :user_ip_addr => CurrentUser.ip_addr})

View File

@ -0,0 +1,94 @@
class PostSetMaintainersController < ApplicationController
respond_to :html
respond_to :js, except: [:index]
before_action :member_only
def index
@invites = PostSetMaintainer.where(user_id: CurrentUser.id).includes(:post_set)
end
def create
@set = PostSet.find(params[:post_set_id])
@user = User.find_by_name(params[:username])
check_edit_access(@set)
@invite = PostSetMaintainer.new(post_set_id: @set.id, user_id: @user.id, status: 'pending')
@invite.validate
if @invite.invalid?
flash[:notice] = @invite.errors.full_messages.join('; ')
redirect_to maintainers_post_set_path(@set)
return
end
if RateLimiter.check_limit("set.invite.#{CurrentUser.id}", 5, 1.hours)
flash[:notice] = "You must wait an hour before inviting more set maintainers."
end
PostSetMaintainer.where(user_id: @user.id, post_set_id: @set.id).destroy_all
@invite.save
if @invite.valid?
RateLimiter.hit("set.invite.#{CurrentUser.id}", 1.hours)
flash[:notice] = "#{@user.pretty_name} invited to be a maintainer"
else
flash[:notice] = @invite.errors.full_messages.join('; ')
end
redirect_to maintainers_post_set_path(@set)
end
def destroy
@maintainer = PostSetMaintainer.find(params[:id] || params[:post_set_maintainer][:id])
@set = @maintainer.post_set
check_edit_access(@set)
check_cancel_access(@maintainer)
@maintainer.cancel!
end
def approve
@maintainer = PostSetMaintainer.find(params[:id])
check_approve_access(@maintainer)
@maintainer.approve!
end
def deny
@maintainer = PostSetMaintainer.find(params[:id])
raise User::PrivilegeError unless @maintainer.user_id == CurrentUser.id
@maintainer.deny!
end
def block
@maintainer = PostSetMaintainer.find(params[:id])
check_block_access(@maintainer)
@maintainer.block!
end
private
def check_approve_access(maintainer)
raise User::PrivilegeError unless maintainer.user_id == CurrentUser.id
raise User::PrivilegeError if ['blocked', 'approved'].include?(maintainer.status)
end
def check_cancel_access(maintainer)
raise User::PrivilegeError if maintainer.status == 'blocked'
raise User::PrivilegeError if maintainer.status == 'cooldown' && @maintainer.created_at > 24.hours.ago
end
def check_block_access(maintainer)
raise User::PrivilegeError unless maintainer.user_id == CurrentUser.id
raise User::PrivilegeError if maintainer.status == 'blocked'
end
def check_edit_access(set)
unless set.is_owner?(CurrentUser) || CurrentUser.is_admin?
raise User::PrivilegeError
end
end
end

View File

@ -0,0 +1,166 @@
class PostSetsController < ApplicationController
respond_to :html, :json, :xml
before_action :member_only, only: [:new, :create, :update, :destroy,
:edit, :maintainers, :update_posts,
:add_post, :remove_post]
def index
if !params[:post_id].blank?
if CurrentUser.is_admin?
@sets = PostSet.paginate(include: :set_entries, conditions: ["set_entries.post_id = ?", params[:post_id]], page: 1, per_page: 50)
else
@sets = PostSet.paginate(include: :set_entries, conditions: ["(post_sets.public = TRUE OR post_sets.user_id = ?) AND set_entries.post_id = ?", CurrentUser.id || 0, params[:post_id]], page: 1, per_page: 50)
end
elsif !params[:maintainer_id].blank?
if CurrentUser.is_admin?
@sets = PostSet.paginate(include: :set_maintainers, conditions: ["(set_maintainers.user_id = ? AND set_maintainers.status = 'approved')", params[:maintainer_id]], page: 1, per_page: 50)
else
@sets = PostSet.paginate(include: :set_maintainers, conditions: ["(post_sets.public = TRUE OR post_sets.user_id = ?) AND (set_maintainers.user_id = ? AND set_maintainers.status = 'approved')", CurrentUser.id || 0, params[:maintainer_id]], page: 1, per_page: 50)
end
else
@sets = PostSet.search(search_params).paginate(params[:page], limit: params[:limit])
end
respond_with(@sets)
end
def atom
begin
@sets = PostSet.visible.order(id: :desc).limit(32)
headers["Content-Type"] = "application/atom+xml"
rescue RuntimeError => e
@set = []
end
render layout: false
end
def new
@set = PostSet.new
end
def create
@set = PostSet.create(set_params)
flash[:notice] = @set.valid? ? 'Set created' : @set.errors.full_messages.join('; ')
respond_with(@set)
end
def show
@set = PostSet.find(params[:id])
check_view_access(@set)
respond_with(@set)
end
def edit
@set = PostSet.find(params[:id])
check_edit_access(@set)
@can_edit = @set.is_owner?(CurrentUser) || CurrentUser.is_admin?
respond_with(@set)
end
def update
@set = PostSet.find(params[:id])
check_edit_access(@set)
@set.update(set_params)
flash[:notice] = @set.valid? ? 'Set updated.' : @set.errors.full_messages.join('; ')
if CurrentUser.is_admin? && !@set.is_owner?(CurrentUser.user)
if @set.saved_change_to_is_public?
ModAction.log("User ##{CurrentUser.id} marked set ##{@set.id} as private", :set_mark_private)
end
if @set.saved_change_to_watched_attribute?
ModAction.log("Admin ##{CurrentUser.id} edited set ##{@set.id}", :set_edit)
end
end
respond_with(@set)
end
def maintainers
@set = PostSet.find(params[:id])
end
def post_list
@set = PostSet.find(params[:id])
check_edit_access(@set)
respond_with(@set)
end
def update_posts
@set = PostSet.find(params[:id])
check_edit_access(@set)
@set.update(update_posts_params)
@set.synchronize!
flash[:notice] = @set.valid? ? 'Set posts updated.' : @set.errors.full_messages.join('; ')
redirect_back(fallback_location: post_list_post_set_path(@set))
end
def destroy
@set = PostSet.find(params[:id])
unless @set.is_owner?(CurrentUser.user) || CurrentUser.is_admin?
raise User::PrivilegeError
end
@set.destroy
respond_with(@set)
end
def select
@sets_owned = PostSet.all(order: "lower(name) ASC", conditions: ["user_id = ?", CurrentUser.id])
@sets_maintained = SetMaintainer.all(joins: :post_set, select: "set_maintainers.*, post_sets.*", conditions: ["set_maintainers.user_id = ? AND post_sets.public = TRUE", CurrentUser.id])
@grouped_options = {
"Owned" => @sets_owned.map {|x| [truncate(x.name.tr("_", " "), length: 35), x.id]},
"Maintained" => @sets_maintained.map {|x| [truncate(x.name.tr("_", " "), length: 35), x.id]}
}
render layout: false
end
def add_posts
@set = PostSet.find(params[:set_id])
check_edit_access(@set)
@set.add(params[:post_ids])
@set.save
respond_with(@set)
end
def remove_post
@set = PostSet.find(params[:set_id])
check_edit_access(@set)
@set.remove(params[:post_ids])
@set.save
respond_with(@set)
end
private
def check_edit_access(set)
unless set.is_owner?(CurrentUser.user) || set.is_maintainer?(CurrentUser.user)
raise User::PrivilegeError
end
if !set.is_public && !set.is_owner?(CurrentUser.user)
raise User::PrivilegeError
end
end
def check_view_access(set)
unless set.is_public || set.is_owner?(CurrentUser.user) || CurrentUser.is_admin?
raise User::PrivilegeError
end
end
def set_params
params.require(:post_set).permit(%i[name shortname description is_public transfer_on_delete])
end
def update_posts_params
params.require(:post_set).permit([:post_ids_string])
end
def search_params
params.fetch(:search, {}).permit!
end
end

View File

@ -105,7 +105,7 @@ private
@error_message = post.errors.full_messages.join("; ")
render :template => "static/error", :status => 500
else
response_params = {:q => params[:tags_query], :pool_id => params[:pool_id], :favgroup_id => params[:favgroup_id]}
response_params = {:q => params[:tags_query], :pool_id => params[:pool_id], post_set_id: params[:post_set_id], favgroup_id => params[:favgroup_id]}
response_params.reject!{|key, value| value.blank?}
redirect_to post_path(post, response_params)
end

View File

@ -23,7 +23,7 @@ module PostsHelper
def missed_post_search_count_js
return nil unless post_search_counts_enabled?
if params[:ms] == "1" && @post_set.post_count == 0 && @post_set.is_single_tag?
session_id = session.id
verifier = ActiveSupport::MessageVerifier.new(Danbooru.config.reportbooru_key, serializer: JSON, digest: "SHA256")
@ -34,7 +34,7 @@ module PostsHelper
def post_search_count_js
return nil unless post_search_counts_enabled?
if action_name == "index" && params[:page].nil?
tags = Tag.scan_query(params[:tags]).sort.join(" ")
@ -143,10 +143,17 @@ module PostsHelper
def is_pool_selected?(pool)
return false if params.has_key?(:q)
return false if params.has_key?(:favgroup_id)
return false if !params.has_key?(:pool_id)
return false unless params.has_key?(:pool_id)
return params[:pool_id].to_i == pool.id
end
def is_post_set_selected?(post_set)
return false if params.has_key?(:q)
return false if params.has_key?(:pool_id)
return false unless params.has_key?(:post_set_id)
return params[:post_set_id].to_i == post_set.id
end
def show_tag_change_notice?
Tag.scan_query(params[:tags]).size == 1 && TagChangeNoticeService.get_forum_topic_id(params[:tags])
end

View File

@ -70,6 +70,18 @@ module PostSets
::Pool.find_by_name(pool_name)
end
def post_set_name
@post_set_name ||= Tag.has_metatag?(tag_array, :set)
end
def has_post_set?
is_single_tag? && post_set_name && post_set
end
def post_set
::PostSet.find_by_shortname(post_set_name)
end
def favgroup_name
@favgroup_name ||= Tag.has_metatag?(tag_array, :favgroup)
end

View File

@ -0,0 +1,22 @@
class RateLimiter
def self.check_limit(key, max_attempts, lockout_time = 1.minute)
return true if Cache.get("#{key}:lockout")
attempts = Cache.get(key) || 0
if attempts >= max_attempts
Cache.put("#{key}:lockout", true, lockout_time)
reset_limit(key)
end
end
def self.hit(key, time_period = 1.minute)
value = Cache.get(key) || 0
Cache.put(key, value.to_i + 1, time_period)
return value.to_i + 1
end
def self.reset_limit(key)
Cache.delete(key)
end
end

View File

@ -35,6 +35,13 @@ class ApplicationRecord < ActiveRecord::Base
where.not("#{qualified_column_for(attr)} ~ ?", "(?e)" + value)
end
def attribute_exact_matches(attribute, value, **options)
return all unless value.present?
column = qualified_column_for_attribute(attribute)
where("#{column} = ?", value)
end
def attribute_matches(attribute, value, **options)
return all if value.nil?

View File

@ -1061,6 +1061,32 @@ class Post < ApplicationRecord
end
end
module SetMethods
def post_sets
@post_sets ||= begin
return PostSet.none if pool_string.blank?
post_set_ids = pool_string.scan(/\d+/)
PostSet.where(id: post_set_ids)
end
end
def add_set!(set, force = false)
with_lock do
self.pool_string = "#{pool_string} set:#{set.id}".strip
update_column(:pool_string, pool_string) unless new_record?
end
# TODO: Add some indexing step here to trigger an index update when elasticsearch is merged
end
def remove_set!(set)
with_lock do
self.pool_string = pool_string.gsub(/(?:\A| )set:#{set.id}(?:\Z| )/, " ").strip
update_column(:pool_string, pool_string) unless new_record?
end
# TODO: Add some indexing step here to trigger an index update when elasticsearch is merged
end
end
module PoolMethods
def pools
@pools ||= begin
@ -1886,6 +1912,7 @@ class Post < ApplicationRecord
include FavoriteMethods
include UploaderMethods
include PoolMethods
include SetMethods
include VoteMethods
extend CountMethods
include ParentMethods

310
app/models/post_set.rb Normal file
View File

@ -0,0 +1,310 @@
# -*- encoding : utf-8 -*-
class PostSet < ApplicationRecord
array_attribute :post_ids, parse: /\d+/, cast: :to_i
has_many :post_set_maintainers, dependent: :destroy do
def in_cooldown(user)
where(creator_id: user.id, status: 'cooldown').where('created_at < ?', 24.hours.ago)
end
def active
where(status: 'approved')
end
def pending
where(status: 'pending')
end
def banned
where(status: 'banned')
end
end
has_many :maintainers, class_name: "User", through: :post_set_maintainers
belongs_to_creator counter_cache: 'set_count'
validates_length_of :name, :shortname, in: 3..100, message: "must be between three and one hundred characters long"
validates_uniqueness_of :name, :shortname, case_sensitive: false, message: "is already taken"
validates_length_of :shortname, in: 1..50, message: 'must be between one and fifty characters long'
validates_format_of :shortname, with: /\A[\w]+\z/, message: "must only contain numbers, letters, and underscores"
validates_format_of :shortname, with: /\A\d*[a-z_][\w]*\z/, message: "must contain at least one letter or underscore"
validates_length_of :description, maximum: 10000
validate :validate_number_of_posts
validate :can_make_public
validate :set_per_hour_limit, on: :create
validate :can_create_new_set_limit, on: :create
after_update :send_maintainer_public_dmails
before_destroy :send_maintainer_destroy_dmails
before_save :update_post_count
def self.name_to_id(name)
if name =~ /^\d+$/
name.to_i
else
select_value_sql("SELECT id FROM post_sets WHERE lower(shortname) = ?", name.downcase.tr(" ", "_")).to_i
end
end
def self.name_to_id_visible(name)
end
def self.visible(user = CurrentUser)
return all() if user.is_admin?
where('is_public = true OR creator_id = ?', user.id)
end
def saved_change_to_watched_attributes?
saved_change_to_name? || saved_change_to_shortname? || saved_change_to_description?
end
module ValidationMethods
def send_maintainer_public_dmails
if RateLimiter.check_limit("set.public.#{id}", 1, 24.hours)
return
end
if is_public_changed? && !is_public # If set was made private
RateLimiter.hit("set.public.#{id}", 24.hours)
PostSetMaintainer.active.where(post_set_id: id).each do |maintainer|
Dmail.create_automated(to_id: maintainer.user_id, title: "A set you maintain was made private",
body: "The set \"#{name}\":#{post_set_path(self)} by \"#{creator.name}\":#{user_path(creator)} that you maintain was set to private. You will not be able to view, add posts, or remove posts from the set until the owner makes it public again.")
end
PostSetMaintainer.pending.where(post_set_id: id).delete
elsif is_public_changed? && is_public # If set was made public
RateLimiter.hit("set.public.#{id}", 24.hours)
PostSetMaintainer.active.where(post_set_id: id).each do |maintainer|
Dmail.create_automated(to_id: maintainer.user_id, titlet: "A private set you had maintained was made public again",
body: "The set \"#{name}\":#{post_set_path(self)} by \"#{creaator.name}\":#{user_path(creator)} that you previously maintained was made public again. You are now able to view the set and add/remove posts.")
end
end
end
def send_maintainer_destroy_dmails
PostSetMaintainer.active.where(post_set_id: id).each do |maintainer|
Dmail.create_automated(to_id: maintainer.user_id,
title: "A set you maintain was deleted",
body: "The set #{name} by \"#{creator.name}\":#{user_path(creator)} that you maintain was deleted.")
end
end
def can_make_public
if is_public && creator.created_at > 3.days.ago && !creator.is_builder?
errors.add(:base, "Can't make a set public until your account is at least three days old")
false
else
true
end
end
def can_create_new_set_limit
if PostSet.where(creator_id: creator.id).count() >= 75
errors.add(:base, "You can only create 75 sets.")
return false
end
true
end
def set_per_hour_limit
if where("created_at > ? AND creator_id = ?", 1.hour.ago, creator.id).count() > 6 && !creator.is_builder?
first = where("created_at > ? AND creator_id = ?", 1.hour.ago, creator.id).order(:created_at).first()
errors.add(:base, "You have already created 6 sets in the last hour. You can make a new set in #{time_ago_in_words(first.created_at)}")
false
else
true
end
end
def validate_number_of_posts
if post_ids.size > 10_000
errors.add(:base, "Sets can have up to 10,000 posts each")
false
else
true
end
end
end
module AccessMethods
def can_view?(user)
is_public || user_id == user.id || user.is_admin?
end
def can_edit?(user)
is_owner?(user) || is_maintainer?(user)
end
def is_maintainer?(user)
return false if user.is_blocked?
post_set_maintainers.where(user_id: user.id, status: 'approved').count() > 0
end
def is_invited?(user)
post_set_maintainers.where(user_id: user.id, status: 'pending').count() > 0
end
def is_blocked?(user)
post_set_maintainers.where(user_id: user.id, status: 'blocked').count() > 0
end
def is_owner?(user)
return false if user.is_blocked?
creator_id == user.id || user.is_admin?
end
end
module PostMethods
def contains?(post_id)
post_ids.include?(post_id)
end
def page_number(post_id)
post_ids.find_index(post_id).to_i + 1
end
def add(ids)
real_ids = Post.select(:id).where(id: ids)
real_ids.each do |post|
next if contains?(post.id)
self.post_ids = post_ids + [post.id]
end
end
def add!(post)
return if contains?(post.id)
with_lock do
update(post_ids: post_ids + [post.id])
post.add_set!(self, true)
end
end
def remove(ids)
ids.each do |id|
next if contains?(id)
self.post_ids = post_ids - [id]
end
end
def remove!(post)
return unless contains?(post.id)
with_lock do
update(post_ids: post_ids - [post.id])
post.remove_set!(self)
end
end
def posts(options = {})
offset = options[:offset] || 0
limit = options[:limit] || Danbooru.config.posts_per_page
slice = post_ids.slice(offset, limit)
if slice && slice.any?
Post.where(id: slice).sort_by {|record| slice.index {|id| id == record.id}}
else
[]
end
end
def post_count
post_ids.size
end
def first_post?(post_id)
post_id == post_ids.first
end
def last_post?(post_id)
post_id == post_ids.last
end
def previous_post_id(post_id)
return nil if first_post?(post_id) || !contains?(post_id)
n = post_ids.index(post_id) - 1
post_ids[n]
end
def next_post_id(post_id)
return nil if last_post?(post_id) || !contains?(post_id)
n = post_ids.index(post_id) + 1
post_ids[n]
end
def synchronize
post_ids_before = post_ids_before_last_save || post_ids_was
added = post_ids - post_ids_before
removed = post_ids_before - post_ids
added_posts = Post.where(id: added)
added_posts.each do |post|
post.add_set!(self, true)
end
removed_posts = Post.where(id: removed)
removed_posts.each do |post|
post.remove_set!(self)
end
normalize_post_ids
end
def synchronize!
synchronize
save if will_save_change_to_post_ids?
end
def normalize_post_ids
self.post_ids = post_ids.uniq
end
def update_post_count
normalize_post_ids
self.post_count = post_ids.size
end
end
module SearchMethods
def selected_first(current_set_id)
return where("true") if current_set_id.blank?
current_set_id = current_set_id.to_i
reorder(Arel.sql("(case post_sets.id when #{current_set_id} then 0 else 1 end), post_sets.name"))
end
def self.search(prams)
q = super
q = q.where('is_public = true')
q = q.or(where('(is_public = false AND creator_id = ?', creator_id)) if creator_id
if params[:creator_name].present?
user = User.find_by_name(params[:username])
q = q.where(creator_id: user)
end
q = q.attribute_exact_matches(:creator_id, params[:creator_id])
q = q.search_text_attribute(:name, params)
q = q.attribute_exact_matches(:shortname, params[:shortname])
case params[:order]
when 'name'
q = q.order(:name, id: :desc)
when 'shortname'
q = q.order(:shortname, id: :desc)
when 'postcount'
q = q.order(post_count: :desc, id: :desc)
when 'created_at'
q = q.order(:id)
when 'update'
q = q.order(updated_at: :desc)
else
q = q.order(id: :desc)
end
q
end
end
extend SearchMethods
include ValidationMethods
include AccessMethods
include PostMethods
end

View File

@ -0,0 +1,149 @@
class PostSetMaintainer < ApplicationRecord
belongs_to :user
belongs_to :post_set
validate :ensure_not_set_owner, on: :create
validate :ensure_set_public, on: :create
validate :ensure_maintainer_count, on: :create
validate :ensure_not_duplicate, on: :create
after_create :notify_maintainer
def notify_maintainer
body = "\"#{post_set.creator.name}\":/users/#{post_set.creator_id} invited you to be a maintainer of the \"#{post_set.name}\":/post_sets/#{post_set_id} set. This would allow you to add and remove posts from it.
\"Click here\":/post_set_maintainers/approve/#{id} to approve the request and become a maintainer.
\"Click here\":/post_set_maintainers/deny/#{id} to deny the request.
\"Click here\":/post_set_maintainers/block/#{id} to deny the request and prevent yourself from being invited to this set again in the future."
Dmail.create_automated(
to_id: user_id,
title: "You were invite to be a maintainer of #{post_set.name}",
body: body
)
end
def notify_destroy
end
def cancel!
if status == 'pending'
self.status = 'cooldown'
save
return
end
if status == 'approved'
body = "\"#{post_set.creator.name}\":/users/#{post_set.creator_id} removed you as a maintainer of the \"#{post_set.name}\":/post_sets/#{post_set.id} set."
Dmail.create_automated(
to_id: user_id,
title: "You were removed as a set maintainer of #{post_set.name}",
body: body
)
end
destroy
end
def approve!
self.status = 'approved'
save
Dmail.create_automated(
to_id: post_set.creator_id,
title: "#{user.name} approved your invite to maintain #{post_set.name}",
body: "\"#{user.name}\":/users/#{user_id} approved your invite to maintain \"#{post_set.name}\":/post_sets/#{post_set.id}."
)
end
def deny!
if status == "pending"
Dmail.create_automated(
to_id: post_set.creator_id,
title: "#{user.name} denied your invite to maintain #{post_set.name}",
body: "\"#{user.name}\":/users/#{user.id} denied your invite to maintain \"#{post_set.name}\":/post_sets/#{post_set.id}."
)
elsif status == "approved"
Dmail.create_automated(
to_id: post_set.creator_id,
title: "#{user.name} removed themselves as a maintainer of #{post_set.name}",
body: "\"#{user.name}\":/users/#{user.id} removed themselves as a maintainer of \"#{post_set.name}\":/post_sets/#{post_set.id}."
)
end
destroy
end
def block!
if status == "approved"
Dmail.create_automated(
to_id: post_set.creator_id,
title: "#{user.name} removed themselves as a maintainer of #{post_set.name}",
body: "\"#{user.name}\":/users/#{user.id} removed themselves as a maintainer of \"#{post_set.name}\":/post_sets/#{post_set.id} and blocked all future invites."
)
elsif status == "pending"
Dmail.create_automated(
to_id: post_set.creator_id,
title: "#{user.name} denied your invite to maintain #{post_set.name}",
body: "\"#{user.name}\":/users/#{user.id} denied your invite to maintain \"#{post_set.name}\":/post_sets/#{post_set.id} and blocked all future invites."
)
end
self.status = 'blocked'
save
end
module ValidaitonMethods
def ensure_not_set_owner
if post_set.creator_id == user_id
errors.add(:user, "owns this set and can't be added as a maintainer")
false
end
end
def ensure_maintainer_count
if PostSetMaintainer.where(post_set_id: post_set_id).count >= 75
errors.add(:post_set, "current have too many maintainers")
false
end
end
def ensure_not_duplicate
existing = PostSetMaintainer.where(post_set_id: post_set_id, user_id: user_id).first
if existing.nil?
return
end
if ['approved', 'pending'].include?(existing.status)
errors.add(:base, "Already a maintainer of this set")
return false
end
if existing.status == 'blocked'
errors.add(:base, 'User has blocked you from inviting them to maintain this set')
return false
end
if existing.status == 'cooldown' && existing.created_at > 24.hours.ago
errors.add(:base, "User has been invited to maintain this set too recently")
return false
end
end
def ensure_set_public
unless post_set.is_public
errors.add(:post_set, 'must be public')
false
end
end
end
def maintained(user)
where(user_id: user.id, status: 'approved').joins(:post_set).where('post_set.is_public = true')
end
def self.active
where(status: 'approved')
end
def self.pending
where(status: 'pending')
end
include ValidaitonMethods
end

View File

@ -18,7 +18,7 @@ class Tag < ApplicationRecord
-locked locked width height mpixels ratio score favcount filesize source
-source id -id date age order limit -status status tagcount parent -parent
child pixiv_id pixiv search upvote downvote filetype -filetype flagger
-flagger appealer -appealer disapproval -disapproval
-flagger appealer -appealer disapproval -disapproval set -set
] + TagCategory.short_name_list.map {|x| "#{x}tags"} + COUNT_METATAGS + COUNT_METATAG_SYNONYMS
SUBQUERY_METATAGS = %w[commenter comm noter noteupdater artcomm flagger -flagger appealer -appealer]
@ -657,6 +657,25 @@ class Tag < ApplicationRecord
q[:tags][:related] << "pool:#{pool_id}"
q[:ordpool] = pool_id
when "set"
post_set_id = PostSet.name_to_id(g2)
post_set = PostSet.find(post_set_id)
unless post_set.can_view?(CurrentUser.user)
raise User::PrivilegeError
end
q[:tags][:related] << "set:#{post_set_id}"
when "-set"
post_set_id = PostSet.name_to_id(g2)
post_set = PostSet.find(post_set_id)
unless post_set.can_view?(CurrentUser.user)
raise User::PrivilegeError
end
q[:tags][:exclude] << "set:#{post_set_id}"
when "-favgroup"
favgroup_id = FavoriteGroup.name_to_id(g2)
favgroup = FavoriteGroup.find(favgroup_id)

View File

@ -36,8 +36,8 @@ class PostPresenter < Presenter
if options[:pool_id]
locals[:link_params]["pool_id"] = options[:pool_id]
end
if options[:favgroup_id]
locals[:link_params]["favgroup_id"] = options[:favgroup_id]
if options[:post_set_id]
locals[:link_params]["post_set_id"] = options[:post_set_id]
end
locals[:tooltip] = "#{post.tag_string} rating:#{post.rating} score:#{post.score}"
@ -160,12 +160,12 @@ class PostPresenter < Presenter
end
def has_nav_links?(template)
has_sequential_navigation?(template.params) || @post.pools.undeleted.any? || @post.favorite_groups(active_id=template.params[:favgroup_id]).any?
has_sequential_navigation?(template.params) || @post.pools.undeleted.any? || @post.post_sets.visible.any?
end
def has_sequential_navigation?(params)
return false if Tag.has_metatag?(params[:q], :order, :ordfav, :ordpool)
return false if params[:pool_id].present? || params[:favgroup_id].present?
return false if params[:pool_id].present? || params[:post_set_id].present?
return CurrentUser.user.enable_sequential_post_navigation
end
end

View File

@ -8,7 +8,7 @@ module PostSetPresenters
html = ""
if posts.empty?
return template.render("post_sets/blank")
return template.render("posts/blank")
end
posts.each do |post|

View File

@ -4,7 +4,7 @@ module PostSetPresenters
html = ""
if posts.empty?
return template.render("post_sets/blank")
return template.render("posts/blank")
end
posts.each do |post|

View File

@ -16,7 +16,7 @@ module PostSetPresenters
html = ""
if posts.empty?
return template.render("post_sets/blank")
return template.render("posts/blank")
end
posts.each do |post|

View File

@ -11,7 +11,7 @@ module PostSetPresenters
html = ""
if pools.empty?
return template.render("post_sets/blank")
return template.render("posts/blank")
end
pools.each do |pool|

View File

@ -0,0 +1 @@
location.reload();

View File

@ -0,0 +1 @@
location.reload();

View File

@ -0,0 +1 @@
location.reload();

View File

@ -0,0 +1,74 @@
<div id="c-set-maintainer">
<div id="a-index">
<h2>Pending Invites</h2>
<table class="rounded nomargin" width="600px" style="margin-bottom:30px;">
<thead>
<th width="55%"><%= link_to "Set", action: "index", order: "set" %></th>
<th width="20%"><%= link_to "Status", action: "index", order: "status" %></th>
<th width="25%"></th>
</thead>
<tbody>
<% @invites.where(status: 'pending').each do |m| %>
<% if m.post_set.is_public %>
<tr>
<td><%= link_to m.post_set.name, post_set_path(m.post_set) %></td>
<td><%= m.status.capitalize %></td>
<td>
<%= link_to "Accept", approve_post_set_maintainer_path(m), remote: true, method: :post, 'data-confirm': 'Are you sure you want to accept this invite?' %>
<%= link_to "Ignore", deny_post_set_maintainer_path(m), remote: true, method: :post, 'data-confirm': 'Are you sure you want to ignore this invite?' %>
<%= link_to "Block", block_post_set_maintainer_path(m), remote: true, method: :post, 'data-confirm': 'Aer you sure you want to ignore and block future invites for this set?' %>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
<h2>Approved Invites</h2>
<table class="rounded nomargin" width="600px" style="margin-bottom:30px;">
<thead>
<th width="55%"><%= link_to "Set", action: "index", order: "set" %></th>
<th width="20%"><%= link_to "Status", action: "index", order: "status" %></th>
<th width="25%"></th>
</thead>
<tbody>
<% @invites.where(status: 'approved').each do |m| %>
<% if m.post_set.is_public %>
<tr>
<td><%= link_to m.post_set.name, post_set_path(m.post_set) %></td>
<td><%= m.status.capitalize %></td>
<td>
<%= link_to "Remove", deny_post_set_maintainer_path(m), remote: true, method: :post, 'data-confirm': 'Are you sure you want to remove yourself as maintainer of this set?' %>
<%= link_to "Block", block_post_set_maintainer_path(m), remote: true, method: :post, 'data-confirm': 'Aer you sure you want to remove yourself and block future invites for this set?' %>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
<h2>Blocked Invites</h2>
<table class="rounded nomargin" width="600px">
<thead>
<th width="55%"><%= link_to "Set", action: "index", order: "set" %></th>
<th width="20%"><%= link_to "Status", action: "index", order: "status" %></th>
<th width="25%"></th>
</thead>
<tbody>
<% @invites.where(status: 'blocked').each do |m| %>
<% if m.post_set.is_public %>
<tr>
<td><%= link_to m.post_set.name, post_set_path(m.post_set) %></td>
<td><%= m.status.capitalize %></td>
<td>
<%= link_to "Unblock", deny_post_set_maintainer_path(m), remote: true, method: :post, 'data-confirm': 'Are you sure you want to unblock invites for this set?' %>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
</div>
</div>
<%= render partial: "post_sets/secondary_links" %>

View File

@ -0,0 +1,29 @@
<% unless params[:show] %><div id='searchform_hide'><%= link_to_function "Show Search Options", "$j('#searchform').fadeIn('fast'); $('searchform_hide').hide();" %></div><% end %>
<div class='section' id='searchform' style='width:400px;<% unless params[:show] %>display:none;<% end %>'>
<% unless params[:show] %><%= link_to_function "Hide Search Options", "$j('#searchform').fadeOut('fast'); $('searchform_hide').show();" %><% end %>
<% form_tag({action: "index"}, method: :get) do %>
<table class='nomargin'>
<tr><td><label for="name">Name</label></td> <td><%= text_field_tag "name", params[:name], style: "width:195px"%></td></tr>
<tr><td><label for="name">Short Name</label></td> <td><%= text_field_tag "shortname", params[:shortname], style: "width:195px"%></td></tr>
<tr><td><label for="name">Username</label></td> <td><%= text_field_tag "username", params[:username], style: "width:195px"%></td></tr>
<tr><td><label for="type">Order</label></td> <td>
<%= select_tag "order", options_for_select([
["Name", "name"],
["Short Name", "shortname"],
["Creator", "creator"],
["Post Count", "postcount"],
["Created", "created"],
["Updated", "updated"]
], params[:order]), style: "width:200px;" %>
</td></tr>
<tr><td colspan="2"><%= submit_tag "Search", name: nil %></td></tr>
</table>
<% if params[:show] %>
<input type='hidden' name='show' value='1'/>
<% end %>
<% end %>
</div>
<% if params[:name] || params[:type] || params[:status] %>
<script type='text/javascript'>$('searchform_hide').hide(); $('searchform').show();</script>
<% end %>

View File

@ -0,0 +1,29 @@
<% content_for(:secondary_links) do %>
<menu>
<%= subnav_link_to "List", post_sets_path %>
<%= subnav_link_to "New", new_post_set_path %>
<%= subnav_link_to "Help", wiki_pages_path(title: 'help:sets') %>
<% if CurrentUser.is_member? %>
<%= subnav_link_to "Mine", post_sets_path(search: {creator_id: CurrentUser.id}) %>
<%= subnav_link_to "Invites", post_set_maintainers_path %>
<% end %>
<% if @set %>
<li>|</li>
<%= subnav_link_to "Posts", posts_path(tags: "set:#{@set.shortname}") %>
<%= subnav_link_to "Maintainers", action: "maintainers", id: params[:id] %>
<% if @set.is_owner?(CurrentUser.user) || CurrentUser.is_admin? %>
<%= subnav_link_to "Edit", edit_post_set_path(@set) %>
<% end %>
<% if @set.is_owner?(CurrentUser.user) || @set.is_maintainer?(CurrentUser.user) %>
<%= subnav_link_to "Edit Posts", post_list_post_set_path(@set) %>
<% end %>
<% if @set.is_owner?(CurrentUser.user) || CurrentUser.is_admin? %>
<%= subnav_link_to "Delete", post_set_path(@set), method: 'delete', data: {confirm: 'Are you sure you want to delete this set?'} %>
<% end %>
<%= subnav_link_to "Report", new_ticket_path(type: 'set', disp_id: @set.id) %>
<% end %>
</menu>
<% end %>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title><%= Danbooru.config.app_name %> - Sets</title>
<link href="<%= atom_post_sets_url() %>" rel="self"/>
<link href="<%= post_sets_url() %>>" rel="alternate"/>
<id><%= atom_post_sets_url() %></id>
<% if @sets.any? %>
<updated><%= @sets[0].created_at.gmtime.xmlschema %></updated>
<% end %>
<author><name><%= Danbooru.config.app_name %></name></author>
<% @sets.each do |set| %>
<entry>
<title><%= set.name %></title>
<link href="<%= post_set_url(set) %>" rel="alternate"/>
<id><%= post_set_url(set) %></id>
<updated><%= set.created_at.gmtime.xmlschema %></updated>
<summary><%= Nokogiri::HTML(format_text set.description).text %></summary>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
<%= format_text set.description %>
</div>
</content>
<author>
<name><%= set.creator.pretty_name %></name>
</author>
</entry>
<% end %>
</feed>

View File

@ -0,0 +1,10 @@
<h3>Delete Set</h3>
<div class='section' style='width:500px;'>
<% form_tag(action: "destroy") do %>
<p>Are you sure you wish to delete "<%= h(@set.name) %>"?</p>
<%= submit_tag "Yes" %> <%= button_to_function "No", "history.back()" %>
<% end %>
</div>
<%= render partial: "footer" %>

View File

@ -0,0 +1,38 @@
<div id="a-set">
<div id="c-edit">
<h2>Editing <span class="set-name"><%= @set.name %></span></h2>
<% if @can_edit %>
<div class='section'>
<%= simple_form_for(@set) do |f| %>
<%= f.input :name, as: :string, input_html: {onkeyup: 'fillShortName()'} %>
<%= f.input :shortname, label: 'Short Name', as: :string, input_html: {onchange: 'shortNameChanged()'},
hint: "The short name is used for the set's metatag name. Can only contain letters, numbers, and underscores
and must contain at least one letter or underscore. <a href=\"/post?tags=set%3Aexample\">set:example</a>".html_safe %>
<%= dtext_field "post_set", "description", :classes => "autocomplete-mentions", :input_id => "set_description", :preview_id => "dtext-preview" %>
<%= f.input :is_public, label: 'Public', hint: "Private sets are only visible to you. Public sets are visible to anyone, but only you and users you
assign as maintainers can edit the set. Only accounts three days or older can make public sets." %>
<%= f.input :transfer_on_delete, label: 'Transfer on Deletion', hint: 'If "Transfer on Delete" is enabled, when a post is deleted from the site, its parent (if any) will be
added to this set in its place. Disable if you want posts to simply be removed from this set with no
replacement.' %>
<%= f.button :submit, "Update" %>
<%= dtext_preview_button :post_set, :body, :input_id => "set_description", :preview_id => "dtext-preview" %>
<% end %>
</div>
<% end %>
<% if @set.is_owner?(CurrentUser) %>
<%= link_to "&raquo; Click here to edit maintainers.".html_safe, maintainers_post_set_path(@set) %><br/>
<% end %>
<% if @set.is_owner?(CurrentUser) || @set.is_maintainer?(CurrentUser) %>
<%= link_to "&raquo; Click here to add/remove posts.".html_safe, post_list_post_set_path(@set) %><br/>
<% end %>
</div>
</div>
<%= render partial: "secondary_links" %>

View File

@ -0,0 +1,60 @@
<div id="c-sets">
<div id="a-index">
<%= render partial: 'search' %>
<h2>Sets</h2>
<div id="set-index">
<table width="100%" class="rounded">
<thead>
<tr>
<th width="30%"><%= link_to "Name", action: "index", order: "username" %></th>
<th width="20%"><%= link_to "Short Name", action: "index", order: "shortname" %></th>
<th width="15%">Creator</th>
<th width="5%"><%= link_to "Posts", action: "index", order: "postcount" %></th>
<th width="10%"><%= link_to "Created", action: "index", order: "created" %></th>
<th width="10%"><%= link_to "Updated", action: "index", order: "updated" %></th>
<th width="10%">Status</th>
</tr>
</thead>
<tbody>
<% @sets.each do |s| %>
<tr>
<td><%= link_to s.name, post_set_path(s) %></td>
<td><%= link_to s.shortname, posts_path(tags: "set:#{s.shortname}") %></td>
<td><%= link_to_user s.creator %></td>
<td><%= s.post_count %></td>
<td><span class="date" title="<%= s.created_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(s.created_at) + " ago" %></span></td>
<td><span class="date" title="<%= s.updated_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(s.updated_at) + " ago" %></span></td>
<td>
<% if s.is_public %>
<div class='set-status set-status-public' title='This set is public'>Public</div>
<% else %>
<div class='set-status set-status-private' title='This set is private and only visible to you'>Private</div>
<% end %>
<% if s.is_owner?(CurrentUser.user) %>
<div class='set-status set-status-owner' title='You own this set'>Owner</div>
<% elsif s.is_maintainer?(CurrentUser.user) %>
<div class='set-status set-status-maintainer' title='You are a maintainer of this set and can add and remove posts. Click to view invites'><%= link_to "Maint.", controller: "post_set_maintainers", action: "index" %></div>
<% elsif s.is_invited?(CurrentUser.user) %>
<div class='set-status set-status-invited' title='You have been invited to maintain this set. Click to view invites'><%= link_to "Invited", controller: "post_set_maintainers", action: "index" %></div>
<% elsif s.is_blocked?(CurrentUser.user) %>
<div class='set-status set-status-blocked' title='You have blocked the owner of this set from inviting you to maintain it. Click to view invites'><%= link_to "Blocked", controller: "post_set_maintainers", action: "index" %></div>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<div id="paginator">
<%= numbered_paginator(@sets) %>
</div>
</div>
</div>
<%= render partial: "secondary_links" %>

View File

@ -0,0 +1,74 @@
<div id="c-sets">
<div id="a-maintainers">
<% @maintainers = @set.post_set_maintainers %>
<h2>Maintainers for set <span class="set-name"><%= @set.name %></span></h2>
<p>Maintainers are users who are assigned to a set and have the ability to add or remove posts from it. They cannot
edit any other details of the set.</p>
<h3>Approved</h3>
<table class="rounded nomargin" width="400px" style="margin-bottom:30px;">
<thead>
<tr>
<th width="55%"><%= link_to "Username", action: "maintainers", id: @set.id, order: "username" %></th>
<th width="30%"><%= link_to "Status", action: "maintainers", id: @set.id, order: "status" %></th>
<th width="15%"></th>
</tr>
</thead>
<tbody>
<% @maintainers.active.each do |m| %>
<tr>
<td><%= link_to_user m.user %></td>
<td><%= m.status.capitalize %></td>
<td>
<% if @set.is_owner?(CurrentUser) %>
<%= form_tag({controller: "set_maintainer", action: "destroy"}) do %>
<%= hidden_field_tag "id", m.id %>
<%= submit_tag "Remove" %>
<% end %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<h3>Pending</h3>
<table class="rounded nomargin" width="400px">
<thead>
<tr>
<th width="55%"><%= link_to "Username", action: "maintainers", id: @set.id, order: "username" %></th>
<th width="30%"><%= link_to "Status", action: "maintainers", id: @set.id, order: "status" %></th>
<th width="15%"></th>
</tr>
</thead>
<tbody>
<% @maintainers.pending.each do |m| %>
<tr>
<td><%= link_to_user m.user %></td>
<td><%= m.status.capitalize %></td>
<td>
<% if @set.is_owner?(CurrentUser) %>
<%= link_to "Remove", post_set_maintainer_path(m), method: :delete, remote: true, data: {confirm: "Are you sure you want to remove this pending invite?"} %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% if @set.is_owner?(CurrentUser) && @set.is_public %>
<h3 style="margin-top:15px;">Add new Maintainer</h3>
<div class='section' style="width:380px;">
<%= form_tag(post_set_maintainers_path) do %>
<label for="set_maintainer_name">Username</label><br/>
<%= hidden_field_tag "post_set_id", @set.id %>
<%= text_field_tag "username" %>
<%= submit_tag "Create" %>
<% end %>
</div>
<% end %>
</div>
</div>
<%= render partial: "secondary_links" %>

View File

@ -0,0 +1,31 @@
<div id="c-sets">
<div id="a-new">
<h2>New Set</h2>
<div>
<%= simple_form_for(@set) do |f| %>
<%= error_messages_for :post_set %>
<%= f.input :name, as: :string, input_html: {onkeyup: 'fillShortName()'} %>
<%= f.input :shortname, label: 'Short Name', as: :string, input_html: {onchange: 'shortNameChanged()'},
hint: "The short name is used for the set's metatag name. Can only contain letters, numbers, and underscores
and must contain at least one letter or underscore. <a href=\"/post?tags=set%3Aexample\">set:example</a>".html_safe %>
<%= dtext_field "post_set", "description", :classes => "autocomplete-mentions", :input_id => "set_description", :preview_id => "dtext-preview" %>
<%= f.input :is_public, label: 'Public', hint: "Private sets are only visible to you. Public sets are visible to anyone, but only you and users you
assign as maintainers can edit the set. Only accounts three days or older can make public sets." %>
<%= f.input :transfer_on_delete, label: 'Transfer on Deletion', hint: 'If "Transfer on Delete" is enabled, when a post is deleted from the site, its parent (if any) will be
added to this set in its place. Disable if you want posts to simply be removed from this set with no
replacement.' %>
<p>You can add posts to the set and assign maintainers on the next page.</p>
<%= f.button :submit, "Create" %>
<%= dtext_preview_button :post_set, :body, :input_id => "set_description", :preview_id => "dtext-preview" %>
<% end %>
</div>
</div>
</div>
<%= render partial: "secondary_links" %>

View File

@ -0,0 +1,12 @@
<div id="c-sets">
<div id="a-posts">
<h2>Post list for set <span class="set-name"><%= @set.name %></span></h2>
<%= simple_form_for(@set, url: update_posts_post_set_path(@set), method: :post) do |f| %>
<%= f.input :post_ids_string, as: :text, label: 'Post IDs' %>
<%= f.button :submit, "Update" %>
<% end %>
</div>
</div>
<%= render partial: "secondary_links" %>

View File

@ -0,0 +1,7 @@
<% form_tag(action: "add_post") do %>
<%= hidden_field_tag "post_id", params[:post_id] %>
<%= select_tag "set_id", unsorted_grouped_options_for_select(@grouped_options, session[:last_set_id]) %>
<% if !params[:mode] %>
<%= button_to_function "Add", "PostSet.add_post(#{params[:post_id]}, jQuery('#set_id').val())" %>
<% end %>
<% end %>

View File

@ -0,0 +1,79 @@
<div id="c-sets">
<div id="a-show">
<div class="set-header"><span class="set-name"><%= @set.name %></span> by
<span class="set-creator"><%= link_to_user @set.creator %></span>
<% if @set.is_public %>
<div class='set-status set-status-public' title='This set is public'>Public</div>
<% else %>
<div class='set-status set-status-private' title='This set is private and only visible to you'>Private</div>
<% end %>
<% if @set.is_owner?(CurrentUser.user) %>
<div class='set-status set-status-owner' title='You own this set'>Owner</div>
<% elsif @set.is_maintainer?(CurrentUser.user) %>
<div class='set-status set-status-maintainer' title='You are a maintainer of this set and can add and remove posts'>Maint.</div>
<% end %>
</div>
<div class="set-shortname">Short
Name: <%= link_to @set.shortname, posts_path(tags: "set:#{@set.shortname}") %></div>
Created:
<span class="date" title="<%= @set.created_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(@set.created_at) + " ago" %></span>
|
Updated:
<span class="date" title="<%= @set.updated_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(@set.updated_at) + " ago" %></span><br/>
<% if @set.description.blank? %>
<div class='set-description'>No description.</div>
<% else %>
<div class='set-description'><%= format_text @set.description %></div>
<% end %>
<div class='set-description-bottom'></div>
<span class="set-viewposts"><%= link_to "&raquo; View Posts (#{@set.post_count.to_s})".html_safe, posts_path(tags: "set:#{@set.shortname}") %></span><br/>
<%#= link_to "&raquo; Maintainers (" + PostSetMaintainer.count(conditions: ["post_set_id = ?", @set.id]).to_s + ")", action: "maintainers", id: @set.id %>
<br/>
<% if @set.post_count == 0 %>
<div class="set-empty section">
<p>This set has no posts in it.</p>
<% if @set.is_owner?(CurrentUser.user) || @set.is_maintainer?(CurrentUser.user) %>
To start adding posts to this set:
<ul>
<li>On a post's page, click <strong>Add to Set</strong> under <strong>Options</strong> in the sidebar,
select a set, and click <strong>Add</strong></li>
<li>When viewing the <%= link_to "post index", posts_path %>, select
<strong>Add to set...</strong> from the mode dropdown under the search bar, select a set from the new
dropdown, and click a post thumbnail to add it to the set.
</li>
</ul>
<% end %>
</div>
<% elsif @set.is_owner?(CurrentUser.user) || @set.is_maintainer?(CurrentUser.user) %>
<div class="section">
To add posts to this set:
<ul>
<li>On a post's page, click <strong>Add to Set</strong> under <strong>Options</strong> in the sidebar, select
a set, and click <strong>Add</strong></li>
<li>When viewing the <%= link_to "post index", posts_path %>, select
<strong>Add to set...</strong> from the mode dropdown under the search bar, select a set from the new
dropdown, and click a post thumbnail to add it to the set.
</li>
</ul>
To remove posts from this set:
<ul>
<li>When viewing the <%= link_to "post index", posts_path %>, select
<strong>Remove from set...</strong> from the mode dropdown under the search bar, select a set from the new
dropdown, and click a post thumbnail to remove it to the set.
</li>
</ul>
</div>
<% end %>
</div>
</div>
<% content_for(:secondary_links) do %>
<% end %>
<%= render partial: "secondary_links" %>

View File

@ -5,7 +5,7 @@
<p>Recent updates may not have been processed yet. The most recently processed version was <%= time_ago_in_words_tagged(PostArchive.maximum(:updated_at)) %>.</p>
<% if @post_versions.length == 0 %>
<%= render "post_sets/blank" %>
<%= render "posts/blank" %>
<% else %>
<% if params.dig(:search, :post_id).present? %>
<%= render "revert_listing" %>

View File

@ -5,10 +5,14 @@
<% end %>
<% if post.pools.undeleted.any? %>
<%= render "posts/partials/show/pool_list", post: post, pools: post.pools.undeleted.selected_first(params[:pool_id]) %>
<%= render "posts/partials/show/pool_list", post: post, pools: post.pools.undeleted.selected_first(params[:pool_id]).limit(5) %>
<% end %>
<% if post.favorite_groups(active_id=params[:favgroup_id]).any? %>
<% if post.post_sets.visible.any? %>
<%= render "posts/partials/show/post_set_list", post: post, post_sets: post.post_sets.visible.selected_first(params[:post_set_id]).limit(5) %>
<% end %>
<% if post.favorite_groups(active_id = params[:favgroup_id]).any? %>
<%= render "posts/partials/show/favorite_groups", :post => post %>
<% end %>
</div>

View File

@ -0,0 +1,7 @@
<div id="set-nav">
<ul>
<% post_sets.each do |set| %>
<%= render "posts/partials/show/post_set_list_item", post_set: set, post: post, selected: is_post_set_selected?(set) %>
<% end %>
</ul>
</div>

View File

@ -0,0 +1,33 @@
<%= content_tag :li, id: "nav-link-for-set-#{post_set.id}", class: "set-selected-#{selected}" do -%>
<% if !post_set.first_post?(post.id) && post_set.post_ids.first -%>
<%= link_to("&laquo;".html_safe, post_path(post_set.post_ids.first, post_set_id: post_set.id), class: "first", title: "to first") %>
<% else -%>
<span class="first">&laquo;</span>
<% end -%>
<% post_set.previous_post_id(post.id).tap do |previous_post_id| -%>
<% if previous_post_id %>
<%= link_to "&lsaquo;&thinsp;prev".html_safe, post_path(previous_post_id, post_set_id: post_set.id), rel: selected ? "prev" : nil, class: "prev", title: "to page #{post_set.page_number(previous_post_id)}" -%>
<% else -%>
<span class="prev">&lsaquo;&thinsp;prev</span>
<% end %>
<% end -%>
<span class="set-name">
<%= link_to("Set: #{post_set.name}", post_set_path(post_set), title: "page #{post_set.page_number(post.id)}/#{post_set.post_count}") -%>
</span>
<% post_set.next_post_id(post.id).tap do |next_post_id| -%>
<% if next_post_id %>
<%= link_to("next&thinsp;&rsaquo;".html_safe, post_path(next_post_id, post_set_id: post_set.id), rel: selected ? "next" : nil, class: "next", title: "to page #{post_set.page_number(next_post_id)}") -%>
<% else -%>
<span class="next">next&thinsp;&rsaquo;</span>
<% end -%>
<% end -%>
<% if !post_set.last_post?(post.id) && post_set.post_ids.last -%>
<%= link_to("&raquo;".html_safe, post_path(post_set.post_ids.last, post_set_id: post_set.id), class: "last", title: "to page #{post_set.post_count}") -%>
<% else -%>
<span class="last">&raquo;</span>
<% end -%>
<% end -%>

View File

@ -360,6 +360,25 @@ Rails.application.routes.draw do
end
end
resources :post_report_reasons
resources :post_sets do
collection do
get :atom
end
member do
get :maintainers
get :post_list
post :update_posts
post :add_posts
post :remove_posts
end
end
resources :post_set_maintainers do
member do
post :approve
post :block
post :deny
end
end
# aliases
resources :wpages, :controller => "wiki_pages"

View File

@ -0,0 +1,25 @@
class CreatePostSets < ActiveRecord::Migration[5.2]
def change
create_table :post_sets do |t|
t.string :name, null: false
t.string :shortname, null: false
t.text :description, default: ''
t.boolean :is_public, null: false, default: false
t.boolean :transfer_on_delete, null: false, default: false
t.integer :creator_id, null: false
t.column :creator_ip_addr, 'inet', null: true
t.column :post_ids, 'integer[]', null: false, default: '{}'
t.integer :post_count, null: false, default: 0
t.timestamps
end
create_table :post_set_maintainers do |t|
t.integer :post_set_id, null: false
t.integer :user_id, null: false
t.string :status, null: false, default: 'pending'
t.timestamps
end
add_column :users, :set_count, :integer, null: false, default: 0
end
end

View File

@ -1528,6 +1528,78 @@ CREATE SEQUENCE public.post_report_reasons_id_seq
ALTER SEQUENCE public.post_report_reasons_id_seq OWNED BY public.post_report_reasons.id;
--
-- Name: post_set_maintainers; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.post_set_maintainers (
id bigint NOT NULL,
post_set_id integer NOT NULL,
user_id integer NOT NULL,
status character varying DEFAULT 'pending'::character varying NOT NULL,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
);
--
-- Name: post_set_maintainers_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.post_set_maintainers_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: post_set_maintainers_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.post_set_maintainers_id_seq OWNED BY public.post_set_maintainers.id;
--
-- Name: post_sets; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.post_sets (
id bigint NOT NULL,
name character varying NOT NULL,
shortname character varying NOT NULL,
description text DEFAULT ''::text,
is_public boolean DEFAULT false NOT NULL,
transfer_on_delete boolean DEFAULT false NOT NULL,
creator_id integer NOT NULL,
creator_ip_addr inet,
post_ids integer[] DEFAULT '{}'::integer[] NOT NULL,
post_count integer DEFAULT 0 NOT NULL,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
);
--
-- Name: post_sets_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.post_sets_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: post_sets_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.post_sets_id_seq OWNED BY public.post_sets.id;
--
-- Name: post_updates; Type: TABLE; Schema: public; Owner: -
--
@ -2208,7 +2280,8 @@ furry -rating:s'::text,
custom_style text,
bit_prefs bigint DEFAULT 0 NOT NULL,
last_ip_addr inet,
unread_dmail_count integer DEFAULT 0 NOT NULL
unread_dmail_count integer DEFAULT 0 NOT NULL,
set_count integer DEFAULT 0 NOT NULL
);
@ -2583,6 +2656,20 @@ ALTER TABLE ONLY public.post_replacements ALTER COLUMN id SET DEFAULT nextval('p
ALTER TABLE ONLY public.post_report_reasons ALTER COLUMN id SET DEFAULT nextval('public.post_report_reasons_id_seq'::regclass);
--
-- Name: post_set_maintainers id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.post_set_maintainers ALTER COLUMN id SET DEFAULT nextval('public.post_set_maintainers_id_seq'::regclass);
--
-- Name: post_sets id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.post_sets ALTER COLUMN id SET DEFAULT nextval('public.post_sets_id_seq'::regclass);
--
-- Name: post_votes id; Type: DEFAULT; Schema: public; Owner: -
--
@ -3029,6 +3116,22 @@ ALTER TABLE ONLY public.post_report_reasons
ADD CONSTRAINT post_report_reasons_pkey PRIMARY KEY (id);
--
-- Name: post_set_maintainers post_set_maintainers_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.post_set_maintainers
ADD CONSTRAINT post_set_maintainers_pkey PRIMARY KEY (id);
--
-- Name: post_sets post_sets_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.post_sets
ADD CONSTRAINT post_sets_pkey PRIMARY KEY (id);
--
-- Name: post_votes post_votes_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@ -4485,6 +4588,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20190222082952'),
('20190228144206'),
('20190305165101'),
('20190313221440');
('20190313221440'),
('20190317024446');