Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SKIL-457 #794

Merged
merged 22 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ce7de7c
backend is now logging the feedback; a bit of work is need on the fro…
Nov 20, 2024
c4a24c3
Seems to now be saving and displaying correctly. I need to do some mo…
Nov 21, 2024
03b7fae
Merge branch 'master' into SKIL-457
Nov 21, 2024
4637573
removing debugging
Nov 21, 2024
1b86ab8
Merge branch 'master' into SKIL-457
Nov 21, 2024
b8d2e5a
changes nothing fixed
Nov 21, 2024
d232330
saving some changes to jump to another branch
Nov 22, 2024
f2930ac
new route created to begin teasing out the complex functionality stuf…
Nov 22, 2024
94aef66
figuring out some datetime things
Nov 23, 2024
b1fd67a
sending mass notifications for ats without teams works now.
Nov 25, 2024
5d53c77
The send notification button can now be used on both individuals and …
Nov 25, 2024
385ca17
I mean that it works on mass sending to individuals at or team ats
Nov 25, 2024
2f9eb8a
might want to rework the single email sender to make code reuse a thi…
Nov 26, 2024
40b53d6
added a new button and added the single message route in the backend.…
Nov 27, 2024
b48e4b8
front end button can now distinguish who is calling it. Need to work …
Nov 27, 2024
42144ce
There undefiened error i need to find
Nov 28, 2024
f216ad1
frontend msg buttons are now in working order
Nov 29, 2024
5df0548
modified query for finding students to email
Nov 29, 2024
0b56aff
backend and frontend single messaging are now hooked up
Nov 29, 2024
743efb1
updated documentation
Nov 30, 2024
e317e43
Merging from master
Nov 30, 2024
a45a1df
Allowing email to raise the proper errors again.
Dec 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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