Browse Source

Merge branch 'feature/subgroups'

Maarten van den Berg 7 years ago
parent
commit
f1c38c509e
50 changed files with 1299 additions and 215 deletions
  1. 3 0
      .rbenv-vars-sample
  2. 3 0
      Gemfile
  3. 7 1
      Gemfile.lock
  4. 42 0
      app/assets/javascripts/activities.coffee
  5. 87 84
      app/assets/javascripts/buttonhandlers.jsx.js
  6. 160 10
      app/controllers/activities_controller.rb
  7. 2 1
      app/controllers/dashboard_controller.rb
  8. 57 13
      app/controllers/groups_controller.rb
  9. 4 0
      app/helpers/authentication_helper.rb
  10. 35 1
      app/mailers/participant_mailer.rb
  11. 149 5
      app/models/activity.rb
  12. 6 0
      app/models/default_subgroup.rb
  13. 3 0
      app/models/group.rb
  14. 28 1
      app/models/participant.rb
  15. 15 0
      app/models/subgroup.rb
  16. 43 4
      app/views/activities/_form.html.erb
  17. 3 6
      app/views/activities/_state_counts.html.haml
  18. 75 5
      app/views/activities/edit.html.haml
  19. 31 0
      app/views/activities/edit_subgroups.html.haml
  20. 19 8
      app/views/activities/index.html.haml
  21. 2 2
      app/views/activities/mass_new.html.haml
  22. 66 34
      app/views/activities/show.html.haml
  23. 28 0
      app/views/activities/subgroup_editor.html.haml
  24. 15 14
      app/views/dashboard/home.html.haml
  25. 0 6
      app/views/groups/edit.html.erb
  26. 62 0
      app/views/groups/edit.html.haml
  27. 14 5
      app/views/groups/mass_add_members.html.haml
  28. 4 1
      app/views/participant_mailer/attendance_reminder.html.haml
  29. 5 1
      app/views/participant_mailer/attendance_reminder.text.erb
  30. 32 0
      app/views/participant_mailer/subgroup_notification.html.haml
  31. 25 0
      app/views/participant_mailer/subgroup_notification.text.erb
  32. 8 0
      config/locales/aardbei_en.yml
  33. 8 1
      config/locales/aardbei_nl.yml
  34. 52 1
      config/locales/activities/en.yml
  35. 58 3
      config/locales/activities/nl.yml
  36. 16 0
      config/locales/defaultsubgroups_en.yml
  37. 16 0
      config/locales/defaultsubgroups_nl.yml
  38. 5 0
      config/locales/groups/en.yml
  39. 5 0
      config/locales/groups/nl.yml
  40. 21 0
      config/locales/translation_nl.yml
  41. 13 0
      config/routes.rb
  42. 11 0
      db/migrate/20170930201201_create_default_subgroups.rb
  43. 11 0
      db/migrate/20171001124009_create_subgroups.rb
  44. 5 0
      db/migrate/20171001150124_add_subgroup_to_participants.rb
  45. 6 0
      db/migrate/20171023080215_add_subgroup_job_markers_to_activities.rb
  46. 5 0
      db/migrate/20180206181016_add_no_response_action_to_activities.rb
  47. 26 3
      db/schema.rb
  48. 4 2
      db/seeds.rb
  49. 2 2
      public/batch_activities.csv
  50. 2 1
      public/batch_persons.csv

+ 3 - 0
.rbenv-vars-sample

@@ -35,3 +35,6 @@ MAILGUN_API_KEY=
35 35
 
36 36
 # Do we bind to a socket or port?
37 37
 PUMA_BIND=
38
+
39
+# Set to enable Sentry reporting
40
+SENTRY_DSN=

+ 3 - 0
Gemfile

@@ -64,6 +64,9 @@ gem 'delayed_job'
64 64
 gem 'delayed_job_active_record'
65 65
 gem 'daemons'
66 66
 
67
+# Error reporting
68
+gem 'sentry-raven'
69
+
67 70
 group :development, :test do
68 71
   # Call 'byebug' anywhere in the code to stop execution and get a debugger console
69 72
   gem 'byebug', platform: :mri

+ 7 - 1
Gemfile.lock

@@ -75,6 +75,8 @@ GEM
75 75
     execjs (2.7.0)
76 76
     faker (1.7.3)
77 77
       i18n (~> 0.5)
78
+    faraday (0.14.0)
79
+      multipart-post (>= 1.2, < 3)
78 80
     ffi (1.9.18)
79 81
     font-awesome-sass (4.7.0)
80 82
       sass (>= 3.2)
@@ -113,6 +115,7 @@ GEM
113 115
     mini_portile2 (2.1.0)
114 116
     minitest (5.10.1)
115 117
     multi_json (1.12.1)
118
+    multipart-post (2.0.0)
116 119
     netrc (0.11.0)
117 120
     nio4r (2.0.0)
118 121
     nokogiri (1.7.1)
@@ -163,6 +166,8 @@ GEM
163 166
       sprockets (>= 2.8, < 4.0)
164 167
       sprockets-rails (>= 2.0, < 4.0)
165 168
       tilt (>= 1.1, < 3)
169
+    sentry-raven (2.7.2)
170
+      faraday (>= 0.7.6, < 1.0)
166 171
     spring (2.0.1)
167 172
       activesupport (>= 4.2)
168 173
     spring-watcher-listen (2.0.1)
@@ -231,6 +236,7 @@ DEPENDENCIES
231 236
   rabl
232 237
   rails (~> 5.0.0, >= 5.0.0.1)
233 238
   sass-rails (~> 5.0)
239
+  sentry-raven
234 240
   spring
235 241
   spring-watcher-listen (~> 2.0.0)
236 242
   sqlite3
@@ -243,4 +249,4 @@ DEPENDENCIES
243 249
   yard
244 250
 
245 251
 BUNDLED WITH
246
-   1.13.6
252
+   1.16.0

+ 42 - 0
app/assets/javascripts/activities.coffee

@@ -2,6 +2,7 @@ $(document).on 'turbolinks:load', ->
2 2
   clipboard = new Clipboard('.copy-reactions', {
3 3
     'text': clipreactions
4 4
   })
5
+  $('.subgroup-filter').on('change', (e) -> filterparticipants(e))
5 6
 
6 7
 @clipreactions = (trigger) ->
7 8
   id = trigger.dataset['activity']
@@ -24,3 +25,44 @@ $(document).on 'turbolinks:load', ->
24 25
     res.push(resp['unknown']['message'])
25 26
 
26 27
   res.join('\n')
28
+
29
+@filterparticipants = (e) ->
30
+  show = e.target.value
31
+  if (show == 'all')
32
+    $('.participant-row').show()
33
+    @updatecounts()
34
+    this.subgroupfilter = null
35
+  else if (show == 'withoutgroup')
36
+    selector = "tr.participant-row.success:not([data-subgroup-id])"
37
+    $('.participant-row').hide()
38
+    $(selector).show()
39
+    @updatecounts()
40
+    this.subgroupfilter = show
41
+  else
42
+    selector = "[data-subgroup-id=" + e.target.value + "]"
43
+    $('.participant-row').hide()
44
+    $(selector).show()
45
+    @updatecounts(show)
46
+    this.subgroupfilter = show
47
+
48
+@updatecounts = (subgroupid) ->
49
+  selector = 'tr.countable.participant-row'
50
+  selectorend = '[style!="display: none;"]'
51
+
52
+  if (subgroupid)
53
+    selectorend = '[data-subgroup-id=' + subgroupid + ']' + selectorend
54
+
55
+  pselect = selector + '.success' + selectorend
56
+  uselect = selector + '.warning' + selectorend
57
+  aselect = selector + '.danger' + selectorend
58
+
59
+  numall = $(selector + selectorend).length
60
+  numpresent = $(pselect).length
61
+  numunknown = $(uselect).length
62
+  numabsent  = $(aselect).length
63
+
64
+  $('.state-count.all-count').html(numall)
65
+  $('.state-count.present-count').html(numpresent)
66
+  $('.state-count.unknown-count').html(numunknown)
67
+  $('.state-count.absent-count').html(numabsent)
68
+  [numpresent, numabsent, numunknown]

+ 87 - 84
app/assets/javascripts/buttonhandlers.jsx.js

@@ -13,55 +13,55 @@ function setup_handlers()
13 13
 // Creates an AJAX-request and registers the appropriate handlers once it is done.
14 14
 function change_presence(e)
15 15
 {
16
-	// Gather data
17
-	var group, person, activity, state;
18
-	group 	 = this.dataset["groupId"];
19
-	person   = this.dataset["personId"];
20
-	activity = this.dataset["activityId"];
21
-	rstate 	 = this.dataset["newState"];
22
-
23
-	var state;
24
-	switch (rstate)
25
-	{
26
-		case "present":
27
-			state = true;
28
-			break;
29
-
30
-		case "absent":
31
-			state = false;
32
-			break;
33
-
34
-		case "unknown":
35
-			state = null;
36
-			break;
37
-	}
38
-
39
-	// Make request
40
-	var req;
41
-	req = $.ajax(`/groups/${group}/activities/${activity}/presence`,
42
-		{
43
-		  method: 'PUT',
44
-		  data: {person_id: person, attending: state},
45
-		  statusCode: {
46
-			423: function() {
47
-				alert( "De deadline is al verstreken! Vraag orgi of bestuur of het nog kan.");
48
-			},
49
-			403: function() {
50
-				alert( "Je hebt geen rechten om iemand anders aan te passen!");
51
-			}
52
-		  }
53
-		}
54
-	)
55
-	.done( activity_changed );
56
-
57
-	// Pack data for success
58
-	req.aardbei_activity_data =
59
-		{
60
-			group: group,
61
-			person: person,
62
-			activity: activity,
63
-			state: state
64
-		};
16
+    // Gather data
17
+    var group, person, activity, state;
18
+    group    = this.dataset["groupId"];
19
+    person   = this.dataset["personId"];
20
+    activity = this.dataset["activityId"];
21
+    rstate   = this.dataset["newState"];
22
+
23
+    var state;
24
+    switch (rstate)
25
+    {
26
+        case "present":
27
+            state = true;
28
+            break;
29
+
30
+        case "absent":
31
+            state = false;
32
+            break;
33
+
34
+        case "unknown":
35
+            state = null;
36
+            break;
37
+    }
38
+
39
+    // Make request
40
+    var req;
41
+    req = $.ajax(`/groups/${group}/activities/${activity}/presence`,
42
+        {
43
+          method: 'PUT',
44
+          data: {person_id: person, attending: state},
45
+          statusCode: {
46
+            423: function() {
47
+                alert( "De deadline is al verstreken! Vraag orgi of bestuur of het nog kan.");
48
+            },
49
+            403: function() {
50
+                alert( "Je hebt geen rechten om iemand anders aan te passen!");
51
+            }
52
+          }
53
+        }
54
+    )
55
+    .done( activity_changed );
56
+
57
+    // Pack data for success
58
+    req.aardbei_activity_data =
59
+        {
60
+            group: group,
61
+            person: person,
62
+            activity: activity,
63
+            state: state
64
+        };
65 65
 }
66 66
 
67 67
 // Update all references on the page to this activity:
@@ -69,40 +69,40 @@ function change_presence(e)
69 69
 // 2. The present/absent buttons
70 70
 function activity_changed(data, textStatus, xhr)
71 71
 {
72
-	// Unpack activity-data
73
-	var target;
74
-	target = xhr.aardbei_activity_data;
75
-
76
-	// Determine what color and icons we're going to use
77
-	var new_rowclass;
78
-	var new_confirm_icon, new_decline_icon;
79
-	switch (target.state)
80
-	{
81
-		case true:
82
-			new_rowclass = "success";
83
-			new_confirm_icon = check_selected;
84
-			new_decline_icon = times_unselected;
85
-			break;
86
-
87
-		case false:
88
-			new_rowclass = "danger";
89
-			new_confirm_icon = check_unselected;
90
-			new_decline_icon = times_selected;
91
-			break;
92
-
93
-		case null:
94
-			new_rowclass = "warning";
95
-			new_confirm_icon = check_unselected;
96
-			new_decline_icon = times_unselected;
97
-			break;
98
-	}
99
-
100
-	// Update all tr's containing this person's presence
101
-	$(`tr[data-person-id=${target.person}][data-activity-id=${target.activity}]`)
102
-	  .removeClass('success danger warning')
103
-	  .addClass(new_rowclass);
104
-
105
-	// Update all buttons for this person's presence
72
+    // Unpack activity-data
73
+    var target;
74
+    target = xhr.aardbei_activity_data;
75
+
76
+    // Determine what color and icons we're going to use
77
+    var new_rowclass;
78
+    var new_confirm_icon, new_decline_icon;
79
+    switch (target.state)
80
+    {
81
+        case true:
82
+            new_rowclass = "success";
83
+            new_confirm_icon = check_selected;
84
+            new_decline_icon = times_unselected;
85
+            break;
86
+
87
+        case false:
88
+            new_rowclass = "danger";
89
+            new_confirm_icon = check_unselected;
90
+            new_decline_icon = times_selected;
91
+            break;
92
+
93
+        case null:
94
+            new_rowclass = "warning";
95
+            new_confirm_icon = check_unselected;
96
+            new_decline_icon = times_unselected;
97
+            break;
98
+    }
99
+
100
+    // Update all tr's containing this person's presence
101
+    $(`tr[data-person-id=${target.person}][data-activity-id=${target.activity}]`)
102
+      .removeClass('success danger warning')
103
+      .addClass(new_rowclass);
104
+
105
+    // Update all buttons for this person's presence
106 106
     $(`.btn-present[data-person-id=${target.person}][data-activity-id=${target.activity}]`)
107 107
       .html(new_confirm_icon);
108 108
     $(`.btn-absent[data-person-id=${target.person}][data-activity-id=${target.activity}]`)
@@ -113,11 +113,14 @@ function activity_changed(data, textStatus, xhr)
113 113
       .append(" Present");
114 114
     $(`.btn-absent[data-person-id=${target.person}][data-activity-id=${target.activity}][data-wide=1]`)
115 115
       .append(" Absent");
116
+
117
+    if (window.updatecounts != undefined)
118
+        updatecounts(window.subgroupfilter);
116 119
 }
117 120
 
118 121
 function alert_failure(data, textStatus, xhr)
119 122
 {
120
-	alert(`Something broke! We got a ${textStatus}, (${data}).`);
123
+    alert(`Something broke! We got a ${textStatus}, (${data}).`);
121 124
 }
122 125
 
123 126
 var check_unselected = '<i class="fa fa-check"></i>';

+ 160 - 10
app/controllers/activities_controller.rb

@@ -1,19 +1,40 @@
1 1
 class ActivitiesController < ApplicationController
2 2
   include GroupsHelper
3 3
   include ActivitiesHelper
4
-  before_action :set_activity_and_group, only: [:show, :edit, :update, :destroy, :presence, :change_organizer]
5
-  before_action :set_group,            except: [:show, :edit, :update, :destroy, :presence, :change_organizer]
4
+
5
+  has_activity_id = [
6
+    :show, :edit, :edit_subgroups, :update, :update_subgroups, :destroy,
7
+    :presence, :change_organizer, :create_subgroup, :update_subgroup,
8
+    :destroy_subgroup, :immediate_subgroups, :clear_subgroups
9
+  ]
10
+  before_action :set_activity_and_group, only: has_activity_id
11
+  before_action :set_group,            except: has_activity_id
12
+
13
+  before_action :set_subgroup, only: [:update_subgroup, :destroy_subgroup]
6 14
   before_action :require_membership!
7
-  before_action :require_leader!, only: [:mass_new, :mass_create, :new, :create, :destroy]
8
-  before_action :require_organizer!, only: [:edit, :update, :change_organizer]
15
+  before_action :require_leader!, only: [
16
+    :mass_new, :mass_create, :new, :create, :destroy
17
+  ]
18
+  before_action :require_organizer!, only: [
19
+    :edit, :update, :change_organizer, :create_subgroup, :update_subgroup,
20
+    :destroy_subgroup, :edit_subgroups, :update_subgroups, :immediate_subgroups,
21
+    :clear_subgroups
22
+  ]
9 23
 
10 24
   # GET /groups/:id/activities
11 25
   # GET /activities.json
12 26
   def index
13
-    @activities = @group.activities
14
-      .where('start > ?', Time.now)
15
-      .order(start: :asc)
16
-      .paginate(page: params[:page], per_page: 25)
27
+    if params[:past]
28
+      @activities = @group.activities
29
+        .where('start < ?', Time.now)
30
+        .order(start: :desc)
31
+        .paginate(page: params[:page], per_page: 25)
32
+    else
33
+      @activities = @group.activities
34
+        .where('start > ?', Time.now)
35
+        .order(start: :asc)
36
+        .paginate(page: params[:page], per_page: 25)
37
+    end
17 38
   end
18 39
 
19 40
   # GET /activities/1
@@ -33,6 +54,15 @@ class ActivitiesController < ApplicationController
33 54
       .find_by(person: current_person)
34 55
     @counts = @activity.state_counts
35 56
     @num_participants = @counts.values.sum
57
+    @assignable_subgroups = @activity.subgroups
58
+      .where(is_assignable: true)
59
+      .order(name: :asc)
60
+      .pluck(:name)
61
+    @subgroup_ids = @activity.subgroups
62
+      .order(name: :asc)
63
+      .pluck(:name, :id)
64
+    @subgroup_ids.prepend( [I18n.t('activities.subgroups.filter_nofilter'), 'all'] )
65
+    @subgroup_ids.append( [I18n.t('activities.subgroups.filter_nogroup'), 'withoutgroup'] )
36 66
   end
37 67
 
38 68
   # GET /activities/new
@@ -45,6 +75,81 @@ class ActivitiesController < ApplicationController
45 75
     set_edit_parameters!
46 76
   end
47 77
 
78
+  # GET /activities/1/edit_subgroups
79
+  def edit_subgroups
80
+    @subgroups = @activity.subgroups.order(is_assignable: :desc, name: :asc)
81
+
82
+    if @subgroups.none?
83
+      flash_message(:error, I18n.t('activities.errors.cannot_subgroup_without_subgroups'))
84
+      redirect_to group_activity_edit(@group, @activity)
85
+    end
86
+
87
+    @subgroup_options = @subgroups.map { |sg| [sg.name, sg.id] }
88
+    @subgroup_options.prepend(['--', 'nil'])
89
+
90
+    @participants = @activity.participants
91
+      .joins(:person)
92
+      .where.not(attending: false)
93
+      .order(:subgroup_id)
94
+      .order('people.first_name', 'people.last_name')
95
+  end
96
+
97
+  # POST /activities/1/update_subgroups
98
+  def update_subgroups
99
+    Participant.transaction do
100
+      # For each key in participant_subgroups:
101
+      params[:participant_subgroups].each do |k, v|
102
+        # Get Participant, Subgroup
103
+        p = Participant.find_by id: k
104
+        sg = Subgroup.find_by id: v unless v == 'nil'
105
+
106
+        # Verify that the Participant and Subgroup belong to this activity
107
+        # Edit-capability is enforced by before_filter.
108
+        if !p || p.activity != @activity || (!sg && v != 'nil') || (sg && sg.activity != @activity)
109
+          flash_message(:danger, I18n.t(:somethingbroke))
110
+          redirect_to group_activity_edit_subgroups_path(@group, @activity)
111
+          raise ActiveRecord::Rollback
112
+        end
113
+
114
+        if v != 'nil'
115
+          p.subgroup = sg
116
+        else
117
+          p.subgroup = nil
118
+        end
119
+
120
+        p.save
121
+      end
122
+    end
123
+
124
+    flash_message(:success, I18n.t('activities.subgroups.edited'))
125
+    redirect_to edit_group_activity_path(@group, @activity, anchor: 'subgroups')
126
+  end
127
+
128
+  # POST /activities/1/immediate_subgroups
129
+  def immediate_subgroups
130
+    if params[:overwrite]
131
+      @activity.clear_subgroups!
132
+    end
133
+
134
+    @activity.assign_subgroups!
135
+
136
+    if params[:overwrite]
137
+      flash_message(:success, I18n.t('activities.subgroups.redistributed'))
138
+    else
139
+      flash_message(:success, I18n.t('activities.subgroups.remaining_distributed'))
140
+    end
141
+
142
+    redirect_to edit_group_activity_path(@group, @activity)
143
+  end
144
+
145
+  # POST /activities/1/clear_subgroups
146
+  def clear_subgroups
147
+    @activity.clear_subgroups!
148
+
149
+    flash_message(:success, I18n.t('activities.subgroups.cleared'))
150
+    redirect_to edit_group_activity_path(@group, @activity)
151
+  end
152
+
48 153
   # Shared lookups for rendering the edit-view
49 154
   def set_edit_parameters!
50 155
     @non_organizers = @activity.participants.where(is_organizer: [false, nil])
@@ -55,6 +160,9 @@ class ActivitiesController < ApplicationController
55 160
 
56 161
     @non_organizers_options.sort!
57 162
     @organizers_options.sort!
163
+
164
+    @subgroup = Subgroup.new if !@subgroup
165
+    @subgroups = @activity.subgroups.order(is_assignable: :desc, name: :asc)
58 166
   end
59 167
 
60 168
   # POST /activities
@@ -91,7 +199,7 @@ class ActivitiesController < ApplicationController
91 199
     end
92 200
     flash_message(:success, message)
93 201
 
94
-    redirect_to edit_group_activity_path(@group, @activity)
202
+    redirect_to edit_group_activity_path(@group, @activity, anchor: 'organizers-add')
95 203
   end
96 204
 
97 205
   # PATCH/PUT /activities/1
@@ -125,6 +233,40 @@ class ActivitiesController < ApplicationController
125 233
     end
126 234
   end
127 235
 
236
+  # POST /activities/1/subgroups
237
+  def create_subgroup
238
+    @subgroup = Subgroup.new(subgroup_params)
239
+    @subgroup.activity = @activity
240
+
241
+    if @subgroup.save
242
+      flash_message :success, I18n.t('activities.subgroups.created')
243
+      redirect_to edit_group_activity_path(@group, @activity, anchor: 'subgroups-add')
244
+    else
245
+      flash_message :danger, I18n.t('activities.subgroups.create_failed')
246
+      set_edit_parameters!
247
+      render :edit
248
+    end
249
+  end
250
+
251
+  # PATCH /activities/1/subgroups/:subgroup_id
252
+  def update_subgroup
253
+    if @subgroup.update(subgroup_params)
254
+      flash_message :success, I18n.t('activities.subgroups.updated')
255
+      redirect_to edit_group_activity_path(@group, @activity, anchor: 'subgroups')
256
+    else
257
+      flash_message :danger, I18n.t('activities.subgroups.update_failed')
258
+      set_edit_parameters!
259
+      render :edit
260
+    end
261
+  end
262
+
263
+  # DELETE /activities/1/subgroups/:subgroup_id
264
+  def destroy_subgroup
265
+    @subgroup.destroy
266
+    flash_message :success, I18n.t('activities.subgroups.destroyed')
267
+    redirect_to edit_group_activity_path(@group, @activity, anchor: 'subgroups')
268
+  end
269
+
128 270
   # PATCH/PUT /groups/:group_id/activities/:id/presence
129 271
   # PATCH/PUT /groups/:group_id/activities/:id/presence.json
130 272
   def presence
@@ -176,8 +318,16 @@ class ActivitiesController < ApplicationController
176 318
       @group = Group.find(params[:group_id])
177 319
     end
178 320
 
321
+    def set_subgroup
322
+      @subgroup = Subgroup.find(params[:subgroup_id])
323
+    end
324
+
179 325
     # Never trust parameters from the scary internet, only allow the white list through.
180 326
     def activity_params
181
-      params.require(:activity).permit(:name, :description, :location, :start, :end, :deadline, :reminder_at)
327
+      params.require(:activity).permit(:name, :description, :location, :start, :end, :deadline, :reminder_at, :subgroup_division_enabled, :no_response_action)
328
+    end
329
+
330
+    def subgroup_params
331
+      params.require(:subgroup).permit(:name, :is_assignable)
182 332
     end
183 333
 end

+ 2 - 1
app/controllers/dashboard_controller.rb

@@ -7,10 +7,11 @@ class DashboardController < ApplicationController
7 7
       .joins(:activity)
8 8
       .where('activities.end >= ? OR (activities.end IS NULL AND activities.start >= ?)', DateTime.now, DateTime.now)
9 9
       .order('activities.start ASC')
10
-      .paginate(page: params[:upage], per_page: 10)
11 10
     @user_organized = @upcoming
12 11
       .where(is_organizer: true)
13 12
       .limit(3)
13
+    @upcoming = @upcoming
14
+      .paginate(page: params[:upage], per_page: 10)
14 15
     @need_response = @upcoming
15 16
       .where(attending: nil)
16 17
       .paginate(page: params[:nrpage], per_page: 5)

+ 57 - 13
app/controllers/groups_controller.rb

@@ -1,9 +1,10 @@
1 1
 class GroupsController < ApplicationController
2 2
   include GroupsHelper
3
-  before_action :set_group, only: [:show, :edit, :update, :destroy]
4
-  before_action :require_admin!, only: [:index, :process_mass_add_members, :mass_add_members]
3
+  before_action :set_group, only: [:show, :edit, :update, :destroy, :create_default_subgroup, :update_default_subgroup, :destroy_default_subgroup, :mass_add_members, :process_mass_add_members]
4
+  before_action :set_default_subgroup, only: [:update_default_subgroup, :destroy_default_subgroup]
5
+  before_action :require_admin!, only: [:index]
5 6
   before_action :require_membership!, only: [:show]
6
-  before_action :require_leader!, only: [:edit, :update, :destroy]
7
+  before_action :require_leader!, only: [:edit, :update, :destroy, :create_default_subgroup, :update_default_subgroup, :destroy_default_subgroup, :process_mass_add_members, :mass_add_members]
7 8
 
8 9
   # GET /groups
9 10
   # GET /groups.json
@@ -18,11 +19,13 @@ class GroupsController < ApplicationController
18 19
   # GET /groups/1
19 20
   # GET /groups/1.json
20 21
   def show
21
-    @organized_activities = current_person.organized_activities.
22
-      joins(:activity).where(
23
-        'activities.group_id': @group.id
24
-    )
25
-    if @organized_activities.count > 0
22
+    @organized_activities = current_person
23
+      .organized_activities
24
+      .joins(:activity)
25
+      .where('activities.group_id': @group.id)
26
+      .where('start > ?', Date.today)
27
+
28
+    if @organized_activities.any?
26 29
       @groupmenu = 'col-md-6'
27 30
     else
28 31
       @groupmenu = 'col-md-12'
@@ -45,6 +48,7 @@ class GroupsController < ApplicationController
45 48
 
46 49
   # GET /groups/1/edit
47 50
   def edit
51
+    @defaultsubgroup = DefaultSubgroup.new
48 52
   end
49 53
 
50 54
   # POST /groups
@@ -77,6 +81,7 @@ class GroupsController < ApplicationController
77 81
         }
78 82
         format.json { render :show, status: :ok, location: @group }
79 83
       else
84
+        @defaultsubgroup = DefaultSubgroup.new
80 85
         format.html { render :edit }
81 86
         format.json { render json: @group.errors, status: :unprocessable_entity }
82 87
       end
@@ -97,34 +102,73 @@ class GroupsController < ApplicationController
97 102
   end
98 103
 
99 104
   def mass_add_members
100
-    @group = Group.find(params[:group_id])
101 105
   end
102 106
 
103 107
   def process_mass_add_members
104
-    @group = Group.find(params[:group_id])
105 108
     require 'csv'
106 109
     uploaded_io = params[:spreadsheet]
107 110
     result = Person.from_csv(uploaded_io.read)
108 111
 
109 112
     result.each do |p|
110 113
       m = Member.find_by(person: p, group: @group)
111
-      if not m
114
+      unless m
112 115
         m = Member.new(person: p, group: @group)
113 116
         m.save!
114 117
       end
115 118
     end
116
-    flash_message(:success, "#{result.count} people added to group")
119
+    flash_message(:success, I18n.t('groups.mass_add_success', count: result.count))
117 120
     redirect_to group_members_path(@group)
118 121
   end
119 122
 
123
+  # POST /groups/:id/default_subgroups
124
+  def create_default_subgroup
125
+    @defaultsubgroup = DefaultSubgroup.new(default_subgroup_params)
126
+    @defaultsubgroup.group = @group
127
+
128
+    if @defaultsubgroup.save
129
+      flash_message(:success, I18n.t('defaultsubgroups.created'))
130
+      redirect_to edit_group_path(@group)
131
+    else
132
+      flash_message(:danger, I18n.t('defaultsubgroups.create_failed'))
133
+      render :edit
134
+    end
135
+  end
136
+
137
+  # PATCH /groups/:id/default_subgroups/:default_subgroup_id
138
+  def update_default_subgroup
139
+    if @defaultsubgroup.update(default_subgroup_params)
140
+      flash_message(:success, I18n.t('defaultsubgroups.updated'))
141
+      redirect_to edit_group_path(@group)
142
+    else
143
+      flash_message(:danger, I18n.t('defaultsubgroups.update_failed'))
144
+      render :edit
145
+    end
146
+  end
147
+
148
+  # DELETE /groups/:id/default_subgroups/:default_subgroup_id
149
+  def destroy_default_subgroup
150
+    @defaultsubgroup.destroy
151
+    flash_message(:info, I18n.t('defaultsubgroups.destroyed'))
152
+    redirect_to edit_group_path(@group)
153
+  end
154
+
120 155
   private
121 156
     # Use callbacks to share common setup or constraints between actions.
122 157
     def set_group
123
-      @group = Group.find(params[:id])
158
+      @group = Group.find(params[:group_id] || params[:id])
159
+    end
160
+
161
+    # Retrieve DefaultSubgroup to update or delete
162
+    def set_default_subgroup
163
+      @defaultsubgroup = DefaultSubgroup.find(params[:default_subgroup_id])
124 164
     end
125 165
 
126 166
     # Never trust parameters from the scary internet, only allow the white list through.
127 167
     def group_params
128 168
       params.require(:group).permit(:name)
129 169
     end
170
+
171
+    def default_subgroup_params
172
+      params.require(:default_subgroup).permit(:name, :is_assignable)
173
+    end
130 174
 end

+ 4 - 0
app/helpers/authentication_helper.rb

@@ -123,6 +123,10 @@ module AuthenticationHelper
123 123
       redirect_to controller: 'authentication', action: 'login_form'
124 124
       return false
125 125
     end
126
+
127
+    Raven.user_context(
128
+      user_firstname: current_person.first_name
129
+    )
126 130
     return true
127 131
   end
128 132
 

+ 35 - 1
app/mailers/participant_mailer.rb

@@ -3,7 +3,41 @@ class ParticipantMailer < ApplicationMailer
3 3
     @person = person
4 4
     @activity = activity
5 5
 
6
-    subject = I18n.t('activities.emails.attendance_reminder.subject', activity: @activity.name)
6
+    if activity.no_response_action # is true
7
+      key = 'activities.emails.attendance_reminder.subject_present'
8
+    else
9
+      key = 'activities.emails.attendance_reminder.subject_absent'
10
+    end
11
+
12
+    subject = I18n.t(key, activity: @activity.name)
13
+
14
+    mail(to: @person.email, subject: subject)
15
+  end
16
+
17
+  def subgroup_notification(person, activity, participant)
18
+    @person = person
19
+    @activity = activity
20
+
21
+    @subgroup = participant.subgroup.name
22
+
23
+    @others = participant
24
+      .subgroup
25
+      .participants
26
+      .where.not(person: @person)
27
+      .map { |pp| pp.person.full_name }
28
+      .sort
29
+      .join(', ')
30
+
31
+    @subgroups = @activity
32
+      .subgroups
33
+      .order(name: :asc)
34
+
35
+    @organizers = @activity
36
+      .organizer_names
37
+      .sort
38
+      .join(', ')
39
+
40
+    subject = I18n.t('activities.emails.subgroup_notification.subject', subgroup: @subgroup, activity: @activity.name)
7 41
 
8 42
     mail(to: @person.email, subject: subject)
9 43
   end

+ 149 - 5
app/models/activity.rb

@@ -38,6 +38,20 @@ class Activity < ApplicationRecord
38 38
   # @!attribute reminder_done
39 39
   #   @return [Boolean]
40 40
   #     whether or not sending the reminder has finished.
41
+  #
42
+  # @!attribute subgroup_division_enabled
43
+  #   @return [Boolean]
44
+  #     whether automatic subgroup division on the deadline is enabled.
45
+  #
46
+  # @!attribute subgroup_division_done
47
+  #   @return [Boolean]
48
+  #     whether subgroup division has been performed.
49
+  #
50
+  # @!attribute no_response_action
51
+  #   @return [Boolean]
52
+  #     what action to take when a participant has not responded and the
53
+  #     reminder is being sent. True to set the participant to attending, false
54
+  #     to set to absent.
41 55
 
42 56
   belongs_to :group
43 57
 
@@ -45,14 +59,25 @@ class Activity < ApplicationRecord
45 59
     dependent: :destroy
46 60
   has_many :people, through: :participants
47 61
 
62
+  has_many :subgroups,
63
+    dependent: :destroy
64
+
48 65
   validates :name, presence: true
49 66
   validates :start, presence: true
50 67
   validate  :deadline_before_start, unless: "self.deadline.blank?"
51 68
   validate  :end_after_start,       unless: "self.end.blank?"
52 69
   validate  :reminder_before_deadline, unless: "self.reminder_at.blank?"
70
+  validate  :subgroups_for_division_present, on: :update
53 71
 
54 72
   after_create :create_missing_participants!
55
-  after_commit :schedule_reminder, if: Proc.new {|a| a.previous_changes["reminder_at"] }
73
+  after_create :copy_default_subgroups!
74
+  after_commit :schedule_reminder,
75
+               if: Proc.new { |a| a.previous_changes["reminder_at"] }
76
+  after_commit :schedule_subgroup_division,
77
+               if: Proc.new { |a| (a.previous_changes['deadline'] ||
78
+                                   a.previous_changes['subgroup_division_enabled']) &&
79
+                                  !a.subgroup_division_done &&
80
+                                  a.subgroup_division_enabled }
56 81
 
57 82
   # Get all people (not participants) that are organizers. Does not include
58 83
   # group leaders, although they may modify the activity as well.
@@ -60,6 +85,10 @@ class Activity < ApplicationRecord
60 85
     self.participants.includes(:person).where(is_organizer: true)
61 86
   end
62 87
 
88
+  def organizer_names
89
+    self.organizers.map { |o| o.person.full_name }
90
+  end
91
+
63 92
   # Determine whether the passed Person participates in the activity.
64 93
   def is_participant?(person)
65 94
     Participant.exists?(
@@ -115,6 +144,22 @@ class Activity < ApplicationRecord
115 144
     end
116 145
   end
117 146
 
147
+  # Create Subgroups from the defaults set using DefaultSubgroups
148
+  def copy_default_subgroups!
149
+    defaults = self.group.default_subgroups
150
+
151
+    # If there are no subgroups, there cannot be subgroup division.
152
+    self.update_attribute(:subgroup_division_enabled, false) if defaults.none?
153
+
154
+    defaults.each do |dsg|
155
+      sg = Subgroup.new(activity: self)
156
+      sg.name = dsg.name
157
+      sg.is_assignable = dsg.is_assignable
158
+      sg.save! # Should never fail, as DSG and SG have identical validation, and names cannot clash.
159
+    end
160
+
161
+  end
162
+
118 163
   # Create multiple Activities from data in a CSV file, assign to a group, return.
119 164
   def self.from_csv(content, group)
120 165
     reader = CSV.parse(content, {headers: true, skip_blanks: true})
@@ -131,15 +176,31 @@ class Activity < ApplicationRecord
131 176
       st            = Time.strptime(row['start_time'], '%H:%M')
132 177
       a.start       = Time.zone.local(sd.year, sd.month, sd.day, st.hour, st.min)
133 178
 
134
-      if not row['end_date'].blank?
179
+      unless row['end_date'].blank?
135 180
         ed          = Date.strptime(row['end_date'])
136 181
         et          = Time.strptime(row['end_time'], '%H:%M')
137 182
         a.end       = Time.zone.local(ed.year, ed.month, ed.day, et.hour, et.min)
138 183
       end
139 184
 
140
-      dd            = Date.strptime(row['deadline_date'])
141
-      dt            = Time.strptime(row['deadline_time'], '%H:%M')
142
-      a.deadline    = Time.zone.local(dd.year, dd.month, dd.day, dt.hour, dt.min)
185
+      unless row['deadline_date'].blank?
186
+        dd            = Date.strptime(row['deadline_date'])
187
+        dt            = Time.strptime(row['deadline_time'], '%H:%M')
188
+        a.deadline    = Time.zone.local(dd.year, dd.month, dd.day, dt.hour, dt.min)
189
+      end
190
+
191
+      unless row['reminder_at_date'].blank?
192
+        rd            = Date.strptime(row['reminder_at_date'])
193
+        rt            = Time.strptime(row['reminder_at_time'], '%H:%M')
194
+        a.reminder_at = Time.zone.local(rd.year, rd.month, rd.day, rt.hour, rt.min)
195
+      end
196
+
197
+      unless row['subgroup_division_enabled'].blank?
198
+        a.subgroup_division_enabled = row['subgroup_division_enabled'].downcase == 'y'
199
+      end
200
+
201
+      unless row['no_response_action'].blank?
202
+        a.no_response_action = row['no_response_action'].downcase == 'p'
203
+      end
143 204
 
144 205
       result << a
145 206
     end
@@ -167,7 +228,83 @@ class Activity < ApplicationRecord
167 228
     self.delay(run_at: self.reminder_at).send_reminder
168 229
   end
169 230
 
231
+  def schedule_subgroup_division
232
+    return if self.deadline.nil? || self.subgroup_division_done
233
+
234
+    self.delay(run_at: self.deadline).assign_subgroups!(mail: true)
235
+  end
236
+
237
+  # Assign a subgroup to all attending participants without one.
238
+  def assign_subgroups!(mail= false)
239
+    # Sanity check: we need subgroups to divide into.
240
+    return unless self.subgroups.any?
241
+
242
+    # Get participants in random order
243
+    ps = self
244
+      .participants
245
+      .where(attending: true)
246
+      .where(subgroup: nil)
247
+      .to_a
248
+
249
+    ps.shuffle!
250
+
251
+    # Get groups, link to participant count
252
+    groups = self
253
+      .subgroups
254
+      .where(is_assignable: true)
255
+      .to_a
256
+      .map { |sg| [sg.participants.count, sg] }
257
+
258
+    ps.each do |p|
259
+      # Sort groups so the group with the least participants gets the following participant
260
+      groups.sort!
261
+
262
+      # Assign participant to group with least members
263
+      p.subgroup = groups.first.second
264
+      p.save
265
+
266
+      # Update the group's position in the list, will sort when next participant is processed.
267
+      groups.first[0] += 1
268
+    end
269
+
270
+    if mail
271
+      self.notify_subgroups!
272
+    end
273
+  end
274
+
275
+  def clear_subgroups!(only_assignable = true)
276
+    sgs = self
277
+      .subgroups
278
+
279
+    if only_assignable
280
+    sgs = sgs
281
+      .where(is_assignable: true)
282
+    end
283
+
284
+    ps = self
285
+      .participants
286
+      .where(subgroup: sgs)
287
+
288
+    ps.each do |p|
289
+      p.subgroup = nil
290
+      p.save
291
+    end
292
+  end
293
+
294
+  # Notify participants of the current subgroups, if any.
295
+  def notify_subgroups!
296
+    ps = self
297
+      .participants
298
+      .joins(:person)
299
+      .where.not(subgroup: nil)
300
+
301
+    ps.each do |pp|
302
+      pp.send_subgroup_notification
303
+    end
304
+  end
305
+
170 306
   private
307
+
171 308
   # Assert that the deadline for participants to change the deadline, if any,
172 309
   # is set before the event starts.
173 310
   def deadline_before_start
@@ -190,4 +327,11 @@ class Activity < ApplicationRecord
190 327
       errors.add(:reminder_at, I18n.t('activities.errors.must_be_before_deadline'))
191 328
     end
192 329
   end
330
+
331
+  # Assert that there is at least one divisible subgroup.
332
+  def subgroups_for_division_present
333
+    if self.subgroups.where(is_assignable: true).none? && subgroup_division_enabled?
334
+      errors.add(:subgroup_division_enabled, I18n.t('activities.errors.cannot_divide_without_subgroups'))
335
+    end
336
+  end
193 337
 end

+ 6 - 0
app/models/default_subgroup.rb

@@ -0,0 +1,6 @@
1
+class DefaultSubgroup < ApplicationRecord
2
+  belongs_to :group
3
+
4
+  validates :name, presence: true, uniqueness: { scope: :group, case_sensitive: false }
5
+  validates :group, presence: true
6
+end

+ 3 - 0
app/models/group.rb

@@ -13,6 +13,9 @@ class Group < ApplicationRecord
13 13
   has_many :activities,
14 14
     dependent: :destroy
15 15
 
16
+  has_many :default_subgroups,
17
+    dependent: :destroy
18
+
16 19
   validates :name,
17 20
     presence: true,
18 21
     uniqueness: {

+ 28 - 1
app/models/participant.rb

@@ -17,6 +17,9 @@ class Participant < ApplicationRecord
17 17
 
18 18
   belongs_to :person
19 19
   belongs_to :activity
20
+  belongs_to :subgroup, optional: true
21
+
22
+  after_validation :clear_subgroup, if: 'self.attending != true'
20 23
 
21 24
   validates :person_id,
22 25
     uniqueness: {
@@ -24,6 +27,18 @@ class Participant < ApplicationRecord
24 27
       message: I18n.t('activities.errors.already_in')
25 28
     }
26 29
 
30
+  HUMAN_ATTENDING = {
31
+    true => I18n.t('activities.state.present'),
32
+    false => I18n.t('activities.state.absent'),
33
+    nil => I18n.t('activities.state.unknown')
34
+  }
35
+
36
+  # @return [String]
37
+  #   the name for the Participant's current state in the current locale.
38
+  def human_attending
39
+    HUMAN_ATTENDING[self.attending]
40
+  end
41
+
27 42
   # TODO: Move to a more appropriate place
28 43
   # @return [String]
29 44
   #   the class for a row containing this activity.
@@ -46,7 +61,7 @@ class Participant < ApplicationRecord
46 61
   def send_reminder
47 62
     return unless self.attending.nil?
48 63
 
49
-    self.attending = true
64
+    self.attending = self.activity.no_response_action
50 65
     notes = self.notes || ""
51 66
     notes << '[auto]'
52 67
     self.notes = notes
@@ -56,4 +71,16 @@ class Participant < ApplicationRecord
56 71
     ParticipantMailer.attendance_reminder(self.person, self.activity).deliver_later
57 72
   end
58 73
 
74
+  # Send subgroup information email if person is attending.
75
+  def send_subgroup_notification
76
+    return unless self.attending && self.subgroup
77
+
78
+    ParticipantMailer.subgroup_notification(self.person, self.activity, self).deliver_later
79
+  end
80
+
81
+  # Clear subgroup if person is set to 'not attending'.
82
+  def clear_subgroup
83
+    self.subgroup = nil
84
+  end
85
+
59 86
 end

+ 15 - 0
app/models/subgroup.rb

@@ -0,0 +1,15 @@
1
+class Subgroup < ApplicationRecord
2
+  belongs_to :activity
3
+  has_many :participants
4
+
5
+  validates :name, presence: true, uniqueness: { scope: :activity, case_sensitive: false }
6
+  validates :activity, presence: true
7
+
8
+  def participant_names
9
+    self
10
+      .participants
11
+      .joins(:person)
12
+      .map { |p| p.person.full_name }
13
+      .sort
14
+  end
15
+end

+ 43 - 4
app/views/activities/_form.html.erb

@@ -1,7 +1,9 @@
1 1
 <%= form_for([@group, activity]) do |f| %>
2 2
   <% if activity.errors.any? %>
3 3
     <div id="error_explanation">
4
-      <h2><%= pluralize(activity.errors.count, "error") %> prohibited this activity from being saved:</h2>
4
+      <h2>
5
+        <%= I18n.t(:could_not_be_saved, errorcount: I18n.t(:error, count: activity.errors.count), class: I18n.t('activities.singular')) %>
6
+      </h2>
5 7
 
6 8
       <ul>
7 9
       <% activity.errors.full_messages.each do |message| %>
@@ -40,10 +42,47 @@
40 42
       <%= f.label :deadline %>
41 43
       <%= f.datetime_field :deadline, class: 'form-control' %>
42 44
     </div>
45
+    <div class="form-group row">
46
+      <div class="col-md-6">
47
+        <%= f.label :reminder_at %>
48
+        <%= f.datetime_field :reminder_at, class: 'form-control' %>
49
+      </div>
50
+      <div class="col-md-6">
51
+        <%= f.label :no_response_action %>
52
+        <%= f.select(:no_response_action, options_for_select([
53
+          [I18n.t('activities.no_response_action.auto_present'), 'true'],
54
+          [I18n.t('activities.no_response_action.auto_absent'), 'false']
55
+        ], selected: @activity.no_response_action.to_s), {}, {class: 'form-control'}) %>
56
+      </div>
57
+    </div>
43 58
     <div class="form-group">
44
-      <%= f.label :reminder_at %>
45
-      <%= f.datetime_field :reminder_at, class: 'form-control' %>
59
+      <div class="check-box">
60
+        <%= f.check_box(:subgroup_division_enabled) %>
61
+        <%= t 'activerecord.attributes.activity.subgroup_division_enabled' %>
62
+      </div>
63
+    </div>
64
+    <div class="form-group btn-group">
65
+      <%= f.submit class: 'btn btn-primary' %>
66
+      <% unless activity.new_record? %>
67
+        <%= link_to I18n.t('activities.subgroups.distribute_remaining'),
68
+          { action: 'immediate_subgroups', group_id: activity.group_id, activity_id: activity.id },
69
+          class: 'btn btn-warning',
70
+          method: :post,
71
+          data: { confirm: I18n.t('activities.subgroups.distribute_remaining_explanation')}
72
+        %>
73
+        <%= link_to I18n.t('activities.subgroups.redistribute'),
74
+          { action: 'immediate_subgroups', group_id: activity.group_id, activity_id: activity.id, overwrite: true },
75
+          method: :post,
76
+          class: 'btn btn-danger',
77
+          data: { confirm: I18n.t('activities.subgroups.redistribute_explanation')}
78
+        %>
79
+        <%= link_to I18n.t('activities.subgroups.clear'),
80
+          { action: 'clear_subgroups', group_id: activity.group_id, activity_id: activity.id, },
81
+          method: :post,
82
+          class: 'btn btn-danger',
83
+          data: { confirm: I18n.t('activities.subgroups.clear_explanation')}
84
+        %>
85
+      <% end %>
46 86
     </div>
47
-    <%= f.submit class: 'btn btn-primary' %>
48 87
   </div>
49 88
 <% end %>

+ 3 - 6
app/views/activities/_state_counts.html.haml

@@ -1,13 +1,10 @@
1 1
 (
2 2
 %span.state-count.present-count
3 3
   = counts[true] or 0
4
-  P
5
-,
4
+P,
6 5
 %span.state-count.unknown-count
7 6
   = counts[nil] or 0
8
-  ?
9
-,
7
+?,
10 8
 %span.state-count.absent-count
11 9
   = counts[false] or 0
12
-  A
13
-)
10
+A)

+ 75 - 5
app/views/activities/edit.html.haml

@@ -7,7 +7,7 @@
7 7
   = t 'activities.organizers.manage'
8 8
 .row
9 9
   .col-md-6
10
-    %h4
10
+    %h4#organizers-add
11 11
       = t 'activities.organizers.add'
12 12
     - if @non_organizers.count > 0
13 13
       = form_tag(group_activity_change_organizer_path(@group, @activity), method: 'post') do
@@ -20,7 +20,7 @@
20 20
       = t 'activities.organizers.no_non_organizers'
21 21
 
22 22
   .col-md-6
23
-    %h4
23
+    %h4#organizers-remove
24 24
       = t 'activities.organizers.remove'
25 25
     - if @organizers.count > 0
26 26
       = form_tag(group_activity_change_organizer_path(@group, @activity), method: 'post') do
@@ -33,6 +33,76 @@
33 33
     - else
34 34
       = t 'activities.organizers.no_organizers'
35 35
 
36
-= link_to t(:back), group_activity_path(@group, @activity)
37
-|
38
-= link_to t(:overview), group_activities_path(@group)
36
+%h2
37
+  = t 'activities.subgroups.manage'
38
+
39
+.row
40
+  .col-md-6
41
+    %h4#subgroups-add
42
+      = t 'activities.subgroups.create'
43
+
44
+    = form_for(@subgroup, url: group_activity_create_subgroup_path(@group, @activity), method: :post) do |f|
45
+
46
+      - if @subgroup.errors.any?
47
+        .has-error.form-group#error_explanation
48
+          %ul
49
+            - @subgroup.errors.full_messages.each do |message|
50
+              %li= message
51
+
52
+      .form-group{ class: [ ('has-error' if @subgroup.errors.any?) ] }
53
+
54
+        %label
55
+          = t 'activerecord.attributes.subgroup.name'
56
+        = f.text_field :name, class: 'form-control'
57
+
58
+      .form-group
59
+        .check-box
60
+          %label
61
+            = f.check_box :is_assignable
62
+            = t 'activerecord.attributes.subgroup.is_assignable'
63
+            (
64
+            %i.fa.fa-random
65
+            )
66
+
67
+      = f.submit t('activities.subgroups.create'), class: 'btn btn-success'
68
+
69
+  .col-md-6#subgroups
70
+    - if @activity.subgroups.blank?
71
+      %p
72
+        = t 'activities.subgroups.none'
73
+    - else
74
+      %table.table
75
+        %tr
76
+          %th
77
+            = t 'activerecord.attributes.subgroup.name'
78
+
79
+          %th
80
+            %i.fa.fa-random
81
+
82
+          %th
83
+            %i.fa.fa-cogs
84
+        - @subgroups.each do |sg|
85
+          %tr
86
+            %td
87
+              = sg.name
88
+              = surround '(', ')' do
89
+                = sg.participants.count
90
+            %td
91
+              = link_to group_activity_update_subgroup_path(@group, @activity, sg.id, 'subgroup[is_assignable]' => !sg.is_assignable), method: :patch, class: 'btn btn-default btn-xs' do
92
+                - if sg.is_assignable
93
+                  %i.fa.fa-check
94
+                - else
95
+                  %i.fa.fa-times
96
+
97
+            %td
98
+              = link_to group_activity_destroy_subgroup_path(@group, @activity, sg.id), method: :delete, class: 'btn btn-danger btn-xs' do
99
+                %i.fa.fa-trash
100
+
101
+      = link_to(group_activity_edit_subgroups_path(@group, @activity), class: 'btn btn-default') do
102
+        %i.fa.fa-edit
103
+        = t 'activities.subgroups.edit'
104
+
105
+
106
+.btn-group
107
+  = link_to t(:back), group_activity_path(@group, @activity), class: 'btn btn-default'
108
+  = link_to t(:overview), group_activities_path(@group), class: 'btn btn-default'

+ 31 - 0
app/views/activities/edit_subgroups.html.haml

@@ -0,0 +1,31 @@
1
+.row
2
+  .alert.alert-info
3
+    = t 'activities.subgroups.only_present_people'
4
+.row
5
+  .col-md-12
6
+    = form_tag(group_activity_update_subgroups_path(@group, @activity)) do
7
+      %table.table
8
+        %tr
9
+          %th
10
+            Naam
11
+
12
+          %th
13
+            Huidig
14
+
15
+          %th
16
+            Nieuw
17
+
18
+        - @participants.each do |p|
19
+          %tr
20
+            %td
21
+              = p.person.full_name
22
+
23
+            %td
24
+              = p.subgroup&.name || '--'
25
+
26
+            %td
27
+              = select_tag("participant_subgroups[#{p.id}]", options_for_select(@subgroup_options, p.subgroup_id || 'nil'), class: 'form-control input-sm')
28
+
29
+      = submit_tag("Opslaan", class: 'btn btn-primary')
30
+      = link_to(edit_group_activity_path(@group, @activity), class: 'btn btn-default') do
31
+        = t :back

+ 19 - 8
app/views/activities/index.html.haml

@@ -1,20 +1,32 @@
1 1
 %h1
2
-  = t 'activerecord.models.activity.other'
3
-
4
-= link_to new_group_activity_path(@group), class: 'btn btn-default pull-right' do
5
-  %i.fa.fa-plus
6
-  = t 'activities.new'
2
+  - if params[:past]
3
+    = t 'activities.past'
4
+  - else
5
+    = t 'activities.upcoming'
7 6
 
8 7
 - isleader = @group.leaders.include?(current_person) || current_person.is_admin?
8
+.btn-group.pull-right
9
+  - if params[:past]
10
+    = link_to group_activities_path(@group), class: 'btn btn-default' do
11
+      %i.fa.fa-history
12
+      = t 'activities.upcoming'
13
+  - else
14
+    = link_to group_activities_path(@group, past: true), class: 'btn btn-default' do
15
+      %i.fa.fa-history
16
+      = t 'activities.past'
17
+  - if isleader
18
+    = link_to new_group_activity_path(@group), class: 'btn btn-default' do
19
+      %i.fa.fa-plus
20
+      = t 'activities.new'
9 21
 
10 22
 %table.table
11 23
   %thead
12 24
     %tr
13 25
       %th
14
-        = t 'activerecord.attributes.activities.name'
26
+        = t 'activerecord.attributes.activity.name'
15 27
 
16 28
       %th
17
-        = t 'activerecord.attributes.activities.start'
29
+        = t 'activerecord.attributes.activity.start'
18 30
 
19 31
       %th
20 32
         P/A/?
@@ -40,4 +52,3 @@
40 52
               %i.fa.fa-pencil
41 53
 
42 54
 = will_paginate @activities
43
-

+ 2 - 2
app/views/activities/mass_new.html.haml

@@ -11,6 +11,6 @@
11 11
     .col-md-12
12 12
       = form_tag(group_activities_mass_new_path(@group), method: 'post', multipart: true) do
13 13
         .form-group
14
-          = file_field_tag 'spreadsheet'
14
+          = file_field_tag 'spreadsheet', required: true
15 15
         .form-group
16
-          = submit_tag
16
+          = submit_tag("Go!", class: 'btn btn-warning', confirm: t(:areyousure))

+ 66 - 34
app/views/activities/show.html.haml

@@ -25,22 +25,26 @@
25 25
 
26 26
 
27 27
       %table.table
28
-        %tr
29
-          %td
30
-            = t 'activities.attrs.organizers'
31
-          %td
32
-            = @organizers
33
-        %tr
34
-          %td
35
-            = t 'activities.attrs.description'
36
-          %td
37
-            = @activity.description
28
+        - unless @organizers.blank?
29
+          %tr
30
+            %td
31
+              = t 'activities.attrs.organizers'
32
+            %td
33
+              = @organizers
38 34
 
39
-        %tr
40
-          %td
41
-            = t 'activities.attrs.where'
42
-          %td
43
-            = @activity.location
35
+        - unless @activity.description.blank?
36
+          %tr
37
+            %td
38
+              = t 'activities.attrs.description'
39
+            %td
40
+              = @activity.description
41
+
42
+        - unless @activity.location.blank?
43
+          %tr
44
+            %td
45
+              = t 'activities.attrs.where'
46
+            %td
47
+              = @activity.location
44 48
 
45 49
         %tr
46 50
           %td
@@ -55,14 +59,29 @@
55 59
               - else
56 60
                 = l @activity.end, format: :long
57 61
 
58
-        %tr
59
-          %td
60
-            = t 'activities.attrs.deadline'
62
+        - if @activity.deadline
63
+          %tr
64
+            %td
65
+              = t 'activities.attrs.deadline'
61 66
 
62
-          %td
63
-            - if @activity.deadline
67
+            %td
64 68
               = l @activity.deadline, format: :long
65 69
 
70
+        - if @assignable_subgroups.any?
71
+          %tr
72
+            %td
73
+              = t 'activerecord.attributes.activity.subgroups'
74
+
75
+            %td
76
+              = @assignable_subgroups.join(', ')
77
+
78
+        - if @ownparticipant&.subgroup
79
+          %tr
80
+            %td
81
+              = t 'activities.participant.yoursubgroup'
82
+            %td
83
+              = @ownparticipant.subgroup.name
84
+
66 85
   - if @ownparticipant
67 86
     .col-md-3
68 87
       .panel.panel-default
@@ -79,14 +98,20 @@
79 98
             emptytext: t('activities.participant.add_notes')
80 99
 
81 100
 .hidden-xs
82
-  %h2
83
-    = @num_participants
84
-    = t 'activities.participant.plural'
85
-    = render partial: "state_counts", locals: {counts: @counts}
101
+  .row
102
+    .col-md-6
103
+      %h2
104
+        %span.state-count.all-count
105
+          = @num_participants
106
+        = t 'activities.participant.plural'
107
+        = render partial: "state_counts", locals: {counts: @counts}
108
+    .col-md-6
109
+      - if @activity.subgroups.any?
110
+        = select_tag(:subgroup_filter, options_for_select(@subgroup_ids), class: 'form-control subgroup-filter')
86 111
 
87 112
   %table.table.table-bordered
88 113
     - @participants.each do |p|
89
-      %tr{class: p.row_class, data: {person_id: p.person.id, activity_id: @activity.id}}
114
+      %tr.participant-row.countable{class: p.row_class, data: {person_id: p.person.id, activity_id: @activity.id, subgroup_id: p.subgroup_id}}
90 115
         %td
91 116
           = p.person.full_name
92 117
           - if p.is_organizer
@@ -100,6 +125,13 @@
100 125
             = render partial: "activities/presence_buttons", locals: {activity: @activity, person: p.person, state: p.attending}
101 126
 
102 127
 .hidden-sm.hidden-md.hidden-lg
128
+  - if @activity.subgroups.any?
129
+    .panel.panel-default
130
+      .panel-heading
131
+        = t 'activerecord.attrs.activities.subgroups'
132
+      .panel-body
133
+        = select_tag(:subgroup_filter, options_for_select(@subgroup_ids), class: 'form-control subgroup-filter')
134
+
103 135
   .panel.panel-default.panel-success
104 136
     .panel-heading
105 137
       %a{role: 'button', href: '#present-collapse', data: {toggle: 'collapse'}, 'aria-expanded': 'false'}
@@ -111,13 +143,13 @@
111 143
           %i.fa.fa-angle-up
112 144
 
113 145
         = t 'activities.state.present'
114
-        %span.badge
146
+        %span.badge.state-count.present-count
115 147
           = @counts[true] || "0"
116 148
 
117 149
     %table.table.collapse#present-collapse
118 150
       %tbody
119 151
         - @participants.where(attending: true).each do |p|
120
-          %tr{data: {person_id: p.person.id, activity_id: @activity.id}}
152
+          %tr.participant-row{data: {person_id: p.person.id, activity_id: @activity.id, subgroup_id: p.subgroup_id}}
121 153
             %td
122 154
               = p.person.full_name
123 155
               - if p.is_organizer
@@ -127,7 +159,7 @@
127 159
               - if p.person.id == current_person.id || all_buttons
128 160
                 = render partial: "activities/presence_buttons", locals: {activity: @activity, person: p.person, state: p.attending}
129 161
 
130
-          %tr{data: {person_id: p.person_id, activity_id: @activity.id}}
162
+          %tr.participant-row{data: {person_id: p.person_id, activity_id: @activity.id, subgroup_id: p.subgroup_id}}
131 163
             %td{colspan: "2"}
132 164
               = editable p, :notes, url: presence_group_activity_path(@activity.group, @activity, person_id: p.person_id), title: t('activities.participant.notes'), value: p.notes, emptytext: "--"
133 165
 
@@ -144,13 +176,13 @@
144 176
 
145 177
         = t 'activities.state.need_response'
146 178
 
147
-        %span.badge
179
+        %span.badge.state-count.unknown-count
148 180
           = @counts[nil] || "0"
149 181
 
150 182
     %table.table.collapse#unknown-collapse
151 183
       %tbody
152 184
         - @participants.where(attending: nil).each do |p|
153
-          %tr{data: {person_id: p.person.id, activity_id: @activity.id}}
185
+          %tr.participant-row{data: {person_id: p.person.id, activity_id: @activity.id, subgroup_id: p.subgroup_id}}
154 186
             %td
155 187
               = p.person.full_name
156 188
               - if p.is_organizer
@@ -160,7 +192,7 @@
160 192
               - if p.person.id == current_person.id || all_buttons
161 193
                 = render partial: "activities/presence_buttons", locals: {activity: @activity, person: p.person, state: p.attending}
162 194
 
163
-          %tr{data: {person_id: p.person_id, activity_id: @activity.id}}
195
+          %tr.participant-row{data: {person_id: p.person_id, activity_id: @activity.id, subgroup_id: p.subgroup_id}}
164 196
             %td{colspan: "2"}
165 197
               = editable p, :notes, url: presence_group_activity_path(@activity.group, @activity, person_id: p.person_id), title: t('activities.participant.notes'), value: p.notes, emptytext: "--"
166 198
 
@@ -176,13 +208,13 @@
176 208
 
177 209
         = t 'activities.state.absent'
178 210
 
179
-        %span.badge
211
+        %span.badge.state-count.absent-count
180 212
           = @counts[false] || "0"
181 213
 
182 214
     %table.table.collapse#absent-collapse
183 215
       %tbody
184 216
         - @participants.where(attending: false).each do |p|
185
-          %tr{data: {person_id: p.person.id, activity_id: @activity.id}}
217
+          %tr.participant-row{data: {person_id: p.person.id, activity_id: @activity.id, subgroup_id: p.subgroup_id}}
186 218
             %td
187 219
               = p.person.full_name
188 220
               - if p.is_organizer
@@ -192,6 +224,6 @@
192 224
               - if p.person.id == current_person.id || all_buttons
193 225
                 = render partial: "activities/presence_buttons", locals: {activity: @activity, person: p.person, state: p.attending}
194 226
 
195
-          %tr{data: {person_id: p.person_id, activity_id: @activity.id}}
227
+          %tr.participant-row{data: {person_id: p.person_id, activity_id: @activity.id, subgroup_id: p.subgroup_id}}
196 228
             %td{colspan: "2"}
197 229
               = editable p, :notes, url: presence_group_activity_path(@activity.group, @activity, person_id: p.person_id), title: t('activities.participant.notes'), value: p.notes, emptytext: "--"

+ 28 - 0
app/views/activities/subgroup_editor.html.haml

@@ -0,0 +1,28 @@
1
+.row
2
+  .col-md-12
3
+    %table.table
4
+      %tr
5
+        %th
6
+          Naam
7
+
8
+        %th
9
+          Status
10
+
11
+        %th
12
+          Huidig
13
+
14
+        %th
15
+          Nieuw
16
+
17
+      - @participants.each do |p|
18
+        %td
19
+          = p.person.full_name
20
+
21
+        %td
22
+          = p.attending
23
+
24
+        %td
25
+          = p.subgroup.name
26
+
27
+        %td
28
+          TODO

+ 15 - 14
app/views/dashboard/home.html.haml

@@ -90,21 +90,22 @@
90 90
                     = link_to group do
91 91
                       = group.name
92 92
 
93
-    .col-md-6
94
-      .panel.panel-default
95
-        .panel-heading
96
-          = t 'dashboard.organized_you'
93
+    - if @user_organized.any?
94
+      .col-md-6
95
+        .panel.panel-default
96
+          .panel-heading
97
+            = t 'dashboard.organized_you'
97 98
 
98
-        .panel-body
99
-          %table.table.table-striped.table-bordered
100
-            %tbody
101
-              - @user_organized.each do |p|
102
-                - a = p.activity
103
-                %tr
104
-                  %td
105
-                    = link_to group_activity_url(a.group, a) do
106
-                      = a.name
107
-                      = "(#{a.human_state_counts})"
99
+          .panel-body
100
+            %table.table.table-striped.table-bordered
101
+              %tbody
102
+                - @user_organized.each do |p|
103
+                  - a = p.activity
104
+                  %tr
105
+                    %td
106
+                      = link_to group_activity_url(a.group, a) do
107
+                        = a.name
108
+                        = "(#{a.human_state_counts})"
108 109
 
109 110
   .row
110 111
     .col-md-12

+ 0 - 6
app/views/groups/edit.html.erb

@@ -1,6 +0,0 @@
1
-<h1>Editing Group</h1>
2
-
3
-<%= render 'form', group: @group %>
4
-
5
-<%= link_to 'Show', @group %> |
6
-<%= link_to 'Back', groups_path %>

+ 62 - 0
app/views/groups/edit.html.haml

@@ -0,0 +1,62 @@
1
+%h1
2
+  = t 'groups.edit'
3
+
4
+= render 'form', group: @group
5
+
6
+%h2
7
+  = t 'defaultsubgroups.manage'
8
+
9
+%p
10
+  = t 'defaultsubgroups.settings_blurb'
11
+
12
+.row
13
+  .col-md-6
14
+    %h4
15
+      = t 'defaultsubgroups.create'
16
+
17
+    -#= form_tag(group_create_default_subgroup_path(@group), method: :post, class: 'form') do
18
+    = form_for(@defaultsubgroup, url: group_create_default_subgroup_path(@group), method: :post) do |f|
19
+      - if @defaultsubgroup.errors.any?
20
+        .has-error.form-group#error_explanation
21
+          %ul
22
+            - @defaultsubgroup.errors.full_messages.each do |message|
23
+              %li
24
+                = message
25
+
26
+      .form-group{ class: [ ('has-error' if @defaultsubgroup.errors.any?) ] }
27
+        %label
28
+          = t 'activerecord.attributes.default_subgroup.name'
29
+        = f.text_field(:name, class: 'form-control')
30
+
31
+      .form-group
32
+        .check-box
33
+          %label
34
+            = f.check_box(:is_assignable)
35
+            = t 'activerecord.attributes.default_subgroup.is_assignable'
36
+
37
+      = f.submit t('defaultsubgroups.create'), class: 'btn btn-success'
38
+
39
+  .col-md-6
40
+    %h4
41
+      = t 'defaultsubgroups.destroy'
42
+
43
+    - if @group.default_subgroups.blank?
44
+      %p
45
+        = t 'defaultsubgroups.none'
46
+
47
+    - else
48
+      = form_tag(group_destroy_default_subgroup_path(@group), method: :delete, class: 'form') do
49
+        .form-group
50
+          %label
51
+            = t 'activerecord.models.default_subgroup.one'
52
+          - options = @group.default_subgroups.pluck(:name, :id)
53
+          = select_tag(:default_subgroup_id, options_for_select(options), class: 'form-control')
54
+
55
+        = submit_tag(t('defaultsubgroups.destroy'), class: 'btn btn-danger')
56
+
57
+.row
58
+  .col-md-12
59
+    .btn-group
60
+      = link_to t(:back), @group, class: 'btn btn-default'
61
+      = link_to t('activities.mass_import_short'), group_activities_mass_new_path(@group), class: 'btn btn-default'
62
+      = link_to t('groups.mass_add_short'), group_mass_add_path(@group), class: 'btn btn-default'

+ 14 - 5
app/views/groups/mass_add_members.html.haml

@@ -2,8 +2,17 @@
2 2
   .row
3 3
     .col-md-12
4 4
       %h1
5
-        Mass-adding members to
6
-        = @group.name
7
-      = form_tag("/groups/#{@group.id}/mass_add", method: 'post', multipart: true) do
8
-        = file_field_tag 'spreadsheet'
9
-        = submit_tag
5
+        = t 'groups.mass_add_members', group: @group.name
6
+  .row
7
+    .col-md-12
8
+      %p
9
+        = t 'groups.mass_add_explanation'
10
+      = link_to asset_path('batch_persons.csv'), class: 'btn btn-default' do
11
+        = t :download
12
+  .row
13
+    .col-md-12
14
+      = form_tag(group_mass_add_path(@group), method: 'post', multipart: true) do
15
+        .form-group
16
+          = file_field_tag 'spreadsheet', required: true
17
+        .form-group
18
+          = submit_tag("Go!", class: 'btn btn-warning', confirm: t(:areyousure))

+ 4 - 1
app/views/participant_mailer/attendance_reminder.html.haml

@@ -1,6 +1,9 @@
1 1
 %p= t 'authentication.emails.greeting', name: @person.first_name
2 2
 
3
-%p= t 'activities.emails.attendance_reminder.havenot_responded', activity: @activity.name
3
+- if @activity.no_response_action
4
+  %p= t 'activities.emails.attendance_reminder.set_to_present', activity: @activity.name
5
+- else
6
+  %p= t 'activities.emails.attendance_reminder.set_to_absent', activity: @activity.name
4 7
 
5 8
 %p= t 'activities.emails.attendance_reminder.if_cannot', deadline: l(@activity.deadline, format: :short)
6 9
 

+ 5 - 1
app/views/participant_mailer/attendance_reminder.text.erb

@@ -1,6 +1,10 @@
1 1
 <%= t 'authentication.emails.greeting', name: @person.first_name %>
2 2
 
3
-<%= t 'activities.emails.attendance_reminder.havenot_responded', activity: @activity.name %>
3
+<% if @activity.no_response_action %>
4
+<%= t 'activities.emails.attendance_reminder.set_to_present', activity: @activity.name %>
5
+<% else %>
6
+<%= t 'activities.emails.attendance_reminder.set_to_absent', activity: @activity.name %>
7
+<% end %>
4 8
 
5 9
 <%= t 'activities.emails.attendance_reminder.if_cannot', deadline: l(@activity.deadline, format: :short) %>
6 10
 

+ 32 - 0
app/views/participant_mailer/subgroup_notification.html.haml

@@ -0,0 +1,32 @@
1
+%p= t 'authentication.emails.greeting', name: @person.first_name
2
+
3
+%p= t 'activities.emails.subgroup_notification.yoursubgroupis', activity: @activity.name, subgroup: @subgroup
4
+
5
+- if !@others.empty?
6
+  %p= t 'activities.emails.subgroup_notification.subgroupmembers', others: @others
7
+- else
8
+  %p= t 'activities.emails.subgroup_notification.noothersinsubgroup'
9
+
10
+%p= t 'activities.emails.subgroup_notification.allsubgroups'
11
+%ul
12
+  - @subgroups.each do |sg|
13
+    %li
14
+      = succeed ':' do
15
+        = sg.name
16
+      = sg.participant_names.join ', '
17
+
18
+%p= t 'activities.emails.subgroup_notification.cannotdecline', organizers: @organizers
19
+
20
+%p
21
+  = link_to group_activity_url(@activity.group, @activity) do
22
+    = t 'activities.emails.open_activity'
23
+
24
+%p
25
+  = t('activities.emails.ending').sample
26
+  %br
27
+  Aardbei
28
+
29
+%hr
30
+%footer
31
+  %p
32
+    = t 'activities.emails.cant_turn_off'

+ 25 - 0
app/views/participant_mailer/subgroup_notification.text.erb

@@ -0,0 +1,25 @@
1
+<%= t 'authentication.emails.greeting', name: @person.first_name %>
2
+
3
+<%= t 'activities.emails.subgroup_notification.yoursubgroupis', activity: @activity.name, subgroup: @subgroup %>
4
+
5
+<% if !@others.empty? %>
6
+<%= t 'activities.emails.subgroup_notification.subgroupmembers', others: @others %>
7
+<% else %>
8
+<%= t 'activities.emails.subgroup_notification.noothersinsubgroup' %>
9
+<% end %>
10
+
11
+<%= t 'activity.emails.subgroup_notification.allsubgroups' %>
12
+
13
+<% @subgroups.each do |sg| %>
14
+- <%= sg.name %>: <%= sg.participant_names.join(', ') %>
15
+<% end %>
16
+
17
+<%= t 'activity.emails.subgroup_notification.cannotdecline', organizers: @organizers %>
18
+
19
+<%= group_activity_url(@activity.group, @activity) %>
20
+
21
+<%= t('activities.emails.ending').sample %>
22
+Aardbei
23
+
24
+---
25
+<%= t 'activities.emails.cant_turn_off' %>

+ 8 - 0
config/locales/aardbei_en.yml

@@ -46,6 +46,13 @@ en:
46 46
 
47 47
   invalid_csrf: "You submitted an invalid request! If you got here after clicking a link, it's possible that someone is doing something nasty!"
48 48
 
49
+  could_not_be_saved: "%{errorcount} prevented this %{class} from being saved:"
50
+
51
+  error:
52
+    zero: "no errors"
53
+    one: "an error"
54
+    other: "%{count} errors"
55
+
49 56
   activerecord:
50 57
     models:
51 58
       person:
@@ -83,3 +90,4 @@ en:
83 90
         end: "End"
84 91
         description: "Description"
85 92
         deadline: "Deadline"
93
+        no_response_action: "Action when no response"

+ 8 - 1
config/locales/aardbei_nl.yml

@@ -19,8 +19,15 @@ nl:
19 19
 
20 20
   areyousure: "Weet je het zeker?"
21 21
 
22
-  somethingbroke: "Er is iets misgegaan!"
22
+  somethingbroke: "Er is iets misgegaan! Neem contact op als dit blijft gebeuren."
23 23
 
24 24
   value_required: "Een verplichte vraag was leeg."
25 25
 
26 26
   invalid_csrf: "Ongeldig authenticiteitstoken! Als je hier terecht bent gekomen na het klikken op een link is het mogelijk dat iemand iets naars probeerde!"
27
+
28
+  could_not_be_saved: "Door %{errorcount} kon deze %{class} niet worden opgeslagen:"
29
+
30
+  error:
31
+    zero: "geen fouten"
32
+    one: "een fout"
33
+    other: "%{count} fouten"

+ 52 - 1
config/locales/activities/en.yml

@@ -8,9 +8,13 @@ en:
8 8
     edit: "Edit activity"
9 9
     updated: "Activity updated."
10 10
     destroyed: "Activity destroyed."
11
+    mass_import_short: "Import activities"
11 12
     mass_import: "Create multiple activities in %{group}"
12 13
     mass_imported: "%{count} activities created!"
13 14
 
15
+    upcoming: 'Upcoming activities'
16
+    past: 'Past activities'
17
+
14 18
     mass_import_explanation: "Download the example file using the link below, fill in the fields (end and deadline are optional), and upload the file using the upload form."
15 19
 
16 20
     upcoming_yours: "Upcoming activities organized by you"
@@ -19,7 +23,9 @@ en:
19 23
     errors:
20 24
       must_be_before_start: "must be before start"
21 25
       must_be_after_start: "must be after start"
26
+      must_be_before_deadline: "must be before deadline"
22 27
       already_in: "person already participates in activity"
28
+      cannot_divide_without_subgroups: "a divisible subgroup must exist"
23 29
 
24 30
     participant:
25 31
       singular: "participant"
@@ -27,6 +33,7 @@ en:
27 33
       notes: "Notes"
28 34
       add_notes: "Add notes"
29 35
       yourresponse: "Your response"
36
+      yoursubgroup: "Your subgroup"
30 37
 
31 38
       copy_responses: "Copy responses"
32 39
       copy_absent: "Copy absentees"
@@ -69,9 +76,53 @@ en:
69 76
       open_activity: "Open activity"
70 77
       open_settings: "Preferences"
71 78
       dont_want_mail: "If you no longer want to receive these emails, change your preferences using the following link:"
79
+      cant_turn_off: "This email cannot be disabled, because it contains important information."
72 80
       ending:
73 81
         - "Cheers,"
74 82
       attendance_reminder:
75 83
         subject: "You are now listed as 'attending' for %{activity}"
76
-        havenot_responded: "You have not yet indicated if you will be at %{activity}. Because we assume that you're present if you don't respond, your response has been set to 'attending'."
84
+        set_to_present: "You have not yet indicated if you will be at %{activity}. Because we assume that you're present if you don't respond, your response has been set to 'attending'."
85
+        set_to_absent: "You have not yet indicated if you will be at %{activity}. This activity is set to assume to set you to absent if you don't respond, so your response has been set to 'absent'."
77 86
         if_cannot: "If you wish to change this, you can change your response until %{deadline} using the following link:"
87
+
88
+      subgroup_notification:
89
+        subject: "You have been assigned to subgroup %{subgroup} for %{activity}"
90
+        yoursubgroupis: "The upcoming activity %{activity} uses subgroups, and you have been assigned to subgroup %{subgroup}."
91
+        subgroupmembers: "The other people in this subgroup are: %{others}"
92
+        noothersinsubgroup: "There are no other people in this subgroup. :("
93
+        allsubgroups: "All subgroups (including yours):"
94
+        cannotdecline: "This email was sent when the deadline expired. If you find you cannot attend, please contact the Drerrie, or one of the organisers (%{organizers})."
95
+
96
+    subgroups:
97
+      manage: 'Manage subgroups'
98
+      create: 'Create subgroup'
99
+      created: 'Subgroup created.'
100
+      create_failed: 'Could not create subgroup!'
101
+
102
+      edit: 'Edit subgroup division'
103
+      edited: 'Subgroup division updated.'
104
+
105
+      update: 'Update subgroup'
106
+      updated: 'Subgroup updated.'
107
+      update_failed: 'Could not update subgroup!'
108
+
109
+      destroy: 'Destroy subgroup'
110
+      destroyed: 'Subgroup destroyed.'
111
+      none: 'There are no subgroups.'
112
+
113
+      filter_nogroup: 'Attending, no subgroup'
114
+      filter_nofilter: 'Show everyone'
115
+
116
+      only_present_people: "Only people who will attend are listed, because people who don't cannot be in a subgroup."
117
+
118
+      clear: 'Clear subgroups'
119
+      clear_explanation: 'This removes everyone in a assignable subgroup from their subgroup! Are you sure you want this?'
120
+      cleared: 'Subgroups cleared.'
121
+
122
+      redistribute: 'Redistribute subgroups'
123
+      redistribute_explanation: 'This removes everyone in a assignable subgroup from that subgroup, and then randomly reassigns everyone to their subgroup. Are you sure you want this?'
124
+      redistributed: 'Subgroups redistributed.'
125
+
126
+      distribute_remaining: 'Distribute remaining to subgroups'
127
+      distribute_remaining_explanation: 'This distributes all remaining attending participants not yet in a subgroup to a subgroup. Do you want this?'
128
+      remaining_distributed: 'Remaining participants distributed.'

+ 58 - 3
config/locales/activities/nl.yml

@@ -8,9 +8,13 @@ nl:
8 8
     edit: "Activiteit bewerken"
9 9
     updated: "Activiteit bijgewerkt."
10 10
     destroyed: "Activiteit verwijderd."
11
+    mass_import_short: "Activiteiten importeren"
11 12
     mass_import: "Meerdere activiteiten aanmaken in %{group}"
12 13
     mass_imported: "%{count} activiteiten aangemaakt!"
13 14
 
15
+    upcoming: 'Aankomende activiteiten'
16
+    past: 'Afgelopen activiteiten'
17
+
14 18
     mass_import_explanation: "Download het voorbeeldbestand via de link hieronder, vul de velden in in het formaat zoals het staat aangegeven (einde en deadline zijn niet verplicht), en upload het via de knop onderaan."
15 19
 
16 20
     upcoming_yours: "Aankomende activiteiten georganiseerd door jou"
@@ -21,6 +25,7 @@ nl:
21 25
       must_be_after_start: "moet na start zijn"
22 26
       must_be_before_deadline: "moet voor deadline zijn"
23 27
       already_in: "persoon doet al mee aan activiteit"
28
+      cannot_divide_without_subgroups: "moet indeelbare subgroep hebben"
24 29
 
25 30
     participant:
26 31
       singular: "deelnemer"
@@ -28,6 +33,7 @@ nl:
28 33
       notes: "Opmerkingen"
29 34
       add_notes: "Opmerkingen toevoegen"
30 35
       yourresponse: "Jouw antwoord"
36
+      yoursubgroup: "Jouw subgroep"
31 37
 
32 38
       copy_responses: "Kopieer overzicht"
33 39
       copy_absent: "Kopieer afwezigen"
@@ -66,10 +72,15 @@ nl:
66 72
       description: "Omschrijving"
67 73
       deadline: "Deadline"
68 74
 
75
+    no_response_action:
76
+      auto_present: 'Automatisch aanmelden'
77
+      auto_absent: 'Automatisch afmelden'
78
+
69 79
     emails:
70 80
       open_activity: "Activiteit openen"
71 81
       open_settings: "Instellingen"
72 82
       dont_want_mail: "Als je dit soort mailtjes niet meer wilt ontvangen, kun je dit uitschakelen via de volgende link:"
83
+      cant_turn_off: "Omdat deze mail belangrijk is kan je hem niet uitzetten."
73 84
       ending:
74 85
         - "Met griendelijke vroet,"
75 86
         - "Met groetelijke doei,"
@@ -80,6 +91,50 @@ nl:
80 91
         - "Doi,"
81 92
         - "Talla,"
82 93
       attendance_reminder:
83
-        subject: "Je bent automatisch aangemeld voor %{activity}"
84
-        havenot_responded: "Je hebt nog niet aangegeven of je bij %{activity} kunt zijn. Omdat we ervan uitgaan dat je er bent als je niks aangeeft, is je reactie automatisch op 'aanwezig' gezet."
85
-        if_cannot: "Als je toch niet aanwezig kunt zijn, kan je dit tot %{deadline} aangeven via de volgende link:"
94
+        subject_present: "Je bent automatisch aangemeld voor %{activity}"
95
+        subject_absent: "Je bent automatisch afgemeld voor %{activity}"
96
+        set_to_present: "Je hebt nog niet aangegeven of je bij %{activity} kunt zijn. De organisatoreen van deze activiteit gaan ervan uit dat je er wel bent als je niets aangeeft,  dus je reactie is op 'aanwezig' gezet."
97
+        set_to_absent: "Je hebt nog niet aangegeven of je bij %{activity} kan zijn. De organisatoren van deze activiteit gaan ervan uit dat je er niet bent als je niets aangeeft, dus je reactie is op 'afwezig' gezet."
98
+        if_cannot: "Als je dit nog wilt veranderen kan je dit tot %{deadline} aangeven via de volgende link:"
99
+
100
+      subgroup_notification:
101
+        subject: "Je bent ingedeeld in subgroep %{subgroup} voor %{activity}"
102
+        yoursubgroupis: "De aankomende opkomst %{activity} gebruikt subgroepen, en jij bent ingedeeld in subgroep %{subgroup}."
103
+        subgroupmembers: "De andere mensen in deze subgroep zijn: %{others}"
104
+        noothersinsubgroup: "Er zijn geen andere mensen in deze subgroep. :("
105
+        allsubgroups: "Alle groepjes (inclusief de jouwe) zijn:"
106
+        cannotdecline: "Deze email is verstuurd toen de deadline voor het afmelden verstreek. Als je om wat voor reden dan ook toch niet kan, neem dan contact op met de Drerrie (afmeld@maartenberg.nl) of een van de organisators van de opkomst (%{organizers})."
107
+
108
+    subgroups:
109
+      manage: 'Subgroepen aanpassen'
110
+      create: 'Subgroep aanmaken'
111
+      created: 'Subgroep aangemaakt.'
112
+      create_failed: 'Kon subgroep niet opslaan!'
113
+
114
+      edit: 'Subgroepindeling bewerken'
115
+      edited: 'Subgroepindeling bijgewerkt.'
116
+
117
+      update: 'Subgroep bijwerken'
118
+      updated: 'Subgroep bijgewerkt.'
119
+      update_failed: 'Kon subgroep niet bijwerken!'
120
+
121
+      destroy: 'Subgroep verwijderen'
122
+      destroyed: 'Subgroep verwijderd.'
123
+      none: 'Er zijn geen subgroepen.'
124
+
125
+      filter_nogroup: 'Aangemeld, geen groep'
126
+      filter_nofilter: 'Toon iedereen'
127
+
128
+      only_present_people: 'Je ziet alleen maar mensen die zijn aangemeld in dit overzicht, omdat mensen die zijn afgemeld niet in een subgroep kunnen zitten.'
129
+
130
+      clear: 'Subgroepen legen'
131
+      clear_explanation: 'Dit verwijdert iedereen in een indeelbare subgroep uit zijn subgroep! Weet je zeker dat je dit wilt?'
132
+      cleared: 'Subgroepen geleegd.'
133
+
134
+      redistribute: 'Subgroepen opnieuw indelen'
135
+      redistribute_explanation: 'Dit verwijdert iedereen in een indeelbare subgroep uit die subgroep, en deelt daarna iedereen willekeurig opnieuw in. Weet je zeker dat je dit wilt?'
136
+      redistributed: 'Subgroepen opnieuw ingedeeld.'
137
+
138
+      distribute_remaining: 'Overgebleven naar subgroepen'
139
+      distribute_remaining_explanation: 'Dit deelt iedereen die is aangemeld, maar nog niet in een subgroep zit, in in een willekeurige subgroep (volgens normale volgorde van kleinste groep eerst). Wil je dit?'
140
+      remaining_distributed: 'Overgebleven personen ingedeeld in subgroepen.'

+ 16 - 0
config/locales/defaultsubgroups_en.yml

@@ -0,0 +1,16 @@
1
+en:
2
+  defaultsubgroups:
3
+    manage: 'Manage default subgroups'
4
+    settings_blurb: "The groups set here will be added automatically to each new activity that is created. Note that actually assigning participants to groups has to be enabled separately."
5
+
6
+    create: 'Add default subgroup'
7
+    created: 'Default subgroup added.'
8
+    create_failed: 'Could not create default subgroup!'
9
+
10
+    updated: 'Default subgroup updated.'
11
+    update_failed: 'Could not update default subgroup!'
12
+
13
+    destroy: 'Remove default subgroup'
14
+    destroyed: 'Default subgroup destroyed.'
15
+
16
+    none: 'There are no default subgroups.'

+ 16 - 0
config/locales/defaultsubgroups_nl.yml

@@ -0,0 +1,16 @@
1
+nl:
2
+  defaultsubgroups:
3
+    manage: 'Standaard subgroepen aanpassen'
4
+    settings_blurb: "De groepen die hier staan aangegeven worden automatisch aangemaakt voor iedere nieuwe activiteit die wordt toegevoegd. Het daadwerkelijk indelen in deze groepen moet wel per activiteit worden ingeschakeld."
5
+
6
+    create: 'Standaardgroep toevoegen'
7
+    created: 'Standaardgroep toegevoegd.'
8
+    create_failed: 'Kon de standaardgroep niet opslaan!'
9
+
10
+    updated: 'Standaardgroep bijgewerkt.'
11
+    update_failed: 'Kon de standaardgroep niet bijwerken!'
12
+
13
+    destroy: 'Standaardgroep verwijderen'
14
+    destroyed: 'Standaardgroep verwijderd.'
15
+
16
+    none: 'Er zijn geen standaardgroepen.'

+ 5 - 0
config/locales/groups/en.yml

@@ -22,6 +22,11 @@ en:
22 22
     member_updated: "Member updated."
23 23
     member_removed: "%{name} was removed from the group."
24 24
 
25
+    mass_add_members: "Adding multiple members to %{group}"
26
+    mass_add_short: "Import members"
27
+    mass_add_explanation: "Download the example file using the link below, and fill in this file in the format shown. Upload the completed form using the form at the bottom of this page. Note that all data fields will be ignored if a user with the same email address exists. Date of birth is not required."
28
+    mass_add_success: "%{count} members added to group."
29
+
25 30
     member:
26 31
       add: "New Member"
27 32
       adding: "Add member to %{name}"

+ 5 - 0
config/locales/groups/nl.yml

@@ -22,6 +22,11 @@ nl:
22 22
     member_updated: "Lid bijgewerkt."
23 23
     member_removed: "%{name} is verwijderd uit de groep."
24 24
 
25
+    mass_add_members: "Meerdere leden toevoegen aan %{group}"
26
+    mass_add_short: "Leden importeren"
27
+    mass_add_explanation: "Download het voorbeeldbestand via de link hieronder, vul de velden in in het formaat zoals aangegeven, en upload het ingevulde formulier via de knop onderaan. Merk op dat aangepaste velden worden genegeerd als al een gebruiker met hetzelfde e-mailadres bestaat. Geboortedatum is niet verplicht."
28
+    mass_add_success: "%{count} leden toegevoegd aan de groep."
29
+
25 30
     member:
26 31
       add: "Lid toevoegen"
27 32
       adding: "Lid toevoegen aan %{name}"

+ 21 - 0
config/locales/translation_nl.yml

@@ -24,6 +24,14 @@ nl:
24 24
       token: Token  #g
25 25
       user: Gebruiker  #g
26 26
 
27
+      default_subgroup:
28
+        one: Standaardgroep
29
+        other: Standaardgroepen
30
+
31
+      subgroup:
32
+        one: Subgroep
33
+        other: Subgroepen
34
+
27 35
     attributes:
28 36
       activity:
29 37
         deadline: Deadline  #g
@@ -37,9 +45,14 @@ nl:
37 45
         start: Start  #g
38 46
         reminder_at: Herinnering om
39 47
         reminder_done: Herinnering verstuurd
48
+        subgroups: :activerecord.models.subgroup.other
49
+        subgroup_division_enabled: Subgroepen indelen
50
+        subgroup_division_done: Subgroepen ingedeeld
51
+        no_response_action: Actie bij geen reactie
40 52
 
41 53
       group:
42 54
         activities: Activiteiten  #g
55
+        default_subgroups: Standaardsubgroepen
43 56
         members: Leden  #g
44 57
         name: Naam  #g
45 58
         people: Mensen  #g
@@ -87,3 +100,11 @@ nl:
87 100
         email: E-mail  #g
88 101
         password_digest: Wachtwoord-digest  #g
89 102
         person: :activerecord.models.person  #g
103
+
104
+      default_subgroup:
105
+        name: Naam
106
+        is_assignable: Gebruiken voor indelen
107
+
108
+      subgroup:
109
+        name: Naam
110
+        is_assignable: Gebruiken voor indelen

+ 13 - 0
config/routes.rb

@@ -36,6 +36,10 @@ Rails.application.routes.draw do
36 36
     get 'mass_add', to: 'groups#mass_add_members'
37 37
     post 'mass_add', to: 'groups#process_mass_add_members'
38 38
 
39
+    post 'subgroups', to: 'groups#create_default_subgroup', as: 'create_default_subgroup'
40
+    patch 'subgroups/:default_subgroup_id', to: 'groups#update_default_subgroup', as: 'update_default_subgroup'
41
+    delete 'subgroups(/:default_subgroup_id)', to: 'groups#destroy_default_subgroup', as: 'destroy_default_subgroup'
42
+
39 43
     resources :members do
40 44
       post 'promote', to: 'members#promote', on: :member
41 45
       post 'demote', to: 'members#demote', on: :member
@@ -48,6 +52,15 @@ Rails.application.routes.draw do
48 52
       post 'change_organizer', to: 'activities#change_organizer'
49 53
       put 'presence', to: 'activities#presence', on: :member
50 54
       patch 'presence', to: 'activities#presence', on: :member
55
+
56
+      post 'subgroups', to: 'activities#create_subgroup', as: 'create_subgroup'
57
+      patch 'subgroups/:subgroup_id', to: 'activities#update_subgroup', as: 'update_subgroup'
58
+      delete 'subgroups(/:subgroup_id)', to: 'activities#destroy_subgroup', as: 'destroy_subgroup'
59
+
60
+      get 'edit_subgroups', to: 'activities#edit_subgroups'
61
+      post 'update_subgroups', to: 'activities#update_subgroups'
62
+      post 'immediate_subgroups', to: 'activities#immediate_subgroups'
63
+      post 'clear_subgroups', to: 'activities#clear_subgroups'
51 64
     end
52 65
   end
53 66
   get 'my_groups', to: 'groups#user_groups', as: :user_groups

+ 11 - 0
db/migrate/20170930201201_create_default_subgroups.rb

@@ -0,0 +1,11 @@
1
+class CreateDefaultSubgroups < ActiveRecord::Migration[5.0]
2
+  def change
3
+    create_table :default_subgroups do |t|
4
+      t.references :group, foreign_key: true
5
+      t.string :name, null: false
6
+      t.boolean :is_assignable
7
+
8
+      t.timestamps
9
+    end
10
+  end
11
+end

+ 11 - 0
db/migrate/20171001124009_create_subgroups.rb

@@ -0,0 +1,11 @@
1
+class CreateSubgroups < ActiveRecord::Migration[5.0]
2
+  def change
3
+    create_table :subgroups do |t|
4
+      t.references :activity, foreign_key: true
5
+      t.string :name, null: false
6
+      t.boolean :is_assignable
7
+
8
+      t.timestamps
9
+    end
10
+  end
11
+end

+ 5 - 0
db/migrate/20171001150124_add_subgroup_to_participants.rb

@@ -0,0 +1,5 @@
1
+class AddSubgroupToParticipants < ActiveRecord::Migration[5.0]
2
+  def change
3
+    add_reference :participants, :subgroup, foreign_key: true
4
+  end
5
+end

+ 6 - 0
db/migrate/20171023080215_add_subgroup_job_markers_to_activities.rb

@@ -0,0 +1,6 @@
1
+class AddSubgroupJobMarkersToActivities < ActiveRecord::Migration[5.0]
2
+  def change
3
+    add_column :activities, :subgroup_division_enabled, :boolean
4
+    add_column :activities, :subgroup_division_done, :boolean
5
+  end
6
+end

+ 5 - 0
db/migrate/20180206181016_add_no_response_action_to_activities.rb

@@ -0,0 +1,5 @@
1
+class AddNoResponseActionToActivities < ActiveRecord::Migration[5.0]
2
+  def change
3
+    add_column :activities, :no_response_action, :boolean, default: true
4
+  end
5
+end

+ 26 - 3
db/schema.rb

@@ -10,7 +10,7 @@
10 10
 #
11 11
 # It's strongly recommended that you check this file into your version control system.
12 12
 
13
-ActiveRecord::Schema.define(version: 20170917140643) do
13
+ActiveRecord::Schema.define(version: 20180206181016) do
14 14
 
15 15
   create_table "activities", force: :cascade do |t|
16 16
     t.string   "name"
@@ -20,13 +20,25 @@ ActiveRecord::Schema.define(version: 20170917140643) do
20 20
     t.datetime "end"
21 21
     t.datetime "deadline"
22 22
     t.integer  "group_id"
23
-    t.datetime "created_at",    null: false
24
-    t.datetime "updated_at",    null: false
23
+    t.datetime "created_at",                               null: false
24
+    t.datetime "updated_at",                               null: false
25 25
     t.datetime "reminder_at"
26 26
     t.boolean  "reminder_done"
27
+    t.boolean  "subgroup_division_enabled"
28
+    t.boolean  "subgroup_division_done"
29
+    t.boolean  "no_response_action",        default: true
27 30
     t.index ["group_id"], name: "index_activities_on_group_id"
28 31
   end
29 32
 
33
+  create_table "default_subgroups", force: :cascade do |t|
34
+    t.integer  "group_id"
35
+    t.string   "name",          null: false
36
+    t.boolean  "is_assignable"
37
+    t.datetime "created_at",    null: false
38
+    t.datetime "updated_at",    null: false
39
+    t.index ["group_id"], name: "index_default_subgroups_on_group_id"
40
+  end
41
+
30 42
   create_table "delayed_jobs", force: :cascade do |t|
31 43
     t.integer  "priority",   default: 0, null: false
32 44
     t.integer  "attempts",   default: 0, null: false
@@ -67,9 +79,11 @@ ActiveRecord::Schema.define(version: 20170917140643) do
67 79
     t.text     "notes"
68 80
     t.datetime "created_at",   null: false
69 81
     t.datetime "updated_at",   null: false
82
+    t.integer  "subgroup_id"
70 83
     t.index ["activity_id"], name: "index_participants_on_activity_id"
71 84
     t.index ["person_id", "activity_id"], name: "index_participants_on_person_id_and_activity_id", unique: true
72 85
     t.index ["person_id"], name: "index_participants_on_person_id"
86
+    t.index ["subgroup_id"], name: "index_participants_on_subgroup_id"
73 87
   end
74 88
 
75 89
   create_table "people", force: :cascade do |t|
@@ -96,6 +110,15 @@ ActiveRecord::Schema.define(version: 20170917140643) do
96 110
     t.index ["user_id"], name: "index_sessions_on_user_id"
97 111
   end
98 112
 
113
+  create_table "subgroups", force: :cascade do |t|
114
+    t.integer  "activity_id"
115
+    t.string   "name",          null: false
116
+    t.boolean  "is_assignable"
117
+    t.datetime "created_at",    null: false
118
+    t.datetime "updated_at",    null: false
119
+    t.index ["activity_id"], name: "index_subgroups_on_activity_id"
120
+  end
121
+
99 122
   create_table "tokens", force: :cascade do |t|
100 123
     t.string   "token"
101 124
     t.datetime "expires"

+ 4 - 2
db/seeds.rb

@@ -22,7 +22,8 @@ u = User.create!(
22 22
   email: 'maarten@maartenberg.nl',
23 23
   person: p,
24 24
   password: 'aardbei123',
25
-  password_confirmation: 'aardbei123'
25
+  password_confirmation: 'aardbei123',
26
+  confirmed: true
26 27
 )
27 28
 
28 29
 p2 = Person.create!(
@@ -75,7 +76,8 @@ Group.all.each do |g|
75 76
       start: starttime,
76 77
       end: endtime,
77 78
       deadline: deadline,
78
-      group: g
79
+      group: g,
80
+      no_response_action: Faker::Boolean.boolean
79 81
     )
80 82
   end
81 83
 end

+ 2 - 2
public/batch_activities.csv

@@ -1,2 +1,2 @@
1
-name,description,location,start_date,start_time,end_date,end_time,deadline_date,deadline_time
2
-Name,Description,Location,2017-12-31,12:34:00,2017-12-31,12:43:00,2017-12-28,13:37:00
1
+name,description,location,start_date,start_time,end_date,end_time,deadline_date,deadline_time,reminder_at_date,reminder_at_time,subgroup_division_enabled,no_response_action
2
+Name,Description,Where,2017-12-31,12:34,2017-12-31,12:43,2017-12-28,13:37,2017-12-27,23:59,Y(es)/N(o),P(resent)/A(bsent)

+ 2 - 1
public/batch_persons.csv

@@ -1 +1,2 @@
1
-first_name,infix,last_name,birth_date,email,organizers
1
+first_name,infix,last_name,birth_date,email
2
+Gekke,,Henkie,1-1-1997,gekkehenkie@maartenberg.nl