Skip to content

Commit

Permalink
Refactor Code Base to Remove Getters and Setters (#20)
Browse files Browse the repository at this point in the history
* Create View, Delete, Edit Course Run Pages

* Add configs

* Add pages for Add Course and View Course Sessions

* Fix wrong HTTP request type

* Fix checkstyle

* Add READMEs

* Implement Attendance Pages

* Implement Assessment Pages

* Update Assessment pages

* Change spelling of enrolment

* Implement Enrolment pages

* Update Enrolment Page

* Update HTTP and Verification utils

* Improve optional parameter verification

* Update Enrolment Page

* Update Enrolment pages

* Implement Encryption/Decryption Page

* Implement Enrolment Test Cases

* Update encryption/decryption page

* Delete unused configuration files

* Fix bug with en-decryption page

* Refactor attendance-related classes

* Update test cases

* Refactor assessment-related classes

* Refactor enrolment-related classes

* Refactor courses-related classes and enums

* Implement email validation

* Update sidebar

* Update type annotations

* Fix bug with outputs for courses

* Implement email validation on frontend UI

* Fix bug with updating enrolment records

* Fix bug with logger file

* Fix bug with logger

* Rename application root folder

* Revert "Rename application root folder"

This reverts commit ce0e605.
  • Loading branch information
georgetayqy authored Jun 25, 2024
1 parent 3d45acf commit 5769827
Show file tree
Hide file tree
Showing 29 changed files with 8,057 additions and 4,807 deletions.
71 changes: 44 additions & 27 deletions revamped_application/Home.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
FILE_LOC = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.dirname(FILE_LOC))


# ignore E402 rule for this part only
import base64 # noqa: E402
import streamlit as st # noqa: E402
Expand All @@ -21,10 +20,9 @@
from core.system.logger import Logger # noqa: E402
from core.constants import Endpoints # noqa: E402


# initialise all variables and logger
init()
LOGGER = Logger(__name__)
LOGGER = Logger("Home")

# each new connection to the app cleans up the temp folder
start_schedule()
Expand All @@ -33,10 +31,10 @@

with st.sidebar:
st.header("View Configs")
st.markdown("Click the `Configs` button to view your loaded configurations at any time!")
if st.button("Configs", key="config_display"):
display_config()


st.image("assets/sf.png", width=200)
st.title("SSG API Sample Application")
st.markdown("Welcome to the SSG API Sample Application!\n\n"
Expand All @@ -59,24 +57,51 @@
st.markdown("Key in your UEN number, as well as your encryption keys, certificate key (`.pem`) and private key "
"(`.pem`) below!")

# UEN to be loaded outside of a form
uen = st.text_input(label="Enter in your UEN",
help="UEN stands for **Unique Entity Number**. It is used by the SSG API "
"to identify your organisation.",
value="" if st.session_state["uen"] is None else st.session_state["uen"])

if len(uen) > 0:
if not Validators.verify_uen(uen):
LOGGER.error("Invalid UEN provided!")
st.error("Invalid **UEN** provided!", icon="🚨")
else:
st.session_state["uen"] = uen.upper() # UENs only have upper case characters
LOGGER.info("UEN loaded!")
st.success("**UEN** loaded successfully!", icon="✅")

# AES Encryption Key to be loaded outside of a form
enc_key = st.text_input("Enter in your encryption key", type="password",
help="Refer to this [guide](https://developer.ssg-wsg.gov.sg/webapp/guides/"
"6gvz7gEnwU2dSIKPrTcXnq#authentication-types) for more info.",
value="" if st.session_state["encryption_key"] is None else st.session_state["encryption_key"])

if len(enc_key) > 0:
if not Validators.verify_aes_encryption_key(enc_key):
LOGGER.error("Invalid AES-256 encryption key provided!")
st.error("Invalid **AES-256 Encryption Key** provided!", icon="🚨")
else:
st.session_state["encryption_key"] = enc_key
LOGGER.info("Encryption Key loaded!")
st.success("**Encryption Key** loaded successfully!", icon="✅")

# Credentials need to be loaded in a form to ensure that the pair submitted is valid
with st.form(key="init_config"):
uen = st.text_input("Enter in your UEN", help="UEN stands for **Unique Entity Number**. It is used by the SSG API "
"to identify your organisation.")
enc_key = st.text_input("Enter in your encryption key", type="password",
help="Refer to this [guide](https://developer.ssg-wsg.gov.sg/webapp/guides/"
"6gvz7gEnwU2dSIKPrTcXnq#authentication-types) for more info.")
cert_pem = st.file_uploader("Upload your Certificate Key", type=["pem"], accept_multiple_files=False, key="cert")
key_pem = st.file_uploader("Upload your Private Key", type=["pem"], accept_multiple_files=False, key="key")
cert_pem = st.file_uploader(label="Upload your Certificate Key",
type=["pem"],
accept_multiple_files=False,
key="cert")
key_pem = st.file_uploader(label="Upload your Private Key",
type=["pem"],
accept_multiple_files=False,
key="key")

if st.form_submit_button("Load"):
LOGGER.info("Loading configurations...")
if not Validators.verify_uen(uen):
LOGGER.error("Invalid UEN provided!")
st.error("Invalid **UEN** provided!", icon="🚨")
elif not Validators.verify_aes_encryption_key(enc_key):
LOGGER.error("Invalid AES-256 encryption key provided!")
st.error("Invalid **AES-256 Encryption Key** provided!", icon="🚨")
elif cert_pem is None:

if cert_pem is None:
LOGGER.error("No valid Certificate key provided!")
st.error("**Certificate Key** is not provided!", icon="🚨")
elif key_pem is None:
Expand All @@ -102,15 +127,7 @@
raise AssertionError("Certificate and private key are not valid! Are you sure that you "
"have uploaded your certificates and private keys properly?")
LOGGER.info("Certificate and key verified!")

st.session_state["uen"] = uen.upper() # UENs only have upper case characters
LOGGER.info("UEN loaded!")

st.session_state["encryption_key"] = enc_key
LOGGER.info("Encryption Key loaded!")

st.success("**Configurations loaded successfully!**\n\nClick on the **`Configs`** button on the "
"Sidebar to view the configurations you have loaded up!", icon="✅")
st.success("**Certificate and Key loaded successfully!**\n\n", icon="✅")
except base64.binascii.Error:
LOGGER.error("Certificate/Private key is not encoded in Base64, or that the cert/key is invalid!")
st.error("Certificate or private key is invalid!", icon="🚨")
Expand Down
8 changes: 2 additions & 6 deletions revamped_application/core/abc/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ class AbstractRequestInfo(ABC):
different APIs
"""

@abstractmethod
def __init__(self, *args, **kwargs):
pass

@abstractmethod
def __repr__(self):
pass
Expand All @@ -54,8 +50,8 @@ def __str__(self):
pass

@abstractmethod
def validate(self) -> None | list[str]:
"""Validates the inputs passed to the APIs"""
def validate(self, **kwargs) -> tuple[list[str], list[str]]:
"""Validates the inputs passed to the APIs."""

pass

Expand Down
33 changes: 28 additions & 5 deletions revamped_application/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,36 @@ def __str__(self):
return f"{self.value[0]}: {self.value[1]}"


class OptionalSelector(Enum):
"""Enum representing a selector that permits empty responses."""

NIL = ("Select a value", None)
YES = ("Yes", True)
NO = ("No", False)

def __str__(self):
return self.value[0]


# ===== COURSE CONSTANTS ===== #
class Role(Enum):
"""Enum to represent the 2 roles a trainer may have."""

TRAINER = {
"id": 1,
"description": "Trainer"
"role": {
"id": 1,
"description": "Trainer"
}
}
ASSESSOR = {
"id": 2,
"description": "Assessor"
"role": {
"id": 2,
"description": "Assessor"
}
}

def __str__(self):
return self.value["description"]
return self.value["role"]["description"]


class ModeOfTraining(Enum):
Expand Down Expand Up @@ -115,6 +130,14 @@ def __str__(self):
return self.value[1]


class TrainerType(Enum):
EXISTING = "1"
NEW = "2"

def __str__(self):
return f"{self.value} - {self.name.title()}"


# ===== ASSESSMENT CONSTANTS ===== #
class Grade(Enum):
"""Enum represents the overall grade of an assessment."""
Expand Down
12 changes: 5 additions & 7 deletions revamped_application/core/courses/add_course_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@
import requests
import streamlit as st

from typing import Literal

from revamped_application.core.models.course_runs import AddRunInfo
from revamped_application.core.abc.abstract import AbstractRequest
from revamped_application.core.constants import HttpMethod
from revamped_application.core.constants import HttpMethod, OptionalSelector
from revamped_application.utils.http_utils import HTTPRequestBuilder


Expand All @@ -18,7 +16,7 @@ class AddCourseRun(AbstractRequest):

_TYPE: HttpMethod = HttpMethod.POST

def __init__(self, include_expired: Literal["Select a value", "Yes", "No"], runinfo: AddRunInfo):
def __init__(self, include_expired: OptionalSelector, runinfo: AddRunInfo):
super().__init__()
self.req: HTTPRequestBuilder = None
self._prepare(include_expired, runinfo)
Expand All @@ -33,7 +31,7 @@ def __str__(self):

return self.__repr__()

def _prepare(self, include_expired: Literal["Select a value", "Yes", "No"], runinfo: AddRunInfo) -> None:
def _prepare(self, include_expired: OptionalSelector, runinfo: AddRunInfo) -> None:
"""
Creates an HTTP POST request for creating/publishing a course run.
Expand All @@ -47,9 +45,9 @@ def _prepare(self, include_expired: Literal["Select a value", "Yes", "No"], runi
.with_header("Content-Type", "application/json") \

match include_expired:
case "Yes":
case OptionalSelector.YES:
self.req = self.req.with_param("includeExpiredCourses", "true")
case "No":
case OptionalSelector.NO:
self.req = self.req.with_param("includeExpiredCourses", "false")

self.req = self.req.with_body(runinfo.payload())
Expand Down
14 changes: 5 additions & 9 deletions revamped_application/core/courses/delete_course_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,16 @@

from revamped_application.core.abc.abstract import AbstractRequest
from revamped_application.core.models.course_runs import DeleteRunInfo
from revamped_application.core.constants import HttpMethod
from revamped_application.core.constants import HttpMethod, OptionalSelector
from revamped_application.utils.http_utils import HTTPRequestBuilder

from typing import Literal


class DeleteCourseRun(AbstractRequest):
"""Class used for deleting a course run."""

_TYPE: HttpMethod = HttpMethod.POST

def __init__(self, runId: str, include_expired: Literal["Select a value", "Yes", "No"],
delete_runinfo: DeleteRunInfo):
def __init__(self, runId: str, include_expired: OptionalSelector, delete_runinfo: DeleteRunInfo):
super().__init__()
self.req: HTTPRequestBuilder = None
self._prepare(runId, include_expired, delete_runinfo)
Expand All @@ -34,8 +31,7 @@ def __str__(self):

return self.__repr__()

def _prepare(self, runId: str, include_expired: Literal["Select a value", "Yes", "No"],
delete_runinfo: DeleteRunInfo) -> None:
def _prepare(self, runId: str, include_expired: OptionalSelector, delete_runinfo: DeleteRunInfo) -> None:
"""
Creates an HTTP POST request for deleting a course run.
Expand All @@ -50,9 +46,9 @@ def _prepare(self, runId: str, include_expired: Literal["Select a value", "Yes",
.with_header("Content-Type", "application/json")

match include_expired:
case "Yes":
case OptionalSelector.YES:
self.req = self.req.with_param("includeExpiredCourses", True)
case "No":
case OptionalSelector.NO:
self.req = self.req.with_param("includeExpiredCourses", False)

self.req = self.req.with_body(delete_runinfo.payload())
Expand Down
14 changes: 5 additions & 9 deletions revamped_application/core/courses/edit_course_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@
import requests
import streamlit as st

from typing import Literal

from revamped_application.core.models.course_runs import EditRunInfo
from revamped_application.core.abc.abstract import AbstractRequest
from revamped_application.core.constants import HttpMethod
from revamped_application.core.constants import HttpMethod, OptionalSelector
from revamped_application.utils.http_utils import HTTPRequestBuilder


Expand All @@ -18,8 +16,7 @@ class EditCourseRun(AbstractRequest):

_TYPE: HttpMethod = HttpMethod.POST

def __init__(self, runId: str, include_expired: Literal["Select a value", "Yes", "No"],
runinfo: EditRunInfo):
def __init__(self, runId: str, include_expired: OptionalSelector, runinfo: EditRunInfo):
super().__init__()
self.req: HTTPRequestBuilder = None
self._prepare(runId, include_expired, runinfo)
Expand All @@ -34,8 +31,7 @@ def __str__(self):

return self.__repr__()

def _prepare(self, runId: str, include_expired: Literal["Select a value", "Yes", "No"],
runinfo: EditRunInfo) -> None:
def _prepare(self, runId: str, include_expired: OptionalSelector, runinfo: EditRunInfo) -> None:
"""
Creates an HTTP POST request for editing the details of a course run.
Expand All @@ -50,9 +46,9 @@ def _prepare(self, runId: str, include_expired: Literal["Select a value", "Yes",
.with_header("Content-Type", "application/json")

match include_expired:
case "Yes":
case OptionalSelector.YES:
self.req = self.req.with_param("includeExpiredCourses", "true")
case "No":
case OptionalSelector.NO:
self.req = self.req.with_param("includeExpiredCourses", "false")

self.req = self.req.with_body(runinfo.payload())
Expand Down
12 changes: 5 additions & 7 deletions revamped_application/core/courses/view_course_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,15 @@

from revamped_application.utils.http_utils import HTTPRequestBuilder
from revamped_application.core.abc.abstract import AbstractRequest
from revamped_application.core.constants import HttpMethod

from typing import Literal
from revamped_application.core.constants import HttpMethod, OptionalSelector


class ViewCourseRun(AbstractRequest):
"""Class used for viewing course runs."""

_TYPE: HttpMethod = HttpMethod.GET

def __init__(self, runId: str, include_expired: Literal["Select a value", "Yes", "No"]):
def __init__(self, runId: str, include_expired: OptionalSelector):
super().__init__()
self.req: HTTPRequestBuilder = None
self._prepare(runId, include_expired)
Expand All @@ -32,7 +30,7 @@ def __str__(self):

return self.__repr__()

def _prepare(self, runId: str, include_expired: Literal["Select a value", "Yes", "No"]) -> None:
def _prepare(self, runId: str, include_expired: OptionalSelector) -> None:
"""
Creates an HTTP GET request for retrieving course runs by runId.
Expand All @@ -46,9 +44,9 @@ def _prepare(self, runId: str, include_expired: Literal["Select a value", "Yes",
.with_header("Content-Type", "application/json")

match include_expired:
case "Yes":
case OptionalSelector.YES:
self.req = self.req.with_param("includeExpiredCourses", "true")
case "No":
case OptionalSelector.NO:
self.req = self.req.with_param("includeExpiredCourses", "false")

def execute(self) -> requests.Response:
Expand Down
Loading

0 comments on commit 5769827

Please sign in to comment.