Repository: fossasia/open-event-server

Issue:
Deleted Event Restoration fails
```
{"errors": [{"status": 422, "source": {"pointer": "/data/attributes/starts-at"}, "title": "Unprocessable Entity", "detail": "starts-at should be after current date-time"}], "jsonapi": {"version": "1.0"}}
```


Discussion:
@CosmicCoder96 this check was implemented on event schema to avoid any event to have invalid dates.
What exactly do we need to fix this issue? As the check is on the schema, we cannot simply remove it for a few cases and apply it for other cases.
@shreyanshdwivedi How about we ignore the test for deleted events?
@uds5501 actually when the event is restored, a PATCH request is sent which updates the deleted-at to null again and so when these values are passed to schema, we cannot know if the event was deleted or not.
One thing we can do is update the event start and end time to current time while restoration.
Should I follow this approach? @iamareebjamal @uds5501 
Why can't we just only check dates if event is being created/ or dates are being updated
Actually the check is on schema and it is validated automatically for any update. @iamareebjamal 
So there is no way to see if the entity is being created or updated?
I haven't found a way to know if the event is being created or updated.
The data which is being passed to `validate_date` while restoration-
```
{
'schedule_published_on': datetime.datetime(1970, 1, 1, 0, 0, tzinfo=tzutc()), 
'starts_at': datetime.datetime(2019, 6, 16, 18, 30, tzinfo=tzutc()), 
'is_featured': False, 'state': 'published', 
'is_map_shown': True, 
'is_sessions_speakers_enabled': False, 
'can_pay_by_stripe': False, 
'longitude': 0.0, 
'coorganizers': [], 
'privacy': 'public', 
'location_name': 'A', 
'can_pay_by_omise': False, 
'is_sponsors_enabled': False, 
'is_event_online': False, 
'can_pay_by_cheque': False,
'track_organizers': [], 
'moderators': [], 
'organizers': [], 
'is_tax_enabled': False, 
'has_owner_info': False, 
'owner': '1', 
'name': 'A', 
'can_pay_by_alipay': False, 
'payment_currency': 'USD', 
'show_remaining_tickets': False, 
'is_ticketing_enabled': False, 
'registrars': [], 
'can_pay_onsite': False, 
'is_donation_enabled': False, 
'description': '', 'timezone': 'Asia/Kolkata', 
'latitude': 0.0, 
'can_pay_by_paypal': False, 
'ends_at': datetime.datetime(2019, 6, 20, 18, 30, tzinfo=tzutc()), 
'can_pay_by_bank': False, 'is_ticket_form_enabled': True
}
```
I didn't find any relevant key to know if the event is created or updated.
> I didn't find any relevant key to know if the event is created or
updated.
Before update, after update , before create, after create hooks? What do
you mean?

@CosmicCoder96 I meant that the schema check which is implemented, it has no way to know if the event is being created or updated. It works automatically and we don't call it explicitly.
Then please delete the schema check and add programmatic check

Resolving patch:
diff --git a/app/api/events.py b/app/api/events.py
--- a/app/api/events.py
+++ b/app/api/events.py
@@ -12,7 +12,7 @@
 from app.api.data_layers.EventCopyLayer import EventCopyLayer
 from app.api.helpers.db import save_to_db, safe_query
 from app.api.helpers.events import create_custom_forms_for_attendees
-from app.api.helpers.exceptions import ForbiddenException, ConflictException
+from app.api.helpers.exceptions import ForbiddenException, ConflictException, UnprocessableEntity
 from app.api.helpers.permission_manager import has_access
 from app.api.helpers.utilities import dasherize
 from app.api.schema.events import EventSchemaPublic, EventSchema
@@ -88,6 +88,26 @@ def validate_event(user, modules, data):
                                 "Online Event does not have any locaton")
 
 
+def validate_date(event, data):
+    if event:
+        if 'starts_at' not in data:
+            data['starts_at'] = event.starts_at
+
+        if 'ends_at' not in data:
+            data['ends_at'] = event.ends_at
+
+    if not data.get('starts_at') or not data.get('ends_at'):
+        raise UnprocessableEntity({'pointer': '/data/attributes/date'},
+                                  "enter required fields starts-at/ends-at")
+
+    if data['starts_at'] >= data['ends_at']:
+        raise UnprocessableEntity({'pointer': '/data/attributes/ends-at'},
+                                  "ends-at should be after starts-at")
+
+    if datetime.timestamp(data['starts_at']) <= datetime.timestamp(datetime.now()):
+        raise UnprocessableEntity({'pointer': '/data/attributes/starts-at'},
+                                  "starts-at should be after current date-time")
+
 class EventList(ResourceList):
     def before_get(self, args, kwargs):
         """
@@ -155,6 +175,7 @@ def before_post(self, args, kwargs, data=None):
         user = User.query.filter_by(id=kwargs['user_id']).first()
         modules = Module.query.first()
         validate_event(user, modules, data)
+        validate_date(None, data)
 
     def after_create_object(self, event, data, view_kwargs):
         """
@@ -471,6 +492,9 @@ def before_update_object(self, event, data, view_kwargs):
         :param view_kwargs:
         :return:
         """
+        if data.get('starts_at') != event.starts_at or data.get('ends_at') != event.ends_at:
+            validate_date(event, data)
+
         if has_access('is_admin') and data.get('deleted_at') != event.deleted_at:
             if len(event.orders) != 0:
                 raise ForbiddenException({'source': ''}, "Event associated with orders cannot be deleted")
diff --git a/app/api/schema/events.py b/app/api/schema/events.py
--- a/app/api/schema/events.py
+++ b/app/api/schema/events.py
@@ -24,32 +24,6 @@ class Meta:
         self_view_many = 'v1.event_list'
         inflect = dasherize
 
-    @validates_schema(pass_original=True)
-    def validate_date(self, data, original_data):
-        if 'id' in original_data['data']:
-            try:
-                event = Event.query.filter_by(id=original_data['data']['id']).one()
-            except NoResultFound:
-                raise ObjectNotFound({'source': 'data/id'}, "Event id not found")
-
-            if 'starts_at' not in data:
-                data['starts_at'] = event.starts_at
-
-            if 'ends_at' not in data:
-                data['ends_at'] = event.ends_at
-
-        if 'starts_at' not in data or 'ends_at' not in data:
-            raise UnprocessableEntity({'pointer': '/data/attributes/date'},
-                                      "enter required fields starts-at/ends-at")
-
-        if data['starts_at'] >= data['ends_at']:
-            raise UnprocessableEntity({'pointer': '/data/attributes/ends-at'},
-                                      "ends-at should be after starts-at")
-
-        if datetime.timestamp(data['starts_at']) <= datetime.timestamp(datetime.now()):
-            raise UnprocessableEntity({'pointer': '/data/attributes/starts-at'},
-                                      "starts-at should be after current date-time")
-
     @validates_schema(pass_original=True)
     def validate_timezone(self, data, original_data):
         if 'id' in original_data['data']:


Test patch:
diff --git a/tests/all/integration/api/validation/test_events.py b/tests/all/integration/api/validation/test_events.py
deleted file mode 100644
--- a/tests/all/integration/api/validation/test_events.py
+++ /dev/null
@@ -1,70 +0,0 @@
-import unittest
-from datetime import datetime
-from pytz import timezone
-
-from app import current_app as app
-from tests.all.integration.utils import OpenEventTestCase
-from app.api.helpers.exceptions import UnprocessableEntity
-from app.api.schema.events import EventSchema
-from app.factories.event import EventFactoryBasic
-from app.models import db
-from app.api.helpers.db import save_to_db
-from tests.all.integration.setup_database import Setup
-
-
-class TestEventValidation(OpenEventTestCase):
-    def setUp(self):
-        self.app = Setup.create_app()
-
-    def test_date_pass(self):
-        """
-        Events Validate Date - Tests if the function runs without an exception
-        :return:
-        """
-        schema = EventSchema()
-        original_data = {
-            'data': {}
-        }
-        data = {
-            'starts_at': datetime(2099, 8, 4, 12, 30, 45).replace(tzinfo=timezone('UTC')),
-            'ends_at': datetime(2099, 9, 4, 12, 30, 45).replace(tzinfo=timezone('UTC'))
-        }
-        EventSchema.validate_date(schema, data, original_data)
-
-    def test_date_start_gt_end(self):
-        """
-        Events Validate Date - Tests if exception is raised when ends_at is before starts_at
-        :return:
-        """
-        schema = EventSchema()
-        original_data = {
-            'data': {}
-        }
-        data = {
-            'starts_at': datetime(2099, 9, 4, 12, 30, 45).replace(tzinfo=timezone('UTC')),
-            'ends_at': datetime(2099, 8, 4, 12, 30, 45).replace(tzinfo=timezone('UTC'))
-        }
-        with self.assertRaises(UnprocessableEntity):
-            EventSchema.validate_date(schema, data, original_data)
-
-    def test_date_db_populate(self):
-        """
-        Events Validate Date - Tests if validation works on values stored in db and not given in 'data'
-        :return:
-        """
-        with app.test_request_context():
-            schema = EventSchema()
-            obj = EventFactoryBasic()
-            save_to_db(obj)
-
-            original_data = {
-                'data': {
-                    'id': 1
-                }
-            }
-            data = {}
-            EventSchema.validate_date(schema, data, original_data)
-
-
-if __name__ == '__main__':
-    unittest.main()