plugins/newsletter/lib/newsletter_plugin/newsletter.rb
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