Noosfero/noosfero

View on GitHub
plugins/newsletter/lib/newsletter_plugin/newsletter.rb

Summary

Maintainability
B
4 hrs
Test Coverage
require "csv"

class NewsletterPlugin::Newsletter < ApplicationRecord
  belongs_to :environment, optional: true
  belongs_to :person, optional: true
  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 do |blog|
                blog.posts
                    .reorder("articles.position DESC, published_at DESC")
                    .limit limit
              end
            else
              self.blogs.flat_map do |blog|
                blog.posts
                    .where("published_at >= :last_send_at", last_send_at: self.last_send_at)
                    .reorder("articles.position DESC, published_at DESC")
                    .limit limit
              end
    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 public_view_link
    content_tag(:div, message_to_public_link, style: CSS["newsletter-public-link"])
  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 = {})
    mailing_link = data[:mailing] ? public_view_link : ""
    content_tag(:div, mailing_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 || "").html_safe, 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

  extend ActsAsHavingImage::ClassMethods
  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!
    save!
  end
end