Repository: fossasia/open-event-server

Issue:
Add session status change validations
These are the allowed state changes for session:

Draft -> Pending -> Confirmed, Accepted, Rejected -> Cancelled, Withdrawn

Draft: Not ready to be reviewed, won't be shown to the organizer
Pending: Ready to be reviewed by the organizer and accepted or rejected
Confirmed/Accepted: Don't know what's the distinction between these two
Rejected: Session was rejected
Cancelled: Organizer can cancel the session even after it was accepted. Only an organizer can cancel a session
Withdrawn: A session can be withdrawn by speaker

Of course things will get more complicated with the logistics about actions on each status change.

Currently, there are 2 state change triggers:

- Accepted
  - With Email
  - Without Email
- Rejected
  - With Email
  - Without Email

What other state changes are required? 

Cancelled Email? Contents and who to send it to? Speaker and Attendees?
Withdrawn Email? Contents and who to send it to? Organizers and Attendees?

Action Possible States:

Withdraw: This action can only take place when action is confirmed or accepted, or both?
Cancelled: Same as above

We can start implementing the state change and actions once these queries are clear



Discussion:
**SESSION ACCEPTANCE FLOW**
Draft (Speaker is working on a submission, but has not submitted it. Organizer can see "draft", but cannot accept it.)
↓
Pending (Speaker submitted session. Organizer can see submission and change status to accepted or directly to confirmed)
↲ ↳
↲ 1) Accepted (Organizer has accepted the session. Usually at this stage the organizer communicates with the speaker about details. Some speakers cancel, other speakers confirm their session. Speaker sees in the system that their status has changed to "Accepted".)
↳ 2) Rejected: (Session is rejected by organizer. Speaker can see the status.)
↲ ↳
↲ 1) Confirmed (Organizers confirmed the session of the speakers. Speakers see the status "Confirmed".)
↳ 2) Canceled (Organizers cancel the speaker session for some reason. Speakers see the status "Canceled".)
↳ 3) Withdrawn (Alternatively organizers or speakers have the option to change to "Withdrawn" status, which is technically the same as canceled. However, it helps in the organization sometimes to indicate the reason for a status change. As a matter of fact canceled or withdrawn status can be chosen at any time as well in the flow.)

**SCHEDULER**
For the scheduler. Which session can be added to the scheduler?
* Accepted
* Confirmed
For now it seems to work out best if both "Accepted" and "Confirmed" sessions could be added to the scheduler.

Does this make sense?
> Withdraw: This action can only take place when action is confirmed or accepted, or both?

The speaker can withdraw at any time after he/she submitted. (It would not make sense to withdraw in draft mode.)
The organizer can also mark a session "Withdrawn" at any time after the speaker submitted it. Sometimes speakers only write an email and dont login to perform this action. Therefore organizers should also have the right to do that.

> Cancelled: Same as above

Sessions can only be cancelled after the status "Accepted" or "Confirmed". A session that is in pending mode cannot be canceled as it has not been in the acceptance list or on the scheduler.

>Rejected:

A session that was pending can be changed to "Rejected". Sometimes speakers cancel and slots become available. Rejected sessions can then be accepted and confirmed later. So, it should be possible to allow the organizer to change the mode from "Rejected" to "Accepted" or "Confirmed".
> Emails:

We need email actions for each state. Let's work this out in a separate issue and focus on the flow in this issue. If it is easier maybe just allow status changes without emails for now.
Everything else makes sense, but what operations should be allowed in withdrawn mode? Can a speaker put it back in pending mode? Can an organizer change status from withdrawn to accepted, confirmed, rejected?

What about organizer changing accepted, rejected, confirmed, canceled sessions to pending mode? Should it be allowed?
These are the possible transition states as of now. If anything needs to be changed, please tell

```python
SESSION_STATE_DICT = {
    'organizer': {
        'draft': {}, # No state change allowed
        'pending': {'withdrawn': True, 'accepted': True, 'rejected': True, 'confirmed': True},
        'accepted': {'withdrawn': True, 'rejected': True, 'confirmed': True, 'canceled': True},
        'confirmed': {'withdrawn': True, 'accepted': True, 'rejected': True, 'canceled': True},
        'rejected': {'withdrawn': True, 'accepted': True, 'confirmed': True},
        'canceled': {'withdrawn': True, 'accepted': True, 'rejected': True, 'confirmed': True},
        'withdrawn': {'accepted': True, 'rejected': True, 'confirmed': True}
    },
    'speaker': {
        'draft': {'pending': True},
        'pending': {'withdrawn': True},
        'accepted': {'withdrawn': True},
        'confirmed': {'withdrawn': True},
        'rejected': {'withdrawn': True},
        'canceled': {'withdrawn': True},
        'withdrawn': {'pending': True}
    }
}
```
> Everything else makes sense, but what operations should be allowed in withdrawn mode? Can a speaker put it back in pending mode? 

No, I think the most logic thing is: If someone withdraws the withdraw is final. A message that says "Are you sure you want to withdraw this sessions? This cannot be reverted." would be good.

> Can an organizer change status from withdrawn to accepted, confirmed, rejected?

I think withdrawing is final. Of course we need to see if I am wrong here. We will use this and see how it works, but I think withdrawing is final. If a speaker wants to submit again, he/she needs to fill it in again.
 
> What about organizer changing accepted, rejected, confirmed, canceled sessions to pending mode? Should it be allowed?

I am unsure about this. There might be cases, where this is needed. It is a bit odd though: Imagine a speaker receives an accepted session info and suddenly is set to rejected or pending. On the other hand, we should probably leave the decision how to run things to organizers.

So, yes, lets allow organizers to revert to pending status again or to set to rejected status as well.

Resolving patch:
diff --git a/app/api/routes.py b/app/api/routes.py
--- a/app/api/routes.py
+++ b/app/api/routes.py
@@ -165,6 +165,7 @@
     SessionListPost,
     SessionRelationshipOptional,
     SessionRelationshipRequired,
+    get_session_states,
 )
 from app.api.settings import SettingDetail
 from app.api.social_links import (
diff --git a/app/api/sessions.py b/app/api/sessions.py
--- a/app/api/sessions.py
+++ b/app/api/sessions.py
@@ -1,4 +1,4 @@
-from flask import request, g
+from flask import Blueprint, g, jsonify, request
 from flask_jwt_extended import current_user
 from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship
 from flask_rest_jsonapi.querystring import QueryStringManager as QSManager
@@ -29,6 +29,8 @@
 from app.models.user import User
 from app.settings import get_settings
 
+sessions_blueprint = Blueprint('sessions_blueprint', __name__, url_prefix='/v1/sessions')
+
 
 class SessionListPost(ResourceList):
     """
@@ -200,7 +202,7 @@ def query(self, view_kwargs):
             'rejected': True,
             'confirmed': True,
         },
-        'withdrawn': {'accepted': True, 'rejected': True, 'confirmed': True},
+        'withdrawn': {},  # Withdrawn is final
     },
     'speaker': {
         'draft': {'pending': True},
@@ -209,11 +211,16 @@ def query(self, view_kwargs):
         'confirmed': {'withdrawn': True},
         'rejected': {'withdrawn': True},
         'canceled': {'withdrawn': True},
-        'withdrawn': {'pending': True},
+        'withdrawn': {},  # Withdrawn is final
     },
 }
 
 
+@sessions_blueprint.route('/states')
+def get_session_states():
+    return jsonify(SESSION_STATE_DICT)
+
+
 class SessionDetail(ResourceDetail):
     """
     Session detail by id
diff --git a/app/instance.py b/app/instance.py
--- a/app/instance.py
+++ b/app/instance.py
@@ -150,6 +150,7 @@ def create_app():
         from app.api.auth import authorised_blueprint
         from app.api.admin_translations import admin_blueprint
         from app.api.orders import alipay_blueprint
+        from app.api.sessions import sessions_blueprint
         from app.api.settings import admin_misc_routes
         from app.api.server_version import info_route
         from app.api.custom.orders import ticket_blueprint
@@ -176,6 +177,7 @@ def create_app():
         app.register_blueprint(ticket_blueprint)
         app.register_blueprint(order_blueprint)
         app.register_blueprint(event_blueprint)
+        app.register_blueprint(sessions_blueprint)
 
         add_engine_pidguard(db.engine)
 


Test patch:
diff --git a/tests/all/integration/api/session/test_session_state_api.py b/tests/all/integration/api/session/test_session_state_api.py
--- a/tests/all/integration/api/session/test_session_state_api.py
+++ b/tests/all/integration/api/session/test_session_state_api.py
@@ -28,6 +28,8 @@ def get_session(db, user, event_owner=True, **kwargs):
     return session
 
 
+# TODO(Areeb): Remove some states from tests to reduce testing time.
+# Not every state change needs to be tested
 def _test_state_change(
     db, client, user, jwt, new_state, state, event_owner=True, error=True
 ):
@@ -121,10 +123,8 @@ def test_withdraw_speaker_allow(db, client, user, jwt, new_state, state):
 
 
 @pytest.mark.parametrize('state,new_state', states)
-def test_revert_withdraw_speaker_allow(db, client, user, jwt, new_state, state):
-    _test_state_change(
-        db, client, user, jwt, new_state, state=state, event_owner=False, error=False
-    )
+def test_revert_withdraw_speaker_disallow(db, client, user, jwt, new_state, state):
+    _test_state_change(db, client, user, jwt, new_state, state=state, event_owner=False)
 
 
 def test_withdraw_speaker_error(
@@ -147,8 +147,8 @@ def test_withdraw_organizer_allow(db, client, user, jwt, new_state, state):
 
 
 @pytest.mark.parametrize('state,new_state', states)
-def test_revert_withdraw_organizer_allow(db, client, user, jwt, new_state, state):
-    _test_state_change(db, client, user, jwt, new_state, state=state, error=False)
+def test_revert_withdraw_organizer_disallow(db, client, user, jwt, new_state, state):
+    _test_state_change(db, client, user, jwt, new_state, state=state)
 
 
 states = _create_permutations(['pending'], ['accepted', 'confirmed', 'rejected'])