Repository: fossasia/open-event-server

Issue:
Implement support for Export of other formats
There are a number of formats of events that are similar but still different. How can we export more formats?

Please
- [x] Find other formats
- [x] Implement import support for https://github.com/frab
  - [x] Pentabarf XML, example https://archive.fosdem.org/2016/schedule/xml (#2498)
  - [x] iCal, example https://archive.fosdem.org/2016/schedule/ical (#2499)
  - [x] xCal, example https://archive.fosdem.org/2016/schedule/xcal (#2500)

Related to: https://github.com/fossasia/open-event-orga-server/issues/2185



Discussion:
@mariobehling we can already export it in iCal format once the scheduling of the session is done for a particular event. I don't think we can export in a calendar format until the sessions are scheduled. Please share your views.

Meanwhile I will check out the other formats.

@SaptakS Yes, correct.


Resolving patch:
diff --git a/app/helpers/export.py b/app/helpers/export.py
--- a/app/helpers/export.py
+++ b/app/helpers/export.py
@@ -1,4 +1,5 @@
 import icalendar
+import pytz
 from flask import url_for
 from icalendar import Calendar
 from pentabarf.Event import Event
@@ -10,6 +11,7 @@
 from sqlalchemy import cast
 from sqlalchemy import func
 from app.models.event import Event as EventModel
+from xml.etree.ElementTree import Element, SubElement, Comment, tostring
 
 from app import db
 from app.models.session import Session
@@ -41,28 +43,41 @@ def export_as_pentabarf(event_id):
         event = DataGetter.get_event(event_id)
         diff = (event.end_time - event.start_time)
 
-        conference = Conference(title=event.name, start=event.start_time, end=event.end_time,
+        tz = event.timezone or 'UTC'
+        tz = pytz.timezone(tz)
+
+        conference = Conference(title=event.name, start=tz.localize(event.start_time), end=tz.localize(event.end_time),
                                 days=diff.days if diff.days > 0 else 1,
                                 day_change="00:00", timeslot_duration="00:15")
         dates = (db.session.query(cast(Session.start_time, DATE))
-                 .filter_by(event_id=event_id).order_by(asc(Session.start_time)).distinct().all())
+                 .filter_by(event_id=event_id)
+                 .filter_by(state='accepted')
+                 .filter(Session.in_trash is not True)
+                 .order_by(asc(Session.start_time)).distinct().all())
 
         for date in dates:
             date = date[0]
             day = Day(date=date)
             microlocation_ids = list(db.session.query(Session.microlocation_id)
                                      .filter(func.date(Session.start_time) == date)
+                                     .filter_by(state='accepted')
+                                     .filter(Session.in_trash is not True)
                                      .order_by(asc(Session.microlocation_id)).distinct())
             for microlocation_id in microlocation_ids:
                 microlocation_id = microlocation_id[0]
                 microlocation = DataGetter.get_microlocation(microlocation_id)
                 sessions = Session.query.filter_by(microlocation_id=microlocation_id) \
-                    .filter(func.date(Session.start_time) == date).order_by(asc(Session.start_time)).all()
+                    .filter(func.date(Session.start_time) == date) \
+                    .filter_by(state='accepted')\
+                    .filter(Session.in_trash is not True)\
+                    .order_by(asc(Session.start_time)).all()
+
                 room = Room(name=microlocation.name)
                 for session in sessions:
+
                     session_event = Event(id=session.id,
-                                          date=session.start_time,
-                                          start=session.start_time.strftime("%H:%M"),
+                                          date=tz.localize(session.start_time),
+                                          start=tz.localize(session.start_time).strftime("%H:%M"),
                                           duration=format_timedelta(session.end_time - session.start_time),
                                           track=session.track.name,
                                           abstract=session.short_abstract,
@@ -88,17 +103,117 @@ def export_as_pentabarf(event_id):
     @staticmethod
     def export_as_ical(event_id):
         """Takes an event id and returns the event in iCal format"""
+
+        event = EventModel.query.get(event_id)
+
         cal = Calendar()
-        event = icalendar.Event()
-        matching_event = EventModel.query.get(event_id)
-        event.add('summary', matching_event.name)
-        event.add('geo', (matching_event.latitude, matching_event.longitude))
-        event.add('location', matching_event.location_name)
-        event.add('dtstart', matching_event.start_time)
-        event.add('dtend', matching_event.end_time)
-        event.add('logo', matching_event.logo)
-        event.add('email', matching_event.email)
-        event.add('description', matching_event.description)
-        event.add('url', url_for('event_detail.display_event_detail_home', identifier=matching_event.identifier, _external=True))
-        cal.add_component(event)
+        cal.add('prodid', '-//fossasia//open-event//EN')
+        cal.add('version', '2.0')
+        cal.add('x-wr-caldesc', event.name)
+        cal.add('x-wr-calname', "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.in_trash is not True) \
+            .order_by(asc(Session.start_time)).all()
+
+        for session in sessions:
+
+            if session and session.start_time and session.end_time:
+                event_component = icalendar.Event()
+                event_component.add('summary', session.title)
+                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.start_time))
+                event_component.add('dtend', tz.localize(session.end_time))
+                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))
+
+                attendees = []
+                for speaker in session.speakers:
+                    attendees.append(speaker.name)
+                    event_component.add('attendee', attendees)
+
+                cal.add_component(event_component)
+
         return cal.to_ical()
+
+    @staticmethod
+    def export_as_xcal(event_id):
+        event = DataGetter.get_event(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 = event.name
+        cal_name_node = SubElement(v_calendar_node, 'x-wr-calname')
+        cal_name_node.text = "Schedule for sessions at " + event.name
+
+        sessions = Session.query\
+            .filter_by(event_id=event_id) \
+            .filter_by(state='accepted') \
+            .filter(Session.in_trash is not True) \
+            .order_by(asc(Session.start_time)).all()
+
+        for session in sessions:
+
+            if session and session.start_time and session.end_time:
+
+                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)
+
+                dtstart_node = SubElement(v_event_node, 'dtstart')
+                dtstart_node.text = tz.localize(session.start_time).isoformat()
+
+                dtend_node = SubElement(v_event_node, 'dtend')
+                dtend_node.text = tz.localize(session.end_time).isoformat()
+
+                duration_node = SubElement(v_event_node, 'duration')
+                duration_node.text = format_timedelta(session.end_time - session.start_time) + "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)
diff --git a/app/views/admin/models_views/export.py b/app/views/admin/models_views/export.py
--- a/app/views/admin/models_views/export.py
+++ b/app/views/admin/models_views/export.py
@@ -36,5 +36,14 @@ def pentabarf_export_view(self, event_id):
     def ical_export_view(self, event_id):
         response = make_response(ExportHelper.export_as_ical(event_id))
         response.headers["Content-Type"] = "text/calendar"
-        response.headers["Content-Disposition"] = "attachment; filename=event-calendar-" + str(event_id) + ".ics"
+        response.headers["Content-Disposition"] = "attachment; filename=calendar.ics"
         return response
+
+    @expose('/calendar.xcs')
+    @flask_login.login_required
+    def xcal_export_view(self, event_id):
+        response = make_response(ExportHelper.export_as_xcal(event_id))
+        response.headers["Content-Type"] = "text/calendar"
+        response.headers["Content-Disposition"] = "attachment; filename=calendar.xcs"
+        return response
+
diff --git a/app/views/public/event_detail.py b/app/views/public/event_detail.py
--- a/app/views/public/event_detail.py
+++ b/app/views/public/event_detail.py
@@ -119,6 +119,20 @@ def display_event_schedule_ical(self, identifier):
         response.headers["Content-Type"] = "text/calendar"
         return response
 
+    @expose('/<identifier>/schedule/calendar.xcs')
+    def display_event_schedule_xcal(self, identifier):
+        event = get_published_event_or_abort(identifier)
+        if not event.has_session_speakers:
+            abort(404)
+        accepted_sessions = DataGetter.get_sessions(event.id)
+        if not accepted_sessions or not event.schedule_published_on:
+            abort(404)
+
+        response = make_response(ExportHelper.export_as_xcal(event.id))
+        response.headers["Content-Type"] = "application/xml"
+        return response
+
+
     @expose('/<identifier>/cfs/', methods=('GET',))
     def display_event_cfs(self, identifier, via_hash=False):
         event = get_published_event_or_abort(identifier)


Test patch: