Skip to content

Commit

Permalink
Merge branch 'master' into SKIL-555
Browse files Browse the repository at this point in the history
  • Loading branch information
sah0017 committed Dec 21, 2024
2 parents 0ece360 + 3c6f4cd commit 8108cd7
Show file tree
Hide file tree
Showing 37 changed files with 1,063 additions and 255 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ package-lock.json
BackEndFlask/logs/all.log
BackEndFlask/models/hidden.py
BackEndFlask/logs/all.log
BackEndFlask/.env
.vscode

# Nginx configuration files
Expand Down
4 changes: 4 additions & 0 deletions BackEndFlask/.env
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ DEMO_ADMIN_PASSWORD=demo_admin
DEMO_TA_INSTRUCTOR_PASSWORD=demo_ta
DEMO_STUDENT_PASSWORD=demo_student
SECRET_KEY=Thisissupposedtobesecret!
MYSQL_HOST=localhost:3306
MYSQL_USER=rubricapp_admin
MYSQL_PASSWORD=ThisReallyNeedsToBeASecret1!
MYSQL_DATABASE=rubricapp
3 changes: 2 additions & 1 deletion BackEndFlask/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ Functions/test.py
Models/hidden.py
dump.rdb
BackEndFlaskVenv
Test
Test
.env
51 changes: 46 additions & 5 deletions BackEndFlask/Functions/exportCsv.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def _format(self) -> None:
self._singular[Csv_Data.USER_ID.value],
self._singular[Csv_Data.TEAM_ID.value],
self._at_id, category)

# Adding the other column names which are the ocs and sfi text.
headers += ["OC:" + i[0] for i in oc_sfi_per_category[0]] + ["SFI:" + i[0] for i in oc_sfi_per_category[1]]

Expand All @@ -234,14 +234,49 @@ def _format(self) -> None:
self._writer.writerow(row)
self._writer.writerow([''])

class Comments_Csv(Csv_Creation):
"""
Description: Singleton that creates a csv string of comments per category per student.
"""
def __init__(self, at_id: int) -> None:
"""
Parameters:
at_id: <class 'int'>
"""
super().__init__(at_id)

def _format(self) -> None:
"""
Description: Formats the data in the csv string.
Exceptions: None except what IO can rise.
"""
column_name = ["First Name"] + ["Last Name"] if not self._is_teams else ["Team Name"]

# Adding the column name. Noitice that done and comments is skipped since they are categories but are not important.
column_name += [i for i in self._singular[Csv_Data.JSON.value] if (i != "done" and i !="comments")]

self._writer.writerow(column_name)

row_info = None

# Notice that in the list comphrehensions done and comments are skiped since they are categories but dont hold relavent data.
for individual in self._completed_assessment_data:

row_info = [individual[Csv_Data.FIRST_NAME.value]] + [individual[Csv_Data.LAST_NAME.value]] if not self._is_teams else [individual[Csv_Data.TEAM_NAME.value]]

row_info += [individual[Csv_Data.JSON.value][category]["comments"] for category in individual[Csv_Data.JSON.value] if (category != "done" and category !="comments")]

self._writer.writerow(row_info)

class CSV_Type(Enum):
"""
Description: This is the enum for the different types of csv file formats the clients have requested.
"""
RATING_CSV = 0
OCS_SFI_CSV = 1
COMMENTS_CSV = 2

def create_csv_strings(at_id:int, type_csv=CSV_Type.OCS_SFI_CSV.value) -> str:
def create_csv_strings(at_id:int, type_csv:int=1) -> str:
"""
Description: Creates a csv file with the data in the format specified by type_csv.
Expand All @@ -254,10 +289,16 @@ def create_csv_strings(at_id:int, type_csv=CSV_Type.OCS_SFI_CSV.value) -> str:
Exceptions: None except the chance the database or IO calls raise one.
"""
try:
type_csv = CSV_Type(type_csv)
except:
raise ValueError("No type of csv is associated for the value passed.")
match type_csv:
case CSV_Type.RATING_CSV.value:
case CSV_Type.RATING_CSV:
return Ratings_Csv(at_id).return_csv_str()
case CSV_Type.OCS_SFI_CSV.value:
case CSV_Type.OCS_SFI_CSV:
return Ocs_Sfis_Csv(at_id).return_csv_str()
case CSV_Type.COMMENTS_CSV:
return Comments_Csv(at_id).return_csv_str()
case _:
return "No current class meets the deisred csv format. Error in create_csv_strings()."
return "Error in create_csv_strings()."
1 change: 0 additions & 1 deletion BackEndFlask/controller/Routes/Assessment_task_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,6 @@ def add_assessment_task():
)



@bp.route('/assessment_task', methods = ['PUT'])
@jwt_required()
@bad_token_check()
Expand Down
13 changes: 10 additions & 3 deletions BackEndFlask/controller/Routes/Completed_assessment_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ def get_all_completed_assessments():

get_assessment_task(assessment_task_id) # Trigger an error if not exists.
completed_assessments = get_completed_assessment_with_team_name(assessment_task_id)

if not completed_assessments:
completed_assessments = get_completed_assessment_with_user_name(assessment_task_id)

completed_count = get_completed_assessment_count(assessment_task_id)
result = [
{**completed_assessment_schema.dump(assessment), 'completed_count': completed_count}
Expand Down Expand Up @@ -155,17 +159,20 @@ def get_completed_assessment_by_team_or_user_id():
def add_completed_assessment():
try:
assessment_data = request.json

team_id = int(assessment_data["team_id"])
if (team_id == -1):
assessment_data["team_id"] = None
assessment_task_id = int(request.args.get("assessment_task_id"))
user_id = int(assessment_data["user_id"])
if (user_id == -1):
assessment_data["user_id"] = None

completed = completed_assessment_exists(team_id, assessment_task_id, user_id)

if completed:
completed = replace_completed_assessment(request.json, completed.completed_assessment_id)
completed = replace_completed_assessment(assessment_data, completed.completed_assessment_id)
else:
completed = create_completed_assessment(request.json)
completed = create_completed_assessment(assessment_data)

return create_good_response(completed_assessment_schema.dump(completed), 201, "completed_assessments")

Expand Down
8 changes: 4 additions & 4 deletions BackEndFlask/controller/Routes/Rating_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,16 @@ def student_view_feedback():
used to calculate lag time.
"""
try:
user_id = request.json["user_id"]
completed_assessment_id = request.json["completed_assessment_id"]
user_id = request.json.get("user_id")
completed_assessment_id = request.json.get("completed_assessment_id")

exists = check_feedback_exists(user_id, completed_assessment_id)
if exists:
return create_bad_response(f"Feedback already exists", "feedbacks", 409)

feedback_data = request.json
feedback_time = datetime.now()
feedback_data["feedback_time"] = feedback_time.strftime('%Y-%m-%dT%H:%M:%S')
string_format ='%Y-%m-%dT%H:%M:%S.%fZ'
feedback_data["feedback_time"] = datetime.now().strftime(string_format)

feedback = create_feedback(feedback_data)

Expand Down
3 changes: 2 additions & 1 deletion BackEndFlask/controller/Routes/Refresh_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from controller.Route_response import *
from flask_jwt_extended import jwt_required, create_access_token
from controller.security.CustomDecorators import AuthCheck, bad_token_check
import datetime

@bp.route('/refresh', methods=['POST'])
@jwt_required(refresh=True)
Expand All @@ -14,7 +15,7 @@ def refresh_token():
try:
user_id = int(request.args.get('user_id'))
user = user_schema.dump(get_user(user_id))
jwt = create_access_token([user_id])
jwt = create_access_token([user_id], fresh=datetime.timedelta(minutes=60), expires_delta=datetime.timedelta(minutes=60))
return create_good_response(user, 200, "user", jwt)
except:
return create_bad_response("Bad request: user_id must be provided", "user", 400)
Expand Down
130 changes: 130 additions & 0 deletions BackEndFlask/controller/Routes/notification_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#------------------------------------------------------------
# This is file holds the routes that handle sending
# notifications to individuals.
#
# Explanation of how Assessment.notification_sent will be
# used:
# If a completed assessments last update is after
# assessment.notification_sent, then they are
# considered to be new and elligble to send a msg
# to agian. Any more complex feture will require
# another table or trigger table to be added.
#------------------------------------------------------------

from flask import request
from flask_sqlalchemy import *
from controller import bp
from models.assessment_task import get_assessment_task, toggle_notification_sent_to_true
from controller.Route_response import *
from models.queries import get_students_for_emailing
from flask_jwt_extended import jwt_required
from models.utility import email_students_feedback_is_ready_to_view
import datetime

from controller.security.CustomDecorators import (
AuthCheck, bad_token_check,
admin_check
)

@bp.route('/mass_notification', methods = ['PUT'])
@jwt_required()
@bad_token_check()
@AuthCheck()
@admin_check()
def mass_notify_new_ca_users():
"""
Description:
This route will email individuals/teams of a related AT;
New/updated completed ATs will be notified upon successive
use.
Parameters(from the json):
assessment_task_id: <class 'str'>r (AT)
team: <class 'bool'> (is the at team based)
notification_message: <class 'str'> (message to send over in the email)
date: <class 'str'> (date to record things)
user_id: <class 'int'> (who is requested the route[The decorators need it])
Returns:
Bad or good response.
Exceptions:
None all should be caught and handled
"""
try:
at_id = int(request.args.get('assessment_task_id'))
is_teams = request.args.get('team') == "true"
msg_to_students = request.json["notification_message"]
date = request.json["date"]

# Raises InvalidAssessmentTaskID if non-existant AT.
at_time = get_assessment_task(at_id).notification_sent

# Lowest possible time for easier comparisons.
if at_time == None : at_time = datetime.datetime(1,1,1,0,0,0,0)

collection = get_students_for_emailing(is_teams, at_id=at_id)

left_to_notifiy = [singular_student for singular_student in collection if singular_student.last_update > at_time]

email_students_feedback_is_ready_to_view(left_to_notifiy, msg_to_students)

# Updating AT notification time
toggle_notification_sent_to_true(at_id, date)

return create_good_response(
"Message Sent",
201,
"Mass_notified"
)
except Exception as e:
return create_bad_response(
f"An error occurred emailing users: {e}", "mass_not_notified", 400
)


@bp.route('/send_single_email', methods = ['POST'])
@jwt_required()
@bad_token_check()
@AuthCheck()
@admin_check()
def send_single_email():
"""
Description:
This function sends emails to select single students or teams based on a completed_assessment_id.
The function was teased out from the above function to allow the addition of new features.
Parameters:
user_id: <class 'int'> (who requested the route {decorators uses it})
is_team: <class 'bool'> (is this a team or individual msg)
targeted_id: <class 'int'> (intended student/team for the message)
msg: <class 'str'> (The message the team or individual should recive)
Returns:
Good or bad Response
Exceptions:
None
"""

try:
is_teams = request.args.get('team') == "true"
completed_assessment_id = request.args.get('completed_assessment_id')
msg = request.json['notification_message']

collection = get_students_for_emailing(is_teams, completed_at_id= completed_assessment_id)

# Putting into a list as thats what the function wants.
left_to_notifiy = [singular_student for singular_student in collection]

email_students_feedback_is_ready_to_view(left_to_notifiy, msg)

return create_good_response(
"Message Sent",
201,
"Individual/Team notified"
)
except Exception as e:
return create_bad_response(
f"An error occurred emailing user/s: {e}", "Individual/Team not notified", 400
)
1 change: 1 addition & 0 deletions BackEndFlask/controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from controller.Routes import Feedback_routes
from controller.Routes import Refresh_route
from controller.Routes import Csv_routes
from controller.Routes import notification_routes
from controller.security import utility
from controller.security import CustomDecorators
from controller.security import blacklist
2 changes: 1 addition & 1 deletion BackEndFlask/controller/security/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# jwt expires in 15mins; refresh token expires in 30days
def create_tokens(user_i_d: any) -> 'tuple[str, str]':
with app.app_context():
jwt = create_access_token(str(user_i_d), fresh=datetime.timedelta(minutes=60))
jwt = create_access_token(str(user_i_d), fresh=datetime.timedelta(minutes=60), expires_delta=datetime.timedelta(minutes=60))
refresh = request.args.get('refresh_token')
if not refresh:
refresh = create_refresh_token(str(user_i_d))
Expand Down
22 changes: 15 additions & 7 deletions BackEndFlask/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
from models.tests import testing
from dotenv import load_dotenv
from flask import Flask
from flask_cors import CORS
import subprocess
load_dotenv()
import sys
import os
import subprocess
import re
import redis

Expand Down Expand Up @@ -78,13 +80,19 @@ def setup_cron_jobs():

# Initialize JWT
jwt = JWTManager(app)
account_db_path = os.getcwd() + os.path.join(os.path.sep, "core") + os.path.join(os.path.sep, "account.db")

MYSQL_HOST=os.getenv('MYSQL_HOST')

MYSQL_USER=os.getenv('MYSQL_USER')

MYSQL_PASSWORD=os.getenv('MYSQL_PASSWORD')

MYSQL_DATABASE=os.getenv('MYSQL_DATABASE')

db_uri = (f"mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}/{MYSQL_DATABASE}")

# Database configuration
account_db_path = os.path.join(os.getcwd(), "core", "account.db")
if os.path.exists(account_db_path):
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///./account.db'
else:
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///../instance/account.db'
app.config['SQLALCHEMY_DATABASE_URI'] = db_uri

db = SQLAlchemy(app)
ma = Marshmallow(app)
Expand Down
Loading

0 comments on commit 8108cd7

Please sign in to comment.