diff --git a/Gemfile b/Gemfile index 34c26d262..d6bc0a997 100644 --- a/Gemfile +++ b/Gemfile @@ -49,6 +49,9 @@ gem 'pg' # Full text search gem 'pg_search' +# iCalendar feeds +gem 'icalendar' + group :production, :staging do gem 'sentry-raven' gem 'uglifier' diff --git a/Gemfile.lock b/Gemfile.lock index 679b14960..a697b3980 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -151,6 +151,9 @@ GEM rails-i18n rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) + icalendar (2.10.1) + ice_cube (~> 0.16) + ice_cube (0.16.4) image_processing (1.12.1) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) @@ -348,6 +351,7 @@ DEPENDENCIES i15r (~> 0.5.5) i18n-js i18n-tasks (~> 0.9.31) + icalendar image_processing impressionist! listen diff --git a/app/controllers/admin/members_controller.rb b/app/controllers/admin/members_controller.rb index 82f3f7b06..2147aed58 100644 --- a/app/controllers/admin/members_controller.rb +++ b/app/controllers/admin/members_controller.rb @@ -75,7 +75,6 @@ def create impressionist(@member, 'nieuwe lid') redirect_to(@member) else - # If the member hasn't filled in a study, again show an empty field @member.educations.build(id: '-1') if @member.educations.empty? diff --git a/app/controllers/api/calendars_controller.rb b/app/controllers/api/calendars_controller.rb new file mode 100644 index 000000000..d2bccc629 --- /dev/null +++ b/app/controllers/api/calendars_controller.rb @@ -0,0 +1,56 @@ +require 'icalendar_helper' + +# Controller for all calendar related endpoints +class Api::CalendarsController < ActionController::Base + # supply the personalised iCal feed from the user + def show + @member = Member.find_by(calendar_id: params[:calendar_id]) + # member variable will be accessible in other methods as well now + + unless @member # No member with the specified hash was found + render(json: { error: I18n.t('calendars.errors.unkown_hash') }, status: :not_found) + return + end + + # If the HTTP request is a HEAD type, return the headers only + if request.head? + head(:ok) + return + end + + respond_to do |format| + format.ics do + send_data(create_personal_calendar, + type: 'text/calendar', + disposition: 'attachment', + filename: "#{ @member.first_name }_#{ I18n.t('calendars.jargon.activities') }.ics") + end + end + end + + def index + if current_user.nil? + render(json: { error: I18n.t('calendars.errors.not_logged_in') }, status: :forbidden) + return + end + + @member = Member.find(current_user.credentials_id) + render(plain: url_for(action: 'show', calendar_id: @member.calendar_id), format: :ics) + end + + # Not exposed to API directly, but through #show + def create_personal_calendar + # Convert activities to events, and mark activities where the member is + # is enrolled as reservist + @reservist_activity_ids = @member.reservist_activities.ids + events = @member.activities.map do |a| + if @reservist_activity_ids.include?(a.id) + a.name = "[#{ I18n.t('calendars.jargon.reservist').upcase }] #{ a.name }" + end + a.to_calendar_event(I18n.locale) + end + + # Return the calendar + IcalendarHelper.create_calendar(events, @locale).to_ical + end +end diff --git a/app/controllers/members/participants_controller.rb b/app/controllers/members/participants_controller.rb index 4d6906df7..b31dfb4be 100644 --- a/app/controllers/members/participants_controller.rb +++ b/app/controllers/members/participants_controller.rb @@ -121,7 +121,6 @@ def create participant_limit: @activity.participant_limit, participant_count: @activity.participants.count }) - return else @new_enrollment = Participant.new( member_id: @member.id, diff --git a/app/javascript/src/members/activities/activities.js b/app/javascript/src/members/activities/activities.js index 304aa4d9f..fbfe210ba 100644 --- a/app/javascript/src/members/activities/activities.js +++ b/app/javascript/src/members/activities/activities.js @@ -10,16 +10,46 @@ import { Activity } from "./activity.js"; var token, modal; function copyICSToClipboard() { - /* Link to copy */ - var copy_text = - "https://calendar.google.com/calendar/ical/stickyutrecht.nl_thvhicj5ijouaacp1elsv1hceo%40group.calendar.google.com/public/basic.ics"; new Clipboard("#copy-btn", { text: function () { - return copy_text; + return "https://calendar.google.com/calendar/ical/stickyutrecht.nl_thvhicj5ijouaacp1elsv1hceo%40group.calendar.google.com/public/basic.ics"; }, }); } +function copyPersonalICSToClipboard() { + fetch("/api/calendar/fetch") + .then((response) => response.text()) + .then((icsFeed) => { + new Clipboard("#copy-btn-personal", { + text: function () { + return icsFeed; + }, + }); + }) + .catch((error) => { + console.log(error); + }); +} // TODO makes an API call even if the button is not pressed + +document.getElementById("copy-btn-personal").addEventListener("click", (_) => { + Swal.fire({ + title: I18n.t( + "members.activities.calendar.confirm_understand_icalendar.title", + ), + text: I18n.t( + "members.activities.calendar.confirm_understand_icalendar.text", + ), + icon: "warning", + showCancelButton: false, + confirmButtonText: I18n.t( + "members.activities.calendar.confirm_understand_icalendar.confirm", + ), + }).then((_) => { + /* Do nothing, warning has been displayed and that's enough */ + }); +}); + export function get_activity_container() { return $("#activity-container"); } @@ -244,6 +274,7 @@ $(document).on("ready page:load turbolinks:load", function () { initialize_enrollment(); initialize_modal(); copyICSToClipboard(); + copyPersonalICSToClipboard(); }); document.addEventListener("turbolinks:load", function () { diff --git a/app/models/activity.rb b/app/models/activity.rb index 0f430faec..665551a1d 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -1,3 +1,5 @@ +require 'icalendar' # https://github.com/icalendar/icalendar + # Represents an activity in the database. #:nodoc: class Activity < ApplicationRecord @@ -224,6 +226,34 @@ def end Activity.combine_dt(end_date, end_time) end + def whole_day? + return !start_time && !end_time + end + + # Format a datetime in UTC for the iCalendar format + def format_utc(datetime) + datetime.utc.strftime('%Y%m%dT%H%M%SZ') + end + + # Format a datetime to a whole day for the iCalendar format + def format_whole_day(datetime) + datetime.strftime('%Y%m%d') + # For whole days, do not convert to UTC, because if 'start' is a date, it's + # time will be 00:00:00 and will be converted to the previous day + end + + # Properly format the start datetime, depending on if the event is a whole day event or not + def calendar_start + normalised_start = start_time ? start : start.change(hour: 0, min: 0) # Won't have effect if whole_day + return whole_day? ? format_whole_day(normalised_start) : format_utc(normalised_start) + end + + # Properly format the end datetime, depending on if the event is a whole day event or not + def calendar_end + normalised_end = end_time ? self.end : self.end.change(hour: 23, min: 59) # Won't have effect if whole_day + return whole_day? ? format_whole_day(normalised_end + 1.day) : format_utc(normalised_end) # +1 day, end is exclusive + end + def when_open Activity.combine_dt(open_date, open_time) end @@ -360,19 +390,25 @@ def activity_url return "https://koala.svsticky.nl/activities/#{ id }" end - # pass along locale default to nil - def google_event(loc = nil) + # The description of the activity in the specified locale + def description_localised(locale) + return locale == :nl ? description_nl : description_en + end + + # This generates an URL representing a calendar activity template, filled with data from the koala activity + def google_event(locale = nil) return nil if start.nil? || self.end.nil? - fmt_dt = ->(dt) { dt.utc.strftime('%Y%m%dT%H%M%SZ') } + locale = I18n.locale if locale.nil? + + disclaimer = "[#{ I18n.t('activerecord.attributes.activity.disclaimer', deep_interpolation: true, + datetime: DateTime.current.utc.to_s) }]" + description = "#{ activity_url }\n\n#{ description_localised(locale) }\n\n#{ disclaimer }" - loc = I18n.locale if loc.nil? - description = "#{ activity_url }\n\n#{ loc == :nl ? description_nl : description_en }" uri_name = URI.encode_www_form_component(name) uri_description = URI.encode_www_form_component(description) uri_location = URI.encode_www_form_component(location) - calendar_end = end_time.nil? ? self.end.change(hour: 23, min: 59) : self.end - return "https://www.google.com/calendar/render?action=TEMPLATE&text=#{ uri_name }&dates=#{ fmt_dt.call(start) }%2F#{ fmt_dt.call(calendar_end) }&details=#{ uri_description }&location=#{ uri_location }&sf=true&output=xml" + return "https://www.google.com/calendar/render?action=TEMPLATE&text=#{ uri_name }&dates=#{ calendar_start }%2F#{ calendar_end }&details=#{ uri_description }&location=#{ uri_location }&sf=true&output=xml" end # Add a message containing the Activity's id and name to the logs before deleting the activity. @@ -394,7 +430,7 @@ def whatsapp_message(loc) location: location, price: pc, url: activity_url, - description: loc == :nl ? description_nl : description_en, + description: description_localised(loc), locale: loc) end @@ -409,4 +445,27 @@ def gen_time_string(loc) return fmt_dt.call(start_date) + fmt_tm.call(start_time) + edt end + + # Converts a Sticky activity to an iCalendar event + def to_calendar_event(locale) + event = Icalendar::Event.new + event.uid = id.to_s + + if whole_day? # Adhire to the iCalendar spec + event.dtstart = Icalendar::Values::Date.new(calendar_start) + event.dtstart.ical_param("VALUE", "DATE") + event.dtend = Icalendar::Values::Date.new(calendar_end) + event.dtend.ical_param("VALUE", "DATE") + else + event.dtstart = calendar_start + event.dtend = calendar_end + end + + event.summary = name + event.description = "#{ activity_url }\r\n\r\n"\ + "#{ description_localised(locale) }\r\n\r\n"\ + "Last synced on: #{ DateTime.current.utc }" + event.location = location + return event + end end diff --git a/app/models/member.rb b/app/models/member.rb index 9b5199843..ed04bc1ae 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -16,6 +16,7 @@ class Member < ApplicationRecord validates :emergency_phone_number, presence: true, if: :underage? validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: /\A.+@(?!(.+\.)*uu\.nl\z).+\..+\z/i } + validates :calendar_id, presence: true, uniqueness: true # An attr_accessor is basically a variable attached to the model but not stored in the database attr_accessor :require_student_id @@ -203,6 +204,11 @@ def groups return groups.values end + # Whilst we cannot assign an id on creation, we can assign an id before validation, which is almost the same + before_validation on: [:save, :create] do + self.calendar_id = SecureRandom.uuid if calendar_id.blank? + end + # Rails also has hooks you can hook on to the process of saving, updating or deleting. Here the join_date is automatically filled in on creating a new member # We also check for a duplicate study, and discard the duplicate if found. # (Not doing this would lead to a database constraint violation.) diff --git a/app/views/members/activities/index.html.haml b/app/views/members/activities/index.html.haml index 71f5fa507..e6fa35462 100644 --- a/app/views/members/activities/index.html.haml +++ b/app/views/members/activities/index.html.haml @@ -8,6 +8,8 @@ = I18n.t 'members.activities.index.activities_calendar' %button.btn.btn-secondary#copy-btn{:type => 'button'} = I18n.t 'members.activities.index.copy_ICS' + %button.btn.btn-secondary#copy-btn-personal{:type => 'button'} + = I18n.t 'members.activities.index.copy_ICS_personal' - else .alert.alert-warning= I18n.t('members.activities.index.no_activities') diff --git a/config/locales/en.yml b/config/locales/en.yml index 9f9c81ee7..de7a005b0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -33,9 +33,10 @@ en: description: Description description_en: English description description_nl: Dutch description + disclaimer: Data about this activity has been added as a one-time copy from what was known on %{datetime}, and as such could be outdated. end_date: Enddate end_time: Endtime - google_event: Add to calendar + google_event: Copy once to calendar is_alcoholic: Alcoholic(18+) is_enrollable: Enrollable is_freshmans: First year students @@ -415,6 +416,15 @@ en: study: Study/Studies transactions: Transactions association_name: Study association Sticky + calendars: + errors: + not_logged_in: Not logged in + unkown_hash: Unkown calendar hash + jargon: + activities: activities + reservist: reservist + personalised_activities_calendar: + name: Sticky Activities date: day_names: - Sunday diff --git a/config/locales/members.en.yml b/config/locales/members.en.yml index c1843d63b..39dea5c5a 100644 --- a/config/locales/members.en.yml +++ b/config/locales/members.en.yml @@ -16,6 +16,11 @@ en: unenroll: Unenroll update_info: Update info back_to_overview: Back to activity overview + calendar: + confirm_understand_icalendar: + confirm: I understand + text: Koala will maintain the icalendar feed you just copied to your clipboard, at all times. However, koala is only aware of activities for which you are enrolled through koala, and not through some other platform (like Pretix or a Google form). Remember to add those activities to your calendar manually. + title: Warning error: edit: Could not edit! enroll: Could not enroll! @@ -25,7 +30,8 @@ en: full: FULL! index: activities_calendar: Activities calendar - copy_ICS: Copy Webcal link + copy_ICS: Copy Webcal link for all activitites + copy_ICS_personal: Copy Personalised Webcal link no_activities: There are no activities for which you can enroll at the moment info: more_info: More info @@ -33,7 +39,7 @@ en: home: edit: board: the board - board_only_change_info: Some data can't be edit by yourself (for example your name, date of birth and student number). If this needs to be updated please contact + board_only_change_info: Some data can't be edited by yourself (for example your name, date of birth and student number). If this needs to be updated please contact download: activities: Activities address: Address diff --git a/config/locales/members.nl.yml b/config/locales/members.nl.yml index 4c7f40061..6759d4eda 100644 --- a/config/locales/members.nl.yml +++ b/config/locales/members.nl.yml @@ -16,6 +16,11 @@ nl: unenroll: Uitschrijven update_info: Update info back_to_overview: Terug naar overzicht + calendar: + confirm_understand_icalendar: + confirm: Ik begrijp het + text: Koala zal de icalendar feed die je zojuist hebt gekopieerd naar je klembord ten aller tijde bijwerken. Echter, is koala alleen op de hoogte van activiteiten waarvoor je je via Koala hebt ingeschreven, en niet via een ander platform (zoals Pretix of een Google Form). Vergeet niet om die activiteiten handmatig aan je agenda toe te voegen. + title: Waarschuwing error: edit: Kon niet bijwerken! enroll: Kon niet inschrijven! @@ -25,7 +30,8 @@ nl: full: VOL! index: activities_calendar: Activiteitenkalender - copy_ICS: Kopieer Webcal link + copy_ICS: Kopieer Webcal link voor alle activiteiten + copy_ICS_personal: Kopieer gepersonaliseerde Webcal link no_activities: Er zijn op het moment geen activiteiten waar je je voor kunt inschrijven info: more_info: Meer info diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 751bd5ebe..84935bd1a 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -33,9 +33,10 @@ nl: description: Beschrijving description_en: Engelse beschrijving description_nl: Nederlandse beschrijving + disclaimer: Gegevens over deze activiteit zijn toegevoegd als een eenmalig kopie van wat bekend was op %{datetime}, en kunnen dus verouderd zijn. end_date: Einddatum end_time: Eindtijd - google_event: Voeg toe aan je agenda + google_event: Kopiƫer eenmalig naar agenda is_alcoholic: Alcohol (18+) is_enrollable: Inschrijfbaar is_freshmans: Eerstejaars @@ -415,6 +416,15 @@ nl: study: Studie(s) transactions: Transacties association_name: Studievereniging Sticky + calendars: + errors: + not_logged_in: Niet ingelogd + unkown_hash: Onbekende kalender hash + jargon: + activities: activiteiten + reservist: reservist + personalised_activities_calendar: + name: Sticky Activiteiten date: day_names: - zondag diff --git a/config/routes.rb b/config/routes.rb index 74df2f5e8..dc18a8330 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -126,6 +126,8 @@ end scope 'api' do + get 'calendar/pull/:calendar_id', to: 'api/calendars#show', defaults: { format: 'ics' } + get 'calendar/fetch', to: 'api/calendars#index' use_doorkeeper do # skip_controllers :token_info, :applications, :authorized_applications end diff --git a/db/migrate/20240521180025_add_calendar_id_to_member.rb b/db/migrate/20240521180025_add_calendar_id_to_member.rb new file mode 100644 index 000000000..84b51a4fc --- /dev/null +++ b/db/migrate/20240521180025_add_calendar_id_to_member.rb @@ -0,0 +1,18 @@ +class AddCalendarIdToMember < ActiveRecord::Migration[6.1] + def up + add_column :members, :calendar_id, :uuid + + # Populate existing records with a random UUID + Member.reset_column_information + Member.find_each do |record| + record.update_columns(calendar_id: SecureRandom.uuid) + end + + # Add an index to the UUID column, because why not + add_index :members, :calendar_id, unique: true + end + + def down + remove_column :members, :calendar_id + end +end diff --git a/db/seeds/members.rb b/db/seeds/members.rb index d2d3d1dc5..89fdb0529 100644 --- a/db/seeds/members.rb +++ b/db/seeds/members.rb @@ -18,7 +18,8 @@ student_id: "F#{ Faker::Number.number(digits: 6) }", birth_date: Faker::Date.between(from: 28.years.ago, to: 16.years.ago), join_date: Faker::Date.between(from: 6.years.ago, to: Date.today), - comments: (Faker::Boolean.boolean(true_ratio: 0.3) ? Faker::Hacker.say_something_smart : nil) + comments: (Faker::Boolean.boolean(true_ratio: 0.3) ? Faker::Hacker.say_something_smart : nil), + calendar_id: Faker::Internet.uuid ) puts " [#{ member.valid? ? ' Ok ' : 'Fail' }] #{ member.name } (#{ member.student_id })" diff --git a/db/structure.sql b/db/structure.sql index 5a7c7b597..2a065d398 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -550,7 +550,8 @@ CREATE TABLE public.members ( consent integer DEFAULT 0, consent_at date, created_at timestamp without time zone, - updated_at timestamp without time zone + updated_at timestamp without time zone, + calendar_id uuid ); @@ -1449,6 +1450,13 @@ CREATE UNIQUE INDEX index_group_members_on_member_id_and_group_id_and_year ON pu CREATE INDEX index_impressions_on_user_id ON public.impressions USING btree (user_id); +-- +-- Name: index_members_on_calendar_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_members_on_calendar_id ON public.members USING btree (calendar_id); + + -- -- Name: index_members_on_email; Type: INDEX; Schema: public; Owner: - -- @@ -1737,6 +1745,6 @@ INSERT INTO "schema_migrations" (version) VALUES ('20220406092056'), ('20220524203723'), ('20240125003700'), -('20240809152416'); - +('20240809152416'), +('20240521180025'); diff --git a/gemset.nix b/gemset.nix index 9de9c6cf0..28aca3c01 100644 --- a/gemset.nix +++ b/gemset.nix @@ -543,6 +543,27 @@ }; version = "0.9.37"; }; + icalendar = { + dependencies = ["ice_cube"]; + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "03ki7wm2iqr3dv7mgrxv2b8vbh42c7yv55dc33a077n8jnxhhc8z"; + type = "gem"; + }; + version = "2.10.1"; + }; + ice_cube = { + groups = ["default"]; + platforms = []; + source = { + remotes = ["https://rubygems.org"]; + sha256 = "1dri4mcya1fwzrr9nzic8hj1jr28a2szjag63f9k7p2bw9fpw4fs"; + type = "gem"; + }; + version = "0.16.4"; + }; image_processing = { dependencies = ["mini_magick" "ruby-vips"]; groups = ["default"]; diff --git a/lib/icalendar_helper.rb b/lib/icalendar_helper.rb new file mode 100644 index 000000000..626851204 --- /dev/null +++ b/lib/icalendar_helper.rb @@ -0,0 +1,22 @@ +require 'icalendar' # https://github.com/icalendar/icalendar + +# Includes all abstractions over iCalender representations +module IcalendarHelper + # Combines zero or more Icalendar events into an iCalendar abstract object + def self.create_calendar(events, locale) + calendar = Icalendar::Calendar.new + calendar.x_wr_calname = I18n.t("calendars.personalised_activities_calendar.name", locale: locale) + events.each { |e| calendar.add_event(e) } + calendar.publish + return calendar + # Returns the abstract icalendar object, not the ICS string ready to + # be stored in an ICS file. To convert this calendar into an ICS string, + # use `calendar.to_ical` or the `create_file` method below. + end + + # Stores the calendar to an *.ics file + def self.create_file(calendar, path) + calendar_string = calendar.to_ical + File.write(path, calendar_string) + end +end diff --git a/test/fixtures/members.yml b/test/fixtures/members.yml index 90749bb4c..4f1aac8ca 100644 --- a/test/fixtures/members.yml +++ b/test/fixtures/members.yml @@ -11,6 +11,7 @@ martyparty: email: martyparty@maartenberg.nl birth_date: <%= Date.today %> join_date: <%= Date.today %> + calendar_id: 75a3ebf0-9e07-435f-b9cf-28e720103011 dannypanny: first_name: Danny @@ -25,6 +26,7 @@ dannypanny: email: danoontjepower@maartenberg.nl birth_date: <%= Date.today %> join_date: <%= Date.today %> + calendar_id: 75a3ebf0-9e07-435f-b9cf-28e720103012 captain_underpants: first_name: Kapitein @@ -38,6 +40,7 @@ captain_underpants: email: kapiteinonderbroek@maartenberg.nl birth_date: <%= 19.years.ago %> join_date: <%= 2.weeks.ago %> + calendar_id: 75a3ebf0-9e07-435f-b9cf-28e720103009 yorici: first_name: Yorici @@ -51,6 +54,7 @@ yorici: email: moneymoneymoney@maartenberg.nl birth_date: <%= 20.years.ago %> join_date: <%= 2.years.ago %> + calendar_id: 75a3ebf0-9e07-435f-b9cf-28e720103013 # Use this user to test for 18+ activities correctly not allowing 18- members to join an activity m8eld: @@ -65,6 +69,7 @@ m8eld: email: mevrouwmetdehamer@maartenberg.nl birth_date: <%= 17.years.ago %> join_date: <%= 1.days.ago %> + calendar_id: 75a3ebf0-9e07-435f-b9cf-28e720103014 glenniepennie: first_name: Stewart @@ -78,6 +83,7 @@ glenniepennie: email: stewartlittle@maartenberg.nl birth_date: <%= 20.years.ago %> join_date: <%= 2.years.ago %> + calendar_id: 75a3ebf0-9e07-435f-b9cf-28e720103015 #This user has a German international phone number, just in case we want to test this roosje: @@ -92,6 +98,7 @@ roosje: email: slapendeschone@maartenberg.nl birth_date: <%= 19.years.ago %> join_date: <%= 7.weeks.ago %> + calendar_id: 75a3ebf0-9e07-435f-b9cf-28e720103016 meneerdeveurzitter: first_name: Emilius @@ -105,6 +112,7 @@ meneerdeveurzitter: email: veurzitter@maartenberg.nl birth_date: <%= 29.years.ago %> join_date: <%= 1.weeks.ago %> + calendar_id: 75a3ebf0-9e07-435f-b9cf-28e720103017 needfuldoer: first_name: Raji @@ -118,6 +126,7 @@ needfuldoer: email: indianguywhodoesneedful@maartenberg.nl birth_date: <%= 40.years.ago %> join_date: <%= 9.weeks.ago %> + calendar_id: 75a3ebf0-9e07-435f-b9cf-28e720103018 thecreator: first_name: Martijn @@ -131,6 +140,7 @@ thecreator: email: inthebeginninghecreatedkoala@maartenberg.nl birth_date: <%= 20.years.ago %> join_date: <%= 19.years.ago %> + calendar_id: 75a3ebf0-9e07-435f-b9cf-28e720103019 masterboy030man: first_name: Masterboy @@ -144,6 +154,7 @@ masterboy030man: email: masterboy030man@maartenberg.nl birth_date: <%= 25.years.ago %> join_date: <%= 19.years.ago %> + calendar_id: 75a3ebf0-9e07-435f-b9cf-28e720103020 feutemeteut: first_name: Feu @@ -158,3 +169,4 @@ feutemeteut: email: eenofanderverschrikkelijkadres@maartenberg.nl birth_date: <%= 18.years.ago %> join_date: <%= 17.years.ago %> + calendar_id: 75a3ebf0-9e07-435f-b9cf-28e720103021 diff --git a/test/models/member_test.rb b/test/models/member_test.rb index fbb87cff7..9017c74e9 100644 --- a/test/models/member_test.rb +++ b/test/models/member_test.rb @@ -12,7 +12,7 @@ class MemberTest < ActiveSupport::TestCase 'first_name' => '', 'last_name' => '', 'address' => '', 'house_number' => '', 'postal_code' => '', 'city' => '', 'phone_number' => '', 'emergency_phone_number' => '', 'email' => '', - 'birth_date' => nil, 'join_date' => nil + 'birth_date' => nil, 'join_date' => nil, 'calendar_id' => '' } # Verify that a Member model with the minimal set of attributes defined above