1require 'ipaddr' |
|
3require_dependency 'archived' |
|
5module Archived |
|
|
6 class Signature < ActiveRecord::Base |
7 include GeoipLookup |
|
9 ISO8601_TIMESTAMP = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\z/ |
|
11 PENDING_STATE = 'pending' |
|
12 FRAUDULENT_STATE = 'fraudulent' |
|
13 VALIDATED_STATE = 'validated' |
|
14 INVALIDATED_STATE = 'invalidated' |
|
16 STATES = [ |
|
17 PENDING_STATE, FRAUDULENT_STATE, |
|
18 VALIDATED_STATE, INVALIDATED_STATE |
|
19 ]
|
|
21 TIMESTAMPS = { |
|
22 'government_response' => :government_response_email_at, |
|
23 'debate_scheduled' => :debate_scheduled_email_at, |
|
24 'debate_outcome' => :debate_outcome_email_at, |
|
25 'petition_email' => :petition_email_at |
|
26 }
|
|
28 belongs_to :petition |
|
29 belongs_to :invalidation |
|
30 belongs_to :constituency, primary_key: :external_id |
|
32 validates :constituency_id, length: { maximum: 255 } |
|
33 validates :email, presence: true |
|
34 validates :location_code, presence: true |
|
35 validates :name, presence: true, length: { maximum: 255 } |
|
36 validates :state, presence: true, inclusion: { in: STATES } |
|
38 attr_readonly :sponsor, :creator |
|
40 before_destroy do |
|
41 !creator?
|
|
42 end |
|
44 class << self |
|
45 def batch(id = 0, limit: 1000) |
|
46 where(arel_table[:id].gteq(id)).order(id: :asc).limit(limit) |
|
47 end |
|
49 def by_most_recent |
|
50 order(created_at: :desc) |
|
51 end |
|
53 def column_name_for(timestamp) |
|
54 TIMESTAMPS.fetch(timestamp) |
|
55 rescue |
|
56 raise ArgumentError, "Unknown petition email timestamp: #{timestamp.inspect}" |
|
57 end |
|
59 def destroy!(signature_ids) |
|
60 signatures = find(signature_ids)
|
|
62 transaction do |
|
63 signatures.each do |signature| |
|
64 signature.destroy!
|
|
65 end |
|
66 end |
|
67 end |
|
69 def for_domain(domain) |
|
70 where("SUBSTRING(email FROM POSITION('@' IN email) + 1) = ?", domain[1..-1]) |
|
71 end |
|
73 def for_email(email) |
|
74 where("(REGEXP_REPLACE(LEFT(email, POSITION('@' IN email) - 1), '\\.|\\+.+', '', 'g') || SUBSTRING(email FROM POSITION('@' IN email)) = ?)", normalize_email(email)) |
|
75 end |
|
77 def for_ip(ip) |
|
78 where("inet(ip_address) <<= inet(?)", ip) |
|
79 end |
|
81 def for_name(name) |
|
82 where(arel_table[:name].lower.eq(name.downcase)) |
|
83 end |
|
85 def for_petition(id) |
|
86 where(petition_id: id) |
|
87 end |
|
89 def for_postcode(postcode) |
|
90 where(postcode: PostcodeSanitizer.call(postcode)) |
|
91 end |
|
93 def for_sector(postcode) |
|
94 where("LEFT(postcode, -3) = ?", PostcodeSanitizer.call(postcode)[0..-4]) |
|
95 end |
|
97 def for_timestamp(timestamp, since:) |
|
98 column = arel_table[column_name_for(timestamp)]
|
|
99 where(column.eq(nil).or(column.lt(since))) |
|
100 end |
|
102 def need_emailing_for(timestamp, since:) |
|
103 validated.subscribed.for_timestamp(timestamp, since: since) |
|
104 end |
|
106 def subscribed |
|
107 where(notify_by_email: true) |
|
108 end |
|
110 def validated |
|
111 where(state: VALIDATED_STATE) |
|
112 end |
|
114 def creator |
|
115 where(arel_table[:creator].eq(true)) |
|
116 end |
|
118 def sponsors |
|
119 where(arel_table[:sponsor].eq(true)) |
|
120 end |
|
|
122 def search(query, options = {}) |
123 query = query.to_s
|
|
124 state = options[:state] |
|
125 window = options[:window] |
|
126 page = [options[:page].to_i, 1].max |
|
127 scope = preload(:petition).by_most_recent |
|
129 if state.in?(STATES) |
|
130 scope = scope.where(state: state) |
|
131 end |
|
133 if window && window =~ ISO8601_TIMESTAMP |
|
134 starts_at = window.in_time_zone.at_beginning_of_hour
|
|
135 ends_at = starts_at.advance(hours: 1) |
|
136 scope = scope.where(created_at: starts_at..ends_at) |
|
137 elsif window =~ /\A\d+\z/ |
|
138 starts_at = window.to_i.seconds.ago
|
|
139 ends_at = Time.current |
|
140 scope = scope.where(created_at: starts_at..ends_at) |
|
141 end |
|
143 if ip_search?(query) |
|
144 scope = scope.for_ip(query)
|
|
145 elsif domain_search?(query) |
|
146 scope = scope.for_domain(query)
|
|
147 elsif email_search?(query) |
|
148 scope = scope.for_email(query)
|
|
149 elsif petition_search?(query) |
|
150 scope = scope.for_petition(query)
|
|
151 elsif postcode_search?(query) |
|
152 scope = scope.for_postcode(query)
|
|
153 elsif sector_search?(query) |
|
154 scope = scope.for_sector(query)
|
|
155 else |
|
156 scope = scope.for_name(query)
|
|
157 end |
|
159 scope.paginate(page: page, per_page: 50) |
|
160 end |
|
162 def subscribe!(signature_ids) |
|
163 signatures = find(signature_ids)
|
|
165 transaction do |
|
166 signatures.each do |signature| |
|
167 signature.update!(notify_by_email: true) |
|
168 end |
|
169 end |
|
170 end |
|
172 def unsubscribe!(signature_ids) |
|
173 signatures = find(signature_ids)
|
|
175 transaction do |
|
176 signatures.each do |signature| |
|
177 if signature.creator? |
|
178 raise RuntimeError, "Can't unsubscribe the creator signature" |
|
179 elsif signature.pending? |
|
180 raise RuntimeError, "Can't unsubscribe a pending signature" |
|
181 else |
|
182 signature.update!(notify_by_email: false) |
|
183 end |
|
184 end |
|
185 end |
|
186 end |
|
188 private
|
|
190 def ip_search?(query) |
|
191 IPAddr.new(query) |
|
192 rescue IPAddr::InvalidAddressError => e |
|
193 false |
|
194 end |
|
196 def domain_search?(query) |
|
197 query.starts_with?('@') |
|
198 end |
|
200 def email_search?(query) |
|
201 query.include?('@') |
|
202 end |
|
204 def petition_search?(query) |
|
205 query =~ /\A\d+\z/ |
|
206 end |
|
208 def postcode_search?(query) |
|
209 PostcodeSanitizer.call(query) =~ PostcodeValidator::PATTERN |
|
210 end |
|
212 def sector_search?(query) |
|
213 PostcodeSanitizer.call(query) =~ /\A[A-Z]{1,2}[0-9][0-9A-Z]?XXX\z/ |
|
214 end |
|
216 def normalize_email(email) |
|
217 "#{normalize_user(email)}@#{normalize_domain(email)}" |
|
218 end |
|
220 def normalize_user(email) |
|
221 email.split("@").first.split("+").first.tr(".", "").downcase |
|
222 end |
|
224 def normalize_domain(email) |
|
225 email.split("@").last.downcase |
|
226 end |
|
227 end |
|
229 def get_email_sent_at_for(timestamp) |
|
230 self[column_name_for(timestamp)] |
|
231 end |
|
233 def set_email_sent_at_for(timestamp, to: Time.current) |
|
234 update_column(column_name_for(timestamp), to)
|
|
235 end |
|
237 def pending? |
|
238 state == PENDING_STATE |
|
239 end |
|
241 def fraudulent? |
|
242 state == FRAUDULENT_STATE |
|
243 end |
|
245 def validated? |
|
246 state == VALIDATED_STATE |
|
247 end |
|
249 def invalidated? |
|
250 state == INVALIDATED_STATE |
|
251 end |
|
253 def subscribed? |
|
254 validated? && !unsubscribed?
|
|
255 end |
|
257 def unsubscribed? |
|
258 notify_by_email == false |
|
259 end |
|
261 def unsubscribe!(token) |
|
262 if unsubscribed? |
|
263 errors.add(:base, "Already Unsubscribed") |
|
264 elsif unsubscribe_token != token |
|
265 errors.add(:base, "Invalid Unsubscribe Token") |
|
266 else |
|
267 update(notify_by_email: false) |
|
268 end |
|
269 end |
|
271 def already_unsubscribed? |
|
272 errors[:base].include?("Already Unsubscribed") |
|
273 end |
|
275 def invalid_unsubscribe_token? |
|
276 errors[:base].include?("Invalid Unsubscribe Token") |
|
277 end |
|
279 def united_kingdom? |
|
280 location_code == 'GB' |
|
281 end |
|
282 alias_method :uk?, :united_kingdom? |
|
284 def location |
|
285 if postcode? |
|
286 "#{formatted_postcode}, #{location_code}" |
|
287 else |
|
288 location_code
|
|
289 end |
|
290 end |
|
292 def account |
|
293 Mail::Address.new(email).local |
|
294 rescue Mail::Field::ParseError |
|
295 nil |
|
296 end |
|
298 def domain |
|
299 Mail::Address.new(email).domain |
|
300 rescue Mail::Field::ParseError |
|
301 nil |
|
302 end |
|
304 private
|
|
306 def formatted_postcode |
|
307 if united_kingdom? |
|
308 postcode.gsub(/\A([A-Z0-9]+?)([A-Z0-9]{3})\z/, "\\1 \\2") |
|
309 else |
|
310 postcode
|
|
311 end |
|
312 end |
|
314 def column_name_for(timestamp) |
|
315 self.class.column_name_for(timestamp) |
|
316 end |
|
317 end |
|
318end |