-
-
Notifications
You must be signed in to change notification settings - Fork 197
/
user.rb
711 lines (598 loc) · 20.1 KB
/
user.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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
# == Schema Information
# Schema version: 20230301110831
#
# Table name: users
#
# id :integer not null, primary key
# email :string not null
# name :string not null
# hashed_password :string not null
# salt :string
# created_at :datetime not null
# updated_at :datetime not null
# email_confirmed :boolean default(FALSE), not null
# url_name :text not null
# last_daily_track_email :datetime default(Sat, 01 Jan 2000 00:00:00.000000000 GMT +00:00)
# ban_text :text default(""), not null
# about_me :text default(""), not null
# locale :string
# email_bounced_at :datetime
# email_bounce_message :text default(""), not null
# no_limit :boolean default(FALSE), not null
# receive_email_alerts :boolean default(TRUE), not null
# can_make_batch_requests :boolean default(FALSE), not null
# otp_enabled :boolean default(FALSE), not null
# otp_secret_key :string
# otp_counter :integer default(1)
# confirmed_not_spam :boolean default(FALSE), not null
# comments_count :integer default(0), not null
# info_requests_count :integer default(0), not null
# track_things_count :integer default(0), not null
# request_classifications_count :integer default(0), not null
# public_body_change_requests_count :integer default(0), not null
# info_request_batches_count :integer default(0), not null
# daily_summary_hour :integer
# daily_summary_minute :integer
# closed_at :datetime
# login_token :string
# receive_user_messages :boolean default(TRUE), not null
# user_messages_count :integer default(0), not null
#
class User < ApplicationRecord
include AlaveteliFeatures::Helpers
include AlaveteliPro::PhaseCounts
include User::Authentication
include User::LoginToken
include User::OneTimePassword
include User::Slug
include User::SpreadableAlerts
include User::Survey
include Rails.application.routes.url_helpers
include LinkToHelper
DEFAULT_CONTENT_LIMITS = {
info_requests: AlaveteliConfiguration.max_requests_per_user_per_day,
comments: AlaveteliConfiguration.max_requests_per_user_per_day,
user_messages: AlaveteliConfiguration.max_requests_per_user_per_day
}.freeze
cattr_accessor :content_limits, default: DEFAULT_CONTENT_LIMITS
rolify before_add: :setup_pro_account,
after_add: :assign_role_features,
after_remove: :assign_role_features
strip_attributes allow_empty: true
admin_columns include: [:user_messages_count],
exclude: [:otp_secret_key, :url_name]
attr_accessor :no_xapian_reindex
has_many :info_requests,
-> { order(created_at: :desc) },
inverse_of: :user,
dependent: :destroy
has_many :info_request_events,
-> { reorder(created_at: :desc) },
through: :info_requests
has_many :embargoes,
inverse_of: :user,
through: :info_requests
has_many :outgoing_messages,
inverse_of: :user,
through: :info_requests
has_many :draft_info_requests,
-> { order(created_at: :desc) },
inverse_of: :user,
dependent: :destroy
has_many :user_info_request_sent_alerts,
inverse_of: :user,
dependent: :destroy
has_many :post_redirects,
-> { order(created_at: :desc) },
inverse_of: :user,
dependent: :destroy
has_many :track_things,
-> { order(created_at: :desc) },
inverse_of: :tracking_user,
foreign_key: 'tracking_user_id',
dependent: :destroy
has_many :citations,
-> { order(created_at: :desc) },
inverse_of: :user,
dependent: :destroy
has_many :comments,
-> { order(created_at: :desc) },
inverse_of: :user,
dependent: :destroy
has_many :public_body_change_requests,
-> { order(created_at: :desc) },
inverse_of: :user,
dependent: :destroy
has_one :profile_photo,
inverse_of: :user,
dependent: :destroy
has_many :censor_rules,
-> { order(created_at: :desc) },
inverse_of: :user,
dependent: :destroy
has_many :info_request_batches,
-> { order(created_at: :desc) },
inverse_of: :user,
dependent: :destroy
has_many :draft_info_request_batches,
-> { order(created_at: :desc) },
inverse_of: :user,
dependent: :destroy,
class_name: 'AlaveteliPro::DraftInfoRequestBatch'
has_many :request_classifications,
inverse_of: :user,
dependent: :destroy
has_one :pro_account,
inverse_of: :user,
dependent: :destroy
has_many :request_summaries,
inverse_of: :user,
dependent: :destroy,
class_name: 'AlaveteliPro::RequestSummary'
has_many :notifications,
inverse_of: :user,
dependent: :destroy
has_many :track_things_sent_emails,
inverse_of: :user,
dependent: :destroy
has_many :track_things_sent_emails,
dependent: :destroy
has_many :announcements,
inverse_of: :user
has_many :announcement_dismissals,
inverse_of: :user,
dependent: :destroy
has_many :memberships, class_name: 'Project::Membership'
has_many :projects, through: :memberships do
def owner
unscope(:joins).joins(:owner_membership)
end
def contributor
unscope(:joins).joins(:contributor_memberships)
end
end
has_many :sign_ins,
class_name: 'User::SignIn',
inverse_of: :user,
dependent: :destroy
has_many :user_messages,
-> { order(created_at: :desc) },
inverse_of: :user,
dependent: :destroy
scope :active, -> { not_banned.not_closed }
scope :banned, -> { where.not(ban_text: '') }
scope :not_banned, -> { where(ban_text: '') }
scope :closed, -> { where.not(closed_at: nil) }
scope :not_closed, -> { where(closed_at: nil) }
validates_presence_of :email, message: _('Please enter your email address')
validates_presence_of :name, message: _('Please enter your name')
validates_length_of :about_me,
maximum: 500,
message: _('Please keep it shorter than 500 characters')
validates :email,
uniqueness: { case_sensitive: false,
message: _('This email is already in use') }
validate :email_and_name_are_valid
after_update :update_pro_account
after_update :reindex_referencing_models, :invalidate_cached_pages,
unless: :no_xapian_reindex
acts_as_xapian texts: [:name, :about_me],
values: [
[:created_at_numeric, 1, 'created_at', :number] # for sorting
],
terms: [[:variety, 'V', 'variety']],
if: :indexed_by_search?
def self.search(query)
where(<<~SQL, query: query)
lower(users.name) LIKE lower('%'||:query||'%') OR
lower(users.email) LIKE lower('%'||:query||'%') OR
lower(users.about_me) LIKE lower('%'||:query||'%')
SQL
end
def self.pro
with_role(:pro)
end
# Return user given login email, password and other form parameters (e.g. name)
#
# The specific_user_login parameter says that login as a particular user is
# expected, so no parallel registration form is being displayed.
def self.authenticate_from_form(params, specific_user_login = false)
params[:email].strip!
if specific_user_login
auth_fail_message = _("Either the email or password was not recognised, please try again.")
else
auth_fail_message = _("Either the email or password was not recognised, please try again. Or create a new account using the form on the left.")
end
user = find_user_by_email(params[:email])
if user
# There is user with email, check password
unless user.has_this_password?(params[:password])
user.errors.add(:base, auth_fail_message)
end
if user.has_this_password?(params[:password]) && user.closed?
logger.info "Closed user attempted login: #{ params[:email] }"
user.errors.add(:base, _('This account has been closed.'))
end
else
# No user of same email, make one (that we don't save in the database)
# for the forms code to use.
user = User.new(params)
# deliberately same message as above so as not to leak whether registered
user.errors.add(:base, auth_fail_message)
end
user
end
def self.authenticate_from_session(session)
return unless session[:user_id]
find_by(id: session[:user_id], login_token: session[:user_login_token])
end
# Case-insensitively find a user from their email
def self.find_user_by_email(email)
return nil if email.blank?
where('lower(email) = lower(?)', email.strip).first
end
# The "internal admin" is a special user for internal use.
def self.internal_admin_user
user = find_by(email: AlaveteliConfiguration.contact_email)
return user if user
password = PostRedirect.generate_random_token
create!(
name: 'Internal admin user',
email: AlaveteliConfiguration.contact_email,
password: password,
password_confirmation: password
)
end
# Should the user be kept logged into their own account
# if they follow a /c/ redirect link belonging to another user?
def self.stay_logged_in_on_redirect?(user)
user&.is_admin?
end
def self.record_bounce_for_email(email, message)
user = User.find_user_by_email(email)
return false if user.nil?
user.record_bounce(message) if user.email_bounced_at.nil?
true
end
def self.find_similar_named_users(user)
User.where('name ILIKE ? AND email_confirmed = ? AND id <> ?',
user.name, true, user.id).order(:created_at)
end
def view_hidden?
is_admin?
end
def view_embargoed?
is_pro_admin?
end
def view_hidden_and_embargoed?
view_hidden? && view_embargoed?
end
def transactions(*associations)
opts = {}
opts[:transaction_associations] = associations if associations.any?
TransactionCalculator.new(self, opts)
end
def created_at_numeric
# format it here as no datetime support in Xapian's value ranges
created_at.strftime("%Y%m%d%H%M%S")
end
def variety
'user'
end
# requested_by: and commented_by: search queries also need updating after save
def reindex_referencing_models
return unless saved_change_to_attribute?(:url_name)
expire_comments
expire_requests
end
def expire_requests
InfoRequestExpireJob.perform_later(self, :info_requests)
end
def expire_comments
comments.find_each(&:reindex_request_events)
end
def invalidate_cached_pages
NotifyCacheJob.perform_later(self)
end
def locale
(super || AlaveteliLocalization.locale).to_s
end
def name
_name = read_attribute(:name)
if suspended?
_name = _('{{user_name}} (Account suspended)', user_name: _name)
end
_name
end
# When name is changed, also change the url name
def name=(name)
write_attribute(:name, name.try(:strip))
end
def previous_names
outgoing_messages.unscope(:order).
distinct(:from_name).
where.not(from_name: read_attribute(:name)).
pluck(:from_name)
end
def safe_previous_names
outgoing_messages.map(&:safe_from_name).uniq - [read_attribute(:name)]
end
# For use in to/from in email messages
def name_and_email
MailHandler.address_from_name_and_email(name, email)
end
# Returns list of requests which the user hasn't described (and last
# changed more than a day ago)
def get_undescribed_requests
info_requests.
where(awaiting_description: true).
where("#{ InfoRequest.last_event_time_clause } < ?", 1.day.ago)
end
# Does the user magically gain powers as if they owned every request?
# e.g. Can classify it
def owns_every_request?
is_admin?
end
def can_admin_roles
roles.
flat_map { |role| Role.grants_and_revokes(role.name.to_sym) }.
compact.
uniq
end
def can_admin_role?(role)
can_admin_roles.include?(role)
end
# Does the user get "(admin)" links on each page on the main site?
def admin_page_links?
is_admin?
end
def banned?
ban_text.present?
end
def close
close!
rescue ActiveRecord::RecordInvalid
false
end
def close!
update!(closed_at: Time.zone.now, receive_email_alerts: false)
end
def closed?
closed_at.present?
end
def erase
erase!
rescue ActiveRecord::RecordInvalid
false
end
def erase!
raise ActiveRecord::RecordInvalid unless closed?
sha = Digest::SHA1.hexdigest(rand.to_s)
transaction do
slugs.destroy_all
sign_ins.destroy_all
profile_photo&.destroy!
outgoing_messages.update!(
from_name: _('[Name Removed]')
)
update!(
name: _('[Name Removed]'),
email: "#{sha}@invalid",
url_name: sha,
about_me: '',
password: MySociety::Util.generate_token
)
end
end
def anonymise!
return if info_requests.none? && comments.none?
current_name = read_attribute(:name)
[current_name, *previous_names].each do |name|
censor_rules.create!(text: name,
replacement: _('[Name Removed]'),
last_edit_editor: 'User#anonymise!',
last_edit_comment: 'User#anonymise!')
end
end
def close_and_anonymise
transaction do
close!
anonymise!
erase!
end
end
def active?
!banned? && !closed?
end
def suspended?
!active?
end
def prominence
return 'hidden' if banned?
return 'backpage' if closed?
return 'backpage' unless email_confirmed?
'normal'
end
# Various ways the user can be banned, and text to describe it if failed
def can_file_requests?
active? && !exceeded_limit?(:info_requests)
end
def can_make_followup?
active?
end
def can_make_comments?
return false unless active?
return true if no_limit? || is_admin? || is_pro_admin?
!exceeded_limit?(:comments) &&
!Comment.exceeded_creation_rate?(comments)
end
def can_contact_other_users?
active? && !exceeded_limit?(:user_messages)
end
def exceeded_limit?(content)
return false if no_limit?
return false if can_make_batch_requests?
return false if content_limit(content).blank?
# Has the User created too much of the content in the past 24 hours?
recent_content =
content.to_s.classify.constantize.
where(["user_id = ? AND created_at > now() - '1 day'::interval", id]).
count
recent_content >= content_limit(content)
end
def next_request_permitted_at
return nil if no_limit
n_most_recent_requests =
InfoRequest.
where(["user_id = ? AND created_at > now() - '1 day'::interval", id]).
order(created_at: :desc).
limit(AlaveteliConfiguration.max_requests_per_user_per_day)
if n_most_recent_requests.size < AlaveteliConfiguration.max_requests_per_user_per_day
return nil
end
nth_most_recent_request = n_most_recent_requests[-1]
nth_most_recent_request.created_at + 1.day
end
def can_fail_html
if banned?
text = ban_text.strip
elsif closed?
text = _('Account closed at user request')
else
raise 'Unknown reason for ban'
end
text = CGI.escapeHTML(text)
text = MySociety::Format.make_clickable(text, contract: 1)
text = text.gsub(/\n/, '<br>')
text.html_safe
end
# Returns domain part of user's email address
def email_domain
PublicBody.extract_domain_from_email(email)
end
# A photograph of the user (to make it all more human)
def set_profile_photo(new_profile_photo)
ActiveRecord::Base.transaction do
profile_photo.destroy unless profile_photo.nil?
self.profile_photo = new_profile_photo
save!
end
end
def show_profile_photo?
active? && profile_photo
end
def about_me_already_exists?
return false if about_me.blank?
self.class.where(about_me: about_me).where.not(id: id).any?
end
# Return about me text for display as HTML
# TODO: Move this to a view helper
def get_about_me_for_html_display
text = about_me.strip
text = CGI.escapeHTML(text)
text = MySociety::Format.make_clickable(text, contract: 1, nofollow: true)
text = text.gsub(/\n/, '<br>')
text.html_safe
end
def json_for_api
{
id: id,
url_name: url_name,
name: name,
ban_text: ban_text,
about_me: about_me
# :profile_photo => self.profile_photo # ought to have this, but too hard to get URL out for now
# created_at / updated_at we only show the year on the main page for privacy reasons, so don't put here
}
end
def record_bounce(message)
update!(
email_bounced_at: Time.zone.now,
email_bounce_message: convert_string_to_utf8(message).string
)
end
def confirm(save_record = false)
self.email_confirmed = true
save! if save_record
end
def confirm!
confirm
save!
end
def should_be_emailed?
active? && email_confirmed? && receive_email_alerts? && !email_bounced_at
end
def indexed_by_search?
email_confirmed && active?
end
# Notify a user about an info_request_event, allowing the user's preferences
# to determine how that notification is delivered.
def notify(info_request_event)
Notification.create(
info_request_event: info_request_event,
frequency: Notification.frequencies[notification_frequency],
user: self
)
end
# Return a timestamp for the next time a user should be sent a daily summary
def next_daily_summary_time
summary_time = Time.zone.now.change(daily_summary_time)
summary_time += 1.day if summary_time < Time.zone.now
summary_time
end
def daily_summary_time
{ hour: daily_summary_hour,
min: daily_summary_minute }
end
# With what frequency does the user want to be notified?
def notification_frequency
if features.enabled?(:notifications)
Notification::DAILY
else
Notification::INSTANTLY
end
end
def features
# Will return enabled and disabled features. Call #enabled? to see the
# current state
AlaveteliFeatures.features.with_actor(self)
end
def features=(new_features)
features.assign_features(new_features)
end
# Define an id number for use with the Flipper gem's user-by-user feature
# flagging. We prefix with the class because features can be enabled for
# other types of objects (e.g Roles) in the same way and will be stored in
# the same table. See:
# https://github.com/jnunemaker/flipper/blob/master/docs/Gates.md
def flipper_id
"User;#{id}"
end
def cached_urls
[
user_path(self)
]
end
private
def email_and_name_are_valid
if email != "" && !MySociety::Validate.is_valid_email(email)
errors.add(:email, _("Please enter a valid email address"))
end
if MySociety::Validate.is_valid_email(name)
errors.add(:name, _("Please enter your name, not your email address, in the name field."))
end
end
def assign_role_features(_role)
features.assign_role_features
end
def setup_pro_account(role)
return unless role == Role.pro_role
pro_account || build_pro_account if feature_enabled?(:pro_pricing)
end
def update_pro_account
pro_account.update_stripe_customer if pro_account
end
def content_limit(content)
content_limits[content]
end
end