From 31413efdeb8ad58db06cbe1e225677808e8b3154 Mon Sep 17 00:00:00 2001 From: Alexandria Date: Wed, 18 May 2022 18:40:58 +0100 Subject: [PATCH] feat: add online event detection (#1239) feat: add online_address_id and online_event? to base importer event feat: add online address detection for eventbrite, ics, meetup, mcu fix: only validate if an event is unique on creation fix: make activerecord work in rake workers task --- .../calendar_importer_task.rb | 2 + app/jobs/calendar_importer/event_resolver.rb | 8 +++- app/jobs/calendar_importer/events/base.rb | 13 +++++- .../events/eventbrite_event.rb | 12 ++++- .../events/facebook_event.rb | 3 +- .../calendar_importer/events/ics_event.rb | 45 +++++++++++++++++++ .../events/manchester_uni_event.rb | 8 ++-- .../calendar_importer/events/meetup_event.rb | 9 +++- .../events/ticketsolve_event.rb | 1 - .../calendar_importer/parsers/eventbrite.rb | 2 +- app/models/calendar.rb | 1 - app/models/event.rb | 12 ++--- lib/tasks/workers.rake | 4 +- 13 files changed, 97 insertions(+), 23 deletions(-) diff --git a/app/jobs/calendar_importer/calendar_importer_task.rb b/app/jobs/calendar_importer/calendar_importer_task.rb index 6224aec41..fe7c282b1 100644 --- a/app/jobs/calendar_importer/calendar_importer_task.rb +++ b/app/jobs/calendar_importer/calendar_importer_task.rb @@ -63,6 +63,8 @@ def run next if parsed_event.is_private? next if parsed_event.has_no_occurences? + parsed_event.determine_online_location + parsed_event.determine_location_for_strategy # next if parsed_event.is_address_missing? diff --git a/app/jobs/calendar_importer/event_resolver.rb b/app/jobs/calendar_importer/event_resolver.rb index 271db6dd8..97d9edd44 100644 --- a/app/jobs/calendar_importer/event_resolver.rb +++ b/app/jobs/calendar_importer/event_resolver.rb @@ -25,7 +25,7 @@ def is_private? end def has_no_occurences? - occurences.count == 0 + occurences.count.zero? end def is_address_missing? @@ -36,6 +36,10 @@ def occurences @occurences ||= data.occurrences_between(@from_date, Calendar.import_up_to) end + def determine_online_location + data.online_address_id = data.online_event? + end + def determine_location_for_strategy # this algorithm is derived from the notion doc at # GFSC @@ -206,7 +210,7 @@ def save_all_occurences event_time[:are_spaces_available] = occurence.status if occurence.respond_to?(:status) - unless event.update data.attributes.merge(event_time) + unless event.update! data.attributes.merge(event_time) notices << { event: event, errors: event.errors.full_messages } end diff --git a/app/jobs/calendar_importer/events/base.rb b/app/jobs/calendar_importer/events/base.rb index 94515c3b9..e21b91e23 100644 --- a/app/jobs/calendar_importer/events/base.rb +++ b/app/jobs/calendar_importer/events/base.rb @@ -8,7 +8,10 @@ def initialize(event) @event = event end - attr_accessor :place_id, :address_id, :partner_id + attr_accessor :place_id, + :address_id, + :partner_id, + :online_address_id def rrule nil @@ -52,7 +55,8 @@ def attributes place_id: place_id, address_id: address_id, partner_id: partner_id, - publisher_url: publisher_url + publisher_url: publisher_url, + online_address_id: online_address_id } end @@ -95,5 +99,10 @@ def ip_class def private? ip_class&.casecmp('private')&.zero? || (description&.include?('#placecal-ignore')) end + + def online_event? + # TODO: Put in default here + return nil + end end end diff --git a/app/jobs/calendar_importer/events/eventbrite_event.rb b/app/jobs/calendar_importer/events/eventbrite_event.rb index 7e3cf33d9..a652899b8 100644 --- a/app/jobs/calendar_importer/events/eventbrite_event.rb +++ b/app/jobs/calendar_importer/events/eventbrite_event.rb @@ -28,10 +28,12 @@ def place def location return if place.blank? + address = place['address'] if address.present? - [ place['name'], + [ + place['name'], address['address_1'], address['address_2'], address['city'], @@ -56,11 +58,17 @@ def dtend end def occurrences_between(*) - #TODO: Expand when multi-day events supported + # TODO: Expand when multi-day events supported @occurrences = [] @occurrences << Dates.new(dtstart, dtend) @occurrences end + def online_event? + return nil unless @event['online_event'] + + online_address = OnlineAddress.find_or_create_by(url: @event['url']) + online_address.id + end end end diff --git a/app/jobs/calendar_importer/events/facebook_event.rb b/app/jobs/calendar_importer/events/facebook_event.rb index b44d62ac2..277f3c9be 100644 --- a/app/jobs/calendar_importer/events/facebook_event.rb +++ b/app/jobs/calendar_importer/events/facebook_event.rb @@ -28,6 +28,7 @@ def publisher_url def location return if place.blank? + address = place['location'] if address.present? [place['name'], address['street'], address['city'], address['zip']].reject(&:blank?).join(', ') @@ -52,7 +53,7 @@ def last_updated @event.updated_time end - #TODO: Make this a link back to facebook + # TODO: Make this a link back to facebook def footer; end def recurring_event? diff --git a/app/jobs/calendar_importer/events/ics_event.rb b/app/jobs/calendar_importer/events/ics_event.rb index ffe218c43..67cf5373d 100644 --- a/app/jobs/calendar_importer/events/ics_event.rb +++ b/app/jobs/calendar_importer/events/ics_event.rb @@ -45,5 +45,50 @@ def recurring_event? def occurrences_between(from, to) @event.occurrences_between(from, to) end + + def online_event? + # Either return the google conference value, or find the link in the description + link = @event.custom_properties.fetch 'x_google_conference', nil + link ||= find_event_link + + return unless link + + # Then grab the first element of either the match object or the conference array + # (The match object returns ICal Text, not a String, so we have to cast) + # (We can't use .first here because the match object doesn't support it!) + online_address = OnlineAddress.find_or_create_by url: link[0].to_s + online_address.id + end + + private + + def find_event_link + regex = event_link_regex + regex.match description + end + + def event_link_regex + # (http(s)?://)? - will match against https:// or http:// or nothing + # [A-Za-z0-9]+ - will match against alphanumeric strings + # [^\s]+ - grab until we see a whitespace character + # + # We deal with the following strings: + # meet.jit.si/foobarbaz + # meet.google.com/kbv-byuf-cvq + # facebook.com/events/(really long url) + # us04web.zoom.us/j/(really long url) + # zoom.us/j/(really long url) + + http = %r{(http(s)?://)?} + alphanum = %r{[A-Za-z0-9]+} + links = { + 'jitsi': %r{#{http}meet.jit.si/[^\s]+}, + 'meets': %r{#{http}meet.google.com/[^\s]+}, + 'facebook': %r{#{http}facebook.com/events/[^\s]+}, + 'zoom': %r{#{http}(#{alphanum}\.)?zoom.us/j/[^\s]+} + } + + Regexp.union links.values + end end end diff --git a/app/jobs/calendar_importer/events/manchester_uni_event.rb b/app/jobs/calendar_importer/events/manchester_uni_event.rb index f2886ce01..e5fa93641 100644 --- a/app/jobs/calendar_importer/events/manchester_uni_event.rb +++ b/app/jobs/calendar_importer/events/manchester_uni_event.rb @@ -22,8 +22,7 @@ def dtstart date = @event.at_xpath('./ns:times[@type="local"] //ns:start //ns:date') time = @event.at_xpath('./ns:times[@type="local"] //ns:start //ns:time') DateTime.parse([date, time].join(', ')) - - rescue StandardError + rescue StandardError nil end @@ -31,8 +30,7 @@ def dtend date = @event.at_xpath('./ns:times[@type="local"] //ns:end //ns:date') time = @event.at_xpath('./ns:times[@type="local"] //ns:end //ns:time') DateTime.parse([date, time].join(', ')) - - rescue StandardError + rescue StandardError nil end @@ -41,7 +39,7 @@ def recurring_event? end def occurrences_between(*) - #TODO: Expand when multi-day events supported + # TODO: Expand when multi-day events supported @occurrences = [] @occurrences << Dates.new(dtstart, dtend) @occurrences diff --git a/app/jobs/calendar_importer/events/meetup_event.rb b/app/jobs/calendar_importer/events/meetup_event.rb index 5a28b9641..bdb6e7cf0 100644 --- a/app/jobs/calendar_importer/events/meetup_event.rb +++ b/app/jobs/calendar_importer/events/meetup_event.rb @@ -54,7 +54,14 @@ def dtend end def occurrences_between(*) - [ Dates.new(dtstart, dtend) ] + [Dates.new(dtstart, dtend)] + end + + def online_event? + return unless @event['is_online_event'] + + online_address = OnlineAddress.find_or_create_by(url: @event['link']) + online_address.id end end end diff --git a/app/jobs/calendar_importer/events/ticketsolve_event.rb b/app/jobs/calendar_importer/events/ticketsolve_event.rb index d24c884dc..848f92e9b 100644 --- a/app/jobs/calendar_importer/events/ticketsolve_event.rb +++ b/app/jobs/calendar_importer/events/ticketsolve_event.rb @@ -1,6 +1,5 @@ module CalendarImporter::Events class TicketsolveEvent < Base - def uid @event.attribute('id').text end diff --git a/app/jobs/calendar_importer/parsers/eventbrite.rb b/app/jobs/calendar_importer/parsers/eventbrite.rb index 98aee332f..2fe17fd6c 100644 --- a/app/jobs/calendar_importer/parsers/eventbrite.rb +++ b/app/jobs/calendar_importer/parsers/eventbrite.rb @@ -35,7 +35,7 @@ def download_calendar end def import_events_from(data) - data.map { |d| CalendarImporter::Events::EventbriteEvent.new(d) } + data.map { |d| CalendarImporter::Events::EventbriteEvent.new(d) } end end end diff --git a/app/models/calendar.rb b/app/models/calendar.rb index 6f16060b1..932e9f0ba 100644 --- a/app/models/calendar.rb +++ b/app/models/calendar.rb @@ -211,4 +211,3 @@ def is_busy? calendar_state.in_queue? || calendar_state.in_worker? end end - diff --git a/app/models/event.rb b/app/models/event.rb index a733ce536..654a277b7 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -14,7 +14,7 @@ class Event < ApplicationRecord validates :summary, :dtstart, :partner, presence: true before_validation :set_address_from_place validate :require_location - validate :unique_event + validate :unique_event, on: :create # If we are updating the event we don't want it to trigger! before_save :sanitize_rrule @@ -160,11 +160,13 @@ def set_address_from_place end def require_location - if calendar.present? - return if calendar.strategy == 'event' - return if calendar.strategy == 'no_location' - end + # 'event', 'no_location', and 'online_only' do not require a Location + return if %w[event no_location online_only].include?(calendar.strategy) + + # If we have an online address we don't need a physical one + return if self.online_address_id.present? + # If the address exists then the error doesn't apply return unless self.address_id.blank? errors.add(:base, 'No place or address could be created or found for ' \ diff --git a/lib/tasks/workers.rake b/lib/tasks/workers.rake index 6d285fadd..c65d5c453 100644 --- a/lib/tasks/workers.rake +++ b/lib/tasks/workers.rake @@ -4,7 +4,7 @@ require 'yaml' namespace :workers do desc 'Clear and remove all jobs' - task purge_all: [] do + task purge_all: :environment do puts 'Force stopping all workers...' # Force stop of all workers @@ -25,7 +25,7 @@ namespace :workers do end desc 'Show any salient worker errors (You should pipe this into less)' - task inspect_errors: [] do + task inspect_errors: :environment do workers = ActiveRecord::Base.connection .execute('select * from delayed_jobs') .to_a