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
jcallison1 committed Dec 21, 2024
2 parents 576339d + eaea22e commit 412faa1
Show file tree
Hide file tree
Showing 40 changed files with 1,146 additions and 295 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()."
26 changes: 19 additions & 7 deletions BackEndFlask/Functions/teamBulkUpload.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from models.team_user import *
from models.user_course import *
from models.course import *
from models.queries import does_team_user_exist
from Functions.test_files.PopulationFunctions import xlsx_to_csv

from datetime import date
Expand Down Expand Up @@ -35,14 +36,14 @@ def __expect(lst: list[list[str]], cols: int | None = None) -> list[str]:
will modify the original list passed.
"""
hd: list[str] = lst.pop(0)

# Clean the row - specifically handle 'Unnamed:' columns and empty strings
cleaned = []
for x in hd:
stripped = x.strip()
if stripped and not stripped.startswith('Unnamed:'):
cleaned.append(stripped)

if cols is not None and len(cleaned) != cols:
raise TooManyColumns(1, cols, len(cleaned))
return cleaned
Expand Down Expand Up @@ -71,7 +72,16 @@ def __parse(lst: list[list[str]]) -> list[TBUTeam]:
raise EmptyTeamName if team_name == "" else EmptyTAEmail
teams.append(TBUTeam(team_name, ta, students))
students = []
current_state = EXPECT_TA

multiple_observers = True
if len(lst) > 2:
hd = __expect(lst)
lookAhead = __expect(lst)
lst.insert(0, lookAhead)
lst.insert(0, hd)
multiple_observers = len(hd) == len(lookAhead) == 1

current_state = EXPECT_TA if multiple_observers else EXPECT_TEAM
continue

# Process based on what type of row we're expecting
Expand Down Expand Up @@ -263,10 +273,12 @@ def __handle_student(student: TBUStudent, team_name: str, tainfo):
else:
set_inactive_status_of_user_to_active(user_course.user_course_id)

create_team_user({
"team_id": team_id,
"user_id": user_id
})
# Prevents duplicaition in the team user table.
if not does_team_user_exist(user_id, team_id):
create_team_user({
"team_id": team_id,
"user_id": user_id
})

tainfo = __handle_ta()

Expand Down
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
9 changes: 4 additions & 5 deletions BackEndFlask/controller/Routes/Rating_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ def get_student_individual_ratings():
@jwt_required()
@bad_token_check()
@AuthCheck()
@admin_check()
def student_view_feedback():
"""
Description:
Expand All @@ -67,16 +66,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.args.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
Loading

0 comments on commit 412faa1

Please sign in to comment.