decidim-meetings 0.13.1 → 0.14.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/config/admin/decidim_meetings_manifest.js +1 -0
  3. data/app/assets/javascripts/decidim/meetings/admin/registrations_invite_form.es6 +25 -0
  4. data/app/cells/decidim/meetings/join_meeting_button/show.erb +2 -0
  5. data/app/cells/decidim/meetings/meeting_cell.rb +1 -1
  6. data/app/commands/decidim/meetings/admin/invite_user_to_join_meeting.rb +57 -8
  7. data/app/commands/decidim/meetings/admin/validate_registration_code.rb +51 -0
  8. data/app/commands/decidim/meetings/decline_invitation.rb +45 -0
  9. data/app/commands/decidim/meetings/join_meeting.rb +8 -3
  10. data/app/controllers/decidim/meetings/admin/attachment_collections_controller.rb +0 -4
  11. data/app/controllers/decidim/meetings/admin/attachments_controller.rb +0 -4
  12. data/app/controllers/decidim/meetings/admin/invites_controller.rb +14 -5
  13. data/app/controllers/decidim/meetings/admin/registrations_controller.rb +20 -0
  14. data/app/controllers/decidim/meetings/registrations_controller.rb +16 -0
  15. data/app/events/decidim/meetings/registration_code_validated_event.rb +15 -0
  16. data/app/forms/decidim/meetings/admin/close_meeting_form.rb +1 -1
  17. data/app/forms/decidim/meetings/admin/meeting_registration_invite_form.rb +9 -2
  18. data/app/forms/decidim/meetings/admin/validate_registration_code_form.rb +33 -0
  19. data/app/mailers/decidim/meetings/registration_mailer.rb +2 -1
  20. data/app/models/decidim/meetings/invite.rb +38 -0
  21. data/app/models/decidim/meetings/meeting.rb +6 -0
  22. data/app/models/decidim/meetings/registration.rb +18 -0
  23. data/app/permissions/decidim/meetings/admin/permissions.rb +2 -2
  24. data/app/permissions/decidim/meetings/permissions.rb +8 -1
  25. data/app/presenters/decidim/meetings/admin_log/invite_presenter.rb +35 -0
  26. data/app/presenters/decidim/meetings/invite_presenter.rb +26 -0
  27. data/app/queries/decidim/meetings/admin/invites.rb +59 -0
  28. data/app/serializers/decidim/meetings/data_portability_invite_serializer.rb +35 -0
  29. data/app/serializers/decidim/meetings/data_portability_registration_serializer.rb +1 -0
  30. data/app/serializers/decidim/meetings/registration_serializer.rb +1 -0
  31. data/app/views/decidim/meetings/admin/invite_join_meeting_mailer/invite.html.erb +1 -0
  32. data/app/views/decidim/meetings/admin/invites/_form.html.erb +34 -5
  33. data/app/views/decidim/meetings/admin/invites/index.html.erb +99 -0
  34. data/app/views/decidim/meetings/admin/meeting_copies/new.html.erb +1 -1
  35. data/app/views/decidim/meetings/admin/meetings/_form.html.erb +2 -3
  36. data/app/views/decidim/meetings/admin/meetings/index.html.erb +2 -0
  37. data/app/views/decidim/meetings/admin/registrations/_form.html.erb +1 -1
  38. data/app/views/decidim/meetings/admin/registrations/edit.html.erb +24 -0
  39. data/app/views/decidim/meetings/meetings/_meetings.html.erb +1 -1
  40. data/app/views/decidim/meetings/meetings/index.js.erb +0 -1
  41. data/app/views/decidim/meetings/meetings/show.html.erb +9 -0
  42. data/app/views/decidim/meetings/registration_mailer/confirmation.html.erb +2 -0
  43. data/app/views/decidim/participatory_spaces/_meeting.html.erb +1 -1
  44. data/app/views/devise/mailer/join_meeting.html.erb +1 -0
  45. data/app/views/devise/mailer/join_meeting.text.erb +4 -0
  46. data/config/locales/ca.yml +53 -5
  47. data/config/locales/en.yml +53 -5
  48. data/config/locales/es-PY.yml +52 -4
  49. data/config/locales/es.yml +53 -5
  50. data/config/locales/eu.yml +52 -4
  51. data/config/locales/fi.yml +164 -116
  52. data/config/locales/fr.yml +53 -5
  53. data/config/locales/gl.yml +52 -4
  54. data/config/locales/hu.yml +391 -0
  55. data/config/locales/it.yml +52 -4
  56. data/config/locales/nl.yml +52 -4
  57. data/config/locales/pl.yml +52 -4
  58. data/config/locales/pt-BR.yml +56 -8
  59. data/config/locales/pt.yml +52 -4
  60. data/config/locales/ru.yml +57 -9
  61. data/config/locales/sv.yml +135 -87
  62. data/config/locales/uk.yml +56 -8
  63. data/db/migrate/20180607142020_create_decidim_meetings_invites.rb +15 -0
  64. data/db/migrate/20180615160839_add_code_to_decidim_meetings_registrations.rb +7 -0
  65. data/db/migrate/20180711111023_add_validated_at_to_decidim_meetings_registrations.rb +7 -0
  66. data/lib/decidim/meetings.rb +1 -0
  67. data/lib/decidim/meetings/admin_engine.rb +3 -1
  68. data/lib/decidim/meetings/component.rb +2 -0
  69. data/lib/decidim/meetings/engine.rb +1 -0
  70. data/lib/decidim/meetings/registrations.rb +14 -0
  71. data/lib/decidim/meetings/registrations/code_generator.rb +39 -0
  72. data/lib/decidim/meetings/test/factories.rb +20 -4
  73. data/lib/decidim/meetings/version.rb +1 -1
  74. metadata +30 -14
  75. data/app/views/decidim/meetings/admin/invites/new.html.erb +0 -21
@@ -8,9 +8,16 @@ module Decidim
8
8
  class MeetingRegistrationInviteForm < Form
9
9
  attribute :name, String
10
10
  attribute :email, String
11
+ attribute :user_id, Integer
12
+ attribute :existing_user, Boolean, default: false
11
13
 
12
- validates :name, presence: true
13
- validates :email, presence: true, 'valid_email_2/email': { disposable: true }
14
+ validates :name, presence: true, unless: proc { |object| object.existing_user }
15
+ validates :email, presence: true, 'valid_email_2/email': { disposable: true }, unless: proc { |object| object.existing_user }
16
+ validates :user, presence: true, if: proc { |object| object.existing_user }
17
+
18
+ def user
19
+ @user ||= current_organization.users.find_by(id: user_id)
20
+ end
14
21
  end
15
22
  end
16
23
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ module Admin
6
+ # This class holds a Form to validate registration codes from Decidim's admin panel.
7
+ class ValidateRegistrationCodeForm < Decidim::Form
8
+ attribute :code, String
9
+
10
+ validates :code, presence: true
11
+ validate :registration_exists
12
+
13
+ def registration
14
+ @registration ||= meeting.registrations.find_by(code: code, validated_at: nil)
15
+ end
16
+
17
+ private
18
+
19
+ def meeting
20
+ @meeting ||= context[:meeting]
21
+ end
22
+
23
+ def registration_exists
24
+ return unless registration.nil?
25
+ errors.add(
26
+ :code,
27
+ I18n.t("registrations.validate_registration_code.invalid", scope: "decidim.meetings.admin")
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -11,10 +11,11 @@ module Decidim
11
11
  helper Decidim::ResourceHelper
12
12
  helper Decidim::TranslationsHelper
13
13
 
14
- def confirmation(user, meeting)
14
+ def confirmation(user, meeting, registration)
15
15
  with_user(user) do
16
16
  @user = user
17
17
  @meeting = meeting
18
+ @registration = registration
18
19
  @organization = @meeting.organization
19
20
  @locator = Decidim::ResourceLocatorPresenter.new(@meeting)
20
21
 
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ # The data store for an Invite in the Decidim::Meetings component.
6
+ class Invite < Meetings::ApplicationRecord
7
+ include Decidim::Traceable
8
+ include Decidim::Loggable
9
+ include Decidim::DataPortability
10
+
11
+ belongs_to :meeting, foreign_key: "decidim_meeting_id", class_name: "Decidim::Meetings::Meeting"
12
+ belongs_to :user, foreign_key: "decidim_user_id", class_name: "Decidim::User"
13
+
14
+ validates :user, uniqueness: { scope: :meeting }
15
+
16
+ def self.export_serializer
17
+ Decidim::Meetings::DataPortabilityInviteSerializer
18
+ end
19
+
20
+ def self.log_presenter_class_for(_log)
21
+ Decidim::Meetings::AdminLog::InvitePresenter
22
+ end
23
+
24
+ def self.user_collection(user)
25
+ where(decidim_user_id: user.id)
26
+ end
27
+
28
+ def accept!
29
+ update!(accepted_at: Time.current, rejected_at: nil)
30
+ end
31
+
32
+ def reject!
33
+ update!(rejected_at: Time.current, accepted_at: nil)
34
+ end
35
+ alias decline! reject!
36
+ end
37
+ end
38
+ end
@@ -20,6 +20,7 @@ module Decidim
20
20
 
21
21
  belongs_to :organizer, foreign_key: "organizer_id", class_name: "Decidim::User", optional: true
22
22
  has_many :registrations, class_name: "Decidim::Meetings::Registration", foreign_key: "decidim_meeting_id", dependent: :destroy
23
+ has_many :invites, class_name: "Decidim::Meetings::Invite", foreign_key: "decidim_meeting_id", dependent: :destroy
23
24
  has_one :minutes, class_name: "Decidim::Meetings::Minutes", foreign_key: "decidim_meeting_id", dependent: :destroy
24
25
  has_one :agenda, class_name: "Decidim::Meetings::Agenda", foreign_key: "decidim_meeting_id", dependent: :destroy
25
26
 
@@ -85,6 +86,11 @@ module Decidim
85
86
  commentable? && !component.current_settings.comments_blocked
86
87
  end
87
88
 
89
+ # Public: Overrides the `allow_resource_permissions?` Resourceable concern method.
90
+ def allow_resource_permissions?
91
+ component.settings.resources_permissions_enabled
92
+ end
93
+
88
94
  # Public: Overrides the `comments_have_alignment?` Commentable concern method.
89
95
  def comments_have_alignment?
90
96
  true
@@ -10,6 +10,10 @@ module Decidim
10
10
  belongs_to :user, foreign_key: "decidim_user_id", class_name: "Decidim::User"
11
11
 
12
12
  validates :user, uniqueness: { scope: :meeting }
13
+ validates :code, uniqueness: { allow_blank: true, scope: :meeting }
14
+ validates :code, presence: true, on: :create
15
+
16
+ before_validation :generate_code, on: :create
13
17
 
14
18
  def self.user_collection(user)
15
19
  where(decidim_user_id: user.id)
@@ -18,6 +22,20 @@ module Decidim
18
22
  def self.export_serializer
19
23
  Decidim::Meetings::DataPortabilityRegistrationSerializer
20
24
  end
25
+
26
+ private
27
+
28
+ def generate_code
29
+ self[:code] ||= calculate_registration_code
30
+ end
31
+
32
+ # Calculates a unique code for the model using the class
33
+ # provided by the configuration and scoped to the meeting.
34
+ #
35
+ # Returns a String.
36
+ def calculate_registration_code
37
+ Decidim::Meetings::Registrations.code_generator.generate(self)
38
+ end
21
39
  end
22
40
  end
23
41
  end
@@ -33,9 +33,9 @@ module Decidim
33
33
  return unless permission_action.subject == :meeting
34
34
 
35
35
  case permission_action.action
36
- when :close, :copy, :destroy, :export_registrations, :update
36
+ when :close, :copy, :destroy, :export_registrations, :update, :read_invites
37
37
  toggle_allow(meeting.present?)
38
- when :invite_user
38
+ when :invite_attendee
39
39
  toggle_allow(meeting.present? && meeting.registrations_enabled?)
40
40
  when :create
41
41
  allow!
@@ -17,6 +17,8 @@ module Decidim
17
17
  toggle_allow(can_join_meeting?)
18
18
  when :leave
19
19
  toggle_allow(can_leave_meeting?)
20
+ when :decline_invitation
21
+ toggle_allow(can_decline_invitation?)
20
22
  end
21
23
 
22
24
  permission_action
@@ -30,12 +32,17 @@ module Decidim
30
32
 
31
33
  def can_join_meeting?
32
34
  meeting.can_be_joined_by?(user) &&
33
- authorized?(:join)
35
+ authorized?(:join, resource: meeting)
34
36
  end
35
37
 
36
38
  def can_leave_meeting?
37
39
  meeting.registrations_enabled?
38
40
  end
41
+
42
+ def can_decline_invitation?
43
+ meeting.registrations_enabled? &&
44
+ meeting.invites.where(user: user).exists?
45
+ end
39
46
  end
40
47
  end
41
48
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ module AdminLog
6
+ # This class holds the logic to present a `Decidim::Meetings::Invite`
7
+ # for the `AdminLog` log.
8
+ #
9
+ # Usage should be automatic and you shouldn't need to call this class
10
+ # directly, but here's an example:
11
+ #
12
+ # action_log = Decidim::ActionLog.last
13
+ # view_helpers # => this comes from the views
14
+ # InvitePresenter.new(action_log, view_helpers).present
15
+ class InvitePresenter < Decidim::Log::BasePresenter
16
+ private
17
+
18
+ def action_string
19
+ case action
20
+ when "create", "delete", "update"
21
+ "decidim.meetings.admin_log.invite.#{action}"
22
+ else
23
+ super
24
+ end
25
+ end
26
+
27
+ def i18n_params
28
+ super.merge(
29
+ attendee_name: action_log.resource.user.name
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ #
6
+ # Decorator for meeting invites
7
+ #
8
+ class InvitePresenter < SimpleDelegator
9
+ def status
10
+ return I18n.t("accepted", scope: "decidim.meetings.models.invite.status", at: I18n.l(accepted_at, format: :decidim_short)) if accepted_at.present?
11
+ return I18n.t("rejected", scope: "decidim.meetings.models.invite.status", at: I18n.l(rejected_at, format: :decidim_short)) if rejected_at.present?
12
+ return I18n.t("sent", scope: "decidim.meetings.models.invite.status") if sent_at.present?
13
+
14
+ "-"
15
+ end
16
+
17
+ def status_html_class
18
+ return "success" if accepted_at.present?
19
+ return "danger" if rejected_at.present?
20
+ return "warning" if sent_at.present?
21
+
22
+ ""
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ module Admin
6
+ # A class used to find the Invites by their status status.
7
+ class Invites < Rectify::Query
8
+ # Syntactic sugar to initialize the class and return the queried objects.
9
+ #
10
+ # invites - the initial Invites relation that needs to be filtered.
11
+ # query - query to filter invites
12
+ # status - invite status to be used as a filter
13
+ def self.for(invites, query = nil, status = nil)
14
+ new(invites, query, status).query
15
+ end
16
+
17
+ # Initializes the class.
18
+ #
19
+ # invites - the initial Invites relation that need to be filtered
20
+ # query - query to filter invites
21
+ # status - invite status to be used as a filter
22
+ def initialize(invites, query = nil, status = nil)
23
+ @invites = invites
24
+ @query = query
25
+ @status = status
26
+ end
27
+
28
+ # List the invites by the different filters.
29
+ def query
30
+ @invites = filter_by_search(@invites)
31
+ @invites = filter_by_status(@invites)
32
+ @invites
33
+ end
34
+
35
+ private
36
+
37
+ def filter_by_search(invites)
38
+ return invites if @query.blank?
39
+ invites.joins(:user).where(
40
+ User.arel_table[:name].lower.matches("%#{@query}%").or(User.arel_table[:email].lower.matches("%#{@query}%"))
41
+ )
42
+ end
43
+
44
+ def filter_by_status(invites)
45
+ case @status
46
+ when "accepted"
47
+ invites.where.not(accepted_at: nil)
48
+ when "rejected"
49
+ invites.where.not(rejected_at: nil)
50
+ when "sent"
51
+ invites.where.not(sent_at: nil).where(accepted_at: nil, rejected_at: nil)
52
+ else
53
+ invites
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ class DataPortabilityInviteSerializer < Decidim::Exporters::Serializer
6
+ # Serializes an invite for data portability
7
+ def serialize
8
+ {
9
+ id: resource.id,
10
+ sent_at: resource.sent_at,
11
+ accepted_at: resource.accepted_at,
12
+ rejected_at: resource.rejected_at,
13
+ user: {
14
+ name: resource.user.name,
15
+ email: resource.user.email
16
+ },
17
+ meeting: {
18
+ title: resource.meeting.title,
19
+ description: resource.meeting.description,
20
+ start_time: resource.meeting.start_time,
21
+ end_time: resource.meeting.end_time,
22
+ address: resource.meeting.address,
23
+ location: resource.meeting.location,
24
+ location_hints: resource.meeting.location_hints,
25
+ reference: resource.meeting.reference,
26
+ attendees_count: resource.meeting.attendees_count,
27
+ attending_organizations: resource.meeting.attending_organizations,
28
+ closed_at: resource.meeting.closed_at,
29
+ closing_report: resource.meeting.closing_report
30
+ }
31
+ }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -7,6 +7,7 @@ module Decidim
7
7
  def serialize
8
8
  {
9
9
  id: resource.id,
10
+ code: resource.code,
10
11
  user: {
11
12
  name: resource.user.name,
12
13
  email: resource.user.email
@@ -7,6 +7,7 @@ module Decidim
7
7
  def serialize
8
8
  {
9
9
  id: resource.id,
10
+ code: resource.code,
10
11
  user: {
11
12
  name: resource.user.name,
12
13
  email: resource.user.email
@@ -4,4 +4,5 @@
4
4
  <%= t ".invited_you_to_join_a_meeting", invited_by: @invited_by.name, application: @user.organization.name %>
5
5
  </p>
6
6
 
7
+ <p><%= link_to t(".decline", meeting_title: translated_attribute(@meeting.title)),routes.decline_invitation_meeting_registration_path(meeting_id: @meeting, participatory_space_id: @meeting.component.participatory_space) %>
7
8
  <p><%= link_to t(".join", meeting_title: translated_attribute(@meeting.title)),routes.meeting_registration_url(meeting_id: @meeting, participatory_space_id: @meeting.component.participatory_space) %>
@@ -1,7 +1,36 @@
1
- <div class="row column">
2
- <%= form.text_field :name %>
3
- </div>
1
+ <div class="attendee-fields">
2
+ <div class="row column">
3
+ <fieldset class="check-radio-collection">
4
+ <legend><%= t(".attendee_type") %></legend>
5
+ <%= form.collection_radio_buttons(:existing_user, [[t(".non_user"), false], [t(".existing_user"), true]], :last, :first) %>
6
+ </fieldset>
7
+ </div>
8
+
9
+ <div class="text-warning attendee-fields--new-user">
10
+ <p><%= t(".invite_explanation") %></p>
11
+ </div>
12
+
13
+ <div class="grid-x grid-margin-x">
14
+ <div class="auto cell attendee-fields--new-user">
15
+ <%= form.text_field :name, disabled: disabled %>
16
+ </div>
17
+ <div class="auto cell attendee-fields--new-user">
18
+ <%= form.text_field :email, disabled: disabled %>
19
+ </div>
4
20
 
5
- <div class="row column">
6
- <%= form.text_field :email %>
21
+ <div class="auto cell attendee-fields--user-picker">
22
+ <% prompt_options = { url: decidim_admin.users_organization_url, placeholder: t(".select_user") } %>
23
+ <%= form.autocomplete_select(:user_id, form.object.user.presence, { multiple: false, class: "autocomplete-field--results-inline" }, prompt_options) do |user|
24
+ { value: user.id, label: "#{user.name} (@#{user.nickname})" }
25
+ end %>
26
+ </div>
27
+
28
+ <div class="shrink cell">
29
+ <div class="text-center mt-sm">
30
+ <%= form.submit t(".invite"), disabled: disabled %>
31
+ </div>
32
+ </div>
33
+ </div>
7
34
  </div>
35
+
36
+ <%= javascript_include_tag "decidim/meetings/admin/registrations_invite_form" %>
@@ -0,0 +1,99 @@
1
+ <div class="card">
2
+ <div class="card-divider">
3
+ <h2 class="card-title"><%= t(".invite_attendee") %></h2>
4
+ </div>
5
+ <div class="card-section">
6
+ <%= decidim_form_for(@form, url: meeting_registrations_invites_path, method: :post, html: { class: "form new_meeting_registration_invite" }) do |f| %>
7
+ <% disable_form = !allowed_to?(:invite_attendee, :meeting, meeting: meeting) %>
8
+
9
+ <%= render partial: "form", object: f, locals: { disabled: disable_form } %>
10
+
11
+ <% unless meeting.registrations_enabled? %>
12
+ <div class="text-alert">
13
+ <p><%= t(".registrations_disabled") %></p>
14
+ </div>
15
+ <% end %>
16
+ <% end %>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="filters grid-x mt-m">
21
+ <div class="medium-7">
22
+ <span class="dropdown-menu-inverted_label"><%= t(".filter_by") %> :</span>
23
+ <ul class="dropdown menu dropdown-inverted" data-dropdown-menu data-click-open="true" data-close-on-click-inside="false">
24
+ <li class="is-dropdown-submenu-parent">
25
+ <a href="#">
26
+ <% if @status.present? %>
27
+ <%= t(".filter.#{@status}") %>
28
+ <% else %>
29
+ <%= t(".filter.all") %>
30
+ <% end %>
31
+ </a>
32
+ <ul class="menu is-dropdown-submenu">
33
+ <li><%= link_to t(".filter.sent"), url_for(status: "sent", q: @query) %></li>
34
+ <li><%= link_to t(".filter.accepted"), url_for(status: "accepted", q: @query) %></li>
35
+ <li><%= link_to t(".filter.rejected"), url_for(status: "rejected", q: @query) %></li>
36
+ <li><%= link_to t(".filter.all"), url_for(q: @query) %></li>
37
+ </ul>
38
+ </li>
39
+ </ul>
40
+ </div>
41
+ <div class="medium-5">
42
+ <%= form_tag "", method: :get do %>
43
+ <div class="filters__search">
44
+ <div class="input-group">
45
+ <%= search_field_tag :q, @query,label: false, class: "input-group-field", placeholder: t(".search") %>
46
+ <%= hidden_field_tag :state, @state %>
47
+ <div class="input-group-button">
48
+ <button type="submit" class="button button--muted">
49
+ <%= icon "magnifying-glass", aria_label: t(".search") %>
50
+ </button>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ <% end %>
55
+ </div>
56
+ </div>
57
+
58
+ <div class="card" id="meeting-invites">
59
+ <div class="card-divider">
60
+ <h2 class="card-title"><%= title %> - <%= t(".invites") %></h2>
61
+ </div>
62
+
63
+ <div class="card-section">
64
+ <div class="table-scroll">
65
+ <table class="table-list">
66
+ <thead>
67
+ <tr>
68
+ <th><%= t("models.invite.fields.name", scope: "decidim.meetings") %></th>
69
+ <th><%= t("models.invite.fields.email", scope: "decidim.meetings") %></th>
70
+ <th><%= t("models.invite.fields.sent_at", scope: "decidim.meetings") %></th>
71
+ <th><%= t("models.invite.fields.status", scope: "decidim.meetings") %></th>
72
+ </tr>
73
+ </thead>
74
+ <tbody>
75
+ <% invites.each do |invite| %>
76
+ <% presenter = Decidim::Meetings::InvitePresenter.new(invite) %>
77
+ <tr data-id="<%= invite.id %>">
78
+ <td>
79
+ <%= invite.user.name %>
80
+ </td>
81
+ <td>
82
+ <%= invite.user.email %>
83
+ </td>
84
+ <td>
85
+ <% if invite.sent_at %>
86
+ <%= l invite.sent_at, format: :long %>
87
+ <% end %>
88
+ </td>
89
+ <td class="<%= presenter.status_html_class %>">
90
+ <%= presenter.status %>
91
+ </td>
92
+ </tr>
93
+ <% end %>
94
+ </tbody>
95
+ </table>
96
+ <%= paginate invites, theme: "decidim" %>
97
+ </div>
98
+ </div>
99
+ </div>