ref: master
plugins/newsletter/lib/newsletter_plugin/newsletter.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
require 'csv' class NewsletterPlugin::Newsletter < ApplicationRecord 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 || "").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 |