Skip to content

Commit

Permalink
Merge pull request #794 from Lunatic-Labs/SKIL-457
Browse files Browse the repository at this point in the history
SKIL-457
  • Loading branch information
aparriaran authored Dec 3, 2024
2 parents 44b206c + a45a1df commit ea70f3b
Show file tree
Hide file tree
Showing 8 changed files with 354 additions and 52 deletions.
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
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
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
56 changes: 55 additions & 1 deletion BackEndFlask/models/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -1133,4 +1133,58 @@ def is_admin_by_user_id(user_id: int) -> bool:

if is_admin[0][0]:
return True
return False
return False

def get_students_for_emailing(is_teams: bool, completed_at_id: int = None, at_id: int = None) -> tuple[dict[str],dict[str]]:
"""
Description:
Returns the needed data for emailing students who should be reciving the notification from
their professors. Note that it can also work for it you have a at_id or completed_at_id.
Parameters:
is_teams: <class 'bool'> (are we looking for students associated to a team?)
at_id: <class 'int'> (assessment Id)
completed_at_id: <class 'int'> (Completed assessment Id)
Returns:
tuple[dict[str],dict[str]] (The students information such as first_name, last_name, last_update, and email)
Exceptions:
TypeError if completed_id and at_id are None.
"""
# Note a similar function exists but its a select * query which hinders prefomance.

if at_id is None and completed_at_id is None:
raise TypeError("Both at_id and completed_at_id can not be <class 'NoneType'>.")

student_info = db.session.query(
CompletedAssessment.last_update,
User.first_name,
User.last_name,
User.email
)

if is_teams:
student_info = student_info.join(
TeamUser,
TeamUser.team_id == CompletedAssessment.team_id
).join(
User,
User.user_id == TeamUser.user_id
)
else:
student_info = student_info.join(
User,
User.user_id == CompletedAssessment.user_id
)

if at_id is not None:
student_info = student_info.filter(
CompletedAssessment.assessment_task_id == at_id
)
else:
student_info = student_info.filter(
CompletedAssessment.completed_assessment_id == completed_at_id
)

return student_info.all()
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import IconButton from '@mui/material/IconButton';
import VisibilityIcon from '@mui/icons-material/Visibility';
import { Box, Typography } from "@mui/material";
import CustomButton from "../../../Student/View/Components/CustomButton";
import { genericResourcePUT } from "../../../../utility";
import { genericResourcePOST, genericResourcePUT } from "../../../../utility";
import ResponsiveNotification from "../../../Components/SendNotification";
import CourseInfo from "../../../Components/CourseInfo";

Expand All @@ -23,6 +23,8 @@ class ViewCompleteIndividualAssessmentTasks extends Component {
showDialog: false,
notes: '',
notificationSent: false,
isSingleMsg: false,
compATId: null,

errors: {
notes:''
Expand All @@ -42,14 +44,16 @@ class ViewCompleteIndividualAssessmentTasks extends Component {
});
};

handleDialog = () => {
handleDialog = (isSingleMessage, singleCompletedAT) => {
this.setState({
showDialog: this.state.showDialog === false ? true : false,
})
isSingleMsg: isSingleMessage,
compATId: singleCompletedAT,
});
}

handleSendNotification = () => {
var notes = this.state.notes;
var notes = this.state.notes;

var navbar = this.props.navbar;

Expand All @@ -68,21 +72,39 @@ class ViewCompleteIndividualAssessmentTasks extends Component {

return;
}

genericResourcePUT(
`/assessment_task?assessment_task_id=${chosenAssessmentTask["assessment_task_id"]}&notification=${true}`,
this, JSON.stringify({
"notification_date": date,
"notification_message": notes
})
).then((result) => {
if (result !== undefined && result.errorMessage === null) {
this.setState({
showDialog: false,
notificationSent: date,
if(this.state.isSingleMsg) {
this.setState({isSingleMsg: false}, () => {
genericResourcePOST(
`/send_single_email?team=${false}&completed_assessment_id=${this.state.compATId}`,
this, JSON.stringify({
"notification_message": notes,
})
).then((result) => {
if(result !== undefined && result.errorMessage === null){
this.setState({
showDialog: false,
notificationSent: date,
});
}
});
}
});
});
} else {
genericResourcePUT(
`/mass_notification?assessment_task_id=${chosenAssessmentTask["assessment_task_id"]}&team=${false}`,
this, JSON.stringify({
"notification_message": notes,
"date" : date
})
).then((result) => {
if (result !== undefined && result.errorMessage === null) {
this.setState({
showDialog: false,
notificationSent: date,
});
}
});
}

};

render() {
Expand Down Expand Up @@ -255,7 +277,38 @@ class ViewCompleteIndividualAssessmentTasks extends Component {
}
}
}
}
},
{
name: "Student/Team Id",
label: "Message",
options: {
filter: false,
sort: false,
setCellHeaderProps: () => { return { align:"center", className:"button-column-alignment"}},
setCellProps: () => { return { align:"center", className:"button-column-alignment"} },
customBodyRender: (completedAssessmentId, completeAssessmentTasks) => {
const rowIndex = completeAssessmentTasks.rowIndex;
const completedATIndex = 5;
completedAssessmentId = completeAssessmentTasks.tableData[rowIndex][completedATIndex];
if (completedAssessmentId !== null) {
return (
<CustomButton
onClick={() => this.handleDialog(true, completedAssessmentId)}
label="Message"
align="center"
isOutlined={true}
disabled={notificationSent}
aria-label="Send individual messages"
/>
)
}else{
return(
<p variant='contained' align='center' > {''} </p>
)
}
}
}
},
];

const options = {
Expand Down Expand Up @@ -294,7 +347,7 @@ class ViewCompleteIndividualAssessmentTasks extends Component {

<CustomButton
label="Send Notification"
onClick={this.handleDialog}
onClick={() => this.handleDialog(false)}
isOutlined={false}
disabled={notificationSent}
aria-label="viewCompletedAssessmentSendNotificationButton"
Expand All @@ -303,13 +356,12 @@ class ViewCompleteIndividualAssessmentTasks extends Component {
</Box>

<Box className="table-spacing">
<CustomDataTable
data={completedAssessmentTasks ? completedAssessmentTasks : []}
columns={columns}
options={options}
/>
<CustomDataTable
data={completedAssessmentTasks ? completedAssessmentTasks : []}
columns={columns}
options={options}
/>
</Box>

</Box>
);
}
Expand Down
Loading

0 comments on commit ea70f3b

Please sign in to comment.