require 'uri' require 'net/http' class Article < Content include TypoGuid content_fields :body, :extended has_many :pings, :dependent => :destroy, :order => "created_at ASC" has_many :comments, :dependent => :destroy, :order => "created_at ASC" with_options(:conditions => { :published => true }, :order => 'created_at DESC') do |this| this.has_many :published_comments, :class_name => "Comment", :order => "created_at ASC" this.has_many :published_trackbacks, :class_name => "Trackback", :order => "created_at ASC" this.has_many :published_feedback, :class_name => "Feedback", :order => "created_at ASC" end has_many :trackbacks, :dependent => :destroy, :order => "created_at ASC" has_many :feedback, :order => "created_at DESC" has_many :resources, :order => "created_at DESC", :class_name => "Resource", :foreign_key => 'article_id' after_destroy :fix_resources has_many :categorizations has_many :categories, \ :through => :categorizations, \ :uniq => true, \ :order => 'categorizations.is_primary DESC, categories.position' has_and_belongs_to_many :tags, :foreign_key => 'article_id' belongs_to :user has_many :triggers, :as => :pending_item after_save :post_trigger attr_accessor :draft has_state(:state, :valid_states => [:new, :draft, :publication_pending, :just_published, :published, :just_withdrawn, :withdrawn], :initial_state => :new, :handles => [:withdraw, :post_trigger, :after_save, :send_pings, :send_notifications, :published_at=, :just_published?]) include Article::States class << self def published_articles find(:conditions => { :published => true }, :order => 'published_at DESC') end def count_published_articles count(:conditions => { :published => true }) end end accents = { ['á','à','â','ä','ã','Ã','Ä','Â','À'] => 'a', ['é','è','ê','ë','Ë','É','È','Ê'] => 'e', ['í','ì','î','ï','I','Î','Ì'] => 'i', ['ó','ò','ô','ö','õ','Õ','Ö','Ô','Ò'] => 'o', ['œ'] => 'oe', ['ß'] => 'ss', ['ú','ù','û','ü','U','Û','Ù'] => 'u', ['ç','Ç'] => 'c' } FROM, TO = accents.inject(['','']) { |o,(k,v)| o[0] << k * ''; o[1] << v * k.size o } def stripped_title self.title.tr(FROM, TO).gsub(/<[^>]*>/, '').to_url end def permalink_url_options(nesting = false) {:year => published_at.year, :month => sprintf("%.2d", published_at.month), :day => sprintf("%.2d", published_at.day), :controller => 'articles', :action => 'show', (nesting ? :article_id : :id) => permalink} end def permalink_url(anchor=nil, only_path=true) @cached_permalink_url ||= {} @cached_permalink_url["#{anchor}#{only_path}"] ||= \ blog.with_options(permalink_url_options) do |b| b.url_for(:anchor => anchor, :only_path => only_path) end end def param_array @param_array ||= returning([published_at.year, sprintf('%.2d', published_at.month), sprintf('%.2d', published_at.day), permalink]) \ do |params| this = self k = class << params; self; end k.send(:define_method, :to_s) { params[-1] } end end def to_param param_array end def trackback_url blog.url_for(permalink_url_options(true).merge(:controller => 'trackbacks', :action => 'index')) end def feed_url(format = :rss20) blog.url_for(:controller => 'xml', :action => 'feed', :type => 'article', :format => format, :id => id) end def edit_url blog.url_for(:controller => "/admin/content", :action =>"edit", :id => id) end def delete_url blog.url_for(:controller => "/admin/content", :action =>"destroy", :id => id) end def html_urls urls = Array.new html.gsub(/]*>/) do |tag| if(tag =~ /href="([^"]+)"/) urls.push($1) end end urls.uniq end def really_send_pings(serverurl = blog.base_url, articleurl = nil) return unless blog.send_outbound_pings articleurl ||= permalink_url(nil, false) weblogupdatesping_urls = blog.ping_urls.gsub(/ +/,'').split(/[\n\r]+/) pingback_or_trackback_urls = self.html_urls ping_urls = weblogupdatesping_urls + pingback_or_trackback_urls ping_urls.uniq.each do |url| begin unless pings.collect { |p| p.url }.include?(url.strip) ping = pings.build("url" => url) if weblogupdatesping_urls.include?(url) ping.send_weblogupdatesping(serverurl, articleurl) elsif pingback_or_trackback_urls.include?(url) ping.send_pingback_or_trackback(articleurl) end end rescue Exception => e logger.error(e) # in case the remote server doesn't respond or gives an error, # we should throw an xmlrpc error here. end end end def next self.class.find(:first, :conditions => ['published_at > ?', published_at], :order => 'published_at asc') end def previous self.class.find(:first, :conditions => ['published_at < ?', published_at], :order => 'published_at desc') end # Count articles on a certain date def self.count_by_date(year, month = nil, day = nil, limit = nil) if !year.blank? count(:conditions => { :published_at => time_delta(year, month, day), :published => true }) else count(:conditions => { :published => true }) end end # Find all articles on a certain date def self.find_all_by_date(year, month = nil, day = nil) if !year.blank? find_published(:all, :conditions => { :published_at => time_delta(year,month,day) }) else find_published(:all) end end # Find one article on a certain date def self.find_by_date(year, month, day) find_all_by_date(year, month, day).first end def self.find_by_published_at super(:published_at) end def self.date_from(params_hash) params_hash[:article_year] \ ? params_hash.values_at(:article_year, :article_month, :article_day, :article_id) \ : params_hash.values_at(:year, :month, :day, :id) end # Finds one article which was posted on a certain date and matches the supplied dashed-title def self.find_by_permalink(year, month=nil, day=nil, title=nil) unless month case year when Hash year, month, day, title = date_from(year) when Array year, month, day, title = year end end date_range = self.time_delta(year, month, day) find_published(:first, :conditions => { :permalink => title, :published_at => date_range }) \ or raise ActiveRecord::RecordNotFound end def self.find_by_params_hash(params = {}) params[:id] ||= params[:article_id] if params[:id] find_by_permalink(params) else find_by_date(*date_from(params)) end end # Fulltext searches the body of published articles def self.search(query) if !query.to_s.strip.empty? tokens = query.split.collect {|c| "%#{c.downcase}%"} find_published(:all, :conditions => [(["(LOWER(body) LIKE ? OR LOWER(extended) LIKE ? OR LOWER(title) LIKE ?)"] * tokens.size).join(" AND "), *tokens.collect { |token| [token] * 3 }.flatten]) else [] end end def keywords_to_tags Article.transaction do tags.clear keywords.to_s.scan(/((['"]).*?\2|[\.\w]+)/).collect do |x| x.first.tr("\"'", '') end.uniq.each do |tagword| tags << Tag.get(tagword) end end end def interested_users User.find_boolean(:all, :notify_on_new_articles) end def notify_user_via_email(user) if user.notify_via_email? EmailNotify.send_article(self, user) end end def comments_closed? !(allow_comments? && in_feedback_window?) end def in_feedback_window? self.blog.sp_article_auto_close.zero? || self.created_at.to_i > self.blog.sp_article_auto_close.days.ago.to_i end def cast_to_boolean(value) ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value) end # Cast the input value for published= before passing it to the state. def published=(newval) state.published = cast_to_boolean(newval) end # Bloody rails reloading. Nasty workaround. def allow_comments=(newval) returning(cast_to_boolean(newval)) do |val| if self[:allow_comments] != val changed if published? self[:allow_comments] = val end end end def allow_pings=(newval) returning(cast_to_boolean(newval)) do |val| if self[:allow_pings] != val changed if published? self[:allow_pings] = val end end end def body=(newval) if self[:body] != newval changed if published? self[:body] = newval end self[:body] end def body_html typo_deprecated "Use html(:body)" html(:body) end def extended=(newval) if self[:extended] != newval changed if published? self[:extended] = newval end self[:extended] end def extended_html typo_deprecated "Use html(:extended)" html(:extended) end def self.html_map(field=nil) html_map = { :body => true, :extended => true } if field html_map[field.to_sym] else html_map end end def content_fields [:body, :extended] end ## Feed Stuff def rss_trackback(xml) return unless allow_pings? xml.trackback :ping, trackback_url end def rss_enclosure(xml) return if resources.empty? res = resources.first xml.enclosure(:url => blog.file_url(res.filename), :length => res.size, :type => res.mime) end def rss_groupings(xml) categories.each { |v| v.to_rss(xml) } tags.each { |v| v.to_rss(xml) } end def rss_author(xml) if link_to_author? xml.author("#{user.email} (#{user.name})") end end def rss_comments(xml) xml.comments(permalink_url + "#comments") end def link_to_author? !user.email.blank? && blog.link_to_author end def rss_title(xml) xml.title title end def atom_author(xml) xml.author { xml.name user.name } end def atom_title(xml) xml.title title, "type" => "html" end def atom_groupings(xml) categories.each {|v| v.to_atom(xml) } tags.each { |v| v.to_atom(xml) } end def atom_enclosures(xml) resources.each do |value| xml.with_options(value.size > 0 ? { :length => value.size } : { }) do |xm| xm.link "rel" => "enclosure", :type => value.mime, :title => title, :href => blog.file_url(value.filename) end end end def atom_content(xml) xml.summary "type" => "xhtml" do xml.div(:xmlns => "http://www.w3.org/1999/xhtml") {xml << html(:body) } end if blog.show_extended_on_rss xml.content(:type => "xhtml") do xml.div(:xmlns => 'http://www.w3.org/1999/xhtml') { xml << html(:all) } end end end def add_comment(params) comments.build(params) end def add_category(category, is_primary = false) self.categorizations.build(:category => category, :is_primary => is_primary) end protected before_create :set_defaults, :create_guid before_save :set_published_at after_save :keywords_to_tags after_create :add_notifications def set_published_at if self.published and self[:published_at].nil? self[:published_at] = self.created_at || Time.now end end def set_defaults if self.attributes.include?("permalink") and (self.permalink.blank? or self.permalink.to_s =~ /article-draft/ or self.state == "draft" ) self.permalink = self.stripped_title end if blog && self.allow_comments.nil? self.allow_comments = blog.default_allow_comments end if blog && self.allow_pings.nil? self.allow_pings = blog.default_allow_pings end true end def add_notifications self.notify_users = User.find_boolean(:all, :notify_on_new_articles) self.notify_users << self.user if (self.user.notify_watch_my_articles? rescue false) self.notify_users.uniq! end def self.time_delta(year, month = nil, day = nil) from = Time.mktime(year, month || 1, day || 1) to = from.next_year to = from.next_month unless month.blank? to = from + 1.day unless day.blank? to = to - 1 # pull off 1 second so we don't overlap onto the next day return from..to end validates_uniqueness_of :guid validates_presence_of :title private def fix_resources Resource.find(:all, :conditions => "article_id = #{id}").each do |fu| fu.article_id = nil fu.save end end end