Repository: fossasia/open-event-server

Issue:
Add feature to export event as iCalendar
**Is your feature request related to a problem? Please describe.**
Add the API endpoint to export event as iCalendar.
 
**Describe the solution you'd like**
Follow the process defined in v1 - open-event-legacy for the task and the export event as a zip endpoint in v2 for the API.

**Describe alternatives you've considered**
None



Discussion:

Resolving patch:
diff --git a/app/api/exports.py b/app/api/exports.py
--- a/app/api/exports.py
+++ b/app/api/exports.py
@@ -70,3 +70,23 @@ def event_export_task_base(event_id, settings):
     if path.startswith('/'):
         path = path[1:]
     return path
+
+
+@export_routes.route('/events/<string:event_identifier>/export/ical', methods=['GET'])
+@jwt_required()
+def export_event_ical(event_identifier):
+    if not event_identifier.isdigit():
+        event = db.session.query(Event).filter_by(identifier=event_identifier).first()
+        event_id = event.id
+    else:
+        event_id = event_identifier
+
+    from .helpers.tasks import export_ical_task
+
+    task = export_ical_task.delay(event_id)
+
+    create_export_job(task.id, event_id)
+
+    return jsonify(
+        task_url=url_for('tasks.celery_task', task_id=task.id)
+    )
diff --git a/app/api/helpers/ICalExporter.py b/app/api/helpers/ICalExporter.py
new file mode 100644
--- /dev/null
+++ b/app/api/helpers/ICalExporter.py
@@ -0,0 +1,62 @@
+import icalendar
+import pytz
+from flask import url_for
+from icalendar import Calendar, vCalAddress, vText
+from sqlalchemy import asc
+
+from app.models.event import Event as EventModel
+from app.models.session import Session
+
+
+class ICalExporter:
+    def __init__(self):
+        pass
+
+    @staticmethod
+    def export(event_id):
+        """Takes an event id and returns the event in iCal format"""
+
+        event = EventModel.query.get(event_id)
+
+        cal = Calendar()
+        cal.add('prodid', '-//fossasia//open-event//EN')
+        cal.add('version', '2.0')
+        cal.add('x-wr-calname', event.name)
+        cal.add('x-wr-caldesc', "Schedule for sessions at " + event.name)
+
+        tz = event.timezone or 'UTC'
+        tz = pytz.timezone(tz)
+
+        sessions = Session.query \
+            .filter_by(event_id=event_id) \
+            .filter_by(state='accepted') \
+            .filter(Session.deleted_at.is_(None)) \
+            .order_by(asc(Session.starts_at)).all()
+
+        for session in sessions:
+
+            if not (session and session.starts_at and session.ends_at):
+                continue
+
+            event_component = icalendar.Event()
+            event_component.add('summary', session.title)
+            event_component.add('uid', str(session.id) + "-" + event.identifier)
+            event_component.add('geo', (event.latitude, event.longitude))
+            event_component.add('location', session.microlocation.name or '' + " " + event.location_name)
+            event_component.add('dtstart', tz.localize(session.starts_at))
+            event_component.add('dtend', tz.localize(session.ends_at))
+            event_component.add('email', event.email)
+            event_component.add('description', session.short_abstract)
+            event_component.add('url', url_for('event_detail.display_event_detail_home',
+                                               identifier=event.identifier, _external=True))
+
+            for speaker in session.speakers:
+                # Ref: http://icalendar.readthedocs.io/en/latest/usage.html#file-structure
+                # can use speaker.email below but privacy reasons
+                attendee = vCalAddress('MAILTO:' + event.email if event.email else 'undefined@email.com')
+                attendee.params['cn'] = vText(speaker.name)
+                event_component.add('attendee', attendee)
+
+            cal.add_component(event_component)
+
+        return cal.to_ical()
diff --git a/app/api/helpers/tasks.py b/app/api/helpers/tasks.py
--- a/app/api/helpers/tasks.py
+++ b/app/api/helpers/tasks.py
@@ -3,6 +3,8 @@
 
 from app.api.helpers.utilities import strip_tags
 from app.views.celery_ import celery
+from flask import current_app
+import os
 
 """
 Define all API v2 celery tasks here
@@ -16,11 +18,14 @@
 from app.api.helpers.notification import send_notif_after_import, send_notif_after_export
 from app.api.helpers.db import safe_query
 from .import_helpers import update_import_job
-from app.models.event import Event
 from app.models.user import User
 from app.models import db
 from app.api.exports import event_export_task_base
 from app.api.imports import import_event_task_base
+from app.models.event import Event
+from app.api.helpers.ICalExporter import ICalExporter
+from app.api.helpers.storage import UploadedFile, upload, UPLOAD_PATHS
+from app.api.helpers.db import save_to_db
 
 
 @celery.task(name='send.email.post')
@@ -102,3 +107,28 @@ def import_event_task(self, email, file, source_type, creator_id):
         send_notif_after_import(user=user, error_text=str(e))
 
     return result
+
+
+@celery.task(base=RequestContextTask, name='export.ical', bind=True)
+def export_ical_task(self, event_id):
+    event = db.session.query(Event).filter_by(id=event_id).first()
+    try:
+        filedir = current_app.config.get('BASE_DIR') + '/static/uploads/temp/'
+        if not os.path.isdir(filedir):
+            os.makedirs(filedir)
+        filename = "ical.ics"
+        file_path = current_app.config['BASE_DIR'] + '/static/media/temp/' + filename
+        with open(file_path, "w") as temp_file:
+            temp_file.write(str(ICalExporter.export(event_id), 'utf-8'))
+        ical_file = UploadedFile(file_path=file_path, filename=filename)
+        event.ical_url = upload(ical_file, UPLOAD_PATHS['exports']['ical'].format(event_id=event_id))
+        save_to_db(event)
+        result = {
+            'download_url': event.ical_url
+        }
+
+    except Exception as e:
+        print(traceback.format_exc())
+        result = {'__error': True, 'result': str(e)}
+
+    return result


Test patch:
diff --git a/tests/hook_main.py b/tests/hook_main.py
--- a/tests/hook_main.py
+++ b/tests/hook_main.py
@@ -3402,7 +3402,7 @@ def event_stripe_authorization_get_detail(transaction):
 
 # ------------------------- Export -------------------------
 @hooks.before(
-    "Event Export > Start Event Export > Start a Task to Export an Event")
+    "Event Export > Start Event Export as Zip > Start a Task to Export an Event as Zip")
 def event_export_post(transaction):
     """
     :param transaction: