Mercurial > hg > soundsoftware-site
comparison app/models/user.rb @ 514:7eba09d624db live
Merge
author | Chris Cannam |
---|---|
date | Thu, 14 Jul 2011 10:50:53 +0100 |
parents | 753f1380d6bc |
children | ec7c78040115 |
comparison
equal
deleted
inserted
replaced
512:b9aebdd7dd40 | 514:7eba09d624db |
---|---|
1 # Redmine - project management software | 1 # Redmine - project management software |
2 # Copyright (C) 2006-2009 Jean-Philippe Lang | 2 # Copyright (C) 2006-2011 Jean-Philippe Lang |
3 # | 3 # |
4 # This program is free software; you can redistribute it and/or | 4 # This program is free software; you can redistribute it and/or |
5 # modify it under the terms of the GNU General Public License | 5 # modify it under the terms of the GNU General Public License |
6 # as published by the Free Software Foundation; either version 2 | 6 # as published by the Free Software Foundation; either version 2 |
7 # of the License, or (at your option) any later version. | 7 # of the License, or (at your option) any later version. |
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | 17 |
18 require "digest/sha1" | 18 require "digest/sha1" |
19 | 19 |
20 class User < Principal | 20 class User < Principal |
21 | 21 include Redmine::SafeAttributes |
22 | |
22 # Account statuses | 23 # Account statuses |
23 STATUS_ANONYMOUS = 0 | 24 STATUS_ANONYMOUS = 0 |
24 STATUS_ACTIVE = 1 | 25 STATUS_ACTIVE = 1 |
25 STATUS_REGISTERED = 2 | 26 STATUS_REGISTERED = 2 |
26 STATUS_LOCKED = 3 | 27 STATUS_LOCKED = 3 |
32 :lastname_coma_firstname => '#{lastname}, #{firstname}', | 33 :lastname_coma_firstname => '#{lastname}, #{firstname}', |
33 :username => '#{login}' | 34 :username => '#{login}' |
34 } | 35 } |
35 | 36 |
36 MAIL_NOTIFICATION_OPTIONS = [ | 37 MAIL_NOTIFICATION_OPTIONS = [ |
37 [:all, :label_user_mail_option_all], | 38 ['all', :label_user_mail_option_all], |
38 [:selected, :label_user_mail_option_selected], | 39 ['selected', :label_user_mail_option_selected], |
39 [:none, :label_user_mail_option_none], | 40 ['only_my_events', :label_user_mail_option_only_my_events], |
40 [:only_my_events, :label_user_mail_option_only_my_events], | 41 ['only_assigned', :label_user_mail_option_only_assigned], |
41 [:only_assigned, :label_user_mail_option_only_assigned], | 42 ['only_owner', :label_user_mail_option_only_owner], |
42 [:only_owner, :label_user_mail_option_only_owner] | 43 ['none', :label_user_mail_option_none] |
43 ] | 44 ] |
44 | 45 |
45 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)}, | 46 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)}, |
46 :after_remove => Proc.new {|user, group| group.user_removed(user)} | 47 :after_remove => Proc.new {|user, group| group.user_removed(user)} |
47 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify | 48 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify |
48 has_many :changesets, :dependent => :nullify | 49 has_many :changesets, :dependent => :nullify |
49 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' | 50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' |
50 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'" | 51 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'" |
51 has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'" | 52 has_one :api_token, :class_name => 'Token', :conditions => "action='api'" |
52 belongs_to :auth_source | 53 belongs_to :auth_source |
53 | 54 |
54 has_one :ssamr_user_detail, :dependent => :destroy, :class_name => 'SsamrUserDetail' | 55 has_one :ssamr_user_detail, :dependent => :destroy, :class_name => 'SsamrUserDetail' |
55 accepts_nested_attributes_for :ssamr_user_detail | 56 accepts_nested_attributes_for :ssamr_user_detail |
56 | 57 |
60 acts_as_customizable | 61 acts_as_customizable |
61 | 62 |
62 attr_accessor :password, :password_confirmation | 63 attr_accessor :password, :password_confirmation |
63 attr_accessor :last_before_login_on | 64 attr_accessor :last_before_login_on |
64 # Prevents unauthorized assignments | 65 # Prevents unauthorized assignments |
65 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids | 66 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password |
66 | 67 |
67 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } | 68 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } |
68 | 69 |
69 # TODO: is this validation correct validates_presence_of :ssamr_user_detail | 70 # TODO: is this validation correct validates_presence_of :ssamr_user_detail |
70 | 71 |
71 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false | 72 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false |
72 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false | 73 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false |
73 # Login must contain lettres, numbers, underscores only | 74 # Login must contain lettres, numbers, underscores only |
74 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i | 75 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i |
75 validates_length_of :login, :maximum => 30 | 76 validates_length_of :login, :maximum => 30 |
76 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i | |
77 validates_length_of :firstname, :lastname, :maximum => 30 | 77 validates_length_of :firstname, :lastname, :maximum => 30 |
78 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true | 78 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true |
79 validates_length_of :mail, :maximum => 60, :allow_nil => true | 79 validates_length_of :mail, :maximum => 60, :allow_nil => true |
80 validates_confirmation_of :password, :allow_nil => true | 80 validates_confirmation_of :password, :allow_nil => true |
81 | 81 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true |
82 | |
83 before_destroy :remove_references_before_destroy | |
84 | |
82 validates_acceptance_of :terms_and_conditions, :on => :create, :message => :must_accept_terms_and_conditions | 85 validates_acceptance_of :terms_and_conditions, :on => :create, :message => :must_accept_terms_and_conditions |
83 | 86 |
87 named_scope :in_group, lambda {|group| | |
88 group_id = group.is_a?(Group) ? group.id : group.to_i | |
89 { :conditions => ["#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] } | |
90 } | |
91 named_scope :not_in_group, lambda {|group| | |
92 group_id = group.is_a?(Group) ? group.id : group.to_i | |
93 { :conditions => ["#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] } | |
94 } | |
95 | |
84 def before_create | 96 def before_create |
85 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank? | 97 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank? |
86 true | 98 true |
87 end | 99 end |
88 | 100 |
89 def before_save | 101 def before_save |
90 # update hashed_password if password was set | 102 # update hashed_password if password was set |
91 self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank? | 103 if self.password && self.auth_source_id.blank? |
104 salt_password(password) | |
105 end | |
92 end | 106 end |
93 | 107 |
94 def reload(*args) | 108 def reload(*args) |
95 @name = nil | 109 @name = nil |
110 @projects_by_role = nil | |
96 super | 111 super |
97 end | 112 end |
98 | 113 |
99 def mail=(arg) | 114 def mail=(arg) |
100 write_attribute(:mail, arg.to_s.strip) | 115 write_attribute(:mail, arg.to_s.strip) |
128 if user.auth_source | 143 if user.auth_source |
129 # user has an external authentication method | 144 # user has an external authentication method |
130 return nil unless user.auth_source.authenticate(login, password) | 145 return nil unless user.auth_source.authenticate(login, password) |
131 else | 146 else |
132 # authentication with local password | 147 # authentication with local password |
133 return nil unless User.hash_password(password) == user.hashed_password | 148 return nil unless user.check_password?(password) |
134 end | 149 end |
135 else | 150 else |
136 # user is not yet registered, try to authenticate with available sources | 151 # user is not yet registered, try to authenticate with available sources |
137 attrs = AuthSource.authenticate(login, password) | 152 attrs = AuthSource.authenticate(login, password) |
138 if attrs | 153 if attrs |
207 | 222 |
208 def lock! | 223 def lock! |
209 update_attribute(:status, STATUS_LOCKED) | 224 update_attribute(:status, STATUS_LOCKED) |
210 end | 225 end |
211 | 226 |
227 # Returns true if +clear_password+ is the correct user's password, otherwise false | |
212 def check_password?(clear_password) | 228 def check_password?(clear_password) |
213 if auth_source_id.present? | 229 if auth_source_id.present? |
214 auth_source.authenticate(self.login, clear_password) | 230 auth_source.authenticate(self.login, clear_password) |
215 else | 231 else |
216 User.hash_password(clear_password) == self.hashed_password | 232 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password |
217 end | 233 end |
234 end | |
235 | |
236 # Generates a random salt and computes hashed_password for +clear_password+ | |
237 # The hashed password is stored in the following form: SHA1(salt + SHA1(password)) | |
238 def salt_password(clear_password) | |
239 self.salt = User.generate_salt | |
240 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}") | |
218 end | 241 end |
219 | 242 |
220 # Does the backend storage allow this user to change their password? | 243 # Does the backend storage allow this user to change their password? |
221 def change_password_allowed? | 244 def change_password_allowed? |
222 return true if auth_source_id.blank? | 245 return true if auth_source_id.blank? |
269 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty? | 292 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty? |
270 @notified_projects_ids = nil | 293 @notified_projects_ids = nil |
271 notified_projects_ids | 294 notified_projects_ids |
272 end | 295 end |
273 | 296 |
297 def valid_notification_options | |
298 self.class.valid_notification_options(self) | |
299 end | |
300 | |
274 # Only users that belong to more than 1 project can select projects for which they are notified | 301 # Only users that belong to more than 1 project can select projects for which they are notified |
275 def valid_notification_options | 302 def self.valid_notification_options(user=nil) |
276 # Note that @user.membership.size would fail since AR ignores | 303 # Note that @user.membership.size would fail since AR ignores |
277 # :include association option when doing a count | 304 # :include association option when doing a count |
278 if memberships.length < 1 | 305 if user.nil? || user.memberships.length < 1 |
279 MAIL_NOTIFICATION_OPTIONS.delete_if {|option| option.first == :selected} | 306 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'} |
280 else | 307 else |
281 MAIL_NOTIFICATION_OPTIONS | 308 MAIL_NOTIFICATION_OPTIONS |
282 end | 309 end |
283 end | 310 end |
284 | 311 |
354 # Return true if the user is a member of project | 381 # Return true if the user is a member of project |
355 def member_of?(project) | 382 def member_of?(project) |
356 !roles_for_project(project).detect {|role| role.member?}.nil? | 383 !roles_for_project(project).detect {|role| role.member?}.nil? |
357 end | 384 end |
358 | 385 |
386 # Returns a hash of user's projects grouped by roles | |
387 def projects_by_role | |
388 return @projects_by_role if @projects_by_role | |
389 | |
390 @projects_by_role = Hash.new {|h,k| h[k]=[]} | |
391 memberships.each do |membership| | |
392 membership.roles.each do |role| | |
393 @projects_by_role[role] << membership.project if membership.project | |
394 end | |
395 end | |
396 @projects_by_role.each do |role, projects| | |
397 projects.uniq! | |
398 end | |
399 | |
400 @projects_by_role | |
401 end | |
402 | |
359 # Return true if the user is allowed to do the specified action on a specific context | 403 # Return true if the user is allowed to do the specified action on a specific context |
360 # Action can be: | 404 # Action can be: |
361 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') | 405 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') |
362 # * a permission Symbol (eg. :edit_project) | 406 # * a permission Symbol (eg. :edit_project) |
363 # Context can be: | 407 # Context can be: |
364 # * a project : returns true if user is allowed to do the specified action on this project | 408 # * a project : returns true if user is allowed to do the specified action on this project |
365 # * a group of projects : returns true if user is allowed on every project | 409 # * an array of projects : returns true if user is allowed on every project |
366 # * nil with options[:global] set : check if user has at least one role allowed for this action, | 410 # * nil with options[:global] set : check if user has at least one role allowed for this action, |
367 # or falls back to Non Member / Anonymous permissions depending if the user is logged | 411 # or falls back to Non Member / Anonymous permissions depending if the user is logged |
368 def allowed_to?(action, context, options={}) | 412 def allowed_to?(action, context, options={}, &block) |
369 if context && context.is_a?(Project) | 413 if context && context.is_a?(Project) |
370 # No action allowed on archived projects | 414 # No action allowed on archived projects |
371 return false unless context.active? | 415 return false unless context.active? |
372 # No action allowed on disabled modules | 416 # No action allowed on disabled modules |
373 return false unless context.allows_to?(action) | 417 return false unless context.allows_to?(action) |
374 # Admin users are authorized for anything else | 418 # Admin users are authorized for anything else |
375 return true if admin? | 419 return true if admin? |
376 | 420 |
377 roles = roles_for_project(context) | 421 roles = roles_for_project(context) |
378 return false unless roles | 422 return false unless roles |
379 roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)} | 423 roles.detect {|role| |
380 | 424 (context.is_public? || role.member?) && |
425 role.allowed_to?(action) && | |
426 (block_given? ? yield(role, self) : true) | |
427 } | |
381 elsif context && context.is_a?(Array) | 428 elsif context && context.is_a?(Array) |
382 # Authorize if user is authorized on every element of the array | 429 # Authorize if user is authorized on every element of the array |
383 context.map do |project| | 430 context.map do |project| |
384 allowed_to?(action,project,options) | 431 allowed_to?(action, project, options, &block) |
385 end.inject do |memo,allowed| | 432 end.inject do |memo,allowed| |
386 memo && allowed | 433 memo && allowed |
387 end | 434 end |
388 elsif options[:global] | 435 elsif options[:global] |
389 # Admin users are always authorized | 436 # Admin users are always authorized |
390 return true if admin? | 437 return true if admin? |
391 | 438 |
392 # authorize if user has at least one role that has this permission | 439 # authorize if user has at least one role that has this permission |
393 roles = memberships.collect {|m| m.roles}.flatten.uniq | 440 roles = memberships.collect {|m| m.roles}.flatten.uniq |
394 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action)) | 441 roles << (self.logged? ? Role.non_member : Role.anonymous) |
442 roles.detect {|role| | |
443 role.allowed_to?(action) && | |
444 (block_given? ? yield(role, self) : true) | |
445 } | |
395 else | 446 else |
396 false | 447 false |
397 end | 448 end |
398 end | 449 end |
399 | 450 |
400 # Is the user allowed to do the specified action on any project? | 451 # Is the user allowed to do the specified action on any project? |
401 # See allowed_to? for the actions and valid options. | 452 # See allowed_to? for the actions and valid options. |
402 def allowed_to_globally?(action, options) | 453 def allowed_to_globally?(action, options, &block) |
403 allowed_to?(action, nil, options.reverse_merge(:global => true)) | 454 allowed_to?(action, nil, options.reverse_merge(:global => true), &block) |
404 end | 455 end |
456 | |
457 safe_attributes 'login', | |
458 'firstname', | |
459 'lastname', | |
460 'mail', | |
461 'mail_notification', | |
462 'language', | |
463 'custom_field_values', | |
464 'custom_fields', | |
465 'identity_url' | |
466 | |
467 safe_attributes 'status', | |
468 'auth_source_id', | |
469 :if => lambda {|user, current_user| current_user.admin?} | |
470 | |
471 safe_attributes 'group_ids', | |
472 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?} | |
405 | 473 |
406 # Utility method to help check if a user should be notified about an | 474 # Utility method to help check if a user should be notified about an |
407 # event. | 475 # event. |
408 # | 476 # |
409 # TODO: only supports Issue events currently | 477 # TODO: only supports Issue events currently |
410 def notify_about?(object) | 478 def notify_about?(object) |
411 case mail_notification.to_sym | 479 case mail_notification |
412 when :all | 480 when 'all' |
413 true | 481 true |
414 when :selected | 482 when 'selected' |
415 # Handled by the Project | 483 # user receives notifications for created/assigned issues on unselected projects |
416 when :none | |
417 false | |
418 when :only_my_events | |
419 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self) | 484 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self) |
420 true | 485 true |
421 else | 486 else |
422 false | 487 false |
423 end | 488 end |
424 when :only_assigned | 489 when 'none' |
490 false | |
491 when 'only_my_events' | |
492 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self) | |
493 true | |
494 else | |
495 false | |
496 end | |
497 when 'only_assigned' | |
425 if object.is_a?(Issue) && object.assigned_to == self | 498 if object.is_a?(Issue) && object.assigned_to == self |
426 true | 499 true |
427 else | 500 else |
428 false | 501 false |
429 end | 502 end |
430 when :only_owner | 503 when 'only_owner' |
431 if object.is_a?(Issue) && object.author == self | 504 if object.is_a?(Issue) && object.author == self |
432 true | 505 true |
433 else | 506 else |
434 false | 507 false |
435 end | 508 end |
454 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0) | 527 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0) |
455 raise 'Unable to create the anonymous user.' if anonymous_user.new_record? | 528 raise 'Unable to create the anonymous user.' if anonymous_user.new_record? |
456 end | 529 end |
457 anonymous_user | 530 anonymous_user |
458 end | 531 end |
532 | |
533 # Salts all existing unsalted passwords | |
534 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password)) | |
535 # This method is used in the SaltPasswords migration and is to be kept as is | |
536 def self.salt_unsalted_passwords! | |
537 transaction do | |
538 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user| | |
539 next if user.hashed_password.blank? | |
540 salt = User.generate_salt | |
541 hashed_password = User.hash_password("#{salt}#{user.hashed_password}") | |
542 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] ) | |
543 end | |
544 end | |
545 end | |
459 | 546 |
460 protected | 547 protected |
461 | 548 |
462 def validate | 549 def validate |
463 # Password length validation based on setting | 550 # Password length validation based on setting |
465 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i) | 552 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i) |
466 end | 553 end |
467 end | 554 end |
468 | 555 |
469 private | 556 private |
557 | |
558 # Removes references that are not handled by associations | |
559 # Things that are not deleted are reassociated with the anonymous user | |
560 def remove_references_before_destroy | |
561 return if self.id.nil? | |
562 | |
563 substitute = User.anonymous | |
564 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] | |
565 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] | |
566 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] | |
567 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id] | |
568 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] | |
569 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s] | |
570 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s] | |
571 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] | |
572 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] | |
573 # Remove private queries and keep public ones | |
574 Query.delete_all ['user_id = ? AND is_public = ?', id, false] | |
575 Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] | |
576 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] | |
577 Token.delete_all ['user_id = ?', id] | |
578 Watcher.delete_all ['user_id = ?', id] | |
579 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] | |
580 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] | |
581 end | |
470 | 582 |
471 # Return password digest | 583 # Return password digest |
472 def self.hash_password(clear_password) | 584 def self.hash_password(clear_password) |
473 Digest::SHA1.hexdigest(clear_password || "") | 585 Digest::SHA1.hexdigest(clear_password || "") |
474 end | 586 end |
587 | |
588 # Returns a 128bits random salt as a hex string (32 chars long) | |
589 def self.generate_salt | |
590 ActiveSupport::SecureRandom.hex(16) | |
591 end | |
592 | |
475 end | 593 end |
476 | 594 |
477 class AnonymousUser < User | 595 class AnonymousUser < User |
478 | 596 |
479 def validate_on_create | 597 def validate_on_create |
490 def admin; false end | 608 def admin; false end |
491 def name(*args); I18n.t(:label_user_anonymous) end | 609 def name(*args); I18n.t(:label_user_anonymous) end |
492 def mail; nil end | 610 def mail; nil end |
493 def time_zone; nil end | 611 def time_zone; nil end |
494 def rss_key; nil end | 612 def rss_key; nil end |
613 | |
614 # Anonymous user can not be destroyed | |
615 def destroy | |
616 false | |
617 end | |
495 end | 618 end |