diff --git a/BackEndFlask/controller/Routes/Completed_assessment_routes.py b/BackEndFlask/controller/Routes/Completed_assessment_routes.py index 1f664562f..b2ada7505 100644 --- a/BackEndFlask/controller/Routes/Completed_assessment_routes.py +++ b/BackEndFlask/controller/Routes/Completed_assessment_routes.py @@ -123,6 +123,26 @@ def get_all_completed_assessments(): except Exception as e: return create_bad_response(f"An error occurred retrieving all completed assessments: {e}", "completed_assessments", 400) +@bp.route('/completed_assessment', methods = ['GET']) +@jwt_required() +@bad_token_check() +@AuthCheck() +def get_completed_assessment_by_team_or_user_id(): + try: + completed_assessment_id = request.args.get("completed_assessment_id") + unit = request.args.get("unit") + if not completed_assessment_id: + return create_bad_response("No completed_assessment_id provided", "completed_assessments", 400) + + if unit == "team": + one_completed_assessment = get_completed_assessment_with_team_name(completed_assessment_id) + elif unit == "user": + one_completed_assessment = get_completed_assessment_with_user_name(completed_assessment_id) + else: + create_bad_response("Invalid unit provided", "completed_assessments", 400) + return create_good_response(completed_assessment_schema.dump(one_completed_assessment), 200, "completed_assessments") + except Exception as e: + return create_bad_response(f"An error occurred fetching a completed assessment: {e}", "completed_assessments", 400) @bp.route('/completed_assessment', methods = ['POST']) @jwt_required() diff --git a/BackEndFlask/controller/Routes/Team_routes.py b/BackEndFlask/controller/Routes/Team_routes.py index 95e187988..f8d8f4d11 100644 --- a/BackEndFlask/controller/Routes/Team_routes.py +++ b/BackEndFlask/controller/Routes/Team_routes.py @@ -8,8 +8,11 @@ get_team_by_course_id, create_team, get_teams_by_observer_id, - replace_team + replace_team, + delete_team ) +from models.assessment_task import get_assessment_tasks_by_team_id +from models.completed_assessment import completed_assessment_team_or_user_exists from models.team_user import * from controller.security.CustomDecorators import AuthCheck, bad_token_check from models.queries import ( @@ -94,14 +97,14 @@ def get_nonfull_adhoc_teams(): try: if request.args and request.args.get("assessment_task_id"): assessment_task_id = int(request.args.get("assessment_task_id")) - - valid_teams = [{"team_name": f"Team {team}", "team_id": team} for team in get_all_nonfull_adhoc_teams(assessment_task_id)] - - return create_good_response(valid_teams, 200, "teams") - + + valid_teams = [{"team_name": f"Team {team}", "team_id": team} for team in get_all_nonfull_adhoc_teams(assessment_task_id)] + + return create_good_response(valid_teams, 200, "teams") + except Exception as e: return create_bad_response(f"An error occurred getting nonfull adhoc teams {e}", "teams", 400) - + @bp.route('/team', methods = ['POST']) @jwt_required() @@ -167,6 +170,34 @@ def update_team_user_by_edit(): except Exception as e: return create_bad_response(f"An error occurred updating a team: {e}", "teams", 400) +@bp.route('/team', methods = ['DELETE']) +@jwt_required() +@bad_token_check() +@AuthCheck() +def delete_selected_teams(): + try: + if request.args and request.args.get("team_id"): + team_id = int(request.args.get("team_id")) + team = get_team(team_id) + if not team: + return create_bad_response("Team does not exist", "teams", 400) + + associated_tasks = completed_assessment_team_or_user_exists(team_id, user_id=None) + if associated_tasks is None: + associated_tasks = [] + if len(associated_tasks) > 0: + refetched_tasks = completed_assessment_team_or_user_exists(team_id, user_id=None) + if not refetched_tasks: + delete_team(team_id) + return create_good_response([], 200, "teams") + else: + return create_bad_response("Cannot delete team with associated tasks", "teams", 400) + else: + delete_team(team_id) + return create_good_response([], 200, "teams") + + except Exception as e: + return create_bad_response(f"An error occurred deleting a team: {e}", "teams", 400) class TeamSchema(ma.Schema): class Meta: diff --git a/BackEndFlask/models/assessment_task.py b/BackEndFlask/models/assessment_task.py index eca3cbde5..243e201df 100644 --- a/BackEndFlask/models/assessment_task.py +++ b/BackEndFlask/models/assessment_task.py @@ -63,7 +63,7 @@ def get_assessment_tasks_by_role_id(role_id): @error_log def get_assessment_tasks_by_team_id(team_id): - return db.session.query(AssessmentTask).join(Team, AssessmentTask.course_id == Team.course_id).filter( + db.session.query(AssessmentTask).join(Team, AssessmentTask.course_id == Team.course_id).filter( Team.team_id == team_id and ( @@ -72,7 +72,6 @@ def get_assessment_tasks_by_team_id(team_id): (AssessmentTask.due_date >= Team.date_created and AssessmentTask.due_date <= Team.active_until) ) ).all() - @error_log def get_assessment_task(assessment_task_id): one_assessment_task = AssessmentTask.query.filter_by(assessment_task_id=assessment_task_id).first() diff --git a/BackEndFlask/models/completed_assessment.py b/BackEndFlask/models/completed_assessment.py index 36644eeb5..bcd4fef93 100644 --- a/BackEndFlask/models/completed_assessment.py +++ b/BackEndFlask/models/completed_assessment.py @@ -53,6 +53,14 @@ def completed_assessment_exists(team_id, assessment_task_id, user_id): else: return CompletedAssessment.query.filter_by(user_id=user_id, assessment_task_id=assessment_task_id).first() +@error_log +def completed_assessment_team_or_user_exists(team_id, user_id): + if team_id is not None: + return CompletedAssessment.query.filter_by(team_id=team_id).all() + elif user_id is not None: + return CompletedAssessment.query.filter_by(user_id=user_id).all() + else: + return [] @error_log def create_completed_assessment(completed_assessment_data): diff --git a/FrontEndReact/src/View/Admin/View/ViewTeams/AdminViewTeams.js b/FrontEndReact/src/View/Admin/View/ViewTeams/AdminViewTeams.js index 14f5d98da..19869dfca 100644 --- a/FrontEndReact/src/View/Admin/View/ViewTeams/AdminViewTeams.js +++ b/FrontEndReact/src/View/Admin/View/ViewTeams/AdminViewTeams.js @@ -1,118 +1,152 @@ -import 'bootstrap/dist/css/bootstrap.css'; -import React, { Component } from 'react'; -import ErrorMessage from '../../../Error/ErrorMessage.js'; -import ViewTeams from './ViewTeams.js'; -import { genericResourceGET, parseUserNames } from '../../../../utility.js'; -import { Box, Button, Typography } from '@mui/material'; -import Loading from '../../../Loading/Loading.js'; -import SuccessMessage from '../../../Success/SuccessMessage.js'; - - +import "bootstrap/dist/css/bootstrap.css"; +import React, { Component } from "react"; +import ErrorMessage from "../../../Error/ErrorMessage.js"; +import ViewTeams from "./ViewTeams.js"; +import { genericResourceGET, + parseUserNames } from "../../../../utility.js"; +import { Box, Button, Typography } from "@mui/material"; +import Loading from "../../../Loading/Loading.js"; +import SuccessMessage from "../../../Success/SuccessMessage.js"; class AdminViewTeams extends Component { - constructor(props) { - super(props); - - this.state = { - errorMessage: null, - isLoaded: false, - teams: null, - users: null - } - } - - componentDidMount() { - var navbar = this.props.navbar; - var state = navbar.state; - var chosenCourse = state.chosenCourse; - - genericResourceGET(`/team?course_id=${chosenCourse["course_id"]}`, "teams", this); - - var url = ( - chosenCourse["use_tas"] ? - `/user?course_id=${chosenCourse["course_id"]}&role_id=4` : - `/user?uid=${chosenCourse["admin_id"]}` - ); - - genericResourceGET(url, "users", this); + constructor(props) { + super(props); + + this.state = { + errorMessage: null, + isLoaded: false, + teams: null, + users: null, + prevTeamsLength: 0, + successMessage: null + }; + } + + + fetchData = () => { + var navbar = this.props.navbar; + var state = navbar.state; + var chosenCourse = state.chosenCourse; + + genericResourceGET( + `/team?course_id=${chosenCourse["course_id"]}`, + "teams", + this, + ); + + var url = chosenCourse["use_tas"] + ? `/user?course_id=${chosenCourse["course_id"]}&role_id=4` + : `/user?uid=${chosenCourse["admin_id"]}`; + + genericResourceGET(url, "users", this); + }; + + componentDidMount() { + this.fetchData(); + } + + + componentDidUpdate(){ + if (this.state.teams && this.state.teams.length !== this.state.prevTeamsLength) { + this.setState({ prevTeamsLength: this.state.teams.length }); + this.fetchData(); } - - render() { - const { - errorMessage, - isLoaded, - teams, - users - } = this.state; - - var navbar = this.props.navbar; - - navbar.adminViewTeams.teams = teams; - navbar.adminViewTeams.users = users ? parseUserNames(users) : []; - - var setNewTab = navbar.setNewTab; - var setAddTeamTabWithUsers = navbar.setAddTeamTabWithUsers; - - if (errorMessage) { - return( -
- -
- ) - - } else if (!isLoaded || !teams || !users) { - return( - - ) - - } else { - return( - - {navbar.state.successMessage !== null && -
- -
- } - - Teams - - - - - - - - -
- ) - } + } + setErrorMessage = (message) => { + this.setState({ errorMessage: message }); + // Clear error message after 3 seconds + setTimeout(() => { + this.setState({ errorMessage: null }); + }, 3000); + } + + setSuccessMessage = (message) => { + this.setState({ successMessage: message }); + // Clear success message after 3 seconds + setTimeout(() => { + this.setState({ successMessage: null }); + }, 3000); + } + render() { + const { errorMessage, isLoaded, teams, users, successMessage } = this.state; + + var navbar = this.props.navbar; + + navbar.adminViewTeams.teams = teams; + navbar.adminViewTeams.users = users ? parseUserNames(users) : []; + + var setNewTab = navbar.setNewTab; + var setAddTeamTabWithUsers = navbar.setAddTeamTabWithUsers; + + if (errorMessage) { + return ( +
+ +
+ ); + } else if (!isLoaded || !teams || !users) { + return ; + } else { + return ( + + {successMessage && ( +
+ +
+ )} + {errorMessage && ( +
+ +
+ )} + + + Teams + + + + + + + + + +
+ ); } + } } -export default AdminViewTeams; \ No newline at end of file +export default AdminViewTeams; diff --git a/FrontEndReact/src/View/Admin/View/ViewTeams/ViewTeams.js b/FrontEndReact/src/View/Admin/View/ViewTeams/ViewTeams.js index 0ba428df9..71dae9bf1 100644 --- a/FrontEndReact/src/View/Admin/View/ViewTeams/ViewTeams.js +++ b/FrontEndReact/src/View/Admin/View/ViewTeams/ViewTeams.js @@ -1,13 +1,36 @@ -import React, { Component } from "react" -import 'bootstrap/dist/css/bootstrap.css'; -import IconButton from '@mui/material/IconButton'; -import EditIcon from '@mui/icons-material/Edit'; -import VisibilityIcon from '@mui/icons-material/Visibility'; +import React, { Component } from "react"; +import "bootstrap/dist/css/bootstrap.css"; +import IconButton from "@mui/material/IconButton"; +import EditIcon from "@mui/icons-material/Edit"; +import DeleteIcon from "@mui/icons-material/Delete"; +import VisibilityIcon from "@mui/icons-material/Visibility"; import CustomDataTable from "../../../Components/CustomDataTable.js"; +import { genericResourceDELETE } from "../../../../utility.js"; - - -class ViewTeams extends Component{ +class ViewTeams extends Component { + async deleteTeam(teamId) { + try { + const result = await genericResourceDELETE(`/team?team_id=${teamId}`, this, { + dest: "teams", + }); + if (result.errorMessage) { + throw new Error(result.errorMessage); + } + //window.alert("Team can be deleted") + this.props.onSuccess("Team deleted successfully"); + setTimeout(() => { + this.props.refreshData(); + }, 1000); + } catch (error) { + const errorMessage = error.message || "Cannot delete team. There are assessment task associated with this team."; + window.alert(errorMessage); + this.props.onError(errorMessage); + setTimeout(() => { + this.props.refreshData(); + }, 1000); + } + } + render() { var navbar = this.props.navbar; var adminViewTeams = navbar.adminViewTeams; @@ -23,59 +46,69 @@ class ViewTeams extends Component{ label: "Team Name", options: { filter: true, - setCellHeaderProps: () => { return { width:"20%"}}, - setCellProps: () => { return { width:"20%"} }, - } + setCellHeaderProps: () => { + return { width: "20%" }; + }, + setCellProps: () => { + return { width: "20%" }; + }, + }, }, { name: "observer_id", label: "Observer Name", options: { filter: true, - setCellHeaderProps: () => { return { width:"30%"}}, - setCellProps: () => { return { width:"30%"} }, + setCellHeaderProps: () => { + return { width: "30%" }; + }, + setCellProps: () => { + return { width: "30%" }; + }, customBodyRender: (observerId) => { - return( - observerId === chosenCourse["admin_id"] ? -

Admin

: + return observerId === chosenCourse["admin_id"] ? ( +

Admin

+ ) : (

{users[observerId]}

- ) - } - } + ); + }, + }, }, { name: "date_created", label: "Date Created", options: { filter: true, - setCellHeaderProps: () => { return { width:"20%"}}, - setCellProps: () => { return { width:"20%"} }, + setCellHeaderProps: () => { + return { width: "20%" }; + }, + setCellProps: () => { + return { width: "20%" }; + }, customBodyRender: (date) => { var year = ""; var month = ""; var day = ""; - for(var dateIndex = 0; dateIndex < date.length; dateIndex++) { - if(date[dateIndex]!=='-') { - if(dateIndex >= 0 && dateIndex < 4) { - year += date[dateIndex]; - } + for (var dateIndex = 0; dateIndex < date.length; dateIndex++) { + if (date[dateIndex] !== "-") { + if (dateIndex >= 0 && dateIndex < 4) { + year += date[dateIndex]; + } - if(dateIndex === 5 || dateIndex === 6) { - month += date[dateIndex]; - } + if (dateIndex === 5 || dateIndex === 6) { + month += date[dateIndex]; + } - if(dateIndex > 6 && dateIndex < date.length) { - day += date[dateIndex]; - } + if (dateIndex > 6 && dateIndex < date.length) { + day += date[dateIndex]; } + } } - return( -

{month+'/'+day+'/'+year}

- ) - } - } + return

{month + "/" + day + "/" + year}

; + }, + }, }, { name: "team_id", @@ -83,22 +116,73 @@ class ViewTeams extends Component{ options: { filter: false, sort: false, - setCellHeaderProps: () => { return { align:"center", width:"10%", className:"button-column-alignment"}}, - setCellProps: () => { return { align:"center", width:"10%", className:"button-column-alignment"} }, + setCellHeaderProps: () => { + return { + align: "center", + width: "10%", + className: "button-column-alignment", + }; + }, + setCellProps: () => { + return { + align: "center", + width: "10%", + className: "button-column-alignment", + }; + }, + customBodyRender: (teamId) => { + return ( + { + setAddTeamTabWithTeam(teams, teamId, users, "AddTeam"); + }} + aria-label="editTeamIconButton" + > + + + ); + }, + }, + }, + { + name: "team_id", + label: "Delete", + options: { + filter: false, + sort: false, + setCellHeaderProps: () => { + return { + align: "center", + width: "10%", + className: "button-column-alignment", + }; + }, + setCellProps: () => { + return { + align: "center", + width: "10%", + className: "button-column-alignment", + }; + }, customBodyRender: (teamId) => { - return( + return ( { - setAddTeamTabWithTeam(teams, teamId, users, "AddTeam");; - }} - aria-label="editTeamIconButton" + align="center" + onClick={() => { + if ( + window.confirm("Are you sure you want to delete this team?") + ) { + this.deleteTeam(teamId); + } + }} + aria-label="deleteTeamIconButton" > - + - ) - } - } + ); + }, + }, }, { name: "team_id", @@ -106,10 +190,22 @@ class ViewTeams extends Component{ options: { filter: false, sort: false, - setCellHeaderProps: () => { return { align:"center", width:"10%", className:"button-column-alignment"}}, - setCellProps: () => { return { align:"center", width:"10%", className:"button-column-alignment"} }, + setCellHeaderProps: () => { + return { + align: "center", + width: "10%", + className: "button-column-alignment", + }; + }, + setCellProps: () => { + return { + align: "center", + width: "10%", + className: "button-column-alignment", + }; + }, customBodyRender: (teamId) => { - return( + return ( { @@ -117,11 +213,11 @@ class ViewTeams extends Component{ }} aria-label="viewTeamsIconButton" > - - - ) - } - } + + + ); + }, + }, }, ]; @@ -133,16 +229,17 @@ class ViewTeams extends Component{ selectableRows: "none", selectableRowsHeader: false, responsive: "vertical", - tableBodyMaxHeight: "55vh" + tableBodyMaxHeight: "55vh", }; return ( - - ) + + ); } } -export default ViewTeams; \ No newline at end of file +export default ViewTeams; diff --git a/FrontEndReact/src/View/Navbar/AppState.js b/FrontEndReact/src/View/Navbar/AppState.js index df9ce9095..87fc14b0c 100644 --- a/FrontEndReact/src/View/Navbar/AppState.js +++ b/FrontEndReact/src/View/Navbar/AppState.js @@ -217,7 +217,6 @@ class AppState extends Component { }); } } - this.setAddTeamTabWithTeam = (teams, teamId, users, tab, addTeamAction) => { var newTeam = null;