decidim-notify 0.3

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.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-AGPLv3.txt +661 -0
  3. data/README.md +160 -0
  4. data/Rakefile +40 -0
  5. data/app/assets/config/decidim_notify_manifest.css +2 -0
  6. data/app/assets/config/decidim_notify_manifest.js +3 -0
  7. data/app/assets/images/decidim/notify/icon.svg +1 -0
  8. data/app/assets/javascripts/cable.js +13 -0
  9. data/app/assets/javascripts/channels/decidim/notify/conversations.js +90 -0
  10. data/app/assets/javascripts/decidim/notify/conversations.js.es6 +70 -0
  11. data/app/assets/javascripts/decidim/notify/select2.js.es6 +100 -0
  12. data/app/assets/stylesheets/decidim/notify/_hexagon.scss +126 -0
  13. data/app/assets/stylesheets/decidim/notify/admin.scss +25 -0
  14. data/app/assets/stylesheets/decidim/notify/notify.scss +220 -0
  15. data/app/cells/decidim/notify/chapter/show.erb +35 -0
  16. data/app/cells/decidim/notify/chapter_cell.rb +43 -0
  17. data/app/cells/decidim/notify/note/show.erb +20 -0
  18. data/app/cells/decidim/notify/note_cell.rb +38 -0
  19. data/app/cells/decidim/notify/participant/show.erb +9 -0
  20. data/app/cells/decidim/notify/participant_cell.rb +26 -0
  21. data/app/channels/decidim/notify/chapters_channel.rb +17 -0
  22. data/app/channels/decidim/notify/connection.rb +17 -0
  23. data/app/channels/decidim/notify/notes_channel.rb +17 -0
  24. data/app/channels/decidim/notify/participants_channel.rb +17 -0
  25. data/app/commands/decidim/notify/admin/create_chapter.rb +51 -0
  26. data/app/commands/decidim/notify/admin/destroy_chapter.rb +30 -0
  27. data/app/commands/decidim/notify/admin/update_chapter.rb +52 -0
  28. data/app/commands/decidim/notify/admin/update_config.rb +71 -0
  29. data/app/commands/decidim/notify/create_note.rb +52 -0
  30. data/app/commands/decidim/notify/delete_chapter.rb +28 -0
  31. data/app/commands/decidim/notify/delete_note.rb +28 -0
  32. data/app/commands/decidim/notify/update_chapter.rb +35 -0
  33. data/app/commands/decidim/notify/update_note.rb +51 -0
  34. data/app/controllers/concerns/decidim/notify/broadcasts.rb +56 -0
  35. data/app/controllers/concerns/decidim/notify/needs_ajax_rescue.rb +25 -0
  36. data/app/controllers/decidim/notify/admin/application_controller.rb +18 -0
  37. data/app/controllers/decidim/notify/admin/chapters_controller.rb +90 -0
  38. data/app/controllers/decidim/notify/admin/conversations_controller.rb +61 -0
  39. data/app/controllers/decidim/notify/application_controller.rb +16 -0
  40. data/app/controllers/decidim/notify/chapters_controller.rb +41 -0
  41. data/app/controllers/decidim/notify/conversations_controller.rb +105 -0
  42. data/app/forms/decidim/notify/admin/chapter_form.rb +14 -0
  43. data/app/forms/decidim/notify/admin/notify_config_form.rb +14 -0
  44. data/app/forms/decidim/notify/chapter_form.rb +13 -0
  45. data/app/forms/decidim/notify/note_form.rb +16 -0
  46. data/app/helpers/decidim/notify/application_helper.rb +10 -0
  47. data/app/models/concerns/decidim/notify/belongs_to_notify_component.rb +31 -0
  48. data/app/models/decidim/notify/application_record.rb +10 -0
  49. data/app/models/decidim/notify/author.rb +29 -0
  50. data/app/models/decidim/notify/chapter.rb +22 -0
  51. data/app/models/decidim/notify/note.rb +23 -0
  52. data/app/permissions/decidim/notify/admin/permissions.rb +23 -0
  53. data/app/permissions/decidim/notify/permissions.rb +45 -0
  54. data/app/views/decidim/notify/admin/chapters/_form.html.erb +20 -0
  55. data/app/views/decidim/notify/admin/chapters/edit.html.erb +7 -0
  56. data/app/views/decidim/notify/admin/chapters/index.html.erb +52 -0
  57. data/app/views/decidim/notify/admin/chapters/new.html.erb +7 -0
  58. data/app/views/decidim/notify/admin/conversations/_form.html.erb +27 -0
  59. data/app/views/decidim/notify/admin/conversations/index.html.erb +27 -0
  60. data/app/views/decidim/notify/conversations/_chapter.html.erb +1 -0
  61. data/app/views/decidim/notify/conversations/_form.html.erb +9 -0
  62. data/app/views/decidim/notify/conversations/_note.html.erb +1 -0
  63. data/app/views/decidim/notify/conversations/_participant.html.erb +1 -0
  64. data/app/views/decidim/notify/conversations/_script.js.erb +7 -0
  65. data/app/views/decidim/notify/conversations/index.html.erb +48 -0
  66. data/app/views/decidim/notify/conversations/private.html.erb +11 -0
  67. data/config/i18n-tasks.yml +35 -0
  68. data/config/locales/ca.yml +110 -0
  69. data/config/locales/cs.yml +110 -0
  70. data/config/locales/en.yml +110 -0
  71. data/config/locales/es.yml +110 -0
  72. data/db/migrate/20200504071404_create_notify_notes.rb +11 -0
  73. data/db/migrate/20200505061547_create_notify_authors.rb +17 -0
  74. data/db/migrate/20200505061640_add_notify_notes_references.rb +9 -0
  75. data/db/migrate/20200505195918_add_notify_author_admin.rb +7 -0
  76. data/db/migrate/20200507103034_change_notify_notes_authors.rb +15 -0
  77. data/db/migrate/20200514144040_add_notify_note_chapters.rb +15 -0
  78. data/lib/decidim/notify.rb +57 -0
  79. data/lib/decidim/notify/admin.rb +10 -0
  80. data/lib/decidim/notify/admin_engine.rb +25 -0
  81. data/lib/decidim/notify/component.rb +135 -0
  82. data/lib/decidim/notify/engine.rb +39 -0
  83. data/lib/decidim/notify/seeds/avatar1.png +0 -0
  84. data/lib/decidim/notify/seeds/avatar2.png +0 -0
  85. data/lib/decidim/notify/seeds/avatar3.png +0 -0
  86. data/lib/decidim/notify/seeds/avatar4.png +0 -0
  87. data/lib/decidim/notify/seeds/avatar5.png +0 -0
  88. data/lib/decidim/notify/test/factories.rb +48 -0
  89. data/lib/decidim/notify/test/shared_examples/component_examples.rb +23 -0
  90. data/lib/decidim/notify/version.rb +9 -0
  91. data/vendor/assets/javascripts/select2.js +6147 -0
  92. data/vendor/assets/stylesheets/select2-foundation-theme.css +249 -0
  93. data/vendor/assets/stylesheets/select2.css +515 -0
  94. metadata +177 -0
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Notify
5
+ class DeleteNote < Rectify::Command
6
+ # Public: Initializes the command.
7
+ #
8
+ def initialize(id)
9
+ @id = id
10
+ end
11
+
12
+ # Executes the command. Broadcasts these events:
13
+ #
14
+ # - :ok when everything is valid.
15
+ # - :invalid if we couldn't proceed.
16
+ #
17
+ # Returns nothing.
18
+ def call
19
+ note = Note.for(current_component).find(@id)
20
+ note.destroy!
21
+
22
+ broadcast(:ok)
23
+ rescue ActiveRecord::ActiveRecordError => e
24
+ broadcast(:invalid, e.message)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Notify
5
+ class UpdateChapter < Admin::UpdateChapter
6
+ # Public: Initializes the command.
7
+ #
8
+ # form - A config form
9
+ def initialize(form)
10
+ @form = form
11
+ end
12
+
13
+ # Executes the command. Broadcasts these events:
14
+ #
15
+ # - :ok when everything is valid.
16
+ # - :invalid if we couldn't proceed.
17
+ #
18
+ # Returns nothing.
19
+ def call
20
+ return broadcast(:invalid) if form.invalid?
21
+
22
+ begin
23
+ @chapter = Chapter.find(form.id)
24
+
25
+ unset_actives
26
+ update_chapter!
27
+
28
+ broadcast(:ok, @chapter)
29
+ rescue ActiveRecord::ActiveRecordError => e
30
+ broadcast(:invalid, e.message)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Notify
5
+ class UpdateNote < Rectify::Command
6
+ # Public: Initializes the command.
7
+ #
8
+ # form - A config form
9
+ def initialize(form)
10
+ @form = form
11
+ end
12
+
13
+ # Executes the command. Broadcasts these events:
14
+ #
15
+ # - :ok when everything is valid.
16
+ # - :invalid if we couldn't proceed.
17
+ #
18
+ # Returns nothing.
19
+ def call
20
+ return broadcast(:invalid) if form.invalid?
21
+
22
+ begin
23
+ note = Note.find(form.id)
24
+ note.author = Author.find_by(code: form.code, component: current_component)&.user
25
+ note.body = form.body
26
+ note.creator = current_user unless note.creator
27
+ note.chapter = create_chapter
28
+ note.save!
29
+
30
+ broadcast(:ok, note, @new_chapter)
31
+ rescue ActiveRecord::ActiveRecordError => e
32
+ broadcast(:invalid, e.message)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :form
39
+
40
+ def create_chapter
41
+ return nil if form.chapter.blank?
42
+
43
+ chapter = Chapter.find_or_initialize_by(title: form.chapter, component: current_component)
44
+ @new_chapter = chapter unless chapter.id
45
+ chapter.title = form.chapter
46
+ chapter.save!
47
+ chapter
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Decidim
6
+ module Notify
7
+ module Broadcasts
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ def broadcast_create_note(note)
12
+ html = render_to_string(partial: "decidim/notify/conversations/note", locals: { note: note })
13
+ Decidim::Notify.server.broadcast("notify-notes-#{current_component.id}", create: html, chapterId: note.chapter&.id)
14
+ end
15
+
16
+ def broadcast_update_note(note)
17
+ html = render_to_string(partial: "decidim/notify/conversations/note", locals: { note: note })
18
+ Decidim::Notify.server.broadcast("notify-notes-#{current_component.id}", id: note.id, update: html, chapterId: note.chapter&.id)
19
+ end
20
+
21
+ def broadcast_destroy_note(id)
22
+ Decidim::Notify.server.broadcast("notify-notes-#{current_component.id}", destroy: id)
23
+ end
24
+
25
+ def broadcast_participants(participants)
26
+ view1 = render_to_string(partial: "decidim/notify/conversations/participant", collection: participants.select(&:admin))
27
+ view2 = render_to_string(partial: "decidim/notify/conversations/participant", collection: participants.reject(&:admin))
28
+ Decidim::Notify.server.broadcast("notify-participants-#{current_component.id}", noteTakers: view1, participants: view2)
29
+ end
30
+
31
+ def broadcast_create_chapter(chapter)
32
+ data = {
33
+ id: chapter.id,
34
+ title: chapter.title,
35
+ create: render_to_string(partial: "decidim/notify/conversations/chapter",
36
+ locals: { chapter: OpenStruct.new(title: chapter.title, id: chapter.id, component: chapter.component) })
37
+ }
38
+ Decidim::Notify.server.broadcast("notify-chapters-#{current_component.id}", data)
39
+ end
40
+
41
+ def broadcast_update_chapter(chapter)
42
+ data = {
43
+ id: chapter.id,
44
+ active: chapter.active,
45
+ update: chapter.title
46
+ }
47
+ Decidim::Notify.server.broadcast("notify-chapters-#{current_component.id}", data)
48
+ end
49
+
50
+ def broadcast_destroy_chapter(id)
51
+ Decidim::Notify.server.broadcast("notify-chapters-#{current_component.id}", destroy: id)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Decidim
6
+ module Notify
7
+ module NeedsAjaxRescue
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ class ::Decidim::ActionForbidden < StandardError
12
+ end
13
+
14
+ rescue_from ::Decidim::ActionForbidden, with: :user_has_no_permission
15
+
16
+ # Overrides original user permissions handling to take into account ajax
17
+ def user_has_no_permission
18
+ return super unless request.xhr?
19
+
20
+ render plain: t("actions.unauthorized", scope: "decidim.core"), status: :forbidden
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Notify
5
+ module Admin
6
+ # This controller is the abstract class from which all other controllers of
7
+ # this engine inherit.
8
+ #
9
+ # Note that it inherits from `Decidim::Admin::Components::BaseController`, which
10
+ # override its layout and provide all kinds of useful methods.
11
+ class ApplicationController < Decidim::Admin::Components::BaseController
12
+ def permission_class_chain
13
+ [::Decidim::Notify::Admin::Permissions] + super
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Notify
5
+ module Admin
6
+ class ChaptersController < Admin::ApplicationController
7
+ include Broadcasts
8
+
9
+ helper_method :chapters
10
+
11
+ def new
12
+ enforce_permission_to :create, :chapter
13
+ @form = form(ChapterForm).instance
14
+ end
15
+
16
+ def create
17
+ enforce_permission_to :create, :chapter
18
+
19
+ @form = form(ChapterForm).from_params(params)
20
+
21
+ CreateChapter.call(@form) do
22
+ on(:ok) do |chapter|
23
+ flash[:notice] = I18n.t("chapters.create.success", scope: "decidim.notify.admin")
24
+
25
+ broadcast_create_chapter chapter
26
+ redirect_to EngineRouter.admin_proxy(current_component).chapters_path
27
+ end
28
+
29
+ on(:invalid) do
30
+ flash.now[:alert] = I18n.t("chapters.create.error", scope: "decidim.notify.admin")
31
+ render :new
32
+ end
33
+ end
34
+ end
35
+
36
+ def edit
37
+ enforce_permission_to :update, :chapter, chapter: current_chapter
38
+ @form = form(ChapterForm).from_model(current_chapter)
39
+ end
40
+
41
+ def update
42
+ enforce_permission_to :update, :chapter, chapter: current_chapter
43
+ @form = form(ChapterForm).from_params(params)
44
+
45
+ UpdateChapter.call(@form, current_chapter) do
46
+ on(:ok) do
47
+ flash[:notice] = I18n.t("chapters.update.success", scope: "decidim.notify.admin")
48
+
49
+ broadcast_update_chapter current_chapter
50
+ redirect_to EngineRouter.admin_proxy(current_component).chapters_path
51
+ end
52
+
53
+ on(:invalid) do
54
+ flash.now[:alert] = I18n.t("chapters.update.error", scope: "decidim.notify.admin")
55
+ render :edit
56
+ end
57
+ end
58
+ end
59
+
60
+ def destroy
61
+ enforce_permission_to :destroy, :chapter, chapter: current_chapter
62
+
63
+ DestroyChapter.call(current_chapter) do
64
+ on(:ok) do
65
+ flash[:notice] = I18n.t("chapters.destroy.success", scope: "decidim.notify.admin")
66
+
67
+ broadcast_destroy_chapter params[:id]
68
+ redirect_to EngineRouter.admin_proxy(current_component).chapters_path
69
+ end
70
+
71
+ on(:invalid) do
72
+ flash.now[:alert] = I18n.t("chapters.destroy.error", scope: "decidim.notify.admin")
73
+ redirect_to EngineRouter.admin_proxy(current_component).chapters_path
74
+ end
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def chapters
81
+ Chapter.for(current_component)
82
+ end
83
+
84
+ def current_chapter
85
+ @current_chapter ||= Chapter.find(params[:id])
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Notify
5
+ module Admin
6
+ class ConversationsController < Admin::ApplicationController
7
+ include NeedsAjaxRescue
8
+ include Broadcasts
9
+
10
+ def index
11
+ enforce_permission_to :index, :config
12
+
13
+ @users = Author.for(current_component).map { |user| OpenStruct.new(text: format_user_name(user), id: user.decidim_user_id) }
14
+ @note_takers = Author.for(current_component).note_takers.map { |user| OpenStruct.new(text: format_user_name(user), id: user.decidim_user_id) }
15
+ @form = form(NotifyConfigForm).from_params(current_component.attributes["settings"]["global"])
16
+ end
17
+
18
+ def create
19
+ enforce_permission_to :update, :config
20
+
21
+ @form = form(NotifyConfigForm).from_params(params)
22
+ UpdateConfig.call(@form) do
23
+ on(:ok) do |participants|
24
+ flash[:notice] = I18n.t("decidim.notify.admin.conversations.success")
25
+ broadcast_participants participants
26
+ end
27
+ on(:invalid) do |message|
28
+ flash[:alert] = I18n.t("decidim.notify.admin.conversations.error", message: message)
29
+ end
30
+ end
31
+ redirect_to EngineRouter.admin_proxy(current_component).conversations_path
32
+ end
33
+
34
+ def users
35
+ enforce_permission_to :update, :config
36
+
37
+ respond_to do |format|
38
+ format.json do
39
+ if (term = params[:term].to_s).present?
40
+ query = current_organization.users.order(name: :asc)
41
+ query = query.where("name ILIKE :term OR nickname ILIKE :term OR email ILIKE :term", term: "%#{term}%")
42
+
43
+ render json: query.all.collect { |u| { id: u.id, text: format_user_name(u) } }
44
+ else
45
+ render json: []
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def format_user_name(user)
54
+ text = "#{user.name} (@#{user.nickname})"
55
+ text = "<b>#{user.code}</b> - #{text}" if defined? user.code
56
+ text
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Notify
5
+ # This controller is the abstract class from which all other controllers of
6
+ # this engine inherit.
7
+ #
8
+ # Note that it inherits from `Decidim::Components::BaseController`, which
9
+ # override its layout and provide all kinds of useful methods.
10
+ class ApplicationController < Decidim::Components::BaseController
11
+ def permission_class_chain
12
+ [::Decidim::Notify::Permissions] + super
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Notify
5
+ class ChaptersController < Decidim::Notify::ApplicationController
6
+ include FormFactory
7
+ include NeedsAjaxRescue
8
+ include Broadcasts
9
+
10
+ def update
11
+ enforce_permission_to :create, :chapter
12
+
13
+ @form = form(ChapterForm).from_params(params)
14
+ UpdateChapter.call(@form) do
15
+ on(:ok) do |chapter, _old_title|
16
+ broadcast_update_chapter chapter
17
+
18
+ render json: { message: "✔" }
19
+ end
20
+ on(:invalid) do |message|
21
+ render json: { message: t("update.error", scope: "decidim.notify.chapter", message: message) }, status: :unprocessable_entity
22
+ end
23
+ end
24
+ end
25
+
26
+ def destroy
27
+ enforce_permission_to :destroy, :chapter
28
+
29
+ DeleteChapter.call(params[:id]) do
30
+ on(:ok) do
31
+ broadcast_destroy_chapter params[:id]
32
+ render json: { message: "✔" }
33
+ end
34
+ on(:invalid) do |message|
35
+ render json: { message: t("destroy.error", scope: "decidim.notify.chapter", message: message) }, status: :unprocessable_entity
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Notify
5
+ class ConversationsController < Decidim::Notify::ApplicationController
6
+ include FormFactory
7
+ include NeedsAjaxRescue
8
+ include Broadcasts
9
+
10
+ helper_method :chapters, :active_chapter
11
+
12
+ def index
13
+ return render :private unless allowed_to? :index, :notes
14
+
15
+ @unclassified = Chapter.new(notes: unclassified_notes)
16
+ @participants = Author.for(current_component).participants
17
+ @note_takers = Author.for(current_component).note_takers
18
+ @form = form(NoteForm).instance
19
+ end
20
+
21
+ def create
22
+ enforce_permission_to :create, :notes
23
+
24
+ @form = form(NoteForm).from_params(params)
25
+ CreateNote.call(@form) do
26
+ on(:ok) do |note, new_chapter|
27
+ broadcast_create_chapter new_chapter if new_chapter
28
+ broadcast_create_note note
29
+
30
+ render json: { message: "✔" }
31
+ end
32
+ on(:invalid) do |message|
33
+ render json: { message: I18n.t("decidim.notify.conversations.create.error", message: message) }, status: :unprocessable_entity
34
+ end
35
+ end
36
+ end
37
+
38
+ def update
39
+ enforce_permission_to :update, :notes
40
+
41
+ @form = form(NoteForm).from_params(params)
42
+ UpdateNote.call(@form) do
43
+ on(:ok) do |note, new_chapter|
44
+ broadcast_create_chapter new_chapter if new_chapter
45
+ broadcast_update_note note
46
+
47
+ render json: { message: "✔" }
48
+ end
49
+ on(:invalid) do |message|
50
+ render json: { message: I18n.t("decidim.notify.conversations.update.error", message: message) }, status: :unprocessable_entity
51
+ end
52
+ end
53
+ end
54
+
55
+ def destroy
56
+ enforce_permission_to :destroy, :notes
57
+
58
+ DeleteNote.call(params[:id]) do
59
+ on(:ok) do
60
+ broadcast_destroy_note params[:id]
61
+ render json: { message: "✔" }
62
+ end
63
+ on(:invalid) do |message|
64
+ render json: { message: I18n.t("decidim.notify.conversations.destroy.error", message: message) }, status: :unprocessable_entity
65
+ end
66
+ end
67
+ end
68
+
69
+ def users
70
+ enforce_permission_to :create, :notes
71
+
72
+ respond_to do |format|
73
+ format.json do
74
+ if (term = params[:term].to_s).present?
75
+ query = Author.for(current_component).joins(:user)
76
+ query = query.where("decidim_notify_authors.code=:code OR decidim_users.name ILIKE :term OR decidim_users.nickname ILIKE :term", code: term.to_i, term: "%#{term}%")
77
+
78
+ render json: query.all.collect { |u| { id: u.code, name: u.name, avatar: u&.avatar_url, nickname: u.nickname, text: format_user_name(u) } }
79
+ else
80
+ render json: []
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def chapters
89
+ @chapters ||= Chapter.for(current_component).all
90
+ end
91
+
92
+ def active_chapter
93
+ chapters.find(&:active)
94
+ end
95
+
96
+ def unclassified_notes
97
+ @unclassified_notes ||= Note.for(current_component).unclassified
98
+ end
99
+
100
+ def format_user_name(user)
101
+ "#{user.name} (@#{user.nickname})"
102
+ end
103
+ end
104
+ end
105
+ end