Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: personalised ical feed #1101

Merged
merged 25 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fb1d780
feat: calendar endpoint
SilasPeters May 21, 2024
449c654
feat: added button to copy personalised webcal link
SilasPeters May 21, 2024
1a5616b
feat: if participant is enrolled as reservist, activity is marked as …
SilasPeters May 22, 2024
77eb45d
chore: expanded database, processed TODOs and handled exceptiosn
SilasPeters May 25, 2024
7146a5d
chore: conform to linter
SilasPeters May 25, 2024
56cbaff
fix: expand test fixture
SilasPeters May 25, 2024
60b6fa0
fix: whole-day activities are now properly formatted
SilasPeters May 31, 2024
079f670
fix: properly respond to HEAD request
SilasPeters Jun 4, 2024
241fc3d
refactor: move activity_to_event to activity model
SilasPeters Jun 4, 2024
756bb2d
chore: localised everything
SilasPeters Jun 4, 2024
eda2d3a
chore: conform to linters
SilasPeters Jun 4, 2024
a18cda1
fix: normalised locales
SilasPeters Jun 4, 2024
729f0c3
feat: adds clarification on icalendar feed
SilasPeters Aug 2, 2024
6d2cebd
Merge branch 'master' into feat/personalised-ical-feed
SilasPeters Aug 11, 2024
8c42095
feat: added disclaimer in activity template
SilasPeters Sep 5, 2024
05d540e
fixup! feat: added disclaimer in activity template
SilasPeters Sep 5, 2024
b5c6d20
fixup! fixup! feat: added disclaimer in activity template
SilasPeters Sep 7, 2024
7ef1da6
Merge branch 'master' into feat/personalised-ical-feed
SilasPeters Sep 7, 2024
7400bc8
fixup! Merge branch 'master' into feat/personalised-ical-feed
SilasPeters Sep 7, 2024
b67445f
Merge branch 'master' into feat/personalised-ical-feed
SilasPeters Sep 17, 2024
55a2e1e
fixup! Merge branch 'master' into feat/personalised-ical-feed
SilasPeters Sep 17, 2024
12d9cb5
Merge branch 'master' into feat/personalised-ical-feed
SilasPeters Sep 24, 2024
09db7bd
Merge branch 'master' into feat/personalised-ical-feed
SilasPeters Oct 15, 2024
4736f5d
fix: locale translation
SilasPeters Oct 25, 2024
660cab8
Merge branch 'master' into feat/personalised-ical-feed
SilasPeters Oct 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -348,6 +351,7 @@ DEPENDENCIES
i15r (~> 0.5.5)
i18n-js
i18n-tasks (~> 0.9.31)
icalendar
image_processing
impressionist!
listen
Expand Down
1 change: 0 additions & 1 deletion app/controllers/admin/members_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down
56 changes: 56 additions & 0 deletions app/controllers/api/calendars_controller.rb
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion app/controllers/members/participants_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
39 changes: 35 additions & 4 deletions app/javascript/src/members/activities/activities.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down Expand Up @@ -244,6 +274,7 @@ $(document).on("ready page:load turbolinks:load", function () {
initialize_enrollment();
initialize_modal();
copyICSToClipboard();
copyPersonalICSToClipboard();
});

document.addEventListener("turbolinks:load", function () {
Expand Down
67 changes: 60 additions & 7 deletions app/models/activity.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'icalendar' # https://github.com/icalendar/icalendar

# Represents an activity in the database.
#:nodoc:
class Activity < ApplicationRecord
Expand Down Expand Up @@ -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.utc.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
Expand Down Expand Up @@ -360,19 +390,21 @@ def activity_url
return "https://koala.svsticky.nl/activities/#{ id }"
end

# pass along locale default to nil
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(loc = nil)
return nil if start.nil? || self.end.nil?

fmt_dt = ->(dt) { dt.utc.strftime('%Y%m%dT%H%M%SZ') }

loc = I18n.locale if loc.nil?
description = "#{ activity_url }\n\n#{ loc == :nl ? description_nl : description_en }"
disclaimer = "[#{ I18n.t('activerecord.attributes.activity.disclaimer') }]"
description = "#{ activity_url }\n\n#{ description_localised(loc) }\n\n#{ disclaimer }"
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.
Expand All @@ -394,7 +426,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

Expand All @@ -409,4 +441,25 @@ 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 = description_localised(locale)
event.location = location
return event
end
end
6 changes: 6 additions & 0 deletions app/models/member.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.)
Expand Down
2 changes: 2 additions & 0 deletions app/views/members/activities/index.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
12 changes: 11 additions & 1 deletion config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ en:
description: Description
description_en: English description
description_nl: Dutch description
disclaimer: Data on this activity may be outdated, as it was addes as a one-time copy of the information given at that time. Up-to-date info can be found on Koala.
SilasPeters marked this conversation as resolved.
Show resolved Hide resolved
end_date: Enddate
end_time: Endtime
google_event: Add to calendar
google_event: Copy once to calendar
stickyPiston marked this conversation as resolved.
Show resolved Hide resolved
is_alcoholic: Alcoholic(18+)
is_enrollable: Enrollable
is_freshmans: First year students
Expand Down Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions config/locales/members.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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, at all times. However, koala is only aware of activities for which you are enrolled through koala, and not trough some other platform (like Pretix or a Google form). Remember to add those activities to your calendar manually.
title: Warning
SilasPeters marked this conversation as resolved.
Show resolved Hide resolved
error:
edit: Could not edit!
enroll: Could not enroll!
Expand All @@ -25,15 +30,16 @@ 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
notes_mandatory: Extra info required!
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
Expand Down
8 changes: 7 additions & 1 deletion config/locales/members.nl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 te allen 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!
Expand All @@ -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
Expand Down
Loading
Loading