Repository: fossasia/open-event-server

Issue:
Order Email: Add ics as attachment and standardize for calendar
When an email arrives from other event platforms Gmail and other email clients automatically provide an option to add it to the calendar. 

1. Please research and implement the structure of the email and possibly tags, so these emails provide the same functionality.
2. Add an .ics file as an attachment to order emails for the event as well.

Example how an event shows up on Gmail.

![Screenshot from 2021-01-19 10-53-13](https://user-images.githubusercontent.com/1583873/105017866-95a7a500-5a44-11eb-9cb6-060c726f7d62.png)



Discussion:
I would like to work on this issue.
I can't figure out how to set the `SMTP_HOST`, `SMTP_USERNAME` etc for development purposes, some help would be appreciated.
Set in settings table
Is there any easy way to do it other than using SQL queries in the Postgres Container?
Run frontend and change through it
I am unable to create a super user for it. I tried running `python manage.py initialize_db -c admin@example.com:password`, but still the user doesn't exist.

Even though the command executes without any exceptions.

![image](https://user-images.githubusercontent.com/31778302/106036536-7cb18a80-60fb-11eb-9ae0-6edd27892e61.png)

Should have run create_db.py while installing like mentioned in the docs

I have installed using docker.

Those docs do not mention to run `create_db.py` after installation.
Even when I run `python create_db.py example@example.com password`, it doesn't create a new user.

This is the output I get:
```
/data/app # python create_db.py example@example.com password
/opt/pysetup/.venv/lib/python3.8/site-packages/envparse.py:195: UserWarning: Could not any envfile.
  warnings.warn('Could not any envfile.')
/opt/pysetup/.venv/lib/python3.8/site-packages/envparse.py:195: UserWarning: Could not any envfile.
  warnings.warn('Could not any envfile.')
WARNING:app.instance:Using random secret "45ab1b0c5a8cb8cb2f04a98162b95cbad71d83987dd0b38c25754b3da3dcf95b" for development server. This is NOT recommended. Set proper SECRET_KEY in .env or environment variables
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
/data/app #
```

It should've logged "User updated" after creating a new super user.
or at least logged "DB Exception" if it is failing.
I don't understand why are you using docker?
It is easier to setup using docker. Is it not possible to use docker for development purpose?
No, it is for deployment only
Can we create another issue to do necessary changes in order to make docker available for development purposes too?
After solving this issue.
What's difficult in local installation?
There aren't any issues, but docker installation is easier.
And development harder
Upon some research through [here](https://github.com/sendgrid/sendgrid-python/issues/179#issuecomment-244974054), [here](https://stackoverflow.com/questions/45055188/calendar-invite-is-received-as-ics-file-in-outlook-laravel), and [here](https://github.com/PHPMailer/PHPMailer/issues/175#issuecomment-33802847). It looks like outlook and some other clients (excluding Gmail) needs `Method: Request;` in the file headers for it to display the "Add to calendar" and "RSVP" button.

Upon my testing, there is currently no way add that header using Sendgrid or marrow-mail (for SMTP) as of now. However Gmail displays the "Add to calendar" button just by attaching a `.ics` file.

Resolving patch:
diff --git a/app/api/helpers/files.py b/app/api/helpers/files.py
--- a/app/api/helpers/files.py
+++ b/app/api/helpers/files.py
@@ -13,6 +13,7 @@
 from weasyprint import HTML
 from xhtml2pdf import pisa
 
+from app.api.helpers.ICalExporter import ICalExporter
 from app.api.helpers.storage import UPLOAD_PATHS, UploadedFile, generate_hash, upload
 from app.models.image_size import ImageSizes
 from app.settings import get_settings
@@ -384,3 +385,28 @@ def create_save_pdf(
     os.remove(dest)
 
     return new_file
+
+
+def generate_ics_file(event_id, temp=True):
+    """
+    Generate the ICS file for the {event_id}
+    """
+
+    if temp:
+        filedir = os.path.join(
+            current_app.config.get('BASE_DIR'),
+            f'static/uploads/temp/{event_id}/',
+        )
+    else:
+        filedir = os.path.join(
+            current_app.config.get('BASE_DIR'), 'static/uploads/' + event_id + '/'
+        )
+
+    if not os.path.isdir(filedir):
+        os.makedirs(filedir)
+    filename = "ical.ics"
+    file_path = os.path.join(filedir, filename)
+    with open(file_path, "w") as temp_file:
+        temp_file.write(str(ICalExporter.export(event_id), 'utf-8'))
+
+    return file_path
diff --git a/app/api/helpers/mail.py b/app/api/helpers/mail.py
--- a/app/api/helpers/mail.py
+++ b/app/api/helpers/mail.py
@@ -1,6 +1,6 @@
 import base64
 import logging
-from datetime import datetime
+import os
 from itertools import groupby
 from typing import Dict
 
@@ -8,7 +8,7 @@
 from sqlalchemy.orm import joinedload
 
 from app.api.helpers.db import save_to_db
-from app.api.helpers.files import make_frontend_url
+from app.api.helpers.files import generate_ics_file, make_frontend_url
 from app.api.helpers.log import record_activity
 from app.api.helpers.system_mails import MAILS
 from app.api.helpers.utilities import get_serializer, str_generator, string_empty
@@ -329,6 +329,15 @@ def send_email_to_attendees(order):
     if current_app.config['ATTACH_ORDER_PDF']:
         attachments = [order.ticket_pdf_path, order.invoice_pdf_path]
 
+    event = order.event
+    ical_file_path = generate_ics_file(event.id)
+
+    if os.path.exists(ical_file_path):
+        if attachments is None:
+            attachments = [ical_file_path]
+        else:
+            attachments.append(ical_file_path)
+
     attendees = (
         TicketHolder.query.options(
             joinedload(TicketHolder.ticket), joinedload(TicketHolder.user)
@@ -338,7 +347,6 @@ def send_email_to_attendees(order):
     )
     email_group = groupby(attendees, lambda a: a.email)
 
-    event = order.event
     context = dict(
         order=order,
         settings=get_settings(),
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
@@ -26,8 +26,8 @@
     create_save_image_sizes,
     create_save_pdf,
     create_save_resized_image,
+    generate_ics_file,
 )
-from app.api.helpers.ICalExporter import ICalExporter
 from app.api.helpers.mail import check_smtp_config, send_export_mail, send_import_mail
 from app.api.helpers.notification import send_notif_after_export, send_notif_after_import
 from app.api.helpers.pentabarfxml import PentabarfExporter
@@ -115,16 +115,20 @@ def send_email_task_sendgrid(payload):
         message.reply_to = payload['reply_to']
 
     if payload['attachments'] is not None:
-        for attachment in payload['attachments']:
-            with open(attachment, 'rb') as f:
+        for filename in payload['attachments']:
+            with open(filename, 'rb') as f:
                 file_data = f.read()
                 f.close()
             encoded = base64.b64encode(file_data).decode()
             attachment = Attachment()
             attachment.file_content = FileContent(encoded)
-            attachment.file_type = FileType('application/pdf')
-            attachment.file_name = FileName(payload['to'])
             attachment.disposition = Disposition('attachment')
+            if filename.endswith('.pdf'):
+                attachment.file_type = FileType('application/pdf')
+                attachment.file_name = FileName(payload['to'])
+            elif filename.endswith('.ics'):
+                attachment.file_type = FileType('text/calendar')
+                attachment.file_name = FileName('ical.ics')
             message.add_attachment(attachment)
     sendgrid_client = SendGridAPIClient(get_settings()['sendgrid_key'])
     logging.info(
@@ -326,22 +330,9 @@ def export_ical_task(self, event_id, temp=True):
     event = safe_query(Event, 'id', event_id, 'event_id')
 
     try:
-        if temp:
-            filedir = os.path.join(
-                current_app.config.get('BASE_DIR'),
-                f'static/uploads/temp/{event_id}/',
-            )
-        else:
-            filedir = os.path.join(
-                current_app.config.get('BASE_DIR'), 'static/uploads/' + event_id + '/'
-            )
+        file_path = generate_ics_file(event_id, temp)
 
-        if not os.path.isdir(filedir):
-            os.makedirs(filedir)
-        filename = "ical.ics"
-        file_path = os.path.join(filedir, filename)
-        with open(file_path, "w") as temp_file:
-            temp_file.write(str(ICalExporter.export(event_id), 'utf-8'))
+        filename = os.path.basename(file_path)
         ical_file = UploadedFile(file_path=file_path, filename=filename)
         if temp:
             ical_url = upload(


Test patch: