newsletter.rb 7.47 KB
require 'csv'

class NewsletterPlugin::Newsletter < ActiveRecord::Base

  belongs_to :environment
  belongs_to :person
  validates_presence_of :environment, :person
  validates_uniqueness_of :environment_id
  validates_numericality_of :periodicity, only_integer: true, greater_than: -1, message: _('must be a positive number')
  validates_numericality_of :posts_per_blog, only_integer: true, greater_than: -1, message: _('must be a positive number')

  attr_accessible :environment, :enabled, :periodicity, :subject, :posts_per_blog, :footer, :blog_ids, :additional_recipients, :person, :person_id, :moderated

  scope :enabled, -> { where enabled: true }

  # These methods are used by NewsletterMailing
  def people
    list = unsubscribers.map{|i| "'#{i}'"}.join(',')
    if list.empty?
      environment.people
    else
      environment.people
        .joins('LEFT OUTER JOIN users ON (users.id = profiles.user_id)')
        .where("users.email NOT IN (#{list})")
    end
  end

  def name
    environment.name
  end

  def contact_email
    environment.noreply_email
  end

  def top_url
    environment.top_url
  end

  def unsubscribe_url
    "#{top_url}/plugin/newsletter/unsubscribe"
  end

  serialize :blog_ids, Array
  serialize :additional_recipients, Array

  def blog_ids
    self[:blog_ids].map(&:to_i) || []
  end

  validates_each :blog_ids do |record, attr, value|
    if record.environment
      unless value.delete_if(&:zero?).select { |id| !Blog.find_by(id: id) || Blog.find(id).environment != record.environment }.empty?
        record.errors.add(attr, _('must be valid'))
      end
    end
    unless value.uniq.length == value.length
      record.errors.add(attr, _('must not have duplicates'))
    end
  end

  validates_each :additional_recipients do |record, attr, value|
    unless value.reject { |recipient| recipient[:email] =~ Noosfero::Constants::EMAIL_FORMAT }.empty?
      record.errors.add(attr, _('must have only valid emails'))
    end
  end

  def next_send_at
    (self.last_send_at || DateTime.now) + self.periodicity.days
  end

  def must_be_sent_today?
    return true unless self.last_send_at
    Date.today >= self.next_send_at.to_date
  end

  def blogs
    Blog.where(:id => blog_ids)
  end

  def posts(data = {})
    limit = self.posts_per_blog.zero? ? nil : self.posts_per_blog
    posts = if self.last_send_at.nil?
      self.blogs.flat_map{ |blog| blog.posts.limit limit }
    else
      self.blogs.flat_map{ |blog| blog.posts.where("published_at >= :last_send_at", {last_send_at: self.last_send_at}).limit limit }
    end
    data[:post_ids].nil? ? posts : posts.select{|post| data[:post_ids].include?(post.id.to_s)}
  end

  CSS = {
    'breakingnews-wrap' => 'background-color: #EFEFEF; padding: 40px 0',
    'breakingnews' => 'width: 640px; margin: auto; background-color: white; border: 1px solid #ddd; border-spacing: 0; padding: 0',
    'newsletter-public-link' => 'width: 640px; margin: auto; font-size: small; color: #555; font-style: italic; text-align: right; margin-bottom: 15px; font-family: sans;',
    'newsletter-header' => 'padding: 0',
    'header-image' => 'width: 100%',
    'post-image' => 'padding-left: 20px; width: 25%; border-bottom: 1px dashed #DDD',
    'post-info' => 'font-family: Arial, Verdana; padding: 20px; width: 75%; border-bottom: 1px dashed #DDD',
    'post-date' => 'font-size: 12px;',
    'post-lead' => 'font-size: 14px; text-align: justify',
    'post-title' => 'color: #000; text-decoration: none; font-size: 16px; text-align: justify',
    'read-more-line' => 'text-align: right',
    'read-more-link' => 'color: #000; font-size: 12px;',
    'newsletter-unsubscribe' => 'width: 640px; margin: auto; font-size: small; color: #555; font-style: italic; text-align: center; margin-top: 15px; font-family: sans;'
  }

  # to be able to generate HTML
  include ActionView::Helpers
  include Rails.application.routes.url_helpers
  include DatesHelper

  def message_to_public_link
    content_tag(:p, (_("If you can't view this email, %s.") % link_to(_('click here'), '{mailing_url}')).html_safe, :id => 'newsletter-public-link').html_safe
  end

  def message_to_unsubscribe
    content_tag(:div, _("This is an automatically generated email, please do not reply. If you do not wish to receive future newsletter emails, %s.").html_safe % link_to(_("cancel your subscription here"), self.unsubscribe_url, :style => CSS['public-link']), :style => CSS['newsletter-unsubscribe'], :id => 'newsletter-unsubscribe').html_safe
  end

  def read_more(link_address)
    content_tag(:p, link_to(_('Read more'), link_address, :style => CSS['read-more-link']), :style => CSS['read-more-line'])
  end

  def post_with_image(post)
    content_tag(:tr,content_tag(:td,tag(:img, :src => "#{self.environment.top_url}#{post.image.public_filename(:big)}", :id => post.id),:style => CSS['post-image'])+content_tag(:td,content_tag(:span, show_date(post.published_at), :style => CSS['post-date'])+content_tag(:h3, link_to(h(post.title), post.url, :style => CSS['post-title']))+content_tag(:p,sanitize(post.lead(190), tags: %w(strong em b i)),:style => CSS['post-lead'])+read_more(post.url), :style => CSS['post-info']))
  end

  def post_without_image(post)
    content_tag(:tr, content_tag(:td,content_tag(:span, show_date(post.published_at),:style => CSS['post-date'], :id => post.id)+content_tag(:h3, link_to(h(post.title), post.url,:style => CSS['post-title']))+content_tag(:p,sanitize(post.lead(360), tags: %w(strong em b i)),:style => CSS['post-lead'])+read_more(post.url),:colspan => 2, :style => CSS['post-info']))
  end

  def body(data = {})
    content_tag(:div, content_tag(:div, message_to_public_link, :style => CSS['newsletter-public-link']).html_safe+content_tag(:table,(self.image.nil? ? '' : content_tag(:tr, content_tag(:th, tag(:img, :src => "#{self.environment.top_url}#{self.image.public_filename}", :style => CSS['header-image']),:colspan => 2),:style => CSS['newsletter-header'])).html_safe+self.posts(data).map do |post|
        if post.image
          post_with_image(post)
        else
          post_without_image(post)
        end
      end.join().html_safe+content_tag(:tr, content_tag(:td, self.footer, :colspan => 2)),:style => CSS['breakingnews']).html_safe+content_tag(:div,message_to_unsubscribe, :style => CSS['newsletter-unsubscribe']),:style => CSS['breakingnews-wrap']).html_safe
  end

  def default_subject
    _('Breaking news')
  end

  def subject
    self[:subject] || default_subject
  end

  def import_recipients(file, name_column = nil, email_column = nil, headers = nil)
    name_column ||= 1
    email_column ||= 2
    headers ||= false

    if File.extname(file.original_filename) == '.csv'
      [",", ";", "\t"].each do |sep|
        parsed_recipients = []
        CSV.foreach(file.path, { headers: headers, col_sep: sep }) do |row|
          parsed_recipients << {name: row[name_column.to_i - 1], email: row[email_column.to_i - 1]}
        end
        self.additional_recipients = parsed_recipients
        break if self.valid? || !self.errors.include?(:additional_recipients)
      end
    else
      #FIXME find a better way to deal with errors
      self.errors.add(:additional_recipients, _("have unknown file type: %s" % file.original_filename))
    end
  end

  acts_as_having_image

  def last_send_at
    last_mailing = NewsletterPlugin::NewsletterMailing.where(source_id: self.id).last
    last_mailing.nil? ? nil : last_mailing.created_at
  end

  def has_posts_in_the_period?
    ! self.posts.empty?
  end

  serialize :unsubscribers, Array

  def unsubscribe(email)
    unsubscribers.push(email).uniq!
  end

end