fat_free_crm 0.11.1 → 0.11.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of fat_free_crm might be problematic. Click here for more details.

Files changed (179) hide show
  1. data/Gemfile +30 -12
  2. data/Gemfile.lock +131 -119
  3. data/Procfile +1 -1
  4. data/README.md +1 -1
  5. data/app/assets/images/notifications.png +0 -0
  6. data/app/assets/javascripts/application.js.erb +3 -0
  7. data/app/assets/javascripts/crm_textarea_autocomplete.js +44 -0
  8. data/app/assets/stylesheets/application.css.erb +2 -0
  9. data/app/assets/stylesheets/common.scss +7 -11
  10. data/app/assets/stylesheets/textarea_autocomplete.scss +42 -0
  11. data/app/controllers/admin/application_controller.rb +5 -5
  12. data/app/controllers/admin/field_groups_controller.rb +11 -51
  13. data/app/controllers/admin/fields_controller.rb +13 -59
  14. data/app/controllers/admin/plugins_controller.rb +1 -4
  15. data/app/controllers/admin/settings_controller.rb +0 -4
  16. data/app/controllers/admin/tags_controller.rb +11 -66
  17. data/app/controllers/admin/users_controller.rb +20 -83
  18. data/app/controllers/application_controller.rb +83 -69
  19. data/app/controllers/comments_controller.rb +12 -29
  20. data/app/controllers/emails_controller.rb +1 -5
  21. data/app/controllers/entities/accounts_controller.rb +13 -32
  22. data/app/controllers/entities/campaigns_controller.rb +17 -32
  23. data/app/controllers/entities/contacts_controller.rb +20 -38
  24. data/app/controllers/entities/leads_controller.rb +33 -55
  25. data/app/controllers/entities/opportunities_controller.rb +26 -42
  26. data/app/controllers/entities_controller.rb +92 -83
  27. data/app/controllers/home_controller.rb +1 -10
  28. data/app/controllers/lists_controller.rb +1 -4
  29. data/app/controllers/{entities/tasks_controller.rb → tasks_controller.rb} +21 -32
  30. data/app/controllers/users_controller.rb +6 -5
  31. data/app/helpers/accounts_helper.rb +32 -9
  32. data/app/helpers/application_helper.rb +15 -1
  33. data/app/helpers/campaigns_helper.rb +1 -1
  34. data/app/helpers/comments_helper.rb +11 -1
  35. data/app/helpers/leads_helper.rb +1 -1
  36. data/app/helpers/opportunities_helper.rb +1 -1
  37. data/app/{models/mailers/notifier.rb → mailers/dropbox_mailer.rb} +5 -16
  38. data/app/mailers/subscription_mailer.rb +37 -0
  39. data/{lib/tasks/dropbox.rake → app/mailers/user_mailer.rb} +11 -13
  40. data/app/models/entities/account.rb +3 -1
  41. data/app/models/entities/campaign.rb +3 -1
  42. data/app/models/entities/contact.rb +3 -1
  43. data/app/models/entities/lead.rb +6 -5
  44. data/app/models/entities/opportunity.rb +3 -1
  45. data/app/models/fields/field.rb +1 -1
  46. data/app/models/polymorphic/comment.rb +34 -0
  47. data/app/models/{entities → polymorphic}/task.rb +16 -3
  48. data/app/models/setting.rb +15 -15
  49. data/app/models/users/ability.rb +12 -5
  50. data/app/models/users/user.rb +7 -2
  51. data/app/views/accounts/index.html.haml +1 -1
  52. data/app/views/accounts/index.js.rjs +1 -1
  53. data/app/views/admin/plugins/index.html.haml +1 -7
  54. data/app/views/{shared/auto_complete.html.haml → application/_auto_complete.html.haml} +0 -0
  55. data/app/views/{shared → application}/index.atom.builder +1 -1
  56. data/app/views/{shared → application}/index.rss.builder +1 -1
  57. data/app/views/campaigns/index.html.haml +1 -1
  58. data/app/views/campaigns/index.js.rjs +1 -1
  59. data/app/views/comments/_new.html.haml +6 -0
  60. data/app/views/comments/_subscription_links.html.haml +13 -0
  61. data/app/views/comments/new.js.rjs +2 -0
  62. data/app/views/contacts/_top_section.html.haml +3 -13
  63. data/app/views/contacts/index.html.haml +1 -1
  64. data/app/views/contacts/index.js.rjs +1 -1
  65. data/app/views/{notifier/dropbox_ack_notification.html.haml → dropbox_mailer/dropbox_notification.html.haml} +2 -2
  66. data/app/views/{shared → entities}/attach.js.rjs +1 -1
  67. data/app/views/entities/contacts.js.rjs +1 -1
  68. data/app/views/{shared/discard.rjs → entities/discard.js.rjs} +0 -0
  69. data/app/views/entities/leads.js.rjs +1 -1
  70. data/app/views/entities/opportunities.js.rjs +1 -1
  71. data/app/views/entities/subscription_update.js.rjs +4 -0
  72. data/app/views/entities/versions.js.rjs +1 -1
  73. data/app/views/layouts/_footer.html.haml +1 -1
  74. data/app/views/layouts/application.html.haml +3 -0
  75. data/app/views/leads/_contact.html.haml +1 -0
  76. data/app/views/leads/index.html.haml +1 -1
  77. data/app/views/leads/index.js.rjs +1 -1
  78. data/app/views/opportunities/_top_section.html.haml +4 -14
  79. data/app/views/opportunities/index.html.haml +1 -1
  80. data/app/views/opportunities/index.js.rjs +1 -1
  81. data/app/views/subscription_mailer/comment_notification.text.erb +7 -0
  82. data/app/views/{notifier → user_mailer}/password_reset_instructions.html.haml +0 -0
  83. data/config/application.rb +3 -1
  84. data/config/environments/development.rb +1 -1
  85. data/config/environments/test.rb +3 -0
  86. data/config/initializers/action_mailer.rb +8 -5
  87. data/config/initializers/cancan.rb +151 -0
  88. data/config/initializers/constants.rb +1 -0
  89. data/config/initializers/locale.rb +20 -0
  90. data/config/initializers/paper_trail.rb +4 -5
  91. data/config/initializers/relative_url_root.rb +0 -1
  92. data/config/initializers/squeel.rb +5 -0
  93. data/config/locales/cz_fat_free_crm.yml +3 -3
  94. data/config/locales/de.yml +2 -2
  95. data/config/locales/de_fat_free_crm.yml +651 -596
  96. data/config/locales/en-GB_fat_free_crm.yml +3 -3
  97. data/config/locales/en-US_fat_free_crm.yml +13 -3
  98. data/config/locales/es_fat_free_crm.yml +3 -3
  99. data/config/locales/fr-CA_fat_free_crm.yml +3 -3
  100. data/config/locales/fr_fat_free_crm.yml +3 -3
  101. data/config/locales/it_fat_free_crm.yml +3 -3
  102. data/config/locales/pl_fat_free_crm.yml +3 -3
  103. data/config/locales/pt-BR_fat_free_crm.yml +3 -3
  104. data/config/locales/ru_fat_free_crm.yml +3 -3
  105. data/config/locales/sv-SE_fat_free_crm.yml +3 -3
  106. data/config/locales/th_fat_free_crm.yml +3 -3
  107. data/config/routes.rb +10 -0
  108. data/config/settings.default.yml +29 -10
  109. data/config/unicorn.rb +4 -0
  110. data/db/migrate/20111201030535_add_field_groups_klass_name.rb +3 -1
  111. data/db/migrate/20120314080441_add_subscribed_users_to_entities.rb +23 -0
  112. data/db/migrate/20120405080727_change_subscribed_users_to_set.rb +24 -0
  113. data/db/migrate/20120405080742_change_further_subscribed_users_to_set.rb +27 -0
  114. data/db/migrate/20120413034923_add_index_on_versions_item_type.rb +5 -0
  115. data/db/schema.rb +109 -126
  116. data/fat_free_crm.gemspec +12 -18
  117. data/lib/fat_free_crm.rb +0 -1
  118. data/lib/fat_free_crm/core_ext/array.rb +1 -0
  119. data/lib/fat_free_crm/gem_dependencies.rb +1 -0
  120. data/lib/fat_free_crm/mail_processor/base.rb +226 -0
  121. data/lib/fat_free_crm/mail_processor/comment_replies.rb +86 -0
  122. data/lib/fat_free_crm/mail_processor/dropbox.rb +288 -0
  123. data/lib/fat_free_crm/permissions.rb +6 -19
  124. data/lib/fat_free_crm/renderers.rb +0 -8
  125. data/lib/fat_free_crm/tabs.rb +1 -1
  126. data/lib/fat_free_crm/version.rb +1 -1
  127. data/lib/plugins/country_select/lib/country_select.rb +2 -2
  128. data/lib/tasks/mail_processing.rake +60 -0
  129. data/spec/controllers/admin/users_controller_spec.rb +0 -2
  130. data/spec/controllers/{accounts_controller_spec.rb → entities/accounts_controller_spec.rb} +7 -9
  131. data/spec/controllers/{campaigns_controller_spec.rb → entities/campaigns_controller_spec.rb} +7 -7
  132. data/spec/controllers/{contacts_controller_spec.rb → entities/contacts_controller_spec.rb} +5 -9
  133. data/spec/controllers/{leads_controller_spec.rb → entities/leads_controller_spec.rb} +7 -9
  134. data/spec/controllers/{opportunities_controller_spec.rb → entities/opportunities_controller_spec.rb} +8 -15
  135. data/spec/controllers/tasks_controller_spec.rb +1 -5
  136. data/spec/controllers/users_controller_spec.rb +5 -9
  137. data/spec/factories/subscription_factories.rb +6 -0
  138. data/spec/lib/mail_processor/base_spec.rb +164 -0
  139. data/spec/lib/mail_processor/comment_replies_spec.rb +63 -0
  140. data/spec/lib/{dropbox_spec.rb → mail_processor/dropbox_spec.rb} +73 -181
  141. data/spec/lib/mail_processor/sample_emails/dropbox.rb +167 -0
  142. data/spec/mailers/subscription_mailer_spec.rb +17 -0
  143. data/spec/models/{base → entities}/account_contact_spec.rb +0 -0
  144. data/spec/models/{base → entities}/account_opportunity_spec.rb +0 -0
  145. data/spec/models/{base → entities}/account_spec.rb +4 -0
  146. data/spec/models/{base → entities}/campaign_spec.rb +4 -0
  147. data/spec/models/{base → entities}/contact_opportunity_spec.rb +0 -0
  148. data/spec/models/{base → entities}/contact_spec.rb +4 -0
  149. data/spec/models/{base → entities}/lead_spec.rb +4 -0
  150. data/spec/models/{base → entities}/opportunity_spec.rb +4 -0
  151. data/spec/models/polymorphic/comment_spec.rb +15 -0
  152. data/spec/models/{base → polymorphic}/task_spec.rb +124 -30
  153. data/spec/models/polymorphic/version_spec.rb +1 -1
  154. data/spec/shared/controllers.rb +5 -7
  155. data/spec/shared/models.rb +46 -0
  156. data/spec/spec_helper.rb +3 -4
  157. data/spec/support/mail_processor_mocks.rb +30 -0
  158. data/spec/support/uploaded_file.rb +3 -0
  159. data/spec/views/{common → application}/auto_complete.haml_spec.rb +1 -1
  160. data/vendor/assets/images/jquery-ui/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  161. data/vendor/assets/images/jquery-ui/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  162. data/vendor/assets/images/jquery-ui/ui-bg_flat_10_000000_40x100.png +0 -0
  163. data/vendor/assets/images/jquery-ui/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  164. data/vendor/assets/images/jquery-ui/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  165. data/vendor/assets/images/jquery-ui/ui-bg_glass_65_ffffff_1x400.png +0 -0
  166. data/vendor/assets/images/jquery-ui/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  167. data/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  168. data/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  169. data/vendor/assets/images/jquery-ui/ui-icons_222222_256x240.png +0 -0
  170. data/vendor/assets/images/jquery-ui/ui-icons_228ef1_256x240.png +0 -0
  171. data/vendor/assets/images/jquery-ui/ui-icons_ef8c08_256x240.png +0 -0
  172. data/vendor/assets/images/jquery-ui/ui-icons_ffd27a_256x240.png +0 -0
  173. data/vendor/assets/images/jquery-ui/ui-icons_ffffff_256x240.png +0 -0
  174. data/vendor/assets/javascripts/textarea_autocomplete.js +605 -0
  175. data/vendor/assets/stylesheets/jquery-ui.custom.css.erb +565 -0
  176. metadata +234 -154
  177. data/config/locales/simple_form.en.yml +0 -24
  178. data/lib/fat_free_crm/dropbox.rb +0 -439
  179. data/spec/lib/dropbox/email_samples.rb +0 -77
@@ -49,6 +49,8 @@ class Account < ActiveRecord::Base
49
49
  has_one :shipping_address, :dependent => :destroy, :as => :addressable, :class_name => "Address", :conditions => "address_type = 'Shipping'"
50
50
  has_many :emails, :as => :mediator
51
51
 
52
+ serialize :subscribed_users, Set
53
+
52
54
  accepts_nested_attributes_for :billing_address, :allow_destroy => true
53
55
  accepts_nested_attributes_for :shipping_address, :allow_destroy => true
54
56
 
@@ -66,7 +68,7 @@ class Account < ActiveRecord::Base
66
68
  uses_user_permissions
67
69
  acts_as_commentable
68
70
  acts_as_taggable_on :tags
69
- has_paper_trail
71
+ has_paper_trail :ignore => [ :subscribed_users ]
70
72
  has_fields
71
73
  exportable
72
74
  sortable :by => [ "name ASC", "rating DESC", "created_at DESC", "updated_at DESC" ], :default => "created_at DESC"
@@ -49,6 +49,8 @@ class Campaign < ActiveRecord::Base
49
49
  has_many :opportunities, :dependent => :destroy, :order => "id DESC"
50
50
  has_many :emails, :as => :mediator
51
51
 
52
+ serialize :subscribed_users, Set
53
+
52
54
  scope :state, lambda { |filters|
53
55
  where('status IN (?)' + (filters.delete('other') ? ' OR status IS NULL' : ''), filters)
54
56
  }
@@ -63,7 +65,7 @@ class Campaign < ActiveRecord::Base
63
65
  uses_user_permissions
64
66
  acts_as_commentable
65
67
  acts_as_taggable_on :tags
66
- has_paper_trail
68
+ has_paper_trail :ignore => [ :subscribed_users ]
67
69
  has_fields
68
70
  exportable
69
71
  sortable :by => [ "name ASC", "target_leads DESC", "target_revenue DESC", "leads_count DESC", "revenue DESC", "starts_on DESC", "ends_on DESC", "created_at DESC", "updated_at DESC" ], :default => "created_at DESC"
@@ -60,6 +60,8 @@ class Contact < ActiveRecord::Base
60
60
  has_one :business_address, :dependent => :destroy, :as => :addressable, :class_name => "Address", :conditions => "address_type = 'Business'"
61
61
  has_many :emails, :as => :mediator
62
62
 
63
+ serialize :subscribed_users, Set
64
+
63
65
  accepts_nested_attributes_for :business_address, :allow_destroy => true
64
66
 
65
67
  scope :created_by, lambda { |user| { :conditions => [ "user_id = ?", user.id ] } }
@@ -82,7 +84,7 @@ class Contact < ActiveRecord::Base
82
84
  uses_user_permissions
83
85
  acts_as_commentable
84
86
  acts_as_taggable_on :tags
85
- has_paper_trail
87
+ has_paper_trail :ignore => [ :subscribed_users ]
86
88
  has_fields
87
89
  exportable
88
90
  sortable :by => [ "first_name ASC", "last_name ASC", "created_at DESC", "updated_at DESC" ], :default => "created_at DESC"
@@ -57,6 +57,8 @@ class Lead < ActiveRecord::Base
57
57
  has_one :business_address, :dependent => :destroy, :as => :addressable, :class_name => "Address", :conditions => "address_type='Business'"
58
58
  has_many :emails, :as => :mediator
59
59
 
60
+ serialize :subscribed_users, Set
61
+
60
62
  accepts_nested_attributes_for :business_address, :allow_destroy => true
61
63
 
62
64
  scope :state, lambda { |filters|
@@ -75,7 +77,7 @@ class Lead < ActiveRecord::Base
75
77
  uses_user_permissions
76
78
  acts_as_commentable
77
79
  acts_as_taggable_on :tags
78
- has_paper_trail
80
+ has_paper_trail :ignore => [ :subscribed_users ]
79
81
  has_fields
80
82
  exportable
81
83
  sortable :by => [ "first_name ASC", "last_name ASC", "company ASC", "rating DESC", "created_at DESC", "updated_at DESC" ], :default => "created_at DESC"
@@ -125,7 +127,7 @@ class Lead < ActiveRecord::Base
125
127
  opportunity = Opportunity.create_for(self, account, params[:opportunity], params[:users])
126
128
  contact = Contact.create_for(self, account, opportunity, params)
127
129
 
128
- return account, opportunity, contact
130
+ [account, opportunity, contact]
129
131
  end
130
132
 
131
133
  #----------------------------------------------------------------------------
@@ -162,7 +164,8 @@ class Lead < ActiveRecord::Base
162
164
  end
163
165
  alias :name :full_name
164
166
 
165
- private
167
+ private
168
+
166
169
  #----------------------------------------------------------------------------
167
170
  def increment_leads_count
168
171
  if self.campaign_id
@@ -182,6 +185,4 @@ class Lead < ActiveRecord::Base
182
185
  def users_for_shared_access
183
186
  errors.add(:access, :share_lead) if self[:access] == "Shared" && !self.permissions.any?
184
187
  end
185
-
186
188
  end
187
-
@@ -48,6 +48,8 @@ class Opportunity < ActiveRecord::Base
48
48
  has_many :tasks, :as => :asset, :dependent => :destroy#, :order => 'created_at DESC'
49
49
  has_many :emails, :as => :mediator
50
50
 
51
+ serialize :subscribed_users, Set
52
+
51
53
  scope :state, lambda { |filters|
52
54
  where('stage IN (?)' + (filters.delete('other') ? ' OR stage IS NULL' : ''), filters)
53
55
  }
@@ -71,7 +73,7 @@ class Opportunity < ActiveRecord::Base
71
73
  uses_user_permissions
72
74
  acts_as_commentable
73
75
  acts_as_taggable_on :tags
74
- has_paper_trail
76
+ has_paper_trail :ignore => [ :subscribed_users ]
75
77
  has_fields
76
78
  exportable
77
79
  sortable :by => [ "name ASC", "amount DESC", "amount*probability DESC", "probability DESC", "closes_on ASC", "created_at DESC", "updated_at DESC" ], :default => "created_at DESC"
@@ -112,7 +112,7 @@ class Field < ActiveRecord::Base
112
112
  when 'date'
113
113
  value && value.strftime(I18n.t("date.formats.default"))
114
114
  when 'datetime'
115
- value && value.strftime(I18n.t("time.formats.short"))
115
+ value && value.strftime(I18n.t("time.formats.long"))
116
116
  when 'check_boxes'
117
117
  value = YAML.load(value) if value.is_a?(String)
118
118
  value.select(&:present?).in_groups_of(2, false).map {|g| g.join(', ')}.join("<br />".html_safe) if Array === value
@@ -41,6 +41,40 @@ class Comment < ActiveRecord::Base
41
41
  has_paper_trail :meta => { :related => :commentable },
42
42
  :ignore => [:state]
43
43
 
44
+ before_create :subscribe_mentioned_users
45
+ after_create :subscribe_user_to_entity, :notify_subscribers
46
+
44
47
  def expanded?; self.state == "Expanded"; end
45
48
  def collapsed?; self.state == "Collapsed"; end
49
+
50
+ private
51
+ # Add user to subscribed_users field on entity
52
+ def subscribe_user_to_entity(u = user)
53
+ commentable.subscribed_users << u.id
54
+ commentable.save
55
+ end
56
+
57
+ # Notify subscribed users when a comment is added, unless user created this comment
58
+ def notify_subscribers
59
+ commentable.subscribed_users.reject{|user_id| user_id == user.id}.each do |subscriber_id|
60
+ if subscriber = User.find_by_id(subscriber_id)
61
+ # Only send email if SMTP settings are configured
62
+ if Rails.application.config.action_mailer.smtp_settings.present?
63
+ SubscriptionMailer.comment_notification(subscriber, self).deliver
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ # If a user is mentioned in the comment body, subscribe them to the entity
70
+ # before creation, so that they are sent an email notification
71
+ def subscribe_mentioned_users
72
+ # Scan for usernames mentioned in the comment,
73
+ # e.g. "Hi @example_user, take a look at this lead. Please show @another_user"
74
+ comment.scan(/@([a-zA-Z0-9_-]+)/).map(&:first).each do |username|
75
+ if (mentioned_user = User.find_by_username(username))
76
+ subscribe_user_to_entity(mentioned_user)
77
+ end
78
+ end
79
+ end
46
80
  end
@@ -45,6 +45,8 @@ class Task < ActiveRecord::Base
45
45
  belongs_to :completor, :class_name => "User", :foreign_key => :completed_by
46
46
  belongs_to :asset, :polymorphic => true
47
47
 
48
+ serialize :subscribed_users, Set
49
+
48
50
  # Tasks created by the user for herself, or assigned to her by others. That's
49
51
  # what gets shown on Tasks/Pending and Tasks/Completed pages.
50
52
  scope :my, lambda { |*args|
@@ -97,7 +99,7 @@ class Task < ActiveRecord::Base
97
99
  }
98
100
 
99
101
  acts_as_commentable
100
- has_paper_trail :meta => { :related => :asset }
102
+ has_paper_trail :meta => { :related => :asset }, :ignore => [ :subscribed_users ]
101
103
  has_fields
102
104
  exportable
103
105
 
@@ -231,10 +233,21 @@ class Task < ActiveRecord::Base
231
233
  rescue ArgumentError
232
234
  errors.add(:calendar, :invalid_date)
233
235
  end
234
-
236
+
235
237
  #----------------------------------------------------------------------------
236
238
  def parse_calendar_date
237
- DateTime.strptime(self.calendar, I18n.t(Setting.task_calendar_with_time ? 'time.formats.mmddyyyy_hhmm' : 'date.formats.mmddyyyy')).utc
239
+ translate_month_and_day_names!(self.calendar) unless I18n.locale == :"en-US"
240
+
241
+ DateTime.strptime(self.calendar,
242
+ I18n.t(Setting.task_calendar_with_time ? 'time.formats.mmddyyyy_hhmm' : 'date.formats.mmddyyyy')).utc
243
+ end
244
+
245
+ # Translates month and day names of a given datetime string.
246
+ #----------------------------------------------------------------------------
247
+ def translate_month_and_day_names!(date_string)
248
+ translated = I18n.t([:month_names, :abbr_month_names, :day_names, :abbr_day_names], :scope => :date).flatten.compact
249
+ original = (Date::MONTHNAMES + Date::ABBR_MONTHNAMES + Date::DAYNAMES + Date::ABBR_DAYNAMES).compact
250
+ translated.each_with_index { |name, i| date_string.gsub!(name, original[i]) }
238
251
  end
239
252
  end
240
253
 
@@ -27,7 +27,7 @@
27
27
  #
28
28
 
29
29
  # Fat Free CRM settings are stored in three places, and are loaded in the following order:
30
- #
30
+ #
31
31
  # 1) config/settings.default.yml
32
32
  # 2) config/settings.yml (if exists)
33
33
  # 3) 'settings' table in database (if exists)
@@ -44,16 +44,16 @@ class Setting < ActiveRecord::Base
44
44
  @@cache = @@yaml_settings = {}.with_indifferent_access
45
45
 
46
46
  class << self
47
-
47
+
48
48
  # Cache should be cleared before each request.
49
49
  def clear_cache!
50
50
  @@cache = {}.with_indifferent_access
51
51
  end
52
-
52
+
53
53
  #-------------------------------------------------------------------
54
54
  def method_missing(method, *args)
55
55
  begin
56
- super(method, *args)
56
+ super
57
57
  rescue NoMethodError
58
58
  method_name = method.to_s
59
59
  if method_name.last == "="
@@ -70,30 +70,30 @@ class Setting < ActiveRecord::Base
70
70
  # Return value if cached
71
71
  return cache[name] if cache.has_key?(name)
72
72
  # Check database
73
- if database_and_table_exists?
74
- if setting = self.find_by_name(name)
73
+ if database_and_table_exists?
74
+ if setting = self.find_by_name(name.to_s)
75
75
  unless setting.value.nil?
76
76
  return cache[name] = setting.value
77
77
  end
78
78
  end
79
79
  end
80
- # Check YAML settings
80
+ # Check YAML settings
81
81
  if yaml_settings.has_key?(name)
82
82
  return cache[name] = yaml_settings[name]
83
83
  end
84
84
  end
85
-
85
+
86
86
 
87
87
  # Set setting value
88
88
  #-------------------------------------------------------------------
89
89
  def []=(name, value)
90
90
  return nil unless database_and_table_exists?
91
- setting = self.find_by_name(name) || self.new(:name => name)
91
+ setting = self.find_by_name(name.to_s) || self.new(:name => name)
92
92
  setting.value = value
93
93
  setting.save
94
94
  cache[name] = value
95
95
  end
96
-
96
+
97
97
 
98
98
  # Unrolls [ :one, :two ] settings array into [[ "One", :one ], [ "Two", :two ]]
99
99
  # picking symbol translations from locale. If setting is not a symbol but
@@ -109,12 +109,12 @@ class Setting < ActiveRecord::Base
109
109
  # instead of crashing the entire application.
110
110
  table_exists? rescue false
111
111
  end
112
-
113
-
112
+
113
+
114
114
  # Loads settings from YAML files
115
115
  def load_settings_from_yaml
116
116
  @@yaml_settings = {}.with_indifferent_access
117
-
117
+
118
118
  setting_files = [
119
119
  FatFreeCRM.root.join("config", "settings.default.yml"),
120
120
  Rails.root.join("config", "settings.yml")
@@ -125,8 +125,8 @@ class Setting < ActiveRecord::Base
125
125
  if File.exist?(file)
126
126
  begin
127
127
  settings = YAML.load_file(file)
128
- # Merge settings into current settings hash
129
- @@yaml_settings.merge!(settings)
128
+ # Merge settings into current settings hash (recursively)
129
+ @@yaml_settings.deep_merge!(settings)
130
130
  rescue Exception => ex
131
131
  puts "Settings couldn't be loaded from #{file}: #{ex.message}"
132
132
  end
@@ -1,13 +1,20 @@
1
+ # See the wiki for details: https://github.com/ryanb/cancan/wiki/Defining-Abilities
2
+
1
3
  class Ability
2
4
  include CanCan::Ability
3
5
 
4
6
  def initialize(user)
5
- # See the wiki for details: https://github.com/ryanb/cancan/wiki/Defining-Abilities
6
- can :create, :all
7
- can [:read, :update, :destroy], :all, :access => 'Public'
8
7
  if user.present?
9
- can [:read, :update, :destroy], :all, :user_id => user.id
10
- can [:read, :update, :destroy], :all, :permissions => {:user_id => user.id}
8
+ entities = [Account, Campaign, Contact, Lead, Opportunity]
9
+
10
+ can :create, :all
11
+ can :manage, entities, :access => 'Public'
12
+ can :manage, entities + [Task], :user_id => user.id
13
+
14
+ entities.each do |klass|
15
+ permissions = user.permissions.where(:asset_type => klass.name)
16
+ can :manage, klass, :id => permissions.map(&:asset_id)
17
+ end
11
18
  end
12
19
  end
13
20
  end
@@ -80,11 +80,16 @@ class User < ActiveRecord::Base
80
80
  where('upper(username) LIKE upper(:s) OR upper(first_name) LIKE upper(:s) OR upper(last_name) LIKE upper(:s)', :s => "#{query}%")
81
81
  }
82
82
 
83
+ scope :my, lambda {
84
+ current_ability = Ability.new(User.current_user)
85
+ accessible_by(current_ability)
86
+ }
87
+
83
88
  acts_as_authentic do |c|
84
89
  c.session_class = Authentication
85
90
  c.validates_uniqueness_of_login_field_options = { :message => :username_taken }
86
91
  c.validates_length_of_login_field_options = { :minimum => 1, :message => :missing_username }
87
- c.merge_validates_format_of_login_field_options(:with => /.*/)
92
+ c.merge_validates_format_of_login_field_options(:with => /[a-zA-Z0-9_-]+/)
88
93
 
89
94
  c.validates_uniqueness_of_email_field_options = { :message => :email_in_use }
90
95
  c.validates_length_of_password_field_options = { :minimum => 0, :allow_blank => true, :if => :require_password? }
@@ -126,7 +131,7 @@ class User < ActiveRecord::Base
126
131
  #----------------------------------------------------------------------------
127
132
  def deliver_password_reset_instructions!
128
133
  reset_perishable_token!
129
- Notifier.password_reset_instructions(self).deliver
134
+ UserMailer.password_reset_instructions(self).deliver
130
135
  end
131
136
 
132
137
  # Override global I18n.locale if the user has individual local preference.
@@ -11,7 +11,7 @@
11
11
  = image_tag("loading.gif", :size => :thumb, :id => "loading", :style => "display: none;")
12
12
  .remote#options{ hidden }
13
13
  .remote#advanced_search{ hidden_if(!params[:q]) }
14
- - if @search
14
+ - if params[:q]
15
15
  = render :partial => "advanced_search"
16
16
  .remote#create_account{ hidden }
17
17
 
@@ -1,4 +1,4 @@
1
- unless @accounts.blank?
1
+ if @accounts.any?
2
2
  page[:accounts].replace_html render @accounts
3
3
  else
4
4
  page[:accounts].replace_html :partial => "shared/empty"
@@ -1,9 +1,3 @@
1
- = styles_for :plugin
2
-
3
1
  .title Plugins
4
2
 
5
- .list#plugins
6
- - if @plugins.any?
7
- = render :partial => "admin/plugins/plugin", :collection => @plugins
8
- - else
9
- = render "shared/empty"
3
+ %p #{t :not_implemented}
@@ -11,7 +11,7 @@ end
11
11
  atom_feed do |feed|
12
12
  feed.title title || t(items.to_sym)
13
13
  feed.updated assets.any? ? assets.max { |a, b| a.updated_at <=> b.updated_at }.updated_at : Time.now
14
- feed.generator "Fat Free CRM v#{FatFreeCRM::Version}"
14
+ feed.generator "Fat Free CRM v#{FatFreeCRM::VERSION::STRING}"
15
15
  feed.author do |author|
16
16
  author.name @current_user.full_name
17
17
  author.email @current_user.email
@@ -11,7 +11,7 @@ end
11
11
  xml.instruct! :xml, :version => "1.0"
12
12
  xml.rss :version => "2.0" do
13
13
  xml.channel do
14
- xml.generator "Fat Free CRM v#{FatFreeCRM::Version}"
14
+ xml.generator "Fat Free CRM v#{FatFreeCRM::VERSION::STRING}"
15
15
  xml.link send(:"#{items}_url")
16
16
  xml.pubDate Time.now.to_s(:rfc822)
17
17
  xml.title title || t(items.to_sym)
@@ -11,7 +11,7 @@
11
11
  = image_tag("loading.gif", :size => :thumb, :id => "loading", :style => "display: none;")
12
12
  .remote#options{ hidden }
13
13
  .remote#advanced_search{ hidden_if(!params[:q]) }
14
- - if @search
14
+ - if params[:q]
15
15
  = render :partial => "advanced_search"
16
16
  .remote#create_campaign{ hidden }
17
17
 
@@ -1,4 +1,4 @@
1
- unless @campaigns.blank?
1
+ if @campaigns.any?
2
2
  page[:campaigns].replace_html render @campaigns
3
3
  else
4
4
  page[:campaigns].replace_html :partial => "shared/empty"
@@ -1,6 +1,10 @@
1
1
  - class_name = commentable.class.name.downcase
2
2
  - id_prefix = "#{class_name}_#{commentable.id}"
3
3
  .comment.highlight.new_comment{ :id => "#{id_prefix}_comment_new" }
4
+ - subscribed_users = commentable.subscribed_users.map{|uid| User.find_by_id(uid) unless uid == @current_user.id }.compact
5
+ - if notification_emails_configured? && subscribed_users.any?
6
+ = t(:following_users_will_be_notified) << ":"
7
+ = subscribed_user_links(subscribed_users)
4
8
 
5
9
  -# Two hidden fields store the IDs of notes and emails shown for the asset. These IDs are used
6
10
  -# by [Expand/Collapse All]. The contents gets updated by actions such as [Add] or [Delete].
@@ -25,3 +29,5 @@
25
29
  %div{ {:id => "#{id_prefix}_ask"}.merge(hidden_if(false))}
26
30
  = text_field_tag :post_new_note, t(:add_note_help), :onclick => remote_function(:url => new_comment_path("#{class_name}_id" => commentable), :method => :get), :id => "#{id_prefix}_post_new_note"
27
31
 
32
+ - if notification_emails_configured?
33
+ = render :partial => "comments/subscription_links", :locals => {:entity => commentable}
@@ -0,0 +1,13 @@
1
+ - class_name = entity.class.name.downcase
2
+ - id_prefix = "#{class_name}_#{entity.id}"
3
+
4
+ - subscribed = entity.subscribed_users.include?(current_user.id)
5
+
6
+ - if subscribed
7
+ - link_text, action = [t(:disable_email_subscriptions), "unsubscribe"]
8
+ - else
9
+ - link_text, action = [t(:subscribe_via_email), "subscribe"]
10
+
11
+ %div{:id => "#{id_prefix}_subscribe", :class => "comment_subscriptions"}
12
+ = image_tag "notifications.png", :title => t(:notifications_tooltip)
13
+ = link_to link_text, url_for(:controller => class_name.pluralize, :action => action, :id => entity.id), :remote => true, :method => :post