From 962d5fb4c180863b312ef6093f15f7e6c1664c2c Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Thu, 23 May 2024 00:57:47 +0100 Subject: [PATCH 1/2] Remove CalendarSource and external events --- apps/admin/__init__.py | 85 +---- apps/api/schedule.py | 36 --- apps/schedule/__init__.py | 3 +- apps/schedule/base.py | 63 ++-- apps/schedule/data.py | 50 +-- apps/schedule/external.py | 153 --------- apps/schedule/tasks.py | 72 ----- calendars.json | 264 ---------------- js/line-up.js | 6 +- js/schedule/App.jsx | 3 +- ...e0c_remove_calendarsource_and_external_.py | 69 +++++ models/__init__.py | 1 - models/ical.py | 293 ------------------ models/user.py | 2 +- templates/admin/_nav.html | 1 - templates/admin/edit-feed.html | 39 --- templates/admin/schedule-feeds.html | 33 -- templates/schedule/_external_lister.html | 51 --- templates/schedule/external/feed.html | 98 ------ templates/schedule/external/feeds.html | 40 --- templates/schedule/favourites.html | 7 +- templates/schedule/item-external.html | 68 ---- templates/schedule/line-up.html | 8 - 23 files changed, 98 insertions(+), 1347 deletions(-) delete mode 100644 apps/schedule/external.py delete mode 100644 apps/schedule/tasks.py delete mode 100644 calendars.json create mode 100644 migrations/versions/bdcf93945e0c_remove_calendarsource_and_external_.py delete mode 100644 models/ical.py delete mode 100644 templates/admin/edit-feed.html delete mode 100644 templates/admin/schedule-feeds.html delete mode 100644 templates/schedule/_external_lister.html delete mode 100644 templates/schedule/external/feed.html delete mode 100644 templates/schedule/external/feeds.html delete mode 100644 templates/schedule/item-external.html diff --git a/apps/admin/__init__.py b/apps/admin/__init__.py index c8ebc55a8..3120d516f 100644 --- a/apps/admin/__init__.py +++ b/apps/admin/__init__.py @@ -14,23 +14,20 @@ from flask_login import current_user -from wtforms.validators import Optional, DataRequired, URL +from wtforms.validators import Optional, DataRequired from wtforms import ( SubmitField, BooleanField, HiddenField, - StringField, FieldList, FormField, SelectField, - IntegerField, ) import logging_tree from main import db from models.payment import Payment, BankAccount, BankPayment, BankTransaction from models.purchase import Purchase -from models.ical import CalendarSource from models.feature_flag import FeatureFlag, DB_FEATURE_FLAGS, refresh_flags from models.site_state import SiteState, VALID_STATES, refresh_states, get_states from models.scheduled_task import tasks, ScheduledTaskResult @@ -278,86 +275,6 @@ def payment_config_verify(): ) -@admin.route("/schedule-feeds") -def schedule_feeds(): - feeds = CalendarSource.query.all() - return render_template("admin/schedule-feeds.html", feeds=feeds) - - -class ScheduleForm(Form): - feed_name = StringField("Feed Name", [DataRequired()]) - url = StringField("URL", [DataRequired(), URL()]) - enabled = BooleanField("Feed Enabled") - location = SelectField("Location") - published = BooleanField("Publish events from this feed") - priority = IntegerField("Priority", [Optional()]) - preview = SubmitField("Preview") - submit = SubmitField("Save") - delete = SubmitField("Delete") - - def update_feed(self, feed): - feed.name = self.feed_name.data - feed.url = self.url.data - feed.enabled = self.enabled.data - feed.published = self.published.data - feed.priority = self.priority.data - - def init_from_feed(self, feed): - self.feed_name.data = feed.name - self.url.data = feed.url - self.enabled.data = feed.enabled - self.published.data = feed.published - self.priority.data = feed.priority - - if feed.mapobj: - self.location.data = str(feed.mapobj.id) - else: - self.location.data = "" - - -@admin.route("/schedule-feeds/", methods=["GET", "POST"]) -def schedule_feed(feed_id): - feed = CalendarSource.query.get_or_404(feed_id) - form = ScheduleForm() - - form.location.choices = [] - - if form.validate_on_submit(): - if form.delete.data: - for event in feed.events: - db.session.delete(event) - db.session.delete(feed) - db.session.commit() - flash("Feed deleted") - return redirect(url_for(".schedule_feeds", feed_id=feed_id)) - - form.update_feed(feed) - db.session.commit() - msg = "Updated feed %s" % feed.name - flash(msg) - app.logger.info(msg) - return redirect(url_for(".schedule_feed", feed_id=feed_id)) - - form.init_from_feed(feed) - return render_template("admin/edit-feed.html", feed=feed, form=form) - - -@admin.route("/schedule-feeds/new", methods=["GET", "POST"]) -def new_feed(): - form = ScheduleForm() - - if form.validate_on_submit(): - feed = CalendarSource("") - form.update_feed(feed) - db.session.add(feed) - db.session.commit() - msg = "Created feed %s" % feed.name - flash(msg) - app.logger.info(msg) - return redirect(url_for(".schedule_feed", feed_id=feed.id)) - return render_template("admin/edit-feed.html", form=form) - - @admin.route("/scheduled-tasks") def scheduled_tasks(): data = [] diff --git a/apps/api/schedule.py b/apps/api/schedule.py index f775d8d72..ee20faa18 100644 --- a/apps/api/schedule.py +++ b/apps/api/schedule.py @@ -8,7 +8,6 @@ from . import api from main import db from models.cfp import Proposal -from models.ical import CalendarEvent from models.admin_message import AdminMessage from models.event_tickets import EventTicket @@ -101,40 +100,6 @@ def put(self, proposal_id): return {"is_favourite": new_state} -class FavouriteExternal(Resource): - def get(self, event_id): - if not current_user.is_authenticated: - abort(401) - - event = CalendarEvent.query.get_or_404(event_id) - current_state = event in current_user.calendar_favourites - - return {"is_favourite": current_state} - - def put(self, event_id): - """Put with no data to toggle""" - if not current_user.is_authenticated: - abort(401) - - event = CalendarEvent.query.get_or_404(event_id) - current_state = event in current_user.calendar_favourites - - data = request.get_json() - if data.get("state") is not None: - new_state = bool(data["state"]) - else: - new_state = not current_state - - if new_state and not current_state: - current_user.calendar_favourites.append(event) - elif current_state and not new_state: - current_user.calendar_favourites.remove(event) - - db.session.commit() - - return {"is_favourite": new_state} - - class UpdateLotteryPreferences(Resource): def post(self, proposal_type): if proposal_type not in ["workshop", "youthworkshop"]: @@ -183,6 +148,5 @@ def get(self): api.add_resource(ProposalResource, "/proposal/") api.add_resource(FavouriteProposal, "/proposal//favourite") -api.add_resource(FavouriteExternal, "/external//favourite") api.add_resource(ScheduleMessage, "/schedule_messages") api.add_resource(UpdateLotteryPreferences, "/schedule/tickets//preferences") diff --git a/apps/schedule/__init__.py b/apps/schedule/__init__.py index cd83e938f..2d38e0233 100644 --- a/apps/schedule/__init__.py +++ b/apps/schedule/__init__.py @@ -69,6 +69,5 @@ def feed_redirect(fmt): from . import base # noqa from . import feeds # noqa -from . import external # noqa from . import attendee_content # noqa -from . import tasks # noqa + diff --git a/apps/schedule/base.py b/apps/schedule/base.py index b64fa982c..1f2700dd6 100644 --- a/apps/schedule/base.py +++ b/apps/schedule/base.py @@ -20,7 +20,6 @@ from main import db from models import event_year from models.cfp import Proposal, Venue -from models.ical import CalendarSource, CalendarEvent from models.user import generate_api_token from models.admin_message import AdminMessage from models.event_tickets import EventTicket @@ -87,10 +86,8 @@ def line_up(): # (Because we don't want a bias in starring) random.Random(current_user.get_id()).shuffle(proposals) - externals = CalendarSource.get_enabled_events() - return render_template( - "schedule/line-up.html", proposals=proposals, externals=externals + "schedule/line-up.html", proposals=proposals, ) @@ -101,30 +98,20 @@ def add_favourite(): event_id = int(request.form["fave"]) event_type = request.form["event_type"] - if event_type == "proposal": - proposal = Proposal.query.get_or_404(event_id) - if proposal in current_user.favourites: - current_user.favourites.remove(proposal) - else: - current_user.favourites.append(proposal) - - db.session.commit() - return redirect( - url_for(".main_year", year=event_year()) - + "#proposal-{}".format(proposal.id) - ) + if event_type != "proposal": + abort(400) + proposal = Proposal.query.get_or_404(event_id) + if proposal in current_user.favourites: + current_user.favourites.remove(proposal) else: - event = CalendarEvent.query.get_or_404(event_id) - if event in current_user.calendar_favourites: - current_user.calendar_favourites.remove(event) - else: - current_user.calendar_favourites.append(event) + current_user.favourites.append(proposal) - db.session.commit() - return redirect( - url_for(".main_year", year=event_year()) + "#event-{}".format(event.id) - ) + db.session.commit() + return redirect( + url_for(".main_year", year=event_year()) + + "#proposal-{}".format(proposal.id) + ) @schedule.route("/favourites", methods=["GET", "POST"]) @@ -133,38 +120,28 @@ def favourites(): if (request.method == "POST") and current_user.is_authenticated: event_id = int(request.form["fave"]) event_type = request.form["event_type"] - if event_type == "proposal": - proposal = Proposal.query.get_or_404(event_id) - if proposal in current_user.favourites: - current_user.favourites.remove(proposal) - else: - current_user.favourites.append(proposal) - - db.session.commit() - return redirect(url_for(".favourites") + "#proposal-{}".format(proposal.id)) + if event_type != "proposal": + abort(400) + proposal = Proposal.query.get_or_404(event_id) + if proposal in current_user.favourites: + current_user.favourites.remove(proposal) else: - event = CalendarEvent.query.get_or_404(event_id) - if event in current_user.calendar_favourites: - current_user.calendar_favourites.remove(event) - else: - current_user.calendar_favourites.append(event) + current_user.favourites.append(proposal) - db.session.commit() - return redirect(url_for(".favourites") + "#event-{}".format(event.id)) + db.session.commit() + return redirect(url_for(".favourites") + "#proposal-{}".format(proposal.id)) if current_user.is_anonymous: return redirect(url_for("users.login", next=url_for(".favourites"))) proposals = [p for p in current_user.favourites if not p.hide_from_schedule] - externals = current_user.calendar_favourites token = generate_api_token(app.config["SECRET_KEY"], current_user.id) return render_template( "schedule/favourites.html", proposals=proposals, - externals=externals, token=token, ) diff --git a/apps/schedule/data.py b/apps/schedule/data.py index d313b0e38..2570ed517 100644 --- a/apps/schedule/data.py +++ b/apps/schedule/data.py @@ -6,7 +6,6 @@ from models import event_year from models.cfp import Proposal, Venue -from models.ical import CalendarSource from main import external_url from . import event_tz @@ -59,33 +58,6 @@ def _get_proposal_dict(proposal: Proposal, favourites_ids): return res -def _get_ical_dict(event, favourites_ids): - res = { - "id": -event.id, - "start_date": event_tz.localize(event.start_dt), - "end_date": event_tz.localize(event.end_dt), - "venue": event.location or "(Unknown)", - "latlon": event.latlon, - "map_link": event.map_link, - "title": event.summary, - "speaker": "", - "user_id": None, - "description": event.description, - "type": "talk", - "may_record": False, - "is_fave": event.id in favourites_ids, - "source": "external", - "link": external_url( - ".item_external", year=event_year(), slug=event.slug, event_id=event.id - ), - } - if event.type in ["workshop", "youthworkshop"]: - res["cost"] = event.display_cost - res["equipment"] = event.display_participant_equipment - res["age_range"] = event.display_age_range - return res - - def _filter_obj_to_dict(filter_obj): """Request.args uses a MulitDict this lets us pass filter_obj as plain dicts and have everything work as expected. @@ -103,10 +75,9 @@ def _get_scheduled_proposals(filter_obj={}, override_user=None): user = current_user if user.is_anonymous: - proposal_favourites = external_favourites = [] + proposal_favourites = [] else: proposal_favourites = [f.id for f in user.favourites] - external_favourites = [f.id for f in user.calendar_favourites] schedule = Proposal.query.filter( Proposal.is_accepted, @@ -118,14 +89,6 @@ def _get_scheduled_proposals(filter_obj={}, override_user=None): schedule = [_get_proposal_dict(p, proposal_favourites) for p in schedule] - ical_sources = CalendarSource.query.filter_by(enabled=True, published=True) - - for source in ical_sources: - for e in source.events: - d = _get_ical_dict(e, external_favourites) - d["venue"] = source.mapobj.name - schedule.append(d) - if "is_favourite" in filter_obj and filter_obj["is_favourite"]: schedule = [s for s in schedule if s.get("is_fave", False)] @@ -171,16 +134,9 @@ def _get_priority_sorted_venues(venues_to_allow): main_venues = Venue.query.filter().all() main_venue_names = [(v.name, "main", v.priority) for v in main_venues] - ical_sources = CalendarSource.query.filter_by(enabled=True, published=True) - ical_source_names = [ - (v.mapobj.name, "ical", v.priority) - for v in ical_sources - if v.mapobj and v.events - ] - res = [] seen_names = [] - for venue in main_venue_names + ical_source_names: + for venue in main_venue_names: name = venue[0] if name not in seen_names and name in venues_to_allow: seen_names.append(name) @@ -193,5 +149,5 @@ def _get_priority_sorted_venues(venues_to_allow): } ) - res = sorted(res, key=lambda v: (v["source"] != "ical", v["order"]), reverse=True) + res = sorted(res, key=lambda v: v["order"], reverse=True) return res diff --git a/apps/schedule/external.py b/apps/schedule/external.py deleted file mode 100644 index a9fa02afd..000000000 --- a/apps/schedule/external.py +++ /dev/null @@ -1,153 +0,0 @@ -""" Views for dealing with external schedules provided by villages.""" - -from wtforms import StringField, SubmitField, BooleanField -from wtforms.validators import DataRequired, URL -from flask_login import login_required, current_user -from flask import render_template, redirect, url_for, flash, request, abort - -from main import db -from models import event_year -from models.ical import CalendarSource, CalendarEvent - -from ..common.forms import Form -from ..common import feature_flag - -from . import schedule - - -@schedule.route("/schedule//external/", methods=["GET", "POST"]) -@schedule.route( - "/schedule//external/-", methods=["GET", "POST"] -) -@feature_flag("LINE_UP") -def item_external(year, event_id, slug=None): - if year != event_year(): - abort(404) - - event = CalendarEvent.query.get_or_404(event_id) - - if not current_user.is_anonymous: - is_fave = event in current_user.calendar_favourites - else: - is_fave = False - - if (request.method == "POST") and not current_user.is_anonymous: - if is_fave: - current_user.calendar_favourites.remove(event) - msg = 'Removed "%s" from favourites' % event.title - else: - current_user.calendar_favourites.append(event) - msg = 'Added "%s" to favourites' % event.title - db.session.commit() - flash(msg) - return redirect( - url_for(".item_external", year=year, event_id=event.id, slug=event.slug) - ) - - if slug != event.slug: - return redirect( - url_for(".item_external", year=year, event_id=event.id, slug=event.slug) - ) - - return render_template( - "schedule/item-external.html", - event=event, - is_fave=is_fave, - venue_name=event.venue, - ) - - -class AddExternalFeedForm(Form): - url = StringField("URL", [DataRequired(), URL()]) - preview = SubmitField("Preview") - - -@schedule.route("/schedule/external/feeds", methods=["GET", "POST"]) -def external_feeds_redirect(): - return redirect(url_for(".external_feeds")) - - -@schedule.route("/schedule/publish", methods=["GET", "POST"]) -@login_required -@feature_flag("LINE_UP") -def external_feeds(): - form = AddExternalFeedForm() - - if form.validate_on_submit(): - url = form.url.data.strip() - source = CalendarSource.query.filter_by( - user_id=current_user.id, url=url - ).one_or_none() - - if not source: - source = CalendarSource(url=url, user=current_user) - db.session.commit() - - return redirect(url_for(".external_feed", source_id=source.id)) - - calendars = current_user.calendar_sources - return render_template( - "schedule/external/feeds.html", form=form, calendars=calendars - ) - - -class UpdateExternalFeedForm(Form): - url = StringField("URL", [DataRequired(), URL()]) - name = StringField("Feed Name", [DataRequired()]) - published = BooleanField("Publish events from this feed") - preview = SubmitField("Preview") - save = SubmitField("Save") - - -@schedule.route("/schedule/publish/", methods=["GET", "POST"]) -@login_required -@feature_flag("LINE_UP") -def external_feed(source_id): - calendar = CalendarSource.query.get(source_id) - if calendar.user != current_user: - abort(403) - - form = UpdateExternalFeedForm(obj=calendar) - - if request.method != "POST": - if calendar.mapobj: - form.location.data = str(calendar.mapobj.id) - else: - form.location.data = "" - - if form.validate_on_submit(): - if form.save.data: - calendar.url = form.url.data - calendar.name = form.name.data - calendar.published = form.published.data - - try: - calendar.refresh() - except Exception: - pass - db.session.commit() - return redirect(url_for(".external_feeds")) - - calendar.url = form.url.data - - try: - alerts = calendar.refresh() - except Exception: - alerts = [("danger", "An error occurred trying to load the feed")] - preview_events = [] - else: - preview_events = list(calendar.events) - - if not preview_events: - alerts = [("danger", "We could not load any events from this feed")] - - db.session.rollback() - - return render_template( - "schedule/external/feed.html", - form=form, - calendar=calendar, - preview_events=preview_events, - alerts=alerts, - preview=True, - ) diff --git a/apps/schedule/tasks.py b/apps/schedule/tasks.py deleted file mode 100644 index b664e0ab9..000000000 --- a/apps/schedule/tasks.py +++ /dev/null @@ -1,72 +0,0 @@ -""" Schedule CLI tasks """ -import json -from collections import OrderedDict - -from flask import current_app as app - -from main import db -from models.ical import CalendarSource - -from . import schedule - - -@schedule.cli.command("create_calendars") -def create_calendars(self): - icals = json.load(open("calendars.json")) - - for cal in icals: - existing_calendar = CalendarSource.query.filter_by(url=cal["url"]).first() - if existing_calendar: - source = existing_calendar - app.logger.info("Refreshing calendar %s", source.name) - else: - source = CalendarSource(cal["url"]) - app.logger.info("Adding calendar %s", cal["name"]) - - cal["lat"] = cal.get("lat") - cal["lon"] = cal.get("lon") - - for f in ["name", "type", "priority", "main_venue", "lat", "lon"]: - cur_val = getattr(source, f) - new_val = cal[f] - - if cur_val != new_val: - app.logger.info(" %10s: %r -> %r", f, cur_val, new_val) - setattr(source, f, new_val) - - db.session.add(source) - - db.session.commit() - - -@schedule.cli.command("refresh_calendars") -def refresh_calendars(self): - for source in CalendarSource.query.filter_by(enabled=True).all(): - source.refresh() - - db.session.commit() - - -@schedule.cli.command("export_calendars") -def export_calendars(self): - data = [] - calendars = CalendarSource.query.filter_by(enabled=True).order_by( - CalendarSource.priority, CalendarSource.id - ) - for source in calendars: - source_data = OrderedDict( - [ - ("name", source.name), - ("url", source.url), - ("type", source.type), - ("priority", source.priority), - ("main_venue", source.main_venue), - ] - ) - if source.lat: - source_data["lat"] = source.lat - source_data["lon"] = source.lon - - data.append(source_data) - - json.dump(data, open("calendars.json", "w"), indent=4, separators=(",", ": ")) diff --git a/calendars.json b/calendars.json deleted file mode 100644 index bdc4764f3..000000000 --- a/calendars.json +++ /dev/null @@ -1,264 +0,0 @@ -[ - { - "name": "ISS", - "url": "http://jonty.co.uk/bits/ISS-Guildford.ics", - "type": "Space", - "priority": -10, - "main_venue": "The Sky" - }, - { - "name": "Sai's blind navigation crash course", - "url": "https://calendar.google.com/calendar/ical/0vtdvsar622kkghhbmn2gj1amg%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": -2, - "main_venue": "Contact @saizai" - }, - { - "name": "Radio Village", - "url": "https://calendar.google.com/calendar/ical/lhifjvb20rqasl45up83uug6v4%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": -1, - "main_venue": "Radio Village", - "lat": 51.2125, - "lon": -0.60941 - }, - { - "name": "EMF Music", - "url": "https://calendar.google.com/calendar/ical/3s832k79jjbrl9o9e8ifmgflhg%40group.calendar.google.com/public/basic.ics", - "type": "Music", - "priority": 0, - "main_venue": "Stage A", - "lat": 51.21191, - "lon": -0.6068 - }, - { - "name": "EMF Film", - "url": "http://p04-calendars.icloud.com/published/2/iSu19GpxhFD47NHBtEuNRQWpt0qew4aaj3FVRj-Fjglz2QMdy6opkoZBTTTvPN_cxUU7ZxlBg8ZoTELIBiyjyenVNi82c_10agpwUpaXJro", - "type": "Film", - "priority": 0, - "main_venue": "Stage B", - "lat": 51.21304, - "lon": -0.60838 - }, - { - "name": "Maths Village", - "url": "http://www.mscroggs.co.uk/emfcal", - "type": "Village", - "priority": 0, - "main_venue": "Maths Village", - "lat": 51.21211, - "lon": -0.60869 - }, - { - "name": "Hacking Hamlet", - "url": "https://calendar.google.com/calendar/ical/nvs1s1r6pmvf7v4d82349ns9u4%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": 0, - "main_venue": "Hacking Hamlet", - "lat": 51.212925, - "lon": -0.60971 - }, - { - "name": "Nottinghack", - "url": "https://calendar.google.com/calendar/ical/o8apcgfbmjkkv0uk5fos4tuc3o%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": 0, - "main_venue": "Nottinghack Village", - "lat": 51.212692, - "lon": -0.607659 - }, - { - "name": "HABville Village", - "url": "https://calendar.google.com/calendar/ical/hp2b34jg53gghdpn4e6evg5c34%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": 0, - "main_venue": "HABville Village", - "lat": 51.21202, - "lon": -0.60861 - }, - { - "name": "Haxors of the Roses", - "url": "https://calendar.google.com/calendar/ical/bowerham.net_8qqgj22h6pvgubuloa6uv6de14%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": 0, - "main_venue": "Haxors of the Roses Village", - "lat": 51.212831, - "lon": -0.607225 - }, - { - "name": "Microsoft", - "url": "https://calendar.google.com/calendar/ical/msftemfcamp%40gmail.com/public/basic.ics", - "type": "Village", - "priority": 0, - "main_venue": "Make. Invent. Do. Village", - "lat": 51.21222, - "lon": -0.607215 - }, - { - "name": "Never Too Much Bunting", - "url": "https://calendar.google.com/calendar/ical/kp6kvl468ijk9hsmva85v4gl88%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": 0, - "main_venue": "Never Too Much Bunting Village", - "lat": 51.21275, - "lon": -0.60973 - }, - { - "name": "Fablab Truck", - "url": "http://jonty.co.uk/bits/Fablab_camp.ics", - "type": "Village", - "priority": 0, - "main_venue": "Fablab Truck", - "lat": 51.21215, - "lon": -0.60671 - }, - { - "name": "Millers Hollow", - "url": "https://calendar.google.com/calendar/ical/1adatrf0m3plpfv8gh47mrk7ko%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": 0, - "main_venue": "Millers Hollow Village", - "lat": 51.21297, - "lon": -0.60874 - }, - { - "name": "Scottish Consulate", - "url": "https://calendar.google.com/calendar/ical/ebms9h8dn8mcthk3k0i2cttjsg%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": 0, - "main_venue": "Scottish Consulate Village", - "lat": 51.21253, - "lon": -0.60887 - }, - { - "name": "Minecraft Village", - "url": "https://calendar.google.com/calendar/ical/kr1iudroe96pnq2l18hnghvp38%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": 0, - "main_venue": "Minecraft Village", - "lat": 51.21249, - "lon": -0.60822 - }, - { - "name": "Makespace Village", - "url": "https://calendar.google.com/calendar/ical/1esdfs4rg0oq0du82jvi3alaec%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": 0, - "main_venue": "Makespace Village", - "lat": 51.21229, - "lon": -0.60764 - }, - { - "name": "Games", - "url": "https://calendar.google.com/calendar/ical/gueemqqifr9tup0lsfplnu2pcc%40group.calendar.google.com/public/basic.ics", - "type": "Performance", - "priority": 0, - "main_venue": "Stage C", - "lat": 51.21214, - "lon": -0.60757 - }, - { - "name": "HABville Village 2", - "url": "https://calendar.google.com/calendar/ical/21i3eglok30ei233bj3iter1b0%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": 0, - "main_venue": "HABville Village" - }, - { - "name": "Blacksmiths", - "url": "https://calendar.google.com/calendar/ical/i1fa3r5jokroruadgp15aqfo5c%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": 10, - "main_venue": "Blacksmiths", - "lat": 51.21158, - "lon": -0.60814 - }, - { - "name": "SawuGo, The One Ring Workshop", - "url": "https://calendar.google.com/calendar/ical/gum1il3s07gvpv42l22ed32bmg%40group.calendar.google.com/private-c0607d549af2324117298b0b04f02cb4/basic.ics", - "type": "Village", - "priority": 10, - "main_venue": "SawuGo, The One Ring Workshop", - "lat": 51.212, - "lon": -0.60851 - }, - { - "name": "Empty Epsilon", - "url": "http://jonty.co.uk/bits/EmptyEpsilon_spacheship_simulator.ics", - "type": "Village", - "priority": 10, - "main_venue": "Empty Epsilon Village", - "lat": 51.21302, - "lon": -0.60875 - }, - { - "name": "Laser Tag", - "url": "https://outlook.office365.com/owa/calendar/6da5d9aa319d42c3ab887f1af52bc239@serviceinmotion.co.uk/87751db86eed4989a2531c3a59f10ed516309432096381974961/calendar.ics", - "type": "Activity", - "priority": 12, - "main_venue": "Up by the EMF sign", - "lat": 51.21469, - "lon": -0.60864 - }, - { - "name": "Nature Chamber", - "url": "https://calendar.google.com/calendar/ical/l27kb7id5cpsqjknoq5tf5i3bg%40group.calendar.google.com/public/basic.ics", - "type": "Workshop", - "priority": 15, - "main_venue": "The Lounge" - }, - { - "name": "Algorave", - "url": "https://calendar.google.com/calendar/ical/slab.org_77m028f92l9guddfbhrrtghj2g%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": 20, - "main_venue": "Algorave Village", - "lat": 51.21133, - "lon": -0.6075 - }, - { - "name": "Badge Operations Center", - "url": "https://calendar.google.com/calendar/ical/bowerham.net_47cgvma5a74hs7fnlashjc0gpg%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": 20, - "main_venue": "Badge Operations Center", - "lat": 51.21174, - "lon": -0.60775 - }, - { - "name": "UAV Flying Field", - "url": "https://calendar.google.com/calendar/ical/e6t2qrog0t6343bf1gebnh2jl8%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": 20, - "main_venue": "UAV Flying Field", - "lat": 51.21001, - "lon": -0.60738 - }, - { - "name": "Portcullis", - "url": "https://calendar.google.com/calendar/ical/iojq4b3ctp3kegomjogac2a4gc%40group.calendar.google.com/public/basic.ics", - "type": "Village", - "priority": 21, - "main_venue": "Portcullis Village", - "lat": 51.21237, - "lon": -0.60856 - }, - { - "name": "Hack Center", - "url": "https://calendar.google.com/calendar/ical/ublklsm8g031s6mtjv8ds08k4k%40group.calendar.google.com/private-63dd14bde661eb38b31c53a73f8a3bec/basic.ics", - "type": "Village", - "priority": 30, - "main_venue": "Hack Center", - "lat": 51.21121, - "lon": -0.60681 - }, - { - "name": "EMF Youth", - "url": "https://calendar.google.com/calendar/ical/5nkm5d7nahs9bcgn4q1btjg3c4%40group.calendar.google.com/public/basic.ics", - "type": "Youth", - "priority": 95, - "main_venue": "Workshop 3", - "lat": 51.21331, - "lon": -0.60804 - } -] \ No newline at end of file diff --git a/js/line-up.js b/js/line-up.js index d04bd10d3..9cc697f6b 100644 --- a/js/line-up.js +++ b/js/line-up.js @@ -2,12 +2,8 @@ $(function() { $('.favourite-button').click(async (event) => { event.preventDefault(); const btn = event.target.closest('.favourite-button'); - let event_type = 'proposal'; - if (btn.closest('.event')?.classList?.contains('external')) { - event_type = 'external'; - } const event_id = btn.value; - const response = await fetch(`/api/${event_type}/${event_id}/favourite`, { + const response = await fetch(`/api/proposal/${event_id}/favourite`, { method: 'PUT', credentials: 'include', headers: { diff --git a/js/schedule/App.jsx b/js/schedule/App.jsx index 41531b506..03a78c773 100644 --- a/js/schedule/App.jsx +++ b/js/schedule/App.jsx @@ -76,8 +76,7 @@ function App() { }); function toggleFavourite(event) { - let endpoint = event.source === 'database' ? `/api/proposal/${event.id}/favourite` : `/api/external/${Math.abs(event.id)}/favourite`; - fetch(endpoint, { headers: { 'Authorization': apiToken, 'Content-Type': 'application/json' }, method: 'put', body: '{}' }) + fetch(`/api/proposal/${event.id}/favourite`, { headers: { 'Authorization': apiToken, 'Content-Type': 'application/json' }, method: 'put', body: '{}' }) .then((response) => response.json()) .then((data) => { let schedule = JSON.parse(JSON.stringify(rawSchedule)) diff --git a/migrations/versions/bdcf93945e0c_remove_calendarsource_and_external_.py b/migrations/versions/bdcf93945e0c_remove_calendarsource_and_external_.py new file mode 100644 index 000000000..3c424ee73 --- /dev/null +++ b/migrations/versions/bdcf93945e0c_remove_calendarsource_and_external_.py @@ -0,0 +1,69 @@ +"""Remove CalendarSource and external events + +Revision ID: bdcf93945e0c +Revises: 09f776ea71f0 +Create Date: 2024-05-22 23:57:09.873592 + +""" + +# revision identifiers, used by Alembic. +revision = 'bdcf93945e0c' +down_revision = '09f776ea71f0' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('ix_calendar_source_user_id', table_name='calendar_source') + op.drop_table('calendar_source') + op.drop_index('ix_calendar_event_source_id', table_name='calendar_event') + op.drop_table('calendar_event') + op.drop_table('favourite_calendar_event') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('favourite_calendar_event', + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('event_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['event_id'], ['calendar_event.id'], name='fk_favourite_calendar_event_event_id_calendar_event'), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name='fk_favourite_calendar_event_user_id_user'), + sa.PrimaryKeyConstraint('user_id', 'event_id', name='pk_favourite_calendar_event') + ) + op.create_table('calendar_event', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('uid', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('start_dt', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.Column('end_dt', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.Column('source_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('summary', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('location', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('displayed', sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['source_id'], ['calendar_source.id'], name='fk_calendar_event_source_id_calendar_source'), + sa.PrimaryKeyConstraint('id', name='pk_calendar_event'), + sa.UniqueConstraint('source_id', 'uid', name='uq_calendar_event_source_id') + ) + op.create_index('ix_calendar_event_source_id', 'calendar_event', ['source_id'], unique=False) + op.create_table('calendar_source', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('url', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('enabled', sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('type', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('main_venue', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('contact_phone', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('contact_email', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('priority', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('published', sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('displayed', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False), + sa.Column('refreshed_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name='fk_calendar_source_user_id_user'), + sa.PrimaryKeyConstraint('id', name='pk_calendar_source') + ) + op.create_index('ix_calendar_source_user_id', 'calendar_source', ['user_id'], unique=False) + # ### end Alembic commands ### diff --git a/models/__init__.py b/models/__init__.py index ec34a1756..0d6aa597d 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -187,7 +187,6 @@ def config_date(key): from .cfp import * # noqa: F401,F403 from .permission import * # noqa: F401,F403 from .email import * # noqa: F401,F403 -from .ical import * # noqa: F401,F403 from .product import * # noqa: F401,F403 from .purchase import * # noqa: F401,F403 from .basket import * # noqa: F401,F403 diff --git a/models/ical.py b/models/ical.py deleted file mode 100644 index 8adc17d6a..000000000 --- a/models/ical.py +++ /dev/null @@ -1,293 +0,0 @@ -import requests -import re - -from geoalchemy2.shape import to_shape -from icalendar import Calendar -import pendulum -from shapely.geometry import Point -from slugify import slugify_unicode -from sqlalchemy import UniqueConstraint, func, select -from sqlalchemy.orm import column_property -from sqlalchemy.orm.exc import NoResultFound - -from main import db -from . import BaseModel, event_start, event_end - - -class CalendarSource(BaseModel): - __tablename__ = "calendar_source" - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True) - url = db.Column(db.String, nullable=False) - name = db.Column(db.String) - type = db.Column(db.String, default="Village") - priority = db.Column(db.Integer, default=0) - enabled = db.Column(db.Boolean, nullable=False, default=False) - refreshed_at = db.Column(db.DateTime()) - - displayed = db.Column(db.Boolean, nullable=False, default=False) - published = db.Column(db.Boolean, nullable=False, default=False) - main_venue = db.Column(db.String) - contact_phone = db.Column(db.String) - contact_email = db.Column(db.String) - - user = db.relationship("User", backref="calendar_sources") - - # Make sure these are identifiable to the memoize cache - def __repr__(self): - return "<%s %s: %s>" % (self.__class__.__name__, self.id, self.url) - - @classmethod - def get_export_data(cls): - sources = cls.query.with_entities( - cls.id, - cls.name, - cls.type, - cls.enabled, - cls.url, - cls.main_venue, - cls.priority, - ).order_by(cls.id) - - data = {"public": {"sources": sources}, "tables": ["calendar_source"]} - - return data - - def refresh(self): - request = requests.get(self.url) - - cal = Calendar.from_ical(request.text) - if self.name is None: - self.name = cal.get("X-WR-CALNAME") - - for event in self.events: - event.displayed = False - - local_tz = pendulum.timezone("Europe/London") - alerts = [] - uids_seen = set() - out_of_range_event = False - for component in cal.walk(): - if component.name == "VEVENT": - summary = component.get("Summary") - - # postgres converts to UTC if given an aware datetime, so strip it up front - start_dt = pendulum.instance(component.get("dtstart").dt) - start_dt = local_tz.convert(start_dt).naive() - - end_dt = pendulum.instance(component.get("dtend").dt) - end_dt = local_tz.convert(end_dt).naive() - - name = summary - if summary and start_dt: - name = "'{}' at {}".format(summary, start_dt) - elif summary: - name = "'{}'".format(summary) - elif start_dt: - name = "Event at {}".format(start_dt) - else: - name = len(self.events) + 1 - - if not component.get("uid"): - alerts.append(("danger", "{} has no UID".format(name))) - continue - - uid = str(component["uid"]) - if uid in uids_seen: - alerts.append( - ("danger", "{} has duplicate UID {}".format(name, uid)) - ) - continue - uids_seen.add(uid) - - if "rrule" in component: - alerts.append( - ("warning", "{} has rrule, which is not processed".format(uid)) - ) - - # Allow a bit of slop for build-up events - if ( - start_dt < event_start() - pendulum.duration(days=2) - and not out_of_range_event - ): - alerts.append( - ( - "warning", - "At least one event ({}) is before the start of the event".format( - uid - ), - ) - ) - out_of_range_event = True - - if ( - end_dt > event_end() + pendulum.duration(days=1) - and not out_of_range_event - ): - alerts.append( - ( - "warning", - "At least one event ({}) is after the end of the event".format( - uid - ), - ) - ) - out_of_range_event = True - - if start_dt > end_dt: - alerts.append( - ( - "danger", - "Start time for {} is after its end time".format(uid), - ) - ) - out_of_range_event = True - - try: - event = CalendarEvent.query.filter_by( - source_id=self.id, uid=uid - ).one() - - except NoResultFound: - event = CalendarEvent(uid=uid) - self.events.append(event) - if len(self.events) > 1000: - raise Exception("Too many events in feed") - - event.start_dt = start_dt - event.end_dt = end_dt - event.summary = component.get("summary") - event.description = component.get("description") - event.location = component.get("location") - event.displayed = True - - self.refreshed_at = pendulum.now() - - return alerts - - @property - def latlon(self): - if self.mapobj: - obj = to_shape(self.mapobj.geom) - if isinstance(obj, Point): - return (obj.y, obj.x) - return None - - @classmethod - def get_enabled_events(self): - sources = CalendarSource.query.filter_by(published=True, displayed=True) - events = [] - for source in sources: - events.extend(source.events) - return events - - -FavouriteCalendarEvent = db.Table( - "favourite_calendar_event", - BaseModel.metadata, - db.Column("user_id", db.Integer, db.ForeignKey("user.id"), primary_key=True), - db.Column( - "event_id", db.Integer, db.ForeignKey("calendar_event.id"), primary_key=True - ), -) - - -class CalendarEvent(BaseModel): - __tablename__ = "calendar_event" - - id = db.Column(db.Integer, primary_key=True) - uid = db.Column(db.String) - start_dt = db.Column(db.DateTime(), nullable=False) - end_dt = db.Column(db.DateTime(), nullable=False) - displayed = db.Column(db.Boolean, nullable=False, default=True) - - source_id = db.Column( - db.Integer, db.ForeignKey(CalendarSource.id), nullable=False, index=True - ) - summary = db.Column(db.String, nullable=True) - description = db.Column(db.String, nullable=True) - location = db.Column(db.String, nullable=True) - - source = db.relationship(CalendarSource, backref="events") - calendar_favourites = db.relationship( - "User", secondary=FavouriteCalendarEvent, backref="calendar_favourites" - ) - - favourite_count = column_property( - select([func.count(FavouriteCalendarEvent.c.user_id)]) - .where(FavouriteCalendarEvent.c.user_id == id) - .scalar_subquery(), # type: ignore[attr-defined] - deferred=True, - ) - - @classmethod - def get_export_data(cls): - events = cls.query.with_entities( - cls.source_id, - cls.uid, - cls.start_dt, - cls.end_dt, - cls.summary, - cls.description, - cls.location, - cls.favourite_count, - ).order_by(cls.source_id, cls.id) - - data = { - "public": {"events": events}, - "tables": ["calendar_event", "favourite_calendar_event"], - } - - return data - - @property - def title(self): - return self.summary - - @property - def venue(self): - if self.source.main_venue: - return self.source.main_venue - else: - return self.location - - @property - def type(self): - return self.source.type - - @property - def slug(self): - slug = slugify_unicode(self.summary).lower() - if len(slug) > 60: - words = re.split(" +|[,.;:!?]+", self.summary) - break_words = ["and", "which", "with", "without", "for", "-", ""] - - for i, word in reversed(list(enumerate(words))): - new_slug = slugify_unicode(" ".join(words[:i])).lower() - if word in break_words: - if len(new_slug) > 10 and not len(new_slug) > 60: - slug = new_slug - break - - elif len(slug) > 60 and len(new_slug) > 10: - slug = new_slug - - if len(slug) > 60: - slug = slug[:60] + "-" - - return slug - - @property - def latlon(self): - if self.source.latlon: - return self.source.latlon - return None - - @property - def map_link(self): - latlon = self.latlon - if latlon: - return "https://map.emfcamp.org/#20/%s/%s" % (latlon[0], latlon[1]) - return None - - __table_args__ = (UniqueConstraint(source_id, uid),) diff --git a/models/user.py b/models/user.py index c9b488e97..3ec9da742 100644 --- a/models/user.py +++ b/models/user.py @@ -188,7 +188,7 @@ def verify_checkin_code(key, uid): class User(BaseModel, UserMixin): __tablename__ = "user" - __versioned__ = {"exclude": ["favourites", "calendar_favourites"]} + __versioned__ = {"exclude": ["favourites"]} id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String, unique=True, index=True) diff --git a/templates/admin/_nav.html b/templates/admin/_nav.html index fe0033003..7c5c1d58e 100644 --- a/templates/admin/_nav.html +++ b/templates/admin/_nav.html @@ -70,7 +70,6 @@