Repository: fossasia/open-event-server

Issue:
Sessions: Add seperate resourcelist class, docs, permissions for direct endpoints
parent issue: #4101 


Discussion:

Resolving patch:
diff --git a/app/api/__init__.py b/app/api/__init__.py
--- a/app/api/__init__.py
+++ b/app/api/__init__.py
@@ -10,7 +10,8 @@
 from app.api.event_sub_topics import EventSubTopicList, EventSubTopicDetail, EventSubTopicRelationship
 from app.api.microlocations import MicrolocationList, MicrolocationListPost, MicrolocationDetail, \
     MicrolocationRelationshipRequired, MicrolocationRelationshipOptional
-from app.api.sessions import SessionList, SessionDetail, SessionRelationshipRequired, SessionRelationshipOptional
+from app.api.sessions import SessionList, SessionListPost, SessionDetail, SessionRelationshipRequired, \
+    SessionRelationshipOptional
 from app.api.speakers import SpeakerList, SpeakerDetail, SpeakerRelationshipRequired, SpeakerRelationshipOptional
 from app.api.social_links import SocialLinkList, SocialLinkDetail, SocialLinkRelationship
 from app.api.sponsors import SponsorList, SponsorDetail, SponsorRelationship
@@ -39,6 +40,7 @@
 from app.api.modules import ModuleDetail
 from app.api.custom_placeholders import CustomPlaceholderList, CustomPlaceholderDetail, CustomPlaceholderRelationship
 from app.api.activities import ActivityList, ActivityDetail
+
 # users
 api.route(UserList, 'user_list', '/users')
 api.route(VerifyUser, 'verify_user', '/users/<int:user_id>/verify')
@@ -192,6 +194,7 @@
           '/microlocations/<int:id>/relationships/event')
 
 # sessions
+api.route(SessionListPost, 'session_list_post', '/sessions')
 api.route(SessionList, 'session_list', '/sessions', '/events/<int:event_id>/sessions',
           '/events/<event_identifier>/sessions',
           '/tracks/<int:track_id>/sessions', '/session-types/<int:session_type_id>/sessions',
diff --git a/app/api/helpers/permission_manager.py b/app/api/helpers/permission_manager.py
--- a/app/api/helpers/permission_manager.py
+++ b/app/api/helpers/permission_manager.py
@@ -1,10 +1,11 @@
 from flask import current_app as app
 from flask_jwt import _jwt_required, current_identity
-from app.api.helpers.errors import ForbiddenError, NotFoundError
-from app.api.helpers.permissions import jwt_required
 from sqlalchemy.orm.exc import NoResultFound
 from flask import request
 
+from app.api.helpers.errors import ForbiddenError, NotFoundError
+from app.api.helpers.permissions import jwt_required
+from app.models.session import Session
 from app.models.event import Event
 
 
@@ -93,6 +94,35 @@ def is_coorganizer_or_user_itself(view, view_args, view_kwargs, *args, **kwargs)
     return ForbiddenError({'source': ''}, 'Co-organizer access is required.').respond()
 
 
+@jwt_required
+def is_speaker_for_session(view, view_args, view_kwargs, *args, **kwargs):
+    """
+    Allows admin and super admin access to any resource irrespective of id.
+    Otherwise the user can only access his/her resource.
+    """
+    user = current_identity
+    if user.is_admin or user.is_super_admin:
+        return view(*view_args, **view_kwargs)
+
+    if user.is_staff:
+        return view(*view_args, **view_kwargs)
+
+    try:
+        session = Session.query.filter(Session.id == view_kwargs['id']).one()
+    except NoResultFound:
+        return NotFoundError({'parameter': 'id'}, 'Session not found.').respond()
+
+    if user.is_organizer(session.event_id) or user.is_coorganizer(session.event_id):
+        return view(*view_args, **view_kwargs)
+
+    if session.speakers:
+        for speaker in session.speakers:
+            if speaker.user_id == user.id:
+                return view(*view_args, **view_kwargs)
+
+    return ForbiddenError({'source': ''}, 'Access denied.').respond()
+
+
 @jwt_required
 def is_registrar(view, view_args, view_kwargs, *args, **kwargs):
     """
@@ -182,6 +212,7 @@ def create_event(view, view_args, view_kwargs, *args, **kwargs):
     'user_event': user_event,
     'accessible_role_based_events': accessible_role_based_events,
     'auth_required': auth_required,
+    'is_speaker_for_session': is_speaker_for_session,
     'is_coorganizer_or_user_itself': is_coorganizer_or_user_itself,
     'create_event': create_event,
     'is_user_itself': is_user_itself
diff --git a/app/api/sessions.py b/app/api/sessions.py
--- a/app/api/sessions.py
+++ b/app/api/sessions.py
@@ -16,6 +16,8 @@
 from app.api.helpers.exceptions import UnprocessableEntity
 from app.api.helpers.db import safe_query
 from app.api.helpers.utilities import require_relationship
+from app.api.helpers.permission_manager import has_access
+from app.api.helpers.exceptions import ForbiddenException
 
 
 class SessionSchema(Schema):
@@ -46,6 +48,19 @@ def validate_date(self, data, original_data):
         if data['starts_at'] >= data['ends_at']:
             raise UnprocessableEntity({'pointer': '/data/attributes/ends-at'}, "ends-at should be after starts-at")
 
+        if 'state' in data:
+            if data['state'] is not 'draft' or not 'pending':
+                if not has_access('is_coorganizer', event_id=data['event']):
+                    return ForbiddenException({'source': ''}, 'Co-organizer access is required.')
+
+        if 'track' in data:
+            if not has_access('is_coorganizer', event_id=data['event']):
+                return ForbiddenException({'source': ''}, 'Co-organizer access is required.')
+
+        if 'microlocation' in data:
+            if not has_access('is_coorganizer', event_id=data['event']):
+                return ForbiddenException({'source': ''}, 'Co-organizer access is required.')
+
     id = fields.Str(dump_only=True)
     title = fields.Str(required=True)
     subtitle = fields.Str(allow_none=True)
@@ -61,7 +76,7 @@ def validate_date(self, data, original_data):
     audio_url = fields.Url(allow_none=True)
     signup_url = fields.Url(allow_none=True)
     state = fields.Str(validate=validate.OneOf(choices=["pending", "accepted", "confirmed", "rejected", "draft"]),
-                       allow_none=True)
+                       allow_none=True, default='draft')
     created_at = fields.DateTime(dump_only=True)
     deleted_at = fields.DateTime(dump_only=True)
     submitted_at = fields.DateTime(allow_none=True)
@@ -104,13 +119,24 @@ def validate_date(self, data, original_data):
         type_='speaker')
 
 
-class SessionList(ResourceList):
+class SessionListPost(ResourceList):
     """
-    List and create Sessions
+    List Sessions
     """
     def before_post(self, args, kwargs, data):
         require_relationship(['event'], data)
 
+    decorators = (api.has_permission('create_event'),)
+    schema = SessionSchema
+    data_layer = {'session': db.session,
+                  'model': Session}
+
+
+class SessionList(ResourceList):
+    """
+    List Sessions
+    """
+
     def query(self, view_kwargs):
         query_ = self.session.query(Session)
         if view_kwargs.get('track_id') is not None:
@@ -134,25 +160,13 @@ def query(self, view_kwargs):
             query_ = Session.query.filter(Session.speakers.any(id=speaker.id))
         return query_
 
-    def before_create_object(self, data, view_kwargs):
-        event = None
-        if view_kwargs.get('event_id'):
-            event = safe_query(self, Event, 'id', view_kwargs['event_id'], 'event_id')
-        elif view_kwargs.get('event_identifier'):
-            event = safe_query(self, Event, 'identifier', view_kwargs['event_identifier'], 'event_identifier')
-        if event:
-            data['event_id'] = event.id
-
     view_kwargs = True
-    decorators = (api.has_permission('is_coorganizer', fetch="event_id",
-                  fetch_as="event_id", methods="POST",
-                  check=lambda a: a.get('event_id') or a.get('event_identifier')), )
+    methods = ['GET']
     schema = SessionSchema
     data_layer = {'session': db.session,
                   'model': Session,
                   'methods': {
-                      'query': query,
-                      'before_create_object': before_create_object
+                      'query': query
                   }}
 
 
@@ -165,8 +179,7 @@ def before_get_object(self, view_kwargs):
             event = safe_query(self, Event, 'identifier', view_kwargs['event_identifier'], 'identifier')
             view_kwargs['event_id'] = event.id
 
-    decorators = (api.has_permission('is_coorganizer', fetch="event_id",
-                                     fetch_as="event_id", model=Session, methods="PATCH,DELETE"),)
+    decorators = (api.has_permission('is_speaker_for_session', methods="PATCH,DELETE"),)
     schema = SessionSchema
     data_layer = {'session': db.session,
                   'model': Session,


Test patch:
diff --git a/tests/hook_main.py b/tests/hook_main.py
--- a/tests/hook_main.py
+++ b/tests/hook_main.py
@@ -463,9 +463,6 @@ def session_post(transaction):
     :param transaction:
     :return:
     """
-    # Skip until docs for direct endpoints added
-    transaction['skip'] = True
-
     with stash['app'].app_context():
         event = EventFactoryBasic()
         db.session.add(event)