diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 561a8e30e..781c2de88 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -24,6 +24,7 @@
//= require pickadate/picker.time
//= require subscriptions-toggle
//= require invitations
+//= require dietary-restrictions
//= require cocoon
//= require font_awesome5
diff --git a/app/assets/javascripts/dietary-restrictions.js b/app/assets/javascripts/dietary-restrictions.js
new file mode 100644
index 000000000..323c50c15
--- /dev/null
+++ b/app/assets/javascripts/dietary-restrictions.js
@@ -0,0 +1,12 @@
+$(document).ready(function() {
+ $('#member_dietary_restrictions_other').on('change', function () {
+ const $otherDietaryRestrictions = $('#member_other_dietary_restrictions');
+ const $elementToToggle = $otherDietaryRestrictions.parent();
+ if ($elementToToggle.hasClass('d-none')) {
+ $elementToToggle.removeClass('d-none').hide().slideDown(50);
+ $otherDietaryRestrictions.focus();
+ } else {
+ $elementToToggle.slideUp(50, () => $elementToToggle.addClass('d-none'));
+ }
+ });
+});
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index a320bb756..82b19730d 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -27,3 +27,9 @@
$primary: $dark-codebar-blue !default;
@import "bootstrap-custom";
+
+/* Bootstrap's Reboot sets legends to float: left, which puts the first check box in a fieldset off to the
+ right instead of underneath the legend. This overrides that. */
+legend {
+ float: none !important;
+}
diff --git a/app/controllers/admin/members_controller.rb b/app/controllers/admin/members_controller.rb
index aa6b02880..2600de475 100644
--- a/app/controllers/admin/members_controller.rb
+++ b/app/controllers/admin/members_controller.rb
@@ -6,7 +6,7 @@ def index
end
def show
- @member = Member.find(params[:id])
+ @member = MemberPresenter.new(Member.find(params[:id]))
load_attendance_data(@member)
@actions = admin_actions(@member).sort_by(&:created_at).reverse
diff --git a/app/controllers/concerns/member_concerns.rb b/app/controllers/concerns/member_concerns.rb
index 8c75a43f6..fa8f7eed4 100644
--- a/app/controllers/concerns/member_concerns.rb
+++ b/app/controllers/concerns/member_concerns.rb
@@ -10,8 +10,16 @@ module InstanceMethods
def member_params
params.require(:member).permit(
- :pronouns, :name, :surname, :email, :mobile, :about_you, :skill_list, :newsletter
- )
+ :pronouns, :name, :surname, :email, :mobile, :about_you, :skill_list, :newsletter, :other_dietary_restrictions,
+ dietary_restrictions: [],
+ ).tap do |params|
+ # We want to keep Rails' hidden blank field in the form so that all dietary restrictions for a member can be
+ # removed by submitting the form with all check boxes unticked. However, we want to remove the blank value
+ # before setting the dietary restrictions attribute on the model.
+ # See Gotcha section here:
+ # https://api.rubyonrails.org/v7.1/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-collection_check_boxes
+ params[:dietary_restrictions] = params[:dietary_restrictions].reject(&:blank?) if params[:dietary_restrictions]
+ end
end
def suppress_notices
diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb
index e08932e12..ace862104 100644
--- a/app/controllers/members_controller.rb
+++ b/app/controllers/members_controller.rb
@@ -1,7 +1,7 @@
class MembersController < ApplicationController
include MemberConcerns
- before_action :set_member, only: %i[edit step2 profile update]
+ before_action :set_member, only: %i[edit step2 update]
before_action :authenticate_member!, only: %i[edit step2 profile]
before_action :suppress_notices, only: %i[step2]
@@ -20,6 +20,7 @@ def step2
end
def profile
+ @member = MemberPresenter.new(current_user)
render :show
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 59ef928aa..59ef29027 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -20,6 +20,10 @@ class Member < ApplicationRecord
validates :email, uniqueness: true
validates :about_you, length: { maximum: 255 }
+ DIETARY_RESTRICTIONS = %w[vegan vegetarian pescetarian halal gluten_free dairy_free other].freeze
+ validates_inclusion_of :dietary_restrictions, in: DIETARY_RESTRICTIONS
+ validates_presence_of :other_dietary_restrictions, if: :other_dietary_restrictions?
+
scope :accepted_toc, -> { where.not(accepted_toc_at: nil) }
scope :order_by_email, -> { order(:email) }
scope :subscribers, -> { joins(:subscriptions).order('created_at desc').uniq }
@@ -118,6 +122,10 @@ def recent_notes
member_notes.where('created_at > ?', notes_from_date)
end
+ def other_dietary_restrictions?
+ dietary_restrictions.present? && dietary_restrictions.include?('other')
+ end
+
def self.find_members_by_name(name)
name.strip!
name.eql?('') ? self.none : where("CONCAT(name, ' ', surname) ILIKE ?", "%#{name}%")
diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb
index 295acbe77..f8bde2269 100644
--- a/app/presenters/member_presenter.rb
+++ b/app/presenters/member_presenter.rb
@@ -23,6 +23,14 @@ def pairing_details_array(role, tutorial, note)
role.eql?('Coach') ? coach_pairing_details(note) : student_pairing_details(tutorial, note)
end
+ def displayed_dietary_restrictions
+ return [] if dietary_restrictions.nil?
+
+ (dietary_restrictions - ['other']).map(&:humanize).tap do |drs|
+ drs << other_dietary_restrictions if other_dietary_restrictions? && other_dietary_restrictions.present?
+ end.map(&:upcase_first)
+ end
+
private
def coach_pairing_details(note)
diff --git a/app/views/admin/members/_profile.html.haml b/app/views/admin/members/_profile.html.haml
index bd4e7f583..8b6061ee6 100644
--- a/app/views/admin/members/_profile.html.haml
+++ b/app/views/admin/members/_profile.html.haml
@@ -10,7 +10,15 @@
.mb-4
%h5 Skills
- @member.skills.each do |skill|
- .badge.bg-success= skill.name
+ .badge.bg-secondary= skill.name
+
+ .mb-4
+ %h5 Dietary Restrictions
+ - if @member.dietary_restrictions.present?
+ - @member.displayed_dietary_restrictions.each do |dr|
+ .badge.bg-secondary.text-wrap.text-break.mb-1.text-start= dr
+ - else
+ .text-muted None
- if @workshop_attendances.positive? || @meeting_rsvps.positive? || @event_rsvps.positive?
.attendance-summary.mt-4
diff --git a/app/views/admin/workshop/_attendances.html.haml b/app/views/admin/workshop/_attendances.html.haml
index d0800f1a8..13e79fe6e 100644
--- a/app/views/admin/workshop/_attendances.html.haml
+++ b/app/views/admin/workshop/_attendances.html.haml
@@ -31,6 +31,10 @@
= invitation.member.email
- else
= invitation.member.full_name
+ - if invitation.member.dietary_restrictions.present?
+ %p
+ - invitation.member.displayed_dietary_restrictions.each do |dr|
+ %span.badge.bg-secondary.text-wrap.text-break.mb-1.text-start= dr
.col-6
- if invitation.tutorial?
%p.mb-1 Tutorial: #{invitation.tutorial}
diff --git a/app/views/invitation/_coach.html.haml b/app/views/invitation/_coach.html.haml
index abb30d043..8120b0b42 100644
--- a/app/views/invitation/_coach.html.haml
+++ b/app/views/invitation/_coach.html.haml
@@ -12,4 +12,4 @@
- if @workshop.virtual?
= t('workshop.virtual.invitation.shared_intro_3_html', email_link: mail_to(@workshop.chapter.email, @workshop.chapter.email))
- else
- %p= t('workshop.invitation.shared_intro_4_html', email_link: mail_to(@workshop.chapter.email, @workshop.chapter.email).html_safe)
+ %p= t('workshop.invitation.shared_intro_4_html', update_your_details_link: link_to(t('members.update_your_details_lower'), edit_member_path).html_safe)
diff --git a/app/views/invitation/_student.html.haml b/app/views/invitation/_student.html.haml
index 0a409b04a..0a2d86de0 100644
--- a/app/views/invitation/_student.html.haml
+++ b/app/views/invitation/_student.html.haml
@@ -4,4 +4,4 @@
%p= t('workshop.virtual.invitation.shared_intro_3_html', email_link: mail_to(@workshop.chapter.email, @workshop.chapter.email))
- else
%p= t('workshop.invitation.student_intro_3_html')
- %p= t('workshop.invitation.shared_intro_4_html', email_link: mail_to(@workshop.chapter.email, @workshop.chapter.email).html_safe)
+ %p= t('workshop.invitation.shared_intro_4_html', update_your_details_link: link_to(t('members.update_your_details_lower'), edit_member_path).html_safe)
diff --git a/app/views/member/details/edit.html.haml b/app/views/member/details/edit.html.haml
index 60bc0fbec..1fc548ffe 100644
--- a/app/views/member/details/edit.html.haml
+++ b/app/views/member/details/edit.html.haml
@@ -15,6 +15,10 @@
= f.input :about_you, as: :text, label: t('member.details.edit.coach.about_you'), input_html: { rows: 3 }, required: true
- else
= f.input :about_you, as: :text, label: t('member.details.edit.student.about_you'), input_html: { rows: 3 }, required: true
+ = f.input :dietary_restrictions, collection: Member::DIETARY_RESTRICTIONS, as: :check_boxes,
+ label_method: ->(r) { r.humanize.upcase_first }
+ = f.input :other_dietary_restrictions, placeholder: 'Other dietary restrictions',
+ wrapper_html: { class: class_names('mt-n3', 'd-none': !@member.other_dietary_restrictions?) }, label_html: { class: 'sr-only' }
= f.input :newsletter, as: :boolean, checked_value: true, unchecked_value: false
.text-right.mb-4
= hidden_field_tag :next_page, step2_member_path(member_type: @type)
diff --git a/app/views/members/_new.html.haml b/app/views/members/_new.html.haml
index b6be091db..0a4e2ac36 100644
--- a/app/views/members/_new.html.haml
+++ b/app/views/members/_new.html.haml
@@ -12,6 +12,11 @@
= f.input :mobile
.col-12
= f.input :about_you, as: :text, label: 'Tell us a little bit about yourself', input_html: { rows: 3 }, required: true
+ .col-12
+ = f.input :dietary_restrictions, collection: Member::DIETARY_RESTRICTIONS, as: :check_boxes,
+ label_method: ->(r) { r.humanize.upcase_first }
+ = f.input :other_dietary_restrictions, placeholder: 'Other dietary restrictions',
+ wrapper_html: { class: class_names('mt-n3', 'd-none': !@member.other_dietary_restrictions?) }, label_html: { class: 'sr-only' }
- if @member.coach?
.col-12
= f.input :skill_list, input_html: { value: @member.skill_list.join(", ") }
diff --git a/app/views/members/show.html.haml b/app/views/members/show.html.haml
index 433b9539a..fdb543c01 100644
--- a/app/views/members/show.html.haml
+++ b/app/views/members/show.html.haml
@@ -32,6 +32,14 @@
- @member.skills.each do |skill|
.badge.bg-secondary= skill.name
+ %dt Dietary Restrictions
+ %dd.mb-3
+ - if @member.dietary_restrictions.present?
+ - @member.displayed_dietary_restrictions.each do |dr|
+ .badge.bg-secondary.text-wrap.text-break.mb-1.text-start= dr
+ - else
+ .text-muted None
+
= link_to 'Update your details', edit_member_path, class: 'btn btn-primary', role: 'button'
.col-12.col-md-4.offset-md-1.mb-4
diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb
index b977f6863..7232e7f43 100644
--- a/config/initializers/simple_form_bootstrap.rb
+++ b/config/initializers/simple_form_bootstrap.rb
@@ -73,7 +73,7 @@
end
# vertical input for radio buttons and check boxes
- config.wrappers :vertical_collection, item_wrapper_class: 'form-check', tag: 'fieldset', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
+ config.wrappers :vertical_collection, item_wrapper_class: 'form-check', tag: 'fieldset', class: 'form-group mb-3', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
b.use :html5
b.optional :readonly
b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba|
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 64ebc3b6f..efd509dd2 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -202,7 +202,6 @@ de:
q: "Ich werde pendeln. Können Sie mir bei meinen Reisekosten helfen?"
a: "Wir werden einige Reisestipendien anbieten. Bitte stellen dich sicher, dass du den Fragebogen ausfüllst, nachdem du dich angemeldet hast und wir werden uns mit dir in Verbindung setzen."
coaches:
- want_to_coach: "Möchtest Du in unseren Workshops coachen?"
read_before: "Wenn Du unsere Workshops noch nicht besucht hast, lies bitte unseren"
code_of_conduct: "Verhaltenskodex"
policy: "da wir eine Nulltoleranzpolitik haben und von allen ein entsprechendes Verhalten erwarten. Das solltest Du lesen: Unser"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index f83ed565e..a9ccfb5ac 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -167,7 +167,7 @@ en:
student_intro_1: 'In our workshops, you will get to go through any of our available tutorials, ask for programming advice or get help on your own projects.'
student_intro_2: 'In case you are unable to attend after you RSVP, please make sure you come back and update your attendance. We have limited space and do a coaches request depending on the number of attending students.'
student_intro_3_html: 'Please make sure you bring your laptop with you.'
- shared_intro_4_html: 'PS: There will also be food at the workshop. We always make an effort to have vegetarian, vegan and gluten-free options available. If you have any other dietary requirements, send us an email at %{email_link}.'
+ shared_intro_4_html: 'PS: There will also be food at the workshop. If you have any specific dietary restrictions, please %{update_your_details_link} to let us know about them.'
workshop_invitation:
coach_skills_tooltip: Keeping your skills up-to-date enables organisers to plan ahead and makes running workshops easier.
meeting:
@@ -302,13 +302,11 @@ en:
q: "I will be commuting. Can you help with my travel expenses?"
a: "We will be offering some travel grants. Just make sure you fill in the questionnaire after you RSVP and we will get in touch."
coaches:
- want_to_coach: "Do you want to coach at our workshops?"
read_before: "If you have not attended our workshops before, make sure you read our"
code_of_conduct: "code of conduct"
policy: "as we have a zero tolerance policy and expect everyone to behave appropriately. You should also go through our"
guide: "coaching guide"
unable_attend: "In case you are unable to attend after you RSVP, please make sure you come back and update your attendance as we rely on coaches showing up in order for our workshops to run smoothly."
- food: "PS: There will also be food at the workshop. We always make an effort to have vegetarian, vegan and gluten-free options available. If you have any other dietary requirements, send us email at"
code_of_conduct:
title: "Code of conduct"
summary:
@@ -336,6 +334,8 @@ en:
sign_up_as_student_disclaimer: "* I understand and meet the eligibility criteria."
sign_up_as_coach: "Join us as a coach"
code_of_conduct: "code of conduct"
+ update_your_details: "Update your details"
+ update_your_details_lower: "update your details"
new:
intro_html: "We provide a welcoming, inclusive, and supportive learning environment, with a strict zero-tolerance policy for any form of harassment or inappropriate behavior. Please take a moment to review our code of conduct before signing up."
students:
@@ -724,6 +724,7 @@ en:
number_of_coaches: Coach spots
seats: Student spots
member:
+ dietary_restrictions: If you have any dietary restrictions, let us know about them here
skill_list: Skills
activerecord:
attributes:
diff --git a/config/locales/en_AU.yml b/config/locales/en_AU.yml
index 5b13e8ae9..2dadd9d21 100644
--- a/config/locales/en_AU.yml
+++ b/config/locales/en_AU.yml
@@ -193,13 +193,11 @@ en-AU:
q: "I will be commuting. Can you help with my travel expenses?"
a: "We will be offering some travel grants. Just make sure you fill in the questionnaire after you RSVP and we will get in touch."
coaches:
- want_to_coach: "Do you want to coach at our workshops?"
read_before: "If you have not attended our workshops before, make sure you read our"
code_of_conduct: "code of conduct"
policy: "as we have a zero tolerance policy and expect everyone to behave appropriately. You should also go through our"
guide: "coaching guide"
unable_attend: "In case you are unable to attend after you RSVP, please make sure you come back and update your attendance as we rely on coaches showing up in order for our workshops to run smoothly."
- food: "PS: There will also be food at the workshop. We always make an effort to have vegetarian, vegan and gluten-free options available. If you have any other dietary requirements, send us email at"
code_of_conduct:
title: "Code of conduct"
summary:
diff --git a/config/locales/en_GB.yml b/config/locales/en_GB.yml
index 9dd982b55..01b5b27e1 100644
--- a/config/locales/en_GB.yml
+++ b/config/locales/en_GB.yml
@@ -193,13 +193,11 @@ en-GB:
q: "I will be commuting. Can you help with my travel expenses?"
a: "We will be offering some travel grants. Just make sure you fill in the questionnaire after you RSVP and we will get in touch."
coaches:
- want_to_coach: "Do you want to coach at our workshops?"
read_before: "If you have not attended our workshops before, make sure you read our"
code_of_conduct: "code of conduct"
policy: "as we have a zero tolerance policy and expect everyone to behave appropriately. You should also go through our"
guide: "coaching guide"
unable_attend: "In case you are unable to attend after you RSVP, please make sure you come back and update your attendance as we rely on coaches showing up in order for our workshops to run smoothly."
- food: "PS: There will also be food at the workshop. We always make an effort to have vegetarian, vegan and gluten-free options available. If you have any other dietary requirements, send us email at"
code_of_conduct:
title: "Code of conduct"
summary:
diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml
index 5063d0dbf..4662f4808 100644
--- a/config/locales/en_US.yml
+++ b/config/locales/en_US.yml
@@ -191,13 +191,11 @@ en-US:
q: "I will be commuting. Can you help with my travel expenses?"
a: "We will be offering some travel grants. Just make sure you fill in the questionnaire after you RSVP and we will get in touch."
coaches:
- want_to_coach: "Do you want to coach at our workshops?"
read_before: "If you have not attended our workshops before, make sure you read our"
code_of_conduct: "code of conduct"
policy: "as we have a zero tolerance policy and expect everyone to behave appropriately. You should also go through our"
guide: "coaching guide"
unable_attend: "In case you are unable to attend after you RSVP, please make sure you come back and update your attendance as we rely on coaches showing up in order for our workshops to run smoothly."
- food: "PS: There will also be food at the workshop. We always make an effort to have vegetarian, vegan and gluten-free options available. If you have any other dietary requirements, send us email at"
code_of_conduct:
title: "Code of conduct"
summary:
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 995bc5378..5edc8d789 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -191,13 +191,11 @@ es:
q: "Estaré viajando. ¿Me pueden ayudar con mis gastos de viaje?"
a: "Vamos a ofrecer algunas becas de viaje. Solo asegúrate de completar el cuestionario después de que RSVP y nos pondremos en contacto."
coaches:
- want_to_coach: "¿Quieres formarte en nuestros talleres?"
read_before: "Si no ha asistido a nuestros talleres antes, asegúrate de leer nuestro"
code_of_conduct: "código de conducta"
policy: "Como tenemos una política de tolerancia cero y esperamos que todos se comporten de manera adecuada. También deberías pasar por nuestro"
guide: "guía para entrenadores"
unable_attend: "En caso de que no pueda asistir después de su RSVP, asegúrese de regresar y actualizar su asistencia a medida que confíe en los entrenadores que aparecen para que nuestros talleres se ejecuten sin problemas."
- food: "También habrá comida en el taller. Siempre nos esforzamos en tener disponibles opciones vegetarianas, veganas y sin gluten. Si tiene otros requisitos dietéticos, envíenos un correo electrónico a"
code_of_conduct:
title: "Código de Conducta"
summary:
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 691ebb230..b507db3f0 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -193,13 +193,11 @@ fi:
q: "I will be commuting. Can you help with my travel expenses?"
a: "We will be offering some travel grants. Just make sure you fill in the questionnaire after you RSVP and we will get in touch."
coaches:
- want_to_coach: "Do you want to coach at our workshops?"
read_before: "If you have not attended our workshops before, make sure you read our"
code_of_conduct: "code of conduct"
policy: "as we have a zero tolerance policy and expect everyone to behave appropriately. You should also go through our"
guide: "coaching guide"
unable_attend: "In case you are unable to attend after you RSVP, please make sure you come back and update your attendance as we rely on coaches showing up in order for our workshops to run smoothly."
- food: "PS: There will also be food at the workshop. We always make an effort to have vegetarian, vegan and gluten-free options available. If you have any other dietary requirements, send us email at"
code_of_conduct:
title: "Code of conduct"
summary:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 9cf9c1723..6ab3100fc 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -192,7 +192,6 @@ fr:
q: "Je voyage de loin. Pouvez-vous m'aider avec mes frais de déplacements?"
a: "Nous offrons des bourses de voyage. Vous devez vous assurer de remplir le questionnaire après vous être enregistrer et nous vous contacterons."
coaches:
- want_to_coach: "Voulez vous enseigner à nos ateliers?"
read_before: "Si vous n'êtes jamais venu a nos ateliers, merci de lire notre"
code_of_conduct: "code de conduite"
policy: "vu que nous attendons que chacun se comporte correctement. Vous devriez aussi lire notre"
diff --git a/config/locales/no.yml b/config/locales/no.yml
index 5bb9d6827..46918ac23 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -193,13 +193,11 @@
q: "I will be commuting. Can you help with my travel expenses?"
a: "We will be offering some travel grants. Just make sure you fill in the questionnaire after you RSVP and we will get in touch."
coaches:
- want_to_coach: "Do you want to coach at our workshops?"
read_before: "If you have not attended our workshops before, make sure you read our"
code_of_conduct: "code of conduct"
policy: "as we have a zero tolerance policy and expect everyone to behave appropriately. You should also go through our"
guide: "coaching guide"
unable_attend: "In case you are unable to attend after you RSVP, please make sure you come back and update your attendance as we rely on coaches showing up in order for our workshops to run smoothly."
- food: "PS: There will also be food at the workshop. We always make an effort to have vegetarian, vegan and gluten-free options available. If you have any other dietary requirements, send us email at"
code_of_conduct:
title: "Code of conduct"
summary:
diff --git a/db/migrate/20250820134727_create_dietary_restriction_enum.rb b/db/migrate/20250820134727_create_dietary_restriction_enum.rb
new file mode 100644
index 000000000..bb5848854
--- /dev/null
+++ b/db/migrate/20250820134727_create_dietary_restriction_enum.rb
@@ -0,0 +1,5 @@
+class CreateDietaryRestrictionEnum < ActiveRecord::Migration[7.1]
+ def change
+ create_enum :dietary_restriction_enum, %w[vegan vegetarian pescetarian halal gluten_free dairy_free other]
+ end
+end
diff --git a/db/migrate/20250820145012_add_dietary_restrictions_to_members.rb b/db/migrate/20250820145012_add_dietary_restrictions_to_members.rb
new file mode 100644
index 000000000..46b028da5
--- /dev/null
+++ b/db/migrate/20250820145012_add_dietary_restrictions_to_members.rb
@@ -0,0 +1,6 @@
+class AddDietaryRestrictionsToMembers < ActiveRecord::Migration[7.0]
+ def change
+ add_column :members, :dietary_restrictions, :enum, enum_type: "dietary_restriction_enum", array: true, default: []
+ add_column :members, :other_dietary_restrictions, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 192b8fd69..5c5dbc99c 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,10 +10,14 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2025_08_06_181500) do
+ActiveRecord::Schema[7.1].define(version: 2025_08_20_145012) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
+ # Custom types defined in this database.
+ # Note that some types may not work with other database engines. Be careful if changing database.
+ create_enum "dietary_restriction_enum", ["vegan", "vegetarian", "pescetarian", "halal", "gluten_free", "dairy_free", "other"]
+
create_table "activities", force: :cascade do |t|
t.string "trackable_type"
t.bigint "trackable_id"
@@ -391,6 +395,8 @@
t.string "pronouns"
t.datetime "accepted_toc_at", precision: nil
t.datetime "opt_in_newsletter_at", precision: nil
+ t.enum "dietary_restrictions", default: [], array: true, enum_type: "dietary_restriction_enum"
+ t.string "other_dietary_restrictions"
t.index ["email"], name: "index_members_on_email", unique: true
end
diff --git a/spec/features/admin/members_spec.rb b/spec/features/admin/members_spec.rb
index 34608d316..983b316e1 100644
--- a/spec/features/admin/members_spec.rb
+++ b/spec/features/admin/members_spec.rb
@@ -1,5 +1,6 @@
RSpec.describe 'Admin managing members', type: :feature do
- let(:member) { Fabricate(:student) }
+ let(:member) { Fabricate(:student, dietary_restrictions: %w[vegan other],
+ other_dietary_restrictions: 'peanut allergy') }
let(:admin) { Fabricate(:chapter_organiser) }
let(:invitation) { Fabricate(:attended_workshop_invitation, member: member) }
@@ -15,6 +16,9 @@
expect(page).to have_content(member.name)
expect(page).to have_content(member.email)
expect(page).to have_content(member.about_you)
+
+ expect(page).to have_selector('.badge', text: 'Vegan')
+ expect(page).to have_selector('.badge', text: 'Peanut allergy')
end
it 'can view a summary of member event attendances' do
diff --git a/spec/features/admin/workshops_spec.rb b/spec/features/admin/workshops_spec.rb
index fcf2da21a..2d6d5d51c 100644
--- a/spec/features/admin/workshops_spec.rb
+++ b/spec/features/admin/workshops_spec.rb
@@ -164,6 +164,21 @@
end
end
+ context 'dietary restrictions' do
+ scenario 'displays dietary restriction badges for attendees' do
+ workshop = Fabricate(:workshop)
+ attendee = Fabricate(:attending_workshop_invitation, workshop: workshop)
+ attendee.member.update(dietary_restrictions: %w[vegan gluten_free])
+
+ visit admin_workshop_path(workshop)
+
+ member_link = find('a', exact_text: attendee.member.full_name)
+ sibling = member_link.find(:xpath, 'following-sibling::p')
+ expect(sibling).to have_text('Vegan')
+ expect(sibling).to have_text('Gluten free')
+ end
+ end
+
context '#actions' do
context 'sending invitations to attendees' do
scenario 'for a workshop' do
diff --git a/spec/features/member_joining_spec.rb b/spec/features/member_joining_spec.rb
index 8cfcf27b7..e8050b037 100644
--- a/spec/features/member_joining_spec.rb
+++ b/spec/features/member_joining_spec.rb
@@ -40,6 +40,9 @@
fill_in 'member_surname', with: 'Doe'
fill_in 'member_email', with: 'jane@codebar.io'
fill_in 'member_about_you', with: Faker::Lorem.paragraph
+ check 'Vegan'
+ check 'Other'
+ fill_in 'Other dietary restrictions', with: 'peanut allergy'
click_on 'Next'
expect(page).to have_current_path(step2_member_path)
@@ -50,6 +53,8 @@
expect(page).to have_content('she')
expect(page).to have_content('Jane Doe')
expect(page).to have_link('jane@codebar.io')
+ expect(page).to have_selector('.badge', text: 'Vegan')
+ expect(page).to have_selector('.badge', text: 'Peanut allergy')
end
scenario 'Picking a mailing list on step 2 subscribes you to that list' do
diff --git a/spec/features/member_updating_details_spec.rb b/spec/features/member_updating_details_spec.rb
new file mode 100644
index 000000000..b6630d364
--- /dev/null
+++ b/spec/features/member_updating_details_spec.rb
@@ -0,0 +1,46 @@
+RSpec.feature 'Update your details', type: :feature do
+ before do
+ mock_github_auth
+ end
+
+ scenario 'A member adds a dietary restriction' do
+ member = Fabricate(:member)
+ login member
+
+ visit edit_member_path
+ check 'Vegetarian'
+ click_on 'Save'
+
+ expect(page).to have_content('Your details have been updated.')
+ expect(page).to have_selector(".badge", text: "Vegetarian")
+ end
+
+ scenario 'A member adds a custom dietary restriction' do
+ member = Fabricate(:member)
+ login member
+
+ visit edit_member_path
+ check 'Other'
+ fill_in 'Other dietary restrictions', with: 'peanut allergy'
+ click_on 'Save'
+
+ expect(page).to have_content('Your details have been updated.')
+ expect(page).to have_selector(".badge", text: 'Peanut allergy')
+ member.reload
+ expect(member.dietary_restrictions).to eq(['other'])
+ expect(member.other_dietary_restrictions).to eq('peanut allergy')
+ end
+
+ scenario 'A member removes a dietary restriction' do
+ member = Fabricate(:member, dietary_restrictions: ['vegetarian'])
+ login member
+
+ visit edit_member_path
+ uncheck 'Vegetarian'
+ click_on 'Save'
+
+ expect(page).to have_content('Your details have been updated.')
+ member.reload
+ expect(member.dietary_restrictions).to be_empty
+ end
+end