mirror of
https://github.com/moebooru/moebooru
synced 2025-08-22 01:47:48 +00:00
--HG--
branch : moe extra : convert_revision : svn%3A2d28d66d-8d94-df11-8c86-00306ef368cb/trunk/moe%405
This commit is contained in:
parent
8135474ee3
commit
30ff4fccd3
85
app/controllers/admin_controller.rb
Normal file
85
app/controllers/admin_controller.rb
Normal file
@ -0,0 +1,85 @@
|
||||
class AdminController < ApplicationController
|
||||
layout "default"
|
||||
before_filter :admin_only, :except => [:cache_stats]
|
||||
|
||||
def index
|
||||
set_title "Admin"
|
||||
end
|
||||
|
||||
def edit_user
|
||||
if request.post?
|
||||
@user = User.find_by_name(params[:user][:name])
|
||||
if @user.nil?
|
||||
flash[:notice] = "User not found"
|
||||
redirect_to :action => "edit_user"
|
||||
return
|
||||
end
|
||||
@user.level = params[:user][:level]
|
||||
|
||||
if @user.save
|
||||
flash[:notice] = "User updated"
|
||||
redirect_to :action => "edit_user"
|
||||
else
|
||||
render_error(@user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def reset_password
|
||||
if request.post?
|
||||
@user = User.find_by_name(params[:user][:name])
|
||||
|
||||
if @user
|
||||
new_password = @user.reset_password
|
||||
flash[:notice] = "Password reset to #{new_password}"
|
||||
|
||||
unless @user.email.blank?
|
||||
UserMailer.deliver_new_password(@user, new_password)
|
||||
end
|
||||
else
|
||||
flash[:notice] = "That account does not exist"
|
||||
redirect_to :action => "reset_password"
|
||||
end
|
||||
else
|
||||
@user = User.new
|
||||
end
|
||||
end
|
||||
|
||||
def cache_stats
|
||||
keys = []
|
||||
[0, 20, 30, 35, 40, 50].each do |level|
|
||||
keys << "stats/count/level=#{level}"
|
||||
|
||||
[0, 1, 2, 3, 4, 5].each do |tag_count|
|
||||
keys << "stats/tags/level=#{level}&tags=#{tag_count}"
|
||||
end
|
||||
|
||||
keys << "stats/page/level=#{level}&page=0-10"
|
||||
keys << "stats/page/level=#{level}&page=10-20"
|
||||
keys << "stats/page/level=#{level}&page=20+"
|
||||
end
|
||||
|
||||
@post_stats = keys.inject({}) {|h, k| h[k] = Cache.get(k); h}
|
||||
end
|
||||
|
||||
def reset_post_stats
|
||||
keys = []
|
||||
[0, 20, 30, 35, 40].each do |level|
|
||||
keys << "stats/count/level=#{level}"
|
||||
|
||||
[0, 1, 2, 3, 4, 5].each do |tag_count|
|
||||
keys << "stats/tags/level=#{level}&tags=#{tag_count}"
|
||||
end
|
||||
|
||||
keys << "stats/page/level=#{level}&page=0-10"
|
||||
keys << "stats/page/level=#{level}&page=10-20"
|
||||
keys << "stats/page/level=#{level}&page=20+"
|
||||
end
|
||||
|
||||
keys.each do |key|
|
||||
CACHE.set(key, 0)
|
||||
end
|
||||
|
||||
redirect_to :action => "cache_stats"
|
||||
end
|
||||
end
|
20
app/controllers/advertisement_controller.rb
Normal file
20
app/controllers/advertisement_controller.rb
Normal file
@ -0,0 +1,20 @@
|
||||
class AdvertisementController < ApplicationController
|
||||
layout "bare"
|
||||
before_filter :admin_only, :only => [:reset_stats]
|
||||
|
||||
def redirect_ad
|
||||
ad = Advertisement.find(params[:id])
|
||||
ad.increment!(:hit_count)
|
||||
redirect_to ad.referral_url
|
||||
end
|
||||
|
||||
def show_stats
|
||||
@ads = Advertisement.find(:all, :order => "id")
|
||||
render :layout => "default"
|
||||
end
|
||||
|
||||
def reset_stats
|
||||
Advertisement.update_all("hit_count = 0")
|
||||
redirect_to :action => "show_stats"
|
||||
end
|
||||
end
|
345
app/controllers/application.rb
Normal file
345
app/controllers/application.rb
Normal file
@ -0,0 +1,345 @@
|
||||
require 'digest/md5'
|
||||
|
||||
class ApplicationController < ActionController::Base
|
||||
# This is a proxy class to make various nil checks unnecessary
|
||||
class AnonymousUser
|
||||
def id
|
||||
nil
|
||||
end
|
||||
|
||||
def level
|
||||
0
|
||||
end
|
||||
|
||||
def name
|
||||
"Anonymous"
|
||||
end
|
||||
|
||||
def pretty_name
|
||||
"Anonymous"
|
||||
end
|
||||
|
||||
def is_anonymous?
|
||||
true
|
||||
end
|
||||
|
||||
def has_permission?(obj, foreign_key = :user_id)
|
||||
false
|
||||
end
|
||||
|
||||
def can_change?(record, attribute)
|
||||
method = "can_change_#{attribute.to_s}?"
|
||||
if record.respond_to?(method)
|
||||
record.__send__(method, self)
|
||||
elsif record.respond_to?(:can_change?)
|
||||
record.can_change?(self, attribute)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def show_samples?
|
||||
true
|
||||
end
|
||||
|
||||
def has_avatar?
|
||||
false
|
||||
end
|
||||
|
||||
CONFIG["user_levels"].each do |name, value|
|
||||
normalized_name = name.downcase.gsub(/ /, "_")
|
||||
|
||||
define_method("is_#{normalized_name}?") do
|
||||
false
|
||||
end
|
||||
|
||||
define_method("is_#{normalized_name}_or_higher?") do
|
||||
false
|
||||
end
|
||||
|
||||
define_method("is_#{normalized_name}_or_lower?") do
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module LoginSystem
|
||||
protected
|
||||
def access_denied
|
||||
previous_url = params[:url] || request.request_uri
|
||||
|
||||
respond_to do |fmt|
|
||||
fmt.html {flash[:notice] = "Access denied"; redirect_to(:controller => "user", :action => "login", :url => previous_url)}
|
||||
fmt.xml {render :xml => {:success => false, :reason => "access denied"}.to_xml(:root => "response"), :status => 403}
|
||||
fmt.json {render :json => {:success => false, :reason => "access denied"}.to_json, :status => 403}
|
||||
end
|
||||
end
|
||||
|
||||
def set_current_user
|
||||
if RAILS_ENV == "test" && session[:user_id]
|
||||
@current_user = User.find_by_id(session[:user_id])
|
||||
end
|
||||
|
||||
if @current_user == nil && session[:user_id]
|
||||
@current_user = User.find_by_id(session[:user_id])
|
||||
end
|
||||
|
||||
if @current_user == nil && cookies[:login] && cookies[:pass_hash]
|
||||
@current_user = User.authenticate_hash(cookies[:login], cookies[:pass_hash])
|
||||
end
|
||||
|
||||
if @current_user == nil && params[:login] && params[:password_hash]
|
||||
@current_user = User.authenticate_hash(params[:login], params[:password_hash])
|
||||
end
|
||||
|
||||
if @current_user == nil && params[:user]
|
||||
@current_user = User.authenticate(params[:user][:name], params[:user][:password])
|
||||
end
|
||||
|
||||
if @current_user
|
||||
if @current_user.is_blocked? && @current_user.ban && @current_user.ban.expires_at < Time.now
|
||||
@current_user.update_attribute(:level, CONFIG["starting_level"])
|
||||
Ban.destroy_all("user_id = #{@current_user.id}")
|
||||
end
|
||||
|
||||
session[:user_id] = @current_user.id
|
||||
else
|
||||
@current_user = AnonymousUser.new
|
||||
end
|
||||
|
||||
# For convenient access in activerecord models
|
||||
Thread.current["danbooru-user"] = @current_user
|
||||
Thread.current["danbooru-user_id"] = @current_user.id
|
||||
Thread.current["danbooru-ip_addr"] = request.remote_ip
|
||||
|
||||
# Hash the user's IP to seed things like mirror selection.
|
||||
Thread.current["danbooru-ip_addr_seed"] = Digest::MD5.hexdigest(request.remote_ip)[0..7].hex
|
||||
|
||||
ActiveRecord::Base.init_history
|
||||
|
||||
UserLog.access(@current_user, request)
|
||||
end
|
||||
|
||||
CONFIG["user_levels"].each do |name, value|
|
||||
normalized_name = name.downcase.gsub(/ /, "_")
|
||||
|
||||
define_method("#{normalized_name}_only") do
|
||||
if @current_user.__send__("is_#{normalized_name}_or_higher?")
|
||||
return true
|
||||
else
|
||||
access_denied()
|
||||
end
|
||||
end
|
||||
|
||||
# For many actions, GET invokes the HTML UI, and a POST actually invokes
|
||||
# the action, so we often want to require higher access for POST (so the UI
|
||||
# can invoke the login dialog).
|
||||
define_method("post_#{normalized_name}_only") do
|
||||
return true unless request.post?
|
||||
|
||||
if @current_user.__send__("is_#{normalized_name}_or_higher?")
|
||||
return true
|
||||
else
|
||||
access_denied()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module RespondToHelpers
|
||||
protected
|
||||
def respond_to_success(notice, redirect_to_params, options = {})
|
||||
extra_api_params = options[:api] || {}
|
||||
|
||||
respond_to do |fmt|
|
||||
fmt.html {flash[:notice] = notice ; redirect_to(redirect_to_params)}
|
||||
fmt.json {render :json => extra_api_params.merge(:success => true).to_json}
|
||||
fmt.xml {render :xml => extra_api_params.merge(:success => true).to_xml(:root => "response")}
|
||||
end
|
||||
end
|
||||
|
||||
def respond_to_error(obj, redirect_to_params, options = {})
|
||||
extra_api_params = options[:api] || {}
|
||||
status = options[:status] || 500
|
||||
|
||||
if obj.is_a?(ActiveRecord::Base)
|
||||
obj = obj.errors.full_messages.join(", ")
|
||||
status = 420
|
||||
end
|
||||
|
||||
case status
|
||||
when 420
|
||||
status = "420 Invalid Record"
|
||||
|
||||
when 421
|
||||
status = "421 User Throttled"
|
||||
|
||||
when 422
|
||||
status = "422 Locked"
|
||||
|
||||
when 423
|
||||
status = "423 Already Exists"
|
||||
|
||||
when 424
|
||||
status = "424 Invalid Parameters"
|
||||
end
|
||||
|
||||
respond_to do |fmt|
|
||||
fmt.html {flash[:notice] = "Error: #{obj}" ; redirect_to(redirect_to_params)}
|
||||
fmt.json {render :json => extra_api_params.merge(:success => false, :reason => obj).to_json, :status => status}
|
||||
fmt.xml {render :xml => extra_api_params.merge(:success => false, :reason => obj).to_xml(:root => "response"), :status => status}
|
||||
end
|
||||
end
|
||||
|
||||
def respond_to_list(inst_var_name)
|
||||
inst_var = instance_variable_get("@#{inst_var_name}")
|
||||
|
||||
respond_to do |fmt|
|
||||
fmt.html
|
||||
fmt.json {render :json => inst_var.to_json}
|
||||
fmt.xml {render :xml => inst_var.to_xml(:root => inst_var_name)}
|
||||
end
|
||||
end
|
||||
|
||||
def render_error(record)
|
||||
@record = record
|
||||
render :status => 500, :layout => "bare", :inline => "<%= error_messages_for('record') %>"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
include LoginSystem
|
||||
include RespondToHelpers
|
||||
#include ExceptionNotifiable
|
||||
include CacheHelper
|
||||
#local_addresses.clear
|
||||
|
||||
before_filter :set_title
|
||||
before_filter :set_current_user
|
||||
before_filter :check_ip_ban
|
||||
after_filter :init_cookies
|
||||
|
||||
protected :build_cache_key
|
||||
protected :get_cache_key
|
||||
|
||||
def get_ip_ban()
|
||||
ban = IpBans.find(:first, :conditions => ["? <<= ip_addr", request.remote_ip])
|
||||
if not ban then return nil end
|
||||
return ban
|
||||
end
|
||||
|
||||
protected
|
||||
def check_ip_ban
|
||||
if request.parameters[:controller] == "banned" and request.parameters[:action] == "index" then
|
||||
return
|
||||
end
|
||||
|
||||
ban = get_ip_ban()
|
||||
if not ban then
|
||||
return
|
||||
end
|
||||
|
||||
if ban.expires_at && ban.expires_at < Time.now then
|
||||
IpBans.destroy_all("ip_addr = '#{request.remote_ip}'")
|
||||
return
|
||||
end
|
||||
|
||||
redirect_to :controller => "banned", :action => "index"
|
||||
end
|
||||
|
||||
def check_load_average
|
||||
current_load = Sys::CPU.load_avg[1]
|
||||
|
||||
if request.get? && request.env["HTTP_USER_AGENT"] !~ /Google/ && current_load > CONFIG["load_average_threshold"] && @current_user.is_member_or_lower?
|
||||
render :file => "#{RAILS_ROOT}/public/503.html", :status => 503
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def set_title(title = CONFIG["app_name"])
|
||||
@page_title = CGI.escapeHTML(title)
|
||||
end
|
||||
|
||||
def save_tags_to_cookie
|
||||
if params[:tags] || (params[:post] && params[:post][:tags])
|
||||
tags = TagAlias.to_aliased((params[:tags] || params[:post][:tags]).downcase.scan(/\S+/))
|
||||
tags += cookies["recent_tags"].to_s.scan(/\S+/)
|
||||
cookies["recent_tags"] = tags.slice(0, 20).join(" ")
|
||||
end
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.headers["Cache-Control"] = "max-age=300"
|
||||
end
|
||||
|
||||
def cache_action
|
||||
if request.method == :get && request.env !~ /Googlebot/ && params[:format] != "xml" && params[:format] != "json"
|
||||
key, expiry = get_cache_key(controller_name, action_name, params, :user => @current_user)
|
||||
|
||||
if key && key.size < 200
|
||||
cached = Cache.get(key)
|
||||
|
||||
unless cached.blank?
|
||||
render :text => cached, :layout => false
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
yield
|
||||
|
||||
if key && response.headers['Status'] =~ /^200/
|
||||
Cache.put(key, response.body, expiry)
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def init_cookies
|
||||
unless @current_user.is_anonymous?
|
||||
if @current_user.has_mail?
|
||||
cookies["has_mail"] = "1"
|
||||
else
|
||||
cookies["has_mail"] = "0"
|
||||
end
|
||||
|
||||
if @current_user.is_privileged_or_higher? && ForumPost.updated?(@current_user)
|
||||
cookies["forum_updated"] = "1"
|
||||
else
|
||||
cookies["forum_updated"] = "0"
|
||||
end
|
||||
|
||||
if @current_user.is_privileged_or_higher? && Comment.updated?(@current_user)
|
||||
cookies["comments_updated"] = "1"
|
||||
else
|
||||
cookies["comments_updated"] = "0"
|
||||
end
|
||||
|
||||
if @current_user.is_blocked?
|
||||
if @current_user.ban
|
||||
cookies["block_reason"] = "You have been blocked. Reason: #{@current_user.ban.reason}. Expires: #{@current_user.ban.expires_at.strftime('%Y-%m-%d')}"
|
||||
else
|
||||
cookies["block_reason"] = "You have been blocked."
|
||||
end
|
||||
else
|
||||
cookies["block_reason"] = ""
|
||||
end
|
||||
|
||||
if @current_user.always_resize_images?
|
||||
cookies["resize_image"] = "1"
|
||||
else
|
||||
cookies["resize_image"] = "0"
|
||||
end
|
||||
|
||||
cookies["my_tags"] = @current_user.my_tags
|
||||
cookies["blacklisted_tags"] = @current_user.blacklisted_tags_array
|
||||
cookies["held_post_count"] = @current_user.held_post_count.to_s
|
||||
else
|
||||
cookies["blacklisted_tags"] = CONFIG["default_blacklists"]
|
||||
end
|
||||
|
||||
if flash[:notice] then
|
||||
cookies["notice"] = flash[:notice]
|
||||
end
|
||||
end
|
||||
end
|
103
app/controllers/artist_controller.rb
Normal file
103
app/controllers/artist_controller.rb
Normal file
@ -0,0 +1,103 @@
|
||||
class ArtistController < ApplicationController
|
||||
layout "default"
|
||||
|
||||
before_filter :post_member_only, :only => [:create, :update]
|
||||
before_filter :post_privileged_only, :only => [:destroy]
|
||||
helper :post, :wiki
|
||||
|
||||
def preview
|
||||
render :inline => "<h4>Preview</h4><%= format_text(params[:artist][:notes]) %>"
|
||||
end
|
||||
|
||||
def destroy
|
||||
@artist = Artist.find(params[:id])
|
||||
|
||||
if request.post?
|
||||
if params[:commit] == "Yes"
|
||||
@artist.destroy
|
||||
respond_to_success("Artist deleted", :action => "index", :page => params[:page])
|
||||
else
|
||||
redirect_to :action => "index", :page => params[:page]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if request.post?
|
||||
if params[:commit] == "Cancel"
|
||||
redirect_to :action => "show", :id => params[:id]
|
||||
return
|
||||
end
|
||||
|
||||
artist = Artist.find(params[:id])
|
||||
artist.update_attributes(params[:artist].merge(:updater_ip_addr => request.remote_ip, :updater_id => @current_user.id))
|
||||
|
||||
if artist.errors.empty?
|
||||
respond_to_success("Artist updated", :action => "show", :id => artist.id)
|
||||
else
|
||||
respond_to_error(artist, :action => "update", :id => artist.id)
|
||||
end
|
||||
else
|
||||
@artist = Artist.find(params["id"])
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
if request.post?
|
||||
artist = Artist.create(params[:artist].merge(:updater_ip_addr => request.remote_ip, :updater_id => @current_user.id))
|
||||
|
||||
if artist.errors.empty?
|
||||
respond_to_success("Artist created", :action => "show", :id => artist.id)
|
||||
else
|
||||
respond_to_error(artist, :action => "create", :alias_id => params[:alias_id])
|
||||
end
|
||||
else
|
||||
@artist = Artist.new
|
||||
|
||||
if params[:name]
|
||||
@artist.name = params[:name]
|
||||
|
||||
post = Post.find(:first, :conditions => ["id IN (SELECT post_id FROM posts_tags WHERE tag_id = (SELECT id FROM tags WHERE name = ?)) AND source LIKE 'http%'", params[:name]])
|
||||
unless post == nil || post.source.blank?
|
||||
@artist.urls = post.source
|
||||
end
|
||||
end
|
||||
|
||||
if params[:alias_id]
|
||||
@artist.alias_id = params[:alias_id]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
if params[:name]
|
||||
@artists = Artist.paginate Artist.generate_sql(params[:name]).merge(:per_page => 50, :page => params[:page], :order => "name")
|
||||
elsif params[:url]
|
||||
@artists = Artist.paginate Artist.generate_sql(params[:url]).merge(:per_page => 50, :page => params[:page], :order => "name")
|
||||
else
|
||||
if params[:order] == "date"
|
||||
order = "updated_at DESC"
|
||||
else
|
||||
order = "name"
|
||||
end
|
||||
|
||||
@artists = Artist.paginate :order => order, :per_page => 25, :page => params[:page]
|
||||
end
|
||||
|
||||
respond_to_list("artists")
|
||||
end
|
||||
|
||||
def show
|
||||
if params[:name]
|
||||
@artist = Artist.find_by_name(params[:name])
|
||||
else
|
||||
@artist = Artist.find(params[:id])
|
||||
end
|
||||
|
||||
if @artist.nil?
|
||||
redirect_to :action => "create", :name => params[:name]
|
||||
else
|
||||
redirect_to :controller => "wiki", :action => "show", :title => @artist.name
|
||||
end
|
||||
end
|
||||
end
|
11
app/controllers/banned_controller.rb
Normal file
11
app/controllers/banned_controller.rb
Normal file
@ -0,0 +1,11 @@
|
||||
class BannedController < ApplicationController
|
||||
layout 'bare'
|
||||
def index
|
||||
@ban = get_ip_ban()
|
||||
if not @ban
|
||||
redirect_to :controller => "static", :action => "index"
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
end
|
31
app/controllers/blocks_controller.rb
Normal file
31
app/controllers/blocks_controller.rb
Normal file
@ -0,0 +1,31 @@
|
||||
class CanNotBanSelf < Exception
|
||||
end
|
||||
|
||||
class BlocksController < ApplicationController
|
||||
before_filter :mod_only, :only => [:block_ip, :unblock_ip]
|
||||
|
||||
def block_ip
|
||||
if request.post?
|
||||
begin
|
||||
IpBans.transaction do
|
||||
ban = IpBans.create(params[:ban].merge(:banned_by => @current_user.id))
|
||||
if IpBans.find(:first, :conditions => ["id = ? and inet ? <<= ip_addr", ban.id, request.remote_ip]) then
|
||||
raise CanNotBanSelf
|
||||
end
|
||||
end
|
||||
rescue CanNotBanSelf => e
|
||||
flash[:notice] = "You can not ban yourself"
|
||||
end
|
||||
redirect_to :controller => "user", :action => "show_blocked_users"
|
||||
end
|
||||
end
|
||||
|
||||
def unblock_ip
|
||||
params[:ip_ban].keys.each do |ban_id|
|
||||
IpBans.destroy_all(["id = ?", ban_id])
|
||||
end
|
||||
|
||||
redirect_to :controller => "user", :action => "show_blocked_users"
|
||||
end
|
||||
end
|
||||
|
128
app/controllers/comment_controller.rb
Normal file
128
app/controllers/comment_controller.rb
Normal file
@ -0,0 +1,128 @@
|
||||
class CommentController < ApplicationController
|
||||
layout "default"
|
||||
helper :avatar
|
||||
|
||||
verify :method => :post, :only => [:create, :destroy, :update, :mark_as_spam]
|
||||
before_filter :member_only, :only => [:create, :destroy, :update]
|
||||
before_filter :janitor_only, :only => [:moderate]
|
||||
helper :post
|
||||
|
||||
def edit
|
||||
@comment = Comment.find(params[:id])
|
||||
end
|
||||
|
||||
def update
|
||||
comment = Comment.find(params[:id])
|
||||
if @current_user.has_permission?(comment)
|
||||
comment.update_attributes(params[:comment])
|
||||
respond_to_success("Comment updated", {:action => "index"})
|
||||
else
|
||||
access_denied()
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
comment = Comment.find(params[:id])
|
||||
if @current_user.has_permission?(comment)
|
||||
comment.destroy
|
||||
respond_to_success("Comment deleted", :controller => "post", :action => "show", :id => comment.post_id)
|
||||
else
|
||||
access_denied()
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
if @current_user.is_member_or_lower? && params[:commit] == "Post" && Comment.count(:conditions => ["user_id = ? AND created_at > ?", @current_user.id, 1.hour.ago]) >= CONFIG["member_comment_limit"]
|
||||
# TODO: move this to the model
|
||||
respond_to_error("Hourly limit exceeded", {:action => "index"}, :status => 421)
|
||||
return
|
||||
end
|
||||
|
||||
user_id = session[:user_id]
|
||||
|
||||
comment = Comment.new(params[:comment].merge(:ip_addr => request.remote_ip, :user_id => user_id))
|
||||
if params[:commit] == "Post without bumping"
|
||||
comment.do_not_bump_post = true
|
||||
end
|
||||
|
||||
if comment.save
|
||||
respond_to_success("Comment created", :action => "index")
|
||||
else
|
||||
respond_to_error(comment, :action => "index")
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
set_title "Comment"
|
||||
@comment = Comment.find(params[:id])
|
||||
|
||||
respond_to_list("comment")
|
||||
end
|
||||
|
||||
def index
|
||||
set_title "Comments"
|
||||
|
||||
if params[:format] == "json" || params[:format] == "xml"
|
||||
@comments = Comment.paginate(Comment.generate_sql(params).merge(:per_page => 25, :page => params[:page], :order => "id DESC"))
|
||||
respond_to_list("comments")
|
||||
else
|
||||
@posts = Post.paginate :order => "last_commented_at DESC", :conditions => "last_commented_at IS NOT NULL", :per_page => 10, :page => params[:page]
|
||||
|
||||
comments = []
|
||||
@posts.each { |post| comments.push(*post.recent_comments) }
|
||||
|
||||
newest_comment = comments.max {|a,b| a.created_at <=> b.created_at }
|
||||
if !@current_user.is_anonymous? && newest_comment && @current_user.last_comment_read_at < newest_comment.created_at
|
||||
@current_user.update_attribute(:last_comment_read_at, newest_comment.created_at)
|
||||
end
|
||||
|
||||
@posts = @posts.select {|x| x.can_be_seen_by?(@current_user, {:show_deleted => true})}
|
||||
end
|
||||
|
||||
@votes = {}
|
||||
if !@current_user.is_anonymous?
|
||||
@posts.each { |post|
|
||||
vote = PostVotes.find_by_ids(@current_user.id, post.id)
|
||||
@votes[post.id] = vote.score rescue 0
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def search
|
||||
options = { :order => "id desc", :per_page => 25, :page => params[:page] }
|
||||
if params[:query]
|
||||
query = params[:query].scan(/\S+/).join(" & ")
|
||||
options[:conditions] = ["text_search_index @@ plainto_tsquery(?)", query]
|
||||
end
|
||||
@comments = Comment.paginate options
|
||||
end
|
||||
|
||||
def moderate
|
||||
set_title "Moderate Comments"
|
||||
|
||||
if request.post?
|
||||
ids = params["c"].keys
|
||||
coms = Comment.find(:all, :conditions => ["id IN (?)", ids])
|
||||
|
||||
if params["commit"] == "Delete"
|
||||
coms.each do |c|
|
||||
c.destroy
|
||||
end
|
||||
elsif params["commit"] == "Approve"
|
||||
coms.each do |c|
|
||||
c.update_attribute(:is_spam, false)
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to :action => "moderate"
|
||||
else
|
||||
@comments = Comment.find(:all, :conditions => "is_spam = TRUE", :order => "id DESC")
|
||||
end
|
||||
end
|
||||
|
||||
def mark_as_spam
|
||||
@comment = Comment.find(params[:id])
|
||||
@comment.update_attributes(:is_spam => true)
|
||||
respond_to_success("Comment marked as spam", :action => "index")
|
||||
end
|
||||
end
|
63
app/controllers/dmail_controller.rb
Normal file
63
app/controllers/dmail_controller.rb
Normal file
@ -0,0 +1,63 @@
|
||||
class DmailController < ApplicationController
|
||||
before_filter :blocked_only
|
||||
layout "default"
|
||||
|
||||
def auto_complete_for_dmail_to_name
|
||||
@users = User.find(:all, :order => "lower(name)", :conditions => ["name ilike ? escape '\\\\'", params[:dmail][:to_name] + "%"])
|
||||
render :layout => false, :text => "<ul>" + @users.map {|x| "<li>" + x.name + "</li>"}.join("") + "</ul>"
|
||||
end
|
||||
|
||||
def show_previous_messages
|
||||
@dmails = Dmail.find(:all, :conditions => ["(to_id = ? or from_id = ?) and parent_id = ? and id < ?", @current_user.id, @current_user.id, params[:parent_id], params[:id]], :order => "id asc")
|
||||
render :layout => false
|
||||
end
|
||||
|
||||
def compose
|
||||
@dmail = Dmail.new
|
||||
end
|
||||
|
||||
def create
|
||||
@dmail = Dmail.create(params[:dmail].merge(:from_id => @current_user.id))
|
||||
|
||||
if @dmail.errors.empty?
|
||||
flash[:notice] = "Message sent to #{params[:dmail][:to_name]}"
|
||||
redirect_to :action => "inbox"
|
||||
else
|
||||
flash[:notice] = "Error: " + @dmail.errors.full_messages.join(", ")
|
||||
render :action => "compose"
|
||||
end
|
||||
end
|
||||
|
||||
def inbox
|
||||
@dmails = Dmail.paginate :conditions => ["to_id = ? or from_id = ?", @current_user.id, @current_user.id], :order => "created_at desc", :per_page => 25, :page => params[:page]
|
||||
end
|
||||
|
||||
def show
|
||||
@dmail = Dmail.find(params[:id])
|
||||
|
||||
if @dmail.to_id != @current_user.id && @dmail.from_id != @current_user.id
|
||||
flash[:notice] = "Access denied"
|
||||
redirect_to :controller => "user", :action => "login"
|
||||
return
|
||||
end
|
||||
|
||||
if @dmail.to_id == @current_user.id
|
||||
@dmail.mark_as_read!(@current_user)
|
||||
end
|
||||
end
|
||||
|
||||
def mark_all_read
|
||||
if request.post?
|
||||
if params[:commit] == "Yes"
|
||||
Dmail.find(:all, :conditions => ["to_id = ? and has_seen = false", @current_user.id]).each do |dmail|
|
||||
dmail.update_attribute(:has_seen, true)
|
||||
end
|
||||
|
||||
@current_user.update_attribute(:has_mail, false)
|
||||
respond_to_success("All messages marked as read", {:action => "inbox"})
|
||||
else
|
||||
redirect_to :action => "inbox"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
20
app/controllers/favorite_controller.rb
Normal file
20
app/controllers/favorite_controller.rb
Normal file
@ -0,0 +1,20 @@
|
||||
class FavoriteController < ApplicationController
|
||||
layout "default"
|
||||
before_filter :blocked_only, :only => [:create, :destroy]
|
||||
verify :method => :post, :only => [:create, :destroy]
|
||||
helper :post
|
||||
|
||||
def list_users
|
||||
@post = Post.find(params[:id])
|
||||
respond_to do |fmt|
|
||||
fmt.json do
|
||||
render :json => {:favorited_users => @post.favorited_by.map(&:name).join(",")}.to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def favorited_users_for_post(post)
|
||||
post.favorited_by.map {|x| x.name}.uniq.join(",")
|
||||
end
|
||||
end
|
150
app/controllers/forum_controller.rb
Normal file
150
app/controllers/forum_controller.rb
Normal file
@ -0,0 +1,150 @@
|
||||
class ForumController < ApplicationController
|
||||
layout "default"
|
||||
helper :avatar
|
||||
verify :method => :post, :only => [:create, :destroy, :update, :stick, :unstick, :lock, :unlock]
|
||||
before_filter :mod_only, :only => [:stick, :unstick, :lock, :unlock]
|
||||
before_filter :member_only, :only => [:destroy, :update, :edit, :add, :mark_all_read]
|
||||
before_filter :post_member_only, :only => [:create]
|
||||
|
||||
def stick
|
||||
ForumPost.stick!(params[:id])
|
||||
flash[:notice] = "Topic stickied"
|
||||
redirect_to :action => "show", :id => params[:id]
|
||||
end
|
||||
|
||||
def unstick
|
||||
ForumPost.unstick!(params[:id])
|
||||
flash[:notice] = "Topic unstickied"
|
||||
redirect_to :action => "show", :id => params[:id]
|
||||
end
|
||||
|
||||
def preview
|
||||
render :inline => "<h4>Preview</h4><%= format_text(params[:forum_post][:body]) %>"
|
||||
end
|
||||
|
||||
def new
|
||||
@forum_post = ForumPost.new
|
||||
|
||||
if params[:type] == "alias"
|
||||
@forum_post.title = "Tag Alias: "
|
||||
@forum_post.body = "Aliasing ___ to ___.\n\nReason: "
|
||||
elsif params[:type] == "impl"
|
||||
@forum_post.title = "Tag Implication: "
|
||||
@forum_post.body = "Implicating ___ to ___.\n\nReason: "
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
@forum_post = ForumPost.create(params[:forum_post].merge(:creator_id => session[:user_id]))
|
||||
|
||||
if @forum_post.errors.empty?
|
||||
if params[:forum_post][:parent_id].to_i == 0
|
||||
flash[:notice] = "Forum topic created"
|
||||
redirect_to :action => "show", :id => @forum_post.root_id
|
||||
else
|
||||
flash[:notice] = "Response posted"
|
||||
redirect_to :action => "show", :id => @forum_post.root_id, :page => (@forum_post.root.response_count / 30.0).ceil
|
||||
end
|
||||
else
|
||||
render_error(@forum_post)
|
||||
end
|
||||
end
|
||||
|
||||
def add
|
||||
end
|
||||
|
||||
def destroy
|
||||
@forum_post = ForumPost.find(params[:id])
|
||||
|
||||
if @current_user.has_permission?(@forum_post, :creator_id)
|
||||
@forum_post.destroy
|
||||
flash[:notice] = "Post destroyed"
|
||||
|
||||
if @forum_post.is_parent?
|
||||
redirect_to :action => "index"
|
||||
else
|
||||
redirect_to :action => "show", :id => @forum_post.root_id
|
||||
end
|
||||
else
|
||||
flash[:notice] = "Access denied"
|
||||
redirect_to :action => "show", :id => @forum_post.root_id
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@forum_post = ForumPost.find(params[:id])
|
||||
|
||||
if !@current_user.has_permission?(@forum_post, :creator_id)
|
||||
access_denied()
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@forum_post = ForumPost.find(params[:id])
|
||||
|
||||
if !@current_user.has_permission?(@forum_post, :creator_id)
|
||||
access_denied()
|
||||
return
|
||||
end
|
||||
|
||||
@forum_post.attributes = params[:forum_post]
|
||||
if @forum_post.save
|
||||
flash[:notice] = "Post updated"
|
||||
redirect_to :action => "show", :id => @forum_post.root_id, :page => (@forum_post.root.response_count / 30.0).ceil
|
||||
else
|
||||
render_error(@forum_post)
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@forum_post = ForumPost.find(params[:id])
|
||||
set_title @forum_post.title
|
||||
@children = ForumPost.paginate :order => "id", :per_page => 30, :conditions => ["parent_id = ?", params[:id]], :page => params[:page]
|
||||
|
||||
if !@current_user.is_anonymous? && @current_user.last_forum_topic_read_at < @forum_post.updated_at && @forum_post.updated_at < 3.seconds.ago
|
||||
@current_user.update_attribute(:last_forum_topic_read_at, @forum_post.updated_at)
|
||||
end
|
||||
|
||||
respond_to_list("forum_post")
|
||||
end
|
||||
|
||||
def index
|
||||
set_title CONFIG["app_name"] + " Forum"
|
||||
|
||||
if params[:parent_id]
|
||||
@forum_posts = ForumPost.paginate :order => "is_sticky desc, updated_at DESC", :per_page => 100, :conditions => ["parent_id = ?", params[:parent_id]], :page => params[:page]
|
||||
else
|
||||
@forum_posts = ForumPost.paginate :order => "is_sticky desc, updated_at DESC", :per_page => 30, :conditions => "parent_id IS NULL", :page => params[:page]
|
||||
end
|
||||
|
||||
respond_to_list("forum_posts")
|
||||
end
|
||||
|
||||
def search
|
||||
if params[:query]
|
||||
query = params[:query].scan(/\S+/).join(" & ")
|
||||
@forum_posts = ForumPost.paginate :order => "id desc", :per_page => 30, :conditions => ["text_search_index @@ plainto_tsquery(?)", query], :page => params[:page]
|
||||
else
|
||||
@forum_posts = ForumPost.paginate :order => "id desc", :per_page => 30, :page => params[:page]
|
||||
end
|
||||
|
||||
respond_to_list("forum_posts")
|
||||
end
|
||||
|
||||
def lock
|
||||
ForumPost.lock!(params[:id])
|
||||
flash[:notice] = "Topic locked"
|
||||
redirect_to :action => "show", :id => params[:id]
|
||||
end
|
||||
|
||||
def unlock
|
||||
ForumPost.unlock!(params[:id])
|
||||
flash[:notice] = "Topic unlocked"
|
||||
redirect_to :action => "show", :id => params[:id]
|
||||
end
|
||||
|
||||
def mark_all_read
|
||||
@current_user.update_attribute(:last_forum_topic_read_at, Time.now)
|
||||
render :nothing => true
|
||||
end
|
||||
end
|
3
app/controllers/help_controller.rb
Normal file
3
app/controllers/help_controller.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class HelpController < ApplicationController
|
||||
layout "default"
|
||||
end
|
129
app/controllers/history_controller.rb
Normal file
129
app/controllers/history_controller.rb
Normal file
@ -0,0 +1,129 @@
|
||||
class HistoryController < ApplicationController
|
||||
layout 'default'
|
||||
# before_filter :member_only
|
||||
verify :method => :post, :only => [:undo]
|
||||
|
||||
def index
|
||||
@params = params
|
||||
if params[:action] == "index"
|
||||
@type = "all"
|
||||
else
|
||||
@type = params[:action].pluralize
|
||||
end
|
||||
|
||||
conds = []
|
||||
cond_params = []
|
||||
# :specific_history => showing only one history
|
||||
# :specific_table => showing changes only for a particular table
|
||||
# :show_all_tags => don't omit post tags that didn't change
|
||||
@options = {
|
||||
:show_all_tags => @params[:show_all_tags] == "1"
|
||||
}
|
||||
if params[:search]
|
||||
# If a search specifies a table name, it overrides the action.
|
||||
search_type, param =
|
||||
if params[:search] =~ /^(.+?):(.*)/
|
||||
search_type = $1
|
||||
param = $2
|
||||
|
||||
if search_type == "user"
|
||||
user = User.find_by_name(param)
|
||||
if user
|
||||
conds << "histories.user_id = ?"
|
||||
cond_params << user.id
|
||||
else
|
||||
conds << "false"
|
||||
end
|
||||
elsif search_type == "change"
|
||||
@type = "all"
|
||||
@options[:specific_history] = true
|
||||
conds << "histories.id = ?"
|
||||
cond_params << param.to_i
|
||||
|
||||
set_type_to_result = true
|
||||
else
|
||||
@options[:specific_table] = true
|
||||
@type = search_type.pluralize
|
||||
conds << "histories.group_by_id = ?"
|
||||
cond_params << param.to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if @type != "all"
|
||||
conds << "histories.group_by_table = ?"
|
||||
cond_params << @type
|
||||
end
|
||||
|
||||
@options[:show_name] = false
|
||||
if @type != "all"
|
||||
begin
|
||||
obj = Object.const_get(@type.classify)
|
||||
@options[:show_name] = obj.method_defined?("pretty_name")
|
||||
rescue NameError => e
|
||||
end
|
||||
end
|
||||
|
||||
@changes = History.paginate(History.generate_sql(params).merge(
|
||||
:order => "id DESC", :per_page => 20, :select => "*", :page => params[:page],
|
||||
:conditions => [conds.join(" AND "), *cond_params],
|
||||
:include => [:history_changes]
|
||||
))
|
||||
|
||||
# If we're searching for a specific change, force the display to the
|
||||
# type of the change we found.
|
||||
if set_type_to_result && !@changes.empty?
|
||||
@type = @changes.first.group_by_table.pluralize
|
||||
end
|
||||
|
||||
render :action => :index
|
||||
end
|
||||
|
||||
alias_method :post, :index
|
||||
alias_method :pool, :index
|
||||
alias_method :tag, :index
|
||||
|
||||
def undo
|
||||
ids = params[:id].split(/,/)
|
||||
|
||||
@changes = []
|
||||
ids.each do |id|
|
||||
@changes += HistoryChange.find(:all, :conditions => ["id = ?", id])
|
||||
end
|
||||
|
||||
histories = {}
|
||||
total_histories = 0
|
||||
@changes.each { |change|
|
||||
next if histories[change.history_id]
|
||||
histories[change.history_id] = true
|
||||
total_histories += 1
|
||||
}
|
||||
|
||||
if total_histories > 1 && !@current_user.is_privileged_or_higher?
|
||||
respond_to_error("Only privileged users can undo more than one change at once", :status => 403)
|
||||
return
|
||||
end
|
||||
|
||||
errors = {}
|
||||
History.undo(@changes, @current_user, params[:redo] == "1", errors)
|
||||
|
||||
error_texts = []
|
||||
successful = 0
|
||||
failed = 0
|
||||
@changes.each { |change|
|
||||
if not errors[change]
|
||||
successful += 1
|
||||
next
|
||||
end
|
||||
failed += 1
|
||||
|
||||
case errors[change]
|
||||
when :denied
|
||||
error_texts << "Some changes were not made because you do not have access to make them."
|
||||
end
|
||||
}
|
||||
error_texts.uniq!
|
||||
|
||||
respond_to_success("Changes made.", {:action => "index"}, :api => {:successful=>successful, :failed=>failed, :errors=>error_texts})
|
||||
end
|
||||
end
|
167
app/controllers/inline_controller.rb
Normal file
167
app/controllers/inline_controller.rb
Normal file
@ -0,0 +1,167 @@
|
||||
class InlineController < ApplicationController
|
||||
layout "default"
|
||||
before_filter :member_only, :only => [:create, :copy]
|
||||
verify :method => :post, :only => [:delete_image, :delete, :update, :copy]
|
||||
|
||||
def create
|
||||
# If this user already has an inline with no images, use it.
|
||||
inline = Inline.find(:first, :conditions => ["(SELECT count(*) FROM inline_images WHERE inline_images.inline_id = inlines.id) = 0 AND user_id = ?", @current_user.id])
|
||||
inline ||= Inline.create(:user_id => @current_user.id)
|
||||
redirect_to :action => "edit", :id => inline.id
|
||||
end
|
||||
|
||||
def index
|
||||
order = []
|
||||
if not @current_user.is_anonymous?
|
||||
order << ["user_id = #{@current_user.id} DESC"]
|
||||
end
|
||||
order << ["created_at desc"]
|
||||
|
||||
options = {
|
||||
:per_page => 20,
|
||||
:page => params[:page],
|
||||
# Mods can view all inlines; sort the user's own inlines first.
|
||||
:order => order.join(", ")
|
||||
}
|
||||
|
||||
@inlines = Inline.paginate options
|
||||
|
||||
respond_to_list("inlines")
|
||||
end
|
||||
|
||||
def delete
|
||||
@inline = Inline.find_by_id(params[:id])
|
||||
if @inline.nil?
|
||||
redirect_to "/404"
|
||||
return
|
||||
end
|
||||
|
||||
unless @current_user.has_permission?(@inline)
|
||||
access_denied()
|
||||
return
|
||||
end
|
||||
|
||||
@inline.destroy
|
||||
respond_to_success("Image group deleted", :action => "index")
|
||||
end
|
||||
|
||||
def add_image
|
||||
@inline = Inline.find_by_id(params[:id])
|
||||
if @inline.nil?
|
||||
redirect_to "/404"
|
||||
return
|
||||
end
|
||||
|
||||
unless @current_user.has_permission?(@inline)
|
||||
access_denied()
|
||||
return
|
||||
end
|
||||
|
||||
if request.post?
|
||||
new_image = InlineImage.create(params[:image].merge(:inline_id => @inline.id))
|
||||
if not new_image.errors.empty?
|
||||
respond_to_error(new_image, :action => "edit", :id => @inline.id)
|
||||
return
|
||||
end
|
||||
|
||||
redirect_to :action => "edit", :id => @inline.id
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
def delete_image
|
||||
image = InlineImage.find_by_id(params[:id])
|
||||
if image.nil?
|
||||
redirect_to "/404"
|
||||
return
|
||||
end
|
||||
|
||||
inline = image.inline
|
||||
unless @current_user.has_permission?(inline)
|
||||
access_denied()
|
||||
return
|
||||
end
|
||||
|
||||
image.destroy
|
||||
redirect_to :action => "edit", :id => inline.id
|
||||
end
|
||||
|
||||
def update
|
||||
inline = Inline.find_by_id(params[:id].to_i)
|
||||
if inline.nil?
|
||||
redirect_to "/404"
|
||||
return
|
||||
end
|
||||
unless @current_user.has_permission?(inline)
|
||||
access_denied()
|
||||
return
|
||||
end
|
||||
|
||||
inline.update_attributes(params[:inline])
|
||||
params[:image] ||= []
|
||||
params[:image].each do |id, p|
|
||||
image = InlineImage.find(:first, :conditions => ["id = ? AND inline_id = ?", id, inline.id])
|
||||
image.update_attributes(:description => p[:description]) if p[:description]
|
||||
image.update_attributes(:sequence => p[:sequence]) if p[:sequence]
|
||||
end
|
||||
|
||||
inline.reload
|
||||
inline.renumber_sequences
|
||||
|
||||
flash[:notice] = "Image updated"
|
||||
redirect_to :action => "edit", :id => inline.id
|
||||
end
|
||||
|
||||
# Create a copy of an inline image and all of its images. Allow copying from images
|
||||
# owned by someone else.
|
||||
def copy
|
||||
inline = Inline.find_by_id(params[:id])
|
||||
if inline.nil?
|
||||
redirect_to "/404"
|
||||
return
|
||||
end
|
||||
|
||||
new_inline = Inline.create(:user_id => @current_user.id)
|
||||
new_inline.update_attributes(:description => inline.description)
|
||||
|
||||
for image in inline.inline_images do
|
||||
new_attributes = image.attributes.merge(:inline_id => new_inline.id)
|
||||
new_attributes.delete(:id)
|
||||
new_image = InlineImage.create(new_attributes)
|
||||
new_image.save!
|
||||
end
|
||||
|
||||
respond_to_success("Image copied", :action => "edit", :id => new_inline.id)
|
||||
end
|
||||
|
||||
def edit
|
||||
@inline = Inline.find_by_id(params[:id])
|
||||
if @inline.nil?
|
||||
redirect_to "/404"
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
def crop
|
||||
@inline = Inline.find_by_id(params[:id])
|
||||
@image = @inline.inline_images[0] rescue nil
|
||||
if @inline.nil? || @image.nil?
|
||||
redirect_to "/404"
|
||||
return
|
||||
end
|
||||
unless @current_user.has_permission?(@inline)
|
||||
access_denied()
|
||||
return
|
||||
end
|
||||
|
||||
if request.post?
|
||||
if @inline.crop(params) then
|
||||
redirect_to :action => "edit", :id => @inline.id
|
||||
else
|
||||
respond_to_error(@inline, :action => "edit", :id => @inline.id)
|
||||
end
|
||||
end
|
||||
|
||||
@params = params
|
||||
end
|
||||
end
|
24
app/controllers/job_task_controller.rb
Normal file
24
app/controllers/job_task_controller.rb
Normal file
@ -0,0 +1,24 @@
|
||||
class JobTaskController < ApplicationController
|
||||
layout "default"
|
||||
|
||||
def index
|
||||
@job_tasks = JobTask.paginate(:per_page => 25, :order => "id DESC", :page => params[:page])
|
||||
end
|
||||
|
||||
def show
|
||||
@job_task = JobTask.find(params[:id])
|
||||
|
||||
if @job_task.task_type == "upload_post" && @job_task.status == "finished"
|
||||
redirect_to :controller => "post", :action => "show", :id => @job_task.status_message
|
||||
end
|
||||
end
|
||||
|
||||
def retry
|
||||
@job_task = JobTask.find(params[:id])
|
||||
|
||||
if request.post?
|
||||
@job_task.update_attributes(:status => "pending", :status_message => "")
|
||||
redirect_to :action => "show", :id => @job_task.id
|
||||
end
|
||||
end
|
||||
end
|
89
app/controllers/note_controller.rb
Normal file
89
app/controllers/note_controller.rb
Normal file
@ -0,0 +1,89 @@
|
||||
class NoteController < ApplicationController
|
||||
layout 'default', :only => [:index, :history, :search]
|
||||
before_filter :post_member_only, :only => [:destroy, :update, :revert]
|
||||
verify :method => :post, :only => [:update, :revert, :destroy]
|
||||
helper :post
|
||||
|
||||
def search
|
||||
if params[:query]
|
||||
query = params[:query].scan(/\S+/).join(" & ")
|
||||
@notes = Note.paginate :order => "id asc", :per_page => 25, :conditions => ["text_search_index @@ plainto_tsquery(?)", query], :page => params[:page]
|
||||
|
||||
respond_to_list("notes")
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
set_title "Notes"
|
||||
|
||||
if params[:post_id]
|
||||
@posts = Post.paginate :order => "last_noted_at DESC", :conditions => ["id = ?", params[:post_id]], :per_page => 100, :page => params[:page]
|
||||
else
|
||||
@posts = Post.paginate :order => "last_noted_at DESC", :conditions => "last_noted_at IS NOT NULL", :per_page => 16, :page => params[:page]
|
||||
end
|
||||
|
||||
respond_to do |fmt|
|
||||
fmt.html
|
||||
fmt.xml {render :xml => @posts.map {|x| x.notes}.flatten.to_xml(:root => "notes")}
|
||||
fmt.json {render :json => @posts.map {|x| x.notes}.flatten.to_json}
|
||||
end
|
||||
end
|
||||
|
||||
def history
|
||||
set_title "Note History"
|
||||
|
||||
if params[:id]
|
||||
@notes = NoteVersion.paginate(:page => params[:page], :per_page => 25, :order => "id DESC", :conditions => ["note_id = ?", params[:id]])
|
||||
elsif params[:post_id]
|
||||
@notes = NoteVersion.paginate(:page => params[:page], :per_page => 50, :order => "id DESC", :conditions => ["post_id = ?", params[:post_id]])
|
||||
elsif params[:user_id]
|
||||
@notes = NoteVersion.paginate(:page => params[:page], :per_page => 50, :order => "id DESC", :conditions => ["user_id = ?", params[:user_id]])
|
||||
else
|
||||
@notes = NoteVersion.paginate(:page => params[:page], :per_page => 25, :order => "id DESC")
|
||||
end
|
||||
|
||||
respond_to_list("notes")
|
||||
end
|
||||
|
||||
def revert
|
||||
note = Note.find(params[:id])
|
||||
|
||||
if note.is_locked?
|
||||
respond_to_error("Post is locked", {:action => "history", :id => note.id}, :status => 422)
|
||||
return
|
||||
end
|
||||
|
||||
note.revert_to(params[:version])
|
||||
note.ip_addr = request.remote_ip
|
||||
note.user_id = @current_user.id
|
||||
|
||||
if note.save
|
||||
respond_to_success("Note reverted", :action => "history", :id => note.id)
|
||||
else
|
||||
render_error(note)
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if params[:note][:post_id]
|
||||
note = Note.new(:post_id => params[:note][:post_id])
|
||||
else
|
||||
note = Note.find(params[:id])
|
||||
end
|
||||
|
||||
if note.is_locked?
|
||||
respond_to_error("Post is locked", {:controller => "post", :action => "show", :id => note.post_id}, :status => 422)
|
||||
return
|
||||
end
|
||||
|
||||
note.attributes = params[:note]
|
||||
note.user_id = @current_user.id
|
||||
note.ip_addr = request.remote_ip
|
||||
|
||||
if note.save
|
||||
respond_to_success("Note updated", {:action => "index"}, :api => {:new_id => note.id, :old_id => params[:id].to_i, :formatted_body => HTML5Sanitizer::hs(note.formatted_body)})
|
||||
else
|
||||
respond_to_error(note, :controller => "post", :action => "show", :id => note.post_id)
|
||||
end
|
||||
end
|
||||
end
|
251
app/controllers/pool_controller.rb
Normal file
251
app/controllers/pool_controller.rb
Normal file
@ -0,0 +1,251 @@
|
||||
class PoolController < ApplicationController
|
||||
layout "default"
|
||||
before_filter :member_only, :only => [:destroy, :update, :add_post, :remove_post, :import, :zip]
|
||||
before_filter :post_member_only, :only => [:create]
|
||||
helper :post
|
||||
|
||||
def index
|
||||
options = {
|
||||
:per_page => 20,
|
||||
:page => params[:page]
|
||||
}
|
||||
|
||||
case params[:order]
|
||||
when "name": options[:order] = "nat_sort(name) asc"
|
||||
when "date": options[:order] = "created_at desc"
|
||||
when "updated": options[:order] = "updated_at desc"
|
||||
when "date": options[:order] = "id desc"
|
||||
else
|
||||
if params.has_key?(:query)
|
||||
options[:order] = "nat_sort(name) asc"
|
||||
else
|
||||
options[:order] = "created_at desc"
|
||||
end
|
||||
end
|
||||
|
||||
if params[:query]
|
||||
options[:conditions] = ["lower(name) like ?", "%" + params[:query].to_escaped_for_sql_like + "%"]
|
||||
end
|
||||
|
||||
@pools = Pool.paginate options
|
||||
@samples = {}
|
||||
@pools.each { |p|
|
||||
post = p.get_sample
|
||||
if not post then next end
|
||||
@samples[p] = post
|
||||
}
|
||||
|
||||
respond_to_list("pools")
|
||||
end
|
||||
|
||||
def show
|
||||
if params[:samples] == "0" then params.delete(:samples) end
|
||||
if params[:originals] == "0" then params.delete(:originals) end
|
||||
|
||||
post_assoc = params[:originals] ? :pool_posts : :pool_parent_posts
|
||||
@pool = Pool.find(params[:id], :include => [post_assoc => :post])
|
||||
|
||||
# We have the Pool.pool_parent_posts association for this, but that doesn't seem to want to work...
|
||||
conds = ["pools_posts.pool_id = ?"]
|
||||
cond_params = params[:id]
|
||||
|
||||
if params[:originals]
|
||||
conds << "pools_posts.active = 't'"
|
||||
else
|
||||
conds << "((pools_posts.active = true AND pools_posts.slave_id IS NULL) OR pools_posts.master_id IS NOT NULL)"
|
||||
end
|
||||
|
||||
@posts = Post.paginate :per_page => 24, :order => "nat_sort(pools_posts.sequence), pools_posts.post_id", :joins => "JOIN pools_posts ON posts.id = pools_posts.post_id", :conditions => [conds.join(" AND "), *cond_params], :select => "posts.*", :page => params[:page]
|
||||
|
||||
set_title @pool.pretty_name
|
||||
respond_to do |fmt|
|
||||
fmt.html
|
||||
fmt.xml do
|
||||
builder = Builder::XmlMarkup.new(:indent => 2)
|
||||
builder.instruct!
|
||||
|
||||
xml = @pool.to_xml(:builder => builder, :skip_instruct => true) do
|
||||
builder.posts do
|
||||
@posts.each do |post|
|
||||
post.to_xml(:builder => builder, :skip_instruct => true)
|
||||
end
|
||||
end
|
||||
end
|
||||
render :xml => xml
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@pool = Pool.find(params[:id])
|
||||
|
||||
unless @pool.can_be_updated_by?(@current_user)
|
||||
access_denied()
|
||||
return
|
||||
end
|
||||
|
||||
if request.post?
|
||||
@pool.update_attributes(params[:pool])
|
||||
respond_to_success("Pool updated", :action => "show", :id => params[:id])
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
if request.post?
|
||||
@pool = Pool.create(params[:pool].merge(:user_id => @current_user.id))
|
||||
|
||||
if @pool.errors.empty?
|
||||
respond_to_success("Pool created", :action => "show", :id => @pool.id)
|
||||
else
|
||||
respond_to_error(@pool, :action => "index")
|
||||
end
|
||||
else
|
||||
@pool = Pool.new(:user_id => @current_user.id)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@pool = Pool.find(params[:id])
|
||||
|
||||
if request.post?
|
||||
if @pool.can_be_updated_by?(@current_user)
|
||||
@pool.destroy
|
||||
respond_to_success("Pool deleted", :action => "index")
|
||||
else
|
||||
access_denied()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def add_post
|
||||
if request.post?
|
||||
@pool = Pool.find(params[:pool_id])
|
||||
session[:last_pool_id] = @pool.id
|
||||
|
||||
if params[:pool] && !params[:pool][:sequence].blank?
|
||||
sequence = params[:pool][:sequence]
|
||||
else
|
||||
sequence = nil
|
||||
end
|
||||
|
||||
begin
|
||||
@pool.add_post(params[:post_id], :sequence => sequence, :user => @current_user)
|
||||
respond_to_success("Post added", :controller => "post", :action => "show", :id => params[:post_id])
|
||||
rescue Pool::PostAlreadyExistsError
|
||||
respond_to_error("Post already exists", {:controller => "post", :action => "show", :id => params[:post_id]}, :status => 423)
|
||||
rescue Pool::AccessDeniedError
|
||||
access_denied()
|
||||
rescue Exception => x
|
||||
respond_to_error(x.class, :controller => "post", :action => "show", :id => params[:post_id])
|
||||
end
|
||||
else
|
||||
if @current_user.is_anonymous?
|
||||
@pools = Pool.find(:all, :order => "name", :conditions => "is_active = TRUE AND is_public = TRUE")
|
||||
else
|
||||
@pools = Pool.find(:all, :order => "name", :conditions => ["is_active = TRUE AND (is_public = TRUE OR user_id = ?)", @current_user.id])
|
||||
end
|
||||
|
||||
@post = Post.find(params[:post_id])
|
||||
end
|
||||
end
|
||||
|
||||
def remove_post
|
||||
if request.post?
|
||||
@pool = Pool.find(params[:pool_id])
|
||||
|
||||
begin
|
||||
@pool.remove_post(params[:post_id], :user => @current_user)
|
||||
rescue Pool::AccessDeniedError
|
||||
access_denied()
|
||||
return
|
||||
end
|
||||
|
||||
response.headers["X-Post-Id"] = params[:post_id]
|
||||
respond_to_success("Post removed", :controller => "post", :action => "show", :id => params[:post_id])
|
||||
else
|
||||
@pool = Pool.find(params[:pool_id])
|
||||
@post = Post.find(params[:post_id])
|
||||
end
|
||||
end
|
||||
|
||||
def order
|
||||
@pool = Pool.find(params[:id])
|
||||
|
||||
unless @pool.can_be_updated_by?(@current_user)
|
||||
access_denied()
|
||||
return
|
||||
end
|
||||
|
||||
if request.post?
|
||||
PoolPost.transaction do
|
||||
params[:pool_post_sequence].each do |i, seq|
|
||||
PoolPost.update(i, :sequence => seq)
|
||||
end
|
||||
|
||||
@pool.reload
|
||||
@pool.update_pool_links
|
||||
end
|
||||
|
||||
flash[:notice] = "Ordering updated"
|
||||
redirect_to :action => "show", :id => params[:id]
|
||||
else
|
||||
@pool_posts = @pool.pool_posts
|
||||
end
|
||||
end
|
||||
|
||||
def import
|
||||
@pool = Pool.find(params[:id])
|
||||
|
||||
unless @pool.can_be_updated_by?(@current_user)
|
||||
access_denied()
|
||||
return
|
||||
end
|
||||
|
||||
if request.post?
|
||||
if params[:posts].is_a?(Hash)
|
||||
ordered_posts = params[:posts].sort { |a,b| a[1]<=>b[1] }.map { |a| a[0] }
|
||||
|
||||
PoolPost.transaction do
|
||||
ordered_posts.each do |post_id|
|
||||
begin
|
||||
@pool.add_post(post_id, :skip_update_pool_links => true)
|
||||
rescue Pool::PostAlreadyExistsError
|
||||
# ignore
|
||||
end
|
||||
end
|
||||
@pool.update_pool_links
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to :action => "show", :id => @pool.id
|
||||
else
|
||||
respond_to do |fmt|
|
||||
fmt.html
|
||||
fmt.js do
|
||||
@posts = Post.find_by_tags(params[:query], :order => "id desc", :limit => 500)
|
||||
@posts = @posts.select {|x| x.can_be_seen_by?(@current_user)}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def select
|
||||
if @current_user.is_anonymous?
|
||||
@pools = Pool.find(:all, :order => "name", :conditions => "is_active = TRUE AND is_public = TRUE")
|
||||
else
|
||||
@pools = Pool.find(:all, :order => "name", :conditions => ["is_active = TRUE AND (is_public = TRUE OR user_id = ?)", @current_user.id])
|
||||
end
|
||||
|
||||
render :layout => false
|
||||
end
|
||||
|
||||
# Generate a ZIP control file for lighttpd, and redirect to the ZIP.
|
||||
if CONFIG["pool_zips"]
|
||||
def zip
|
||||
post_assoc = params[:originals] ? :pool_posts : :pool_parent_posts
|
||||
pool = Pool.find(params[:id], :include => [post_assoc => :post])
|
||||
control_path = pool.get_zip_control_file_path(params)
|
||||
redirect_to pool.get_zip_url(control_path, params)
|
||||
end
|
||||
end
|
||||
end
|
721
app/controllers/post_controller.rb
Normal file
721
app/controllers/post_controller.rb
Normal file
@ -0,0 +1,721 @@
|
||||
require "download"
|
||||
|
||||
class PostController < ApplicationController
|
||||
layout 'default'
|
||||
helper :avatar
|
||||
|
||||
verify :method => :post, :only => [:update, :destroy, :create, :revert_tags, :vote, :flag], :redirect_to => {:action => :show, :id => lambda {|c| c.params[:id]}}
|
||||
|
||||
before_filter :member_only, :only => [:create, :destroy, :delete, :flag, :revert_tags, :activate, :update_batch]
|
||||
before_filter :post_member_only, :only => [:update, :upload, :flag]
|
||||
before_filter :janitor_only, :only => [:moderate, :undelete]
|
||||
after_filter :save_tags_to_cookie, :only => [:update, :create]
|
||||
if CONFIG["load_average_threshold"]
|
||||
before_filter :check_load_average, :only => [:index, :popular_by_day, :popular_by_week, :popular_by_month, :random, :atom, :piclens]
|
||||
end
|
||||
|
||||
if CONFIG["enable_caching"]
|
||||
around_filter :cache_action, :only => [:index, :atom, :piclens]
|
||||
end
|
||||
|
||||
helper :wiki, :tag, :comment, :pool, :favorite, :advertisement
|
||||
|
||||
def verify_action(options)
|
||||
redirect_to_proc = false
|
||||
|
||||
if options[:redirect_to] && options[:redirect_to][:id].is_a?(Proc)
|
||||
redirect_to_proc = options[:redirect_to][:id]
|
||||
options[:redirect_to][:id] = options[:redirect_to][:id].call(self)
|
||||
end
|
||||
|
||||
result = super(options)
|
||||
|
||||
if redirect_to_proc
|
||||
options[:redirect_to][:id] = redirect_to_proc
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
def activate
|
||||
ids = params[:post_ids].map { |id| id.to_i }
|
||||
changed = Post.batch_activate(@current_user.is_mod_or_higher? ? nil: @current_user.id, ids)
|
||||
respond_to_success("Posts activated", {:action => "moderate"}, :api => {:count => changed})
|
||||
end
|
||||
|
||||
def upload_problem
|
||||
end
|
||||
|
||||
def upload
|
||||
@deleted_posts = FlaggedPostDetail.new_deleted_posts(@current_user)
|
||||
# redirect_to :action => "upload_problem"
|
||||
# return
|
||||
|
||||
# if params[:url]
|
||||
# @post = Post.find(:first, :conditions => ["source = ?", params[:url]])
|
||||
# end
|
||||
|
||||
if @post.nil?
|
||||
@post = Post.new
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
# respond_to_error("Uploads temporarily disabled due to Amazon S3 issues", :action => "upload_problem")
|
||||
# return
|
||||
|
||||
if @current_user.is_member_or_lower? && Post.count(:conditions => ["user_id = ? AND created_at > ? ", @current_user.id, 1.day.ago]) >= CONFIG["member_post_limit"]
|
||||
respond_to_error("Daily limit exceeded", {:action => "index"}, :status => 421)
|
||||
return
|
||||
end
|
||||
|
||||
if @current_user.is_privileged_or_higher?
|
||||
status = "active"
|
||||
else
|
||||
status = "pending"
|
||||
end
|
||||
|
||||
@post = Post.create(params[:post].merge(:updater_user_id => @current_user.id, :updater_ip_addr => request.remote_ip, :user_id => @current_user.id, :ip_addr => request.remote_ip, :status => status))
|
||||
|
||||
if @post.errors.empty?
|
||||
if params[:md5] && @post.md5 != params[:md5].downcase
|
||||
@post.destroy
|
||||
respond_to_error("MD5 mismatch", {:action => "upload"}, :status => 420)
|
||||
else
|
||||
if CONFIG["dupe_check_on_upload"] && @post.image? && @post.parent_id.nil?
|
||||
if params[:format] == "xml" || params[:format] == "json"
|
||||
options = { :services => SimilarImages.get_services("local"), :type => :post, :source => @post }
|
||||
|
||||
res = SimilarImages.similar_images(options)
|
||||
if not res[:posts].empty?
|
||||
@post.tags = @post.tags + " possible_duplicate"
|
||||
@post.save!
|
||||
end
|
||||
end
|
||||
|
||||
respond_to_success("Post uploaded", {:controller => "post", :action => "similar", :id => @post.id, :initial => 1}, :api => {:post_id => @post.id, :location => url_for(:controller => "post", :action => "similar", :id => @post.id, :initial => 1)})
|
||||
else
|
||||
respond_to_success("Post uploaded", {:controller => "post", :action => "show", :id => @post.id, :tag_title => @post.tag_title}, :api => {:post_id => @post.id, :location => url_for(:controller => "post", :action => "show", :id => @post.id)})
|
||||
end
|
||||
end
|
||||
elsif @post.errors.invalid?(:md5)
|
||||
p = Post.find_by_md5(@post.md5)
|
||||
|
||||
update = { :tags => p.cached_tags + " " + params[:post][:tags], :updater_user_id => session[:user_id], :updater_ip_addr => request.remote_ip }
|
||||
update[:source] = @post.source if p.source.blank? && !@post.source.blank?
|
||||
p.update_attributes(update)
|
||||
|
||||
respond_to_error("Post already exists", {:controller => "post", :action => "show", :id => p.id, :tag_title => @post.tag_title}, :api => {:location => url_for(:controller => "post", :action => "show", :id => p.id)}, :status => 423)
|
||||
else
|
||||
respond_to_error(@post, :action => "upload")
|
||||
end
|
||||
end
|
||||
|
||||
def moderate
|
||||
if request.post?
|
||||
Post.transaction do
|
||||
if params[:ids]
|
||||
params[:ids].keys.each do |post_id|
|
||||
if params[:commit] == "Approve"
|
||||
post = Post.find(post_id)
|
||||
post.approve!(@current_user.id)
|
||||
elsif params[:commit] == "Delete"
|
||||
Post.destroy_with_reason(post_id, params[:reason] || params[:reason2], @current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if params[:commit] == "Approve"
|
||||
respond_to_success("Post approved", {:action => "moderate"})
|
||||
elsif params[:commit] == "Delete"
|
||||
respond_to_success("Post deleted", {:action => "moderate"})
|
||||
end
|
||||
else
|
||||
if params[:query]
|
||||
@pending_posts = Post.find_by_sql(Post.generate_sql(params[:query], :pending => true, :order => "id desc"))
|
||||
@flagged_posts = Post.find_by_sql(Post.generate_sql(params[:query], :flagged => true, :order => "id desc"))
|
||||
else
|
||||
@pending_posts = Post.find(:all, :conditions => "status = 'pending'", :order => "id desc")
|
||||
@flagged_posts= Post.find(:all, :conditions => "status = 'flagged'", :order => "id desc")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@post = Post.find(params[:id])
|
||||
user_id = @current_user.id
|
||||
|
||||
if @post.update_attributes(params[:post].merge(:updater_user_id => user_id, :updater_ip_addr => request.remote_ip))
|
||||
# Reload the post to send the new status back; not all changes will be reflected in
|
||||
# @post due to after_save changes.
|
||||
@post.reload
|
||||
respond_to_success("Post updated", {:action => "show", :id => @post.id, :tag_title => @post.tag_title}, :api => {:post => @post})
|
||||
else
|
||||
respond_to_error(@post, :action => "show", :id => params[:id])
|
||||
end
|
||||
end
|
||||
|
||||
def update_batch
|
||||
user_id = @current_user.id
|
||||
|
||||
ids = {}
|
||||
params["post"].each { |post|
|
||||
post_id = post[:id]
|
||||
post.delete(:id)
|
||||
@post = Post.find(post_id)
|
||||
ids[@post.id] = true
|
||||
|
||||
# If an entry has only an ID, it was just included in the list to receive changes to
|
||||
# a post without changing it (for example, to receive the parent's data after reparenting
|
||||
# a post under it).
|
||||
next if post.empty?
|
||||
|
||||
old_parent_id = @post.parent_id
|
||||
|
||||
if @post.update_attributes(post.merge(:updater_user_id => user_id, :updater_ip_addr => request.remote_ip))
|
||||
# Reload the post to send the new status back; not all changes will be reflected in
|
||||
# @post due to after_save changes.
|
||||
@post.reload
|
||||
end
|
||||
|
||||
if @post.parent_id != old_parent_id
|
||||
ids[@post.parent_id] = true if @post.parent_id
|
||||
ids[old_parent_id] = true if old_parent_id
|
||||
end
|
||||
}
|
||||
|
||||
# Updates to one post may affect others, so only generate the return list after we've already
|
||||
# updated everything.
|
||||
ret = Post.find_by_sql(["SELECT * FROM posts WHERE id IN (?)", ids.map { |id, t| id }])
|
||||
|
||||
respond_to_success("Post updated", {:action => "show", :id => @post.id, :tag_title => @post.tag_title}, :api => {:posts => ret})
|
||||
end
|
||||
|
||||
def delete
|
||||
@post = Post.find(params[:id])
|
||||
|
||||
if @post && @post.parent_id
|
||||
@post_parent = Post.find(@post.parent_id)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if params[:commit] == "Cancel"
|
||||
redirect_to :action => "show", :id => params[:id]
|
||||
return
|
||||
end
|
||||
|
||||
@post = Post.find(params[:id])
|
||||
|
||||
if @post.can_user_delete?(@current_user)
|
||||
if @post.status == "deleted"
|
||||
@post.delete_from_database
|
||||
else
|
||||
Post.destroy_with_reason(@post.id, params[:reason], @current_user)
|
||||
end
|
||||
|
||||
respond_to_success("Post deleted", :action => "index")
|
||||
else
|
||||
access_denied()
|
||||
end
|
||||
end
|
||||
|
||||
def deleted_index
|
||||
if !@current_user.is_anonymous? && params[:user_id] && params[:user_id].to_i == @current_user.id
|
||||
@current_user.update_attribute(:last_deleted_post_seen_at, Time.now)
|
||||
end
|
||||
|
||||
if params[:user_id]
|
||||
@posts = Post.paginate(:per_page => 25, :order => "flagged_post_details.created_at DESC", :joins => "JOIN flagged_post_details ON flagged_post_details.post_id = posts.id", :select => "flagged_post_details.reason, posts.cached_tags, posts.id, posts.user_id", :conditions => ["posts.status = 'deleted' AND posts.user_id = ? ", params[:user_id]], :page => params[:page])
|
||||
else
|
||||
@posts = Post.paginate(:per_page => 25, :order => "flagged_post_details.created_at DESC", :joins => "JOIN flagged_post_details ON flagged_post_details.post_id = posts.id", :select => "flagged_post_details.reason, posts.cached_tags, posts.id, posts.user_id", :conditions => ["posts.status = 'deleted'"], :page => params[:page])
|
||||
end
|
||||
end
|
||||
|
||||
def acknowledge_new_deleted_posts
|
||||
@current_user.update_attribute(:last_deleted_post_seen_at, Time.now) if !@current_user.is_anonymous?
|
||||
respond_to_success("Success", {})
|
||||
end
|
||||
|
||||
def index
|
||||
tags = params[:tags].to_s
|
||||
split_tags = QueryParser.parse(tags)
|
||||
page = params[:page].to_i
|
||||
|
||||
if @current_user.is_member_or_lower? && split_tags.size > 2
|
||||
respond_to_error("You can only search up to two tags at once with a basic account", :action => "index")
|
||||
return
|
||||
elsif split_tags.size > 6
|
||||
respond_to_error("You can only search up to six tags at once", :action => "index")
|
||||
return
|
||||
end
|
||||
|
||||
q = Tag.parse_query(tags)
|
||||
|
||||
limit = params[:limit].to_i if params.has_key?(:limit)
|
||||
limit ||= q[:limit].to_i if q.has_key?(:limit)
|
||||
limit ||= 16
|
||||
limit = 1000 if limit > 1000
|
||||
|
||||
count = 0
|
||||
|
||||
begin
|
||||
count = Post.fast_count(tags)
|
||||
rescue => x
|
||||
respond_to_error("Error: #{x}", :action => "index")
|
||||
return
|
||||
end
|
||||
|
||||
set_title "/" + tags.tr("_", " ")
|
||||
|
||||
if count < 16 && split_tags.size == 1
|
||||
@tag_suggestions = Tag.find_suggestions(tags)
|
||||
end
|
||||
|
||||
@ambiguous_tags = Tag.select_ambiguous(split_tags)
|
||||
|
||||
@posts = WillPaginate::Collection.new(page, limit, count)
|
||||
offset = @posts.offset
|
||||
posts_to_load = @posts.per_page * 2
|
||||
|
||||
# If we're not on the first page, load the previous page for prefetching. Prefetching
|
||||
# the previous page when the user is scanning forward should be free, since it'll already
|
||||
# be in cache, so this makes scanning the index from back to front as responsive as from
|
||||
# front to back.
|
||||
if page && page > 1 then
|
||||
offset -= @posts.per_page
|
||||
posts_to_load += @posts.per_page
|
||||
end
|
||||
|
||||
@showing_holds_only = q.has_key?(:show_holds_only) && q[:show_holds_only]
|
||||
|
||||
from_api = (params[:format] == "json" || params[:format] == "xml")
|
||||
results = Post.find_by_sql(Post.generate_sql(q, :original_query => tags, :from_api => from_api, :order => "p.id DESC", :offset => offset, :limit => @posts.per_page * 3))
|
||||
@preload = []
|
||||
if page && page > 1 then
|
||||
@preload = results[0, limit] || []
|
||||
results = results[limit..-1] || []
|
||||
end
|
||||
@posts.replace(results[0..limit-1])
|
||||
@preload += results[limit..-1] || []
|
||||
|
||||
respond_to do |fmt|
|
||||
fmt.html do
|
||||
if split_tags.any?
|
||||
@tags = Tag.parse_query(tags)
|
||||
else
|
||||
@tags = Cache.get("$poptags", 1.hour) do
|
||||
{:include => Tag.count_by_period(1.day.ago, Time.now, :limit => 25, :exclude_types => CONFIG["exclude_from_tag_sidebar"])}
|
||||
end
|
||||
end
|
||||
end
|
||||
fmt.xml do
|
||||
render :layout => false
|
||||
end
|
||||
fmt.json {render :json => @posts.to_json}
|
||||
end
|
||||
end
|
||||
|
||||
def atom
|
||||
# We get a lot of bogus "/post/atom.feed" requests that spam our error logs. Make sure
|
||||
# we only try to format atom.xml.
|
||||
if not params[:format].nil? then
|
||||
# If we don't change the format, it tries to render "404.feed".
|
||||
params[:format] = "html"
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
|
||||
@posts = Post.find_by_sql(Post.generate_sql(params[:tags], :limit => 20, :order => "p.id DESC"))
|
||||
headers["Content-Type"] = "application/atom+xml"
|
||||
render :layout => false
|
||||
end
|
||||
|
||||
def piclens
|
||||
@posts = WillPaginate::Collection.create(params[:page], 16, Post.fast_count(params[:tags])) do |pager|
|
||||
pager.replace(Post.find_by_sql(Post.generate_sql(params[:tags], :order => "p.id DESC", :offset => pager.offset, :limit => pager.per_page)))
|
||||
end
|
||||
|
||||
headers["Content-Type"] = "application/rss+xml"
|
||||
render :layout => false
|
||||
end
|
||||
|
||||
def show
|
||||
begin
|
||||
if params[:md5]
|
||||
@post = Post.find_by_md5(params[:md5].downcase) || raise(ActiveRecord::RecordNotFound)
|
||||
else
|
||||
@post = Post.find(params[:id])
|
||||
end
|
||||
|
||||
if !@current_user.is_anonymous? && @post
|
||||
@vote = PostVotes.find_by_ids(@current_user.id, @post.id).score rescue 0
|
||||
end
|
||||
|
||||
@pools = Pool.find(:all, :joins => "JOIN pools_posts ON pools_posts.pool_id = pools.id", :conditions => "pools_posts.post_id = #{@post.id} AND (active = true OR master_id IS NOT NULL)", :order => "pools.name", :select => "pools.name, pools.id")
|
||||
@tags = {:include => @post.cached_tags.split(/ /)}
|
||||
@include_tag_reverse_aliases = true
|
||||
set_title @post.title_tags.tr("_", " ")
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render :action => "show_empty", :status => 404
|
||||
end
|
||||
end
|
||||
|
||||
def view
|
||||
redirect_to :action=>"show", :id=>params[:id]
|
||||
end
|
||||
|
||||
def popular_recent
|
||||
case params[:period]
|
||||
when "1w"
|
||||
@period_name = "last week"
|
||||
period = 1.week
|
||||
when "1m"
|
||||
@period_name = "last month"
|
||||
period = 1.month
|
||||
when "1y"
|
||||
@period_name = "last year"
|
||||
period = 1.year
|
||||
else
|
||||
params[:period] = "1d"
|
||||
@period_name = "last 24 hours"
|
||||
period = 1.day
|
||||
end
|
||||
|
||||
@params = params
|
||||
@end = Time.now
|
||||
@start = @end - period
|
||||
@previous = @start - period
|
||||
|
||||
set_title "Exploring %s" % @period_name
|
||||
|
||||
@posts = Post.find(:all, :conditions => ["status <> 'deleted' AND posts.index_timestamp >= ? AND posts.index_timestamp <= ? ", @start, @end], :order => "score DESC", :limit => 20)
|
||||
|
||||
respond_to_list("posts")
|
||||
end
|
||||
|
||||
def popular_by_day
|
||||
if params[:year] && params[:month] && params[:day]
|
||||
@day = Time.gm(params[:year].to_i, params[:month], params[:day])
|
||||
else
|
||||
@day = Time.new.getgm.at_beginning_of_day
|
||||
end
|
||||
|
||||
set_title "Exploring #{@day.year}/#{@day.month}/#{@day.day}"
|
||||
|
||||
@posts = Post.find(:all, :conditions => ["posts.created_at >= ? AND posts.created_at <= ? ", @day, @day.tomorrow], :order => "score DESC", :limit => 20)
|
||||
|
||||
respond_to_list("posts")
|
||||
end
|
||||
|
||||
def popular_by_week
|
||||
if params[:year] && params[:month] && params[:day]
|
||||
@start = Time.gm(params[:year].to_i, params[:month], params[:day]).beginning_of_week
|
||||
else
|
||||
@start = Time.new.getgm.beginning_of_week
|
||||
end
|
||||
|
||||
@end = @start.next_week
|
||||
|
||||
set_title "Exploring #{@start.year}/#{@start.month}/#{@start.day} - #{@end.year}/#{@end.month}/#{@end.day}"
|
||||
|
||||
@posts = Post.find(:all, :conditions => ["posts.created_at >= ? AND posts.created_at < ? ", @start, @end], :order => "score DESC", :limit => 20)
|
||||
|
||||
respond_to_list("posts")
|
||||
end
|
||||
|
||||
def popular_by_month
|
||||
if params[:year] && params[:month]
|
||||
@start = Time.gm(params[:year].to_i, params[:month], 1)
|
||||
else
|
||||
@start = Time.new.getgm.beginning_of_month
|
||||
end
|
||||
|
||||
@end = @start.next_month
|
||||
|
||||
set_title "Exploring #{@start.year}/#{@start.month}"
|
||||
|
||||
@posts = Post.find(:all, :conditions => ["posts.created_at >= ? AND posts.created_at < ? ", @start, @end], :order => "score DESC", :limit => 20)
|
||||
|
||||
respond_to_list("posts")
|
||||
end
|
||||
|
||||
def revert_tags
|
||||
user_id = @current_user.id
|
||||
@post = Post.find(params[:id])
|
||||
@post.update_attributes(:tags => PostTagHistory.find(params[:history_id].to_i).tags, :updater_user_id => user_id, :updater_ip_addr => request.remote_ip)
|
||||
|
||||
respond_to_success("Tags reverted", :action => "show", :id => @post.id, :tag_title => @post.tag_title)
|
||||
end
|
||||
|
||||
def vote
|
||||
p = Post.find(params[:id])
|
||||
score = params[:score].to_i
|
||||
|
||||
if !@current_user.is_mod_or_higher? && score < 0 || score > 3
|
||||
respond_to_error("Invalid score", {:action => "show", :id => params[:id], :tag_title => p.tag_title}, :status => 424)
|
||||
return
|
||||
end
|
||||
|
||||
options = {}
|
||||
if p.vote!(score, @current_user, request.remote_ip, options)
|
||||
voted_by = p.voted_by
|
||||
voted_by.each_key { |vote|
|
||||
users = voted_by[vote]
|
||||
users.map! { |user|
|
||||
{ :name => user.pretty_name, :id => user.id }
|
||||
}
|
||||
}
|
||||
respond_to_success("Vote saved", {:action => "show", :id => params[:id], :tag_title => p.tag_title}, :api => {:vote => score, :score => p.score, :post_id => p.id, :votes => voted_by })
|
||||
else
|
||||
respond_to_error("Already voted", {:action => "show", :id => params[:id], :tag_title => p.tag_title}, :status => 423)
|
||||
end
|
||||
end
|
||||
|
||||
def flag
|
||||
post = Post.find(params[:id])
|
||||
if post.status != "active"
|
||||
respond_to_error("Can only flag active posts", :action => "show", :id => params[:id])
|
||||
return
|
||||
end
|
||||
|
||||
post.flag!(params[:reason], @current_user.id)
|
||||
respond_to_success("Post flagged", :action => "show", :id => params[:id])
|
||||
end
|
||||
|
||||
def random
|
||||
max_id = Post.maximum(:id)
|
||||
|
||||
10.times do
|
||||
post = Post.find(:first, :conditions => ["id = ? AND status <> 'deleted'", rand(max_id) + 1])
|
||||
|
||||
if post != nil && post.can_be_seen_by?(@current_user)
|
||||
redirect_to :action => "show", :id => post.id, :tag_title => post.tag_title
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
flash[:notice] = "Couldn't find a post in 10 tries. Try again."
|
||||
redirect_to :action => "index"
|
||||
end
|
||||
|
||||
def similar
|
||||
@params = params
|
||||
if params[:file].blank? then params.delete(:file) end
|
||||
if params[:url].blank? then params.delete(:url) end
|
||||
if params[:id].blank? then params.delete(:id) end
|
||||
if params[:search_id].blank? then params.delete(:search_id) end
|
||||
if params[:services].blank? then params.delete(:services) end
|
||||
if params[:threshold].blank? then params.delete(:threshold) end
|
||||
if params[:forcegray].blank? || params[:forcegray] == "0" then params.delete(:forcegray) end
|
||||
if params[:initial] == "0" then params.delete(:initial) end
|
||||
if not SimilarImages.valid_saved_search(params[:search_id]) then params.delete(:search_id) end
|
||||
params[:width] = params[:width].to_i if params[:width]
|
||||
params[:height] = params[:height].to_i if params[:height]
|
||||
|
||||
@initial = params[:initial]
|
||||
if @initial && !params[:services]
|
||||
params[:services] = "local"
|
||||
end
|
||||
|
||||
@services = SimilarImages.get_services(params[:services])
|
||||
if params[:id]
|
||||
begin
|
||||
@compared_post = Post.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render :status => 404
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
if @compared_post && @compared_post.is_deleted?
|
||||
respond_to_error("Post deleted", :controller => "post", :action => "show", :id => params[:id], :tag_title => @compared_post.tag_title)
|
||||
return
|
||||
end
|
||||
|
||||
# We can do these kinds of searches:
|
||||
#
|
||||
# File: Search from a specified file. The image is saved locally with an ID, and sent
|
||||
# as a file to the search servers.
|
||||
#
|
||||
# URL: search from a remote URL. The URL is downloaded, and then treated as a :file
|
||||
# search. This way, changing options doesn't repeatedly download the remote image,
|
||||
# and it removes a layer of abstraction when an error happens during download
|
||||
# compared to having the search server download it.
|
||||
#
|
||||
# Post ID: Search from a post ID. The preview image is sent as a URL.
|
||||
#
|
||||
# Search ID: Search using an image uploaded with a previous File search, using
|
||||
# the search MD5 created. We're not allowed to repopulate filename fields in the
|
||||
# user's browser, so we can't re-submit the form as a file search when changing search
|
||||
# parameters. Instead, we hide the search ID in the form, and use it to recall the
|
||||
# file from before. These files are expired after a while; we check for expired files
|
||||
# when doing later searches, so we don't need a cron job.
|
||||
def search(params)
|
||||
options = params.merge({
|
||||
:services => @services,
|
||||
})
|
||||
|
||||
# Check search_id first, so options links that include it will use it. If the
|
||||
# user searches with the actual form, search_id will be cleared on submission.
|
||||
if params[:search_id] then
|
||||
file_path = SimilarImages.find_saved_search(params[:search_id])
|
||||
if file_path.nil?
|
||||
# The file was probably purged. Delete :search_id before redirecting, so the
|
||||
# error doesn't loop.
|
||||
params.delete(:search_id)
|
||||
return { :errors => { :error => "Search expired" } }
|
||||
end
|
||||
elsif params[:url] || params[:file] then
|
||||
# Save the file locally.
|
||||
begin
|
||||
if params[:url] then
|
||||
search = Timeout::timeout(30) do
|
||||
Danbooru.http_get_streaming(params[:url]) do |res|
|
||||
SimilarImages.save_search do |f|
|
||||
res.read_body do |block|
|
||||
f.write(block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else # file
|
||||
search = SimilarImages.save_search do |f|
|
||||
wrote = 0
|
||||
buf = ""
|
||||
while params[:file].read(1024*64, buf) do
|
||||
wrote += buf.length
|
||||
f.write(buf)
|
||||
end
|
||||
|
||||
if wrote == 0 then
|
||||
return { :errors => { :error => "No file received" } }
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue SocketError, URI::Error, SystemCallError, Danbooru::ResizeError => e
|
||||
return { :errors => { :error => "#{e}" } }
|
||||
rescue Timeout::Error => e
|
||||
return { :errors => { :error => "Download timed out" } }
|
||||
end
|
||||
|
||||
file_path = search[:file_path]
|
||||
|
||||
# Set :search_id in params for generated URLs that point back here.
|
||||
params[:search_id] = search[:search_id]
|
||||
|
||||
# The :width and :height params specify the size of the original image, for display
|
||||
# in the results. The user can specify them; if not specified, fill it in.
|
||||
params[:width] ||= search[:original_width]
|
||||
params[:height] ||= search[:original_height]
|
||||
elsif params[:id] then
|
||||
options[:source] = @compared_post
|
||||
options[:type] = :post
|
||||
end
|
||||
|
||||
if params[:search_id] then
|
||||
options[:source] = File.open(file_path, 'rb')
|
||||
options[:source_filename] = params[:search_id]
|
||||
options[:source_thumb] = "/data/search/#{params[:search_id]}"
|
||||
options[:type] = :file
|
||||
end
|
||||
options[:width] = params[:width]
|
||||
options[:height] = params[:height]
|
||||
|
||||
if options[:type] == :file
|
||||
SimilarImages.cull_old_searches
|
||||
end
|
||||
|
||||
return SimilarImages.similar_images(options)
|
||||
end
|
||||
|
||||
unless params[:url].nil? and params[:id].nil? and params[:file].nil? and params[:search_id].nil? then
|
||||
res = search(params)
|
||||
|
||||
@errors = res[:errors]
|
||||
@searched = true
|
||||
@search_id = res[:search_id]
|
||||
|
||||
# Never pass :file on through generated URLs.
|
||||
params.delete(:file)
|
||||
else
|
||||
res = {}
|
||||
@errors = {}
|
||||
@searched = false
|
||||
end
|
||||
|
||||
@posts = res[:posts]
|
||||
@similar = res
|
||||
|
||||
respond_to do |fmt|
|
||||
fmt.html do
|
||||
if @initial=="1" && @posts.empty?
|
||||
flash.keep
|
||||
redirect_to :controller => "post", :action => "show", :id => params[:id], :tag_title => @compared_post.tag_title
|
||||
return
|
||||
end
|
||||
if @errors[:error]
|
||||
flash[:notice] = @errors[:error]
|
||||
end
|
||||
|
||||
if @posts then
|
||||
@posts = res[:posts_external] + @posts
|
||||
@posts = @posts.sort { |a, b| res[:similarity][b] <=> res[:similarity][a] }
|
||||
|
||||
# Add the original post to the start of the list.
|
||||
if res[:source]
|
||||
@posts = [ res[:source] ] + @posts
|
||||
else
|
||||
@posts = [ res[:external_source] ] + @posts
|
||||
end
|
||||
end
|
||||
end
|
||||
fmt.xml do
|
||||
if @errors[:error]
|
||||
respond_to_error(@errors[:error], {:action => "index"}, :status => 503)
|
||||
return
|
||||
end
|
||||
x = Builder::XmlMarkup.new(:indent => 2)
|
||||
x.instruct!
|
||||
render :xml => x.posts() {
|
||||
unless res[:errors].empty?
|
||||
res[:errors].map { |server, error|
|
||||
{ :server=>server, :message=>error[:message], :services=>error[:services].join(",") }.to_xml(:root => "error", :builder => x, :skip_instruct => true)
|
||||
}
|
||||
end
|
||||
|
||||
if res[:source]
|
||||
x.source() {
|
||||
res[:source].to_xml(:builder => x, :skip_instruct => true)
|
||||
}
|
||||
else
|
||||
x.source() {
|
||||
res[:external_source].to_xml(:builder => x, :skip_instruct => true)
|
||||
}
|
||||
end
|
||||
|
||||
@posts.each { |e|
|
||||
x.similar(:similarity=>res[:similarity][e]) {
|
||||
e.to_xml(:builder => x, :skip_instruct => true)
|
||||
}
|
||||
}
|
||||
res[:posts_external].each { |e|
|
||||
x.similar(:similarity=>res[:similarity][e]) {
|
||||
e.to_xml(:builder => x, :skip_instruct => true)
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def undelete
|
||||
post = Post.find(params[:id])
|
||||
post.undelete!
|
||||
respond_to_success("Post was undeleted", :action => "show", :id => params[:id])
|
||||
end
|
||||
|
||||
def exception
|
||||
raise "error"
|
||||
end
|
||||
end
|
50
app/controllers/post_tag_history_controller.rb
Normal file
50
app/controllers/post_tag_history_controller.rb
Normal file
@ -0,0 +1,50 @@
|
||||
class PostTagHistoryController < ApplicationController
|
||||
layout 'default'
|
||||
before_filter :member_only
|
||||
verify :method => :post, :only => [:undo]
|
||||
|
||||
def index
|
||||
@changes = PostTagHistory.paginate(PostTagHistory.generate_sql(params).merge(:order => "id DESC", :per_page => 20, :select => "post_tag_histories.*", :page => params[:page]))
|
||||
@change_list = @changes.map do |c|
|
||||
{ :change => c }.merge(c.tag_changes(c.previous))
|
||||
end
|
||||
end
|
||||
|
||||
def revert
|
||||
@change = PostTagHistory.find(params[:id])
|
||||
@post = Post.find(@change.post_id)
|
||||
|
||||
if request.post?
|
||||
if params[:commit] == "Yes"
|
||||
@post.update_attributes(:updater_ip_addr => request.remote_ip, :updater_user_id => @current_user.id, :tags => @change.tags)
|
||||
flash[:notice] = "Tags reverted"
|
||||
end
|
||||
|
||||
redirect_to :controller => "post", :action => "show", :id => @post.id
|
||||
end
|
||||
end
|
||||
|
||||
def undo
|
||||
ids = params[:id].split(/,/)
|
||||
|
||||
if ids.length > 1 && !@current_user.is_privileged_or_higher?
|
||||
respond_to_error("Only privileged users can undo more than one change at once", :status => 403)
|
||||
return
|
||||
end
|
||||
|
||||
options = {
|
||||
:update_options => { :updater_ip_addr => request.remote_ip, :updater_user_id => @current_user.id }
|
||||
}
|
||||
|
||||
ids.each do |id|
|
||||
@change = PostTagHistory.find(id)
|
||||
@change.undo(options)
|
||||
end
|
||||
|
||||
options[:posts].each do |id, post|
|
||||
post.save!
|
||||
end
|
||||
|
||||
respond_to_success("Tag changes undone", :action => "index")
|
||||
end
|
||||
end
|
75
app/controllers/report_controller.rb
Normal file
75
app/controllers/report_controller.rb
Normal file
@ -0,0 +1,75 @@
|
||||
class ReportController < ApplicationController
|
||||
layout 'default'
|
||||
|
||||
def tag_changes
|
||||
@users = Report.usage_by_user("histories", 3.days.ago, Time.now, ["group_by_table = ?"], ["posts"])
|
||||
GoogleChart::PieChart.new("600x300", "Tag Changes", false) do |pc|
|
||||
@users.each do |user|
|
||||
# Hack to work around the limited Google API: there's no way to send a literal
|
||||
# pipe, since that's the field separator, and it won't render any of the alternate
|
||||
# pipe-like characters.
|
||||
pc.data user["name"].gsub(/\|/, 'l'), user["change_count"].to_i
|
||||
end
|
||||
|
||||
@tag_changes_url = pc.to_url
|
||||
end
|
||||
end
|
||||
|
||||
def note_changes
|
||||
@users = Report.usage_by_user("note_versions", 3.days.ago, Time.now)
|
||||
GoogleChart::PieChart.new("600x300", "Note Changes", false) do |pc|
|
||||
@users.each do |user|
|
||||
pc.data user["name"].gsub(/\|/, 'l'), user["change_count"].to_i
|
||||
end
|
||||
|
||||
@note_changes_url = pc.to_url
|
||||
end
|
||||
end
|
||||
|
||||
def wiki_changes
|
||||
@users = Report.usage_by_user("wiki_page_versions", 3.days.ago, Time.now)
|
||||
GoogleChart::PieChart.new("600x300", "Wiki Changes", false) do |pc|
|
||||
@users.each do |user|
|
||||
pc.data user["name"].gsub(/\|/, 'l'), user["change_count"].to_i
|
||||
end
|
||||
|
||||
@wiki_changes_url = pc.to_url
|
||||
end
|
||||
end
|
||||
|
||||
def votes
|
||||
start = 3.days.ago
|
||||
stop = Time.now
|
||||
@users = Report.usage_by_user("post_votes", start, stop, ["score > 0"], [], "updated_at")
|
||||
GoogleChart::PieChart.new("600x300", "Votes", false) do |pc|
|
||||
@users.each do |user|
|
||||
pc.data user["name"].gsub(/\|/, 'l'), user["change_count"].to_i
|
||||
end
|
||||
|
||||
@votes_url = pc.to_url
|
||||
end
|
||||
|
||||
@users.each do |user|
|
||||
conds = ["updated_at BETWEEN ? AND ?"]
|
||||
params = []
|
||||
params << start
|
||||
params << stop
|
||||
|
||||
if user["user"] then
|
||||
conds << "user_id = ?"
|
||||
params << user["user_id"]
|
||||
else
|
||||
conds << "user_id NOT IN (?)"
|
||||
params << @users.select {|x| x["user_id"]}.map {|x| x["user_id"]}
|
||||
end
|
||||
|
||||
votes = ActiveRecord::Base.connection.select_all(ActiveRecord::Base.sanitize_sql(["SELECT COUNT(score) AS sum, score FROM post_votes WHERE #{conds.join(" AND ")} GROUP BY score", *params]))
|
||||
user["votes"] = {}
|
||||
votes.each { |vote|
|
||||
score = vote["score"].to_i
|
||||
user["votes"][score] = vote["sum"]
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
3
app/controllers/static_controller.rb
Normal file
3
app/controllers/static_controller.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class StaticController < ApplicationController
|
||||
layout "bare"
|
||||
end
|
67
app/controllers/tag_alias_controller.rb
Normal file
67
app/controllers/tag_alias_controller.rb
Normal file
@ -0,0 +1,67 @@
|
||||
class TagAliasController < ApplicationController
|
||||
layout "default"
|
||||
before_filter :member_only, :only => [:create]
|
||||
verify :method => :post, :only => [:create, :update]
|
||||
|
||||
def create
|
||||
ta = TagAlias.new(params[:tag_alias].merge(:is_pending => true))
|
||||
|
||||
if ta.save
|
||||
flash[:notice] = "Tag alias created"
|
||||
else
|
||||
flash[:notice] = "Error: " + ta.errors.full_messages.join(", ")
|
||||
end
|
||||
|
||||
redirect_to :action => "index"
|
||||
end
|
||||
|
||||
def index
|
||||
set_title "Tag Aliases"
|
||||
|
||||
if params[:commit] == "Search Implications"
|
||||
redirect_to :controller => "tag_implication", :action => "index", :query => params[:query]
|
||||
return
|
||||
end
|
||||
|
||||
if params[:query]
|
||||
name = "%" + params[:query].to_escaped_for_sql_like + "%"
|
||||
@aliases = TagAlias.paginate :order => "is_pending DESC, name", :per_page => 20, :conditions => ["name LIKE ? ESCAPE E'\\\\' OR alias_id IN (SELECT id FROM tags WHERE name ILIKE ? ESCAPE E'\\\\')", name, name], :page => params[:page]
|
||||
else
|
||||
@aliases = TagAlias.paginate :order => "is_pending DESC, name", :per_page => 20, :page => params[:page]
|
||||
end
|
||||
|
||||
respond_to_list("aliases")
|
||||
end
|
||||
|
||||
def update
|
||||
ids = params[:aliases].keys
|
||||
|
||||
case params[:commit]
|
||||
when "Delete"
|
||||
if @current_user.is_mod_or_higher? || ids.all? {|x| ta = TagAlias.find(x) ; ta.is_pending? && ta.creator_id == @current_user.id}
|
||||
ids.each {|x| TagAlias.find(x).destroy_and_notify(@current_user, params[:reason])}
|
||||
|
||||
flash[:notice] = "Tag aliases deleted"
|
||||
redirect_to :action => "index"
|
||||
else
|
||||
access_denied
|
||||
end
|
||||
|
||||
when "Approve"
|
||||
if @current_user.is_mod_or_higher?
|
||||
ids.each do |x|
|
||||
if CONFIG["enable_asynchronous_tasks"]
|
||||
JobTask.create(:task_type => "approve_tag_alias", :status => "pending", :data => {"id" => x, "updater_id" => @current_user.id, "updater_ip_addr" => request.remote_ip})
|
||||
else
|
||||
TagAlias.find(x).approve(@current_user.id, request.remote_ip)
|
||||
end
|
||||
end
|
||||
|
||||
flash[:notice] = "Tag alias approval jobs created"
|
||||
redirect_to :controller => "job_task", :action => "index"
|
||||
else
|
||||
access_denied
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
199
app/controllers/tag_controller.rb
Normal file
199
app/controllers/tag_controller.rb
Normal file
@ -0,0 +1,199 @@
|
||||
class TagController < ApplicationController
|
||||
layout 'default'
|
||||
auto_complete_for :tag, :name
|
||||
before_filter :mod_only, :only => [:mass_edit, :edit_preview]
|
||||
before_filter :member_only, :only => [:update, :edit]
|
||||
|
||||
def cloud
|
||||
set_title "Tags"
|
||||
|
||||
@tags = Tag.find(:all, :conditions => "post_count > 0", :order => "post_count DESC", :limit => 100).sort {|a, b| a.name <=> b.name}
|
||||
end
|
||||
|
||||
def index
|
||||
# TODO: convert to nagato
|
||||
set_title "Tags"
|
||||
|
||||
if params[:limit] == "0"
|
||||
limit = nil
|
||||
elsif params[:limit] == nil
|
||||
limit = 50
|
||||
else
|
||||
limit = params[:limit].to_i
|
||||
end
|
||||
|
||||
case params[:order]
|
||||
when "name"
|
||||
order = "name"
|
||||
|
||||
when "count"
|
||||
order = "post_count desc"
|
||||
|
||||
when "date"
|
||||
order = "id desc"
|
||||
|
||||
else
|
||||
order = "name"
|
||||
end
|
||||
|
||||
conds = ["true"]
|
||||
cond_params = []
|
||||
|
||||
unless params[:name].blank?
|
||||
conds << "name LIKE ? ESCAPE E'\\\\'"
|
||||
|
||||
if params[:name].include?("*")
|
||||
cond_params << params[:name].to_escaped_for_sql_like
|
||||
else
|
||||
cond_params << "%" + params[:name].to_escaped_for_sql_like + "%"
|
||||
end
|
||||
end
|
||||
|
||||
unless params[:type].blank?
|
||||
conds << "tag_type = ?"
|
||||
cond_params << params[:type].to_i
|
||||
end
|
||||
|
||||
if params[:after_id]
|
||||
conds << "id >= ?"
|
||||
cond_params << params[:after_id]
|
||||
end
|
||||
|
||||
if params[:id]
|
||||
conds << "id = ?"
|
||||
cond_params << params[:id]
|
||||
end
|
||||
|
||||
respond_to do |fmt|
|
||||
fmt.html do
|
||||
@tags = Tag.paginate :order => order, :per_page => 50, :conditions => [conds.join(" AND "), *cond_params], :page => params[:page]
|
||||
end
|
||||
fmt.xml do
|
||||
order = nil if params[:order] == nil
|
||||
conds = conds.join(" AND ")
|
||||
if conds == "true" && CONFIG["web_server"] == "nginx" && File.exists?("#{RAILS_ROOT}/public/tags.xml")
|
||||
# Special case: instead of rebuilding a list of every tag every time, cache it locally and tell the web
|
||||
# server to stream it directly. This only works on Nginx.
|
||||
response.headers["X-Accel-Redirect"] = "#{RAILS_ROOT}/public/tags.xml"
|
||||
render :nothing => true
|
||||
else
|
||||
render :xml => Tag.find(:all, :order => order, :limit => limit, :conditions => [conds, *cond_params]).to_xml(:root => "tags")
|
||||
end
|
||||
end
|
||||
fmt.json do
|
||||
@tags = Tag.find(:all, :order => order, :limit => limit, :conditions => [conds.join(" AND "), *cond_params])
|
||||
render :json => @tags.to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def mass_edit
|
||||
set_title "Mass Edit Tags"
|
||||
|
||||
if request.post?
|
||||
if params[:start].blank?
|
||||
respond_to_error("Start tag missing", {:action => "mass_edit"}, :status => 424)
|
||||
return
|
||||
end
|
||||
|
||||
if CONFIG["enable_asynchronous_tasks"]
|
||||
task = JobTask.create(:task_type => "mass_tag_edit", :status => "pending", :data => {"start_tags" => params[:start], "result_tags" => params[:result], "updater_id" => session[:user_id], "updater_ip_addr" => request.remote_ip})
|
||||
respond_to_success("Mass tag edit job created", :controller => "job_task", :action => "index")
|
||||
else
|
||||
Tag.mass_edit(params[:start], params[:result], @current_user.id, request.remote_ip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def edit_preview
|
||||
@posts = Post.find_by_sql(Post.generate_sql(params[:tags], :order => "p.id DESC", :limit => 500))
|
||||
render :layout => false
|
||||
end
|
||||
|
||||
def edit
|
||||
if params[:id]
|
||||
@tag = Tag.find(params[:id]) or Tag.new
|
||||
else
|
||||
@tag = Tag.find_by_name(params[:name]) or Tag.new
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
tag = Tag.find_by_name(params[:tag][:name])
|
||||
tag.update_attributes(params[:tag]) if tag
|
||||
|
||||
respond_to_success("Tag updated", :action => "index")
|
||||
end
|
||||
|
||||
def related
|
||||
if params[:type]
|
||||
@tags = Tag.scan_tags(params[:tags])
|
||||
@tags = TagAlias.to_aliased(@tags)
|
||||
@tags = @tags.inject({}) do |all, x|
|
||||
all[x] = Tag.calculate_related_by_type(x, CONFIG["tag_types"][params[:type]]).map {|y| [y["name"], y["post_count"]]}
|
||||
all
|
||||
end
|
||||
else
|
||||
@tags = Tag.scan_tags(params[:tags])
|
||||
@patterns, @tags = @tags.partition {|x| x.include?("*")}
|
||||
@tags = TagAlias.to_aliased(@tags)
|
||||
@tags = @tags.inject({}) do |all, x|
|
||||
all[x] = Tag.find_related(x).map {|y| [y[0], y[1]]}
|
||||
all
|
||||
end
|
||||
@patterns.each do |x|
|
||||
@tags[x] = Tag.find(:all, :conditions => ["name LIKE ? ESCAPE E'\\\\'", x.to_escaped_for_sql_like]).map {|y| [y.name, y.post_count]}
|
||||
end
|
||||
end
|
||||
|
||||
respond_to do |fmt|
|
||||
fmt.xml do
|
||||
# We basically have to do this by hand.
|
||||
builder = Builder::XmlMarkup.new(:indent => 2)
|
||||
builder.instruct!
|
||||
xml = builder.tag!("tags") do
|
||||
@tags.each do |parent, related|
|
||||
builder.tag!("tag", :name => parent) do
|
||||
related.each do |tag, count|
|
||||
builder.tag!("tag", :name => tag, :count => count)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
render :xml => xml
|
||||
end
|
||||
fmt.json {render :json => @tags.to_json}
|
||||
end
|
||||
end
|
||||
|
||||
def popular_by_day
|
||||
if params["year"] and params["month"] and params["day"]
|
||||
@day = Time.gm(params["year"].to_i, params["month"], params["day"])
|
||||
else
|
||||
@day = Time.new.getgm.at_beginning_of_day
|
||||
end
|
||||
|
||||
@tags = Tag.count_by_period(@day.beginning_of_day, @day.tomorrow.beginning_of_day)
|
||||
end
|
||||
|
||||
def popular_by_week
|
||||
if params["year"] and params["month"] and params["day"]
|
||||
@day = Time.gm(params["year"].to_i, params["month"], params["day"]).beginning_of_week
|
||||
else
|
||||
@day = Time.new.getgm.at_beginning_of_day.beginning_of_week
|
||||
end
|
||||
|
||||
@tags = Tag.count_by_period(@day, @day.next_week)
|
||||
end
|
||||
|
||||
def popular_by_month
|
||||
if params["year"] and params["month"]
|
||||
@day = Time.gm(params["year"].to_i, params["month"], params["day"]).beginning_of_month
|
||||
else
|
||||
@day = Time.new.getgm.at_beginning_of_day.beginning_of_month
|
||||
end
|
||||
|
||||
@tags = Tag.count_by_period(@day, @day.next_month)
|
||||
end
|
||||
end
|
67
app/controllers/tag_implication_controller.rb
Normal file
67
app/controllers/tag_implication_controller.rb
Normal file
@ -0,0 +1,67 @@
|
||||
class TagImplicationController < ApplicationController
|
||||
layout "default"
|
||||
before_filter :member_only, :only => [:create]
|
||||
verify :method => :post, :only => [:create, :update]
|
||||
|
||||
def create
|
||||
ti = TagImplication.new(params[:tag_implication].merge(:is_pending => true))
|
||||
|
||||
if ti.save
|
||||
flash[:notice] = "Tag implication created"
|
||||
else
|
||||
flash[:notice] = "Error: " + ti.errors.full_messages.join(", ")
|
||||
end
|
||||
|
||||
redirect_to :action => "index"
|
||||
end
|
||||
|
||||
def update
|
||||
ids = params[:implications].keys
|
||||
|
||||
case params[:commit]
|
||||
when "Delete"
|
||||
if @current_user.is_mod_or_higher? || ids.all? {|x| ti = TagImplication.find(x) ; ti.is_pending? && ti.creator_id == @current_user.id}
|
||||
ids.each {|x| TagImplication.find(x).destroy_and_notify(@current_user, params[:reason])}
|
||||
|
||||
flash[:notice] = "Tag implications deleted"
|
||||
redirect_to :action => "index"
|
||||
else
|
||||
access_denied
|
||||
end
|
||||
|
||||
when "Approve"
|
||||
if @current_user.is_mod_or_higher?
|
||||
ids.each do |x|
|
||||
if CONFIG["enable_asynchronous_tasks"]
|
||||
JobTask.create(:task_type => "approve_tag_implication", :status => "pending", :data => {"id" => x, "updater_id" => @current_user.id, "updater_ip_addr" => request.remote_ip})
|
||||
else
|
||||
TagImplication.find(x).approve(@current_user.id, request.remote_ip)
|
||||
end
|
||||
end
|
||||
|
||||
flash[:notice] = "Tag implication approval jobs created"
|
||||
redirect_to :controller => "job_task", :action => "index"
|
||||
else
|
||||
access_denied
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
set_title "Tag Implications"
|
||||
|
||||
if params[:commit] == "Search Aliases"
|
||||
redirect_to :controller => "tag_alias", :action => "index", :query => params[:query]
|
||||
return
|
||||
end
|
||||
|
||||
if params[:query]
|
||||
name = "%" + params[:query].to_escaped_for_sql_like + "%"
|
||||
@implications = TagImplication.paginate :order => "is_pending DESC, (SELECT name FROM tags WHERE id = tag_implications.predicate_id), (SELECT name FROM tags WHERE id = tag_implications.consequent_id)", :per_page => 20, :conditions => ["predicate_id IN (SELECT id FROM tags WHERE name ILIKE ? ESCAPE '\\\\') OR consequent_id IN (SELECT id FROM tags WHERE name ILIKE ? ESCAPE '\\\\')", name, name], :page => params[:page]
|
||||
else
|
||||
@implications = TagImplication.paginate :order => "is_pending DESC, (SELECT name FROM tags WHERE id = tag_implications.predicate_id), (SELECT name FROM tags WHERE id = tag_implications.consequent_id)", :per_page => 20, :page => params[:page]
|
||||
end
|
||||
|
||||
respond_to_list("implications")
|
||||
end
|
||||
end
|
352
app/controllers/user_controller.rb
Normal file
352
app/controllers/user_controller.rb
Normal file
@ -0,0 +1,352 @@
|
||||
require 'digest/sha2'
|
||||
|
||||
class UserController < ApplicationController
|
||||
layout "default"
|
||||
verify :method => :post, :only => [:authenticate, :update, :create, :unban, :modify_blacklist]
|
||||
before_filter :blocked_only, :only => [:authenticate, :update, :edit, :modify_blacklist]
|
||||
before_filter :janitor_only, :only => [:invites]
|
||||
before_filter :mod_only, :only => [:block, :unblock, :show_blocked_users]
|
||||
before_filter :post_member_only, :only => [:set_avatar]
|
||||
helper :post
|
||||
helper :avatar
|
||||
filter_parameter_logging :password
|
||||
auto_complete_for :user, :name
|
||||
|
||||
protected
|
||||
def save_cookies(user)
|
||||
cookies[:login] = {:value => user.name, :expires => 1.year.from_now}
|
||||
cookies[:pass_hash] = {:value => user.password_hash, :expires => 1.year.from_now}
|
||||
session[:user_id] = user.id
|
||||
end
|
||||
|
||||
public
|
||||
def auto_complete_for_member_name
|
||||
@users = User.find(:all, :order => "lower(name)", :conditions => ["level = ? AND name ILIKE ? ESCAPE E'\\\\'", CONFIG["user_levels"]["Member"], params[:member][:name] + "%"])
|
||||
render :layout => false, :text => "<ul>" + @users.map {|x| "<li>" + x.name + "</li>"}.join("") + "</ul>"
|
||||
end
|
||||
|
||||
def show
|
||||
if params[:name]
|
||||
@user = User.find_by_name(params[:name])
|
||||
else
|
||||
@user = User.find(params[:id])
|
||||
end
|
||||
|
||||
if @user.nil?
|
||||
redirect_to "/404"
|
||||
end
|
||||
if @current_user.is_mod_or_higher?
|
||||
@user_ips = UserLog.find_by_sql("SELECT ul.ip_addr, ul.created_at FROM user_logs ul WHERE ul.user_id = #{@user.id} ORDER BY ul.created_at DESC")
|
||||
@user_ips.map! { |ul| ul.ip_addr }
|
||||
@user_ips.uniq!
|
||||
end
|
||||
end
|
||||
|
||||
def invites
|
||||
if request.post?
|
||||
if params[:member]
|
||||
begin
|
||||
@current_user.invite!(params[:member][:name], params[:member][:level])
|
||||
flash[:notice] = "User was invited"
|
||||
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
flash[:notice] = "Account not found"
|
||||
|
||||
rescue User::NoInvites
|
||||
flash[:notice] = "You have no invites for use"
|
||||
|
||||
rescue User::HasNegativeRecord
|
||||
flash[:notice] = "This use has a negative record and must be invited by an admin"
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to :action => "invites"
|
||||
else
|
||||
@invited_users = User.find(:all, :conditions => ["invited_by = ?", @current_user.id], :order => "lower(name)")
|
||||
end
|
||||
end
|
||||
|
||||
def home
|
||||
set_title "My Account"
|
||||
end
|
||||
|
||||
def index
|
||||
set_title "Users"
|
||||
|
||||
@users = User.paginate(User.generate_sql(params).merge(:per_page => 20, :page => params[:page]))
|
||||
respond_to_list("users")
|
||||
end
|
||||
|
||||
def authenticate
|
||||
save_cookies(@current_user)
|
||||
|
||||
if params[:url].blank?
|
||||
path = {:action => "home"}
|
||||
else
|
||||
path = params[:url]
|
||||
end
|
||||
|
||||
respond_to_success("You are now logged in", path)
|
||||
end
|
||||
|
||||
def check
|
||||
if request.post?
|
||||
user = User.find_by_name(params[:username])
|
||||
ret = { :exists => false }
|
||||
ret[:name] = params[:username]
|
||||
|
||||
if not user
|
||||
respond_to_success("User does not exist", {}, :api => {:response => "unknown-user"}.merge(ret))
|
||||
return
|
||||
end
|
||||
|
||||
# Return some basic information about the user even if the password isn't given, for
|
||||
# UI cosmetics.
|
||||
ret[:exists] = true
|
||||
ret[:id] = user.id
|
||||
ret[:name] = user.name
|
||||
ret[:no_email] = user.email.blank?
|
||||
|
||||
user = User.authenticate(params[:username], params[:password] || "")
|
||||
if not user
|
||||
respond_to_success("Wrong password", {}, :api => {:response => "wrong-password"}.merge(ret))
|
||||
return
|
||||
end
|
||||
|
||||
ret[:pass_hash] = user.password_hash
|
||||
respond_to_success("Successful", {}, :api => {:response => "success"}.merge(ret))
|
||||
end
|
||||
end
|
||||
|
||||
def login
|
||||
set_title "Login"
|
||||
end
|
||||
|
||||
def create
|
||||
user = User.create(params[:user])
|
||||
|
||||
if user.errors.empty?
|
||||
save_cookies(user)
|
||||
|
||||
if CONFIG["enable_account_email_activation"]
|
||||
begin
|
||||
UserMailer::deliver_confirmation_email(user)
|
||||
notice = "New account created. Confirmation email sent to #{user.email}"
|
||||
rescue Net::SMTPSyntaxError, Net::SMTPFatalError
|
||||
user.destroy
|
||||
respond_to_success("Could not send confirmation email; account creation canceled",
|
||||
{:action => "signup"}, :api => {:response => "error", :errors => ["Could not send confirmation email; account creation canceled"]})
|
||||
return
|
||||
end
|
||||
else
|
||||
notice = "New account created"
|
||||
end
|
||||
|
||||
ret = { :exists => false }
|
||||
ret[:name] = user.name
|
||||
ret[:id] = user.id
|
||||
ret[:pass_hash] = user.password_hash
|
||||
|
||||
respond_to_success(notice, {:action => "home"}, :api => {:response => "success"}.merge(ret))
|
||||
else
|
||||
error = user.errors.full_messages.join(", ")
|
||||
respond_to_success("Error: " + error, {:action => "signup"}, :api => {:response => "error", :errors => user.errors.full_messages})
|
||||
end
|
||||
end
|
||||
|
||||
def signup
|
||||
set_title "Signup"
|
||||
@user = User.new
|
||||
end
|
||||
|
||||
def logout
|
||||
set_title "Logout"
|
||||
session[:user_id] = nil
|
||||
cookies[:login] = nil
|
||||
cookies[:pass_hash] = nil
|
||||
|
||||
respond_to_success("You are now logged out", :action => "home")
|
||||
end
|
||||
|
||||
def update
|
||||
if params[:commit] == "Cancel"
|
||||
redirect_to :action => "home"
|
||||
return
|
||||
end
|
||||
|
||||
if @current_user.update_attributes(params[:user])
|
||||
respond_to_success("Account settings saved", :action => "home")
|
||||
else
|
||||
respond_to_error(@current_user, :action => "edit")
|
||||
end
|
||||
end
|
||||
|
||||
def modify_blacklist
|
||||
added_tags = params[:add] || []
|
||||
removed_tags = params[:remove] || []
|
||||
|
||||
tags = @current_user.blacklisted_tags_array
|
||||
added_tags.each { |tag|
|
||||
tags << tag if not tags.include?(tag)
|
||||
}
|
||||
|
||||
tags -= removed_tags
|
||||
|
||||
if @current_user.update_attribute(:blacklisted_tags, tags.join("\n"))
|
||||
respond_to_success("Tag blacklist updated", {:action => "home"}, :api => {:result => @current_user.blacklisted_tags_array})
|
||||
else
|
||||
respond_to_error(@current_user, :action => "edit")
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_blacklist
|
||||
end
|
||||
|
||||
def edit
|
||||
set_title "Edit Account"
|
||||
@user = @current_user
|
||||
end
|
||||
|
||||
def reset_password
|
||||
set_title "Reset Password"
|
||||
|
||||
if request.post?
|
||||
@user = User.find_by_name(params[:user][:name])
|
||||
|
||||
if @user.nil?
|
||||
respond_to_error("That account does not exist", {:action => "reset_password"}, :api => {:result => "unknown-user"})
|
||||
return
|
||||
end
|
||||
|
||||
if @user.email.blank?
|
||||
respond_to_error("You never supplied an email address, therefore you cannot have your password automatically reset",
|
||||
{:action => "login"}, :api => {:result => "no-email"})
|
||||
return
|
||||
end
|
||||
|
||||
if @user.email != params[:user][:email]
|
||||
respond_to_error("That is not the email address you supplied",
|
||||
{:action => "login"}, :api => {:result => "wrong-email"})
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
User.transaction do
|
||||
# If the email is invalid, abort the password reset
|
||||
new_password = @user.reset_password
|
||||
UserMailer.deliver_new_password(@user, new_password)
|
||||
respond_to_success("Password reset. Check your email in a few minutes.",
|
||||
{:action => "login"}, :api => {:result => "success"})
|
||||
return
|
||||
end
|
||||
rescue Net::SMTPSyntaxError, Net::SMTPFatalError
|
||||
respond_to_success("Your email address was invalid",
|
||||
{:action => "login"}, :api => {:result => "invalid-email"})
|
||||
return
|
||||
end
|
||||
else
|
||||
@user = User.new
|
||||
end
|
||||
end
|
||||
|
||||
def block
|
||||
@user = User.find(params[:id])
|
||||
|
||||
if request.post?
|
||||
if @user.is_mod_or_higher?
|
||||
flash[:notice] = "You can not ban other moderators or administrators"
|
||||
redirect_to :action => "block"
|
||||
return
|
||||
end
|
||||
|
||||
Ban.create(params[:ban].merge(:banned_by => @current_user.id, :user_id => params[:id]))
|
||||
redirect_to :action => "show_blocked_users"
|
||||
else
|
||||
@ban = Ban.new(:user_id => @user.id, :duration => "1")
|
||||
end
|
||||
end
|
||||
|
||||
def unblock
|
||||
params[:user].keys.each do |user_id|
|
||||
Ban.destroy_all(["user_id = ?", user_id])
|
||||
user = User.find(user_id)
|
||||
user.level = CONFIG["user_levels"]["Member"]
|
||||
user.save
|
||||
end
|
||||
|
||||
redirect_to :action => "show_blocked_users"
|
||||
end
|
||||
|
||||
def show_blocked_users
|
||||
#@users = User.find(:all, :select => "users.*", :joins => "JOIN bans ON bans.user_id = users.id", :conditions => ["bans.banned_by = ?", @current_user.id])
|
||||
@users = User.find(:all, :select => "users.*", :joins => "JOIN bans ON bans.user_id = users.id")
|
||||
@ip_bans = IpBans.find(:all)
|
||||
end
|
||||
|
||||
if CONFIG["enable_account_email_activation"]
|
||||
def resend_confirmation
|
||||
if request.post?
|
||||
user = User.find_by_email(params[:email])
|
||||
|
||||
if user.nil?
|
||||
flash[:notice] = "No account exists with that email"
|
||||
redirect_to :action => "home"
|
||||
return
|
||||
end
|
||||
|
||||
if user.is_blocked_or_higher?
|
||||
flash[:notice] = "Your account is already activated"
|
||||
redirect_to :action => "home"
|
||||
return
|
||||
end
|
||||
|
||||
UserMailer::deliver_confirmation_email(user)
|
||||
flash[:notice] = "Confirmation email sent"
|
||||
redirect_to :action => "home"
|
||||
end
|
||||
end
|
||||
|
||||
def activate_user
|
||||
flash[:notice] = "Invalid confirmation code"
|
||||
|
||||
users = User.find(:all, :conditions => ["level = ?", CONFIG["user_levels"]["Unactivated"]])
|
||||
users.each do |user|
|
||||
if User.confirmation_hash(user.name) == params["hash"]
|
||||
user.update_attribute(:level, CONFIG["starting_level"])
|
||||
flash[:notice] = "Account has been activated"
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to :action => "home"
|
||||
end
|
||||
end
|
||||
|
||||
def set_avatar
|
||||
@user = @current_user
|
||||
if params[:user_id] then
|
||||
@user = User.find(params[:user_id])
|
||||
respond_to_error("Not found", :action => "index", :status => 404) unless @user
|
||||
end
|
||||
|
||||
if !@user.is_anonymous? && !@current_user.has_permission?(@user, :id)
|
||||
access_denied()
|
||||
return
|
||||
end
|
||||
|
||||
if request.post?
|
||||
if @user.set_avatar(params) then
|
||||
redirect_to :action => "show", :id => @user.id
|
||||
else
|
||||
respond_to_error(@user, :action => "home")
|
||||
end
|
||||
end
|
||||
|
||||
if !@user.is_anonymous? && params[:id] == @user.avatar_post_id then
|
||||
@old = params
|
||||
end
|
||||
|
||||
@params = params
|
||||
@post = Post.find(params[:id])
|
||||
end
|
||||
end
|
40
app/controllers/user_record_controller.rb
Normal file
40
app/controllers/user_record_controller.rb
Normal file
@ -0,0 +1,40 @@
|
||||
class UserRecordController < ApplicationController
|
||||
layout "default"
|
||||
before_filter :privileged_only, :only => [:create, :destroy]
|
||||
|
||||
def index
|
||||
if params[:user_id]
|
||||
@user = User.find(params[:user_id])
|
||||
@user_records = UserRecord.paginate :per_page => 20, :order => "created_at desc", :conditions => ["user_id = ?", params[:user_id]], :page => params[:page]
|
||||
else
|
||||
@user_records = UserRecord.paginate :per_page => 20, :order => "created_at desc", :page => params[:page]
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
@user = User.find(params[:user_id])
|
||||
|
||||
if request.post?
|
||||
if @user.id == @current_user.id
|
||||
flash[:notice] = "You cannot create a record for yourself"
|
||||
else
|
||||
@user_record = UserRecord.create(params[:user_record].merge(:user_id => params[:user_id], :reported_by => @current_user.id))
|
||||
flash[:notice] = "Record updated"
|
||||
end
|
||||
redirect_to :action => "index", :user_id => @user.id
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if request.post?
|
||||
@user_record = UserRecord.find(params[:id])
|
||||
if @current_user.is_mod_or_higher? || @current_user.id == @user_record.reported_by
|
||||
UserRecord.destroy(params[:id])
|
||||
|
||||
respond_to_success("Record updated", :action => "index", :user_id => params[:id])
|
||||
else
|
||||
access_denied()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
172
app/controllers/wiki_controller.rb
Normal file
172
app/controllers/wiki_controller.rb
Normal file
@ -0,0 +1,172 @@
|
||||
class WikiController < ApplicationController
|
||||
layout 'default'
|
||||
before_filter :post_member_only, :only => [:update, :create, :edit, :revert]
|
||||
before_filter :mod_only, :only => [:lock, :unlock, :destroy, :rename]
|
||||
verify :method => :post, :only => [:lock, :unlock, :destroy, :update, :create, :revert]
|
||||
helper :post
|
||||
|
||||
def destroy
|
||||
page = WikiPage.find_page(params[:title])
|
||||
page.destroy
|
||||
respond_to_success("Page deleted", :action => "show", :title => params[:title])
|
||||
end
|
||||
|
||||
def lock
|
||||
page = WikiPage.find_page(params[:title])
|
||||
page.lock!
|
||||
respond_to_success("Page locked", :action => "show", :title => params[:title])
|
||||
end
|
||||
|
||||
def unlock
|
||||
page = WikiPage.find_page(params["title"])
|
||||
page.unlock!
|
||||
respond_to_success("Page unlocked", :action => "show", :title => params[:title])
|
||||
end
|
||||
|
||||
def index
|
||||
set_title "Wiki"
|
||||
|
||||
@params = params
|
||||
if params[:order] == "date"
|
||||
order = "updated_at DESC"
|
||||
else
|
||||
order = "lower(title)"
|
||||
end
|
||||
|
||||
limit = params[:limit] || 25
|
||||
query = params[:query] || ""
|
||||
query = query.scan(/\S+/)
|
||||
|
||||
search_params = {
|
||||
:order => order,
|
||||
:per_page => limit,
|
||||
:page => params[:page]
|
||||
}
|
||||
|
||||
if !query.empty?
|
||||
if query =~ /^title:/
|
||||
search_params[:conditions] = ["title ilike ?", "%" + query[6..-1].to_escaped_for_sql_like + "%"]
|
||||
else
|
||||
search_params[:conditions] = ["text_search_index @@ plainto_tsquery(?)", query.join(" & ")]
|
||||
end
|
||||
end
|
||||
|
||||
@wiki_pages = WikiPage.paginate(search_params)
|
||||
|
||||
respond_to_list("wiki_pages")
|
||||
end
|
||||
|
||||
def preview
|
||||
render :inline => "<%= format_text(params[:body]) %>"
|
||||
end
|
||||
|
||||
def add
|
||||
@wiki_page = WikiPage.new
|
||||
@wiki_page.title = params[:title] || "Title"
|
||||
end
|
||||
|
||||
def create
|
||||
page = WikiPage.create(params[:wiki_page].merge(:ip_addr => request.remote_ip, :user_id => session[:user_id]))
|
||||
|
||||
if page.errors.empty?
|
||||
respond_to_success("Page created", {:action => "show", :title => page.title}, :location => url_for(:action => "show", :title => page.title))
|
||||
else
|
||||
respond_to_error(page, :action => "index")
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
if params[:title] == nil
|
||||
render :text => "no title specified"
|
||||
else
|
||||
@wiki_page = WikiPage.find_page(params[:title], params[:version])
|
||||
|
||||
if @wiki_page == nil
|
||||
redirect_to :action => "add", :title => params[:title]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@page = WikiPage.find_page(params[:title] || params[:wiki_page][:title])
|
||||
|
||||
if @page.is_locked?
|
||||
respond_to_error("Page is locked", {:action => "show", :title => @page.title}, :status => 422)
|
||||
else
|
||||
if @page.update_attributes(params[:wiki_page].merge(:ip_addr => request.remote_ip, :user_id => session[:user_id]))
|
||||
respond_to_success("Page created", :action => "show", :title => @page.title)
|
||||
else
|
||||
respond_to_error(@page, {:action => "show", :title => @page.title})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
if params[:title] == nil
|
||||
render :text => "no title specified"
|
||||
return
|
||||
end
|
||||
|
||||
@title = params[:title]
|
||||
@page = WikiPage.find_page(params[:title], params[:version])
|
||||
@posts = Post.find_by_tags(params[:title], :limit => 8, :order => "id desc").select {|x| x.can_be_seen_by?(@current_user)}
|
||||
@artist = Artist.find_by_name(params[:title])
|
||||
@tag = Tag.find_by_name(params[:title])
|
||||
set_title params[:title].tr("_", " ")
|
||||
end
|
||||
|
||||
def revert
|
||||
@page = WikiPage.find_page(params[:title])
|
||||
|
||||
if @page.is_locked?
|
||||
respond_to_error("Page is locked", {:action => "show", :title => params[:title]}, :status => 422)
|
||||
else
|
||||
@page.revert_to(params[:version])
|
||||
@page.ip_addr = request.remote_ip
|
||||
@page.user_id = @current_user.id
|
||||
|
||||
if @page.save
|
||||
respond_to_success("Page reverted", :action => "show", :title => params[:title])
|
||||
else
|
||||
respond_to_error(@page)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def recent_changes
|
||||
set_title "Recent Changes"
|
||||
|
||||
@wiki_pages = WikiPage.paginate :order => "updated_at DESC", :per_page => (params[:per_page] || 25), :page => params[:page]
|
||||
respond_to_list("wiki_pages")
|
||||
end
|
||||
|
||||
def history
|
||||
set_title "Wiki History"
|
||||
|
||||
@wiki_pages = WikiPageVersion.find(:all, :conditions => ["title = ?", params[:title]], :order => "updated_at DESC")
|
||||
|
||||
respond_to_list("wiki_pages")
|
||||
end
|
||||
|
||||
def diff
|
||||
set_title "Wiki Diff"
|
||||
|
||||
if params[:redirect]
|
||||
redirect_to :action => "diff", :title => params[:title], :from => params[:from], :to => params[:to]
|
||||
return
|
||||
end
|
||||
|
||||
if params[:title].blank? || params[:to].blank? || params[:from].blank?
|
||||
flash[:notice] = "No title was specificed"
|
||||
redirect_to :action => "index"
|
||||
return
|
||||
end
|
||||
|
||||
@oldpage = WikiPage.find_page(params[:title], params[:from])
|
||||
@difference = @oldpage.diff(params[:to])
|
||||
end
|
||||
|
||||
def rename
|
||||
@wiki_page = WikiPage.find_page(params[:title])
|
||||
end
|
||||
end
|
5
app/daemons/job_task_processor.rb
Executable file
5
app/daemons/job_task_processor.rb
Executable file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require File.dirname(__FILE__) + '/../../config/environment'
|
||||
|
||||
JobTask.execute_all
|
6
app/daemons/job_task_processor_ctl.rb
Executable file
6
app/daemons/job_task_processor_ctl.rb
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require 'rubygems'
|
||||
require 'daemons'
|
||||
|
||||
Daemons.run(File.dirname(__FILE__) + "/job_task_processor.rb", :log_output => true, :dir => "../../log")
|
2
app/helpers/admin_helper.rb
Normal file
2
app/helpers/admin_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module AdminHelper
|
||||
end
|
10
app/helpers/advertisement_helper.rb
Normal file
10
app/helpers/advertisement_helper.rb
Normal file
@ -0,0 +1,10 @@
|
||||
module AdvertisementHelper
|
||||
def print_advertisement(ad_type)
|
||||
if CONFIG["can_see_ads"].call(@current_user)
|
||||
ad = Advertisement.find(:first, :conditions => ["ad_type = ? AND status = 'active'", ad_type], :order => "random()")
|
||||
content_tag("div", link_to(image_tag(ad.image_url, :alt => "Advertisement", :width => ad.width, :height => ad.height), :controller => "advertisement", :action => "redirect_ad", :id => ad.id), :style => "margin-bottom: 1em;")
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
end
|
275
app/helpers/application_helper.rb
Normal file
275
app/helpers/application_helper.rb
Normal file
@ -0,0 +1,275 @@
|
||||
module ApplicationHelper
|
||||
# Scale percentage table widths to 100% to make variable column tables
|
||||
# easier.
|
||||
class TableScale
|
||||
def add(*list)
|
||||
@val = nil
|
||||
@list ||= []
|
||||
list.each { |val|
|
||||
@list << val.to_f
|
||||
}
|
||||
@idx ||= 0
|
||||
end
|
||||
|
||||
def get
|
||||
@idx += 1
|
||||
get_idx(@idx - 1)
|
||||
end
|
||||
|
||||
def get_idx(n)
|
||||
if !@val then
|
||||
# Scale the list to sum to 100%.
|
||||
#
|
||||
# Both FF3 and Opera treat values under 1% as unspecified. (Why?) If any value
|
||||
# is less than 1%, clamp it to 1% and recompute the remainder.
|
||||
@val = @list
|
||||
indexes = []
|
||||
@val.each_index { |idx| indexes << idx }
|
||||
while !indexes.empty?
|
||||
sum = @val.length - indexes.length # 1% for each 1% value we excluded
|
||||
indexes.each { |idx| sum += @val[idx] }
|
||||
break if sum == 0
|
||||
|
||||
indexes.each { |idx| @val[idx] = [@val[idx] / (sum/100), 1].max }
|
||||
new_indexes = []
|
||||
indexes.each { |idx| new_indexes << idx if @val[idx] > 1 }
|
||||
break if indexes.length == new_indexes.length
|
||||
indexes = new_indexes
|
||||
end
|
||||
end
|
||||
"%.2f%%" % @val[n]
|
||||
end
|
||||
end
|
||||
|
||||
def navbar_link_to(text, options, html_options = nil)
|
||||
if options[:controller] == params[:controller]
|
||||
klass = "current-page"
|
||||
else
|
||||
klass = nil
|
||||
end
|
||||
|
||||
content_tag("li", link_to(text, options, html_options), :class => klass)
|
||||
end
|
||||
|
||||
def format_text(text, options = {})
|
||||
DText.parse(text)
|
||||
end
|
||||
|
||||
def format_inline(inline, num, id, preview_html=nil)
|
||||
url = inline.inline_images.first.preview_url
|
||||
if not preview_html
|
||||
preview_html = %{<img src="#{url}">}
|
||||
end
|
||||
id_text = "inline-%s-%i" % [id, num]
|
||||
block = %{
|
||||
<div class="inline-image" id="#{id_text}">
|
||||
<div class="inline-thumb" style="display: inline;">
|
||||
#{preview_html}
|
||||
</div>
|
||||
<div class="expanded-image" style="display: none;">
|
||||
<div class="expanded-image-ui"></div>
|
||||
<span class="main-inline-image"></span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
inline_id = "inline-%s-%i" % [id, num]
|
||||
script = 'InlineImage.register("%s", %s);' % [inline_id, inline.to_json]
|
||||
return block, script, inline_id
|
||||
end
|
||||
|
||||
def format_inlines(text, id)
|
||||
num = 0
|
||||
list = []
|
||||
text.gsub!(/image #(\d+)/i) { |t|
|
||||
i = Inline.find($1) rescue nil
|
||||
if i then
|
||||
block, script = format_inline(i, num, id)
|
||||
list << script
|
||||
num += 1
|
||||
block
|
||||
else
|
||||
t
|
||||
end
|
||||
}
|
||||
|
||||
if num > 0 then
|
||||
text << '<script language="javascript">' + list.join("\n") + '</script>'
|
||||
end
|
||||
|
||||
text
|
||||
end
|
||||
|
||||
def id_to_color(id)
|
||||
r = id % 255
|
||||
g = (id >> 8) % 255
|
||||
b = (id >> 16) % 255
|
||||
"rgb(#{r}, #{g}, #{b})"
|
||||
end
|
||||
|
||||
def tag_header(tags)
|
||||
unless tags.blank?
|
||||
'/' + Tag.scan_query(tags).map {|t| link_to(t.tr("_", " "), :controller => "post", :action => "index", :tags => t)}.join("+")
|
||||
end
|
||||
end
|
||||
|
||||
def compact_time(time)
|
||||
if time > Time.now.beginning_of_day
|
||||
time.strftime("%H:%M")
|
||||
elsif time > Time.now.beginning_of_year
|
||||
time.strftime("%b %e")
|
||||
else
|
||||
time.strftime("%b %e, %Y")
|
||||
end
|
||||
end
|
||||
|
||||
def time_ago_in_words(time)
|
||||
from_time = time
|
||||
to_time = Time.now
|
||||
distance_in_minutes = (((to_time - from_time).abs)/60).round
|
||||
distance_in_seconds = ((to_time - from_time).abs).round
|
||||
|
||||
case distance_in_minutes
|
||||
when 0..1
|
||||
'1 minute'
|
||||
|
||||
when 2..44
|
||||
"#{distance_in_minutes} minutes"
|
||||
|
||||
when 45..89
|
||||
'1 hour'
|
||||
|
||||
when 90..1439
|
||||
"#{(distance_in_minutes.to_f / 60.0).round} hours"
|
||||
|
||||
when 1440..2879
|
||||
'1 day'
|
||||
|
||||
when 2880..43199
|
||||
"#{(distance_in_minutes / 1440).round} days"
|
||||
|
||||
when 43200..86399
|
||||
'1 month'
|
||||
|
||||
when 86400..525959
|
||||
"#{(distance_in_minutes / 43200).round} months"
|
||||
|
||||
else
|
||||
"%.1f years" % (distance_in_minutes / 525960.0)
|
||||
end
|
||||
end
|
||||
|
||||
def content_for_prefix(name, &block)
|
||||
existing_content_for = instance_variable_get("@content_for_#{name}").to_s
|
||||
new_content_for = (block_given? ? capture(&block) : content) + existing_content_for
|
||||
instance_variable_set("@content_for_#{name}", new_content_for)
|
||||
end
|
||||
|
||||
def navigation_links(post)
|
||||
html = []
|
||||
|
||||
if post.is_a?(Post)
|
||||
html << tag("link", :rel => "prev", :title => "Previous Post", :href => url_for(:controller => "post", :action => "show", :id => post.id - 1))
|
||||
html << tag("link", :rel => "next", :title => "Next Post", :href => url_for(:controller => "post", :action => "show", :id => post.id + 1))
|
||||
|
||||
elsif post.is_a?(Array)
|
||||
posts = post
|
||||
|
||||
unless posts.previous_page.nil?
|
||||
html << tag("link", :href => url_for(params.merge(:page => 1)), :rel => "first", :title => "First Page")
|
||||
html << tag("link", :href => url_for(params.merge(:page => posts.previous_page)), :rel => "prev", :title => "Previous Page")
|
||||
end
|
||||
|
||||
unless posts.next_page.nil?
|
||||
html << tag("link", :href => url_for(params.merge(:page => posts.next_page)), :rel => "next", :title => "Next Page")
|
||||
html << tag("link", :href => url_for(params.merge(:page => posts.page_count)), :rel => "last", :title => "Last Page")
|
||||
end
|
||||
end
|
||||
|
||||
return html.join("\n")
|
||||
end
|
||||
|
||||
# Return true if the user can access the given level, or if creating an
|
||||
# account would. This is only actually used for actions that require
|
||||
# privileged or higher; it's assumed that starting_level is no lower
|
||||
# than member.
|
||||
def can_access?(level)
|
||||
needed_level = User.get_user_level(level)
|
||||
starting_level = CONFIG["starting_level"]
|
||||
user_level = @current_user.level
|
||||
return true if user_level.to_i >= needed_level
|
||||
return true if starting_level >= needed_level
|
||||
return false
|
||||
end
|
||||
|
||||
# Return true if the starting level is high enough to execute
|
||||
# this action. This is used by User.js.
|
||||
def need_signup?(level)
|
||||
needed_level = User.get_user_level(level)
|
||||
starting_level = CONFIG["starting_level"]
|
||||
return starting_level >= needed_level
|
||||
end
|
||||
|
||||
require "action_view/helpers/form_tag_helper.rb"
|
||||
include ActionView::Helpers::FormTagHelper
|
||||
alias_method :orig_form_tag, :form_tag
|
||||
def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block)
|
||||
# Add the need-signup class if signing up would allow a logged-out user to
|
||||
# execute this action. User.js uses this to determine whether it should ask
|
||||
# the user to create an account.
|
||||
if options[:level]
|
||||
if need_signup?(options[:level])
|
||||
classes = (options[:class] || "").split(" ")
|
||||
classes += ["need-signup"]
|
||||
options[:class] = classes.join(" ")
|
||||
end
|
||||
options.delete(:level)
|
||||
end
|
||||
|
||||
orig_form_tag url_for_options, options, *parameters_for_url, &block
|
||||
end
|
||||
|
||||
require "action_view/helpers/javascript_helper.rb"
|
||||
include ActionView::Helpers::JavaScriptHelper
|
||||
alias_method :orig_link_to_function, :link_to_function
|
||||
def link_to_function(name, *args, &block)
|
||||
html_options = args.extract_options!
|
||||
if html_options[:level]
|
||||
if need_signup?(html_options[:level]) && args[0]
|
||||
args[0] = "User.run_login(false, function() { #{args[0]} })"
|
||||
end
|
||||
html_options.delete(:level)
|
||||
end
|
||||
args = ([args] + [html_options])
|
||||
orig_link_to_function name, *args, &block
|
||||
end
|
||||
|
||||
alias_method :orig_button_to_function, :button_to_function
|
||||
def button_to_function(name, *args, &block)
|
||||
html_options = args.extract_options!
|
||||
if html_options[:level]
|
||||
if need_signup?(html_options[:level]) && args[0]
|
||||
args[0] = "User.run_login(false, function() { #{args[0]} })"
|
||||
end
|
||||
html_options.delete(:level)
|
||||
end
|
||||
args = ([args] + [html_options])
|
||||
orig_button_to_function name, *args, &block
|
||||
end
|
||||
|
||||
require "action_view/helpers/url_helper.rb"
|
||||
include ActionView::Helpers::UrlHelper
|
||||
|
||||
private
|
||||
alias_method :orig_convert_options_to_javascript!, :convert_options_to_javascript!
|
||||
# This handles link_to (and others, not tested).
|
||||
def convert_options_to_javascript!(html_options, url = '')
|
||||
level = html_options["level"]
|
||||
html_options.delete("level")
|
||||
orig_convert_options_to_javascript!(html_options, url)
|
||||
if level
|
||||
if need_signup?(level)
|
||||
html_options["onclick"] = "if(!User.run_login_onclick(event)) return false; #{html_options["onclick"] || "return true;"}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
2
app/helpers/artist_helper.rb
Normal file
2
app/helpers/artist_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module ArtistHelper
|
||||
end
|
30
app/helpers/avatar_helper.rb
Normal file
30
app/helpers/avatar_helper.rb
Normal file
@ -0,0 +1,30 @@
|
||||
module AvatarHelper
|
||||
# id is an identifier for the object referencing this avatar; it's passed down
|
||||
# to the javascripts to implement blacklisting "click again to open".
|
||||
def avatar(user, id, html_options = {})
|
||||
@shown_avatars ||= {}
|
||||
@posts_to_send ||= []
|
||||
|
||||
#if not @shown_avatars[user] then
|
||||
@shown_avatars[user] = true
|
||||
@posts_to_send << user.avatar_post
|
||||
img = image_tag(user.avatar_url + "?" + user.avatar_timestamp.tv_sec.to_s,
|
||||
{:class => "avatar", :width => user.avatar_width, :height => user.avatar_height}.merge(html_options))
|
||||
link_to(img,
|
||||
{ :controller => "post", :action => "show", :id => user.avatar_post.id.to_s },
|
||||
:class => "ca" + user.avatar_post.id.to_s,
|
||||
:onclick => %{return Post.check_avatar_blacklist(#{user.avatar_post.id.to_s}, #{id});})
|
||||
#end
|
||||
end
|
||||
|
||||
def avatar_init
|
||||
return "" if not defined?(@posts_to_send)
|
||||
ret = ""
|
||||
@posts_to_send.uniq.each do |post|
|
||||
ret << %{Post.register(#{ post.to_json })\n}
|
||||
end
|
||||
ret << %{Post.init_blacklisted()}
|
||||
ret
|
||||
end
|
||||
end
|
||||
|
40
app/helpers/cache_helper.rb
Normal file
40
app/helpers/cache_helper.rb
Normal file
@ -0,0 +1,40 @@
|
||||
module CacheHelper
|
||||
def build_cache_key(base, tags, page, limit, options = {})
|
||||
page = page.to_i
|
||||
page = 1 if page < 1
|
||||
tags = tags.to_s.downcase.scan(/\S+/).sort
|
||||
|
||||
if options[:user]
|
||||
user_level = options[:user].level
|
||||
user_level = CONFIG["user_levels"]["Member"] if user_level < CONFIG["user_levels"]["Member"]
|
||||
else
|
||||
user_level = "?"
|
||||
end
|
||||
|
||||
if tags.empty? || tags.any? {|x| x =~ /(?:^-|[*:])/}
|
||||
version = Cache.get("$cache_version").to_i
|
||||
tags = tags.join(",")
|
||||
else
|
||||
version = "?"
|
||||
tags = tags.map {|x| x + ":" + Cache.get("tag:#{x}").to_i.to_s}.join(",")
|
||||
end
|
||||
|
||||
["#{base}/v=#{version}&t=#{tags}&p=#{page}&ul=#{user_level}&l=#{limit}", 0]
|
||||
end
|
||||
|
||||
def get_cache_key(controller_name, action_name, params, options = {})
|
||||
case "#{controller_name}/#{action_name}"
|
||||
when "post/index"
|
||||
build_cache_key("p/i", params[:tags], params[:page], params[:limit], options)
|
||||
|
||||
when "post/atom"
|
||||
build_cache_key("p/a", params[:tags], 1, "", options)
|
||||
|
||||
when "post/piclens"
|
||||
build_cache_key("p/p", params[:tags], params[:page], params[:limit], options)
|
||||
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
2
app/helpers/comment_helper.rb
Normal file
2
app/helpers/comment_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module CommentHelper
|
||||
end
|
2
app/helpers/dmail_helper.rb
Normal file
2
app/helpers/dmail_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module DmailHelper
|
||||
end
|
24
app/helpers/favorite_helper.rb
Normal file
24
app/helpers/favorite_helper.rb
Normal file
@ -0,0 +1,24 @@
|
||||
module FavoriteHelper
|
||||
def favorite_list(post)
|
||||
html = ""
|
||||
|
||||
users = post.favorited_by
|
||||
|
||||
if users.empty?
|
||||
html << "no one"
|
||||
else
|
||||
html << users.slice(0, 6).map {|user| link_to(ERB::Util.h(user.pretty_name), :controller => "user", :action => "show", :id => user.id)}.join(", ")
|
||||
|
||||
if users.size > 6
|
||||
html << content_tag("span", :id => "remaining-favs", :style => "display: none;") do
|
||||
", " + users.slice(6..-1).map {|user| link_to(ERB::Util.h(user.pretty_name), {:controller => "user", :action => "show", :id => user.id})}.join(", ")
|
||||
end
|
||||
html << content_tag("span", :id => "remaining-favs-link") do
|
||||
" (" + link_to_function("#{users.size - 6} more", "$('remaining-favs').show(); $('remaining-favs-link').hide()") + ")"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return html
|
||||
end
|
||||
end
|
2
app/helpers/forum_helper.rb
Normal file
2
app/helpers/forum_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module ForumHelper
|
||||
end
|
372
app/helpers/history_helper.rb
Normal file
372
app/helpers/history_helper.rb
Normal file
@ -0,0 +1,372 @@
|
||||
require 'post_helper'
|
||||
require 'diff'
|
||||
|
||||
module HistoryHelper
|
||||
include PostHelper
|
||||
# :all: By default, some changes are not displayed. When displaying details
|
||||
# for a single change, set :all=>true to display all changes.
|
||||
#
|
||||
# :show_all_tags: Show unchanged tags.
|
||||
def get_default_field_options
|
||||
@default_field_options ||= {
|
||||
:suppress_fields => [],
|
||||
}
|
||||
end
|
||||
|
||||
def get_attribute_options
|
||||
return @att_options if @att_options
|
||||
|
||||
@att_options = {
|
||||
# :suppress_fields => If this attribute was changed, don't display changes to specified
|
||||
# fields to the same object in the same change.
|
||||
#
|
||||
# :force_show_initial => For initial changes, created when the object itself is created,
|
||||
# attributes that are set to an explicit :default are omitted from the display. This
|
||||
# prevents things like "parent:none" being shown for every new post. Set :force_show_initial
|
||||
# to override this behavior.
|
||||
#
|
||||
# :primary_order => Changes are sorted alphabetically by field name. :primary_order
|
||||
# overrides this sorting with a top-level sort (default 1).
|
||||
#
|
||||
# :never_obsolete => Changes that are no longer current or have been reverted are
|
||||
# given the class "obsolete". Changes in fields named by :never_obsolete are not
|
||||
# tested.
|
||||
#
|
||||
# Some cases:
|
||||
#
|
||||
# - When viewing a single object (eg. "post:123"), the display is always changed to
|
||||
# the appropriate type, so if we're viewing a single object, :specific_table will
|
||||
# always be true.
|
||||
#
|
||||
# - Changes to pool descriptions can be large, and are reduced to "description changed"
|
||||
# in the "All" view. The diff is displayed if viewing the Pool view or a specific object.
|
||||
#
|
||||
# - Adding a post to a pool usually causes the sequence number to change, too, but
|
||||
# this isn't very interesting and clutters the display. :suppress_fields is used
|
||||
# to hide these unless viewing the specific change.
|
||||
:Post => {
|
||||
:fields => {
|
||||
:cached_tags => { :primary_order => 2 }, # show tag changes after other things
|
||||
:source => { :primary_order => 3 },
|
||||
},
|
||||
:never_obsolete => {:cached_tags=>true} # tags handle obsolete themselves per-tag
|
||||
},
|
||||
|
||||
:Pool => {
|
||||
:primary_order => 0,
|
||||
|
||||
:fields => {
|
||||
:description => { :primary_order => 5 } # we don't handle commas correctly if this isn't last
|
||||
},
|
||||
:never_obsolete => {:description=>true} # changes to description aren't obsolete just because the text has changed again
|
||||
},
|
||||
|
||||
:PoolPost => {
|
||||
:fields => {
|
||||
:sequence => { :max_to_display => 5 },
|
||||
:active => {
|
||||
:max_to_display => 10,
|
||||
:suppress_fields => [:sequence], # changing active usually changes sequence; this isn't interesting
|
||||
:primary_order => 2, # show pool post changes after other things
|
||||
},
|
||||
:cached_tags => { },
|
||||
},
|
||||
},
|
||||
|
||||
:Tag => {
|
||||
},
|
||||
}
|
||||
|
||||
@att_options.each_key { |classname|
|
||||
@att_options[classname] = {
|
||||
:fields => {},
|
||||
:primary_order => 1,
|
||||
:never_obsolete => {},
|
||||
:force_show_initial => {},
|
||||
}.merge(@att_options[classname])
|
||||
|
||||
c = @att_options[classname][:fields]
|
||||
c.each_key { |field|
|
||||
c[field] = get_default_field_options.merge(c[field])
|
||||
}
|
||||
}
|
||||
|
||||
return @att_options
|
||||
end
|
||||
|
||||
def format_changes(history, options={})
|
||||
html = ""
|
||||
|
||||
changes = history.history_changes
|
||||
|
||||
# Group the changes by class and field.
|
||||
change_groups = {}
|
||||
changes.each do |c|
|
||||
change_groups[c.table_name] ||= {}
|
||||
change_groups[c.table_name][c.field.to_sym] ||= []
|
||||
change_groups[c.table_name][c.field.to_sym] << c
|
||||
end
|
||||
|
||||
att_options = get_attribute_options
|
||||
|
||||
# Number of changes hidden (not including suppressions):
|
||||
hidden = 0
|
||||
parts = []
|
||||
change_groups.each do |table_name, fields|
|
||||
# Apply supressions.
|
||||
to_suppress = []
|
||||
fields.each do |field, group|
|
||||
class_name = group[0].master_class.to_s.to_sym
|
||||
table_options = att_options[class_name] ||= {}
|
||||
field_options = table_options[:fields][field] || get_default_field_options
|
||||
to_suppress += field_options[:suppress_fields]
|
||||
end
|
||||
|
||||
to_suppress.each { |suppress| fields.delete(suppress) }
|
||||
|
||||
fields.each do |field, group|
|
||||
class_name = group[0].master_class.to_s.to_sym
|
||||
table_options = att_options[class_name] ||= {}
|
||||
field_options = table_options[:fields][field] || get_default_field_options
|
||||
|
||||
# Check for entry limits.
|
||||
if not options[:specific_history]
|
||||
max = field_options[:max_to_display]
|
||||
if max && group.length > max
|
||||
hidden += group.length - max
|
||||
group = group[0,max] || []
|
||||
end
|
||||
end
|
||||
|
||||
# Format the rest.
|
||||
group.each do |c|
|
||||
if !c.previous && c.changes_to_default? && !table_options[:force_show_initial][field]
|
||||
next
|
||||
end
|
||||
|
||||
part = format_change(history, c, options, table_options).merge(:primary_order => field_options[:primary_order] || table_options[:primary_order])
|
||||
parts << part
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
parts.sort! { |a,b|
|
||||
comp = 0
|
||||
[:primary_order, :field, :sort_key].each { |field|
|
||||
comp = a[field] <=> b[field]
|
||||
break if comp != 0
|
||||
}
|
||||
comp
|
||||
}
|
||||
|
||||
parts.each_index { |idx|
|
||||
next if idx == 0
|
||||
next if parts[idx][:field] == parts[idx-1][:field]
|
||||
parts[idx-1][:html] << ", "
|
||||
}
|
||||
|
||||
html = ""
|
||||
|
||||
if !options[:show_name] && history.group_by_table == "tags"
|
||||
p history.history_changes.first.obj.pretty_name
|
||||
tag = history.history_changes.first.obj
|
||||
html << tag_link(tag.name)
|
||||
html << ": "
|
||||
end
|
||||
|
||||
html << parts.map { |part| part[:html] }.join(" ")
|
||||
|
||||
if hidden > 0
|
||||
html << " (#{link_to("%i more..." % hidden, :action => @params[:action], :search => "change:%i" % history.id)})"
|
||||
end
|
||||
|
||||
return html
|
||||
end
|
||||
|
||||
def format_change(history, change, options, table_options)
|
||||
html = ""
|
||||
|
||||
classes = []
|
||||
if !table_options[:never_obsolete][change.field.to_sym] && change.is_obsolete? then
|
||||
classes << ["obsolete"]
|
||||
end
|
||||
|
||||
added = %{<span class="added">+</span>}
|
||||
removed = %{<span class="removed">-</span>}
|
||||
|
||||
sort_key = change.remote_id
|
||||
primary_order = 1
|
||||
case change.table_name
|
||||
when"posts"
|
||||
case change.field
|
||||
when "rating"
|
||||
html << %{<span class="changed-post-rating">rating:}
|
||||
html << change.value
|
||||
if change.previous then
|
||||
html << %{←}
|
||||
html << change.previous.value
|
||||
end
|
||||
html << %{</span>}
|
||||
when "parent_id"
|
||||
html << "parent:"
|
||||
if change.value
|
||||
begin
|
||||
new = Post.find(change.value.to_i)
|
||||
html << link_to("%i" % new.id, :controller => "post", :action => "show", :id => new.id)
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
html << "%i" % change.value.to_i
|
||||
end
|
||||
else
|
||||
html << "none"
|
||||
end
|
||||
|
||||
if change.previous
|
||||
html << %{←}
|
||||
if change.previous.value
|
||||
begin
|
||||
old = Post.find(change.previous.value.to_i)
|
||||
html << link_to("%i" % old.id, :controller => "post", :action => "show", :id => old.id)
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
html << "%i" % change.previous.value.to_i
|
||||
end
|
||||
else
|
||||
html << "none"
|
||||
end
|
||||
end
|
||||
|
||||
when "source"
|
||||
if change.previous
|
||||
html << "source changed from <span class='name-change'>%s</span> to <span class='name-change'>%s</span>" % [source_link(change.previous.value, false), source_link(change.value, false)]
|
||||
else
|
||||
html << "source: <span class='name-change'>%s</span>" % [source_link(change.value, false)]
|
||||
end
|
||||
|
||||
when "is_rating_locked"
|
||||
html << (change.value == 't' ? added : removed)
|
||||
html << "rating-locked"
|
||||
|
||||
when "is_note_locked"
|
||||
html << (change.value == 't' ? added : removed)
|
||||
html << "note-locked"
|
||||
|
||||
when "is_shown_in_index"
|
||||
html << (change.value == 't' ? added : removed)
|
||||
html << "shown"
|
||||
|
||||
when "cached_tags"
|
||||
previous = change.previous
|
||||
|
||||
changes = Post.tag_changes(change, previous, change.latest)
|
||||
|
||||
list = []
|
||||
list += tag_list(changes[:added_tags], :obsolete => changes[:obsolete_added_tags], :prefix => "+", :class => "added")
|
||||
list += tag_list(changes[:removed_tags], :obsolete => changes[:obsolete_removed_tags], :prefix=>"-", :class => "removed")
|
||||
|
||||
if options[:show_all_tags]
|
||||
list += tag_list(changes[:unchanged_tags], :prefix => "", :class => "unchanged")
|
||||
end
|
||||
html << list.join(" ")
|
||||
end
|
||||
when "pools"
|
||||
primary_order = 0
|
||||
|
||||
case change.field
|
||||
when "name"
|
||||
if change.previous
|
||||
html << "name changed from <span class='name-change'>%s</span> to <span class='name-change'>%s</span>" % [h(change.previous.value), h(change.value)]
|
||||
else
|
||||
html << "name: <span class='name-change'>%s</span>" % [h(change.value)]
|
||||
end
|
||||
|
||||
when "description"
|
||||
if options[:specific_history] || options[:specific_table]
|
||||
if change.previous
|
||||
html << "description changed:<div class='diff text-block'>#{Danbooru.diff(change.previous.value, change.value)}</div>"
|
||||
else
|
||||
html << "description:<div class='initial-diff text-block'>#{change.value}</div>"
|
||||
end
|
||||
else
|
||||
html << "description changed"
|
||||
end
|
||||
when "is_public"
|
||||
html << (change.value == 't' ? added : removed)
|
||||
html << "public"
|
||||
when "is_active"
|
||||
html << (change.value == 't' ? added : removed)
|
||||
html << "active"
|
||||
end
|
||||
when "pools_posts"
|
||||
# Sort the output by the post id.
|
||||
sort_key = change.obj.post.id
|
||||
case change.field
|
||||
when "active"
|
||||
html << (change.value == 't' ? added : removed)
|
||||
|
||||
html << link_to("post #%i" % change.obj.post_id, :controller => "post", :action => "show", :id => change.obj.post_id)
|
||||
|
||||
when "sequence"
|
||||
seq = "order:%i:%s" % [change.obj.post_id, change.value]
|
||||
if change.previous then
|
||||
seq << %{←#{change.previous.value}}
|
||||
end
|
||||
html << link_to("%s" % seq, :controller => "post", :action => "show", :id => change.obj.post_id)
|
||||
end
|
||||
when "tags"
|
||||
case change.field
|
||||
when "tag_type"
|
||||
html << "type:"
|
||||
tag_type = Tag.type_name_from_value(change.value.to_i)
|
||||
html << %{<span class="tag-type-#{tag_type}">#{tag_type}</span>}
|
||||
if change.previous then
|
||||
tag_type = Tag.type_name_from_value(change.previous.value.to_i)
|
||||
html << %{←<span class="tag-type-#{tag_type}">#{tag_type}</span>}
|
||||
end
|
||||
when "is_ambiguous"
|
||||
html << (change.value == 't' ? added : removed)
|
||||
html << "ambiguous"
|
||||
end
|
||||
end
|
||||
|
||||
span = ""
|
||||
span << %{<span class="#{classes.join(" ")}">#{html}</span>}
|
||||
|
||||
return {
|
||||
:html => span,
|
||||
:field => change.field,
|
||||
:sort_key => sort_key,
|
||||
}
|
||||
end
|
||||
|
||||
def tag_link(name, options = {})
|
||||
name ||= "UNKNOWN"
|
||||
prefix = options[:prefix] || ""
|
||||
obsolete = options[:obsolete] || []
|
||||
|
||||
tag_type = Tag.type_name(name)
|
||||
|
||||
obsolete_tag = ([name] & obsolete).empty? ? "":" obsolete"
|
||||
tag = ""
|
||||
tag << %{<span class="tag-type-#{tag_type}#{obsolete_tag}">}
|
||||
tag << %{#{prefix}<a href="/post/index?tags=#{u(name)}">#{h(name)}</a>}
|
||||
tag << '</span>'
|
||||
tag
|
||||
end
|
||||
|
||||
def tag_list(tags, options = {})
|
||||
return [] if tags.blank?
|
||||
|
||||
html = ""
|
||||
html << %{<span class="#{ options[:class] }">}
|
||||
|
||||
tags_html = []
|
||||
tags.each do |name|
|
||||
tags_html << tag_link(name, options)
|
||||
end
|
||||
|
||||
return [] if tags_html.empty?
|
||||
|
||||
html << tags_html.join(" ")
|
||||
html << %{</span>}
|
||||
return [html]
|
||||
end
|
||||
end
|
15
app/helpers/inline_helper.rb
Normal file
15
app/helpers/inline_helper.rb
Normal file
@ -0,0 +1,15 @@
|
||||
module InlineHelper
|
||||
def inline_image_tag(image, options = {}, tag_options = {})
|
||||
if options[:use_sample] and image.has_sample?
|
||||
url = image.sample_url
|
||||
tag_options[:width] = image.sample_width
|
||||
tag_options[:height] = image.sample_height
|
||||
else
|
||||
url = image.file_url
|
||||
tag_options[:width] = image.width
|
||||
tag_options[:height] = image.height
|
||||
end
|
||||
|
||||
return image_tag(url, tag_options)
|
||||
end
|
||||
end
|
2
app/helpers/invite_helper.rb
Normal file
2
app/helpers/invite_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module InviteHelper
|
||||
end
|
2
app/helpers/job_task_helper.rb
Normal file
2
app/helpers/job_task_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module JobTaskHelper
|
||||
end
|
2
app/helpers/note_helper.rb
Normal file
2
app/helpers/note_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module NoteHelper
|
||||
end
|
25
app/helpers/pool_helper.rb
Normal file
25
app/helpers/pool_helper.rb
Normal file
@ -0,0 +1,25 @@
|
||||
module PoolHelper
|
||||
def pool_list(post)
|
||||
html = ""
|
||||
pools = Pool.find(:all, :joins => "JOIN pools_posts ON pools_posts.pool_id = pools.id", :conditions => "pools_posts.post_id = #{post.id}", :order => "pools.name", :select => "pools.name, pools.id")
|
||||
|
||||
if pools.empty?
|
||||
html << "none"
|
||||
else
|
||||
html << pools.map {|p| link_to(h(p.pretty_name), :controller => "pool", :action => "show", :id => p.id)}.join(", ")
|
||||
end
|
||||
|
||||
return html
|
||||
end
|
||||
|
||||
def link_to_pool_zip(text, pool, zip_params, options={})
|
||||
text = "%s%s (%s)" % [text,
|
||||
options[:has_jpeg]? " PNGs":"",
|
||||
number_to_human_size(pool.get_zip_size(zip_params)),
|
||||
]
|
||||
options = { :action => "zip", :id => pool.id, :filename => pool.get_zip_filename(zip_params) }
|
||||
options[:originals] = 1 if zip_params[:originals]
|
||||
options[:jpeg] = 1 if zip_params[:jpeg]
|
||||
link_to text, options, :level => :member
|
||||
end
|
||||
end
|
144
app/helpers/post_helper.rb
Normal file
144
app/helpers/post_helper.rb
Normal file
@ -0,0 +1,144 @@
|
||||
module PostHelper
|
||||
def source_link(source, abbreviate = true)
|
||||
if source.empty?
|
||||
"none"
|
||||
elsif source[/^http/]
|
||||
text = source
|
||||
text = text[7, 20] + "..." if abbreviate
|
||||
link_to text, source
|
||||
else
|
||||
source
|
||||
end
|
||||
end
|
||||
|
||||
def auto_discovery_link_tag_with_id(type = :rss, url_options = {}, tag_options = {})
|
||||
tag(
|
||||
"link",
|
||||
"rel" => tag_options[:rel] || "alternate",
|
||||
"type" => tag_options[:type] || "application/#{type}+xml",
|
||||
"title" => tag_options[:title] || type.to_s.upcase,
|
||||
"id" => tag_options[:id],
|
||||
"href" => url_options.is_a?(Hash) ? url_for(url_options.merge(:only_path => false)) : url_options
|
||||
)
|
||||
end
|
||||
|
||||
def print_preview(post, options = {})
|
||||
unless CONFIG["can_see_post"].call(@current_user, post)
|
||||
return ""
|
||||
end
|
||||
|
||||
image_class = "preview"
|
||||
image_class += " flagged" if post.is_flagged?
|
||||
image_class += " pending" if post.is_pending?
|
||||
image_class += " has-children" if post.has_children?
|
||||
image_class += " has-parent" if post.parent_id
|
||||
image_id = options[:image_id]
|
||||
image_id = %{id="#{h(image_id)}"} if image_id
|
||||
image_title = h("Rating: #{post.pretty_rating} Score: #{post.score} Tags: #{h(post.cached_tags)} User:#{post.author}")
|
||||
link_onclick = options[:onclick]
|
||||
link_onclick = %{onclick="#{link_onclick}"} if link_onclick
|
||||
link_onmouseover = %{ onmouseover="#{options[:onmouseover]}"} if options[:onmouseover]
|
||||
link_onmouseout = %{ onmouseout="#{options[:onmouseout]}"} if options[:onmouseout]
|
||||
width, height = post.preview_dimensions
|
||||
|
||||
image = %{<img src="#{post.preview_url}" alt="#{image_title}" class="#{image_class}" title="#{image_title}" #{image_id} width="#{width}" height="#{height}">}
|
||||
plid = %{<span class="plid">#pl http://#{h CONFIG["server_host"]}/post/show/#{post.id}</span>}
|
||||
link = %{<a href="/post/show/#{post.id}/#{u(post.tag_title)}" #{link_onclick}#{link_onmouseover}#{link_onmouseout}>#{image}#{plid}</a>}
|
||||
span = %{<span class="thumb">#{link}</span>}
|
||||
|
||||
directlink = if options[:similarity]
|
||||
icon = %{<img src="/favicon.ico" class="service-icon" id="source">}
|
||||
size = %{ (#{post.width}x#{post.height})}
|
||||
|
||||
similarity_class = "similar similar_directlink"
|
||||
similarity_class += " similar_original" if options[:similarity].to_s == "Original"
|
||||
similarity_class += " similar-match" if options[:similarity].to_f >= 90 rescue false
|
||||
similarity_text = options[:similarity].to_s == "Original"?
|
||||
(if @initial then "Your post" else "Original" end):
|
||||
%{#{options[:similarity].to_i}%}
|
||||
|
||||
%{<a class="#{similarity_class}" href="#{post.file_url}"><span>#{icon}#{similarity_text}#{size}</span></a>}
|
||||
else
|
||||
if post.width.to_i > 1500 or post.height.to_i > 1500
|
||||
%{<a class="directlink largeimg" href="#{post.file_url}"><span>#{post.width} x #{post.height}</span></a>}
|
||||
else
|
||||
%{<a class="directlink" href="#{post.file_url}"><span>#{post.width} x #{post.height}</span></a>}
|
||||
end
|
||||
end
|
||||
directlink = "" if options[:hide_directlink]
|
||||
|
||||
li_class = ""
|
||||
li_class += " javascript-hide" if options[:blacklisting]
|
||||
li_class += " creator-id-#{post.user_id}"
|
||||
item = %{<li id="p#{post.id}" class="#{li_class}">#{span}#{directlink}</li>}
|
||||
return item
|
||||
end
|
||||
|
||||
def print_ext_similarity_preview(post, options = {})
|
||||
image_class = "preview external"
|
||||
width, height = post.preview_dimensions
|
||||
|
||||
image = %{<img src="#{post.preview_url}" alt="#{(post.md5)}" class="#{image_class} width="#{width}" height="#{height}">}
|
||||
link = %{<a href="#{post.url}">#{image}</a>}
|
||||
icon = %{<img src="#{post.service_icon}" alt="#{post.service}" class="service-icon" id="source">}
|
||||
span = %{<span class="thumb">#{link}</span>}
|
||||
|
||||
size = if post.width > 0 then (%{ (#{post.width}x#{post.height})}) else "" end
|
||||
similarity_class = "similar"
|
||||
similarity_class += " similar-match" if options[:similarity].to_f >= 90 rescue false
|
||||
similarity_class += " similar_original" if options[:similarity].to_s == "Original"
|
||||
similarity_text = options[:similarity].to_s == "Original"? "Image":%{#{options[:similarity].to_i}%}
|
||||
similarity = %{<a class="#{similarity_class}" href="#{post.url}"><span>#{icon}#{similarity_text}#{size}</span></a>}
|
||||
item = %{<li id="p#{post.id}">#{span}#{similarity}</li>}
|
||||
return item
|
||||
end
|
||||
|
||||
def vote_tooltip_widget(post)
|
||||
return %{<span class="vote-desc" id="vote-desc-#{post.id}"></span>}
|
||||
end
|
||||
|
||||
def vote_widget(post, user, options = {})
|
||||
html = []
|
||||
|
||||
html << %{<span class="stars" id="stars-#{post.id}">}
|
||||
|
||||
if user.is_anonymous?
|
||||
current_user_vote = -100
|
||||
else
|
||||
current_user_vote = PostVotes.find_by_ids(user.id, post.id).score rescue 0
|
||||
end
|
||||
|
||||
#(CONFIG["vote_sum_min"]..CONFIG["vote_sum_max"]).each do |vote|
|
||||
if !user.is_anonymous?
|
||||
html << link_to_function('↶', "Post.vote(#{post.id}, 0)", :class => "star", :onmouseover => "Post.vote_mouse_over('Remove vote', #{post.id}, 0)", :onmouseout => "Post.vote_mouse_out('', #{post.id}, 0)")
|
||||
html << " "
|
||||
|
||||
(1..3).each do |vote|
|
||||
star = '<span class="score-on">★</span><span class="score-off score-visible">☆</span>'
|
||||
|
||||
desc = CONFIG["vote_descriptions"][vote]
|
||||
|
||||
html << link_to_function(star, "Post.vote(#{post.id}, #{vote})", :class => "star star-#{vote}", :id => "star-#{vote}-#{post.id}", :onmouseover => "Post.vote_mouse_over('#{desc}', #{post.id}, #{vote})", :onmouseout => "Post.vote_mouse_out('#{desc}', #{post.id}, #{vote})")
|
||||
end
|
||||
|
||||
html << " (" + link_to_function('vote up', "Post.vote(#{post.id}, Post.posts.get(#{post.id}).vote + 1)", :class => "star") + ")"
|
||||
else
|
||||
html << "(" + link_to_function('vote up', "Post.vote(#{post.id}, +1)", :class => "star") + ")"
|
||||
end
|
||||
|
||||
html << %{</span>}
|
||||
return html
|
||||
end
|
||||
|
||||
def get_tag_types(posts)
|
||||
post_tags = []
|
||||
posts.each { |post| post_tags += post.cached_tags.split(/ /) }
|
||||
tag_types = {}
|
||||
post_tags.uniq.each { |tag| tag_types[tag] = Tag.type_name(tag) }
|
||||
return tag_types
|
||||
end
|
||||
|
||||
def get_service_icon(service)
|
||||
ExternalPost.get_service_icon(service)
|
||||
end
|
||||
end
|
35
app/helpers/post_tag_history_helper.rb
Normal file
35
app/helpers/post_tag_history_helper.rb
Normal file
@ -0,0 +1,35 @@
|
||||
module PostTagHistoryHelper
|
||||
def tag_list(tags, options = {})
|
||||
return "" if tags.blank?
|
||||
prefix = options[:prefix] || ""
|
||||
obsolete = options[:obsolete] || []
|
||||
|
||||
html = ""
|
||||
|
||||
# tags contains versioned metatags; split these out.
|
||||
metatags, tags = tags.partition {|x| x=~ /^(?:rating):/}
|
||||
metatags.each do |name|
|
||||
obsolete_tag = ([name] & obsolete).empty? ? "":" obsolete-tag-change"
|
||||
html << %{<span class="tag-type-meta#{obsolete_tag}">}
|
||||
|
||||
html << %{#{prefix}<a href="/post/index?tags=#{u(name)}">#{h(name)}</a> }
|
||||
html << '</span>'
|
||||
end
|
||||
|
||||
tags = Tag.find(:all, :conditions => ["name in (?)", tags], :select => "name").inject([]) {|all, x| all << x.name; all}.to_a.sort {|a, b| a <=> b}
|
||||
|
||||
tags.each do |name|
|
||||
name ||= "UNKNOWN"
|
||||
|
||||
tag_type = Tag.type_name(name)
|
||||
|
||||
obsolete_tag = ([name] & obsolete).empty? ? "":" obsolete-tag-change"
|
||||
html << %{<span class="tag-type-#{tag_type}#{obsolete_tag}">}
|
||||
|
||||
html << %{#{prefix}<a href="/post/index?tags=#{u(name)}">#{h(name)}</a> }
|
||||
html << '</span>'
|
||||
end
|
||||
|
||||
return html
|
||||
end
|
||||
end
|
2
app/helpers/report_helper.rb
Normal file
2
app/helpers/report_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module ReportHelper
|
||||
end
|
2
app/helpers/static_helper.rb
Normal file
2
app/helpers/static_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module StaticHelper
|
||||
end
|
2
app/helpers/tag_alias_helper.rb
Normal file
2
app/helpers/tag_alias_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module TagAliasHelper
|
||||
end
|
91
app/helpers/tag_helper.rb
Normal file
91
app/helpers/tag_helper.rb
Normal file
@ -0,0 +1,91 @@
|
||||
module TagHelper
|
||||
def tag_link(tag)
|
||||
tag_type = Tag.type_name(tag)
|
||||
html = %{<span class="tag-type-#{tag_type}">}
|
||||
html << link_to(h(tag), :action => "index", :tags => tag)
|
||||
html << %{</span>}
|
||||
end
|
||||
|
||||
def tag_links(tags, options = {})
|
||||
return "" if tags.blank?
|
||||
prefix = options[:prefix] || ""
|
||||
|
||||
html = ""
|
||||
|
||||
case tags[0]
|
||||
when String
|
||||
tags = Tag.find(:all, :conditions => ["name in (?)", tags], :select => "name, post_count, id").inject({}) {|all, x| all[x.name] = [x.post_count, x.id]; all}.sort {|a, b| a[0] <=> b[0]}.map { |a| [a[0], a[1][0], a[1][1]] }
|
||||
|
||||
when Hash
|
||||
tags = tags.map {|x| [x["name"], x["post_count"], nil]}
|
||||
|
||||
when Tag
|
||||
tags = tags.map {|x| [x.name, x.post_count, x.id]}
|
||||
end
|
||||
|
||||
tags.each do |name, count, id|
|
||||
name ||= "UNKNOWN"
|
||||
|
||||
tag_type = Tag.type_name(name)
|
||||
|
||||
html << %{<li class="tag-type-#{tag_type}">}
|
||||
|
||||
if CONFIG["enable_artists"] && tag_type == "artist"
|
||||
html << %{<a href="/artist/show?name=#{u(name)}">?</a> }
|
||||
else
|
||||
html << %{<a href="/wiki/show?title=#{u(name)}">?</a> }
|
||||
end
|
||||
|
||||
if @current_user.is_privileged_or_higher?
|
||||
html << %{<a href="/post/index?tags=#{u(name)}+#{u(params[:tags])}">+</a> }
|
||||
html << %{<a href="/post/index?tags=-#{u(name)}+#{u(params[:tags])}">–</a> }
|
||||
end
|
||||
|
||||
if options[:with_hover_highlight] then
|
||||
mouseover=%{ onmouseover='Post.highlight_posts_with_tag("#{escape_javascript(name).gsub("'", "‘")}")'}
|
||||
mouseout=%{ onmouseout='Post.highlight_posts_with_tag(null)'}
|
||||
end
|
||||
html << %{<a href="/post/index?tags=#{u(name)}"#{mouseover}#{mouseout}>#{h(name.tr("_", " "))}</a> }
|
||||
html << %{<span class="post-count">#{count}</span> }
|
||||
html << '</li>'
|
||||
end
|
||||
|
||||
if options[:with_aliases] then
|
||||
# Map tags to aliases to the tag, and include the original tag so search engines can
|
||||
# find it.
|
||||
id_list = tags.map { |t| t[2] }
|
||||
alternate_tags = TagAlias.find(:all, :select => :name, :conditions => ["alias_id IN (?)", id_list]).map { |t| t.name }.uniq
|
||||
if not alternate_tags.empty?
|
||||
html << %{<span style="display: none;">#{alternate_tags.map { |t| t.tr("_", " ") }.join(" ")}</span>}
|
||||
end
|
||||
end
|
||||
|
||||
return html
|
||||
end
|
||||
|
||||
def cloud_view(tags, divisor = 6)
|
||||
html = ""
|
||||
|
||||
tags.sort {|a, b| a["name"] <=> b["name"]}.each do |tag|
|
||||
size = Math.log(tag["post_count"].to_i) / divisor
|
||||
size = 0.8 if size < 0.8
|
||||
html << %{<a href="/post/index?tags=#{u(tag["name"])}" style="font-size: #{size}em;" title="#{tag["post_count"]} posts">#{h(tag["name"])}</a> }
|
||||
end
|
||||
|
||||
return html
|
||||
end
|
||||
|
||||
def related_tags(tags)
|
||||
if tags.blank?
|
||||
return ""
|
||||
end
|
||||
|
||||
all = []
|
||||
pattern, related = tags.split(/\s+/).partition {|i| i.include?("*")}
|
||||
pattern.each {|i| all += Tag.find(:all, :conditions => ["name LIKE ?", i.tr("*", "%")]).map {|j| j.name}}
|
||||
if related.any?
|
||||
Tag.find(:all, :conditions => ["name IN (?)", TagAlias.to_aliased(related)]).each {|i| all += i.related.map {|j| j[0]}}
|
||||
end
|
||||
all.join(" ")
|
||||
end
|
||||
end
|
2
app/helpers/tag_implication_helper.rb
Normal file
2
app/helpers/tag_implication_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module TagImplicationHelper
|
||||
end
|
2
app/helpers/user_helper.rb
Normal file
2
app/helpers/user_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module UserHelper
|
||||
end
|
9
app/helpers/wiki_helper.rb
Normal file
9
app/helpers/wiki_helper.rb
Normal file
@ -0,0 +1,9 @@
|
||||
module WikiHelper
|
||||
def linked_from(to)
|
||||
links = to.find_pages_that_link_to_this.map do |page|
|
||||
link_to(h(page.pretty_title), :controller => "wiki", :action => "show", :title => page.title)
|
||||
end.join(", ")
|
||||
|
||||
links.empty? ? "None" : links
|
||||
end
|
||||
end
|
3
app/models/advertisement.rb
Normal file
3
app/models/advertisement.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class Advertisement < ActiveRecord::Base
|
||||
validates_inclusion_of :ad_type, :in => %w(horizontal vertical)
|
||||
end
|
244
app/models/artist.rb
Normal file
244
app/models/artist.rb
Normal file
@ -0,0 +1,244 @@
|
||||
class Artist < ActiveRecord::Base
|
||||
module UrlMethods
|
||||
module ClassMethods
|
||||
def find_all_by_url(url)
|
||||
url = ArtistUrl.normalize(url)
|
||||
artists = []
|
||||
|
||||
while artists.empty? && url.size > 10
|
||||
u = url.to_escaped_for_sql_like.gsub(/\*/, '%') + '%'
|
||||
artists += Artist.find(:all, :joins => "JOIN artist_urls ON artist_urls.artist_id = artists.id", :conditions => ["artists.alias_id IS NULL AND artist_urls.normalized_url LIKE ? ESCAPE E'\\\\'", u], :order => "artists.name")
|
||||
|
||||
# Remove duplicates based on name
|
||||
artists = artists.inject({}) {|all, artist| all[artist.name] = artist ; all}.values
|
||||
url = File.dirname(url)
|
||||
end
|
||||
|
||||
return artists[0, 20]
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(m)
|
||||
m.extend(ClassMethods)
|
||||
m.after_save :commit_urls
|
||||
m.has_many :artist_urls, :dependent => :delete_all
|
||||
end
|
||||
|
||||
def commit_urls
|
||||
if @urls
|
||||
artist_urls.clear
|
||||
|
||||
@urls.scan(/\S+/).each do |url|
|
||||
artist_urls.create(:url => url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def urls=(urls)
|
||||
@urls = urls
|
||||
end
|
||||
|
||||
def urls
|
||||
artist_urls.map {|x| x.url}.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
module NoteMethods
|
||||
def self.included(m)
|
||||
m.after_save :commit_notes
|
||||
end
|
||||
|
||||
def wiki_page
|
||||
WikiPage.find_page(name)
|
||||
end
|
||||
|
||||
def notes_locked?
|
||||
wiki_page.is_locked? rescue false
|
||||
end
|
||||
|
||||
def notes
|
||||
wiki_page.body rescue ""
|
||||
end
|
||||
|
||||
def notes=(text)
|
||||
@notes = text
|
||||
end
|
||||
|
||||
def commit_notes
|
||||
unless @notes.blank?
|
||||
if wiki_page.nil?
|
||||
WikiPage.create(:title => name, :body => @notes, :ip_addr => updater_ip_addr, :user_id => updater_id)
|
||||
elsif wiki_page.is_locked?
|
||||
errors.add(:notes, "are locked")
|
||||
else
|
||||
wiki_page.update_attributes(:body => @notes, :ip_addr => updater_ip_addr, :user_id => updater_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module AliasMethods
|
||||
def self.included(m)
|
||||
m.after_save :commit_aliases
|
||||
end
|
||||
|
||||
def commit_aliases
|
||||
transaction do
|
||||
connection.execute("UPDATE artists SET alias_id = NULL WHERE alias_id = #{id}")
|
||||
|
||||
if @alias_names
|
||||
@alias_names.each do |name|
|
||||
a = Artist.find_or_create_by_name(name)
|
||||
a.update_attributes(:alias_id => id, :updater_id => updater_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def alias_names=(names)
|
||||
@alias_names = names.split(/\s*,\s*/)
|
||||
end
|
||||
|
||||
def alias_names
|
||||
aliases.map(&:name).join(", ")
|
||||
end
|
||||
|
||||
def aliases
|
||||
if new_record?
|
||||
return []
|
||||
else
|
||||
return Artist.find(:all, :conditions => "alias_id = #{id}", :order => "name")
|
||||
end
|
||||
end
|
||||
|
||||
def alias_name
|
||||
if alias_id
|
||||
begin
|
||||
return Artist.find(alias_id).name
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
def alias_name=(name)
|
||||
if name.blank?
|
||||
self.alias_id = nil
|
||||
else
|
||||
artist = Artist.find_or_create_by_name(name)
|
||||
self.alias_id = artist.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module GroupMethods
|
||||
def self.included(m)
|
||||
m.after_save :commit_members
|
||||
end
|
||||
|
||||
def commit_members
|
||||
transaction do
|
||||
connection.execute("UPDATE artists SET group_id = NULL WHERE group_id = #{id}")
|
||||
|
||||
if @member_names
|
||||
@member_names.each do |name|
|
||||
a = Artist.find_or_create_by_name(name)
|
||||
a.update_attributes(:group_id => id, :updater_id => updater_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def group_name
|
||||
if group_id
|
||||
return Artist.find(group_id).name
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def members
|
||||
if new_record?
|
||||
return []
|
||||
else
|
||||
Artist.find(:all, :conditions => "group_id = #{id}", :order => "name")
|
||||
end
|
||||
end
|
||||
|
||||
def member_names
|
||||
members.map(&:name).join(", ")
|
||||
end
|
||||
|
||||
def member_names=(names)
|
||||
@member_names = names.split(/\s*,\s*/)
|
||||
end
|
||||
|
||||
def group_name=(name)
|
||||
if name.blank?
|
||||
self.group_id = nil
|
||||
else
|
||||
artist = Artist.find_or_create_by_name(name)
|
||||
self.group_id = artist.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ApiMethods
|
||||
def api_attributes
|
||||
return {
|
||||
:id => id,
|
||||
:name => name,
|
||||
:alias_id => alias_id,
|
||||
:group_id => group_id,
|
||||
:urls => artist_urls.map {|x| x.url}
|
||||
}
|
||||
end
|
||||
|
||||
def to_xml(options = {})
|
||||
attribs = api_attributes
|
||||
attribs[:urls] = attribs[:urls].join(" ")
|
||||
attribs.to_xml(options.merge(:root => "artist"))
|
||||
end
|
||||
|
||||
def to_json(*args)
|
||||
return api_attributes.to_json(*args)
|
||||
end
|
||||
end
|
||||
|
||||
include UrlMethods
|
||||
include NoteMethods
|
||||
include AliasMethods
|
||||
include GroupMethods
|
||||
include ApiMethods
|
||||
|
||||
before_validation :normalize
|
||||
validates_uniqueness_of :name
|
||||
belongs_to :updater, :class_name => "User", :foreign_key => "updater_id"
|
||||
attr_accessor :updater_ip_addr
|
||||
|
||||
def normalize
|
||||
self.name = name.downcase.gsub(/^\s+/, "").gsub(/\s+$/, "").gsub(/ /, '_')
|
||||
end
|
||||
|
||||
def to_s
|
||||
return name
|
||||
end
|
||||
|
||||
def self.generate_sql(name)
|
||||
b = Nagato::Builder.new do |builder, cond|
|
||||
case name
|
||||
when /^[a-fA-F0-9]{32,32}$/
|
||||
cond.add "name IN (SELECT t.name FROM tags t JOIN posts_tags pt ON pt.tag_id = t.id JOIN posts p ON p.id = pt.post_id WHERE p.md5 = ?)", name
|
||||
|
||||
when /^http/
|
||||
cond.add "id IN (?)", find_all_by_url(name).map {|x| x.id}
|
||||
|
||||
else
|
||||
cond.add "name LIKE ? ESCAPE E'\\\\'", name.to_escaped_for_sql_like + "%"
|
||||
end
|
||||
end
|
||||
|
||||
return b.to_hash
|
||||
end
|
||||
end
|
29
app/models/artist_url.rb
Normal file
29
app/models/artist_url.rb
Normal file
@ -0,0 +1,29 @@
|
||||
class ArtistUrl < ActiveRecord::Base
|
||||
before_save :normalize
|
||||
validates_presence_of :url
|
||||
|
||||
def self.normalize(url)
|
||||
if url.nil?
|
||||
return nil
|
||||
else
|
||||
url = url.gsub(/^http:\/\/blog\d+\.fc2/, "http://blog.fc2")
|
||||
url = url.gsub(/^http:\/\/blog-imgs-\d+\.fc2/, "http://blog.fc2")
|
||||
url = url.gsub(/^http:\/\/img\d+\.pixiv\.net/, "http://img.pixiv.net")
|
||||
return url
|
||||
end
|
||||
end
|
||||
|
||||
def self.normalize_for_search(url)
|
||||
if url =~ /\.\w+$/ && url =~ /\w\/\w/
|
||||
url = File.dirname(url)
|
||||
end
|
||||
|
||||
url = url.gsub(/^http:\/\/blog\d+\.fc2/, "http://blog*.fc2")
|
||||
url = url.gsub(/^http:\/\/blog-imgs-\d+\.fc2/, "http://blog*.fc2")
|
||||
url = url.gsub(/^http:\/\/img\d+\.pixiv\.net/, "http://img*.pixiv.net")
|
||||
end
|
||||
|
||||
def normalize
|
||||
self.normalized_url = self.class.normalize(self.url)
|
||||
end
|
||||
end
|
33
app/models/ban.rb
Normal file
33
app/models/ban.rb
Normal file
@ -0,0 +1,33 @@
|
||||
class Ban < ActiveRecord::Base
|
||||
before_create :save_level
|
||||
after_create :save_to_record
|
||||
after_create :update_level
|
||||
after_destroy :restore_level
|
||||
|
||||
def restore_level
|
||||
User.find(user_id).update_attribute(:level, old_level)
|
||||
end
|
||||
|
||||
def save_level
|
||||
self.old_level = User.find(user_id).level
|
||||
end
|
||||
|
||||
def update_level
|
||||
user = User.find(user_id)
|
||||
user.level = CONFIG["user_levels"]["Blocked"]
|
||||
user.save
|
||||
end
|
||||
|
||||
def save_to_record
|
||||
UserRecord.create(:user_id => self.user_id, :reported_by => self.banned_by, :is_positive => false, :body => "Blocked: #{self.reason}")
|
||||
end
|
||||
|
||||
def duration=(dur)
|
||||
self.expires_at = (dur.to_f * 60*60*24).seconds.from_now
|
||||
@duration = dur
|
||||
end
|
||||
|
||||
def duration
|
||||
@duration
|
||||
end
|
||||
end
|
3
app/models/coefficient.rb
Normal file
3
app/models/coefficient.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class Coefficient < ActiveRecord::Base
|
||||
belongs_to :post
|
||||
end
|
59
app/models/comment.rb
Normal file
59
app/models/comment.rb
Normal file
@ -0,0 +1,59 @@
|
||||
class Comment < ActiveRecord::Base
|
||||
validates_format_of :body, :with => /\S/, :message => 'has no content'
|
||||
belongs_to :post
|
||||
belongs_to :user
|
||||
after_save :update_last_commented_at
|
||||
after_destroy :update_last_commented_at
|
||||
attr_accessor :do_not_bump_post
|
||||
|
||||
def self.generate_sql(params)
|
||||
return Nagato::Builder.new do |builder, cond|
|
||||
cond.add_unless_blank "post_id = ?", params[:post_id]
|
||||
end.to_hash
|
||||
end
|
||||
|
||||
def self.updated?(user)
|
||||
conds = []
|
||||
conds += ["user_id <> %d" % [user.id]] unless user.is_anonymous?
|
||||
|
||||
newest_comment = Comment.find(:first, :order => "id desc", :limit => 1, :select => "created_at", :conditions => conds)
|
||||
return false if newest_comment == nil
|
||||
return newest_comment.created_at > user.last_comment_read_at
|
||||
end
|
||||
|
||||
def update_last_commented_at
|
||||
# return if self.do_not_bump_post
|
||||
|
||||
comment_count = connection.select_value("SELECT COUNT(*) FROM comments WHERE post_id = #{post_id}").to_i
|
||||
if comment_count <= CONFIG["comment_threshold"]
|
||||
connection.execute("UPDATE posts SET last_commented_at = (SELECT created_at FROM comments WHERE post_id = #{post_id} ORDER BY created_at DESC LIMIT 1) WHERE posts.id = #{post_id}")
|
||||
end
|
||||
end
|
||||
|
||||
def author
|
||||
return User.find_name(self.user_id)
|
||||
end
|
||||
|
||||
def pretty_author
|
||||
author.tr("_", " ")
|
||||
end
|
||||
|
||||
def api_attributes
|
||||
return {
|
||||
:id => id,
|
||||
:created_at => created_at,
|
||||
:post_id => post_id,
|
||||
:creator => author,
|
||||
:creator_id => user_id,
|
||||
:body => body
|
||||
}
|
||||
end
|
||||
|
||||
def to_xml(options = {})
|
||||
return api_attributes.to_xml(options.merge(:root => "comment"))
|
||||
end
|
||||
|
||||
def to_json(*args)
|
||||
return api_attributes.to_json(*args)
|
||||
end
|
||||
end
|
58
app/models/dmail.rb
Normal file
58
app/models/dmail.rb
Normal file
@ -0,0 +1,58 @@
|
||||
class Dmail < ActiveRecord::Base
|
||||
validates_presence_of :to_id
|
||||
validates_presence_of :from_id
|
||||
validates_format_of :title, :with => /\S/
|
||||
validates_format_of :body, :with => /\S/
|
||||
|
||||
belongs_to :to, :class_name => "User", :foreign_key => "to_id"
|
||||
belongs_to :from, :class_name => "User", :foreign_key => "from_id"
|
||||
|
||||
after_create :update_recipient
|
||||
after_create :send_dmail
|
||||
|
||||
def send_dmail
|
||||
if to.receive_dmails? && to.email.include?("@")
|
||||
UserMailer.deliver_dmail(to, from, title, body)
|
||||
end
|
||||
end
|
||||
|
||||
def mark_as_read!(current_user)
|
||||
update_attribute(:has_seen, true)
|
||||
|
||||
unless Dmail.exists?(["to_id = ? AND has_seen = false", current_user.id])
|
||||
current_user.update_attribute(:has_mail, false)
|
||||
end
|
||||
end
|
||||
|
||||
def update_recipient
|
||||
to.update_attribute(:has_mail, true)
|
||||
end
|
||||
|
||||
def to_name
|
||||
User.find_name(to_id)
|
||||
end
|
||||
|
||||
def from_name
|
||||
User.find_name(from_id)
|
||||
end
|
||||
|
||||
def to_name=(name)
|
||||
user = User.find_by_name(name)
|
||||
return if user.nil?
|
||||
self.to_id = user.id
|
||||
end
|
||||
|
||||
def from_name=(name)
|
||||
user = User.find_by_name(name)
|
||||
return if user.nil?
|
||||
self.from_id = user.id
|
||||
end
|
||||
|
||||
def title
|
||||
if parent_id
|
||||
return "Re: " + self[:title]
|
||||
else
|
||||
return self[:title]
|
||||
end
|
||||
end
|
||||
end
|
2
app/models/favorite.rb
Normal file
2
app/models/favorite.rb
Normal file
@ -0,0 +1,2 @@
|
||||
class Favorite < ActiveRecord::Base
|
||||
end
|
55
app/models/favorite_tag.rb
Normal file
55
app/models/favorite_tag.rb
Normal file
@ -0,0 +1,55 @@
|
||||
class FavoriteTag < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
before_create :initialize_post_ids
|
||||
|
||||
def initialize_post_ids
|
||||
if user.is_privileged_or_higher?
|
||||
self.cached_post_ids = Post.find_by_tags(tag_query, :limit => 60, :select => "p.id").map(&:id).join(",")
|
||||
end
|
||||
end
|
||||
|
||||
def interested?(post_id)
|
||||
Post.find_by_tags(tag_query + " id:#{post_id}").any?
|
||||
end
|
||||
|
||||
def add_post!(post_id)
|
||||
if cached_post_ids.blank?
|
||||
update_attribute :cached_post_ids, post_id.to_s
|
||||
else
|
||||
update_attribute :cached_post_ids, "#{post_id},#{cached_post_ids}"
|
||||
end
|
||||
end
|
||||
|
||||
def prune!
|
||||
hoge = cached_post_ids.split(/,/)
|
||||
|
||||
if hoge.size > CONFIG["favorite_tag_limit"]
|
||||
update_attribute :cached_post_ids, hoge[0, CONFIG["favorite_tag_limit"]].join(",")
|
||||
end
|
||||
end
|
||||
|
||||
def self.find_post_ids(user_id, limit = 60)
|
||||
find(:all, :conditions => ["user_id = ?", user_id], :select => "id, cached_post_ids").map {|x| x.cached_post_ids.split(/,/)}.flatten
|
||||
end
|
||||
|
||||
def self.find_posts(user_id, limit = 60)
|
||||
Post.find(:all, :conditions => ["id in (?)", find_post_ids(user_id, limit)], :order => "id DESC", :limit => limit)
|
||||
end
|
||||
|
||||
def self.process_all(last_processed_post_id)
|
||||
posts = Post.find(:all, :conditions => ["id > ?", last_processed_post_id], :order => "id DESC", :select => "id")
|
||||
fav_tags = FavoriteTag.find(:all)
|
||||
|
||||
fav_tags.each do |fav_tag|
|
||||
if fav_tag.user.is_privileged_or_higher?
|
||||
posts.each do |post|
|
||||
if fav_tag.interested?(post.id)
|
||||
fav_tag.add_post!(post.id)
|
||||
end
|
||||
end
|
||||
|
||||
fav_tag.prune!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
19
app/models/flagged_post_detail.rb
Normal file
19
app/models/flagged_post_detail.rb
Normal file
@ -0,0 +1,19 @@
|
||||
class FlaggedPostDetail < ActiveRecord::Base
|
||||
belongs_to :post
|
||||
belongs_to :user
|
||||
|
||||
def author
|
||||
return User.find_name(self.user_id)
|
||||
end
|
||||
|
||||
def self.new_deleted_posts(user)
|
||||
return 0 if user.is_anonymous?
|
||||
|
||||
return Cache.get("deleted_posts:#{user.id}:#{user.last_deleted_post_seen_at.to_i}", 1.minute) do
|
||||
select_value_sql(
|
||||
"SELECT COUNT(*) FROM flagged_post_details fpd JOIN posts p ON (p.id = fpd.post_id) " +
|
||||
"WHERE p.status = 'deleted' AND p.user_id = ? AND fpd.user_id <> ? AND fpd.created_at > ?",
|
||||
user.id, user.id, user.last_deleted_post_seen_at).to_i
|
||||
end
|
||||
end
|
||||
end
|
160
app/models/forum_post.rb
Normal file
160
app/models/forum_post.rb
Normal file
@ -0,0 +1,160 @@
|
||||
class ForumPost < ActiveRecord::Base
|
||||
belongs_to :creator, :class_name => "User", :foreign_key => :creator_id
|
||||
after_create :initialize_last_updated_by
|
||||
before_validation :validate_title
|
||||
validates_length_of :body, :minimum => 1, :message => "You need to enter a body"
|
||||
|
||||
module LockMethods
|
||||
module ClassMethods
|
||||
def lock!(id)
|
||||
# Run raw SQL to skip the lock check
|
||||
execute_sql("UPDATE forum_posts SET is_locked = TRUE WHERE id = ?", id)
|
||||
end
|
||||
|
||||
def unlock!(id)
|
||||
# Run raw SQL to skip the lock check
|
||||
execute_sql("UPDATE forum_posts SET is_locked = FALSE WHERE id = ?", id)
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(m)
|
||||
m.extend(ClassMethods)
|
||||
m.before_validation :validate_lock
|
||||
end
|
||||
|
||||
def validate_lock
|
||||
if root.is_locked?
|
||||
errors.add_to_base("Thread is locked")
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
module StickyMethods
|
||||
module ClassMethods
|
||||
def stick!(id)
|
||||
# Run raw SQL to skip the lock check
|
||||
execute_sql("UPDATE forum_posts SET is_sticky = TRUE WHERE id = ?", id)
|
||||
end
|
||||
|
||||
def unstick!(id)
|
||||
# Run raw SQL to skip the lock check
|
||||
execute_sql("UPDATE forum_posts SET is_sticky = FALSE WHERE id = ?", id)
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(m)
|
||||
m.extend(ClassMethods)
|
||||
end
|
||||
end
|
||||
|
||||
module ParentMethods
|
||||
def self.included(m)
|
||||
m.after_create :update_parent_on_create
|
||||
m.before_destroy :update_parent_on_destroy
|
||||
m.has_many :children, :class_name => "ForumPost", :foreign_key => :parent_id, :order => "id"
|
||||
m.belongs_to :parent, :class_name => "ForumPost", :foreign_key => :parent_id
|
||||
end
|
||||
|
||||
def update_parent_on_destroy
|
||||
unless is_parent?
|
||||
p = parent
|
||||
p.update_attributes(:response_count => p.response_count - 1)
|
||||
end
|
||||
end
|
||||
|
||||
def update_parent_on_create
|
||||
unless is_parent?
|
||||
p = parent
|
||||
p.update_attributes(:updated_at => updated_at, :response_count => p.response_count + 1, :last_updated_by => creator_id)
|
||||
end
|
||||
end
|
||||
|
||||
def is_parent?
|
||||
return parent_id.nil?
|
||||
end
|
||||
|
||||
def root
|
||||
if is_parent?
|
||||
return self
|
||||
else
|
||||
return ForumPost.find(parent_id)
|
||||
end
|
||||
end
|
||||
|
||||
def root_id
|
||||
if is_parent?
|
||||
return id
|
||||
else
|
||||
return parent_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ApiMethods
|
||||
def api_attributes
|
||||
return {
|
||||
:body => body,
|
||||
:creator => author,
|
||||
:creator_id => creator_id,
|
||||
:id => id,
|
||||
:parent_id => parent_id,
|
||||
:title => title
|
||||
}
|
||||
end
|
||||
|
||||
def to_json(*params)
|
||||
api_attributes.to_json(*params)
|
||||
end
|
||||
|
||||
def to_xml(options = {})
|
||||
api_attributes.to_xml(options.merge(:root => "forum_post"))
|
||||
end
|
||||
end
|
||||
|
||||
include LockMethods
|
||||
include StickyMethods
|
||||
include ParentMethods
|
||||
include ApiMethods
|
||||
|
||||
def self.updated?(user)
|
||||
conds = []
|
||||
conds += ["creator_id <> %d" % [user.id]] unless user.is_anonymous?
|
||||
|
||||
newest_topic = ForumPost.find(:first, :order => "id desc", :limit => 1, :select => "created_at", :conditions => conds)
|
||||
return false if newest_topic == nil
|
||||
return newest_topic.created_at > user.last_forum_topic_read_at
|
||||
end
|
||||
|
||||
def validate_title
|
||||
if is_parent?
|
||||
if title.blank?
|
||||
errors.add :title, "missing"
|
||||
return false
|
||||
end
|
||||
|
||||
if title !~ /\S/
|
||||
errors.add :title, "missing"
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
def initialize_last_updated_by
|
||||
if is_parent?
|
||||
update_attribute(:last_updated_by, creator_id)
|
||||
end
|
||||
end
|
||||
|
||||
def last_updater
|
||||
User.find_name(last_updated_by)
|
||||
end
|
||||
|
||||
def author
|
||||
User.find_name(creator_id)
|
||||
end
|
||||
end
|
159
app/models/history.rb
Normal file
159
app/models/history.rb
Normal file
@ -0,0 +1,159 @@
|
||||
class History < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
has_many :history_changes, :order => "id"
|
||||
|
||||
def group_by_table_class
|
||||
Object.const_get(group_by_table.classify)
|
||||
end
|
||||
|
||||
def get_group_by_controller
|
||||
group_by_table_class.get_versioning_group_by[:controller]
|
||||
end
|
||||
|
||||
def get_group_by_action
|
||||
group_by_table_class.get_versioning_group_by[:action]
|
||||
end
|
||||
|
||||
def group_by_obj
|
||||
group_by_table_class.find(group_by_id)
|
||||
end
|
||||
|
||||
def user
|
||||
User.find(user_id)
|
||||
end
|
||||
|
||||
def author
|
||||
User.find_name(user_id)
|
||||
end
|
||||
|
||||
# Undo all changes in the array changes.
|
||||
def self.undo(changes, user, redo_change=false, errors={})
|
||||
# Save parent objects after child objects, so changes to the children are
|
||||
# committed when we save the parents.
|
||||
objects = {}
|
||||
|
||||
changes.each { |change|
|
||||
# If we have no previous change, this was the first change to this property
|
||||
# and we have no default, so this change can't be undone.
|
||||
previous_change = change.previous
|
||||
if !previous_change && !change.options[:allow_reverting_to_default]
|
||||
next
|
||||
end
|
||||
|
||||
if not user.can_change?(change.obj, change.field.to_sym) then
|
||||
errors[change] = :denied
|
||||
next
|
||||
end
|
||||
|
||||
# Add this node and its parent objects to objects.
|
||||
node = cache_object_recurse(objects, change.table_name, change.remote_id, change.obj)
|
||||
node[:changes] ||= []
|
||||
node[:changes] << change
|
||||
}
|
||||
|
||||
return unless objects[:objects]
|
||||
|
||||
# objects contains one or more trees of objects. Flatten this to an ordered
|
||||
# list, so we can always save child nodes before parent nodes.
|
||||
done = {}
|
||||
stack = []
|
||||
objects[:objects].each { |table_name, rhs|
|
||||
rhs.each { |id, node|
|
||||
# Start adding from the node at the top of the tree.
|
||||
while node[:parent] do
|
||||
node = node[:parent]
|
||||
end
|
||||
self.stack_object_recurse(node, stack, done)
|
||||
}
|
||||
}
|
||||
|
||||
stack.reverse.each { |node|
|
||||
object = node[:o]
|
||||
changes = node[:changes]
|
||||
if changes
|
||||
changes.each { |change|
|
||||
if redo_change
|
||||
redo_func = ("%s_redo" % change.field).to_sym
|
||||
if object.respond_to?(redo_func) then
|
||||
object.send(redo_func, change)
|
||||
else
|
||||
object.attributes = { change.field.to_sym => change.value }
|
||||
end
|
||||
else
|
||||
undo_func = ("%s_undo" % change.field).to_sym
|
||||
if object.respond_to?(undo_func) then
|
||||
object.send(undo_func, change)
|
||||
else
|
||||
if change.previous
|
||||
previous = change.previous.value
|
||||
else
|
||||
previous = change.options[:default] # when :allow_reverting_to_default
|
||||
end
|
||||
object.attributes = { change.field.to_sym => previous }
|
||||
end
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
object.run_callbacks(:after_undo)
|
||||
object.save!
|
||||
}
|
||||
end
|
||||
|
||||
def self.generate_sql(options = {})
|
||||
Nagato::Builder.new do |builder, cond|
|
||||
cond.add_unless_blank "histories.remote_id = ?", options[:remote_id]
|
||||
cond.add_unless_blank "histories.user_id = ?", options[:user_id]
|
||||
|
||||
if options[:user_name]
|
||||
builder.join "users ON users.id = histories.user_id"
|
||||
cond.add "users.name = ?", options[:user_name]
|
||||
end
|
||||
end.to_hash
|
||||
end
|
||||
|
||||
private
|
||||
# Find and return the node for table_name/id in objects. If the node doesn't
|
||||
# exist, create it and point it at object.
|
||||
def self.cache_object(objects, table_name, id, object)
|
||||
objects[:objects] ||= {}
|
||||
objects[:objects][table_name] ||= {}
|
||||
objects[:objects][table_name][id] ||= {
|
||||
:o => object
|
||||
}
|
||||
return objects[:objects][table_name][id]
|
||||
end
|
||||
|
||||
# Find and return the node for table_name/id in objects. Recursively create
|
||||
# nodes for parent objects.
|
||||
def self.cache_object_recurse(objects, table_name, id, object)
|
||||
node = self.cache_object(objects, table_name, id, object)
|
||||
|
||||
# If this class has a master class, register the master object for update callbacks too.
|
||||
master = object.versioned_master_object
|
||||
if master
|
||||
master_node = cache_object_recurse(objects, master.class.to_s, master.id, master)
|
||||
|
||||
master_node[:children] ||= []
|
||||
master_node[:children] << node
|
||||
node[:parent] = master_node
|
||||
end
|
||||
|
||||
return node
|
||||
end
|
||||
|
||||
# Recursively add all nodes to stack, parents first.
|
||||
def self.stack_object_recurse(node, stack, done = {})
|
||||
return if done[node]
|
||||
done[node] = true
|
||||
|
||||
stack << node
|
||||
|
||||
if node[:children] then
|
||||
node[:children].each { |child|
|
||||
self.stack_object_recurse(child, stack, done)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
78
app/models/history_change.rb
Normal file
78
app/models/history_change.rb
Normal file
@ -0,0 +1,78 @@
|
||||
class HistoryChange < ActiveRecord::Base
|
||||
belongs_to :history
|
||||
belongs_to :previous, :class_name => "HistoryChange", :foreign_key => :previous_id
|
||||
after_create :set_previous
|
||||
|
||||
def options
|
||||
master_class.get_versioned_attribute_options(field) or {}
|
||||
end
|
||||
|
||||
def master_class
|
||||
# Hack because Rails is stupid and can't reliably derive class names
|
||||
# from table names:
|
||||
if table_name == "pools_posts"
|
||||
class_name = "PoolPost"
|
||||
else
|
||||
class_name = table_name.classify
|
||||
end
|
||||
Object.const_get(class_name)
|
||||
end
|
||||
|
||||
# Return true if this changes the value to the default value.
|
||||
def changes_to_default?
|
||||
return false if not has_default?
|
||||
|
||||
# Cast our value to the actual type; if this is a boolean value, this
|
||||
# casts "f" to false.
|
||||
column = master_class.columns_hash[field]
|
||||
typecasted_value = column.type_cast(value)
|
||||
|
||||
return typecasted_value == get_default
|
||||
end
|
||||
|
||||
def is_obsolete?
|
||||
latest_change = latest
|
||||
return self.value != latest_change.value
|
||||
end
|
||||
|
||||
def has_default?
|
||||
options.has_key?(:default)
|
||||
end
|
||||
|
||||
def get_default
|
||||
default = options[:default]
|
||||
end
|
||||
|
||||
# Return the default value for the field recorded by this change.
|
||||
def default_history
|
||||
return nil if not has_default?
|
||||
|
||||
History.new :table_name => self.table_name,
|
||||
:remote_id => self.remote_id,
|
||||
:field => self.field,
|
||||
:value => get_default
|
||||
end
|
||||
|
||||
# Return the object this change modifies.
|
||||
def obj
|
||||
@obj ||= master_class.find(self.remote_id)
|
||||
@obj
|
||||
end
|
||||
|
||||
def latest
|
||||
HistoryChange.find(:first, :order => "id DESC",
|
||||
:conditions => ["table_name = ? AND remote_id = ? AND field = ?", table_name, remote_id, field])
|
||||
end
|
||||
|
||||
def next
|
||||
HistoryChange.find(:first, :order => "h.id ASC",
|
||||
:conditions => ["table_name = ? AND remote_id = ? AND id > ? AND field = ?", table_name, remote_id, id, field])
|
||||
end
|
||||
|
||||
def set_previous
|
||||
self.previous = HistoryChange.find(:first, :order => "id DESC",
|
||||
:conditions => ["table_name = ? AND remote_id = ? AND id < ? AND field = ?", table_name, remote_id, id, field])
|
||||
self.save!
|
||||
end
|
||||
end
|
||||
|
83
app/models/inline.rb
Normal file
83
app/models/inline.rb
Normal file
@ -0,0 +1,83 @@
|
||||
class Inline < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
has_many :inline_images, :dependent => :destroy, :order => "sequence"
|
||||
|
||||
# Sequence numbers must start at 1 and increase monotonically, to keep the UI simple.
|
||||
# If we've been given sequences with gaps or duplicates, sanitize them.
|
||||
def renumber_sequences
|
||||
first = 1
|
||||
for image in inline_images do
|
||||
image.sequence = first
|
||||
image.save!
|
||||
first += 1
|
||||
end
|
||||
end
|
||||
|
||||
def pretty_name
|
||||
"x"
|
||||
end
|
||||
|
||||
def crop(params)
|
||||
if params[:top].to_f < 0 or params[:top].to_f > 1 or
|
||||
params[:bottom].to_f < 0 or params[:bottom].to_f > 1 or
|
||||
params[:left].to_f < 0 or params[:left].to_f > 1 or
|
||||
params[:right].to_f < 0 or params[:right].to_f > 1 or
|
||||
params[:top] >= params[:bottom] or
|
||||
params[:left] >= params[:right]
|
||||
then
|
||||
errors.add(:parameter, "error")
|
||||
return false
|
||||
end
|
||||
|
||||
def reduce_and_crop(image_width, image_height, params)
|
||||
cropped_image_width = image_width * (params[:right].to_f - params[:left].to_f)
|
||||
cropped_image_height = image_height * (params[:bottom].to_f - params[:top].to_f)
|
||||
|
||||
size = {}
|
||||
size[:width] = cropped_image_width
|
||||
size[:height] = cropped_image_height
|
||||
size[:crop_top] = image_height * params[:top].to_f
|
||||
size[:crop_bottom] = image_height * params[:bottom].to_f
|
||||
size[:crop_left] = image_width * params[:left].to_f
|
||||
size[:crop_right] = image_width * params[:right].to_f
|
||||
size
|
||||
end
|
||||
|
||||
images = self.inline_images
|
||||
for image in images do
|
||||
# Create a new image with the same properties, crop this image into the new one,
|
||||
# and delete the old one.
|
||||
new_image = InlineImage.new(:description => image.description, :sequence => image.sequence, :inline_id => self.id, :file_ext => "jpg")
|
||||
size = reduce_and_crop(image.width, image.height, params)
|
||||
|
||||
begin
|
||||
# Create one crop for the image, and InlineImage will create the sample and preview from that.
|
||||
Danbooru.resize(image.file_ext, image.file_path, new_image.tempfile_image_path, size, 95)
|
||||
FileUtils.chmod(0775, new_image.tempfile_image_path)
|
||||
rescue Exception => x
|
||||
FileUtils.rm_f(new_image.tempfile_image_path)
|
||||
|
||||
errors.add "crop", "couldn't be generated (#{x})"
|
||||
return false
|
||||
end
|
||||
|
||||
new_image.got_file
|
||||
new_image.save!
|
||||
image.destroy
|
||||
end
|
||||
end
|
||||
|
||||
def api_attributes
|
||||
return {
|
||||
:id => id,
|
||||
:description => description,
|
||||
:user_id => user_id,
|
||||
:images => inline_images
|
||||
}
|
||||
end
|
||||
|
||||
def to_json(*params)
|
||||
api_attributes.to_json(*params)
|
||||
end
|
||||
end
|
||||
|
327
app/models/inline_image.rb
Normal file
327
app/models/inline_image.rb
Normal file
@ -0,0 +1,327 @@
|
||||
require "fileutils"
|
||||
|
||||
# InlineImages can be uploaded, copied directly from posts, or cropped from other InlineImages.
|
||||
# To create an image by cropping a post, the post must be copied to an InlineImage of its own,
|
||||
# and cropped from there; the only UI for cropping is InlineImage->InlineImage.
|
||||
#
|
||||
# InlineImages can be posted directly in the forum and wiki (and possibly comments).
|
||||
#
|
||||
# An inline image can have three versions, like a post. For consistency, they use the
|
||||
# same names: image, sample, preview. As with posts, sample and previews are always JPEG,
|
||||
# and the dimensions of preview is derived from image rather than stored.
|
||||
#
|
||||
# Image files are effectively garbage collected: InlineImages can share files, and the file
|
||||
# is deleted when the last one using it is deleted. This allows any user to copy another user's
|
||||
# InlineImage, to crop it or to include it in an Inline.
|
||||
#
|
||||
# Example use cases:
|
||||
#
|
||||
# - Plain inlining, eg. for tutorials. Thumbs and larger images can be shown inline, allowing
|
||||
# a click to expand.
|
||||
# - Showing edits. Each user can upload his edit as an InlineImage and post it directly
|
||||
# into the forum.
|
||||
# - Comparing edits. A user can upload his own edit, pair it with another version (using
|
||||
# Inline), crop to a region of interest, and post that inline. The images can then be
|
||||
# compared in-place. This can be used to clearly show editing problems and differences.
|
||||
|
||||
class InlineImage < ActiveRecord::Base
|
||||
belongs_to :inline
|
||||
before_validation_on_create :download_source
|
||||
before_validation_on_create :determine_content_type
|
||||
before_validation_on_create :set_image_dimensions
|
||||
before_validation_on_create :generate_sample
|
||||
before_validation_on_create :generate_preview
|
||||
before_validation_on_create :move_file
|
||||
before_validation_on_create :set_default_sequence
|
||||
after_destroy :delete_file
|
||||
before_create :validate_uniqueness
|
||||
|
||||
def tempfile_image_path
|
||||
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}.upload"
|
||||
end
|
||||
|
||||
def tempfile_sample_path
|
||||
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}-sample.upload"
|
||||
end
|
||||
|
||||
def tempfile_preview_path
|
||||
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}-preview.upload"
|
||||
end
|
||||
|
||||
attr_accessor :source
|
||||
attr_accessor :received_file
|
||||
attr_accessor :file_needs_move
|
||||
def post_id=(id)
|
||||
post = Post.find_by_id(id)
|
||||
file = post.file_path
|
||||
|
||||
FileUtils.ln_s(file.local_path, tempfile_image_path)
|
||||
|
||||
self.received_file = true
|
||||
self.md5 = post.md5
|
||||
end
|
||||
|
||||
# Call once a file is available in tempfile_image_path.
|
||||
def got_file
|
||||
generate_hash(tempfile_image_path)
|
||||
FileUtils.chmod(0775, self.tempfile_image_path)
|
||||
self.file_needs_move = true
|
||||
self.received_file = true
|
||||
end
|
||||
|
||||
def file=(f)
|
||||
return if f.nil? || f.size == 0
|
||||
|
||||
if f.local_path
|
||||
FileUtils.cp(f.local_path, tempfile_image_path)
|
||||
else
|
||||
File.open(tempfile_image_path, 'wb') {|nf| nf.write(f.read)}
|
||||
end
|
||||
|
||||
got_file
|
||||
end
|
||||
|
||||
def download_source
|
||||
return if source !~ /^http:\/\// || !file_ext.blank?
|
||||
return if received_file
|
||||
|
||||
begin
|
||||
Danbooru.http_get_streaming(source) do |response|
|
||||
File.open(tempfile_image_path, "wb") do |out|
|
||||
response.read_body do |block|
|
||||
out.write(block)
|
||||
end
|
||||
end
|
||||
end
|
||||
got_file
|
||||
|
||||
return true
|
||||
rescue SocketError, URI::Error, Timeout::Error, SystemCallError => x
|
||||
delete_tempfile
|
||||
errors.add "source", "couldn't be opened: #{x}"
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def determine_content_type
|
||||
return true if self.file_ext
|
||||
|
||||
if not File.exists?(tempfile_image_path)
|
||||
errors.add_to_base("No file received")
|
||||
return false
|
||||
end
|
||||
|
||||
imgsize = ImageSize.new(File.open(tempfile_image_path, "rb"))
|
||||
|
||||
unless imgsize.get_width.nil?
|
||||
self.file_ext = imgsize.get_type.gsub(/JPEG/, "JPG").downcase
|
||||
end
|
||||
|
||||
unless %w(jpg png gif).include?(file_ext.downcase)
|
||||
errors.add(:file, "is an invalid content type: " + (file_ext.downcase or "unknown"))
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
def set_image_dimensions
|
||||
return true if self.width and self.height
|
||||
imgsize = ImageSize.new(File.open(tempfile_image_path, "rb"))
|
||||
self.width = imgsize.get_width
|
||||
self.height = imgsize.get_height
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
def preview_dimensions
|
||||
return Danbooru.reduce_to({:width => width, :height => height}, {:width => 150, :height => 150})
|
||||
end
|
||||
|
||||
def thumb_size
|
||||
size = Danbooru.reduce_to({:width => width, :height => height}, {:width => 400, :height => 400})
|
||||
end
|
||||
|
||||
def generate_sample
|
||||
return true if File.exists?(sample_path)
|
||||
|
||||
# We can generate the sample image during upload or offline. Use tempfile_image_path
|
||||
# if it exists, otherwise use file_path.
|
||||
path = tempfile_image_path
|
||||
path = file_path unless File.exists?(path)
|
||||
unless File.exists?(path)
|
||||
errors.add(:file, "not found")
|
||||
return false
|
||||
end
|
||||
|
||||
# If we're not reducing the resolution for the sample image, only reencode if the
|
||||
# source image is above the reencode threshold. Anything smaller won't be reduced
|
||||
# enough by the reencode to bother, so don't reencode it and save disk space.
|
||||
sample_size = Danbooru.reduce_to({:width => width, :height => height}, {:width => CONFIG["inline_sample_width"], :height => CONFIG["inline_sample_height"]})
|
||||
if sample_size[:width] == width && sample_size[:height] == height && File.size?(path) < CONFIG["sample_always_generate_size"]
|
||||
return true
|
||||
end
|
||||
|
||||
# If we already have a sample image, and the parameters havn't changed,
|
||||
# don't regenerate it.
|
||||
if sample_size[:width] == sample_width && sample_size[:height] == sample_height
|
||||
return true
|
||||
end
|
||||
|
||||
begin
|
||||
Danbooru.resize(file_ext, path, tempfile_sample_path, sample_size, 95)
|
||||
rescue Exception => x
|
||||
errors.add "sample", "couldn't be created: #{x}"
|
||||
return false
|
||||
end
|
||||
|
||||
self.sample_width = sample_size[:width]
|
||||
self.sample_height = sample_size[:height]
|
||||
return true
|
||||
end
|
||||
|
||||
def generate_preview
|
||||
return true if File.exists?(preview_path)
|
||||
|
||||
unless File.exists?(tempfile_image_path)
|
||||
errors.add(:file, "not found")
|
||||
return false
|
||||
end
|
||||
|
||||
# Generate the preview from the new sample if we have one to save CPU, otherwise from the image.
|
||||
if File.exists?(tempfile_sample_path)
|
||||
path, ext = tempfile_sample_path, "jpg"
|
||||
else
|
||||
path, ext = tempfile_image_path, file_ext
|
||||
end
|
||||
|
||||
begin
|
||||
Danbooru.resize(ext, path, tempfile_preview_path, preview_dimensions, 95)
|
||||
rescue Exception => x
|
||||
errors.add "preview", "couldn't be generated (#{x})"
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
def move_file
|
||||
return true if not file_needs_move
|
||||
FileUtils.mv(tempfile_image_path, file_path)
|
||||
|
||||
if File.exists?(tempfile_preview_path)
|
||||
FileUtils.mv(tempfile_preview_path, preview_path)
|
||||
end
|
||||
if File.exists?(tempfile_sample_path)
|
||||
FileUtils.mv(tempfile_sample_path, sample_path)
|
||||
end
|
||||
self.file_needs_move = false
|
||||
return true
|
||||
end
|
||||
|
||||
def set_default_sequence
|
||||
return if not self.sequence.nil?
|
||||
siblings = self.inline.inline_images
|
||||
max_sequence = siblings.map { |image| image.sequence }.max
|
||||
max_sequence ||= 0
|
||||
self.sequence = max_sequence + 1
|
||||
end
|
||||
|
||||
def generate_hash(path)
|
||||
md5_obj = Digest::MD5.new
|
||||
File.open(path, 'rb') { |fp|
|
||||
buf = ""
|
||||
while fp.read(1024*64, buf) do md5_obj << buf end
|
||||
}
|
||||
|
||||
self.md5 = md5_obj.hexdigest
|
||||
end
|
||||
|
||||
def has_sample?
|
||||
return (not self.sample_height.nil?)
|
||||
end
|
||||
|
||||
def file_name
|
||||
"#{md5}.#{file_ext}"
|
||||
end
|
||||
|
||||
def file_name_jpg
|
||||
"#{md5}.jpg"
|
||||
end
|
||||
|
||||
def file_path
|
||||
"#{RAILS_ROOT}/public/data/inline/image/#{file_name}"
|
||||
end
|
||||
|
||||
def preview_path
|
||||
"#{RAILS_ROOT}/public/data/inline/preview/#{file_name_jpg}"
|
||||
end
|
||||
|
||||
def sample_path
|
||||
"#{RAILS_ROOT}/public/data/inline/sample/#{file_name_jpg}"
|
||||
end
|
||||
|
||||
def file_url
|
||||
CONFIG["url_base"] + "/data/inline/image/#{file_name}"
|
||||
end
|
||||
|
||||
def sample_url
|
||||
if self.has_sample?
|
||||
return CONFIG["url_base"] + "/data/inline/sample/#{file_name_jpg}"
|
||||
else
|
||||
return file_url
|
||||
end
|
||||
end
|
||||
|
||||
def preview_url
|
||||
CONFIG["url_base"] + "/data/inline/preview/#{file_name_jpg}"
|
||||
end
|
||||
|
||||
def delete_file
|
||||
# If several inlines use the same image, they'll share the same file via the MD5. Only
|
||||
# delete the file if this is the last one using it.
|
||||
exists = InlineImage.find(:first, :conditions => ["id <> ? AND md5 = ?", self.id, self.md5])
|
||||
return if not exists.nil?
|
||||
|
||||
FileUtils.rm_f(file_path)
|
||||
FileUtils.rm_f(preview_path)
|
||||
FileUtils.rm_f(sample_path)
|
||||
end
|
||||
|
||||
# We should be able to use validates_uniqueness_of for this, but Rails is completely
|
||||
# brain-damaged: it only lets you specify an error message that starts with the name
|
||||
# of the column, capitalized, so if we say "foo", the message is "Md5 foo". This is
|
||||
# useless.
|
||||
def validate_uniqueness
|
||||
siblings = self.inline.inline_images
|
||||
for s in siblings do
|
||||
next if s.id == self
|
||||
if s.md5 == self.md5
|
||||
errors.add_to_base("##{s.sequence} already exists.")
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
def api_attributes
|
||||
return {
|
||||
:id => id,
|
||||
:sequence => sequence,
|
||||
:md5 => md5,
|
||||
:width => width,
|
||||
:height => height,
|
||||
:sample_width => sample_width,
|
||||
:sample_height => sample_height,
|
||||
:preview_width => preview_dimensions[:width],
|
||||
:preview_height => preview_dimensions[:height],
|
||||
:description => description,
|
||||
:file_url => file_url,
|
||||
:sample_url => sample_url,
|
||||
:preview_url => preview_url,
|
||||
}
|
||||
end
|
||||
|
||||
def to_json(*params)
|
||||
api_attributes.to_json(*params)
|
||||
end
|
||||
end
|
18
app/models/ip_bans.rb
Normal file
18
app/models/ip_bans.rb
Normal file
@ -0,0 +1,18 @@
|
||||
class IpBans < ActiveRecord::Base
|
||||
belongs_to :user, :foreign_key => :banned_by
|
||||
|
||||
def duration=(dur)
|
||||
if not dur or dur == "" then
|
||||
self.expires_at = nil
|
||||
@duration = nil
|
||||
else
|
||||
self.expires_at = (dur.to_f * 60*60*24).seconds.from_now
|
||||
@duration = dur
|
||||
end
|
||||
end
|
||||
|
||||
def duration
|
||||
@duration
|
||||
end
|
||||
end
|
||||
|
190
app/models/job_task.rb
Normal file
190
app/models/job_task.rb
Normal file
@ -0,0 +1,190 @@
|
||||
class JobTask < ActiveRecord::Base
|
||||
TASK_TYPES = %w(mass_tag_edit approve_tag_alias approve_tag_implication calculate_favorite_tags upload_posts_to_mirrors periodic_maintenance)
|
||||
STATUSES = %w(pending processing finished error)
|
||||
|
||||
validates_inclusion_of :task_type, :in => TASK_TYPES
|
||||
validates_inclusion_of :status, :in => STATUSES
|
||||
|
||||
def data
|
||||
JSON.parse(data_as_json)
|
||||
end
|
||||
|
||||
def data=(hoge)
|
||||
self.data_as_json = hoge.to_json
|
||||
end
|
||||
|
||||
def execute!
|
||||
if repeat_count > 0
|
||||
count = repeat_count - 1
|
||||
else
|
||||
count = repeat_count
|
||||
end
|
||||
|
||||
begin
|
||||
execute_sql("SET statement_timeout = 0")
|
||||
update_attributes(:status => "processing")
|
||||
__send__("execute_#{task_type}")
|
||||
|
||||
if count == 0
|
||||
update_attributes(:status => "finished")
|
||||
else
|
||||
update_attributes(:status => "pending", :repeat_count => count)
|
||||
end
|
||||
rescue SystemExit => x
|
||||
update_attributes(:status => "pending")
|
||||
raise x
|
||||
rescue Exception => x
|
||||
update_attributes(:status => "error", :status_message => "#{x.class}: #{x}")
|
||||
end
|
||||
end
|
||||
|
||||
def execute_mass_tag_edit
|
||||
start_tags = data["start_tags"]
|
||||
result_tags = data["result_tags"]
|
||||
updater_id = data["updater_id"]
|
||||
updater_ip_addr = data["updater_ip_addr"]
|
||||
Tag.mass_edit(start_tags, result_tags, updater_id, updater_ip_addr)
|
||||
end
|
||||
|
||||
def execute_approve_tag_alias
|
||||
ta = TagAlias.find(data["id"])
|
||||
updater_id = data["updater_id"]
|
||||
updater_ip_addr = data["updater_ip_addr"]
|
||||
ta.approve(updater_id, updater_ip_addr)
|
||||
end
|
||||
|
||||
def execute_approve_tag_implication
|
||||
ti = TagImplication.find(data["id"])
|
||||
updater_id = data["updater_id"]
|
||||
updater_ip_addr = data["updater_ip_addr"]
|
||||
ti.approve(updater_id, updater_ip_addr)
|
||||
end
|
||||
|
||||
def execute_calculate_favorite_tags
|
||||
return if Cache.get("delay-favtags-calc")
|
||||
|
||||
last_processed_post_id = data["last_processed_post_id"].to_i
|
||||
|
||||
if last_processed_post_id == 0
|
||||
last_processed_post_id = Post.maximum("id").to_i
|
||||
end
|
||||
|
||||
Cache.put("delay-favtags-calc", "1", 10.minutes)
|
||||
FavoriteTag.process_all(last_processed_post_id)
|
||||
update_attributes(:data => {"last_processed_post_id" => Post.maximum("id")})
|
||||
end
|
||||
|
||||
def update_data(*args)
|
||||
hash = data.merge(args[0])
|
||||
update_attributes(:data => hash)
|
||||
end
|
||||
|
||||
def execute_periodic_maintenance
|
||||
return if data["next_run"] && data["next_run"] > Time.now.to_i
|
||||
|
||||
update_data("step" => "recalculating post count")
|
||||
Post.recalculate_row_count
|
||||
update_data("step" => "recalculating tag post counts")
|
||||
Tag.recalculate_post_count
|
||||
update_data("step" => "purging old tags")
|
||||
Tag.purge_tags
|
||||
|
||||
update_data("next_run" => Time.now.to_i + 60*60*6, "step" => nil)
|
||||
end
|
||||
|
||||
def execute_upload_posts_to_mirrors
|
||||
# This is a little counterintuitive: if we're backlogged, mirror newer posts first,
|
||||
# since they're the ones that receive the most attention. Mirror held posts after
|
||||
# unheld posts.
|
||||
#
|
||||
# Apply a limit, so if we're backlogged heavily, we'll only upload a few posts and
|
||||
# then give other jobs a chance to run.
|
||||
data = {}
|
||||
(1..10).each do
|
||||
post = Post.find(:first, :conditions => ["NOT is_warehoused AND status <> 'deleted'"], :order => "is_held ASC, index_timestamp DESC")
|
||||
break if not post
|
||||
|
||||
data["left"] = Post.count(:conditions => ["NOT is_warehoused AND status <> 'deleted'"])
|
||||
data["post_id"] = post.id
|
||||
update_attributes(:data => data)
|
||||
|
||||
begin
|
||||
post.upload_to_mirrors
|
||||
ensure
|
||||
data["post_id"] = nil
|
||||
update_attributes(:data => data)
|
||||
end
|
||||
|
||||
data["left"] = Post.count(:conditions => ["NOT is_warehoused AND status <> 'deleted'"])
|
||||
update_attributes(:data => data)
|
||||
end
|
||||
end
|
||||
|
||||
def pretty_data
|
||||
case task_type
|
||||
when "mass_tag_edit"
|
||||
start = data["start_tags"]
|
||||
result = data["result_tags"]
|
||||
user = User.find_name(data["updater_id"])
|
||||
|
||||
"start:#{start} result:#{result} user:#{user}"
|
||||
|
||||
when "approve_tag_alias"
|
||||
ta = TagAlias.find(data["id"])
|
||||
"start:#{ta.name} result:#{ta.alias_name}"
|
||||
|
||||
when "approve_tag_implication"
|
||||
ti = TagImplication.find(data["id"])
|
||||
"start:#{ti.predicate.name} result:#{ti.consequent.name}"
|
||||
|
||||
when "calculate_favorite_tags"
|
||||
"post_id:#{data['last_processed_post_id']}"
|
||||
|
||||
when "upload_posts_to_mirrors"
|
||||
ret = ""
|
||||
if data["post_id"]
|
||||
ret << "uploading post_id #{data["post_id"]}"
|
||||
elsif data["left"]
|
||||
ret << "sleeping"
|
||||
else
|
||||
ret << "idle"
|
||||
end
|
||||
ret << (" (%i left) " % data["left"]) if data["left"]
|
||||
ret
|
||||
|
||||
when "periodic_maintenance"
|
||||
if status == "processing" then
|
||||
data["step"]
|
||||
elsif status != "error" then
|
||||
next_run = (data["next_run"] or 0) - Time.now.to_i
|
||||
next_run_in_minutes = next_run.to_i / 60
|
||||
if next_run_in_minutes > 0
|
||||
eta = "next run in #{(next_run_in_minutes.to_f / 60.0).round} hours"
|
||||
else
|
||||
eta = "next run imminent"
|
||||
end
|
||||
"sleeping (#{eta})"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.execute_once
|
||||
find(:all, :conditions => ["status = ?", "pending"], :order => "id desc").each do |task|
|
||||
task.execute!
|
||||
sleep 1
|
||||
end
|
||||
end
|
||||
|
||||
def self.execute_all
|
||||
# If we were interrupted without finishing a task, it may be left in processing; reset
|
||||
# thos tasks to pending.
|
||||
find(:all, :conditions => ["status = ?", "processing"]).each do |task|
|
||||
task.update_attributes(:status => "pending")
|
||||
end
|
||||
|
||||
while true
|
||||
execute_once
|
||||
sleep 10
|
||||
end
|
||||
end
|
||||
end
|
82
app/models/note.rb
Normal file
82
app/models/note.rb
Normal file
@ -0,0 +1,82 @@
|
||||
class Note < ActiveRecord::Base
|
||||
include ActiveRecord::Acts::Versioned
|
||||
|
||||
belongs_to :post
|
||||
before_save :blank_body
|
||||
acts_as_versioned :order => "updated_at DESC"
|
||||
after_save :update_post
|
||||
|
||||
module LockMethods
|
||||
def self.included(m)
|
||||
m.validate :post_must_not_be_note_locked
|
||||
end
|
||||
|
||||
def post_must_not_be_note_locked
|
||||
if is_locked?
|
||||
errors.add_to_base "Post is note locked"
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def is_locked?
|
||||
if select_value_sql("SELECT 1 FROM posts WHERE id = ? AND is_note_locked = ?", post_id, true)
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ApiMethods
|
||||
def api_attributes
|
||||
return {
|
||||
:id => id,
|
||||
:created_at => created_at,
|
||||
:updated_at => updated_at,
|
||||
:creator_id => user_id,
|
||||
:x => x,
|
||||
:y => y,
|
||||
:width => width,
|
||||
:height => height,
|
||||
:is_active => is_active,
|
||||
:post_id => post_id,
|
||||
:body => body,
|
||||
:version => version
|
||||
}
|
||||
end
|
||||
|
||||
def to_xml(options = {})
|
||||
api_attributes.to_xml(options.merge(:root => "note"))
|
||||
end
|
||||
|
||||
def to_json(*args)
|
||||
return api_attributes.to_json(*args)
|
||||
end
|
||||
end
|
||||
|
||||
include LockMethods
|
||||
include ApiMethods
|
||||
|
||||
def blank_body
|
||||
self.body = "(empty)" if body.blank?
|
||||
end
|
||||
|
||||
# TODO: move this to a helper
|
||||
def formatted_body
|
||||
body.gsub(/<tn>(.+?)<\/tn>/m, '<br><p class="tn">\1</p>').gsub(/\n/, '<br>')
|
||||
end
|
||||
|
||||
def update_post
|
||||
active_notes = select_value_sql("SELECT 1 FROM notes WHERE is_active = ? AND post_id = ? LIMIT 1", true, post_id)
|
||||
|
||||
if active_notes
|
||||
execute_sql("UPDATE posts SET last_noted_at = ? WHERE id = ?", updated_at, post_id)
|
||||
else
|
||||
execute_sql("UPDATE posts SET last_noted_at = ? WHERE id = ?", nil, post_id)
|
||||
end
|
||||
end
|
||||
|
||||
def author
|
||||
User.find_name(user_id)
|
||||
end
|
||||
end
|
13
app/models/note_version.rb
Normal file
13
app/models/note_version.rb
Normal file
@ -0,0 +1,13 @@
|
||||
class NoteVersion < ActiveRecord::Base
|
||||
def to_xml(options = {})
|
||||
{:created_at => created_at, :updated_at => updated_at, :creator_id => user_id, :x => x, :y => y, :width => width, :height => height, :is_active => is_active, :post_id => post_id, :body => body, :version => version}.to_xml(options.merge(:root => "note_version"))
|
||||
end
|
||||
|
||||
def to_json(*args)
|
||||
{:created_at => created_at, :updated_at => updated_at, :creator_id => user_id, :x => x, :y => y, :width => width, :height => height, :is_active => is_active, :post_id => post_id, :body => body, :version => version}.to_json(*args)
|
||||
end
|
||||
|
||||
def author
|
||||
User.find_name(user_id)
|
||||
end
|
||||
end
|
391
app/models/pool.rb
Normal file
391
app/models/pool.rb
Normal file
@ -0,0 +1,391 @@
|
||||
require 'mirror'
|
||||
require "erb"
|
||||
include ERB::Util
|
||||
|
||||
class Pool < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
|
||||
class PostAlreadyExistsError < Exception
|
||||
end
|
||||
|
||||
class AccessDeniedError < Exception
|
||||
end
|
||||
|
||||
module PostMethods
|
||||
def self.included(m)
|
||||
# Prefer child posts (the posts that were actually added to the pool). This is what's displayed
|
||||
# when editing the pool.
|
||||
m.has_many :pool_posts, :class_name => "PoolPost", :order => "nat_sort(sequence), post_id", :conditions => "pools_posts.active = true"
|
||||
|
||||
# Prefer parent posts (the parents of posts that were added to the pool). This is what's displayed by
|
||||
# default in post/show.
|
||||
m.has_many :pool_parent_posts, :class_name => "PoolPost", :order => "nat_sort(sequence), post_id",
|
||||
:conditions => "(pools_posts.active = true AND pools_posts.slave_id IS NULL) OR pools_posts.master_id IS NOT NULL"
|
||||
m.has_many :all_pool_posts, :class_name => "PoolPost", :order => "nat_sort(sequence), post_id"
|
||||
m.versioned :name
|
||||
m.versioned :description, :default => ""
|
||||
m.versioned :is_public, :default => true
|
||||
m.versioned :is_active, :default => true
|
||||
m.after_undo :update_pool_links
|
||||
end
|
||||
|
||||
def can_be_updated_by?(user)
|
||||
is_public? || user.has_permission?(self)
|
||||
end
|
||||
|
||||
def add_post(post_id, options = {})
|
||||
transaction do
|
||||
if options[:user] && !can_be_updated_by?(options[:user])
|
||||
raise AccessDeniedError
|
||||
end
|
||||
|
||||
seq = options[:sequence] || next_sequence
|
||||
|
||||
pool_post = all_pool_posts.find(:first, :conditions => ["post_id = ?", post_id])
|
||||
if pool_post
|
||||
raise PostAlreadyExistsError if pool_post.active
|
||||
pool_post.active = true
|
||||
pool_post.sequence = seq
|
||||
pool_post.save!
|
||||
else
|
||||
PoolPost.create(:pool_id => id, :post_id => post_id, :sequence => seq)
|
||||
end
|
||||
|
||||
increment!(:post_count)
|
||||
|
||||
unless options[:skip_update_pool_links]
|
||||
self.reload
|
||||
update_pool_links
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def remove_post(post_id, options = {})
|
||||
transaction do
|
||||
if options[:user] && !can_be_updated_by?(options[:user])
|
||||
raise AccessDeniedError
|
||||
end
|
||||
|
||||
pool_post = pool_posts.find(:first, :conditions => ["post_id = ?", post_id])
|
||||
if pool_post then
|
||||
pool_post.active = false
|
||||
pool_post.save!
|
||||
self.reload
|
||||
decrement!(:post_count)
|
||||
update_pool_links
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_sample
|
||||
# By preference, pick the first post (by sequence) in the pool that isn't hidden from
|
||||
# the index.
|
||||
PoolPost.find(:all, :order => "posts.is_shown_in_index DESC, nat_sort(pools_posts.sequence), pools_posts.post_id",
|
||||
:joins => "JOIN posts ON posts.id = pools_posts.post_id",
|
||||
:conditions => ["pool_id = ? AND posts.status = 'active' AND pools_posts.active", self.id]).each { |pool_post|
|
||||
return pool_post.post if pool_post.post.can_be_seen_by?(Thread.current["danbooru-user"])
|
||||
}
|
||||
return rescue nil
|
||||
end
|
||||
|
||||
def can_change_is_public?(user)
|
||||
user.has_permission?(self)
|
||||
end
|
||||
|
||||
def has_originals?
|
||||
pool_posts.each { |pp| return true if pp.slave_id }
|
||||
return false
|
||||
end
|
||||
|
||||
def can_change?(user, attribute)
|
||||
return false if not user.is_member_or_higher?
|
||||
return is_public? || user.has_permission?(self)
|
||||
end
|
||||
|
||||
def update_pool_links
|
||||
transaction do
|
||||
pp = pool_parent_posts(true) # force reload
|
||||
pp.each_index do |i|
|
||||
pp[i].next_post_id = nil
|
||||
pp[i].prev_post_id = nil
|
||||
pp[i].next_post_id = pp[i + 1].post_id unless i == pp.size - 1
|
||||
pp[i].prev_post_id = pp[i - 1].post_id unless i == 0
|
||||
pp[i].save
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def next_sequence
|
||||
seq = 0
|
||||
pool_posts.find(:all, :select => "sequence", :order => "sequence DESC").each { |pp|
|
||||
seq = [seq, pp.sequence.to_i].max
|
||||
}
|
||||
|
||||
return seq + 1
|
||||
end
|
||||
end
|
||||
|
||||
module ApiMethods
|
||||
def api_attributes
|
||||
return {
|
||||
:id => id,
|
||||
:name => name,
|
||||
:created_at => created_at,
|
||||
:updated_at => updated_at,
|
||||
:user_id => user_id,
|
||||
:is_public => is_public,
|
||||
:post_count => post_count,
|
||||
}
|
||||
end
|
||||
|
||||
def to_json(*params)
|
||||
api_attributes.to_json(*params)
|
||||
end
|
||||
|
||||
def to_xml(options = {})
|
||||
options[:indent] ||= 2
|
||||
xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
||||
xml.pool(api_attributes) do
|
||||
xml.description(description)
|
||||
yield options[:builder] if block_given?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module NameMethods
|
||||
module ClassMethods
|
||||
def find_by_name(name)
|
||||
if name =~ /^\d+$/
|
||||
find_by_id(name)
|
||||
else
|
||||
find(:first, :conditions => ["lower(name) = lower(?)", name])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(m)
|
||||
m.extend(ClassMethods)
|
||||
m.validates_uniqueness_of :name
|
||||
m.before_validation :normalize_name
|
||||
end
|
||||
|
||||
def normalize_name
|
||||
self.name = name.gsub(/\s/, "_")
|
||||
end
|
||||
|
||||
def pretty_name
|
||||
name.tr("_", " ")
|
||||
end
|
||||
end
|
||||
|
||||
module ZipMethods
|
||||
def get_zip_filename(options={})
|
||||
filename = pretty_name.gsub(/\?/, "")
|
||||
filename += " (JPG)" if options[:jpeg]
|
||||
filename += " (orig)" if options[:originals]
|
||||
"#{filename}.zip"
|
||||
end
|
||||
|
||||
# Return true if any posts in this pool have a generated JPEG version.
|
||||
def has_jpeg_zip?(options={})
|
||||
posts = options[:originals] ? pool_posts : pool_parent_posts
|
||||
posts.each do |pool_post|
|
||||
post = pool_post.post
|
||||
return true if post.has_jpeg?
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
def get_zip_url(control_path, options={})
|
||||
url = Mirrors.select_image_server(self.zip_is_warehoused, self.zip_created_at.to_i, :zipfile => true)
|
||||
url += "/data/zips/#{File.basename(control_path)}"
|
||||
|
||||
# Adds the pretty filename to the end. This is ignored by lighttpd.
|
||||
url += "/#{url_encode(get_zip_filename(options))}"
|
||||
return url
|
||||
end
|
||||
|
||||
# Estimate the size of the ZIP.
|
||||
def get_zip_size(options={})
|
||||
sum = 0
|
||||
posts = options[:originals] ? pool_posts : pool_parent_posts
|
||||
posts.each do |pool_post|
|
||||
post = pool_post.post
|
||||
next if post.status == 'deleted'
|
||||
sum += options[:jpeg] && post.has_jpeg? ? post.jpeg_size : post.file_size
|
||||
end
|
||||
|
||||
return sum
|
||||
end
|
||||
|
||||
def get_zip_control_file_path_for_time(time, options={})
|
||||
jpeg = options[:jpeg] || false
|
||||
originals = options[:originals] || false
|
||||
|
||||
# If this pool has a JPEG version, name the normal version "png". Otherwise, name it
|
||||
# "normal". This only affects the URL used to access the file, so the frontend can
|
||||
# match it for QOS purposes; it doesn't affect the downloaded pool's filename.
|
||||
if jpeg
|
||||
type = "jpeg"
|
||||
elsif has_jpeg_zip?(options) then
|
||||
type = "png"
|
||||
else
|
||||
type = "normal"
|
||||
end
|
||||
|
||||
"#{RAILS_ROOT}/public/data/zips/%s-pool-%08i-%i%s" % [type, self.id, time.to_i, originals ? "-orig":""]
|
||||
end
|
||||
|
||||
def all_posts_in_zip_are_warehoused?(options={})
|
||||
posts = options[:originals] ? pool_posts : pool_parent_posts
|
||||
posts.each do |pool_post|
|
||||
post = pool_post.post
|
||||
next if post.status == 'deleted'
|
||||
return false if not post.is_warehoused?
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
# Generate a mod_zipfile control file for this pool.
|
||||
def get_zip_control_file(options={})
|
||||
return "" if pool_posts.empty?
|
||||
|
||||
jpeg = options[:jpeg] || false
|
||||
originals = options[:originals] || false
|
||||
|
||||
buf = ""
|
||||
|
||||
# Pad sequence numbers in filenames to the longest sequence number. Ignore any text
|
||||
# after the sequence for padding; for example, if we have 1, 5, 10a and 12, then pad
|
||||
# to 2 digits.
|
||||
|
||||
# Always pad to at least 3 digits.
|
||||
max_sequence_digits = 3
|
||||
pool_posts.each do |pool_post|
|
||||
filtered_sequence = pool_post.sequence.gsub(/^([0-9]+(-[0-9]+)?)?.*/, '\1') # 45a -> 45
|
||||
filtered_sequence.split(/-/).each { |p|
|
||||
max_sequence_digits = [p.length, max_sequence_digits].max
|
||||
}
|
||||
end
|
||||
|
||||
filename_count = {}
|
||||
posts = originals ? pool_posts : pool_parent_posts
|
||||
posts.each do |pool_post|
|
||||
post = pool_post.post
|
||||
next if post.status == 'deleted'
|
||||
|
||||
# Strip RAILS_ROOT/public off the file path, so the paths are relative to document-root.
|
||||
if jpeg && post.has_jpeg?
|
||||
path = post.jpeg_path
|
||||
file_ext = "jpg"
|
||||
else
|
||||
path = post.file_path
|
||||
file_ext = post.file_ext
|
||||
end
|
||||
path = path[(RAILS_ROOT + "/public").length .. path.length]
|
||||
|
||||
# For padding filenames, break numbers apart on hyphens and pad each part. For
|
||||
# example, if max_sequence_digits is 3, and we have "88-89", pad it to "088-089".
|
||||
filename = pool_post.sequence.gsub(/^([0-9]+(-[0-9]+)*)(.*)$/) { |m|
|
||||
if $1 != ""
|
||||
suffix = $3
|
||||
numbers = $1.split(/-/).map { |p|
|
||||
"%0*i" % [max_sequence_digits, p.to_i]
|
||||
}.join("-")
|
||||
"%s%s" % [numbers, suffix]
|
||||
else
|
||||
"%s" % [$3]
|
||||
end
|
||||
}
|
||||
|
||||
#filename = "%0*i" % [max_sequence_digits, pool_post.sequence]
|
||||
|
||||
# Avoid duplicate filenames.
|
||||
filename_count[filename] ||= 0
|
||||
filename_count[filename] = filename_count[filename] + 1
|
||||
if filename_count[filename] > 1
|
||||
filename << " (%i)" % [filename_count[filename]]
|
||||
end
|
||||
filename << ".%s" % [file_ext]
|
||||
|
||||
buf << "#{filename}\n"
|
||||
buf << "#{path}\n"
|
||||
if jpeg && post.has_jpeg?
|
||||
buf << "#{post.jpeg_size}\n"
|
||||
buf << "#{post.jpeg_crc32}\n"
|
||||
else
|
||||
buf << "#{post.file_size}\n"
|
||||
buf << "#{post.crc32}\n"
|
||||
end
|
||||
end
|
||||
|
||||
return buf
|
||||
end
|
||||
|
||||
def get_zip_control_file_path(options = {})
|
||||
control_file = self.get_zip_control_file(options)
|
||||
|
||||
# The latest pool ZIP we generated is stored in pool.zip_created_at. If that ZIP
|
||||
# control file still exists, compare it against the control file we just generated,
|
||||
# and reuse it if it hasn't changed.
|
||||
control_path_time = Time.now
|
||||
control_path = self.get_zip_control_file_path_for_time(control_path_time, options)
|
||||
reuse_old_control_file = false
|
||||
if self.zip_created_at then
|
||||
old_path = self.get_zip_control_file_path_for_time(self.zip_created_at, options)
|
||||
begin
|
||||
old_control_file = File.open(old_path).read
|
||||
|
||||
if control_file == old_control_file
|
||||
reuse_old_control_file = true
|
||||
control_path = old_path
|
||||
control_path_time = self.zip_created_at
|
||||
end
|
||||
rescue SystemCallError => e
|
||||
end
|
||||
end
|
||||
|
||||
if not reuse_old_control_file then
|
||||
control_path_temp = control_path + ".temp"
|
||||
File.open(control_path_temp, 'w+') do |fp|
|
||||
fp.write(control_file)
|
||||
end
|
||||
|
||||
FileUtils.mv(control_path_temp, control_path)
|
||||
|
||||
# Only after we've attempted to mirror the control file, update self.zip_created_at.
|
||||
self.update_attributes(:zip_created_at => control_path_time, :zip_is_warehoused => false)
|
||||
end
|
||||
|
||||
if !self.zip_is_warehoused && all_posts_in_zip_are_warehoused?(options)
|
||||
delay = ServerKey.find(:first, :conditions => ["name = 'delay-mirrors-down'"])
|
||||
if delay.nil?
|
||||
delay = ServerKey.create(:name => "delay-mirrors-down", :value => 0)
|
||||
end
|
||||
if delay.value.to_i < Time.now.to_i
|
||||
# Send the control file to all mirrors, if we have any.
|
||||
begin
|
||||
# This is being done interactively, so use a low timeout.
|
||||
Mirrors.copy_file_to_mirrors(control_path, :timeout => 5)
|
||||
self.update_attributes(:zip_is_warehoused => true)
|
||||
rescue Mirrors::MirrorError => e
|
||||
# If mirroring is failing, disable it for a while. It might be timing out, and this
|
||||
# will make the UI unresponsive.
|
||||
delay.update_attributes!(:value => Time.now.to_i + 60*60)
|
||||
ActiveRecord::Base.logger.error("Error warehousing ZIP control file: #{e}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return control_path
|
||||
end
|
||||
end
|
||||
|
||||
include PostMethods
|
||||
include ApiMethods
|
||||
include NameMethods
|
||||
if CONFIG["pool_zips"]
|
||||
include ZipMethods
|
||||
end
|
||||
end
|
||||
|
172
app/models/pool_post.rb
Normal file
172
app/models/pool_post.rb
Normal file
@ -0,0 +1,172 @@
|
||||
class PoolPost < ActiveRecord::Base
|
||||
set_table_name "pools_posts"
|
||||
belongs_to :post
|
||||
belongs_to :pool
|
||||
versioned_parent :pool
|
||||
versioning_display :class => :pool
|
||||
versioned :active, :default => 'f', :allow_reverting_to_default => true
|
||||
versioned :sequence
|
||||
before_save :update_pool
|
||||
|
||||
def can_change_is_public?(user)
|
||||
return user.has_permission?(pool) # only the owner can change is_public
|
||||
end
|
||||
|
||||
def can_change?(user, attribute)
|
||||
return false if not user.is_member_or_higher?
|
||||
return pool.is_public? || user.has_permission?(pool)
|
||||
end
|
||||
|
||||
def pretty_sequence
|
||||
if sequence =~ /^[0-9]+.*/
|
||||
return "##{sequence}"
|
||||
else
|
||||
return "\"#{sequence}\""
|
||||
end
|
||||
end
|
||||
|
||||
def update_pool
|
||||
# Implicit posts never affect the post count, because we always show either the
|
||||
# parent or the child posts in the index, but not both.
|
||||
return if master_id
|
||||
|
||||
if active_changed? then
|
||||
if active then
|
||||
pool.increment!(:post_count)
|
||||
else
|
||||
pool.decrement!(:post_count)
|
||||
end
|
||||
|
||||
pool.save!
|
||||
end
|
||||
end
|
||||
|
||||
# A master pool_post is a post which was added explicitly to the pool whose post has
|
||||
# a parent. A slave pool_post is a post which was added implicitly to the pool, because
|
||||
# it has a child which was added to the pool. (Master/slave terminology is used because
|
||||
# calling these parent and child becomes confusing with its close relationship to
|
||||
# post parents.)
|
||||
#
|
||||
# The active flag is always false for an implicit slave post. Setting the active flag
|
||||
# to true on a slave post means you're adding it explicitly, which will cause it to no
|
||||
# longer be a slave post. This behavior cooperates well with history: simply setting
|
||||
# and unsetting active are converse operations, regardless of whether a post is a slave
|
||||
# or not. For example, if you have a parent and a child that are both explicitly in the
|
||||
# pool, and you remove the parent (causing it to be added as a slave), this will register
|
||||
# as a removal in history; undoing that history action will cause the active flag to be
|
||||
# set to true again, which will undo as expected.
|
||||
belongs_to :master, :class_name => "PoolPost", :foreign_key => "master_id"
|
||||
belongs_to :slave, :class_name => "PoolPost", :foreign_key => "slave_id"
|
||||
|
||||
protected
|
||||
# Find a pool_post that can be a master post of pp: active, explicitly in this pool (not another
|
||||
# slave), doesn't already have a master post, and has self.post as its post parent.
|
||||
def self.find_master_pool_post(pp)
|
||||
sql = <<-SQL
|
||||
SELECT pp.* FROM posts p JOIN pools_posts pp ON (p.id = pp.post_id)
|
||||
WHERE p.parent_id = #{pp.post_id}
|
||||
AND pp.active
|
||||
AND pp.pool_id = #{pp.pool_id}
|
||||
AND pp.master_id IS NULL
|
||||
AND pp.slave_id IS NULL
|
||||
ORDER BY pp.id ASC
|
||||
LIMIT 1
|
||||
SQL
|
||||
new_master = PoolPost.find_by_sql([sql])
|
||||
|
||||
return nil if new_master.empty?
|
||||
return new_master[0]
|
||||
end
|
||||
|
||||
# If our master post is no longer valid, by being deactivated or the post having
|
||||
# its parent changed, unlink us from it.
|
||||
def detach_stale_master
|
||||
# If we already have no master, we have nothing to do.
|
||||
return if not self.master
|
||||
|
||||
# If our master has been deactivated, or we've been explicitly activated, or if our
|
||||
# master is no longer our child, it's no longer a valid parent.
|
||||
return if self.master.active && !self.active && self.master.post.parent_id == self.post_id
|
||||
|
||||
self.master.slave_id = nil
|
||||
self.master.save!
|
||||
|
||||
self.master_id = nil
|
||||
self.master = nil
|
||||
self.save!
|
||||
end
|
||||
|
||||
def find_master_and_propagate
|
||||
# If we have a master post, verify that it's still valid; if not, detach us from it.
|
||||
detach_stale_master
|
||||
|
||||
need_save = false
|
||||
|
||||
# Don't set a slave if we already have a master or a slave, or if we're already active.
|
||||
if !self.slave_id && !self.master_id && !self.active
|
||||
new_master = PoolPost.find_master_pool_post(self)
|
||||
if new_master
|
||||
self.master_id = new_master.id
|
||||
new_master.slave_id = self.id
|
||||
new_master.save!
|
||||
need_save = true
|
||||
end
|
||||
end
|
||||
|
||||
# If we have a master, propagate changes from it to us.
|
||||
if self.master
|
||||
self.sequence = master.sequence
|
||||
need_save = true if self.sequence_changed?
|
||||
end
|
||||
|
||||
self.save! if need_save
|
||||
end
|
||||
|
||||
public
|
||||
# The specified post has had its parent changed.
|
||||
def self.post_parent_changed(post)
|
||||
PoolPost.find(:all, :conditions => ["post_id = ?", post.id]).each { |pp|
|
||||
pp.need_slave_update = true
|
||||
pp.copy_changes_to_slave
|
||||
}
|
||||
end
|
||||
|
||||
# Since copy_changes_to_slave may call self.save, it needs to be run from
|
||||
# post_save and not after_save. We need to know whether attributes have changed
|
||||
# (so we don't run this unnecessarily), so that check needs to be done in after_save,
|
||||
# while dirty flags are still set.
|
||||
after_save :check_if_need_slave_update
|
||||
post_save :copy_changes_to_slave
|
||||
|
||||
attr_accessor :need_slave_update
|
||||
def check_if_need_slave_update
|
||||
self.need_slave_update = true if sequence_changed? || active_changed?
|
||||
return true
|
||||
end
|
||||
|
||||
# After a PoolPost or its post changes, update master PoolPosts.
|
||||
def copy_changes_to_slave
|
||||
return true if !self.need_slave_update
|
||||
self.need_slave_update = false
|
||||
|
||||
# If our sequence changed, we need to copy that to our slave (if any), and if our
|
||||
# active flag was turned off we need to detach from our slave.
|
||||
post_to_update = self.slave
|
||||
|
||||
if !post_to_update && self.active && self.post.parent_id
|
||||
# We have no slave, but we have a parent post and we're active, so we might need to
|
||||
# assign it. Make sure that a PoolPost exists for the parent.
|
||||
post_to_update = PoolPost.find(:first, :conditions => {:pool_id => self.pool_id, :post_id => post.parent_id})
|
||||
if not post_to_update
|
||||
post_to_update = PoolPost.create(:pool_id => self.pool_id, :post_id => post.parent_id, :active => false)
|
||||
end
|
||||
end
|
||||
|
||||
post_to_update.find_master_and_propagate if post_to_update
|
||||
|
||||
self.find_master_and_propagate
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
168
app/models/post.rb
Normal file
168
app/models/post.rb
Normal file
@ -0,0 +1,168 @@
|
||||
Dir["#{RAILS_ROOT}/app/models/post/**/*.rb"].each {|x| require_dependency x}
|
||||
|
||||
class Post < ActiveRecord::Base
|
||||
STATUSES = %w(active pending flagged deleted)
|
||||
|
||||
define_callbacks :after_delete
|
||||
define_callbacks :after_undelete
|
||||
has_many :notes, :order => "id desc"
|
||||
has_one :flag_detail, :class_name => "FlaggedPostDetail"
|
||||
belongs_to :user
|
||||
before_validation_on_create :set_random!
|
||||
before_create :set_index_timestamp!
|
||||
belongs_to :approver, :class_name => "User"
|
||||
attr_accessor :updater_ip_addr, :updater_user_id
|
||||
attr_accessor :metatag_flagged
|
||||
has_many :avatars, :class_name => "User", :foreign_key => "avatar_post_id"
|
||||
after_delete :clear_avatars
|
||||
after_save :commit_flag
|
||||
|
||||
include PostSqlMethods
|
||||
include PostCommentMethods
|
||||
include PostImageStoreMethods
|
||||
include PostVoteMethods
|
||||
include PostTagMethods
|
||||
include PostCountMethods
|
||||
include PostCacheMethods if CONFIG["enable_caching"]
|
||||
include PostParentMethods if CONFIG["enable_parent_posts"]
|
||||
include PostFileMethods
|
||||
include PostChangeSequenceMethods
|
||||
include PostRatingMethods
|
||||
include PostStatusMethods
|
||||
include PostApiMethods
|
||||
include PostMirrorMethods
|
||||
|
||||
def self.destroy_with_reason(id, reason, current_user)
|
||||
post = Post.find(id)
|
||||
post.flag!(reason, current_user.id)
|
||||
if post.flag_detail
|
||||
post.flag_detail.update_attributes(:is_resolved => true)
|
||||
end
|
||||
|
||||
post.delete
|
||||
end
|
||||
|
||||
def delete
|
||||
self.update_attributes(:status => "deleted")
|
||||
self.run_callbacks(:after_delete)
|
||||
end
|
||||
|
||||
def undelete
|
||||
return if self.status == "active"
|
||||
self.update_attributes(:status => "active")
|
||||
self.run_callbacks(:after_undelete)
|
||||
end
|
||||
|
||||
def can_user_delete?(user)
|
||||
if not user.has_permission?(self)
|
||||
return false
|
||||
end
|
||||
|
||||
if not user.is_mod_or_higher? and Time.now - self.created_at > 1.day
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
def clear_avatars
|
||||
User.clear_avatars(self.id)
|
||||
end
|
||||
|
||||
def set_random!
|
||||
self.random = rand;
|
||||
end
|
||||
|
||||
def set_index_timestamp!
|
||||
self.index_timestamp = self.created_at
|
||||
end
|
||||
|
||||
def flag!(reason, creator_id)
|
||||
transaction do
|
||||
update_attributes(:status => "flagged")
|
||||
|
||||
if flag_detail
|
||||
flag_detail.update_attributes(:reason => reason, :user_id => creator_id, :created_at => Time.now)
|
||||
else
|
||||
FlaggedPostDetail.create!(:post_id => id, :reason => reason, :user_id => creator_id, :is_resolved => false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# If the flag_post metatag was used and the current user has access, flag the post.
|
||||
def commit_flag
|
||||
return if self.metatag_flagged.nil?
|
||||
return if not Thread.current["danbooru-user"].is_mod_or_higher?
|
||||
return if self.status != "active"
|
||||
|
||||
self.flag!(self.metatag_flagged, Thread.current["danbooru-user"].id)
|
||||
end
|
||||
|
||||
def approve!(approver_id)
|
||||
if flag_detail
|
||||
flag_detail.update_attributes(:is_resolved => true)
|
||||
end
|
||||
|
||||
update_attributes(:status => "active", :approver_id => approver_id)
|
||||
end
|
||||
|
||||
def voted_by
|
||||
# Cache results
|
||||
if @voted_by.nil?
|
||||
@voted_by = {}
|
||||
(1..3).each { |v|
|
||||
@voted_by[v] = User.find(:all, :joins => "JOIN post_votes v ON v.user_id = users.id", :select => "users.name, users.id", :conditions => ["v.post_id = ? and v.score = ?", self.id, v], :order => "v.updated_at DESC") || []
|
||||
}
|
||||
end
|
||||
|
||||
return @voted_by
|
||||
end
|
||||
|
||||
def favorited_by
|
||||
return voted_by[3]
|
||||
end
|
||||
|
||||
def author
|
||||
return User.find_name(user_id)
|
||||
end
|
||||
|
||||
def delete_from_database
|
||||
delete_file
|
||||
execute_sql("DELETE FROM posts WHERE id = ?", id)
|
||||
end
|
||||
|
||||
def active_notes
|
||||
notes.select {|x| x.is_active?}
|
||||
end
|
||||
|
||||
STATUSES.each do |x|
|
||||
define_method("is_#{x}?") do
|
||||
return status == x
|
||||
end
|
||||
end
|
||||
|
||||
def can_be_seen_by?(user, options={})
|
||||
if not options[:show_deleted] and self.status == 'deleted'
|
||||
return false
|
||||
end
|
||||
CONFIG["can_see_post"].call(user, self)
|
||||
end
|
||||
|
||||
def self.new_deleted?(user)
|
||||
conds = []
|
||||
conds += ["creator_id <> %d" % [user.id]] unless user.is_anonymous?
|
||||
|
||||
newest_topic = ForumPost.find(:first, :order => "id desc", :limit => 1, :select => "created_at", :conditions => conds)
|
||||
return false if newest_topic == nil
|
||||
return newest_topic.created_at > user.last_forum_topic_read_at
|
||||
end
|
||||
|
||||
def normalized_source
|
||||
if source =~ /pixiv\.net\/img\//
|
||||
img_id = source[/(\d+)\.\w+$/, 1]
|
||||
"http://www.pixiv.net/member_illust.php?mode=medium&illust_id=#{img_id}"
|
||||
else
|
||||
source
|
||||
end
|
||||
end
|
||||
end
|
43
app/models/post/api_methods.rb
Normal file
43
app/models/post/api_methods.rb
Normal file
@ -0,0 +1,43 @@
|
||||
module PostApiMethods
|
||||
def api_attributes
|
||||
return {
|
||||
:id => id,
|
||||
:tags => cached_tags,
|
||||
:created_at => created_at,
|
||||
:creator_id => user_id,
|
||||
:author => author,
|
||||
:change => change_seq,
|
||||
:source => source,
|
||||
:score => score,
|
||||
:md5 => md5,
|
||||
:file_size => file_size,
|
||||
:file_url => file_url,
|
||||
:is_shown_in_index => is_shown_in_index,
|
||||
:preview_url => preview_url,
|
||||
:preview_width => preview_dimensions[0],
|
||||
:preview_height => preview_dimensions[1],
|
||||
:sample_url => sample_url,
|
||||
:sample_width => sample_width || width,
|
||||
:sample_height => sample_height || height,
|
||||
:sample_file_size => sample_size,
|
||||
:jpeg_url => jpeg_url,
|
||||
:jpeg_width => jpeg_width || width,
|
||||
:jpeg_height => jpeg_height || height,
|
||||
:jpeg_file_size => jpeg_size,
|
||||
:rating => rating,
|
||||
:has_children => has_children,
|
||||
:parent_id => parent_id,
|
||||
:status => status,
|
||||
:width => width,
|
||||
:height => height
|
||||
}
|
||||
end
|
||||
|
||||
def to_json(*args)
|
||||
return api_attributes.to_json(*args)
|
||||
end
|
||||
|
||||
def to_xml(options = {})
|
||||
return api_attributes.to_xml(options.merge(:root => "post"))
|
||||
end
|
||||
end
|
12
app/models/post/cache_methods.rb
Normal file
12
app/models/post/cache_methods.rb
Normal file
@ -0,0 +1,12 @@
|
||||
module PostCacheMethods
|
||||
def self.included(m)
|
||||
m.after_save :expire_cache
|
||||
m.after_destroy :expire_cache
|
||||
end
|
||||
|
||||
def expire_cache
|
||||
# Have to call this twice in order to expire tags that may have been removed
|
||||
Cache.expire(:tags => old_cached_tags) if old_cached_tags
|
||||
Cache.expire(:tags => cached_tags)
|
||||
end
|
||||
end
|
18
app/models/post/change_sequence_methods.rb
Normal file
18
app/models/post/change_sequence_methods.rb
Normal file
@ -0,0 +1,18 @@
|
||||
module PostChangeSequenceMethods
|
||||
attr_accessor :increment_change_seq
|
||||
|
||||
def self.included(m)
|
||||
m.before_create :touch_change_seq!
|
||||
m.after_save :update_change_seq
|
||||
end
|
||||
|
||||
def touch_change_seq!
|
||||
self.increment_change_seq = true
|
||||
end
|
||||
|
||||
def update_change_seq
|
||||
return if increment_change_seq.nil?
|
||||
execute_sql("UPDATE posts SET change_seq = nextval('post_change_seq') WHERE id = ?", id)
|
||||
self.change_seq = select_value_sql("SELECT change_seq FROM posts WHERE id = ?", id)
|
||||
end
|
||||
end
|
9
app/models/post/comment_methods.rb
Normal file
9
app/models/post/comment_methods.rb
Normal file
@ -0,0 +1,9 @@
|
||||
module PostCommentMethods
|
||||
def self.included(m)
|
||||
m.has_many :comments, :order => "id"
|
||||
end
|
||||
|
||||
def recent_comments
|
||||
Comment.find(:all, :conditions => ["post_id = ?", id], :order => "id desc", :limit => 6).reverse
|
||||
end
|
||||
end
|
49
app/models/post/count_methods.rb
Normal file
49
app/models/post/count_methods.rb
Normal file
@ -0,0 +1,49 @@
|
||||
module PostCountMethods
|
||||
module ClassMethods
|
||||
def fast_count(tags = nil)
|
||||
cache_version = Cache.get("$cache_version").to_i
|
||||
key = "post-count/v=#{cache_version}/#{tags}"
|
||||
|
||||
# memcached protocol is dumb so we need to escape spaces
|
||||
key = key.gsub(/-/, "--").gsub(/ /, "-_")
|
||||
|
||||
count = Cache.get(key) {
|
||||
Post.count_by_sql(Post.generate_sql(tags, :count => true))
|
||||
}.to_i
|
||||
|
||||
return count
|
||||
|
||||
# This is just too brittle, and hard to make work with other features that may
|
||||
# hide posts from the index.
|
||||
# if tags.blank?
|
||||
# return select_value_sql("SELECT row_count FROM table_data WHERE name = 'posts'").to_i
|
||||
# else
|
||||
# c = select_value_sql("SELECT post_count FROM tags WHERE name = ?", tags).to_i
|
||||
# if c == 0
|
||||
# return Post.count_by_sql(Post.generate_sql(tags, :count => true))
|
||||
# else
|
||||
# return c
|
||||
# end
|
||||
# end
|
||||
end
|
||||
|
||||
def recalculate_row_count
|
||||
execute_sql("UPDATE table_data SET row_count = (SELECT COUNT(*) FROM posts WHERE parent_id IS NULL AND status <> 'deleted') WHERE name = 'posts'")
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(m)
|
||||
m.extend(ClassMethods)
|
||||
m.after_create :increment_count
|
||||
m.after_delete :decrement_count
|
||||
m.after_undelete :increment_count
|
||||
end
|
||||
|
||||
def increment_count
|
||||
execute_sql("UPDATE table_data SET row_count = row_count + 1 WHERE name = 'posts'")
|
||||
end
|
||||
|
||||
def decrement_count
|
||||
execute_sql("UPDATE table_data SET row_count = row_count - 1 WHERE name = 'posts'")
|
||||
end
|
||||
end
|
470
app/models/post/file_methods.rb
Normal file
470
app/models/post/file_methods.rb
Normal file
@ -0,0 +1,470 @@
|
||||
require "download"
|
||||
require "zlib"
|
||||
|
||||
# These are methods dealing with getting the image and generating the thumbnail.
|
||||
# It works in conjunction with the image_store methods. Since these methods have
|
||||
# to be called in a specific order, they've been bundled into one module.
|
||||
module PostFileMethods
|
||||
def self.included(m)
|
||||
m.before_validation_on_create :download_source
|
||||
m.before_validation_on_create :ensure_tempfile_exists
|
||||
m.before_validation_on_create :determine_content_type
|
||||
m.before_validation_on_create :validate_content_type
|
||||
m.before_validation_on_create :generate_hash
|
||||
m.before_validation_on_create :set_image_dimensions
|
||||
m.before_validation_on_create :generate_sample
|
||||
m.before_validation_on_create :generate_jpeg
|
||||
m.before_validation_on_create :generate_preview
|
||||
m.before_validation_on_create :move_file
|
||||
end
|
||||
|
||||
def ensure_tempfile_exists
|
||||
unless File.exists?(tempfile_path)
|
||||
errors.add :file, "not found, try uploading again"
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def validate_content_type
|
||||
unless %w(jpg png gif swf).include?(file_ext.downcase)
|
||||
errors.add(:file, "is an invalid content type: " + file_ext.downcase)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def pretty_file_name(options={})
|
||||
# Include the post number and tags. Don't include too many tags for posts that have too
|
||||
# many of them.
|
||||
options[:type] ||= :image
|
||||
|
||||
# If the filename is too long, it might fail to save or lose the extension when saving.
|
||||
# Cut it down as needed. Most tags on moe with lots of tags have lots of characters,
|
||||
# and those tags are the least important (compared to tags like artists, circles, "fixme",
|
||||
# etc).
|
||||
#
|
||||
# Prioritize tags:
|
||||
# - remove artist and circle tags last; these are the most important
|
||||
# - general tags can either be important ("fixme") or useless ("red hair")
|
||||
# - remove character tags first;
|
||||
|
||||
tags = Tag.compact_tags(self.cached_tags, 150)
|
||||
if options[:type] == :sample then
|
||||
tags = "sample"
|
||||
end
|
||||
|
||||
# Filter characters.
|
||||
tags = tags.gsub(/[\/]/, "_")
|
||||
|
||||
name = "#{self.id} #{tags}"
|
||||
if CONFIG["download_filename_prefix"] != ""
|
||||
name = CONFIG["download_filename_prefix"] + " " + name
|
||||
end
|
||||
|
||||
name
|
||||
end
|
||||
|
||||
def file_name
|
||||
md5 + "." + file_ext
|
||||
end
|
||||
|
||||
def delete_tempfile
|
||||
FileUtils.rm_f(tempfile_path)
|
||||
FileUtils.rm_f(tempfile_preview_path)
|
||||
FileUtils.rm_f(tempfile_sample_path)
|
||||
FileUtils.rm_f(tempfile_jpeg_path)
|
||||
end
|
||||
|
||||
def tempfile_path
|
||||
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}.upload"
|
||||
end
|
||||
|
||||
def tempfile_preview_path
|
||||
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}-preview.jpg"
|
||||
end
|
||||
|
||||
# Generate MD5 and CRC32 hashes for the file. Do this before generating samples, so if this
|
||||
# is a duplicate we'll notice before we spend time resizing the image.
|
||||
def regenerate_hash
|
||||
path = tempfile_path
|
||||
if not File.exists?(path)
|
||||
path = file_path
|
||||
end
|
||||
|
||||
if not File.exists?(path)
|
||||
errors.add(:file, "not found")
|
||||
return false
|
||||
end
|
||||
|
||||
# Compute both hashes in one pass.
|
||||
md5_obj = Digest::MD5.new
|
||||
crc32_accum = 0
|
||||
File.open(path, 'rb') { |fp|
|
||||
buf = ""
|
||||
while fp.read(1024*64, buf) do
|
||||
md5_obj << buf
|
||||
crc32_accum = Zlib.crc32(buf, crc32_accum)
|
||||
end
|
||||
}
|
||||
|
||||
self.md5 = md5_obj.hexdigest
|
||||
self.crc32 = crc32_accum
|
||||
end
|
||||
|
||||
def generate_hash
|
||||
if not regenerate_hash
|
||||
return false
|
||||
end
|
||||
|
||||
if Post.exists?(["md5 = ?", md5])
|
||||
delete_tempfile
|
||||
errors.add "md5", "already exists"
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
# Generate the specified image type. If options[:force_regen] is set, generate the file even
|
||||
# if it already exists.
|
||||
def regenerate_images(type, options = {})
|
||||
return true unless image?
|
||||
|
||||
if type == :sample then
|
||||
return false if not generate_sample(options[:force_regen])
|
||||
temp_path = tempfile_sample_path
|
||||
dest_path = sample_path
|
||||
elsif type == :jpeg then
|
||||
return false if not generate_jpeg(options[:force_regen])
|
||||
temp_path = tempfile_jpeg_path
|
||||
dest_path = jpeg_path
|
||||
elsif type == :preview then
|
||||
return false if not generate_preview
|
||||
temp_path = tempfile_preview_path
|
||||
dest_path = preview_path
|
||||
else
|
||||
raise Exception, "unknown type: %s" % type
|
||||
end
|
||||
|
||||
# Only move in the changed files on success. When we return false, the caller won't
|
||||
# save us to the database; we need to only move the new files in if we're going to be
|
||||
# saved. This is normally handled by move_file.
|
||||
if File.exists?(temp_path)
|
||||
FileUtils.mkdir_p(File.dirname(dest_path), :mode => 0775)
|
||||
FileUtils.mv(temp_path, dest_path)
|
||||
FileUtils.chmod(0775, dest_path)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
def generate_preview
|
||||
return true unless image? && width && height
|
||||
|
||||
size = Danbooru.reduce_to({:width=>width, :height=>height}, {:width=>150, :height=>150})
|
||||
|
||||
# Generate the preview from the new sample if we have one to save CPU, otherwise from the image.
|
||||
if File.exists?(tempfile_sample_path)
|
||||
path, ext = tempfile_sample_path, "jpg"
|
||||
elsif File.exists?(sample_path)
|
||||
path, ext = sample_path, "jpg"
|
||||
elsif File.exists?(tempfile_path)
|
||||
path, ext = tempfile_path, file_ext
|
||||
elsif File.exists?(file_path)
|
||||
path, ext = file_path, file_ext
|
||||
else
|
||||
errors.add(:file, "not found")
|
||||
return false
|
||||
end
|
||||
|
||||
begin
|
||||
Danbooru.resize(ext, path, tempfile_preview_path, size, 95)
|
||||
rescue Exception => x
|
||||
errors.add "preview", "couldn't be generated (#{x})"
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
# Automatically download from the source if it's a URL.
|
||||
attr_accessor :received_file
|
||||
def download_source
|
||||
return if source !~ /^http:\/\// || !file_ext.blank?
|
||||
return if received_file
|
||||
|
||||
begin
|
||||
Danbooru.http_get_streaming(source) do |response|
|
||||
File.open(tempfile_path, "wb") do |out|
|
||||
response.read_body do |block|
|
||||
out.write(block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if self.source.to_s =~ /^http/
|
||||
#self.source = "Image board"
|
||||
self.source = ""
|
||||
end
|
||||
|
||||
return true
|
||||
rescue SocketError, URI::Error, Timeout::Error, SystemCallError => x
|
||||
delete_tempfile
|
||||
errors.add "source", "couldn't be opened: #{x}"
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def determine_content_type
|
||||
if not File.exists?(tempfile_path)
|
||||
errors.add_to_base("No file received")
|
||||
return false
|
||||
end
|
||||
|
||||
imgsize = ImageSize.new(File.open(tempfile_path, "rb"))
|
||||
|
||||
unless imgsize.get_width.nil?
|
||||
self.file_ext = imgsize.get_type.gsub(/JPEG/, "JPG").downcase
|
||||
end
|
||||
end
|
||||
|
||||
# Assigns a CGI file to the post. This writes the file to disk and generates a unique file name.
|
||||
def file=(f)
|
||||
return if f.nil? || f.size == 0
|
||||
|
||||
if f.local_path
|
||||
# Large files are stored in the temp directory, so instead of
|
||||
# reading/rewriting through Ruby, just rely on system calls to
|
||||
# copy the file to danbooru's directory.
|
||||
FileUtils.cp(f.local_path, tempfile_path)
|
||||
else
|
||||
File.open(tempfile_path, 'wb') {|nf| nf.write(f.read)}
|
||||
end
|
||||
|
||||
self.received_file = true
|
||||
end
|
||||
|
||||
def set_image_dimensions
|
||||
if image? or flash?
|
||||
imgsize = ImageSize.new(File.open(tempfile_path, "rb"))
|
||||
self.width = imgsize.get_width
|
||||
self.height = imgsize.get_height
|
||||
end
|
||||
self.file_size = File.size(tempfile_path) rescue 0
|
||||
end
|
||||
|
||||
# Returns true if the post is an image format that GD can handle.
|
||||
def image?
|
||||
%w(jpg jpeg gif png).include?(file_ext.downcase)
|
||||
end
|
||||
|
||||
# Returns true if the post is a Flash movie.
|
||||
def flash?
|
||||
file_ext == "swf"
|
||||
end
|
||||
|
||||
def find_ext(file_path)
|
||||
ext = File.extname(file_path)
|
||||
if ext.blank?
|
||||
return "txt"
|
||||
else
|
||||
ext = ext[1..-1].downcase
|
||||
ext = "jpg" if ext == "jpeg"
|
||||
return ext
|
||||
end
|
||||
end
|
||||
|
||||
def content_type_to_file_ext(content_type)
|
||||
case content_type.chomp
|
||||
when "image/jpeg"
|
||||
return "jpg"
|
||||
|
||||
when "image/gif"
|
||||
return "gif"
|
||||
|
||||
when "image/png"
|
||||
return "png"
|
||||
|
||||
when "application/x-shockwave-flash"
|
||||
return "swf"
|
||||
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def preview_dimensions
|
||||
if image?
|
||||
dim = Danbooru.reduce_to({:width => width, :height => height}, {:width => 150, :height => 150})
|
||||
return [dim[:width], dim[:height]]
|
||||
else
|
||||
return [150, 150]
|
||||
end
|
||||
end
|
||||
|
||||
def tempfile_sample_path
|
||||
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}-sample.jpg"
|
||||
end
|
||||
|
||||
def generate_sample(force_regen = false)
|
||||
return true unless image?
|
||||
return true unless CONFIG["image_samples"]
|
||||
return true unless (width && height)
|
||||
return true if (file_ext.downcase == "gif")
|
||||
|
||||
size = Danbooru.reduce_to({:width => width, :height => height}, {:width => CONFIG["sample_width"], :height => CONFIG["sample_height"]}, CONFIG["sample_ratio"])
|
||||
|
||||
# We can generate the sample image during upload or offline. Use tempfile_path
|
||||
# if it exists, otherwise use file_path.
|
||||
path = tempfile_path
|
||||
path = file_path unless File.exists?(path)
|
||||
unless File.exists?(path)
|
||||
errors.add(:file, "not found")
|
||||
return false
|
||||
end
|
||||
|
||||
# If we're not reducing the resolution for the sample image, only reencode if the
|
||||
# source image is above the reencode threshold. Anything smaller won't be reduced
|
||||
# enough by the reencode to bother, so don't reencode it and save disk space.
|
||||
if size[:width] == width && size[:height] == height && File.size?(path) < CONFIG["sample_always_generate_size"]
|
||||
self.sample_width = nil
|
||||
self.sample_height = nil
|
||||
return true
|
||||
end
|
||||
|
||||
# If we already have a sample image, and the parameters havn't changed,
|
||||
# don't regenerate it.
|
||||
if !force_regen && (size[:width] == sample_width && size[:height] == sample_height)
|
||||
return true
|
||||
end
|
||||
|
||||
size = Danbooru.reduce_to({:width => width, :height => height}, {:width => CONFIG["sample_width"], :height => CONFIG["sample_height"]})
|
||||
begin
|
||||
Danbooru.resize(file_ext, path, tempfile_sample_path, size, CONFIG["sample_quality"])
|
||||
rescue Exception => x
|
||||
errors.add "sample", "couldn't be created: #{x}"
|
||||
return false
|
||||
end
|
||||
|
||||
self.sample_width = size[:width]
|
||||
self.sample_height = size[:height]
|
||||
self.sample_size = File.size(tempfile_sample_path)
|
||||
|
||||
crc32_accum = 0
|
||||
File.open(tempfile_sample_path, 'rb') { |fp|
|
||||
buf = ""
|
||||
while fp.read(1024*64, buf) do
|
||||
crc32_accum = Zlib.crc32(buf, crc32_accum)
|
||||
end
|
||||
}
|
||||
self.sample_crc32 = crc32_accum
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
# Returns true if the post has a sample image.
|
||||
def has_sample?
|
||||
sample_width.is_a?(Integer)
|
||||
end
|
||||
|
||||
# Returns true if the post has a sample image, and we're going to use it.
|
||||
def use_sample?(user = nil)
|
||||
if user && !user.show_samples?
|
||||
false
|
||||
else
|
||||
CONFIG["image_samples"] && has_sample?
|
||||
end
|
||||
end
|
||||
|
||||
def sample_url(user = nil)
|
||||
if status != "deleted" && use_sample?(user)
|
||||
store_sample_url
|
||||
else
|
||||
file_url
|
||||
end
|
||||
end
|
||||
|
||||
def get_sample_width(user = nil)
|
||||
if use_sample?(user)
|
||||
sample_width
|
||||
else
|
||||
width
|
||||
end
|
||||
end
|
||||
|
||||
def get_sample_height(user = nil)
|
||||
if use_sample?(user)
|
||||
sample_height
|
||||
else
|
||||
height
|
||||
end
|
||||
end
|
||||
|
||||
def tempfile_jpeg_path
|
||||
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}-jpeg.jpg"
|
||||
end
|
||||
|
||||
# If the JPEG version needs to be generated (or regenerated), output it to tempfile_jpeg_path. On
|
||||
# error, return false; on success or no-op, return true.
|
||||
def generate_jpeg(force_regen = false)
|
||||
return true unless image?
|
||||
return true unless CONFIG["jpeg_enable"]
|
||||
return true unless (width && height)
|
||||
# Only generate JPEGs for PNGs. Don't do it for files that are already JPEGs; we'll just add
|
||||
# artifacts and/or make the file bigger. Don't do it for GIFs; they're usually animated.
|
||||
return true if (file_ext.downcase != "png")
|
||||
|
||||
# We can generate the image during upload or offline. Use tempfile_path
|
||||
# if it exists, otherwise use file_path.
|
||||
path = tempfile_path
|
||||
path = file_path unless File.exists?(path)
|
||||
unless File.exists?(path)
|
||||
errors.add(:file, "not found")
|
||||
return false
|
||||
end
|
||||
|
||||
# If we already have the image, don't regenerate it.
|
||||
if !force_regen && jpeg_width.is_a?(Integer)
|
||||
return true
|
||||
end
|
||||
|
||||
size = Danbooru.reduce_to({:width => width, :height => height}, {:width => CONFIG["jpeg_width"], :height => CONFIG["jpeg_height"]}, CONFIG["jpeg_ratio"])
|
||||
begin
|
||||
Danbooru.resize(file_ext, path, tempfile_jpeg_path, size, CONFIG["jpeg_quality"])
|
||||
rescue Exception => x
|
||||
errors.add "jpeg", "couldn't be created: #{x}"
|
||||
return false
|
||||
end
|
||||
|
||||
self.jpeg_width = size[:width]
|
||||
self.jpeg_height = size[:height]
|
||||
self.jpeg_size = File.size(tempfile_jpeg_path)
|
||||
|
||||
crc32_accum = 0
|
||||
File.open(tempfile_jpeg_path, 'rb') { |fp|
|
||||
buf = ""
|
||||
while fp.read(1024*64, buf) do
|
||||
crc32_accum = Zlib.crc32(buf, crc32_accum)
|
||||
end
|
||||
}
|
||||
self.jpeg_crc32 = crc32_accum
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
def has_jpeg?
|
||||
jpeg_width.is_a?(Integer)
|
||||
end
|
||||
|
||||
# Returns true if the post has a JPEG version, and we're going to use it.
|
||||
def use_jpeg?(user = nil)
|
||||
CONFIG["jpeg_enable"] && has_jpeg?
|
||||
end
|
||||
|
||||
def jpeg_url(user = nil)
|
||||
if status != "deleted" && use_jpeg?(user)
|
||||
store_jpeg_url
|
||||
else
|
||||
file_url
|
||||
end
|
||||
end
|
||||
end
|
312
app/models/post/g
Normal file
312
app/models/post/g
Normal file
@ -0,0 +1,312 @@
|
||||
require "download"
|
||||
|
||||
# These are methods dealing with getting the image and generating the thumbnail.
|
||||
# It works in conjunction with the image_store methods. Since these methods have
|
||||
# to be called in a specific order, they've been bundled into one module.
|
||||
module PostFileMethods
|
||||
def self.included(m)
|
||||
m.before_validation_on_create :download_source
|
||||
m.before_validation_on_create :determine_content_type
|
||||
m.before_validation_on_create :validate_content_type
|
||||
m.before_validation_on_create :generate_hash
|
||||
m.before_validation_on_create :set_image_dimensions
|
||||
m.before_validation_on_create :generate_sample
|
||||
m.before_validation_on_create :generate_preview
|
||||
m.before_validation_on_create :move_file
|
||||
end
|
||||
|
||||
def validate_content_type
|
||||
if file_ext.empty?
|
||||
errors.add_to_base("No file received")
|
||||
return false
|
||||
end
|
||||
|
||||
unless %w(jpg png gif swf).include?(file_ext.downcase)
|
||||
errors.add(:file, "is an invalid content type: " + file_ext.downcase)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def file_name
|
||||
md5 + "." + file_ext
|
||||
end
|
||||
|
||||
def delete_tempfile
|
||||
FileUtils.rm_f(tempfile_path)
|
||||
FileUtils.rm_f(tempfile_preview_path)
|
||||
FileUtils.rm_f(tempfile_sample_path)
|
||||
end
|
||||
|
||||
def tempfile_path
|
||||
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}.upload"
|
||||
end
|
||||
|
||||
def tempfile_preview_path
|
||||
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}-preview.jpg"
|
||||
end
|
||||
|
||||
def file_size
|
||||
File.size(file_path) rescue 0
|
||||
end
|
||||
|
||||
# Generate an MD5 hash for the file.
|
||||
def generate_hash
|
||||
unless File.exists?(tempfile_path)
|
||||
errors.add(:file, "not found")
|
||||
return false
|
||||
end
|
||||
|
||||
self.md5 = File.open(tempfile_path, 'rb') {|fp| Digest::MD5.hexdigest(fp.read)}
|
||||
|
||||
if Post.exists?(["md5 = ?", md5])
|
||||
delete_tempfile
|
||||
errors.add "md5", "already exists"
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
def generate_preview
|
||||
return true unless image? && width && height
|
||||
|
||||
unless File.exists?(tempfile_path)
|
||||
errors.add(:file, "not found")
|
||||
return false
|
||||
end
|
||||
|
||||
size = Danbooru.reduce_to({:width=>width, :height=>height}, {:width=>150, :height=>150})
|
||||
|
||||
# Generate the preview from the new sample if we have one to save CPU, otherwise from the image.
|
||||
if File.exists?(tempfile_sample_path)
|
||||
path, ext = tempfile_sample_path, "jpg"
|
||||
else
|
||||
path, ext = tempfile_path, file_ext
|
||||
end
|
||||
|
||||
begin
|
||||
Danbooru.resize(ext, path, tempfile_preview_path, size, 95)
|
||||
rescue Exception => x
|
||||
errors.add "preview", "couldn't be generated (#{x})"
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
# Automatically download from the source if it's a URL.
|
||||
def download_source
|
||||
return if source !~ /^http:\/\// || !file_ext.blank?
|
||||
|
||||
begin
|
||||
Download.download(:url => source) do |res|
|
||||
File.open(tempfile_path, 'wb') do |out|
|
||||
res.read_body do |block|
|
||||
out.write(block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if self.source.to_s =~ /^http/
|
||||
self.source = "Image board"
|
||||
end
|
||||
|
||||
return true
|
||||
rescue SocketError, URI::Error, SystemCallError => x
|
||||
delete_tempfile
|
||||
errors.add "source", "couldn't be opened: #{x}"
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def determine_content_type
|
||||
if not File.exists?(tempfile_path)
|
||||
errors.add_to_base("No file received")
|
||||
return false
|
||||
end
|
||||
|
||||
imgsize = ImageSize.new(File.open(tempfile_path, 'rb'))
|
||||
if !imgsize.get_width.nil?
|
||||
self.file_ext = imgsize.get_type.gsub(/JPEG/, "JPG").downcase
|
||||
end
|
||||
end
|
||||
|
||||
# Assigns a CGI file to the post. This writes the file to disk and generates a unique file name.
|
||||
def file=(f)
|
||||
return if f.nil? || f.size == 0
|
||||
a = File.new("/tmp/templog", "a+")
|
||||
|
||||
if f.local_path
|
||||
# Large files are stored in the temp directory, so instead of
|
||||
# reading/rewriting through Ruby, just rely on system calls to
|
||||
# copy the file to danbooru's directory.
|
||||
|
||||
a.write("%s to %s\n" % f.local_path, tempfile_path)
|
||||
FileUtils.cp(f.local_path, tempfile_path)
|
||||
else
|
||||
|
||||
a.write("... to %s\n" % tempfile_path)
|
||||
|
||||
File.open(tempfile_path, 'wb') {|nf| nf.write(f.read)}
|
||||
end
|
||||
|
||||
a.close
|
||||
|
||||
end
|
||||
|
||||
def set_image_dimensions
|
||||
if image? or flash?
|
||||
imgsize = ImageSize.new(File.open(tempfile_path, "rb"))
|
||||
self.width = imgsize.get_width
|
||||
self.height = imgsize.get_height
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if the post is an image format that GD can handle.
|
||||
def image?
|
||||
%w(jpg jpeg gif png).include?(file_ext.downcase)
|
||||
end
|
||||
|
||||
# Returns true if the post is a Flash movie.
|
||||
def flash?
|
||||
file_ext == "swf"
|
||||
end
|
||||
|
||||
def find_ext(file_path)
|
||||
ext = File.extname(file_path)
|
||||
if ext.blank?
|
||||
return "txt"
|
||||
else
|
||||
ext = ext[1..-1].downcase
|
||||
ext = "jpg" if ext == "jpeg"
|
||||
return ext
|
||||
end
|
||||
end
|
||||
|
||||
def content_type_to_file_ext(content_type)
|
||||
case content_type.chomp
|
||||
when "image/jpeg"
|
||||
return "jpg"
|
||||
|
||||
when "image/gif"
|
||||
return "gif"
|
||||
|
||||
when "image/png"
|
||||
return "png"
|
||||
|
||||
when "application/x-shockwave-flash"
|
||||
return "swf"
|
||||
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def preview_dimensions
|
||||
if image? && !is_deleted?
|
||||
dim = Danbooru.reduce_to({:width => width, :height => height}, {:width => 150, :height => 150})
|
||||
return [dim[:width], dim[:height]]
|
||||
else
|
||||
return [150, 150]
|
||||
end
|
||||
end
|
||||
|
||||
def tempfile_sample_path
|
||||
"#{RAILS_ROOT}/public/data/#{$PROCESS_ID}-sample.jpg"
|
||||
end
|
||||
|
||||
def regenerate_sample
|
||||
return false unless image?
|
||||
|
||||
if generate_sample && File.exists?(tempfile_sample_path)
|
||||
FileUtils.mkdir_p(File.dirname(sample_path), :mode => 0775)
|
||||
FileUtils.mv(tempfile_sample_path, sample_path)
|
||||
FileUtils.chmod(0775, sample_path)
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def generate_sample
|
||||
return true unless image?
|
||||
return true unless CONFIG["image_samples"]
|
||||
return true unless (width && height)
|
||||
return true if (file_ext.downcase == "gif")
|
||||
|
||||
size = Danbooru.reduce_to({:width => width, :height => height}, {:width => CONFIG["sample_width"], :height => CONFIG["sample_height"]}, CONFIG["sample_ratio"])
|
||||
|
||||
# We can generate the sample image during upload or offline. Use tempfile_path
|
||||
# if it exists, otherwise use file_path.
|
||||
path = tempfile_path
|
||||
path = file_path unless File.exists?(path)
|
||||
unless File.exists?(path)
|
||||
errors.add(:file, "not found")
|
||||
return false
|
||||
end
|
||||
|
||||
# If we're not reducing the resolution for the sample image, only reencode if the
|
||||
# source image is above the reencode threshold. Anything smaller won't be reduced
|
||||
# enough by the reencode to bother, so don't reencode it and save disk space.
|
||||
if size[:width] == width && size[:height] == height &&
|
||||
File.size?(path) < CONFIG["sample_always_generate_size"]
|
||||
return true
|
||||
end
|
||||
|
||||
# If we already have a sample image, and the parameters havn't changed,
|
||||
# don't regenerate it.
|
||||
if size[:width] == sample_width && size[:height] == sample_height
|
||||
return true
|
||||
end
|
||||
|
||||
size = Danbooru.reduce_to({:width => width, :height => height}, {:width => CONFIG["sample_width"], :height => CONFIG["sample_height"]})
|
||||
begin
|
||||
Danbooru.resize(file_ext, path, tempfile_sample_path, size, 95)
|
||||
rescue Exception => x
|
||||
errors.add "sample", "couldn't be created: #{x}"
|
||||
return false
|
||||
end
|
||||
|
||||
self.sample_width = size[:width]
|
||||
self.sample_height = size[:height]
|
||||
return true
|
||||
end
|
||||
|
||||
# Returns true if the post has a sample image.
|
||||
def has_sample?
|
||||
sample_width.is_a?(Integer)
|
||||
end
|
||||
|
||||
# Returns true if the post has a sample image, and we're going to use it.
|
||||
def use_sample?(user = nil)
|
||||
if user && !user.show_samples?
|
||||
false
|
||||
else
|
||||
CONFIG["image_samples"] && has_sample?
|
||||
end
|
||||
end
|
||||
|
||||
def sample_url(user = nil)
|
||||
if status != "deleted" && use_sample?(user)
|
||||
store_sample_url
|
||||
else
|
||||
file_url
|
||||
end
|
||||
end
|
||||
|
||||
def get_sample_width(user = nil)
|
||||
if use_sample?(user)
|
||||
sample_width
|
||||
else
|
||||
width
|
||||
end
|
||||
end
|
||||
|
||||
def get_sample_height(user = nil)
|
||||
if use_sample?(user)
|
||||
sample_height
|
||||
else
|
||||
height
|
||||
end
|
||||
end
|
||||
end
|
56
app/models/post/image_store/amazon_s3.rb
Normal file
56
app/models/post/image_store/amazon_s3.rb
Normal file
@ -0,0 +1,56 @@
|
||||
module PostImageStoreMethods
|
||||
module AmazonS3
|
||||
def move_file
|
||||
begin
|
||||
base64_md5 = Base64.encode64(self.md5.unpack("a2" * (self.md5.size / 2)).map {|x| x.hex.chr}.join)
|
||||
|
||||
AWS::S3::Base.establish_connection!(:access_key_id => CONFIG["amazon_s3_access_key_id"], :secret_access_key => CONFIG["amazon_s3_secret_access_key"])
|
||||
AWS::S3::S3Object.store(file_name, open(self.tempfile_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read, "Content-MD5" => base64_md5, "Cache-Control" => "max-age=315360000")
|
||||
|
||||
if image?
|
||||
AWS::S3::S3Object.store("preview/#{md5}.jpg", open(self.tempfile_preview_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read, "Cache-Control" => "max-age=315360000")
|
||||
end
|
||||
|
||||
if File.exists?(tempfile_sample_path)
|
||||
AWS::S3::S3Object.store("sample/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg", open(self.tempfile_sample_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read, "Cache-Control" => "max-age=315360000")
|
||||
end
|
||||
|
||||
if File.exists?(tempfile_jpeg_path)
|
||||
AWS::S3::S3Object.store("jpeg/#{md5}.jpg", open(self.tempfile_jpeg_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read, "Cache-Control" => "max-age=315360000")
|
||||
end
|
||||
|
||||
return true
|
||||
ensure
|
||||
self.delete_tempfile()
|
||||
end
|
||||
end
|
||||
|
||||
def file_url
|
||||
"http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/#{file_name}"
|
||||
end
|
||||
|
||||
def preview_url
|
||||
if self.image?
|
||||
"http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/preview/#{md5}.jpg"
|
||||
else
|
||||
"http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/preview/download.png"
|
||||
end
|
||||
end
|
||||
|
||||
def store_sample_url
|
||||
"http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/sample/deleted.png"
|
||||
end
|
||||
|
||||
def store_jpeg_url
|
||||
"http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/jpeg/deleted.png"
|
||||
end
|
||||
|
||||
def delete_file
|
||||
AWS::S3::Base.establish_connection!(:access_key_id => CONFIG["amazon_s3_access_key_id"], :secret_access_key => CONFIG["amazon_s3_secret_access_key"])
|
||||
AWS::S3::S3Object.delete(file_name, CONFIG["amazon_s3_bucket_name"])
|
||||
AWS::S3::S3Object.delete("preview/#{md5}.jpg", CONFIG["amazon_s3_bucket_name"])
|
||||
AWS::S3::S3Object.delete("sample/#{md5}.jpg", CONFIG["amazon_s3_bucket_name"])
|
||||
AWS::S3::S3Object.delete("jpeg/#{md5}.jpg", CONFIG["amazon_s3_bucket_name"])
|
||||
end
|
||||
end
|
||||
end
|
88
app/models/post/image_store/local_flat.rb
Normal file
88
app/models/post/image_store/local_flat.rb
Normal file
@ -0,0 +1,88 @@
|
||||
module PostImageStoreMethods
|
||||
module LocalFlat
|
||||
def file_path
|
||||
"#{RAILS_ROOT}/public/data/#{file_name}"
|
||||
end
|
||||
|
||||
def file_url
|
||||
if CONFIG["use_pretty_image_urls"] then
|
||||
CONFIG["url_base"] + "/image/#{md5}/#{url_encode(pretty_file_name)}.#{file_ext}"
|
||||
else
|
||||
CONFIG["url_base"] + "/data/#{file_name}"
|
||||
end
|
||||
end
|
||||
|
||||
def preview_path
|
||||
if status == "deleted"
|
||||
"#{RAILS_ROOT}/public/deleted-preview.png"
|
||||
elsif image?
|
||||
"#{RAILS_ROOT}/public/data/preview/#{md5}.jpg"
|
||||
else
|
||||
"#{RAILS_ROOT}/public/download-preview.png"
|
||||
end
|
||||
end
|
||||
|
||||
def sample_path
|
||||
"#{RAILS_ROOT}/public/data/sample/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
|
||||
end
|
||||
|
||||
def preview_url
|
||||
if image?
|
||||
CONFIG["url_base"] + "/data/preview/#{md5}.jpg"
|
||||
else
|
||||
CONFIG["url_base"] + "/download-preview.png"
|
||||
end
|
||||
end
|
||||
|
||||
def jpeg_path
|
||||
"#{RAILS_ROOT}/public/data/jpeg/#{file_hierarchy}/#{md5}.jpg"
|
||||
end
|
||||
|
||||
def store_jpeg_url
|
||||
if CONFIG["use_pretty_image_urls"] then
|
||||
CONFIG["url_base"] + "/jpeg/#{md5}/#{url_encode(pretty_file_name({:type => :jpeg}))}.jpg"
|
||||
else
|
||||
CONFIG["url_base"] + "/data/jpeg/#{md5}.jpg"
|
||||
end
|
||||
end
|
||||
|
||||
def store_sample_url
|
||||
if CONFIG["use_pretty_image_urls"] then
|
||||
path = "/sample/#{md5}/#{url_encode(pretty_file_name({:type => :sample}))}.jpg"
|
||||
else
|
||||
path = "/data/sample/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
|
||||
end
|
||||
|
||||
CONFIG["url_base"] + path
|
||||
end
|
||||
|
||||
def delete_file
|
||||
FileUtils.rm_f(file_path)
|
||||
FileUtils.rm_f(preview_path) if image?
|
||||
FileUtils.rm_f(sample_path) if image?
|
||||
FileUtils.rm_f(jpeg_path) if image?
|
||||
end
|
||||
|
||||
def move_file
|
||||
FileUtils.mv(tempfile_path, file_path)
|
||||
FileUtils.chmod(0664, file_path)
|
||||
|
||||
if image?
|
||||
FileUtils.mv(tempfile_preview_path, preview_path)
|
||||
FileUtils.chmod(0664, preview_path)
|
||||
end
|
||||
|
||||
if File.exists?(tempfile_sample_path)
|
||||
FileUtils.mv(tempfile_sample_path, sample_path)
|
||||
FileUtils.chmod(0664, sample_path)
|
||||
end
|
||||
|
||||
if File.exists?(tempfile_jpeg_path)
|
||||
FileUtils.mv(tempfile_jpeg_path, jpeg_path)
|
||||
FileUtils.chmod(0664, jpeg_path)
|
||||
end
|
||||
|
||||
delete_tempfile
|
||||
end
|
||||
end
|
||||
end
|
105
app/models/post/image_store/local_flat_with_amazon_s3_backup.rb
Normal file
105
app/models/post/image_store/local_flat_with_amazon_s3_backup.rb
Normal file
@ -0,0 +1,105 @@
|
||||
module PostImageStoreMethods
|
||||
module LocalFlatWithAmazonS3Backup
|
||||
def move_file
|
||||
FileUtils.mv(tempfile_path, file_path)
|
||||
FileUtils.chmod(0664, file_path)
|
||||
|
||||
if image?
|
||||
FileUtils.mv(tempfile_preview_path, preview_path)
|
||||
FileUtils.chmod(0664, preview_path)
|
||||
end
|
||||
|
||||
if File.exists?(tempfile_sample_path)
|
||||
FileUtils.mv(tempfile_sample_path, sample_path)
|
||||
FileUtils.chmod(0664, sample_path)
|
||||
end
|
||||
|
||||
if File.exists?(tempfile_jpeg_path)
|
||||
FileUtils.mv(tempfile_jpeg_path, jpeg_path)
|
||||
FileUtils.chmod(0664, jpeg_path)
|
||||
end
|
||||
|
||||
self.delete_tempfile()
|
||||
|
||||
base64_md5 = Base64.encode64(self.md5.unpack("a2" * (self.md5.size / 2)).map {|x| x.hex.chr}.join)
|
||||
|
||||
AWS::S3::Base.establish_connection!(:access_key_id => CONFIG["amazon_s3_access_key_id"], :secret_access_key => CONFIG["amazon_s3_secret_access_key"])
|
||||
AWS::S3::S3Object.store(file_name, open(self.file_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read, "Content-MD5" => base64_md5)
|
||||
|
||||
if image?
|
||||
AWS::S3::S3Object.store("preview/#{md5}.jpg", open(self.preview_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read)
|
||||
end
|
||||
|
||||
if File.exists?(tempfile_sample_path)
|
||||
AWS::S3::S3Object.store("sample/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg", open(self.sample_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read)
|
||||
end
|
||||
|
||||
if File.exists?(tempfile_jpeg_path)
|
||||
AWS::S3::S3Object.store("jpeg/#{md5}.jpg", open(self.jpeg_path, "rb"), CONFIG["amazon_s3_bucket_name"], :access => :public_read)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
def file_path
|
||||
"#{RAILS_ROOT}/public/data/#{file_name}"
|
||||
end
|
||||
|
||||
def file_url
|
||||
#"http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/#{file_name}"
|
||||
CONFIG["url_base"] + "/data/#{file_name}"
|
||||
end
|
||||
|
||||
def preview_path
|
||||
if status == "deleted"
|
||||
"#{RAILS_ROOT}/public/deleted-preview.png"
|
||||
elsif image?
|
||||
"#{RAILS_ROOT}/public/data/preview/#{md5}.jpg"
|
||||
else
|
||||
"#{RAILS_ROOT}/public/download-preview.png"
|
||||
end
|
||||
end
|
||||
|
||||
def sample_path
|
||||
"#{RAILS_ROOT}/public/data/sample/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
|
||||
end
|
||||
|
||||
def preview_url
|
||||
# if self.image?
|
||||
# "http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/preview/#{md5}.jpg"
|
||||
# else
|
||||
# "http://s3.amazonaws.com/" + CONFIG["amazon_s3_bucket_name"] + "/preview/download.png"
|
||||
# end
|
||||
|
||||
if self.image?
|
||||
CONFIG["url_base"] + "/data/preview/#{md5}.jpg"
|
||||
else
|
||||
CONFIG["url_base"] + "/download-preview.png"
|
||||
end
|
||||
end
|
||||
|
||||
def jpeg_path
|
||||
"#{RAILS_ROOT}/public/data/jpeg/#{md5}.jpg"
|
||||
end
|
||||
|
||||
def store_jpeg_url
|
||||
CONFIG["url_base"] + "/data/jpeg/#{md5}.jpg"
|
||||
end
|
||||
|
||||
def store_sample_url
|
||||
CONFIG["url_base"] + "/data/sample/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
|
||||
end
|
||||
|
||||
def delete_file
|
||||
AWS::S3::Base.establish_connection!(:access_key_id => CONFIG["amazon_s3_access_key_id"], :secret_access_key => CONFIG["amazon_s3_secret_access_key"])
|
||||
AWS::S3::S3Object.delete(file_name, CONFIG["amazon_s3_bucket_name"])
|
||||
AWS::S3::S3Object.delete("preview/#{md5}.jpg", CONFIG["amazon_s3_bucket_name"])
|
||||
AWS::S3::S3Object.delete("sample/#{md5}.jpg", CONFIG["amazon_s3_bucket_name"])
|
||||
AWS::S3::S3Object.delete("jpeg/#{md5}.jpg", CONFIG["amazon_s3_bucket_name"])
|
||||
FileUtils.rm_f(file_path)
|
||||
FileUtils.rm_f(preview_path) if image?
|
||||
FileUtils.rm_f(sample_path) if image?
|
||||
FileUtils.rm_f(jpeg_path) if image?
|
||||
end
|
||||
end
|
||||
end
|
94
app/models/post/image_store/local_hierarchy.rb
Normal file
94
app/models/post/image_store/local_hierarchy.rb
Normal file
@ -0,0 +1,94 @@
|
||||
module PostImageStoreMethods
|
||||
module LocalHierarchy
|
||||
def file_hierarchy
|
||||
"%s/%s" % [md5[0,2], md5[2,2]]
|
||||
end
|
||||
|
||||
def file_path
|
||||
"#{RAILS_ROOT}/public/data/image/#{file_hierarchy}/#{file_name}"
|
||||
end
|
||||
|
||||
def file_url
|
||||
if CONFIG["use_pretty_image_urls"] then
|
||||
CONFIG["url_base"] + "/image/#{md5}/#{url_encode(pretty_file_name)}.#{file_ext}"
|
||||
else
|
||||
CONFIG["url_base"] + "/data/image/#{file_hierarchy}/#{file_name}"
|
||||
end
|
||||
end
|
||||
|
||||
def preview_path
|
||||
if status == "deleted"
|
||||
"#{RAILS_ROOT}/public/deleted-preview.png"
|
||||
elsif image?
|
||||
"#{RAILS_ROOT}/public/data/preview/#{file_hierarchy}/#{md5}.jpg"
|
||||
else
|
||||
"#{RAILS_ROOT}/public/download-preview.png"
|
||||
end
|
||||
end
|
||||
|
||||
def sample_path
|
||||
"#{RAILS_ROOT}/public/data/sample/#{file_hierarchy}/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
|
||||
end
|
||||
|
||||
def preview_url
|
||||
if image?
|
||||
CONFIG["url_base"] + "/data/preview/#{file_hierarchy}/#{md5}.jpg"
|
||||
else
|
||||
CONFIG["url_base"] + "/download-preview.png"
|
||||
end
|
||||
end
|
||||
|
||||
def jpeg_path
|
||||
"#{RAILS_ROOT}/public/data/jpeg/#{file_hierarchy}/#{md5}.jpg"
|
||||
end
|
||||
|
||||
def store_jpeg_url
|
||||
if CONFIG["use_pretty_image_urls"] then
|
||||
CONFIG["url_base"] + "/jpeg/#{md5}/#{url_encode(pretty_file_name({:type => :jpeg}))}.jpg"
|
||||
else
|
||||
CONFIG["url_base"] + "/data/jpeg/#{file_hierarchy}/#{md5}.jpg"
|
||||
end
|
||||
end
|
||||
|
||||
def store_sample_url
|
||||
if CONFIG["use_pretty_image_urls"] then
|
||||
CONFIG["url_base"] + "/sample/#{md5}/#{url_encode(pretty_file_name({:type => :sample}))}.jpg"
|
||||
else
|
||||
CONFIG["url_base"] + "/data/sample/#{file_hierarchy}/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
|
||||
end
|
||||
end
|
||||
|
||||
def delete_file
|
||||
FileUtils.rm_f(file_path)
|
||||
FileUtils.rm_f(preview_path) if image?
|
||||
FileUtils.rm_f(sample_path) if image?
|
||||
FileUtils.rm_f(jpeg_path) if image?
|
||||
end
|
||||
|
||||
def move_file
|
||||
FileUtils.mkdir_p(File.dirname(file_path), :mode => 0775)
|
||||
FileUtils.mv(tempfile_path, file_path)
|
||||
FileUtils.chmod(0664, file_path)
|
||||
|
||||
if image?
|
||||
FileUtils.mkdir_p(File.dirname(preview_path), :mode => 0775)
|
||||
FileUtils.mv(tempfile_preview_path, preview_path)
|
||||
FileUtils.chmod(0664, preview_path)
|
||||
end
|
||||
|
||||
if File.exists?(tempfile_sample_path)
|
||||
FileUtils.mkdir_p(File.dirname(sample_path), :mode => 0775)
|
||||
FileUtils.mv(tempfile_sample_path, sample_path)
|
||||
FileUtils.chmod(0664, sample_path)
|
||||
end
|
||||
|
||||
if File.exists?(tempfile_jpeg_path)
|
||||
FileUtils.mkdir_p(File.dirname(jpeg_path), :mode => 0775)
|
||||
FileUtils.mv(tempfile_jpeg_path, jpeg_path)
|
||||
FileUtils.chmod(0664, jpeg_path)
|
||||
end
|
||||
|
||||
delete_tempfile
|
||||
end
|
||||
end
|
||||
end
|
117
app/models/post/image_store/remote_hierarchy.rb
Normal file
117
app/models/post/image_store/remote_hierarchy.rb
Normal file
@ -0,0 +1,117 @@
|
||||
require "mirror"
|
||||
require "erb"
|
||||
include ERB::Util
|
||||
|
||||
module PostImageStoreMethods
|
||||
module RemoteHierarchy
|
||||
def file_hierarchy
|
||||
"%s/%s" % [md5[0,2], md5[2,2]]
|
||||
end
|
||||
|
||||
def select_random_image_server(*options)
|
||||
Mirrors.select_image_server(self.is_warehoused?, self.created_at.to_i, *options)
|
||||
end
|
||||
|
||||
def file_path
|
||||
"#{RAILS_ROOT}/public/data/image/#{file_hierarchy}/#{file_name}"
|
||||
end
|
||||
|
||||
def file_url
|
||||
if CONFIG["use_pretty_image_urls"] then
|
||||
select_random_image_server + "/image/#{md5}/#{url_encode(pretty_file_name)}.#{file_ext}"
|
||||
else
|
||||
select_random_image_server + "/data/image/#{file_hierarchy}/#{file_name}"
|
||||
end
|
||||
end
|
||||
|
||||
def preview_path
|
||||
if image?
|
||||
"#{RAILS_ROOT}/public/data/preview/#{file_hierarchy}/#{md5}.jpg"
|
||||
else
|
||||
"#{RAILS_ROOT}/public/download-preview.png"
|
||||
end
|
||||
end
|
||||
|
||||
def sample_path
|
||||
"#{RAILS_ROOT}/public/data/sample/#{file_hierarchy}/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
|
||||
end
|
||||
|
||||
def preview_url
|
||||
if self.is_warehoused?
|
||||
if status == "deleted"
|
||||
CONFIG["url_base"] + "/deleted-preview.png"
|
||||
|
||||
elsif image?
|
||||
select_random_image_server(:preview => true) + "/data/preview/#{file_hierarchy}/#{md5}.jpg"
|
||||
else
|
||||
CONFIG["url_base"] + "/download-preview.png"
|
||||
end
|
||||
else
|
||||
if status == "deleted"
|
||||
CONFIG["url_base"] + "/deleted-preview.png"
|
||||
elsif image?
|
||||
Mirrors.select_main_image_server + "/data/preview/#{file_hierarchy}/#{md5}.jpg"
|
||||
else
|
||||
CONFIG["url_base"] + "/download-preview.png"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def jpeg_path
|
||||
"#{RAILS_ROOT}/public/data/jpeg/#{file_hierarchy}/#{md5}.jpg"
|
||||
end
|
||||
|
||||
def store_jpeg_url
|
||||
if CONFIG["use_pretty_image_urls"] then
|
||||
path = CONFIG["url_base"] + "/jpeg/#{md5}/#{url_encode(pretty_file_name({:type => :jpeg}))}.jpg"
|
||||
else
|
||||
path = CONFIG["url_base"] + "/data/jpeg/#{file_hierarchy}/#{md5}.jpg"
|
||||
end
|
||||
|
||||
return select_random_image_server + path
|
||||
end
|
||||
|
||||
def store_sample_url
|
||||
if CONFIG["use_pretty_image_urls"] then
|
||||
path = "/sample/#{md5}/#{url_encode(pretty_file_name({:type => :sample}))}.jpg"
|
||||
else
|
||||
path = "/data/sample/#{file_hierarchy}/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
|
||||
end
|
||||
|
||||
return select_random_image_server + path
|
||||
end
|
||||
|
||||
def delete_file
|
||||
FileUtils.rm_f(file_path)
|
||||
FileUtils.rm_f(preview_path) if image?
|
||||
FileUtils.rm_f(sample_path) if image?
|
||||
FileUtils.rm_f(jpeg_path) if image?
|
||||
end
|
||||
|
||||
def move_file
|
||||
FileUtils.mkdir_p(File.dirname(file_path), :mode => 0775)
|
||||
FileUtils.mv(tempfile_path, file_path)
|
||||
FileUtils.chmod(0664, file_path)
|
||||
|
||||
if image?
|
||||
FileUtils.mkdir_p(File.dirname(preview_path), :mode => 0775)
|
||||
FileUtils.mv(tempfile_preview_path, preview_path)
|
||||
FileUtils.chmod(0664, preview_path)
|
||||
end
|
||||
|
||||
if File.exists?(tempfile_sample_path)
|
||||
FileUtils.mkdir_p(File.dirname(sample_path), :mode => 0775)
|
||||
FileUtils.mv(tempfile_sample_path, sample_path)
|
||||
FileUtils.chmod(0664, sample_path)
|
||||
end
|
||||
|
||||
if File.exists?(tempfile_jpeg_path)
|
||||
FileUtils.mkdir_p(File.dirname(jpeg_path), :mode => 0775)
|
||||
FileUtils.mv(tempfile_jpeg_path, jpeg_path)
|
||||
FileUtils.chmod(0664, jpeg_path)
|
||||
end
|
||||
|
||||
delete_tempfile
|
||||
end
|
||||
end
|
||||
end
|
20
app/models/post/image_store_methods.rb
Normal file
20
app/models/post/image_store_methods.rb
Normal file
@ -0,0 +1,20 @@
|
||||
module PostImageStoreMethods
|
||||
def self.included(m)
|
||||
case CONFIG["image_store"]
|
||||
when :local_flat
|
||||
m.__send__(:include, PostImageStoreMethods::LocalFlat)
|
||||
|
||||
when :local_flat_with_amazon_s3_backup
|
||||
m.__send__(:include, PostImageStoreMethods::LocalFlatWithAmazonS3Backup)
|
||||
|
||||
when :local_hierarchy
|
||||
m.__send__(:include, PostImageStoreMethods::LocalHierarchy)
|
||||
|
||||
when :remote_hierarchy
|
||||
m.__send__(:include, PostImageStoreMethods::RemoteHierarchy)
|
||||
|
||||
when :amazon_s3
|
||||
m.__send__(:include, PostImageStoreMethods::AmazonS3)
|
||||
end
|
||||
end
|
||||
end
|
49
app/models/post/mirror_methods.rb
Normal file
49
app/models/post/mirror_methods.rb
Normal file
@ -0,0 +1,49 @@
|
||||
require "mirror"
|
||||
|
||||
class MirrorError < Exception ; end
|
||||
|
||||
module PostMirrorMethods
|
||||
def upload_to_mirrors
|
||||
return if is_warehoused
|
||||
return if self.status == "deleted"
|
||||
|
||||
files_to_copy = [self.file_path]
|
||||
files_to_copy << self.preview_path if self.image?
|
||||
files_to_copy << self.sample_path if self.has_sample?
|
||||
files_to_copy << self.jpeg_path if self.has_jpeg?
|
||||
files_to_copy = files_to_copy.uniq
|
||||
|
||||
# CONFIG[:data_dir] is equivalent to our local_base.
|
||||
local_base = "#{RAILS_ROOT}/public/data/"
|
||||
|
||||
CONFIG["mirrors"].each { |mirror|
|
||||
remote_user_host = "#{mirror[:user]}@#{mirror[:host]}"
|
||||
remote_dirs = []
|
||||
files_to_copy.each { |file|
|
||||
remote_filename = file[local_base.length, file.length]
|
||||
remote_dir = File.dirname(remote_filename)
|
||||
remote_dirs << mirror[:data_dir] + "/" + File.dirname(remote_filename)
|
||||
}
|
||||
|
||||
# Create all directories in one go.
|
||||
system("/usr/bin/ssh", "-o", "Compression=no", "-o", "BatchMode=yes",
|
||||
remote_user_host, "mkdir -p #{remote_dirs.uniq.join(" ")}")
|
||||
}
|
||||
|
||||
begin
|
||||
files_to_copy.each { |file|
|
||||
Mirrors.copy_file_to_mirrors(file)
|
||||
}
|
||||
rescue MirrorError => e
|
||||
# The post might be deleted while it's uploading. Check the post status after
|
||||
# an error.
|
||||
self.reload
|
||||
raise if self.status != "deleted"
|
||||
end
|
||||
|
||||
# This might take a while. Rather than hold a transaction, just reload the post
|
||||
# after uploading.
|
||||
self.reload
|
||||
self.update_attributes(:is_warehoused => true)
|
||||
end
|
||||
end
|
70
app/models/post/parent_methods.rb
Normal file
70
app/models/post/parent_methods.rb
Normal file
@ -0,0 +1,70 @@
|
||||
module PostParentMethods
|
||||
module ClassMethods
|
||||
def update_has_children(post_id)
|
||||
has_children = Post.exists?(["parent_id = ? AND status <> 'deleted'", post_id])
|
||||
execute_sql("UPDATE posts SET has_children = ? WHERE id = ?", has_children, post_id)
|
||||
end
|
||||
|
||||
def recalculate_has_children
|
||||
transaction do
|
||||
execute_sql("UPDATE posts SET has_children = false WHERE has_children = true")
|
||||
execute_sql("UPDATE posts SET has_children = true WHERE id IN (SELECT parent_id FROM posts WHERE parent_id IS NOT NULL AND status <> 'deleted')")
|
||||
end
|
||||
end
|
||||
|
||||
def set_parent(post_id, parent_id, old_parent_id = nil)
|
||||
if old_parent_id.nil?
|
||||
old_parent_id = select_value_sql("SELECT parent_id FROM posts WHERE id = ?", post_id)
|
||||
end
|
||||
|
||||
if parent_id.to_i == post_id.to_i || parent_id.to_i == 0
|
||||
parent_id = nil
|
||||
end
|
||||
|
||||
execute_sql("UPDATE posts SET parent_id = ? WHERE id = ?", parent_id, post_id)
|
||||
|
||||
update_has_children(old_parent_id)
|
||||
update_has_children(parent_id)
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(m)
|
||||
m.extend(ClassMethods)
|
||||
m.after_save :update_parent
|
||||
m.after_save :update_pool_children
|
||||
m.validate :validate_parent
|
||||
m.after_delete :give_favorites_to_parent
|
||||
m.versioned :parent_id, :default => nil
|
||||
end
|
||||
|
||||
def validate_parent
|
||||
errors.add("parent_id") unless parent_id.nil? or Post.exists?(parent_id)
|
||||
end
|
||||
|
||||
def update_parent
|
||||
return if !parent_id_changed? && !status_changed?
|
||||
self.class.set_parent(id, parent_id, parent_id_was)
|
||||
end
|
||||
|
||||
def update_pool_children
|
||||
# If the parent didn't change, we don't need to update any pool posts. (Don't use
|
||||
# parent_id_changed?; we want to know if the id changed, not if it was just overwritten
|
||||
# with the same value.)
|
||||
return if self.parent_id == self.parent_id_was
|
||||
|
||||
# Give PoolPost a chance to update parenting when post parents change.
|
||||
PoolPost.post_parent_changed(self)
|
||||
end
|
||||
|
||||
def give_favorites_to_parent
|
||||
return if parent_id.nil?
|
||||
parent = Post.find(parent_id)
|
||||
|
||||
transaction do
|
||||
for vote in PostVotes.find(:all, :conditions => ["post_id = ?", self.id], :include => :user)
|
||||
parent.vote!(vote.score, vote.user, nil)
|
||||
self.vote!(0, vote.user, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
55
app/models/post/rating_methods.rb
Normal file
55
app/models/post/rating_methods.rb
Normal file
@ -0,0 +1,55 @@
|
||||
module PostRatingMethods
|
||||
attr_accessor :old_rating
|
||||
|
||||
def self.included(m)
|
||||
m.versioned :rating
|
||||
m.versioned :is_rating_locked, :default => false
|
||||
m.versioned :is_note_locked, :default => false
|
||||
end
|
||||
|
||||
def rating=(r)
|
||||
if r == nil && !new_record?
|
||||
return
|
||||
end
|
||||
|
||||
if is_rating_locked?
|
||||
return
|
||||
end
|
||||
|
||||
r = r.to_s.downcase[0, 1]
|
||||
|
||||
if %w(q e s).include?(r)
|
||||
new_rating = r
|
||||
else
|
||||
new_rating = 'q'
|
||||
end
|
||||
|
||||
return if rating == new_rating
|
||||
self.old_rating = rating
|
||||
write_attribute(:rating, new_rating)
|
||||
touch_change_seq!
|
||||
end
|
||||
|
||||
def pretty_rating
|
||||
case rating
|
||||
when "q"
|
||||
return "Questionable"
|
||||
|
||||
when "e"
|
||||
return "Explicit"
|
||||
|
||||
when "s"
|
||||
return "Safe"
|
||||
end
|
||||
end
|
||||
|
||||
def can_change_is_note_locked?(user)
|
||||
return user.has_permission?(pool)
|
||||
end
|
||||
def can_change_rating_locked?(user)
|
||||
return user.has_permission?(pool)
|
||||
end
|
||||
def can_change_rating?(user)
|
||||
return user.is_member_or_higher? && (!is_rating_locked? || user.has_permission?(self))
|
||||
end
|
||||
end
|
336
app/models/post/sql_methods.rb
Normal file
336
app/models/post/sql_methods.rb
Normal file
@ -0,0 +1,336 @@
|
||||
module PostSqlMethods
|
||||
module ClassMethods
|
||||
def generate_sql_range_helper(arr, field, c, p)
|
||||
case arr[0]
|
||||
when :eq
|
||||
c << "#{field} = ?"
|
||||
p << arr[1]
|
||||
|
||||
when :gt
|
||||
c << "#{field} > ?"
|
||||
p << arr[1]
|
||||
|
||||
when :gte
|
||||
c << "#{field} >= ?"
|
||||
p << arr[1]
|
||||
|
||||
when :lt
|
||||
c << "#{field} < ?"
|
||||
p << arr[1]
|
||||
|
||||
when :lte
|
||||
c << "#{field} <= ?"
|
||||
p << arr[1]
|
||||
|
||||
when :between
|
||||
c << "#{field} BETWEEN ? AND ?"
|
||||
p << arr[1]
|
||||
p << arr[2]
|
||||
|
||||
else
|
||||
# do nothing
|
||||
end
|
||||
end
|
||||
|
||||
def generate_sql(q, options = {})
|
||||
if q.is_a?(Hash)
|
||||
original_query = options[:original_query]
|
||||
else
|
||||
original_query = q
|
||||
q = Tag.parse_query(q)
|
||||
end
|
||||
|
||||
conds = ["true"]
|
||||
joins = ["posts p"]
|
||||
join_params = []
|
||||
cond_params = []
|
||||
|
||||
if q.has_key?(:error)
|
||||
conds << "FALSE"
|
||||
end
|
||||
|
||||
generate_sql_range_helper(q[:post_id], "p.id", conds, cond_params)
|
||||
generate_sql_range_helper(q[:mpixels], "p.width*p.height/1000000.0", conds, cond_params)
|
||||
generate_sql_range_helper(q[:width], "p.width", conds, cond_params)
|
||||
generate_sql_range_helper(q[:height], "p.height", conds, cond_params)
|
||||
generate_sql_range_helper(q[:score], "p.score", conds, cond_params)
|
||||
generate_sql_range_helper(q[:date], "p.created_at::date", conds, cond_params)
|
||||
generate_sql_range_helper(q[:change], "p.change_seq", conds, cond_params)
|
||||
|
||||
if q[:md5].is_a?(String)
|
||||
conds << "p.md5 IN (?)"
|
||||
cond_params << q[:md5].split(/,/)
|
||||
end
|
||||
|
||||
if q[:deleted_only] == true
|
||||
conds << "p.status = 'deleted'"
|
||||
else
|
||||
conds << "p.status <> 'deleted'"
|
||||
end
|
||||
|
||||
if q.has_key?(:parent_id) && q[:parent_id].is_a?(Integer)
|
||||
conds << "(p.parent_id = ? or p.id = ?)"
|
||||
cond_params << q[:parent_id]
|
||||
cond_params << q[:parent_id]
|
||||
elsif q.has_key?(:parent_id) && q[:parent_id] == false
|
||||
conds << "p.parent_id is null"
|
||||
end
|
||||
|
||||
if q[:source].is_a?(String)
|
||||
conds << "p.source LIKE ? ESCAPE E'\\\\'"
|
||||
cond_params << q[:source]
|
||||
end
|
||||
|
||||
if q[:favtag].is_a?(String)
|
||||
user = User.find_by_name(q[:favtag])
|
||||
|
||||
if user
|
||||
post_ids = FavoriteTag.find_post_ids(user.id)
|
||||
conds << "p.id IN (?)"
|
||||
cond_params << post_ids
|
||||
end
|
||||
end
|
||||
|
||||
if q[:fav].is_a?(String)
|
||||
joins << "JOIN favorites f ON f.post_id = p.id JOIN users fu ON f.user_id = fu.id"
|
||||
conds << "lower(fu.name) = lower(?)"
|
||||
cond_params << q[:fav]
|
||||
end
|
||||
|
||||
if q.has_key?(:vote_negated)
|
||||
joins << "LEFT JOIN post_votes v ON p.id = v.post_id AND v.user_id = ?"
|
||||
join_params << q[:vote_negated]
|
||||
conds << "v.score IS NULL"
|
||||
end
|
||||
|
||||
if q.has_key?(:vote)
|
||||
joins << "JOIN post_votes v ON p.id = v.post_id"
|
||||
conds << "v.user_id = ?"
|
||||
cond_params << q[:vote][1]
|
||||
|
||||
generate_sql_range_helper(q[:vote][0], "v.score", conds, cond_params)
|
||||
end
|
||||
|
||||
if q[:user].is_a?(String)
|
||||
joins << "JOIN users u ON p.user_id = u.id"
|
||||
conds << "lower(u.name) = lower(?)"
|
||||
cond_params << q[:user]
|
||||
end
|
||||
|
||||
if q.has_key?(:exclude_pools)
|
||||
q[:exclude_pools].each_index do |i|
|
||||
if q[:exclude_pools][i].is_a?(Integer)
|
||||
joins << "LEFT JOIN pools_posts ep#{i} ON (ep#{i}.post_id = p.id AND ep#{i}.pool_id = ?)"
|
||||
join_params << q[:exclude_pools][i]
|
||||
conds << "ep#{i} IS NULL"
|
||||
end
|
||||
|
||||
if q[:exclude_pools][i].is_a?(String)
|
||||
joins << "LEFT JOIN pools_posts ep#{i} ON ep#{i}.post_id = p.id LEFT JOIN pools epp#{i} ON (ep#{i}.pool_id = epp#{i}.id AND epp#{i}.name ILIKE ? ESCAPE E'\\\\')"
|
||||
join_params << ("%" + q[:exclude_pools][i].to_escaped_for_sql_like + "%")
|
||||
conds << "ep#{i} IS NULL"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if q.has_key?(:pool)
|
||||
if q.has_key?(:pool_posts) && q[:pool_posts] == "all"
|
||||
conds << "(pools_posts.active OR pools_posts.master_id IS NOT NULL)"
|
||||
elsif q.has_key?(:pool_posts) && q[:pool_posts] == "orig"
|
||||
conds << "pools_posts.active = true"
|
||||
else
|
||||
conds << "((pools_posts.active = true AND pools_posts.slave_id IS NULL) OR pools_posts.master_id IS NOT NULL)"
|
||||
end
|
||||
|
||||
if not q.has_key?(:order)
|
||||
pool_ordering = " ORDER BY pools_posts.pool_id ASC, nat_sort(pools_posts.sequence), pools_posts.post_id"
|
||||
end
|
||||
|
||||
if q[:pool].is_a?(Integer)
|
||||
joins << "JOIN pools_posts ON pools_posts.post_id = p.id JOIN pools ON pools_posts.pool_id = pools.id"
|
||||
conds << "pools.id = ?"
|
||||
cond_params << q[:pool]
|
||||
end
|
||||
|
||||
if q[:pool].is_a?(String)
|
||||
joins << "JOIN pools_posts ON pools_posts.post_id = p.id JOIN pools ON pools_posts.pool_id = pools.id"
|
||||
conds << "pools.name ILIKE ? ESCAPE E'\\\\'"
|
||||
cond_params << ("%" + q[:pool].to_escaped_for_sql_like + "%")
|
||||
end
|
||||
end
|
||||
|
||||
if q.has_key?(:include)
|
||||
joins << "JOIN posts_tags ipt ON ipt.post_id = p.id"
|
||||
conds << "ipt.tag_id IN (SELECT id FROM tags WHERE name IN (?))"
|
||||
cond_params << (q[:include] + q[:related])
|
||||
elsif q[:related].any?
|
||||
raise "You cannot search for more than #{CONFIG['tag_query_limit']} tags at a time" if q[:related].size > CONFIG["tag_query_limit"]
|
||||
|
||||
q[:related].each_with_index do |rtag, i|
|
||||
joins << "JOIN posts_tags rpt#{i} ON rpt#{i}.post_id = p.id AND rpt#{i}.tag_id = (SELECT id FROM tags WHERE name = ?)"
|
||||
join_params << rtag
|
||||
end
|
||||
end
|
||||
|
||||
if q[:exclude].any?
|
||||
raise "You cannot search for more than #{CONFIG['tag_query_limit']} tags at a time" if q[:exclude].size > CONFIG["tag_query_limit"]
|
||||
q[:exclude].each_with_index do |etag, i|
|
||||
joins << "LEFT JOIN posts_tags ept#{i} ON p.id = ept#{i}.post_id AND ept#{i}.tag_id = (SELECT id FROM tags WHERE name = ?)"
|
||||
conds << "ept#{i}.tag_id IS NULL"
|
||||
join_params << etag
|
||||
end
|
||||
end
|
||||
|
||||
if q[:rating].is_a?(String)
|
||||
case q[:rating][0, 1].downcase
|
||||
when "s"
|
||||
conds << "p.rating = 's'"
|
||||
|
||||
when "q"
|
||||
conds << "p.rating = 'q'"
|
||||
|
||||
when "e"
|
||||
conds << "p.rating = 'e'"
|
||||
end
|
||||
end
|
||||
|
||||
if q[:rating_negated].is_a?(String)
|
||||
case q[:rating_negated][0, 1].downcase
|
||||
when "s"
|
||||
conds << "p.rating <> 's'"
|
||||
|
||||
when "q"
|
||||
conds << "p.rating <> 'q'"
|
||||
|
||||
when "e"
|
||||
conds << "p.rating <> 'e'"
|
||||
end
|
||||
end
|
||||
|
||||
if q[:unlocked_rating] == true
|
||||
conds << "p.is_rating_locked = FALSE"
|
||||
end
|
||||
|
||||
if options[:pending]
|
||||
conds << "p.status = 'pending'"
|
||||
end
|
||||
|
||||
if options[:flagged]
|
||||
conds << "p.status = 'flagged'"
|
||||
end
|
||||
|
||||
if q.has_key?(:show_holds_only)
|
||||
if q[:show_holds_only]
|
||||
conds << "p.is_held"
|
||||
end
|
||||
else
|
||||
# Hide held posts by default only when not using the API.
|
||||
if not options[:from_api] then
|
||||
conds << "NOT p.is_held"
|
||||
end
|
||||
end
|
||||
|
||||
if q.has_key?(:shown_in_index)
|
||||
if q[:shown_in_index]
|
||||
conds << "p.is_shown_in_index"
|
||||
else
|
||||
conds << "NOT p.is_shown_in_index"
|
||||
end
|
||||
elsif original_query.blank? and not options[:from_api]
|
||||
# Hide not shown posts by default only when not using the API.
|
||||
conds << "p.is_shown_in_index"
|
||||
end
|
||||
|
||||
sql = "SELECT "
|
||||
|
||||
if options[:count]
|
||||
sql << "COUNT(*)"
|
||||
elsif options[:select]
|
||||
sql << options[:select]
|
||||
else
|
||||
sql << "p.*"
|
||||
end
|
||||
|
||||
sql << " FROM " + joins.join(" ")
|
||||
sql << " WHERE " + conds.join(" AND ")
|
||||
|
||||
if q[:order] && !options[:count]
|
||||
case q[:order]
|
||||
when "id"
|
||||
sql << " ORDER BY p.id"
|
||||
|
||||
when "id_desc"
|
||||
sql << " ORDER BY p.id DESC"
|
||||
|
||||
when "score"
|
||||
sql << " ORDER BY p.score DESC"
|
||||
|
||||
when "score_asc"
|
||||
sql << " ORDER BY p.score"
|
||||
|
||||
when "mpixels"
|
||||
# Use "w*h/1000000", even though "w*h" would give the same result, so this can use
|
||||
# the posts_mpixels index.
|
||||
sql << " ORDER BY width*height/1000000.0 DESC"
|
||||
|
||||
when "mpixels_asc"
|
||||
sql << " ORDER BY width*height/1000000.0"
|
||||
|
||||
when "portrait"
|
||||
sql << " ORDER BY 1.0*width/GREATEST(1, height)"
|
||||
|
||||
when "landscape"
|
||||
sql << " ORDER BY 1.0*width/GREATEST(1, height) DESC"
|
||||
|
||||
when "change", "change_asc"
|
||||
sql << " ORDER BY change_seq"
|
||||
|
||||
when "change_desc"
|
||||
sql << " ORDER BY change_seq DESC"
|
||||
|
||||
when "vote"
|
||||
if q.has_key?(:vote)
|
||||
sql << " ORDER BY v.updated_at DESC"
|
||||
end
|
||||
|
||||
when "fav"
|
||||
if q[:fav].is_a?(String)
|
||||
sql << " ORDER BY f.id DESC"
|
||||
end
|
||||
|
||||
when "random"
|
||||
sql << " ORDER BY random"
|
||||
|
||||
else
|
||||
if pool_ordering
|
||||
sql << pool_ordering
|
||||
else
|
||||
if options[:from_api] then
|
||||
# When using the API, default to sorting by ID.
|
||||
sql << " ORDER BY p.id DESC"
|
||||
else
|
||||
sql << " ORDER BY p.index_timestamp DESC"
|
||||
end
|
||||
end
|
||||
end
|
||||
elsif options[:order]
|
||||
sql << " ORDER BY " + options[:order]
|
||||
end
|
||||
|
||||
if options[:limit]
|
||||
sql << " LIMIT " + options[:limit].to_s
|
||||
end
|
||||
|
||||
if options[:offset]
|
||||
sql << " OFFSET " + options[:offset].to_s
|
||||
end
|
||||
|
||||
params = join_params + cond_params
|
||||
return Post.sanitize_sql([sql, *params])
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(m)
|
||||
m.extend(ClassMethods)
|
||||
end
|
||||
end
|
115
app/models/post/status_methods.rb
Normal file
115
app/models/post/status_methods.rb
Normal file
@ -0,0 +1,115 @@
|
||||
module PostStatusMethods
|
||||
def status=(s)
|
||||
return if s == status
|
||||
write_attribute(:status, s)
|
||||
touch_change_seq!
|
||||
end
|
||||
|
||||
def reset_index_timestamp
|
||||
self.index_timestamp = self.created_at
|
||||
end
|
||||
|
||||
# Bump the post to the front of the index.
|
||||
def touch_index_timestamp
|
||||
self.index_timestamp = Time.now
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# If user_id is nil, allow activating any user's posts.
|
||||
def batch_activate(user_id, post_ids)
|
||||
conds = []
|
||||
cond_params = []
|
||||
|
||||
conds << "is_held = true"
|
||||
conds << "id IN (?)"
|
||||
cond_params << post_ids
|
||||
|
||||
if user_id
|
||||
conds << "user_id = ?"
|
||||
cond_params << user_id
|
||||
end
|
||||
|
||||
# Tricky: we want posts to show up in the index in the same order they were posted.
|
||||
# If we just bump the posts, the index_timestamps will all be the same, and they'll
|
||||
# show up in an undefined order. We don't want to do this in the ORDER BY when
|
||||
# searching, because that's too expensive. Instead, tweak the timestamps slightly:
|
||||
# for each post updated, set the index_timestamps 1ms newer than the previous.
|
||||
#
|
||||
# Returns the number of posts actually activated.
|
||||
count = nil
|
||||
transaction do
|
||||
# result_id gives us an index for each result row; multiplying this by 1ms
|
||||
# gives us an increasing counter. This should be a lot easier than this.
|
||||
sql = <<-EOS
|
||||
CREATE TEMP SEQUENCE result_id;
|
||||
|
||||
UPDATE posts
|
||||
SET index_timestamp = now() + (interval '1 ms' * idx)
|
||||
FROM
|
||||
(SELECT nextval('result_id') AS idx, * FROM (
|
||||
SELECT id, index_timestamp FROM posts
|
||||
WHERE #{conds.join(" AND ")}
|
||||
ORDER BY created_at DESC
|
||||
) AS n) AS nn
|
||||
WHERE posts.id IN (nn.id);
|
||||
|
||||
DROP SEQUENCE result_id;
|
||||
EOS
|
||||
execute_sql(sql, *cond_params)
|
||||
|
||||
count = select_value_sql("SELECT COUNT(*) FROM posts WHERE #{conds.join(" AND ")}", *cond_params).to_i
|
||||
|
||||
sql = "UPDATE posts SET is_held = false WHERE #{conds.join(" AND ")}"
|
||||
execute_sql(sql, *cond_params)
|
||||
end
|
||||
|
||||
Cache.expire if count > 0
|
||||
|
||||
return count
|
||||
end
|
||||
end
|
||||
|
||||
def update_status_on_destroy
|
||||
# Can't use update_attributes here since this method is wrapped inside of a destroy call
|
||||
execute_sql("UPDATE posts SET status = ? WHERE id = ?", "deleted", id)
|
||||
Post.update_has_children(parent_id) if parent_id
|
||||
flag_detail.update_attributes(:is_resolved => true) if flag_detail
|
||||
return false
|
||||
end
|
||||
|
||||
def self.included(m)
|
||||
m.extend(ClassMethods)
|
||||
m.before_create :reset_index_timestamp
|
||||
m.versioned :is_shown_in_index, :default => true
|
||||
end
|
||||
|
||||
def is_held=(hold)
|
||||
# Hack because the data comes in as a string:
|
||||
hold = false if hold == "false"
|
||||
|
||||
user = Thread.current["danbooru-user"]
|
||||
|
||||
# Only the original poster can hold or unhold a post.
|
||||
return if user && !user.has_permission?(self)
|
||||
|
||||
if hold
|
||||
# A post can only be held within one minute of posting (except by a moderator);
|
||||
# this is intended to be used on initial posting, before it shows up in the index.
|
||||
return if self.created_at && self.created_at < 1.minute.ago
|
||||
end
|
||||
|
||||
was_held = self.is_held
|
||||
|
||||
write_attribute(:is_held, hold)
|
||||
|
||||
# When a post is unheld, bump it.
|
||||
if was_held && !hold
|
||||
touch_index_timestamp
|
||||
end
|
||||
end
|
||||
|
||||
def undelete!
|
||||
execute_sql("UPDATE posts SET status = ? WHERE id = ?", "active", id)
|
||||
Post.update_has_children(parent_id) if parent_id
|
||||
end
|
||||
end
|
62
app/models/post/t
Normal file
62
app/models/post/t
Normal file
@ -0,0 +1,62 @@
|
||||
Index: image_store/remote_hierarchy.rb
|
||||
===================================================================
|
||||
--- image_store/remote_hierarchy.rb (revision 755)
|
||||
+++ image_store/remote_hierarchy.rb (working copy)
|
||||
@@ -8,9 +8,14 @@
|
||||
end
|
||||
|
||||
def select_random_image_server
|
||||
+ if not self.is_warehoused?
|
||||
+ # return CONFIG["url_base"]
|
||||
+ return CONFIG["image_servers"][0]
|
||||
+ end
|
||||
+
|
||||
# age = Time.now - self.created_at
|
||||
i = 0
|
||||
-# if age > (60*60*3) then
|
||||
+# if age > (60*60*24) then
|
||||
# i = 2 # Ascaroth
|
||||
# elsif age > (60*60*24)*2 then
|
||||
# i = 1 # saki
|
||||
@@ -28,17 +33,9 @@
|
||||
|
||||
def file_url
|
||||
if CONFIG["use_pretty_image_urls"] then
|
||||
- if self.is_warehoused?
|
||||
- select_random_image_server() + "/image/#{md5}/#{url_encode(pretty_file_name)}.#{file_ext}"
|
||||
- else
|
||||
- CONFIG["url_base"] + "/image/#{md5}/#{url_encode(pretty_file_name)}.#{file_ext}"
|
||||
- end
|
||||
+ select_random_image_server + "/image/#{md5}/#{url_encode(pretty_file_name)}.#{file_ext}"
|
||||
else
|
||||
- if self.is_warehoused?
|
||||
- select_random_image_server() + "/data/#{file_hierarchy}/#{file_name}"
|
||||
- else
|
||||
- CONFIG["url_base"] + "/data/#{file_hierarchy}/#{file_name}"
|
||||
- end
|
||||
+ select_random_image_server + "/data/#{file_hierarchy}/#{file_name}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -68,7 +65,7 @@
|
||||
if status == "deleted"
|
||||
CONFIG["url_base"] + "/deleted-preview.png"
|
||||
elsif image?
|
||||
- CONFIG["url_base"] + "/data/preview/#{file_hierarchy}/#{md5}.jpg"
|
||||
+ CONFIG["image_servers"][0] + "/data/preview/#{file_hierarchy}/#{md5}.jpg"
|
||||
else
|
||||
CONFIG["url_base"] + "/download-preview.png"
|
||||
end
|
||||
@@ -82,11 +79,7 @@
|
||||
path = "/data/sample/#{file_hierarchy}/" + CONFIG["sample_filename_prefix"] + "#{md5}.jpg"
|
||||
end
|
||||
|
||||
- if self.is_warehoused?
|
||||
- return select_random_image_server() + path
|
||||
- else
|
||||
- return CONFIG["url_base"] + path
|
||||
- end
|
||||
+ return select_random_image_server + path
|
||||
end
|
||||
|
||||
def delete_file
|
246
app/models/post/tag_methods.rb
Normal file
246
app/models/post/tag_methods.rb
Normal file
@ -0,0 +1,246 @@
|
||||
module PostTagMethods
|
||||
attr_accessor :tags, :new_tags, :old_tags, :old_cached_tags
|
||||
|
||||
module ClassMethods
|
||||
def find_by_tags(tags, options = {})
|
||||
return find_by_sql(Post.generate_sql(tags, options))
|
||||
end
|
||||
|
||||
def recalculate_cached_tags(id = nil)
|
||||
conds = []
|
||||
cond_params = []
|
||||
|
||||
sql = %{
|
||||
UPDATE posts p SET cached_tags = (
|
||||
SELECT array_to_string(coalesce(array(
|
||||
SELECT t.name
|
||||
FROM tags t, posts_tags pt
|
||||
WHERE t.id = pt.tag_id AND pt.post_id = p.id
|
||||
ORDER BY t.name
|
||||
), '{}'::text[]), ' ')
|
||||
)
|
||||
}
|
||||
|
||||
if id
|
||||
conds << "WHERE p.id = ?"
|
||||
cond_params << id
|
||||
end
|
||||
|
||||
sql = [sql, conds].join(" ")
|
||||
execute_sql sql, *cond_params
|
||||
end
|
||||
|
||||
# new, previous and latest are History objects for cached_tags. Split
|
||||
# the tag changes apart.
|
||||
def tag_changes(new, previous, latest)
|
||||
new_tags = new.value.scan(/\S+/)
|
||||
old_tags = (previous.value rescue "").scan(/\S+/)
|
||||
latest_tags = latest.value.scan(/\S+/)
|
||||
|
||||
{
|
||||
:added_tags => new_tags - old_tags,
|
||||
:removed_tags => old_tags - new_tags,
|
||||
:unchanged_tags => new_tags & old_tags,
|
||||
:obsolete_added_tags => (new_tags - old_tags) - latest_tags,
|
||||
:obsolete_removed_tags => (old_tags - new_tags) & latest_tags,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(m)
|
||||
m.extend ClassMethods
|
||||
m.before_save :commit_metatags
|
||||
m.after_save :commit_tags
|
||||
m.after_save :save_post_history
|
||||
m.has_many :tag_history, :class_name => "PostTagHistory", :table_name => "post_tag_histories", :order => "id desc"
|
||||
m.versioned :source, :default => ""
|
||||
m.versioned :cached_tags
|
||||
end
|
||||
|
||||
def cached_tags_undo(change, redo_changes=false)
|
||||
current_tags = self.cached_tags.scan(/\S+/)
|
||||
prev = change.previous
|
||||
|
||||
change, prev = prev, change if redo_changes
|
||||
changes = Post.tag_changes(change, prev, change.latest)
|
||||
new_tags = (current_tags - changes[:added_tags]) | changes[:removed_tags]
|
||||
self.attributes = {:tags => new_tags.join(" ")}
|
||||
end
|
||||
|
||||
def cached_tags_redo(change)
|
||||
cached_tags_undo(change, true)
|
||||
end
|
||||
|
||||
# === Parameters
|
||||
# * :tag<String>:: the tag to search for
|
||||
def has_tag?(tag)
|
||||
return cached_tags =~ /(^|\s)#{tag}($|\s)/
|
||||
end
|
||||
|
||||
# Returns the tags in a URL suitable string
|
||||
def tag_title
|
||||
return title_tags.gsub(/\W+/, "-")[0, 50]
|
||||
end
|
||||
|
||||
# Return the tags we display in URLs, page titles, etc.
|
||||
def title_tags
|
||||
ret = ""
|
||||
ret << "hentai " if self.rating == "e"
|
||||
ret << cached_tags
|
||||
ret
|
||||
end
|
||||
|
||||
def tags
|
||||
cached_tags
|
||||
end
|
||||
|
||||
# Sets the tags for the post. Does not actually save anything to the database when called.
|
||||
#
|
||||
# === Parameters
|
||||
# * :tags<String>:: a whitespace delimited list of tags
|
||||
def tags=(tags)
|
||||
self.new_tags = Tag.scan_tags(tags)
|
||||
|
||||
current_tags = cached_tags.scan(/\S+/)
|
||||
self.touch_change_seq! if new_tags != current_tags
|
||||
end
|
||||
|
||||
# Returns all versioned tags and metatags.
|
||||
def cached_tags_versioned
|
||||
["rating:" + self.rating, cached_tags].map.join(" ")
|
||||
end
|
||||
|
||||
# Commit metatags; this is done before save, so any changes are stored normally.
|
||||
def commit_metatags
|
||||
return if new_tags.nil?
|
||||
|
||||
transaction do
|
||||
metatags, self.new_tags = new_tags.partition {|x| x=~ /^(hold|unhold|show|hide|\+flag)$/}
|
||||
metatags.each do |metatag|
|
||||
case metatag
|
||||
when /^hold$/
|
||||
self.is_held = true
|
||||
|
||||
when /^unhold$/
|
||||
self.is_held = false
|
||||
|
||||
when /^show$/
|
||||
self.is_shown_in_index = true
|
||||
|
||||
when /^hide$/
|
||||
self.is_shown_in_index = false
|
||||
|
||||
when /^\+flag$/
|
||||
# Permissions for this are checked on commit.
|
||||
self.metatag_flagged = "moderator flagged"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Commit any tag changes to the database. This is done after save, so any changes
|
||||
# must be made directly to the database.
|
||||
def commit_tags
|
||||
return if new_tags.nil?
|
||||
|
||||
if old_tags
|
||||
# If someone else committed changes to this post before we did,
|
||||
# then try to merge the tag changes together.
|
||||
current_tags = cached_tags.scan(/\S+/)
|
||||
self.old_tags = Tag.scan_tags(old_tags)
|
||||
self.new_tags = (current_tags + new_tags) - old_tags + (current_tags & new_tags)
|
||||
end
|
||||
|
||||
metatags, self.new_tags = new_tags.partition {|x| x=~ /^(?:-pool|pool|rating|parent):/}
|
||||
|
||||
transaction do
|
||||
metatags.each do |metatag|
|
||||
case metatag
|
||||
when /^pool:(.+)/
|
||||
begin
|
||||
name, seq = $1.split(":")
|
||||
|
||||
pool = Pool.find_by_name(name)
|
||||
options = {:user => User.find(updater_user_id)}
|
||||
if defined?(seq) then
|
||||
options[:sequence] = seq
|
||||
end
|
||||
|
||||
if pool.nil? and name !~ /^\d+$/
|
||||
pool = Pool.create(:name => name, :is_public => false, :user_id => updater_user_id)
|
||||
end
|
||||
|
||||
next if Thread.current["danbooru-user"] && !pool.can_change?(Thread.current["danbooru-user"], nil)
|
||||
pool.add_post(id, options) if pool
|
||||
rescue Pool::PostAlreadyExistsError
|
||||
rescue Pool::AccessDeniedError
|
||||
end
|
||||
|
||||
|
||||
when /^-pool:(.+)/
|
||||
name = $1
|
||||
pool = Pool.find_by_name(name)
|
||||
next if Thread.current["danbooru-user"] && !pool.can_change?(Thread.current["danbooru-user"], nil)
|
||||
|
||||
pool.remove_post(id) if pool
|
||||
|
||||
when /^rating:([qse])/
|
||||
self.rating = $1 # so we don't have to reload for history_tag_string below
|
||||
execute_sql("UPDATE posts SET rating = ? WHERE id = ?", $1, id)
|
||||
|
||||
|
||||
when /^parent:(\d*)/
|
||||
self.parent_id = $1
|
||||
|
||||
if CONFIG["enable_parent_posts"] && (Post.exists?(parent_id) or parent_id == 0)
|
||||
Post.set_parent(id, parent_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.new_tags << "tagme" if new_tags.empty?
|
||||
self.new_tags = TagAlias.to_aliased(new_tags)
|
||||
self.new_tags = TagImplication.with_implied(new_tags).uniq
|
||||
|
||||
# TODO: be more selective in deleting from the join table
|
||||
execute_sql("DELETE FROM posts_tags WHERE post_id = ?", id)
|
||||
self.new_tags = new_tags.map {|x| Tag.find_or_create_by_name(x)}.uniq
|
||||
|
||||
# Tricky: Postgresql's locking won't serialize this DELETE/INSERT, so it's
|
||||
# possible for two simultaneous updates to both delete all tags, then insert
|
||||
# them, duplicating them all.
|
||||
#
|
||||
# Work around this by selecting the existing tags within the INSERT and removing
|
||||
# any that already exist. Normally, the inner SELECT will return no rows; if
|
||||
# another process inserts rows before our INSERT, it'll return the rows that it
|
||||
# inserted and we'll avoid duplicating them.
|
||||
tag_set = new_tags.map {|x| ("(#{id}, #{x.id})")}.join(", ")
|
||||
#execute_sql("INSERT INTO posts_tags (post_id, tag_id) VALUES " + tag_set)
|
||||
sql = <<-EOS
|
||||
INSERT INTO posts_tags (post_id, tag_id)
|
||||
SELECT t.post_id, t.tag_id
|
||||
FROM (VALUES #{tag_set}) AS t(post_id, tag_id)
|
||||
WHERE t.tag_id NOT IN (SELECT tag_id FROM posts_tags pt WHERE pt.post_id = #{self.id})
|
||||
EOS
|
||||
|
||||
execute_sql(sql)
|
||||
|
||||
Post.recalculate_cached_tags(self.id)
|
||||
|
||||
# Store the old cached_tags, so we can expire them.
|
||||
self.old_cached_tags = self.cached_tags
|
||||
self.cached_tags = select_value_sql("SELECT cached_tags FROM posts WHERE id = #{id}")
|
||||
|
||||
self.new_tags = nil
|
||||
end
|
||||
end
|
||||
|
||||
def save_post_history
|
||||
new_cached_tags = cached_tags_versioned
|
||||
if tag_history.empty? or tag_history.first.tags != new_cached_tags
|
||||
PostTagHistory.create(:post_id => id, :tags => new_cached_tags,
|
||||
:user_id => Thread.current["danbooru-user_id"],
|
||||
:ip_addr => Thread.current["danbooru-ip_addr"] || "127.0.0.1")
|
||||
end
|
||||
end
|
||||
end
|
71
app/models/post/vote_methods.rb
Normal file
71
app/models/post/vote_methods.rb
Normal file
@ -0,0 +1,71 @@
|
||||
module PostVoteMethods
|
||||
module ClassMethods
|
||||
def recalculate_score(id=nil)
|
||||
conds = []
|
||||
cond_params = []
|
||||
|
||||
sql = "UPDATE posts AS p SET score = " +
|
||||
"(SELECT COALESCE(SUM(GREATEST(?, LEAST(?, score))), 0) FROM post_votes v WHERE v.post_id = p.id) " +
|
||||
"+ p.anonymous_votes"
|
||||
cond_params << CONFIG["vote_sum_min"]
|
||||
cond_params << CONFIG["vote_sum_max"]
|
||||
|
||||
if id
|
||||
conds << "WHERE p.id = ?"
|
||||
cond_params << id
|
||||
end
|
||||
|
||||
sql = [sql, conds].join(" ")
|
||||
execute_sql sql, *cond_params
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(m)
|
||||
m.extend(ClassMethods)
|
||||
end
|
||||
|
||||
def recalculate_score!()
|
||||
save!
|
||||
Post.recalculate_score(self.id)
|
||||
connection.clear_query_cache
|
||||
reload
|
||||
end
|
||||
|
||||
def vote!(score, user, ip_addr, options={})
|
||||
score = CONFIG["vote_record_min"] if score < CONFIG["vote_record_min"]
|
||||
score = CONFIG["vote_record_max"] if score > CONFIG["vote_record_max"]
|
||||
|
||||
if user.is_anonymous?
|
||||
score = 0 if score < 0
|
||||
score = 1 if score > 1
|
||||
|
||||
if last_voter_ip == ip_addr
|
||||
return false
|
||||
end
|
||||
|
||||
self.anonymous_votes += score
|
||||
self.last_voter_ip = ip_addr
|
||||
self.last_vote = score
|
||||
else
|
||||
vote = PostVotes.find_by_ids(user.id, self.id)
|
||||
|
||||
if ip_addr and last_voter_ip == ip_addr and not vote
|
||||
# The user voted anonymously, then logged in and tried to vote again. A user
|
||||
# may be browsing anonymously, decide to make an account, then once he has access
|
||||
# to full voting, decide to set his permanent vote. Just undo the anonymous vote.
|
||||
self.anonymous_votes -= self.last_vote
|
||||
self.last_vote = 0
|
||||
end
|
||||
|
||||
if not vote
|
||||
vote = PostVotes.find_or_create_by_id(user.id, self.id)
|
||||
end
|
||||
|
||||
vote.update_attributes(:score => score, :updated_at => Time.now)
|
||||
end
|
||||
|
||||
recalculate_score!
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
102
app/models/post_tag_history.rb
Normal file
102
app/models/post_tag_history.rb
Normal file
@ -0,0 +1,102 @@
|
||||
class PostTagHistory < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :post
|
||||
|
||||
def self.undo_user_changes(user_id)
|
||||
posts = Post.find(:all, :joins => "join post_tag_histories pth on pth.post_id = posts.id", :select => "distinct posts.id", :conditions => ["pth.user_id = ?", user_id])
|
||||
puts posts.size
|
||||
p posts.map {|x| x.id}
|
||||
# destroy_all(["user_id = ?", user_id])
|
||||
# posts.each do |post|
|
||||
# post.tags = post.tag_history.first.tags
|
||||
# post.updater_user_id = post.tag_history.first.user_id
|
||||
# post.updater_ip_addr = post.tag_history.first.ip_addr
|
||||
# post.save!
|
||||
# end
|
||||
end
|
||||
|
||||
def self.generate_sql(options = {})
|
||||
Nagato::Builder.new do |builder, cond|
|
||||
cond.add_unless_blank "post_tag_histories.post_id = ?", options[:post_id]
|
||||
cond.add_unless_blank "post_tag_histories.user_id = ?", options[:user_id]
|
||||
|
||||
if options[:user_name]
|
||||
builder.join "users ON users.id = post_tag_histories.user_id"
|
||||
cond.add "users.name = ?", options[:user_name]
|
||||
end
|
||||
end.to_hash
|
||||
end
|
||||
|
||||
def self.undo_changes_by_user(user_id)
|
||||
transaction do
|
||||
posts = Post.find(:all, :joins => "join post_tag_histories pth on pth.post_id = posts.id", :select => "distinct posts.*", :conditions => ["pth.user_id = ?", user_id])
|
||||
|
||||
PostTagHistory.destroy_all(["user_id = ?", user_id])
|
||||
posts.each do |post|
|
||||
first = post.tag_history.first
|
||||
if first
|
||||
post.tags = first.tags
|
||||
post.updater_ip_addr = first.ip_addr
|
||||
post.updater_user_id = first.user_id
|
||||
post.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The contents of options[:posts] must be saved by the caller. This allows
|
||||
# undoing many tag changes across many posts; all †changes to a particular
|
||||
# post will be condensed into one change.
|
||||
def undo(options={})
|
||||
# TODO: refactor. modifying parameters is a bad habit.
|
||||
options[:posts] ||= {}
|
||||
options[:posts][post_id] ||= options[:post] = Post.find(post_id)
|
||||
post = options[:posts][post_id]
|
||||
|
||||
current_tags = post.cached_tags.scan(/\S+/)
|
||||
|
||||
prev = previous
|
||||
return if not prev
|
||||
|
||||
changes = tag_changes(prev)
|
||||
|
||||
new_tags = (current_tags - changes[:added_tags]) | changes[:removed_tags]
|
||||
options[:update_options] ||= {}
|
||||
post.attributes = {:tags => new_tags.join(" ")}.merge(options[:update_options])
|
||||
end
|
||||
|
||||
def author
|
||||
User.find_name(user_id)
|
||||
end
|
||||
|
||||
def tag_changes(prev)
|
||||
new_tags = tags.scan(/\S+/)
|
||||
old_tags = (prev.tags rescue "").scan(/\S+/)
|
||||
latest = Post.find(post_id).cached_tags_versioned
|
||||
latest_tags = latest.scan(/\S+/)
|
||||
|
||||
{
|
||||
:added_tags => new_tags - old_tags,
|
||||
:removed_tags => old_tags - new_tags,
|
||||
:unchanged_tags => new_tags & old_tags,
|
||||
:obsolete_added_tags => (new_tags - old_tags) - latest_tags,
|
||||
:obsolete_removed_tags => (old_tags - new_tags) & latest_tags,
|
||||
}
|
||||
end
|
||||
|
||||
def next
|
||||
return PostTagHistory.find(:first, :order => "id ASC", :conditions => ["post_id = ? AND id > ?", post_id, id])
|
||||
end
|
||||
|
||||
def previous
|
||||
PostTagHistory.find(:first, :order => "id DESC", :conditions => ["post_id = ? AND id < ?", post_id, id])
|
||||
end
|
||||
|
||||
def to_xml(options = {})
|
||||
{:id => id, :post_id => post_id, :tags => tags}.to_xml(options.merge(:root => "tag_history"))
|
||||
end
|
||||
|
||||
def to_json(*args)
|
||||
{:id => id, :post_id => post_id, :tags => tags}.to_json(*args)
|
||||
end
|
||||
end
|
18
app/models/post_votes.rb
Normal file
18
app/models/post_votes.rb
Normal file
@ -0,0 +1,18 @@
|
||||
class PostVotes < ActiveRecord::Base
|
||||
belongs_to :post, :class_name => "Post", :foreign_key => :post_id
|
||||
belongs_to :user, :class_name => "User", :foreign_key => :user_id
|
||||
|
||||
def self.find_by_ids(user_id, post_id)
|
||||
self.find(:first, :conditions => ["user_id = ? AND post_id = ?", user_id, post_id])
|
||||
end
|
||||
|
||||
def self.find_or_create_by_id(user_id, post_id)
|
||||
entry = self.find_by_ids(user_id, post_id)
|
||||
|
||||
if entry
|
||||
return entry
|
||||
else
|
||||
return create(:user_id => user_id, :post_id => post_id)
|
||||
end
|
||||
end
|
||||
end
|
10
app/models/report_mailer.rb
Normal file
10
app/models/report_mailer.rb
Normal file
@ -0,0 +1,10 @@
|
||||
class ReportMailer < ActionMailer::Base
|
||||
default_url_options["host"] = CONFIG["server_host"]
|
||||
|
||||
def moderator_report(email)
|
||||
recipients email
|
||||
from CONFIG["email_from"]
|
||||
subject "#{CONFIG['app_name']} - Moderator Report"
|
||||
content_type "text/html"
|
||||
end
|
||||
end
|
9
app/models/server_key.rb
Normal file
9
app/models/server_key.rb
Normal file
@ -0,0 +1,9 @@
|
||||
class ServerKey < ActiveRecord::Base
|
||||
def self.[](key)
|
||||
begin
|
||||
ActiveRecord::Base.connection.select_value("SELECT value FROM server_keys WHERE name = '#{key}'")
|
||||
rescue Exception
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user