Repository: fossasia/open-event-server

Issue:
Add feature to export event as xCalendar
**Is your feature request related to a problem? Please describe.**
This feature is missing in version 2.

**Describe the solution you'd like**
Follow the steps of version 1.

**Describe alternatives you've considered**
\-


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
@@ -65,6 +65,28 @@ def export_download(event_id, path):
     return response
 
 
+@export_routes.route('/events/<string:event_identifier>/export/xcal', methods=['GET'])
+@jwt_required()
+def export_event_xcal(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_xcal_task
+
+    # queue task
+    task = export_xcal_task.delay(event_id)
+    # create Job
+    create_export_job(task.id, event_id)
+
+    return jsonify(
+        task_url=url_for('tasks.celery_task', task_id=task.id)
+    )
+
+
 def event_export_task_base(event_id, settings):
     path = export_event_json(event_id, settings)
     if path.startswith('/'):
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
@@ -24,6 +24,7 @@
 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.xcal import XCalExporter
 from app.api.helpers.storage import UploadedFile, upload, UPLOAD_PATHS
 from app.api.helpers.db import save_to_db
 
@@ -113,11 +114,11 @@ def import_event_task(self, email, file, source_type, creator_id):
 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/'
+        filedir = current_app.config.get('BASE_DIR') + '/static/uploads/temp/' + event_id + '/'
         if not os.path.isdir(filedir):
             os.makedirs(filedir)
         filename = "ical.ics"
-        file_path = current_app.config['BASE_DIR'] + '/static/media/temp/' + filename
+        file_path = filedir + 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)
@@ -132,3 +133,28 @@ def export_ical_task(self, event_id):
         result = {'__error': True, 'result': str(e)}
 
     return result
+
+
+@celery.task(base=RequestContextTask, name='export.xcal', bind=True)
+def export_xcal_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/' + event_id + '/'
+        if not os.path.isdir(filedir):
+            os.makedirs(filedir)
+        filename = "xcal.xcs"
+        file_path = filedir + filename
+        with open(file_path, "w") as temp_file:
+            temp_file.write(str(XCalExporter.export(event_id), 'utf-8'))
+        xcal_file = UploadedFile(file_path=file_path, filename=filename)
+        event.xcal_url = upload(xcal_file, UPLOAD_PATHS['exports']['xcal'].format(event_id=event_id))
+        save_to_db(event)
+        result = {
+            'download_url': event.xcal_url
+        }
+    except Exception as e:
+        print(traceback.format_exc())
+        result = {'__error': True, 'result': str(e)}
+
+    return result
diff --git a/app/api/helpers/xcal.py b/app/api/helpers/xcal.py
new file mode 100644
--- /dev/null
+++ b/app/api/helpers/xcal.py
@@ -0,0 +1,89 @@
+from xml.etree.ElementTree import Element, SubElement, tostring
+
+import pytz
+from flask import url_for
+from sqlalchemy import asc
+
+from app.models.session import Session
+from app.models.event import Event as Event
+
+
+class XCalExporter:
+    def __init__(self):
+        pass
+
+    @staticmethod
+    def export(event_id):
+        """Takes an event id and returns the event in xCal format"""
+
+        event = Event.query.get(event_id)
+
+        tz = event.timezone or 'UTC'
+        tz = pytz.timezone(tz)
+
+        i_calendar_node = Element('iCalendar')
+        i_calendar_node.set('xmlns:xCal', 'urn:ietf:params:xml:ns:xcal')
+        v_calendar_node = SubElement(i_calendar_node, 'vcalendar')
+        version_node = SubElement(v_calendar_node, 'version')
+        version_node.text = '2.0'
+        prod_id_node = SubElement(v_calendar_node, 'prodid')
+        prod_id_node.text = '-//fossasia//open-event//EN'
+        cal_desc_node = SubElement(v_calendar_node, 'x-wr-caldesc')
+        cal_desc_node.text = "Schedule for sessions at " + event.name
+        cal_name_node = SubElement(v_calendar_node, 'x-wr-calname')
+        cal_name_node.text = event.name
+
+        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 session and session.starts_at and session.ends_at:
+
+                v_event_node = SubElement(v_calendar_node, 'vevent')
+
+                method_node = SubElement(v_event_node, 'method')
+                method_node.text = 'PUBLISH'
+
+                uid_node = SubElement(v_event_node, 'uid')
+                uid_node.text = str(session.id) + "-" + event.identifier
+
+                dtstart_node = SubElement(v_event_node, 'dtstart')
+                dtstart_node.text = tz.localize(session.starts_at).isoformat()
+
+                dtend_node = SubElement(v_event_node, 'dtend')
+                dtend_node.text = tz.localize(session.ends_at).isoformat()
+
+                duration_node = SubElement(v_event_node, 'duration')
+                duration_node.text = str(session.ends_at - session.starts_at) + "00:00"
+
+                summary_node = SubElement(v_event_node, 'summary')
+                summary_node.text = session.title
+
+                description_node = SubElement(v_event_node, 'description')
+                description_node.text = session.short_abstract or 'N/A'
+
+                class_node = SubElement(v_event_node, 'class')
+                class_node.text = 'PUBLIC'
+
+                status_node = SubElement(v_event_node, 'status')
+                status_node.text = 'CONFIRMED'
+
+                categories_node = SubElement(v_event_node, 'categories')
+                categories_node.text = session.session_type.name if session.session_type else ''
+
+                url_node = SubElement(v_event_node, 'url')
+                url_node.text = url_for('event_detail.display_event_detail_home',
+                                        identifier=event.identifier, _external=True)
+
+                location_node = SubElement(v_event_node, 'location')
+                location_node.text = session.microlocation.name
+
+                for speaker in session.speakers:
+                    attendee_node = SubElement(v_event_node, 'attendee')
+                    attendee_node.text = speaker.name
+
+        return tostring(i_calendar_node)


Test patch: