ソースを参照

Merge branch 'feature/subgroups'

Maarten van den Berg 7 年 前
コミット
f1c38c509e
共有50 個のファイルを変更した1299 個の追加215 個の削除を含む
  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
 
35
 
36
 # Do we bind to a socket or port?
36
 # Do we bind to a socket or port?
37
 PUMA_BIND=
37
 PUMA_BIND=
38
+
39
+# Set to enable Sentry reporting
40
+SENTRY_DSN=

+ 3 - 0
Gemfile

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

+ 7 - 1
Gemfile.lock

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

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

2
   clipboard = new Clipboard('.copy-reactions', {
2
   clipboard = new Clipboard('.copy-reactions', {
3
     'text': clipreactions
3
     'text': clipreactions
4
   })
4
   })
5
+  $('.subgroup-filter').on('change', (e) -> filterparticipants(e))
5
 
6
 
6
 @clipreactions = (trigger) ->
7
 @clipreactions = (trigger) ->
7
   id = trigger.dataset['activity']
8
   id = trigger.dataset['activity']
24
     res.push(resp['unknown']['message'])
25
     res.push(resp['unknown']['message'])
25
 
26
 
26
   res.join('\n')
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
 // Creates an AJAX-request and registers the appropriate handlers once it is done.
13
 // Creates an AJAX-request and registers the appropriate handlers once it is done.
14
 function change_presence(e)
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
 // Update all references on the page to this activity:
67
 // Update all references on the page to this activity:
69
 // 2. The present/absent buttons
69
 // 2. The present/absent buttons
70
 function activity_changed(data, textStatus, xhr)
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
     $(`.btn-present[data-person-id=${target.person}][data-activity-id=${target.activity}]`)
106
     $(`.btn-present[data-person-id=${target.person}][data-activity-id=${target.activity}]`)
107
       .html(new_confirm_icon);
107
       .html(new_confirm_icon);
108
     $(`.btn-absent[data-person-id=${target.person}][data-activity-id=${target.activity}]`)
108
     $(`.btn-absent[data-person-id=${target.person}][data-activity-id=${target.activity}]`)
113
       .append(" Present");
113
       .append(" Present");
114
     $(`.btn-absent[data-person-id=${target.person}][data-activity-id=${target.activity}][data-wide=1]`)
114
     $(`.btn-absent[data-person-id=${target.person}][data-activity-id=${target.activity}][data-wide=1]`)
115
       .append(" Absent");
115
       .append(" Absent");
116
+
117
+    if (window.updatecounts != undefined)
118
+        updatecounts(window.subgroupfilter);
116
 }
119
 }
117
 
120
 
118
 function alert_failure(data, textStatus, xhr)
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
 var check_unselected = '<i class="fa fa-check"></i>';
126
 var check_unselected = '<i class="fa fa-check"></i>';

+ 160 - 10
app/controllers/activities_controller.rb

1
 class ActivitiesController < ApplicationController
1
 class ActivitiesController < ApplicationController
2
   include GroupsHelper
2
   include GroupsHelper
3
   include ActivitiesHelper
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
   before_action :require_membership!
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
   # GET /groups/:id/activities
24
   # GET /groups/:id/activities
11
   # GET /activities.json
25
   # GET /activities.json
12
   def index
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
   end
38
   end
18
 
39
 
19
   # GET /activities/1
40
   # GET /activities/1
33
       .find_by(person: current_person)
54
       .find_by(person: current_person)
34
     @counts = @activity.state_counts
55
     @counts = @activity.state_counts
35
     @num_participants = @counts.values.sum
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
   end
66
   end
37
 
67
 
38
   # GET /activities/new
68
   # GET /activities/new
45
     set_edit_parameters!
75
     set_edit_parameters!
46
   end
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
   # Shared lookups for rendering the edit-view
153
   # Shared lookups for rendering the edit-view
49
   def set_edit_parameters!
154
   def set_edit_parameters!
50
     @non_organizers = @activity.participants.where(is_organizer: [false, nil])
155
     @non_organizers = @activity.participants.where(is_organizer: [false, nil])
55
 
160
 
56
     @non_organizers_options.sort!
161
     @non_organizers_options.sort!
57
     @organizers_options.sort!
162
     @organizers_options.sort!
163
+
164
+    @subgroup = Subgroup.new if !@subgroup
165
+    @subgroups = @activity.subgroups.order(is_assignable: :desc, name: :asc)
58
   end
166
   end
59
 
167
 
60
   # POST /activities
168
   # POST /activities
91
     end
199
     end
92
     flash_message(:success, message)
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
   end
203
   end
96
 
204
 
97
   # PATCH/PUT /activities/1
205
   # PATCH/PUT /activities/1
125
     end
233
     end
126
   end
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
   # PATCH/PUT /groups/:group_id/activities/:id/presence
270
   # PATCH/PUT /groups/:group_id/activities/:id/presence
129
   # PATCH/PUT /groups/:group_id/activities/:id/presence.json
271
   # PATCH/PUT /groups/:group_id/activities/:id/presence.json
130
   def presence
272
   def presence
176
       @group = Group.find(params[:group_id])
318
       @group = Group.find(params[:group_id])
177
     end
319
     end
178
 
320
 
321
+    def set_subgroup
322
+      @subgroup = Subgroup.find(params[:subgroup_id])
323
+    end
324
+
179
     # Never trust parameters from the scary internet, only allow the white list through.
325
     # Never trust parameters from the scary internet, only allow the white list through.
180
     def activity_params
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
     end
332
     end
183
 end
333
 end

+ 2 - 1
app/controllers/dashboard_controller.rb

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

+ 57 - 13
app/controllers/groups_controller.rb

1
 class GroupsController < ApplicationController
1
 class GroupsController < ApplicationController
2
   include GroupsHelper
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
   before_action :require_membership!, only: [:show]
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
   # GET /groups
9
   # GET /groups
9
   # GET /groups.json
10
   # GET /groups.json
18
   # GET /groups/1
19
   # GET /groups/1
19
   # GET /groups/1.json
20
   # GET /groups/1.json
20
   def show
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
       @groupmenu = 'col-md-6'
29
       @groupmenu = 'col-md-6'
27
     else
30
     else
28
       @groupmenu = 'col-md-12'
31
       @groupmenu = 'col-md-12'
45
 
48
 
46
   # GET /groups/1/edit
49
   # GET /groups/1/edit
47
   def edit
50
   def edit
51
+    @defaultsubgroup = DefaultSubgroup.new
48
   end
52
   end
49
 
53
 
50
   # POST /groups
54
   # POST /groups
77
         }
81
         }
78
         format.json { render :show, status: :ok, location: @group }
82
         format.json { render :show, status: :ok, location: @group }
79
       else
83
       else
84
+        @defaultsubgroup = DefaultSubgroup.new
80
         format.html { render :edit }
85
         format.html { render :edit }
81
         format.json { render json: @group.errors, status: :unprocessable_entity }
86
         format.json { render json: @group.errors, status: :unprocessable_entity }
82
       end
87
       end
97
   end
102
   end
98
 
103
 
99
   def mass_add_members
104
   def mass_add_members
100
-    @group = Group.find(params[:group_id])
101
   end
105
   end
102
 
106
 
103
   def process_mass_add_members
107
   def process_mass_add_members
104
-    @group = Group.find(params[:group_id])
105
     require 'csv'
108
     require 'csv'
106
     uploaded_io = params[:spreadsheet]
109
     uploaded_io = params[:spreadsheet]
107
     result = Person.from_csv(uploaded_io.read)
110
     result = Person.from_csv(uploaded_io.read)
108
 
111
 
109
     result.each do |p|
112
     result.each do |p|
110
       m = Member.find_by(person: p, group: @group)
113
       m = Member.find_by(person: p, group: @group)
111
-      if not m
114
+      unless m
112
         m = Member.new(person: p, group: @group)
115
         m = Member.new(person: p, group: @group)
113
         m.save!
116
         m.save!
114
       end
117
       end
115
     end
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
     redirect_to group_members_path(@group)
120
     redirect_to group_members_path(@group)
118
   end
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
   private
155
   private
121
     # Use callbacks to share common setup or constraints between actions.
156
     # Use callbacks to share common setup or constraints between actions.
122
     def set_group
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
     end
164
     end
125
 
165
 
126
     # Never trust parameters from the scary internet, only allow the white list through.
166
     # Never trust parameters from the scary internet, only allow the white list through.
127
     def group_params
167
     def group_params
128
       params.require(:group).permit(:name)
168
       params.require(:group).permit(:name)
129
     end
169
     end
170
+
171
+    def default_subgroup_params
172
+      params.require(:default_subgroup).permit(:name, :is_assignable)
173
+    end
130
 end
174
 end

+ 4 - 0
app/helpers/authentication_helper.rb

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

+ 35 - 1
app/mailers/participant_mailer.rb

3
     @person = person
3
     @person = person
4
     @activity = activity
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
     mail(to: @person.email, subject: subject)
42
     mail(to: @person.email, subject: subject)
9
   end
43
   end

+ 149 - 5
app/models/activity.rb

38
   # @!attribute reminder_done
38
   # @!attribute reminder_done
39
   #   @return [Boolean]
39
   #   @return [Boolean]
40
   #     whether or not sending the reminder has finished.
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
   belongs_to :group
56
   belongs_to :group
43
 
57
 
45
     dependent: :destroy
59
     dependent: :destroy
46
   has_many :people, through: :participants
60
   has_many :people, through: :participants
47
 
61
 
62
+  has_many :subgroups,
63
+    dependent: :destroy
64
+
48
   validates :name, presence: true
65
   validates :name, presence: true
49
   validates :start, presence: true
66
   validates :start, presence: true
50
   validate  :deadline_before_start, unless: "self.deadline.blank?"
67
   validate  :deadline_before_start, unless: "self.deadline.blank?"
51
   validate  :end_after_start,       unless: "self.end.blank?"
68
   validate  :end_after_start,       unless: "self.end.blank?"
52
   validate  :reminder_before_deadline, unless: "self.reminder_at.blank?"
69
   validate  :reminder_before_deadline, unless: "self.reminder_at.blank?"
70
+  validate  :subgroups_for_division_present, on: :update
53
 
71
 
54
   after_create :create_missing_participants!
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
   # Get all people (not participants) that are organizers. Does not include
82
   # Get all people (not participants) that are organizers. Does not include
58
   # group leaders, although they may modify the activity as well.
83
   # group leaders, although they may modify the activity as well.
60
     self.participants.includes(:person).where(is_organizer: true)
85
     self.participants.includes(:person).where(is_organizer: true)
61
   end
86
   end
62
 
87
 
88
+  def organizer_names
89
+    self.organizers.map { |o| o.person.full_name }
90
+  end
91
+
63
   # Determine whether the passed Person participates in the activity.
92
   # Determine whether the passed Person participates in the activity.
64
   def is_participant?(person)
93
   def is_participant?(person)
65
     Participant.exists?(
94
     Participant.exists?(
115
     end
144
     end
116
   end
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
   # Create multiple Activities from data in a CSV file, assign to a group, return.
163
   # Create multiple Activities from data in a CSV file, assign to a group, return.
119
   def self.from_csv(content, group)
164
   def self.from_csv(content, group)
120
     reader = CSV.parse(content, {headers: true, skip_blanks: true})
165
     reader = CSV.parse(content, {headers: true, skip_blanks: true})
131
       st            = Time.strptime(row['start_time'], '%H:%M')
176
       st            = Time.strptime(row['start_time'], '%H:%M')
132
       a.start       = Time.zone.local(sd.year, sd.month, sd.day, st.hour, st.min)
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
         ed          = Date.strptime(row['end_date'])
180
         ed          = Date.strptime(row['end_date'])
136
         et          = Time.strptime(row['end_time'], '%H:%M')
181
         et          = Time.strptime(row['end_time'], '%H:%M')
137
         a.end       = Time.zone.local(ed.year, ed.month, ed.day, et.hour, et.min)
182
         a.end       = Time.zone.local(ed.year, ed.month, ed.day, et.hour, et.min)
138
       end
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
       result << a
205
       result << a
145
     end
206
     end
167
     self.delay(run_at: self.reminder_at).send_reminder
228
     self.delay(run_at: self.reminder_at).send_reminder
168
   end
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
   private
306
   private
307
+
171
   # Assert that the deadline for participants to change the deadline, if any,
308
   # Assert that the deadline for participants to change the deadline, if any,
172
   # is set before the event starts.
309
   # is set before the event starts.
173
   def deadline_before_start
310
   def deadline_before_start
190
       errors.add(:reminder_at, I18n.t('activities.errors.must_be_before_deadline'))
327
       errors.add(:reminder_at, I18n.t('activities.errors.must_be_before_deadline'))
191
     end
328
     end
192
   end
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
 end
337
 end

+ 6 - 0
app/models/default_subgroup.rb

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
   has_many :activities,
13
   has_many :activities,
14
     dependent: :destroy
14
     dependent: :destroy
15
 
15
 
16
+  has_many :default_subgroups,
17
+    dependent: :destroy
18
+
16
   validates :name,
19
   validates :name,
17
     presence: true,
20
     presence: true,
18
     uniqueness: {
21
     uniqueness: {

+ 28 - 1
app/models/participant.rb

17
 
17
 
18
   belongs_to :person
18
   belongs_to :person
19
   belongs_to :activity
19
   belongs_to :activity
20
+  belongs_to :subgroup, optional: true
21
+
22
+  after_validation :clear_subgroup, if: 'self.attending != true'
20
 
23
 
21
   validates :person_id,
24
   validates :person_id,
22
     uniqueness: {
25
     uniqueness: {
24
       message: I18n.t('activities.errors.already_in')
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
   # TODO: Move to a more appropriate place
42
   # TODO: Move to a more appropriate place
28
   # @return [String]
43
   # @return [String]
29
   #   the class for a row containing this activity.
44
   #   the class for a row containing this activity.
46
   def send_reminder
61
   def send_reminder
47
     return unless self.attending.nil?
62
     return unless self.attending.nil?
48
 
63
 
49
-    self.attending = true
64
+    self.attending = self.activity.no_response_action
50
     notes = self.notes || ""
65
     notes = self.notes || ""
51
     notes << '[auto]'
66
     notes << '[auto]'
52
     self.notes = notes
67
     self.notes = notes
56
     ParticipantMailer.attendance_reminder(self.person, self.activity).deliver_later
71
     ParticipantMailer.attendance_reminder(self.person, self.activity).deliver_later
57
   end
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
 end
86
 end

+ 15 - 0
app/models/subgroup.rb

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
 <%= form_for([@group, activity]) do |f| %>
1
 <%= form_for([@group, activity]) do |f| %>
2
   <% if activity.errors.any? %>
2
   <% if activity.errors.any? %>
3
     <div id="error_explanation">
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
       <ul>
8
       <ul>
7
       <% activity.errors.full_messages.each do |message| %>
9
       <% activity.errors.full_messages.each do |message| %>
40
       <%= f.label :deadline %>
42
       <%= f.label :deadline %>
41
       <%= f.datetime_field :deadline, class: 'form-control' %>
43
       <%= f.datetime_field :deadline, class: 'form-control' %>
42
     </div>
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
     <div class="form-group">
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
     </div>
86
     </div>
47
-    <%= f.submit class: 'btn btn-primary' %>
48
   </div>
87
   </div>
49
 <% end %>
88
 <% end %>

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

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

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

7
   = t 'activities.organizers.manage'
7
   = t 'activities.organizers.manage'
8
 .row
8
 .row
9
   .col-md-6
9
   .col-md-6
10
-    %h4
10
+    %h4#organizers-add
11
       = t 'activities.organizers.add'
11
       = t 'activities.organizers.add'
12
     - if @non_organizers.count > 0
12
     - if @non_organizers.count > 0
13
       = form_tag(group_activity_change_organizer_path(@group, @activity), method: 'post') do
13
       = form_tag(group_activity_change_organizer_path(@group, @activity), method: 'post') do
20
       = t 'activities.organizers.no_non_organizers'
20
       = t 'activities.organizers.no_non_organizers'
21
 
21
 
22
   .col-md-6
22
   .col-md-6
23
-    %h4
23
+    %h4#organizers-remove
24
       = t 'activities.organizers.remove'
24
       = t 'activities.organizers.remove'
25
     - if @organizers.count > 0
25
     - if @organizers.count > 0
26
       = form_tag(group_activity_change_organizer_path(@group, @activity), method: 'post') do
26
       = form_tag(group_activity_change_organizer_path(@group, @activity), method: 'post') do
33
     - else
33
     - else
34
       = t 'activities.organizers.no_organizers'
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

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
 %h1
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
 - isleader = @group.leaders.include?(current_person) || current_person.is_admin?
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
 %table.table
22
 %table.table
11
   %thead
23
   %thead
12
     %tr
24
     %tr
13
       %th
25
       %th
14
-        = t 'activerecord.attributes.activities.name'
26
+        = t 'activerecord.attributes.activity.name'
15
 
27
 
16
       %th
28
       %th
17
-        = t 'activerecord.attributes.activities.start'
29
+        = t 'activerecord.attributes.activity.start'
18
 
30
 
19
       %th
31
       %th
20
         P/A/?
32
         P/A/?
40
               %i.fa.fa-pencil
52
               %i.fa.fa-pencil
41
 
53
 
42
 = will_paginate @activities
54
 = will_paginate @activities
43
-

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

11
     .col-md-12
11
     .col-md-12
12
       = form_tag(group_activities_mass_new_path(@group), method: 'post', multipart: true) do
12
       = form_tag(group_activities_mass_new_path(@group), method: 'post', multipart: true) do
13
         .form-group
13
         .form-group
14
-          = file_field_tag 'spreadsheet'
14
+          = file_field_tag 'spreadsheet', required: true
15
         .form-group
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
 
25
 
26
 
26
 
27
       %table.table
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
         %tr
49
         %tr
46
           %td
50
           %td
55
               - else
59
               - else
56
                 = l @activity.end, format: :long
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
               = l @activity.deadline, format: :long
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
   - if @ownparticipant
85
   - if @ownparticipant
67
     .col-md-3
86
     .col-md-3
68
       .panel.panel-default
87
       .panel.panel-default
79
             emptytext: t('activities.participant.add_notes')
98
             emptytext: t('activities.participant.add_notes')
80
 
99
 
81
 .hidden-xs
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
   %table.table.table-bordered
112
   %table.table.table-bordered
88
     - @participants.each do |p|
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
         %td
115
         %td
91
           = p.person.full_name
116
           = p.person.full_name
92
           - if p.is_organizer
117
           - if p.is_organizer
100
             = render partial: "activities/presence_buttons", locals: {activity: @activity, person: p.person, state: p.attending}
125
             = render partial: "activities/presence_buttons", locals: {activity: @activity, person: p.person, state: p.attending}
101
 
126
 
102
 .hidden-sm.hidden-md.hidden-lg
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
   .panel.panel-default.panel-success
135
   .panel.panel-default.panel-success
104
     .panel-heading
136
     .panel-heading
105
       %a{role: 'button', href: '#present-collapse', data: {toggle: 'collapse'}, 'aria-expanded': 'false'}
137
       %a{role: 'button', href: '#present-collapse', data: {toggle: 'collapse'}, 'aria-expanded': 'false'}
111
           %i.fa.fa-angle-up
143
           %i.fa.fa-angle-up
112
 
144
 
113
         = t 'activities.state.present'
145
         = t 'activities.state.present'
114
-        %span.badge
146
+        %span.badge.state-count.present-count
115
           = @counts[true] || "0"
147
           = @counts[true] || "0"
116
 
148
 
117
     %table.table.collapse#present-collapse
149
     %table.table.collapse#present-collapse
118
       %tbody
150
       %tbody
119
         - @participants.where(attending: true).each do |p|
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
             %td
153
             %td
122
               = p.person.full_name
154
               = p.person.full_name
123
               - if p.is_organizer
155
               - if p.is_organizer
127
               - if p.person.id == current_person.id || all_buttons
159
               - if p.person.id == current_person.id || all_buttons
128
                 = render partial: "activities/presence_buttons", locals: {activity: @activity, person: p.person, state: p.attending}
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
             %td{colspan: "2"}
163
             %td{colspan: "2"}
132
               = 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: "--"
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
 
176
 
145
         = t 'activities.state.need_response'
177
         = t 'activities.state.need_response'
146
 
178
 
147
-        %span.badge
179
+        %span.badge.state-count.unknown-count
148
           = @counts[nil] || "0"
180
           = @counts[nil] || "0"
149
 
181
 
150
     %table.table.collapse#unknown-collapse
182
     %table.table.collapse#unknown-collapse
151
       %tbody
183
       %tbody
152
         - @participants.where(attending: nil).each do |p|
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
             %td
186
             %td
155
               = p.person.full_name
187
               = p.person.full_name
156
               - if p.is_organizer
188
               - if p.is_organizer
160
               - if p.person.id == current_person.id || all_buttons
192
               - if p.person.id == current_person.id || all_buttons
161
                 = render partial: "activities/presence_buttons", locals: {activity: @activity, person: p.person, state: p.attending}
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
             %td{colspan: "2"}
196
             %td{colspan: "2"}
165
               = 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: "--"
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
 
208
 
177
         = t 'activities.state.absent'
209
         = t 'activities.state.absent'
178
 
210
 
179
-        %span.badge
211
+        %span.badge.state-count.absent-count
180
           = @counts[false] || "0"
212
           = @counts[false] || "0"
181
 
213
 
182
     %table.table.collapse#absent-collapse
214
     %table.table.collapse#absent-collapse
183
       %tbody
215
       %tbody
184
         - @participants.where(attending: false).each do |p|
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
             %td
218
             %td
187
               = p.person.full_name
219
               = p.person.full_name
188
               - if p.is_organizer
220
               - if p.is_organizer
192
               - if p.person.id == current_person.id || all_buttons
224
               - if p.person.id == current_person.id || all_buttons
193
                 = render partial: "activities/presence_buttons", locals: {activity: @activity, person: p.person, state: p.attending}
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
             %td{colspan: "2"}
228
             %td{colspan: "2"}
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: "--"
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

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
                     = link_to group do
90
                     = link_to group do
91
                       = group.name
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
   .row
110
   .row
110
     .col-md-12
111
     .col-md-12

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

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

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
   .row
2
   .row
3
     .col-md-12
3
     .col-md-12
4
       %h1
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
 %p= t 'authentication.emails.greeting', name: @person.first_name
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
 %p= t 'activities.emails.attendance_reminder.if_cannot', deadline: l(@activity.deadline, format: :short)
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
 <%= t 'authentication.emails.greeting', name: @person.first_name %>
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
 <%= t 'activities.emails.attendance_reminder.if_cannot', deadline: l(@activity.deadline, format: :short) %>
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

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

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
 
46
 
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!"
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
   activerecord:
56
   activerecord:
50
     models:
57
     models:
51
       person:
58
       person:
83
         end: "End"
90
         end: "End"
84
         description: "Description"
91
         description: "Description"
85
         deadline: "Deadline"
92
         deadline: "Deadline"
93
+        no_response_action: "Action when no response"

+ 8 - 1
config/locales/aardbei_nl.yml

19
 
19
 
20
   areyousure: "Weet je het zeker?"
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
   value_required: "Een verplichte vraag was leeg."
24
   value_required: "Een verplichte vraag was leeg."
25
 
25
 
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!"
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
     edit: "Edit activity"
8
     edit: "Edit activity"
9
     updated: "Activity updated."
9
     updated: "Activity updated."
10
     destroyed: "Activity destroyed."
10
     destroyed: "Activity destroyed."
11
+    mass_import_short: "Import activities"
11
     mass_import: "Create multiple activities in %{group}"
12
     mass_import: "Create multiple activities in %{group}"
12
     mass_imported: "%{count} activities created!"
13
     mass_imported: "%{count} activities created!"
13
 
14
 
15
+    upcoming: 'Upcoming activities'
16
+    past: 'Past activities'
17
+
14
     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."
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
     upcoming_yours: "Upcoming activities organized by you"
20
     upcoming_yours: "Upcoming activities organized by you"
19
     errors:
23
     errors:
20
       must_be_before_start: "must be before start"
24
       must_be_before_start: "must be before start"
21
       must_be_after_start: "must be after start"
25
       must_be_after_start: "must be after start"
26
+      must_be_before_deadline: "must be before deadline"
22
       already_in: "person already participates in activity"
27
       already_in: "person already participates in activity"
28
+      cannot_divide_without_subgroups: "a divisible subgroup must exist"
23
 
29
 
24
     participant:
30
     participant:
25
       singular: "participant"
31
       singular: "participant"
27
       notes: "Notes"
33
       notes: "Notes"
28
       add_notes: "Add notes"
34
       add_notes: "Add notes"
29
       yourresponse: "Your response"
35
       yourresponse: "Your response"
36
+      yoursubgroup: "Your subgroup"
30
 
37
 
31
       copy_responses: "Copy responses"
38
       copy_responses: "Copy responses"
32
       copy_absent: "Copy absentees"
39
       copy_absent: "Copy absentees"
69
       open_activity: "Open activity"
76
       open_activity: "Open activity"
70
       open_settings: "Preferences"
77
       open_settings: "Preferences"
71
       dont_want_mail: "If you no longer want to receive these emails, change your preferences using the following link:"
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
       ending:
80
       ending:
73
         - "Cheers,"
81
         - "Cheers,"
74
       attendance_reminder:
82
       attendance_reminder:
75
         subject: "You are now listed as 'attending' for %{activity}"
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
         if_cannot: "If you wish to change this, you can change your response until %{deadline} using the following link:"
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
     edit: "Activiteit bewerken"
8
     edit: "Activiteit bewerken"
9
     updated: "Activiteit bijgewerkt."
9
     updated: "Activiteit bijgewerkt."
10
     destroyed: "Activiteit verwijderd."
10
     destroyed: "Activiteit verwijderd."
11
+    mass_import_short: "Activiteiten importeren"
11
     mass_import: "Meerdere activiteiten aanmaken in %{group}"
12
     mass_import: "Meerdere activiteiten aanmaken in %{group}"
12
     mass_imported: "%{count} activiteiten aangemaakt!"
13
     mass_imported: "%{count} activiteiten aangemaakt!"
13
 
14
 
15
+    upcoming: 'Aankomende activiteiten'
16
+    past: 'Afgelopen activiteiten'
17
+
14
     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."
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
     upcoming_yours: "Aankomende activiteiten georganiseerd door jou"
20
     upcoming_yours: "Aankomende activiteiten georganiseerd door jou"
21
       must_be_after_start: "moet na start zijn"
25
       must_be_after_start: "moet na start zijn"
22
       must_be_before_deadline: "moet voor deadline zijn"
26
       must_be_before_deadline: "moet voor deadline zijn"
23
       already_in: "persoon doet al mee aan activiteit"
27
       already_in: "persoon doet al mee aan activiteit"
28
+      cannot_divide_without_subgroups: "moet indeelbare subgroep hebben"
24
 
29
 
25
     participant:
30
     participant:
26
       singular: "deelnemer"
31
       singular: "deelnemer"
28
       notes: "Opmerkingen"
33
       notes: "Opmerkingen"
29
       add_notes: "Opmerkingen toevoegen"
34
       add_notes: "Opmerkingen toevoegen"
30
       yourresponse: "Jouw antwoord"
35
       yourresponse: "Jouw antwoord"
36
+      yoursubgroup: "Jouw subgroep"
31
 
37
 
32
       copy_responses: "Kopieer overzicht"
38
       copy_responses: "Kopieer overzicht"
33
       copy_absent: "Kopieer afwezigen"
39
       copy_absent: "Kopieer afwezigen"
66
       description: "Omschrijving"
72
       description: "Omschrijving"
67
       deadline: "Deadline"
73
       deadline: "Deadline"
68
 
74
 
75
+    no_response_action:
76
+      auto_present: 'Automatisch aanmelden'
77
+      auto_absent: 'Automatisch afmelden'
78
+
69
     emails:
79
     emails:
70
       open_activity: "Activiteit openen"
80
       open_activity: "Activiteit openen"
71
       open_settings: "Instellingen"
81
       open_settings: "Instellingen"
72
       dont_want_mail: "Als je dit soort mailtjes niet meer wilt ontvangen, kun je dit uitschakelen via de volgende link:"
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
       ending:
84
       ending:
74
         - "Met griendelijke vroet,"
85
         - "Met griendelijke vroet,"
75
         - "Met groetelijke doei,"
86
         - "Met groetelijke doei,"
80
         - "Doi,"
91
         - "Doi,"
81
         - "Talla,"
92
         - "Talla,"
82
       attendance_reminder:
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

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

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
     member_updated: "Member updated."
22
     member_updated: "Member updated."
23
     member_removed: "%{name} was removed from the group."
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
     member:
30
     member:
26
       add: "New Member"
31
       add: "New Member"
27
       adding: "Add member to %{name}"
32
       adding: "Add member to %{name}"

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

22
     member_updated: "Lid bijgewerkt."
22
     member_updated: "Lid bijgewerkt."
23
     member_removed: "%{name} is verwijderd uit de groep."
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
     member:
30
     member:
26
       add: "Lid toevoegen"
31
       add: "Lid toevoegen"
27
       adding: "Lid toevoegen aan %{name}"
32
       adding: "Lid toevoegen aan %{name}"

+ 21 - 0
config/locales/translation_nl.yml

24
       token: Token  #g
24
       token: Token  #g
25
       user: Gebruiker  #g
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
     attributes:
35
     attributes:
28
       activity:
36
       activity:
29
         deadline: Deadline  #g
37
         deadline: Deadline  #g
37
         start: Start  #g
45
         start: Start  #g
38
         reminder_at: Herinnering om
46
         reminder_at: Herinnering om
39
         reminder_done: Herinnering verstuurd
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
       group:
53
       group:
42
         activities: Activiteiten  #g
54
         activities: Activiteiten  #g
55
+        default_subgroups: Standaardsubgroepen
43
         members: Leden  #g
56
         members: Leden  #g
44
         name: Naam  #g
57
         name: Naam  #g
45
         people: Mensen  #g
58
         people: Mensen  #g
87
         email: E-mail  #g
100
         email: E-mail  #g
88
         password_digest: Wachtwoord-digest  #g
101
         password_digest: Wachtwoord-digest  #g
89
         person: :activerecord.models.person  #g
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
     get 'mass_add', to: 'groups#mass_add_members'
36
     get 'mass_add', to: 'groups#mass_add_members'
37
     post 'mass_add', to: 'groups#process_mass_add_members'
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
     resources :members do
43
     resources :members do
40
       post 'promote', to: 'members#promote', on: :member
44
       post 'promote', to: 'members#promote', on: :member
41
       post 'demote', to: 'members#demote', on: :member
45
       post 'demote', to: 'members#demote', on: :member
48
       post 'change_organizer', to: 'activities#change_organizer'
52
       post 'change_organizer', to: 'activities#change_organizer'
49
       put 'presence', to: 'activities#presence', on: :member
53
       put 'presence', to: 'activities#presence', on: :member
50
       patch 'presence', to: 'activities#presence', on: :member
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
     end
64
     end
52
   end
65
   end
53
   get 'my_groups', to: 'groups#user_groups', as: :user_groups
66
   get 'my_groups', to: 'groups#user_groups', as: :user_groups

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

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

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

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

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

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
 #
10
 #
11
 # It's strongly recommended that you check this file into your version control system.
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
   create_table "activities", force: :cascade do |t|
15
   create_table "activities", force: :cascade do |t|
16
     t.string   "name"
16
     t.string   "name"
20
     t.datetime "end"
20
     t.datetime "end"
21
     t.datetime "deadline"
21
     t.datetime "deadline"
22
     t.integer  "group_id"
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
     t.datetime "reminder_at"
25
     t.datetime "reminder_at"
26
     t.boolean  "reminder_done"
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
     t.index ["group_id"], name: "index_activities_on_group_id"
30
     t.index ["group_id"], name: "index_activities_on_group_id"
28
   end
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
   create_table "delayed_jobs", force: :cascade do |t|
42
   create_table "delayed_jobs", force: :cascade do |t|
31
     t.integer  "priority",   default: 0, null: false
43
     t.integer  "priority",   default: 0, null: false
32
     t.integer  "attempts",   default: 0, null: false
44
     t.integer  "attempts",   default: 0, null: false
67
     t.text     "notes"
79
     t.text     "notes"
68
     t.datetime "created_at",   null: false
80
     t.datetime "created_at",   null: false
69
     t.datetime "updated_at",   null: false
81
     t.datetime "updated_at",   null: false
82
+    t.integer  "subgroup_id"
70
     t.index ["activity_id"], name: "index_participants_on_activity_id"
83
     t.index ["activity_id"], name: "index_participants_on_activity_id"
71
     t.index ["person_id", "activity_id"], name: "index_participants_on_person_id_and_activity_id", unique: true
84
     t.index ["person_id", "activity_id"], name: "index_participants_on_person_id_and_activity_id", unique: true
72
     t.index ["person_id"], name: "index_participants_on_person_id"
85
     t.index ["person_id"], name: "index_participants_on_person_id"
86
+    t.index ["subgroup_id"], name: "index_participants_on_subgroup_id"
73
   end
87
   end
74
 
88
 
75
   create_table "people", force: :cascade do |t|
89
   create_table "people", force: :cascade do |t|
96
     t.index ["user_id"], name: "index_sessions_on_user_id"
110
     t.index ["user_id"], name: "index_sessions_on_user_id"
97
   end
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
   create_table "tokens", force: :cascade do |t|
122
   create_table "tokens", force: :cascade do |t|
100
     t.string   "token"
123
     t.string   "token"
101
     t.datetime "expires"
124
     t.datetime "expires"

+ 4 - 2
db/seeds.rb

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

+ 2 - 2
public/batch_activities.csv

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
-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