fat_free_crm 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/application.js.erb +1 -0
  3. data/app/assets/javascripts/crm.js.coffee +6 -3
  4. data/app/assets/javascripts/crm_select2.js.coffee +4 -1
  5. data/app/assets/javascripts/crm_tags.js.coffee +4 -4
  6. data/app/assets/javascripts/crm_textarea_autocomplete.js.coffee +6 -9
  7. data/app/assets/javascripts/crm_validations.js.coffee +12 -0
  8. data/app/assets/stylesheets/bootstrap-custom.scss +3 -3
  9. data/app/assets/stylesheets/common.scss +9 -0
  10. data/app/assets/stylesheets/rails.scss +1 -1
  11. data/app/controllers/admin/fields_controller.rb +16 -13
  12. data/app/controllers/application_controller.rb +1 -1
  13. data/app/controllers/emails_controller.rb +1 -1
  14. data/app/controllers/entities/contacts_controller.rb +1 -1
  15. data/app/controllers/entities_controller.rb +1 -1
  16. data/app/controllers/lists_controller.rb +5 -4
  17. data/app/controllers/users_controller.rb +15 -3
  18. data/app/helpers/application_helper.rb +5 -5
  19. data/app/helpers/leads_helper.rb +1 -1
  20. data/app/helpers/opportunities_helper.rb +5 -3
  21. data/app/helpers/users_helper.rb +1 -1
  22. data/app/models/entities/campaign.rb +3 -3
  23. data/app/models/fields/custom_field_pair.rb +6 -7
  24. data/app/models/fields/field.rb +1 -3
  25. data/app/models/list.rb +1 -1
  26. data/app/models/observers/lead_observer.rb +2 -1
  27. data/app/models/observers/task_observer.rb +2 -1
  28. data/app/models/polymorphic/address.rb +2 -1
  29. data/app/models/polymorphic/comment.rb +3 -4
  30. data/app/models/polymorphic/task.rb +1 -1
  31. data/app/models/users/user.rb +7 -1
  32. data/app/views/accounts/_contact_info.html.haml +1 -1
  33. data/app/views/accounts/_top_section.html.haml +3 -3
  34. data/app/views/accounts/show.js.haml +1 -1
  35. data/app/views/accounts/update.js.haml +1 -0
  36. data/app/views/admin/custom_fields/_check_boxes_field.html.haml +4 -1
  37. data/app/views/admin/custom_fields/_date_pair_field.html.haml +1 -1
  38. data/app/views/campaigns/_top_section.html.haml +2 -2
  39. data/app/views/campaigns/show.js.haml +1 -1
  40. data/app/views/campaigns/update.js.haml +1 -0
  41. data/app/views/contacts/_top_section.html.haml +5 -5
  42. data/app/views/contacts/show.js.haml +2 -3
  43. data/app/views/contacts/update.js.haml +1 -0
  44. data/app/views/devise/sessions/new.html.haml +4 -5
  45. data/app/views/fields/_group.html.haml +5 -2
  46. data/app/views/fields/_group_table.html.haml +4 -5
  47. data/app/views/fields/_group_view.html.haml +4 -1
  48. data/app/views/fields/_sidebar_show.html.haml +5 -8
  49. data/app/views/fields/group.js.erb +3 -1
  50. data/app/views/layouts/application.html.haml +2 -4
  51. data/app/views/leads/_top_section.html.haml +4 -4
  52. data/app/views/leads/show.js.haml +1 -1
  53. data/app/views/leads/update.js.haml +1 -0
  54. data/app/views/opportunities/_top_section.html.haml +2 -2
  55. data/app/views/opportunities/show.js.haml +1 -1
  56. data/app/views/opportunities/update.js.haml +1 -0
  57. data/app/views/shared/_add_comment.html.haml +1 -1
  58. data/app/views/shared/_address.html.haml +1 -1
  59. data/app/views/tasks/_top_section.html.haml +4 -4
  60. data/config/application.rb +2 -0
  61. data/config/database.yml +9 -12
  62. data/config/environments/development.rb +15 -0
  63. data/config/initializers/action_mailer.rb +1 -1
  64. data/config/initializers/application_controller_renderer.rb +1 -0
  65. data/config/initializers/backtrace_silencers.rb +1 -0
  66. data/config/initializers/content_security_policy.rb +1 -0
  67. data/config/initializers/custom_field_ransack_translations.rb +2 -2
  68. data/config/initializers/devise.rb +1 -0
  69. data/config/initializers/inflections.rb +1 -0
  70. data/config/initializers/permissions_policy.rb +1 -0
  71. data/config/routes.rb +1 -3
  72. data/config/settings.default.yml +2 -1
  73. data/db/fat_free_crm_development.sqlite3 +0 -0
  74. data/db/fat_free_crm_test.sqlite3 +0 -0
  75. data/db/migrate/20230422234321_optionally_create_action_text_tables.action_text.rb +29 -0
  76. data/db/migrate/20230526211831_create_active_storage_tables.active_storage.rb +3 -3
  77. data/db/migrate/20230526212613_convert_to_active_storage.rb +11 -14
  78. data/db/schema.rb +16 -9
  79. data/db/seeds.rb +1 -0
  80. data/lib/fat_free_crm/callback.rb +0 -1
  81. data/lib/fat_free_crm/core_ext/string.rb +1 -1
  82. data/lib/fat_free_crm/custom_fields.rb +1 -0
  83. data/lib/fat_free_crm/mail_processor/base.rb +6 -10
  84. data/lib/fat_free_crm/version.rb +1 -1
  85. metadata +7 -31
  86. data/public/avatars/User/2/large_rails.png +0 -0
  87. data/public/avatars/User/2/medium_rails.png +0 -0
  88. data/public/avatars/User/2/original_rails.png +0 -0
  89. data/public/avatars/User/2/small_rails.png +0 -0
  90. data/public/avatars/User/2/thumb_rails.png +0 -0
  91. data/public/avatars/User/3/large_rails.png +0 -0
  92. data/public/avatars/User/3/medium_rails.png +0 -0
  93. data/public/avatars/User/3/original_rails.png +0 -0
  94. data/public/avatars/User/3/small_rails.png +0 -0
  95. data/public/avatars/User/3/thumb_rails.png +0 -0
  96. data/public/avatars/User/4/large_rails.png +0 -0
  97. data/public/avatars/User/4/medium_rails.png +0 -0
  98. data/public/avatars/User/4/original_rails.png +0 -0
  99. data/public/avatars/User/4/small_rails.png +0 -0
  100. data/public/avatars/User/4/thumb_rails.png +0 -0
  101. data/public/avatars/User/6/large_rails.png +0 -0
  102. data/public/avatars/User/6/medium_rails.png +0 -0
  103. data/public/avatars/User/6/original_rails.png +0 -0
  104. data/public/avatars/User/6/small_rails.png +0 -0
  105. data/public/avatars/User/6/thumb_rails.png +0 -0
  106. data/public/avatars/User/7/large_rails.png +0 -0
  107. data/public/avatars/User/7/medium_rails.png +0 -0
  108. data/public/avatars/User/7/original_rails.png +0 -0
  109. data/public/avatars/User/7/small_rails.png +0 -0
  110. data/public/avatars/User/7/thumb_rails.png +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 325977901a3011b43c20379a5e03317660a3cd2b702e36ceb7aa255ec0f6ff17
4
- data.tar.gz: 6badcc5bfcc41d304fe646a7ea07d1c4fce631b3a60efd08285752fe645e3572
3
+ metadata.gz: 170884fee8c43222891b33e09f6a1996c6a33c70cf5839de3eae5ab987340ef1
4
+ data.tar.gz: d1f638adddae45577b72fc71d24b88f1ec7ca8513b98ae2a29ba9977ea5b5c28
5
5
  SHA512:
6
- metadata.gz: 58f6a285322ca7ae834bbf2eeeaf0073a2c501f9dd71117b890c9d7a1403dd3b2e5f725e97d2b3814836f854595eb77f322e09a27a94202fb65ca7b28078cd47
7
- data.tar.gz: 004b7dadb4c2c77e4863010b59a87dc89500142bc52fe0d90f81e71532d964b287f1bd97365bdd9460898249c0278e0ca389eba9edba1260c3410d0131302d0b
6
+ metadata.gz: e5284a4f39f18595dfb12b87acf4052fb464a432675959f3c0e58a6edb3eeb59899e11bcff8433e3f251656bba35eea6e5f0abde513d4dc56c1c3fce98f576d3
7
+ data.tar.gz: 5644a00f3ebf9db88505d686591856d8614c4ac7908bf484fe5147494b656aacda669e236fcabdce48801d9e96cf19454b22f586aa2902ffb3e70c3bffd75a32
@@ -17,6 +17,7 @@
17
17
  //= require crm_loginout
18
18
  //= require crm_tags
19
19
  //= require crm_sortable
20
+ //= require crm_validations
20
21
  //= require textarea_autocomplete
21
22
  //= require crm_textarea_autocomplete
22
23
  //= require crm_select2
@@ -9,8 +9,8 @@
9
9
  @[0].toUpperCase() + @.substring(1)
10
10
 
11
11
  window.crm =
12
- EXPANDED: "▼"
13
- COLLAPSED: "►"
12
+ EXPANDED: "▽"
13
+ COLLAPSED: "▷"
14
14
  searchRequest: null
15
15
  autocompleter: null
16
16
  base_url: ""
@@ -336,7 +336,10 @@
336
336
 
337
337
  # Country dropdown needs special treatment ;-)
338
338
  country = $("#" + from + "_attributes_country").select2("data")
339
- $("#" + to + "_attributes_country").select2("data", country, true)
339
+ if country.length == 1
340
+ country_dropdown = $("#" + to + "_attributes_country")
341
+ country_dropdown.val(country[0].id)
342
+ country_dropdown.trigger('change')
340
343
 
341
344
 
342
345
  #----------------------------------------------------------------------------
@@ -15,6 +15,7 @@
15
15
  $(this).select2
16
16
  'width':'resolve'
17
17
  placeholder: $(this).attr("placeholder")
18
+ allowClear: true
18
19
  ajax:
19
20
  url: $(this).data("url")
20
21
  dataType: 'json'
@@ -22,16 +23,18 @@
22
23
  $(this).select2
23
24
  'width':'resolve'
24
25
  placeholder: $(this).attr("placeholder")
26
+ allowClear: true
25
27
 
26
28
  if $(this).prop("disabled") == true
27
29
  $(this).next('.select2-container').disable()
28
- $(this).next('.select2-container').hide()
30
+ $(this).next('.select2-container').hide()
29
31
 
30
32
  $(".select2_tag").not(".select2-container, .select2-offscreen").each ->
31
33
  $(this).select2
32
34
  'width':'resolve'
33
35
  placeholder: $(this).data("placeholder")
34
36
  multiple: $(this).data("multiple")
37
+ allowClear: true
35
38
 
36
39
  $(document).ready ->
37
40
  crm.make_select2()
@@ -7,16 +7,16 @@
7
7
 
8
8
  # The multiselect tag list has listeners to load/remove fieldsets related to tags
9
9
  #----------------------------------------------------------------------------
10
- $(document).on 'select2-selecting', "[name*='tag_list']", (event) ->
10
+ $(document).on 'select2:select', "[name*='tag_list']", (event) ->
11
11
  url = $(this).data('url')
12
12
  asset_id = $(this).data('asset-id')
13
13
  $.get(url, {
14
- tag: event.val
14
+ tag: event.params.data.text
15
15
  asset_id: asset_id
16
16
  collapsed: "no"
17
17
  })
18
18
 
19
- $(document).on 'select2-removing', "[name*='tag_list']", (event) ->
20
- $("#field_groups div[data-tag='" + event.val + "']").remove()
19
+ $(document).on 'select2:unselect', "[name*='tag_list']", (event) ->
20
+ $("#field_groups div[data-tag='" + event.params.data.text + "']").remove()
21
21
 
22
22
  ) jQuery
@@ -17,17 +17,14 @@
17
17
 
18
18
  # Only autocomplete if search term starts with '@'
19
19
  return [] unless text.indexOf("@") is 0
20
- words = []
21
- i = 0
22
-
23
- while i < _ffcrm_users.length
24
- name_query = text.replace("@", "").toLowerCase()
25
- words.push _ffcrm_users[i] unless _ffcrm_users[i].toLowerCase().indexOf(name_query) is -1
26
- i++
27
- cb words, text.replace("@", "")
20
+ $.ajax
21
+ url: "/users/auto_complete"
22
+ data:
23
+ term: text.replace("@", "")
24
+ success: (response) ->
25
+ cb response, text.replace("@", "")
28
26
 
29
27
  selected: (text, data) ->
30
28
  username_regEx = new RegExp("\\((@[^)]+)\\)")
31
29
  text.match(username_regEx)[1]
32
-
33
30
  ) jQuery
@@ -0,0 +1,12 @@
1
+ #------------------------------------------------------------------------------
2
+ (($) ->
3
+
4
+ # Ensure that any html5 required fields are unhidden when invalid
5
+ #----------------------------------------------------------------------------
6
+ $(document).on 'click', 'form.simple_form input:submit', (event) ->
7
+ form = this.closest('form')
8
+ invalidInputs = form.querySelectorAll(':invalid')
9
+ $(invalidInputs).each ->
10
+ $(this).closest('.field_group').show()
11
+
12
+ ) jQuery
@@ -165,12 +165,12 @@ button.navbar-toggler:focus {
165
165
  dl {
166
166
  li {
167
167
  width: 23%;
168
- font-size: 2rem;
168
+ font-size: 1rem;
169
169
  dt {
170
- font-size: inherit;
170
+ font-size: 0.75rem;
171
171
  }
172
172
  tt {
173
- font-size: inherit;
173
+ font-size: 0.75rem;
174
174
  }
175
175
  }
176
176
  }
@@ -784,3 +784,12 @@ table.asset_attributes {
784
784
  background-color: darkgray;
785
785
  }
786
786
  }
787
+
788
+
789
+ // Login page
790
+ .form-group.user_remember_me {
791
+ border: 0px;
792
+ .form-check {
793
+ padding-left: 6px;
794
+ }
795
+ }
@@ -28,7 +28,7 @@
28
28
  padding: 0px; } } }
29
29
 
30
30
  .fieldWithErrors {
31
- input {
31
+ input, select {
32
32
  border: {
33
33
  bottom: 1px solid lightpink;
34
34
  right: 1px solid lightpink; };
@@ -33,7 +33,7 @@ class Admin::FieldsController < Admin::ApplicationController
33
33
  # GET /fields/1/edit AJAX
34
34
  #----------------------------------------------------------------------------
35
35
  def edit
36
- @field = Field.find(params[:id])
36
+ @field = Field.find(params["id"])
37
37
  respond_with(@field)
38
38
  end
39
39
 
@@ -41,12 +41,12 @@ class Admin::FieldsController < Admin::ApplicationController
41
41
  # POST /fields.xml AJAX
42
42
  #----------------------------------------------------------------------------
43
43
  def create
44
- as = field_params[:as]
44
+ as = field_params["as"]
45
+ klass = Field.lookup_class(as).safe_constantize
45
46
  @field =
46
47
  if as.match?(/pair/)
47
- CustomFieldPair.create_pair(params).first
48
+ klass.create_pair("pair" => pair_params, "field" => field_params).first
48
49
  elsif as.present?
49
- klass = find_class(Field.lookup_class(as))
50
50
  klass.create(field_params)
51
51
  else
52
52
  Field.new(field_params).tap(&:valid?)
@@ -59,10 +59,10 @@ class Admin::FieldsController < Admin::ApplicationController
59
59
  # PUT /fields/1.xml AJAX
60
60
  #----------------------------------------------------------------------------
61
61
  def update
62
- if field_params[:as].match?(/pair/)
63
- @field = CustomFieldPair.update_pair(params).first
62
+ if field_params["as"].match?(/pair/)
63
+ @field = CustomFieldPair.update_pair("pair" => pair_params, "field" => field_params).first
64
64
  else
65
- @field = Field.find(params[:id])
65
+ @field = Field.find(params["id"])
66
66
  @field.update(field_params)
67
67
  end
68
68
 
@@ -73,7 +73,7 @@ class Admin::FieldsController < Admin::ApplicationController
73
73
  # DELETE /fields/1.xml HTML and AJAX
74
74
  #----------------------------------------------------------------------------
75
75
  def destroy
76
- @field = Field.find(params[:id])
76
+ @field = Field.find(params["id"])
77
77
  @field.destroy
78
78
 
79
79
  respond_with(@field)
@@ -82,7 +82,7 @@ class Admin::FieldsController < Admin::ApplicationController
82
82
  # POST /fields/sort
83
83
  #----------------------------------------------------------------------------
84
84
  def sort
85
- field_group_id = params[:field_group_id].to_i
85
+ field_group_id = params["field_group_id"].to_i
86
86
  field_ids = params["fields_field_group_#{field_group_id}"] || []
87
87
 
88
88
  field_ids.each_with_index do |id, index|
@@ -96,13 +96,12 @@ class Admin::FieldsController < Admin::ApplicationController
96
96
  #----------------------------------------------------------------------------
97
97
  def subform
98
98
  field = field_params
99
- as = field[:as]
100
-
99
+ as = field_params["as"]
101
100
  @field = if (id = field[:id]).present?
102
101
  Field.find(id).tap { |f| f.as = as }
103
102
  else
104
103
  field_group_id = field[:field_group_id]
105
- klass = find_class(Field.lookup_class(as))
104
+ klass = Field.lookup_class(as).safe_constantize
106
105
  klass.new(field_group_id: field_group_id, as: as)
107
106
  end
108
107
 
@@ -114,7 +113,11 @@ class Admin::FieldsController < Admin::ApplicationController
114
113
  protected
115
114
 
116
115
  def field_params
117
- params[:field].permit!
116
+ params.require(:field).permit(:as, :collection_string, :disabled, :field_group_id, :hint, :label, :maxlength, :minlength, :name, :pair_id, :placeholder, :position, :required, :type, settings: {})
117
+ end
118
+
119
+ def pair_params
120
+ params.require(:pair).permit("0": %i[hint required disabled id], "1": %i[hint required disabled id])
118
121
  end
119
122
 
120
123
  def setup_current_tab
@@ -79,7 +79,7 @@ class ApplicationController < ActionController::Base
79
79
  # See http://blog.nvisium.com/2014/09/understanding-protectfromforgery.html for more details.
80
80
  #----------------------------------------------------------------------------
81
81
  def handle_unverified_request
82
- raise ActionController::InvalidAuthenticityToken
82
+ raise ActionController::InvalidAuthenticityToken unless ENV.fetch('CODESPACE_NAME', nil) && Rails.env.development?
83
83
  end
84
84
 
85
85
  #
@@ -15,6 +15,6 @@ class EmailsController < ApplicationController
15
15
  @email.destroy
16
16
  respond_with(@email)
17
17
  end
18
-
18
+
19
19
  ActiveSupport.run_load_hooks(:fat_free_crm_emails_controller, self)
20
20
  end
@@ -164,6 +164,6 @@ class ContactsController < EntitiesController
164
164
  redirect_to contacts_path
165
165
  end
166
166
  end
167
-
167
+
168
168
  ActiveSupport.run_load_hooks(:fat_free_crm_contacts_controller, self)
169
169
  end
@@ -83,7 +83,7 @@ class EntitiesController < ApplicationController
83
83
  #----------------------------------------------------------------------------
84
84
  def field_group
85
85
  if @tag = Tag.find_by_name(params[:tag].strip)
86
- if @field_group = FieldGroup.find_by_tag_id_and_klass_name(@tag.id, klass.to_s)
86
+ if @field_groups = FieldGroup.where(tag_id: @tag.id, klass_name: klass.to_s).order(:label, :created_at)
87
87
  @asset = klass.find_by_id(params[:asset_id]) || klass.new
88
88
  render('fields/group') && return
89
89
  end
@@ -9,13 +9,14 @@ class ListsController < ApplicationController
9
9
  # POST /lists
10
10
  #----------------------------------------------------------------------------
11
11
  def create
12
- list_params[:user_id] = (current_user.id if params[:is_global].to_i.zero?)
12
+ list_attr = list_params.to_h
13
+ list_attr["user_id"] = current_user.id if params["is_global"] != "1"
13
14
 
14
15
  # Find any existing list with the same name (case insensitive)
15
- if @list = List.where("lower(name) = ?", list_params[:name].downcase).where(user_id: list_params[:user_id]).first
16
- @list.update(list_params)
16
+ if @list = List.where("lower(name) = ?", list_attr[:name].downcase).where(user_id: list_attr[:user_id]).first
17
+ @list.update(list_attr)
17
18
  else
18
- @list = List.create(list_params)
19
+ @list = List.create(list_attr)
19
20
  end
20
21
 
21
22
  respond_with(@list)
@@ -63,9 +63,7 @@ class UsersController < ApplicationController
63
63
  end
64
64
  end
65
65
  responds_to_parent do
66
- # Without return RSpec2 screams bloody murder about rendering twice:
67
- # within the block and after yield in responds_to_parent.
68
- render && (return if Rails.env.test?)
66
+ render
69
67
  end
70
68
  end
71
69
  end
@@ -111,6 +109,20 @@ class UsersController < ApplicationController
111
109
  @unassigned_opportunities = Opportunity.my(current_user).unassigned.pipeline.order(:stage).includes(:account, :user, :tags)
112
110
  end
113
111
 
112
+ def auto_complete
113
+ @query = params[:term] || ''
114
+ @users = User.my(current_user).text_search(@query).limit(10).order(:first_name, :last_name)
115
+
116
+ respond_to do |format|
117
+ format.json do
118
+ results = @users.map do |a|
119
+ helpers.j(a.full_name + " (@" + a.username + ")")
120
+ end
121
+ render json: results
122
+ end
123
+ end
124
+ end
125
+
114
126
  protected
115
127
 
116
128
  def user_params
@@ -36,7 +36,7 @@ module ApplicationHelper
36
36
  end
37
37
 
38
38
  def subtitle_link(id, text, hidden)
39
- link_to("<small>#{hidden ? '&#9658;' : '&#9660;'}</small> #{sanitize text}".html_safe,
39
+ link_to("<small>#{hidden ? '&#9655;' : '&#9661;'}</small> #{sanitize text}".html_safe,
40
40
  url_for(controller: :home, action: :toggle, id: id),
41
41
  remote: true,
42
42
  onclick: "crm.flip_subtitle(this)")
@@ -99,7 +99,7 @@ module ApplicationHelper
99
99
 
100
100
  #----------------------------------------------------------------------------
101
101
  def arrow_for(id)
102
- content_tag(:span, "&#9658;".html_safe, id: "#{id}_arrow", class: :arrow)
102
+ content_tag(:span, "&#9655;".html_safe, id: "#{id}_arrow", class: :arrow)
103
103
  end
104
104
 
105
105
  #----------------------------------------------------------------------------
@@ -115,13 +115,13 @@ module ApplicationHelper
115
115
 
116
116
  #----------------------------------------------------------------------------
117
117
  def link_to_delete(record, options = {})
118
- confirm = options[:confirm] || nil
118
+ confirm = options[:confirm] || t(:confirm_delete, record.class.to_s.downcase)
119
119
 
120
120
  link_to(t(:delete) + "!",
121
121
  options[:url] || url_for(record),
122
122
  method: :delete,
123
123
  remote: true,
124
- confirm: confirm)
124
+ data: { confirm: confirm })
125
125
  end
126
126
 
127
127
  #----------------------------------------------------------------------------
@@ -155,7 +155,7 @@ module ApplicationHelper
155
155
  #----------------------------------------------------------------------------
156
156
  def link_to_email(email, length = nil, &_block)
157
157
  name = (length ? truncate(email, length: length) : email)
158
- bcc = Setting&.email_dropbox
158
+ bcc = Setting.email_dropbox
159
159
  mailto = if bcc && bcc[:address].present?
160
160
  "#{email}?bcc=#{bcc[:address]}"
161
161
  else
@@ -69,7 +69,7 @@ module LeadsHelper
69
69
  #----------------------------------------------------------------------------
70
70
  def lead_summary(lead)
71
71
  summary = []
72
- summary << (lead.status ? t(lead.status) : t(:other))
72
+ summary << t(lead.status || :other)
73
73
 
74
74
  if lead.company? && lead.title?
75
75
  summary << t(:works_at, job_title: lead.title, company: lead.company)
@@ -17,7 +17,7 @@ module OpportunitiesHelper
17
17
  def opportunity_summary(opportunity)
18
18
  summary = []
19
19
  amount = []
20
- summary << (opportunity.stage ? t(opportunity.stage) : t(:other))
20
+ summary << t(opportunity.stage || :other)
21
21
  summary << number_to_currency(opportunity.weighted_amount, precision: 0)
22
22
  unless %w[won lost].include?(opportunity.stage)
23
23
  amount << number_to_currency(opportunity.amount.to_f, precision: 0)
@@ -41,8 +41,10 @@ module OpportunitiesHelper
41
41
  selected_campaign = Campaign.find_by_id(options[:selected])
42
42
  campaigns = ([selected_campaign] + Campaign.my(current_user).order(:name).limit(25)).compact.uniq
43
43
  collection_select :opportunity, :campaign_id, campaigns, :id, :name,
44
- { selected: options[:selected], prompt: t(:select_a_campaign) },
45
- style: 'width:330px;', class: 'select2'
44
+ { selected: options[:selected], prompt: t(:select_a_campaign), include_blank: true },
45
+ style: 'width:330px;', class: 'select2',
46
+ placeholder: t(:select_a_campaign),
47
+ "data-url": auto_complete_campaigns_path(format: 'json')
46
48
  end
47
49
 
48
50
  # Generates the inline revenue message for the opportunity list table.
@@ -25,7 +25,7 @@ module UsersHelper
25
25
  user_options = user_options_for_select(users, myself)
26
26
  select(asset, :assigned_to, user_options,
27
27
  { include_blank: t(:unassigned) },
28
- style: 'width: 160px;',
28
+ style: 'width: 160px;', "data-allow-clear" => false,
29
29
  class: 'select2')
30
30
  end
31
31
 
@@ -35,8 +35,8 @@ class Campaign < ActiveRecord::Base
35
35
  belongs_to :user, optional: true # TODO: Is this really optional?
36
36
  belongs_to :assignee, class_name: "User", foreign_key: :assigned_to, optional: true # TODO: Is this really optional?
37
37
  has_many :tasks, as: :asset, dependent: :destroy # , :order => 'created_at DESC'
38
- has_many :leads, -> { order "id DESC" }, dependent: :destroy
39
- has_many :opportunities, -> { order "id DESC" }, dependent: :destroy
38
+ has_many :leads, -> { order "id DESC" }
39
+ has_many :opportunities, -> { order "id DESC" }
40
40
  has_many :emails, as: :mediator
41
41
 
42
42
  serialize :subscribed_users, Array
@@ -103,7 +103,7 @@ class Campaign < ActiveRecord::Base
103
103
  # Make sure end date > start date.
104
104
  #----------------------------------------------------------------------------
105
105
  def start_and_end_dates
106
- errors.add(:ends_on, :dates_not_in_sequence) if (starts_on && ends_on) && (starts_on > ends_on)
106
+ errors.add(:ends_on, :dates_not_in_sequence) if starts_on && ends_on && (starts_on > ends_on)
107
107
  end
108
108
 
109
109
  # Make sure at least one user has been selected if the campaign is being shared.
@@ -12,10 +12,9 @@ class CustomFieldPair < CustomField
12
12
  #------------------------------------------------------------------------------
13
13
  def self.create_pair(params)
14
14
  fields = params['field']
15
- as = params['field']['as']
16
- pair = params.delete('pair')
15
+ pair = params['pair']
17
16
  base_params = fields.delete_if { |k, _v| !%w[field_group_id label as].include?(k) }
18
- klass = ("custom_field_" + as.gsub('pair', '_pair')).classify.constantize
17
+ klass = Field.lookup_class(fields['as']).safe_constantize
19
18
  field1 = klass.create(base_params.merge(pair['0']))
20
19
  field2 = klass.create(base_params.merge(pair['1']).merge('pair_id' => field1.id, 'required' => field1.required, 'disabled' => field1.disabled))
21
20
  [field1, field2]
@@ -25,19 +24,19 @@ class CustomFieldPair < CustomField
25
24
  #------------------------------------------------------------------------------
26
25
  def self.update_pair(params)
27
26
  fields = params['field']
28
- pair = params.delete('pair')
27
+ pair = params['pair']
29
28
  base_params = fields.delete_if { |k, _v| !%w[field_group_id label as].include?(k) }
30
- field1 = CustomFieldPair.find(params['id'])
29
+ field1 = CustomFieldPair.find(pair['0']['id'])
31
30
  field1.update(base_params.merge(pair['0']))
32
31
  field2 = field1.paired_with
33
32
  field2.update(base_params.merge(pair['1']).merge('required' => field1.required, 'disabled' => field1.disabled))
34
33
  [field1, field2]
35
34
  end
36
35
 
37
- # Returns the field that this field is paired with
36
+ # Returns the field that this field is paired with (bi-directional)
38
37
  #------------------------------------------------------------------------------
39
38
  def paired_with
40
- pair || CustomFieldPair.where(pair_id: id).first
39
+ pair || self.class.find_by_id(pair_id)
41
40
  end
42
41
 
43
42
  ActiveSupport.run_load_hooks(:fat_free_crm_custom_field_pair, self)
@@ -34,7 +34,7 @@ class Field < ActiveRecord::Base
34
34
  serialize :collection, Array
35
35
  serialize :settings, HashWithIndifferentAccess
36
36
 
37
- belongs_to :field_group, optional: true # TODO: Is this really optional?
37
+ belongs_to :field_group, optional: true
38
38
 
39
39
  scope :core_fields, -> { where(type: 'CoreField') }
40
40
  scope :custom_fields, -> { where("type != 'CoreField'") }
@@ -92,8 +92,6 @@ class Field < ActiveRecord::Base
92
92
 
93
93
  def render(value)
94
94
  case as
95
- when 'checkbox'
96
- value.to_s == '0' ? "no" : "yes"
97
95
  when 'date'
98
96
  value&.strftime(I18n.t("date.formats.mmddyy"))
99
97
  when 'datetime'
data/app/models/list.rb CHANGED
@@ -8,7 +8,7 @@
8
8
  class List < ActiveRecord::Base
9
9
  validates_presence_of :name
10
10
  validates_presence_of :url
11
- belongs_to :user
11
+ belongs_to :user, optional: true
12
12
 
13
13
  # Parses the controller from the url
14
14
  def controller
@@ -16,7 +16,8 @@ class LeadObserver < ActiveRecord::Observer
16
16
 
17
17
  def after_update(item)
18
18
  original = @@leads.delete(item.id)
19
- return log_activity(item, :reject) if original&.status != "rejected" && item.status == "rejected"
19
+
20
+ log_activity(item, :reject) if original&.status != "rejected" && item.status == "rejected"
20
21
  end
21
22
 
22
23
  private
@@ -19,7 +19,8 @@ class TaskObserver < ActiveRecord::Observer
19
19
  if original
20
20
  return log_activity(item, :complete) if item.completed_at && original.completed_at.nil?
21
21
  return log_activity(item, :reassign) if item.assigned_to != original.assigned_to
22
- return log_activity(item, :reschedule) if item.bucket != original.bucket
22
+
23
+ log_activity(item, :reschedule) if item.bucket != original.bucket
23
24
  end
24
25
  end
25
26
 
@@ -54,7 +54,8 @@ class Address < ActiveRecord::Base
54
54
  exists = attributes['id'].present?
55
55
  empty = %w[street1 street2 city state zipcode country full_address].map { |name| attributes[name].blank? }.all?
56
56
  attributes[:_destroy] = 1 if exists && empty
57
- (!exists && empty)
57
+
58
+ !exists && empty
58
59
  end
59
60
 
60
61
  ActiveSupport.run_load_hooks(:fat_free_crm_address, self)
@@ -52,10 +52,9 @@ class Comment < ActiveRecord::Base
52
52
 
53
53
  # Notify subscribed users when a comment is added, unless user created this comment
54
54
  def notify_subscribers
55
- commentable.subscribed_users.reject { |user_id| user_id == user.id }.each do |subscriber_id|
56
- if subscriber = User.find_by_id(subscriber_id)
57
- SubscriptionMailer.comment_notification(subscriber, self).deliver_later
58
- end
55
+ users_to_notify = User.where(id: commentable.subscribed_users.reject { |user_id| user_id == user.id })
56
+ users_to_notify.select(&:emailable?).each do |subscriber|
57
+ SubscriptionMailer.comment_notification(subscriber, self).deliver_later
59
58
  end
60
59
  end
61
60
 
@@ -103,7 +103,7 @@ class Task < ActiveRecord::Base
103
103
  scope :completed_last_month, -> { where('completed_at >= ? AND completed_at < ?', (Time.zone.now.beginning_of_month.utc - 1.day).beginning_of_month.utc, Time.zone.now.beginning_of_month.utc) }
104
104
 
105
105
  scope :text_search, lambda { |query|
106
- query = query.gsub(/[^\w\s\-\.'\p{L}]/u, '').strip
106
+ query = query.gsub(/[^\w\s\-.'\p{L}]/u, '').strip
107
107
  where('upper(name) LIKE upper(?)', "%#{query}%")
108
108
  }
109
109
 
@@ -73,7 +73,7 @@ class User < ActiveRecord::Base
73
73
  scope :by_name, -> { order('first_name, last_name, email') }
74
74
 
75
75
  scope :text_search, lambda { |query|
76
- query = query.gsub(/[^\w\s\-\.'\p{L}]/u, '').strip
76
+ query = query.gsub(/[^\w\s\-.'\p{L}]/u, '').strip
77
77
  where('upper(username) LIKE upper(:s) OR upper(email) LIKE upper(:s) OR upper(first_name) LIKE upper(:s) OR upper(last_name) LIKE upper(:s)', s: "%#{query}%")
78
78
  }
79
79
 
@@ -134,6 +134,12 @@ class User < ActiveRecord::Base
134
134
  end
135
135
  end
136
136
 
137
+ # Send emails to active users only
138
+ #----------------------------------------------------------------------------
139
+ def emailable?
140
+ confirmed? && !awaits_approval? && !suspended? && email.present?
141
+ end
142
+
137
143
  #----------------------------------------------------------------------------
138
144
  def preference
139
145
  @preference ||= preferences.build
@@ -1,5 +1,5 @@
1
1
  - edit ||= false
2
- - collapsed = (@account.errors.empty? && session[:account_contact].nil?)
2
+ - collapsed = session[:account_contact].nil?
3
3
  = subtitle :account_contact, collapsed, t(:contact_info)
4
4
  .section
5
5
  %small#account_contact_intro{ hidden_if(!collapsed) }
@@ -2,9 +2,9 @@
2
2
  .section
3
3
  %table
4
4
  %tr
5
- %td(colspan="5")
5
+ %td{class: (@account.errors['name'].present? ? 'fieldWithErrors' : nil)}(colspan="5")
6
6
  .label.top.req #{t :name}:
7
- = f.text_field :name, autofocus: true, style: "width:500px"
7
+ = f.text_field :name, autofocus: true, style: "width:500px", required: "required"
8
8
  %tr
9
9
  %td
10
10
  .label #{t :assigned_to}:
@@ -12,7 +12,7 @@
12
12
  %td= spacer
13
13
  %td
14
14
  .label #{t :category}:
15
- = f.select :category, Setting.unroll(:account_category), { selected: (@account.category || "other").to_sym, include_blank: t(:other) }, { style: "width:160px", class: 'select2' }
15
+ = f.select :category, Setting.unroll(:account_category), { selected: (@account.category || "other").to_sym, include_blank: t(:other) }, { style: "width:160px", class: 'select2', placeholder: t(:other) }
16
16
  %td= spacer
17
17
  %td
18
18
  .label #{t :rating}:
@@ -1,5 +1,5 @@
1
1
  - entity_name = controller.controller_name.singularize.underscore #account
2
2
  - @entity = instance_variable_get("@#{entity_name}")
3
3
 
4
- $('#main').html('#{ j (render template: "#{entity_name.pluralize}/show.html", entity_name => @entity) }');
4
+ $('#main').html('#{ j (render template: "#{entity_name.pluralize}/show", formats: [:html], entity_name => @entity) }');
5
5
  = raw generate_js_for_popups(@entity, :tasks, :contacts, :opportunities)