diff --git a/.gitignore b/.gitignore index f3e6985a7..9109da4a7 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/BackEndFlask/.env b/BackEndFlask/.env index 70fad1b9e..1b5d551e9 100644 --- a/BackEndFlask/.env +++ b/BackEndFlask/.env @@ -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 diff --git a/BackEndFlask/.gitignore b/BackEndFlask/.gitignore index 93272c465..b73b77005 100644 --- a/BackEndFlask/.gitignore +++ b/BackEndFlask/.gitignore @@ -17,4 +17,5 @@ Functions/test.py Models/hidden.py dump.rdb BackEndFlaskVenv -Test \ No newline at end of file +Test +.env \ No newline at end of file diff --git a/BackEndFlask/Functions/exportCsv.py b/BackEndFlask/Functions/exportCsv.py index 78a981c12..1e0476524 100644 --- a/BackEndFlask/Functions/exportCsv.py +++ b/BackEndFlask/Functions/exportCsv.py @@ -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]] @@ -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: + """ + 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. @@ -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()." \ No newline at end of file + return "Error in create_csv_strings()." \ No newline at end of file diff --git a/BackEndFlask/controller/Routes/Assessment_task_routes.py b/BackEndFlask/controller/Routes/Assessment_task_routes.py index 814b6a313..934d6bc27 100644 --- a/BackEndFlask/controller/Routes/Assessment_task_routes.py +++ b/BackEndFlask/controller/Routes/Assessment_task_routes.py @@ -192,7 +192,6 @@ def add_assessment_task(): ) - @bp.route('/assessment_task', methods = ['PUT']) @jwt_required() @bad_token_check() diff --git a/BackEndFlask/controller/Routes/Completed_assessment_routes.py b/BackEndFlask/controller/Routes/Completed_assessment_routes.py index 7013c362d..ced1c1ef3 100644 --- a/BackEndFlask/controller/Routes/Completed_assessment_routes.py +++ b/BackEndFlask/controller/Routes/Completed_assessment_routes.py @@ -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} @@ -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") diff --git a/BackEndFlask/controller/Routes/Rating_routes.py b/BackEndFlask/controller/Routes/Rating_routes.py index 74b1bbc26..0b62faa22 100644 --- a/BackEndFlask/controller/Routes/Rating_routes.py +++ b/BackEndFlask/controller/Routes/Rating_routes.py @@ -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) diff --git a/BackEndFlask/controller/Routes/Refresh_route.py b/BackEndFlask/controller/Routes/Refresh_route.py index ce31aad9b..ebfacb068 100644 --- a/BackEndFlask/controller/Routes/Refresh_route.py +++ b/BackEndFlask/controller/Routes/Refresh_route.py @@ -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) @@ -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) diff --git a/BackEndFlask/controller/Routes/notification_routes.py b/BackEndFlask/controller/Routes/notification_routes.py new file mode 100644 index 000000000..a651d31d9 --- /dev/null +++ b/BackEndFlask/controller/Routes/notification_routes.py @@ -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: r (AT) + team: (is the at team based) + notification_message: (message to send over in the email) + date: (date to record things) + user_id: (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: (who requested the route {decorators uses it}) + is_team: (is this a team or individual msg) + targeted_id: (intended student/team for the message) + msg: (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 + ) \ No newline at end of file diff --git a/BackEndFlask/controller/__init__.py b/BackEndFlask/controller/__init__.py index f438f9180..8752a90c7 100644 --- a/BackEndFlask/controller/__init__.py +++ b/BackEndFlask/controller/__init__.py @@ -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 \ No newline at end of file diff --git a/BackEndFlask/controller/security/utility.py b/BackEndFlask/controller/security/utility.py index 5825203e9..f849ccaef 100644 --- a/BackEndFlask/controller/security/utility.py +++ b/BackEndFlask/controller/security/utility.py @@ -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)) diff --git a/BackEndFlask/core/__init__.py b/BackEndFlask/core/__init__.py index 0b62c26c5..d70f6939a 100644 --- a/BackEndFlask/core/__init__.py +++ b/BackEndFlask/core/__init__.py @@ -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 @@ -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) diff --git a/BackEndFlask/dbcreate.py b/BackEndFlask/dbcreate.py index 8b7e47880..ebed9fc9e 100644 --- a/BackEndFlask/dbcreate.py +++ b/BackEndFlask/dbcreate.py @@ -35,9 +35,21 @@ except Exception as e: print(f"[dbcreate] an error ({e}) occured with db.create_all()") print("[dbcreate] exiting...") - os.abort() + raise e print("[dbcreate] successfully created new db") time.sleep(sleep_time) + if (get_roles().__len__() == 0): + print("[dbcreate] attempting to load existing roles...") + time.sleep(sleep_time) + load_existing_roles() + print("[dbcreate] successfully loaded existing roles") + time.sleep(sleep_time) + if(get_users().__len__()==0): + print("[dbcreate] attempting to load SuperAdminUser...") + time.sleep(sleep_time) + load_SuperAdminUser() + print("[dbcreate] successfully loaded SuperAdminUser") + time.sleep(sleep_time) if(get_rubrics().__len__()==0): print("[dbcreate] attempting to load existing rubrics...") time.sleep(sleep_time) @@ -61,18 +73,6 @@ time.sleep(sleep_time) load_existing_suggestions() print("[dbcreate] successfully loaded existing suggestions") - if(get_roles().__len__()==0): - print("[dbcreate] attempting to load existing roles...") - time.sleep(sleep_time) - load_existing_roles() - print("[dbcreate] successfully loaded existing roles") - time.sleep(sleep_time) - if(get_users().__len__()==0): - print("[dbcreate] attempting to load SuperAdminUser...") - time.sleep(sleep_time) - load_SuperAdminUser() - print("[dbcreate] successfully loaded SuperAdminUser") - time.sleep(sleep_time) if len(sys.argv) == 2 and sys.argv[1]=="demo": if(get_users().__len__()==1): print("[dbcreate] attempting to load demo Admin...") @@ -130,17 +130,17 @@ load_demo_admin_assessment_task() print("[dbcreate] successfully loaded demo AssessmentTask") time.sleep(sleep_time) - if(get_feedback().__len__()==0): - print("[dbcreate] attempting to load demo Feedback...") - time.sleep(sleep_time) - load_demo_feedback() - print("[dbcreate] successfully loaded demo Feedback") - time.sleep(sleep_time) if (get_completed_assessments().__len__() == 0): print("[dbcreate] attempting to load demo completed assessments...") time.sleep(sleep_time) load_demo_completed_assessment() print("[dbcreate] successfully loaded demo completed assessments") time.sleep(sleep_time) + if(get_feedback().__len__()==0): + print("[dbcreate] attempting to load demo Feedback...") + time.sleep(sleep_time) + load_demo_feedback() + print("[dbcreate] successfully loaded demo Feedback") + time.sleep(sleep_time) - print("[dbcreate] exiting...") \ No newline at end of file + print("[dbcreate] exiting...") diff --git a/BackEndFlask/env/.env.production b/BackEndFlask/env/.env.production index eb4bec572..d9c37a3eb 100644 --- a/BackEndFlask/env/.env.production +++ b/BackEndFlask/env/.env.production @@ -1,11 +1,16 @@ # Contains the variables for production enviroment which uses mysql # while the same the urls are meant to override incorrect connection paths meant to serve as examples -DONT_LOOK = 'Thisissupposedtobesecret!2' -WIN_LIN = 'mysql+pymysql://sbadmin_tester:sb_test1@skillbuilder-db.c1db7ief4oer.us-east-2.rds.amazonaws.com:3306/skillbuilder-db' -MAC = 'mysql+pymysql://sbadmin_tester:sb_test1@skillbuilder-db.c1db7ief4oer.us-east-2.rds.amazonaws.com:3306/skillbuilder-db' + +MYSQL_HOST = 'rubricapp-db.c1db7ief4oer.us-east-2.rds.amazonaws.com:3306' +MYSQL_PASSWORD = 'ThisReallyNeedsToBeASecret1!' +MYSQL_USER = 'rubricapp_admin' +MYSQL_DATABASE = 'rubricapp' +WIN_LIN = 'mysql+pymysql://rubricapp_admin:{MYSQL_PASSWORD}@rubricapp-db.c1db7ief4oer.us-east-2.rds.amazonaws.com:3306/rubricapp' +MAC = 'mysql+pymysql://rubricapp_admin:{MYSQL_PASSWORD}@rubricapp-db.c1db7ief4oer.us-east-2.rds.amazonaws.com:3306/rubricapp' + #-----------------------------------------------------------------------------# # Final urls should look like the following for when we move to the server # # WIN_LIN = 'mysql+pymysql://user:password@127.0.0.1:3306/name_of_data_base' # # copy the same for mac to ensure everything is properly overwritten # -#-----------------------------------------------------------------------------# \ No newline at end of file +#-----------------------------------------------------------------------------# diff --git a/BackEndFlask/models/loadExistingRubrics.py b/BackEndFlask/models/loadExistingRubrics.py index 686afc0fd..4ddbe904d 100644 --- a/BackEndFlask/models/loadExistingRubrics.py +++ b/BackEndFlask/models/loadExistingRubrics.py @@ -20,7 +20,9 @@ def load_existing_rubrics(): # (Latest update is September 16, 2022) Problem Solving ["Problem Solving", "Analyzing a complex problem or situation, developing a viable strategy to address it, and executing that strategy (when appropriate)."], # (Latest update is July 19, 2022) Teamwork - ["Teamwork", "Interacting with others and buliding on each other's individual strengths and skills, working toward a common goal."] + ["Teamwork", "Interacting with others and buliding on each other's individual strengths and skills, working toward a common goal."], + # (Latest update is November 21, 2024) Metacognition + ["Metacognition", "Being able to regulate one's thinking and learning through planning, monitoring, and evaluating one's efforts."] ] for rubric in rubrics: r = {} @@ -71,6 +73,11 @@ def load_existing_categories(): [7, "Contributing", "Considered the contributions, strengths and skills of all team members", consistently], [7, "Progressing", "Moved forward towards a common goal", consistently], [7, "Building Community", "Acted as a cohesive unit that supported and included all team members.", consistently], + # (Latest update is November 21, 2024) Metacognition Categories 1-4 + [8, "Planning", "Set learning goals and made plans for achieving them", completely], + [8, "Monitoring", "Paid attention to progress on learning and understanding", completely], + [8, "Evaluating", "Reviewed learning gains and/or performance and determined strengths and areas to improve", completely], + [8, "Realistic Self-assessment", "Produced a self-assessment based on previous and current behaviors and circumstances, using reasonable judgment in future planning", completely], ] for category in categories: c = {} @@ -91,60 +98,73 @@ def load_existing_observable_characteristics(): [1, "Identified the question that needed to be answered or the situation that needed to be addressed"], [1, "Identified any situational factors that may be important to addressing the question or situation"], [1, "Identified the general types of data or information needed to address the question"], + [1, "None"], # Evaluating Observable Characteristics 1-3 [2, "Indicated what information is likely to be most relevant"], [2, "Determined the reliability of the source of information"], [2, "Determined the quality and accuracy of the information itself"], + [2, "None"], # Analyzing Observable Characteristics 1-3 [3, "Discussed information and explored possible meanings"], [3, "Identified general trends or patterns in the data/information that could be used as evidence"], [3, "Processed and/or transformed data/information to put it in forms that could be used as evidence"], + [3, "None"], # Synthesizing Observable Characteristics 1-3 [4, "Identified the relationships between different pieces of information or concepts"], [4, "Compared or contrasted what could be determined from different pieces of information"], [4, "Combined multiple pieces of information or ideas in valid ways to generate a new insight in conclusion"], + [4, "None"], # Forming Arguments Structure Observable Characteristics 1-4 [5, "Stated the conclusion or the claim of the argument"], [5, "Listed the evidence used to support the argument"], [5, "Linked the claim/conclusion to the evidence with focused and organized reasoning"], [5, "Stated any qualifiers that limit the conditions for which the argument is true"], + [5, "None"], # Forming Arguments Validity Observable Characteristics 1-5 [6, "The most relevant evidence was used appropriately to support the claim"], [6, "Reasoning was logical and effectively connected the data to the claim"], [6, "The argument was aligned with disciplinary/community concepts or practices"], [6, "Considered alternative or counter claims"], [6, "Considered evidence that could be used to refute or challenge the claim"], + [6, "None"], # (Latest update is November, 2022) Formal Communication # Intent Observable Characteristics 1-3 [7, "Clearly stated what the audience should gain from the communication"], [7, "Used each part of the communication to convey or support the main message"], [7, "Concluded by summarizing what was to be learned"], + [7, "None"], # Audience Observable Characteristic 1-3 [8, "Communicated to the full range of the audience, including novices and those with expertise"], [8, "Aligned the communication with the interests and background of the particular audience"], [8, "Used vocabulary that aligned with the discipline and was understood by the audience"], + [8, "None"], # Organization Observable Characteristics 1-3 [9, "There was a clear story arc that moved the communication forward"], [9, "Organizational cues and transitions clearly indicated the structure of the communication"], [9, "Sequence of ideas flowed in an order that was easy to follow"], + [9, "None"], # Visual Representations Observable Characteristics 1-3 [10, "Each figure conveyed a clear message"], [10, "Details of the visual representation were easily interpreted by the audience"], [10, "The use of the visual enhanced understanding by the audience"], + [10, "None"], # Format and Style Observable Characteristics 1-3 [11, "Stylistic elements were aesthetically pleasing and did not distract from the message"], [11, "Stylistic elements were designed to make the communication accessbile to the audience (size, colors, contrasts, etc.)"], [11, "The level of formality of the communication aligns with the setting, context, and purpose"], + [11, "None"], # Mechanics Written Word Observable Characteristics 1-4 [12, "Writing contained correct spelling, word choice, punctuation, and capitalization"], [12, "All phrases and sentences were grammatically correct"], [12, "All paragraphs (or slides) were well constructed around a central idea"], [12, "All figures and tables were called out in the narrative, and sources were correctly cited"], + [12, "None"], # Delivery Oral Observable Characteristics 1-4 [13, "Spoke loudly and clearly with a tone that indicated confidence and interest in the subject"], [13, "Vocal tone and pacing helped maintain audience interest"], [13, "Gestures and visual cues further oriented the audience to focus on particular items or messages"], [13, "Body language directed the delivery toward the audience and indicated the speaker was open to engagement"], + [13, "None"], # (Latest update is December 29, 2021) Information Processing # Evaluating Observable Characteristics 1-5 [14, "Established what needs to be accomplished with this information"], @@ -152,101 +172,144 @@ def load_existing_observable_characteristics(): [14, "Indicated what information is relevant"], [14, "Indicated what information is NOT relevant"], [14, "Indicated why certain information is relevant or not"], + [14, "None"], # Interpreting Observable Characteristics 1-4 [15, "Labeled or assigned correct meaning to information (text, tables, symbols, diagrams)"], [15, "Extracted specific details from information"], [15, "Rephrased information in own words"], [15, "Identified patterns in information and derived meaning from them"], + [15, "None"], # Manipulating or Transforming Extent Observable Characteristics 1-3 [16, "Determined what information needs to be converted to accomplish the task"], [16, "Described the process used to generate the transformation"], [16, "Converted all relevant information into a different representation of format"], + [16, "None"], # Manipulating or Transforming Accuracy Observable Characteristics 1-3 [17, "Conveyed the correct or intended meaning of the information in the new representation or format."], [17, "All relevant features of the original information/data are presented in the new representation of format"], [17, "Performed the transformation without errors"], + [17, "None"], # (Latest update is July 5, 2022) Interpersonal Communication # Speaking Observable Characteristics 1-4 [18, "Spoke clear and loudly enough for all team members to hear"], [18, "Used a tone that invited other people to respond"], [18, "Used language that was suitable for the listeners and context"], [18, "Spoke for a reasonable length of time for the situation"], + [18, "None"], # Listening Observable Characteristics 1-4 [19, "Patiently listened without interrupting the speaker"], [19, "Referenced others' ideas to indicate listening and understanding"], [19, "Presented nonverbal cues to indicate attentiveness"], [19, "Avoided engaging in activities that diverted attention"], + [19, "None"], # Responding Observable Characteristics 1-4 [20, "Acknowledged other members for their ideas or contributions"], [20, "Rephrased or referred to what other group members have said"], [20, "Asked other group members to futher explain a concept"], [20, "Elaborated or extended on someone else's idea(s)"], + [20, "None"], # (Latest update is April 24, 2023) Management # Planning Observable Characteristics 1-4 [21, "Generated a summary of the starting and ending points"], [21, "Generated a sequence of steps or tasks to reach the desired goal"], [21, "Discussed a timeline or time frame for completing project tasks"], [21, "Decided on a strategy to share information, updates and progress with all team members"], + [21, "None"], # Organizing Observable Characteristics 1-3 [22, "Decided upon the necessary resources and tools"], [22, "Identified the availability of resources, tools or information"], [22, "Gathered necessary information and tools"], + [22, "None"], # Coordinating Observable Characteristics 1-4 [23, "Determined if tasks need to be delegated or completed by the team as a whole"], [23, "Tailored the tasks toward strengths and availability of team members"], [23, "Assigned specific tasks and responsibilities to team members"], [23, "Established effective communication strategies and productive interactions among team members"], + [23, "None"], # Overseeing Observable Characteristics 1-5 [24, "Reinforced responsibilities and refocused team members toward completing project tasks"], [24, "Communicated status, next steps, and reiterated general plan to accomplish goals"], [24, "Sought and valued input from team members and provided them with constructive feedback"], [24, "Kept track of remaining materials, team and person hours"], [24, "Updated or adapted the tasks or plans as needed"], + [24, "None"], # (Latest update is September 16, 2022) Problem Solving # Analyzing the Situation Observable Characteristics 1-3 [25, "Described the problem that needed to be solved or the decisions that needed to be made"], [25, "Listed complicating factors or constraints that may be important to consider when developing a solution"], [25, "Identified the potential consequences to stakeholders or surrounding"], + [25, "None"], # Identifying Observable Characteristics 1-4 [26, "Reviewed the organized the necessary information and resources"], [26, "Evaluated which available information and resources are critical to solving the problem"], [26, "Determined the limitations of the tools or information that was given or gathered"], [26, "Identified reliable sources that may provide additional needed information, tools, or resources"], + [26, "None"], # Strategizing Observable Characteristics 1-3 [27, "Identified potential starting and ending points for the strategy"], [27, "Determined general steps needed to get from starting point to ending point"], [27, "Sequenced or mapped actions in a logical progression"], + [27, "None"], # Validating Observable Characteristics 1-4 [28, "Reviewed strategy with respect to the identified scope"], [28, "Provided rationale as to how steps within the process were properly sequenced"], [28, "Identified ways the process or stragey could be futher improved or optimized"], [28, "Evaluated the practicality of the overall strategy"], + [28, "None"], # Executing Observable Characteristics 1-4 [29, "Used data and information correctly"], [29, "Made assumptions about the use of data and information that are justifiable"], [29, "Determined that each step is being done in the order and the manner that was planned."], [29, "Verified that each step in the process was providing the desired outcome."], + [29, "None"], # (Latest update is July 19, 2022) Teamwork # Interacting Observable Characteristics 1-3 [30, "All team members communicated ideas related to a common goal"], [30, "Team members responded to each other verbally or nonverbally"], [30, "Directed each other to tasks and information"], + [30, "None"], # Constributing Observable Characteristics 1-4 [31, "Acknowledged the value of the statements of other team members"], [31, "Invited other team members to participate in the conversation, particulary if they had not contributed in a while"], [31, "Expanded on statements of other team members"], [31, "Asked follow-up questions to clarify team members' thoughts"], + [31, "None"], # Progressing Observable Characteristics 1-4 [32, "Stayed on task, focused on the assignment with only brief interruptions"], [32, "Refocused team members to make more effective progress towards the goal"], [32, "Worked simultaneously as single unit on the common goal"], [32, "Checked time to monitor progress on task."], + [32, "None"], # Building Community Observable Characteristics 1-5 [33, "Created a sense of belonging to the team for all team members"], - [33, "Acted as a single unit that did not break up into smaller, gragmented units for the entire task"], + [33, "Acted as a single unit that did not break up into smaller, fragmented units for the entire task"], [33, "Openly and respectfully discussed questions and disagreements between team members"], [33, "Listened carefully to people, and gave weight and respect to their contributions"], [33, "Welcomed and valued the individual identity and experiences of each team member"], + [33, "None"], + # (Latest update is November 20) Metacognition + # Planning Observable Characteristics 1-3 + [34, "Decided on a goal for the task"], + [34, "Determined a strategy, including needed resources, to use in the learning effort"], + [34, "Estimated the time interval needed to reach the goal of the task"], + [34, "None"], + # Monitoring Observable Characteristics 1-4 + [35, "Checked understanding of things to be learned, noting which areas were challenging"], + [35, "Assessed if strategies were effective, and adjusted strategies as needed"], + [35, "Considered if additional resources or assistance would be helpful"], + [35, "Kept track of overall progress on completing the task"], + [35, "None"], + # Evaluating Observable Characteristics 1-3 + [36, "Compared outcomes to personal goals and expectations"], + [36, "Compared performance to external standard or feedback"], + [36, "Identified which strategies were successful and which could be improved"], + [36, "None"], + # Realistic Self-assessment Observable Characteristics 1-4 + [37, "Focused the reflection on the skill or effort that was targeted"], + [37, "Provided specific evidence from past or recent performances in the reflection"], + [37, "Identified how circumstances supported or limited the completion of the task"], + [37, "Made realistic plans (based on previous and current behaviors and circumstances) to improve future performance"], + [37, "None"], ] for observable in observable_characteristics: create_observable_characteristic(observable) @@ -259,24 +322,28 @@ def load_existing_suggestions(): [1, "Highlight or clearly state the question to be addressed or type of conclusion that must be reached."], [1, "List the factors (if any) that may limit the feasibility of some possible conclusions."], [1, "Write down the information you think is needed to address the situation."], + [1, "Nothing specific at this time"], # Evaluating Suggestions 1-5 [2, "Review provided material and circle, highlight, or otherwise indicate information that may be used as evidence in reaching a conclusion."], [2, "Write down the other information (prior knowledge) that might be useful to lead to/support a possible conclusion."], [2, "Set aside any information, patterns, or insights that seem less relevant to addressing the situation at hand."], [2, "Consider whether the information was obtained from a reliable source (textbook, literature, instructor, websites with credible authors)"], [2, "Determine the quality of the information and whether it is sufficient to answer the question."], + [2, "Nothing specific at this time"], # Analyzing Suggestions 1-5 [3, "Interpret and label key pieces of information in text, tables, graphs, diagrams."], [3, "State in your own words what information represents or means."], [3, "Identify general trends in information, and note any information that doesn't fit the pattern."], [3, "Check your understanding of information with others and discuss any differences in understanding."], [3, "State how each piece of information, pattern, or insight can be used to reach a conclusion or support your claim."], + [3, "Nothing specific at this time"], # Synthesizing Suggestions 1-5 [4, "Look for two or more pieces or types of information that can be connected and state how they can be related to each other."], [4, "Write out the aspects that are similar and different in various pieces of information."], [4, "Map out how the information and/or concepts can be combined to support an argument or reach a conclusion."], [4, "Write a statement that summarizes the integration of the information and conveys a new understanding."], [4, "List the ways in which synthesized information could be used as evidence."], + [4, "Nothing specific at this time"], # Forming Arguments Structure Suggestions 1-7 [5, "Review the original goal - what question were you trying to answer?"], [5, "Clearly state your answer to the question (your claim or conclusion)."], @@ -285,12 +352,14 @@ def load_existing_suggestions(): [5, "Explain how each piece of information links to and supports your answer."], [5, "Make sure your answer includes the claim, information and reasoning."], [5, "Make sure the claim or conclusion answers the question."], + [5, "Nothing specific at this time"], # Forming Arguments Validity Suggestions 1-5 [6, "Provide a clear statement that articulates why the evidence you chose leads to the claim or conclusion."], [6, "Check to make sure that your reasoning is consistent with what is accepted in the discipline or context."], [6, "Test your ideas with others, and ask them to judge the quality of the argument or indicate how the argument could be made more convincing."], [6, "Ask yourself (and others) if there is evidence or data that doesn't suport your conclusion or might contradict your claim."], [6, "Consider if there are alternative explanations for the data you are considering."], + [6, "Nothing specific at this time"], # (Latest update is November, 2022) Formal Communication # Intent Suggestions 1-5 [7, "Decide if your main purpose is to inform, to persuade, to argue, to summarize, to entertain, to inspire, etc."], @@ -298,6 +367,7 @@ def load_existing_suggestions(): [7, "Make sure the purpose of the communication is presented early to orient your audience to the focus of the communication."], [7, "Check that the focus of each segment is clearly linked to the main message or intent of the communication."], [7, "Summarize the main ideas to wrap up the presentation (refer back to the initial statement(s) of what was to be learned)."], + [7, "Nothing specific at this time"], # Audience Suggestions 1-6 [8, "Identify the range and level of expertise and interest your audience has for the topic and design your communication to have aspects that will engage all members of the audience."], [8, "Identify what the audience needs to know to understand the narrative."], @@ -305,6 +375,7 @@ def load_existing_suggestions(): [8, "Only use jargon when it is understood readily by most members of your audience, and it makes the communication more effective and succinct."], [8, "Check that the vocabulary, sentence structure, and tone used in your communication is aligned with the level of your audience."], [8, "Collect feedback from others on drafts to make sure the core message of the communication is easily understood."], + [8, "Nothing specific at this time"], # Organization Suggestions 1-6 [9, "Consider the 'story' that you want to tell. Ask yourself what's the main message you want the audience to leave with."], [9, "Identify the critical points for the story (do this before you prepare the actual communication) and map out the key points."], @@ -312,6 +383,7 @@ def load_existing_suggestions(): [9, "Repeat key ideas to ensure the audience can follow the main idea."], [9, "Make sure that you introduce prerequisite information early in the communication."], [9, "Try more than one order for the topics, to see if overall flow is improved."], + [9, "Nothing specific at this time"], # Visual Representations Suggestions 1-6 [10, "Plan what types of figures are needed to support the narrative - consider writing out a figure description before you construct it."], [10, "Avoid including unnecessary details that detract from the intended message."], @@ -319,6 +391,7 @@ def load_existing_suggestions(): [10, "Be sure labels, text, and small details can be easily read."], [10, "Provide a caption that helps interpret the key aspects of the visual."], [10, "Seek feedback on visuals to gauge initial reaction and ease of interpretation."], + [10, "Nothing specific at this time"], # Format Style Suggestions 1-6 [11, "Use titles (headers) and subtitles (subheaders) to orient the audience and help them follow the narrative."], [11, "Look at pages or slides as a whole for an easy-to-read layout, such as white space, headers, line spacing, etc."], @@ -326,6 +399,7 @@ def load_existing_suggestions(): [11, "Use colors to carefully highlight or call attention to key elements to enhance your narrative without distracting from your message."], [11, "Make sure that text, figures, and colors are readable and accessible for all."], [11, "Seek feedback to confirm that the language, tone, and style of your communication match the level of formality needed for your context and purpose."], + [11, "Nothing specific at this time"], # Mechanics Written Words Suggestions 1-7 [12, "Proofread your writing for spelling errors, punctuation, autocorrects, etc."], [12, "Review sentence structure for subject-verb agreement, consistent tense, run on sentences, and other structural problems."], @@ -334,12 +408,14 @@ def load_existing_suggestions(): [12, "Confirm that each figure, table, etc has been numbered consecutively and has been called out and discussed further in the narrative."], [12, "Confirm that all work that has been published elsewhere or ideas/data that were not generated by the author(s) has been properly cited using appropriate conventions."], [12, "Ask someone else to review and provide feedback on your work."], + [12, "Nothing specific at this time"], # Delivery Oral Suggestions 1-5 [13, "Practice for others or record your talk; i. be sure that your voice can be heard, and your word pronunciations are clear. ii. listen for “ums”, “like”, or other verbal tics/filler words that can detract from your message. iii. observe your natural body language, gestures, and stance in front of the audience to be sure that they express confidence and enhance your message."], [13, "Add variety to your speed or vocal tone to emphasize key points or transitions."], [13, "Try to communicate/engage as if telling a story or having a conversation with the audience."], [13, "Face the audience and do not look continuously at the screen or notes."], [13, "Make eye contact with multiple members of the audience."], + [13, "Nothing specific at this time"], # (Latest update is December 29, 2021) Information Processing # Evaluating Suggestions 1-6 [14, "Restate in your own words the task or question that you are trying to address with this information."], @@ -348,23 +424,27 @@ def load_existing_suggestions(): [14, "Write down/circle/highlight the information that is needed to complete the task."], [14, "Put a line through info that you believe is not needed for the task"], [14, "Describe in what ways a particular piece of information may (or may not) be useful (or required) in completing the task"], + [14, "Nothing specific at this time"], # Interpreting Suggestions 1-5 [15, "Add notes or subtitles to key pieces of information found in text, tables, graphs, diagrams to describe its meaning."], [15, "State in your own words what information represents or means."], [15, "Summarize the ideas or relationships the information might convey."], [15, "Determine general trends in information and note any information that doesn't fit the trend"], [15, "Check your understanding of information with others"], + [15, "Nothing specific at this time"], # Manipulating or Transforming Extent Suggestions 1-5 [16, "Identify how the new format of the information differs from the provided format."], [16, "Identify what information needs to be transformed and make notations to ensure that all relevant information has been included."], [16, "Review the new representation or format to be sure all relevant information has been included."], [16, "Consider what information was not included in the new representation or format and make sure it was not necessary."], [16, "Check with peers to see if there is agreement on the method of transformation and final result."], + [16, "Nothing specific at this time"], # Manipulating or Transforming Accuracy Suggestions 1-4 [17, "Write down the features that need to be included in the new form."], [17, "Be sure that you have carefully interpreted the original information and translated that to the new form."], [17, "Carefully check to ensure that the original information is correctly represented in the new form."], [17, "Verify the accuracy of the transformation with others."], + [17, "Nothing specific at this time"], # (Latest update is July 5, 2022) Interpersonal Communication # Speaking Suggestions 1-6 [18, "Direct your voice towards the listeners and ask if you can be heard."], @@ -373,6 +453,7 @@ def load_existing_suggestions(): [18, "Carefully choose your words to align with the nature of the topic and the audience."], [18, "Speak for a length of time that allows frequent back and forth conversation."], [18, "Provide a level of detail appropriate to convey your main idea."], + [18, "Nothing specific at this time"], # Listening Suggestions 1-7 [19, "Allow team members to finish their contribution."], [19, "Indicate if you can't hear someone's spoken words."], @@ -381,12 +462,14 @@ def load_existing_suggestions(): [19, "Face the team member that is speaking and make eye contact."], [19, "Use active-listening body language or facial expressions that indicate attentiveness."], [19, "Remove distractions and direct your attention to the speaker."], + [19, "Nothing specific at this time"], # Responding Suggestions 1-5 [20, "Let team members know when they make a productive contribution."], [20, "State what others have said in your own words and confirm understanding."], [20, "Ask a follow-up question or ask for clarification."], [20, "Reference what others have said when you build on their ideas."], [20, "Offer an altenative to what a team member said."], + [20, "Nothing specific at this time"], # (Latest update is April 24, 2023) Management # Planning Suggestions 1-6 [21, "Write down the general starting point and starting conditions."], @@ -395,10 +478,12 @@ def load_existing_suggestions(): [21, "Double check to make sure that steps are sequenced sensibly."], [21, "Identify time needed for particular steps or other time constraints."], [21, "Make a regular plan to update progress."], + [21, "Nothing specific at this time"], # Organizing Suggestions 1-3 [22, "List the tools, resources, or information that the group needs to obtain."], [22, "List the location of the tools, resources, or information at the group's disposal."], [22, "Strategize about how to obtain the additional/needed tools, resources, or information."], + [22, "Nothing specific at this time"], # Coordinating Suggestions 1-7 [23, "Review the number of people you have addressing each task, and be sure that it is right-sized to make progress."], [23, "Analyze each task for likelihood of success, and be sure you have it staffed appropriately."], @@ -407,6 +492,7 @@ def load_existing_suggestions(): [23, "Delegate tasks outside the team if necessary, especially if the task is too large to complete in the given time."], [23, "Establish a mechanism to share status and work products."], [23, "Set up meetings to discuss challenges and progress."], + [23, "Nothing specific at this time"], # Overseeing Suggestions 1-8 [24, "Check in regularly with each team member to review their progress on tasks."], [24, "Provide a list of steps towards accomplishing the goal that all can refer to and check off each step when completed."], @@ -416,6 +502,7 @@ def load_existing_suggestions(): [24, "Reassign team members to activities that need more attention or person hours as other steps are completed."], [24, "Evaluate whether team members should be reassigned to tasks that better align with their skill sets."], [24, "Check to see if the original plan for project completion is still feasible; make changes if necessary."], + [24, "Nothing specific at this time"], # (Latest update is September 16, 2022) Problem Solving # Analyzing The Situation Suggestions 1-6 [25, "Read closely, and write down short summaries as you read through the entire context of the problem"], @@ -424,18 +511,21 @@ def load_existing_suggestions(): [25, "Prioritize the complicating factors from most to least important"], [25, "List anything that will be significantly impacted by your decision (such as conditions, objects, or people)"], [25, "Deliberate on the consequences of generating a wrong strategy or solution"], + [25, "Nothing specific at this time"], # Identifying Suggestions 1-5 [26, "Highlight or annotate the provided information that may be needed to solve the problem."], [26, "List information or principles that you already know that can help you solve the problem."], [26, "Sort the given and gathered information/resources as 'useful' or 'not useful.'"], [26, "List the particular limitations of the provided information or tools."], [26, "Identify ways to access any additional reliable information, tools or resources that you might need."], + [26, "Nothing specific at this time"], # Strategizing Suggestions 1-5 [27, "Write down a reasonable place to start and add a reasonable end goal"], [27, "Align any two steps in the order or sequence that they must happen. Then, add a third step and so on."], [27, "Consider starting at the end goal and working backwards"], [27, "Sketch a flowchart indicating some general steps from start to finish."], [27, "Add links/actions, or processes that connect the steps"], + [27, "Nothing specific at this time"], # Validating Suggestions 1-7 [28, "Summarize the problem succinctly - does your strategy address each aspect of the problem?"], [28, "Identify any steps that must occur in a specific order and verify that they do."], @@ -444,6 +534,7 @@ def load_existing_suggestions(): [28, "Check to see if you have access to necessary resources, and if not, propose substitutes."], [28, "Check that your strategy is practical and functional, with respect to time, cost, safety, personnel, regulations, etc."], [28, "Take time to continuously assess your strategy throughout the process."], + [28, "Nothing specific at this time"], # Executing Suggestions 1-7 [29, "Use authentic values and reasonable estimates for information needed to solve the problem"], [29, "Make sure that the information you are using applies to the conditions of the problem."], @@ -452,6 +543,7 @@ def load_existing_suggestions(): [29, "List any barriers that you are encountering in executing the steps"], [29, "Identify ways to overcome barriers in implementation steps of the strategy"], [29, "Check the outcome of each step of the strategy for effectiveness."], + [29, "Nothing specific at this time"], # (Latest update is July 19, 2022) Teamwork # Interacting Suggestions 1-6 [30, "Speak up and share your ideas/insights with team members."], @@ -460,6 +552,7 @@ def load_existing_suggestions(): [30, "Explicitly react (nod, speak out loud, write a note, etc.) to contributions from other team members to indicate that you are engaged."], [30, "Restate the prompt to make sure everyone is at the same place on the task."], [30, "Have all members of the team consider the same task at the same time rather than working independently"], + [30, "Nothing specific at this time"], # Contributing Suggestions 1-6 [31, "Acknowledge or point out particularly effective contributions."], [31, "Initiate discussions of agreement or disagreement with statements made by team members."], @@ -467,6 +560,7 @@ def load_existing_suggestions(): [31, "Regularly ask members of the team to share their ideas or explain their reasoning."], [31, "Add information or reasoning to contributions from other team members."], [31, "Ask for clarification or rephrase statements of other team members to ensure understanding."], + [31, "Nothing specific at this time"], # Progressing Suggestions 1-7 [32, "Minimize distractions and focus on the assignment (close unrelated websites or messaging on phone or computer, turn off music, put away unrelated materials)."], [32, "Redirect team members to current task."], @@ -475,6 +569,7 @@ def load_existing_suggestions(): [32, "Compare progress on task to the time remaining on assignment."], [32, "Communicate to team members that you need to move on."], [32, "As a team, list tasks to be done and agree on order for these tasks."], + [32, "Nothing specific at this time"], # Building Community Suggestions 1-8 [33, "Address team members by name."], [33, "Use inclusive (collective) team cues that draw all team members together."], @@ -484,6 +579,46 @@ def load_existing_suggestions(): [33, "Encourage all team members to work together on the same tasks at the same time, as needed."], [33, "Celebrate team successes and persistence through roadblocks."], [33, "Invite other team members to provide alternative views and reasoning."], - ] + [33, "Nothing specific at this time"], + # (Latest update is November 20, 2024) Metacognition + # Planning Suggestions 1-7 + [34, "Describe three or four ways this learning task connects to other topics and components in the course."], + [34, "Identify 2-3 ways that this learning task will help you meet the intended learning goals."], + [34, "Skim the assignment to get a sense of what is involved and to see what resources you will need to support your work."], + [34, "List the things that need to be completed for this assignment or task to be considered successful."], + [34, "Make a detailed plan for how you will complete the assignment."], + [34, "Decide if it makes sense to break the overall assignment into working segments, and figure out how long each would take."], + [34, "Estimate the total amount of time that you will need to complete the task."], + [34, "Nothing specific at this time"], + # Monitoring Suggestions 1-5 + [35, "Read the reflection prompt and objectives before you start and decide what skills or processes you should monitor during the task."], + [35, "Review objectives frequently while completing a task to check your understanding of important concepts."], + [35, "Survey your environment and mindset for distractions that block you from enacting your strategies and making progress (devices, noise, people, physical needs)."], + [35, "Pay attention to where in a process or strategy you are getting stuck, and identify what resources or support would help you to get past that point."], + [35, "Periodically pause and determine the percentage of work that you finished and compare it to the total time available to complete the work."], + [35, "Nothing specific at this time"], + # Evaluating Suggestions 1-7 + [36, "Compare how you actually performed to your initial expectations. Identify which expectations were met and where you fell short."], + [36, "For areas where you met your initial expectations, list your top three effective strategies or activities."], + [36, "For areas where you did not meet your initial expectations, decide if your goals were realistic."], + [36, "If your initial expectations were not realistic, make a list of issues you didn’t account for in your goal setting."], + [36, "For areas where your expectations were realistic but not met, identify what factors made you fall short of your target."], + [36, "Compare how you performed to an external standard or external feedback on your performance. Identify which criteria were met and which require additional work."], + [36, "For areas where you met the criteria, list your top three effective strategies or activities.Describe three or four ways this learning task connects to other topics and components in the course."], + [36, "For areas where you did not meet the criteria, list at least two areas or strategies where you need further work."], + [36, "Determine if you planned your time well by comparing the number of hours you allocated to the number of hours you actually needed to meet your goals."], + [36, "Decide if you were motivated or engaged in this task, and describe how that impacted your efforts."], + [36, "If you weren’t very motivated for this task, generate some ideas for how you could better motivate yourself. Identify 2-3 ways that this learning task will help you meet the intended learning goals."], + [36, "Nothing specific at this time"], + # Realistic Self-assessment Suggestions 1-7 + [37, "Before you start the reflection, review the prompt for the reflection or your goals for the reflection and make sure you are focusing on the intended skill or process (e.g. don’t comment on teamwork if asked to reflect on critical thinking)."], + [37, "List the specific actions that you took in completing this task, including planning and monitoring actions in addition to the task itself.  Which are similar to past approaches?  Which are different?"], + [37, "Considering the actions listed above and your typical approaches; rank them in terms of most productive to least productive."], + [37, "Identify the unproductive behavior or work habit you should change to most positively impact your performance and determine a strategy for how to change it."], + [37, "Consider the contextual factors (physical surroundings, time constraints, life circumstances) that affected your performance; note how your strategies need to be altered to account for them to improve future performance."], + [37, "Summarize 2-3 specific strategies you’ve identified to improve your performance on future tasks."], + [37, "Ask someone who knows you or your work well to review your self-assessment. Ask them if you accurately summarized your past efforts and if they think your future strategies are realistic for you."], + [37, "Nothing specific at this time"], + ] for suggestion in suggestions: create_suggestion(suggestion) \ No newline at end of file diff --git a/BackEndFlask/models/queries.py b/BackEndFlask/models/queries.py index 0c245848a..0c1a20e9a 100644 --- a/BackEndFlask/models/queries.py +++ b/BackEndFlask/models/queries.py @@ -1026,7 +1026,8 @@ def get_csv_categories(rubric_id: int, user_id: int, team_id: int, at_id: int, c for i in range(0, 2): ocs_sfis_query[i] = db.session.query( - ObservableCharacteristic.observable_characteristic_text if i == 0 else SuggestionsForImprovement.suggestion_text + ObservableCharacteristic.observable_characteristic_text if i == 0 else SuggestionsForImprovement.suggestion_text, + ObservableCharacteristic.observable_characteristics_id if i == 0 else SuggestionsForImprovement.suggestion_id, ).join( Category, (ObservableCharacteristic.category_id if i == 0 else SuggestionsForImprovement.category_id) == Category.category_id @@ -1051,8 +1052,8 @@ def get_csv_categories(rubric_id: int, user_id: int, team_id: int, at_id: int, c if team_id is not None : ocs_sfis_query[i].filter(CompletedAssessment.team_id == team_id) # Executing the query - ocs = ocs_sfis_query[0].all() - sfis = ocs_sfis_query[1].all() + ocs = ocs_sfis_query[0].distinct(ObservableCharacteristic.observable_characteristics_id).all() + sfis = ocs_sfis_query[1].distinct(SuggestionsForImprovement.suggestion_id).all() return ocs,sfis @@ -1082,9 +1083,6 @@ def get_course_name_by_at_id(at_id:int) -> str : return course_name[0][0] - - - def get_completed_assessment_ratio(course_id: int, assessment_task_id: int) -> int: """ Description: @@ -1096,9 +1094,19 @@ def get_completed_assessment_ratio(course_id: int, assessment_task_id: int) -> i Return: int (Ratio of users who have completed an assessment task rounded to the nearest whole number) """ + ratio = 0 + all_usernames_for_completed_task = get_completed_assessment_with_user_name(assessment_task_id) - all_students_in_course = get_users_by_course_id_and_role_id(course_id, 5) - ratio = (len(all_usernames_for_completed_task) / len(all_students_in_course)) * 100 + + if all_usernames_for_completed_task: + all_students_in_course = get_users_by_course_id_and_role_id(course_id, 5) + + ratio = len(all_usernames_for_completed_task) / len(all_students_in_course) * 100 + else: + all_teams_in_course = get_team_members_in_course(course_id) + all_teams_for_completed_task = get_completed_assessment_with_team_name(assessment_task_id) + + ratio = len(all_teams_for_completed_task) / len(all_teams_in_course) * 100 ratio_rounded = round(ratio) @@ -1126,4 +1134,58 @@ def is_admin_by_user_id(user_id: int) -> bool: if is_admin[0][0]: return True - return False \ No newline at end of file + 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: (are we looking for students associated to a team?) + at_id: (assessment Id) + completed_at_id: (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 .") + + 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() \ No newline at end of file diff --git a/BackEndFlask/models/schemas.py b/BackEndFlask/models/schemas.py index 3fbc3570d..433602e6d 100644 --- a/BackEndFlask/models/schemas.py +++ b/BackEndFlask/models/schemas.py @@ -16,29 +16,29 @@ Team(team_id, team_name, course_id, observer_id, date_created, active_until) TeamUser(team_user_id, team_id, user_id) AssessmentTask(assessment_task_id, assessment_task_name, course_id, rubric_id, role_id, due_date, time_zone, show_suggestions, show_ratings, unit_of_assessment, comment, number_of_teams) - Completed_Assessment(completed_assessment_id, assessment_task_id, by_role, team_id, user_id, initial_time, last_update, rating_observable_characteristics_suggestions_data) + Checkin(checkin_id, assessment_task_id, team_number, user_id, time) + CompletedAssessment(completed_assessment_id, assessment_task_id, by_role, team_id, user_id, initial_time, last_update, rating_observable_characteristics_suggestions_data) + Feedback(feedback_id, user_id, completed_assessment_id, feedback_time, lag_time) Blacklist(id, token) """ class Role(db.Model): __tablename__ = "Role" - __table_args__ = {'sqlite_autoincrement': True} - role_id = db.Column(db.Integer, primary_key=True) - role_name = db.Column(db.String(100), nullable=False) + role_id = db.Column(db.Integer, primary_key=True, autoincrement=True) + role_name = db.Column(db.Text, nullable=False) class User(db.Model): __tablename__ = "User" - __table_args__ = {'sqlite_autoincrement': True} - user_id = db.Column(db.Integer, primary_key=True) - first_name = db.Column(db.String(30), nullable=False) - last_name = db.Column(db.String(30), nullable=False) - email = db.Column(db.String(255), unique=True, nullable=False) - password = db.Column(db.String(80), nullable=False) + user_id = db.Column(db.Integer, primary_key=True, autoincrement=True) + first_name = db.Column(db.Text, nullable=False) + last_name = db.Column(db.Text, nullable=False) + email = db.Column(db.String(254), unique=True, nullable=False) + password = db.Column(db.Text, nullable=False) lms_id = db.Column(db.Integer, nullable=True) consent = db.Column(db.Boolean, nullable=True) owner_id = db.Column(db.Integer, ForeignKey(user_id), nullable=True) has_set_password = db.Column(db.Boolean, nullable=False) - reset_code = db.Column(db.String(6), nullable=True) + reset_code = db.Column(db.Text, nullable=True) is_admin = db.Column(db.Boolean, nullable=False) class Rubric(db.Model): @@ -46,14 +46,14 @@ class Rubric(db.Model): __table_args__ = {'sqlite_autoincrement': True} rubric_id = db.Column(db.Integer, primary_key=True) rubric_name = db.Column(db.String(100)) - rubric_description = db.Column(db.String(100), nullable=True) + rubric_description = db.Column(db.Text, nullable=True) owner = db.Column(db.Integer, ForeignKey(User.user_id), nullable=True) class Category(db.Model): __tablename__ = "Category" __table_args__ = {'sqlite_autoincrement': True} category_id = db.Column(db.Integer, primary_key=True) - category_name = db.Column(db.String(30), nullable=False) + category_name = db.Column(db.Text, nullable=False) description = db.Column(db.String(255), nullable=False) rating_json = db.Column(db.JSON, nullable=False) @@ -80,12 +80,11 @@ class SuggestionsForImprovement(db.Model): class Course(db.Model): __tablename__ = "Course" - __table_args__ = {'sqlite_autoincrement': True} - course_id = db.Column(db.Integer, primary_key=True) - course_number = db.Column(db.String(10), nullable=False) - course_name = db.Column(db.String(50), nullable=False) + course_id = db.Column(db.Integer, primary_key=True, autoincrement=True) + course_number = db.Column(db.Text, nullable=False) + course_name = db.Column(db.Text, nullable=False) year = db.Column(db.Integer, nullable=False) - term = db.Column(db.String(50), nullable=False) + term = db.Column(db.Text, nullable=False) active = db.Column(db.Boolean, nullable=False) admin_id = db.Column(db.Integer, ForeignKey(User.user_id, ondelete='RESTRICT'), nullable=False) use_tas = db.Column(db.Boolean, nullable=False) @@ -102,9 +101,8 @@ class UserCourse(db.Model): class Team(db.Model): # keeps track of default teams for a fixed team scenario __tablename__ = "Team" - __table_args__ = {'sqlite_autoincrement': True} - team_id = db.Column(db.Integer, primary_key=True) - team_name = db.Column(db.String(25), nullable=False) + team_id = db.Column(db.Integer, primary_key=True, autoincrement=True) + team_name = db.Column(db.Text, nullable=False) course_id = db.Column(db.Integer, ForeignKey(Course.course_id), nullable=False) observer_id = db.Column(db.Integer, ForeignKey(User.user_id, ondelete='RESTRICT'), nullable=False) date_created = db.Column(db.Date, nullable=False) @@ -112,34 +110,31 @@ class Team(db.Model): # keeps track of default teams for a fixed team scenario class TeamUser(db.Model): __tablename__ = "TeamUser" - __table_args__ = {'sqlite_autoincrement': True} - team_user_id = db.Column(db.Integer, primary_key=True) + team_user_id = db.Column(db.Integer, primary_key=True, autoincrement=True) team_id = db.Column(db.Integer, ForeignKey(Team.team_id), nullable=False) user_id = db.Column(db.Integer, ForeignKey(User.user_id), nullable=False) class AssessmentTask(db.Model): __tablename__ = "AssessmentTask" - __table_args__ = {'sqlite_autoincrement' : True} - assessment_task_id = db.Column(db.Integer, primary_key=True) - assessment_task_name = db.Column(db.String(100)) + assessment_task_id = db.Column(db.Integer, primary_key=True, autoincrement=True) + assessment_task_name = db.Column(db.Text) course_id = db.Column(db.Integer, ForeignKey(Course.course_id)) rubric_id = db.Column(db.Integer, ForeignKey(Rubric.rubric_id)) # how to handle updates and deletes role_id = db.Column(db.Integer, ForeignKey(Role.role_id)) due_date = db.Column(db.DateTime, nullable=False) - time_zone = db.Column(db.String(3), nullable=False) + time_zone = db.Column(db.Text, nullable=False) show_suggestions = db.Column(db.Boolean, nullable=False) show_ratings = db.Column(db.Boolean, nullable=False) unit_of_assessment = db.Column(db.Boolean, nullable=False) # true if team, false if individuals - comment = db.Column(db.String(3000), nullable=True) - create_team_password = db.Column(db.String(25), nullable=True) + comment = db.Column(db.Text, nullable=True) + create_team_password = db.Column(db.Text, nullable=True) number_of_teams = db.Column(db.Integer, nullable=True) max_team_size = db.Column(db.Integer, nullable=True) notification_sent = db.Column(DateTime(timezone=True), nullable=True) class Checkin(db.Model): # keeps students checking to take a specific AT __tablename__ = "Checkin" - __table_args__ = {'sqlite_autoincrement': True} - checkin_id = db.Column(db.Integer, primary_key=True) + checkin_id = db.Column(db.Integer, primary_key=True, autoincrement=True) assessment_task_id = db.Column(db.Integer, ForeignKey(AssessmentTask.assessment_task_id), nullable=False) # not a foreign key because in the scenario without fixed teams, there will not be default team entries # to reference. if they are default teams, team_number will equal the team_id of the corresponding team @@ -149,8 +144,7 @@ class Checkin(db.Model): # keeps students checking to take a specific AT class CompletedAssessment(db.Model): __tablename__ = "CompletedAssessment" - __table_args__ = {'sqlite_autoincrement': True} - completed_assessment_id = db.Column(db.Integer, primary_key=True) + completed_assessment_id = db.Column(db.Integer, primary_key=True, autoincrement=True) assessment_task_id = db.Column(db.Integer, ForeignKey(AssessmentTask.assessment_task_id)) completed_by = db.Column(db.Integer, ForeignKey(User.user_id), nullable=False) team_id = db.Column(db.Integer, ForeignKey(Team.team_id), nullable=True) @@ -162,8 +156,7 @@ class CompletedAssessment(db.Model): class Feedback(db.Model): __tablename__ = "Feedback" - __table_args__ = {'sqlite_autoincrement': True} - feedback_id = db.Column(db.Integer, primary_key=True) + feedback_id = db.Column(db.Integer, primary_key=True, autoincrement=True) user_id = db.Column(db.Integer, ForeignKey(User.user_id), nullable=False) completed_assessment_id = db.Column(db.Integer, ForeignKey(CompletedAssessment.completed_assessment_id), nullable=False) feedback_time = db.Column(DateTime(timezone=True), nullable=True) # time the student viewed their feedback \ No newline at end of file diff --git a/BackEndFlask/models/user.py b/BackEndFlask/models/user.py index b9ac1e235..dc9cf4624 100644 --- a/BackEndFlask/models/user.py +++ b/BackEndFlask/models/user.py @@ -186,7 +186,7 @@ def create_user(user_data): lms_id=user_data["lms_id"], consent=user_data["consent"], owner_id=user_data["owner_id"], - is_admin="role_id" in user_data.keys() and user_data["role_id"]==3, + is_admin="role_id" in user_data.keys() and user_data["role_id"] in [1,2,3], has_set_password=has_set_password, reset_code=None ) @@ -242,8 +242,9 @@ def load_SuperAdminUser(): "password": str(os.environ.get('SUPER_ADMIN_PASSWORD')), "lms_id": 0, "consent": None, - "owner_id": 0, - "role_id": None + "owner_id": None, + "role_id": 2, + "is_admin": True }) # user_id = 2 diff --git a/BackEndFlask/models/utility.py b/BackEndFlask/models/utility.py index 942ea62b0..6f9a7d2f3 100644 --- a/BackEndFlask/models/utility.py +++ b/BackEndFlask/models/utility.py @@ -1,6 +1,6 @@ import sys import yagmail -import random, string +import string, secrets from models.logger import logger from controller.Routes.RouteExceptions import EmailFailureException @@ -13,10 +13,15 @@ def send_new_user_email(address: str, password: str): subject = "Welcome to Skillbuilder!" - message = f'''Your password is {password}. You will need to choose a new password after logging in for the first time. - - Cheers, - The Skillbuilder Team''' + message = f'''Welcome to SkillBuilder, a tool to enhance your learning experience this semester! This app will be our hub for assessing and providing feedback on transferable skills (these are also referred to as process skills, professional skills, durable skills). + + Access the app at this link: skill-builder.net + + Login Information: Your Username is {address} + + Temporary Password: {password} + + Please change your password after your first login to keep your account secure.''' send_email(address, subject, message) @@ -58,7 +63,7 @@ def send_email(address: str, subject: str, content: str): def generate_random_password(length: int): letters = string.ascii_letters + string.digits - return ''.join(random.choice(letters) for i in range(length)) + return ''.join(secrets.choice(letters) for i in range(length)) def error_log(f): ''' diff --git a/BackEndFlask/requirements.txt b/BackEndFlask/requirements.txt index 653d710fd..423cf8d5b 100644 --- a/BackEndFlask/requirements.txt +++ b/BackEndFlask/requirements.txt @@ -15,4 +15,5 @@ Werkzeug >= 2.0.2 redis >= 4.5.5 python-dotenv >= 1.0.0 yagmail >= 0.15.293 -openpyxl >= 3.1.2 \ No newline at end of file +openpyxl >= 3.1.2 +cryptography >= 43.0.1 \ No newline at end of file diff --git a/BackEndFlask/run.py b/BackEndFlask/run.py index f1bba48e6..58643d83c 100644 --- a/BackEndFlask/run.py +++ b/BackEndFlask/run.py @@ -1,12 +1,4 @@ from core import app -# this variable is expected by the wsgi server -# application = app if __name__ == '__main__': - #The app.run(debug = True) line is needed if we are working on our local machine - # app.run(debug=True) - - #the app.run(host="0.0.0.0") line is currently commented out and if and only when we are seting up an EC2 instance app.run(host="0.0.0.0") - - # token: MFFt4RjpXNMh1c_T1AQj diff --git a/BackEndFlask/setupEnv.py b/BackEndFlask/setupEnv.py index 9b01a1b30..0effeb690 100755 --- a/BackEndFlask/setupEnv.py +++ b/BackEndFlask/setupEnv.py @@ -41,6 +41,8 @@ def cmd(command, parent_function): err(f"Error running command: {command} in function: {parent_function}") + err(f"Exception: {e}") + err(f"Return code: {e.returncode}") err(f"Output: {e.output}") diff --git a/Dockerfile.backend b/Dockerfile.backend index c43103ed1..386c23bc2 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -24,11 +24,8 @@ RUN pip install --no-cache-dir --upgrade pip \ # Copy the rest of the backend code COPY BackEndFlask/ /app/ -# Initialize the Backend environment -RUN python setupEnv.py -d - # Expose the Backend port EXPOSE 5000 # Start the Flask server -CMD ["python", "setupEnv.py", "-s"] +CMD ["python", "setupEnv.py", "-ds"] diff --git a/FrontEndReact/src/View/Admin/Add/AddCourse/AdminAddCourse.js b/FrontEndReact/src/View/Admin/Add/AddCourse/AdminAddCourse.js index 0f4ddb821..3be2ad788 100644 --- a/FrontEndReact/src/View/Admin/Add/AddCourse/AdminAddCourse.js +++ b/FrontEndReact/src/View/Admin/Add/AddCourse/AdminAddCourse.js @@ -5,7 +5,8 @@ import validator from "validator"; import ErrorMessage from "../../../Error/ErrorMessage.js"; import { genericResourcePOST, genericResourcePUT } from "../../../../utility.js"; import Cookies from "universal-cookie"; -import { Box, Button, FormControl, Typography, TextField, FormControlLabel, Checkbox, FormGroup, } from "@mui/material"; +import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; +import { Box, Button, FormControl, Typography, Popover, TextField, Tooltip, IconButton, FormControlLabel, Checkbox, FormGroup, } from "@mui/material"; @@ -25,7 +26,8 @@ class AdminAddCourse extends Component { year: "", active: true, useTas: true, - useFixedTeams: true, + useFixedTeams: true, + anchorEl: null, errors: { courseName: "", @@ -34,8 +36,13 @@ class AdminAddCourse extends Component { year: "", }, }; + } + setAnchorEl = (element) => { + this.setState({ anchorEl: element }); + }; + componentDidMount() { var navbar = this.props.navbar; var state = navbar.state; @@ -56,6 +63,13 @@ class AdminAddCourse extends Component { }); } } + handleClick = (event) => { + this.setAnchorEl(event.currentTarget); + }; + + handleClose = () => { + this.setAnchorEl(null); + }; handleChange = (e) => { const { id, value } = e.target; @@ -144,9 +158,6 @@ class AdminAddCourse extends Component { if (term.trim() === "") newErrors["term"] = "Term cannot be empty"; - else if (term.trim() !== "Spring" && term.trim() !== "Fall" && term.trim() !== "Summer") - newErrors["term"] = "Term should be either Spring, Fall, or Summer"; - if (newErrors["courseName"] !== "" || newErrors["courseNumber"] !== "" ||newErrors["year"] !== "" ||newErrors["term"] !== "") { this.setState({ errors: newErrors @@ -208,6 +219,8 @@ class AdminAddCourse extends Component { var navbar = this.props.navbar; var state = navbar.state; var addCourse = state.addCourse; + const open = Boolean(this.state.anchorEl); + const id = open ? 'simple-popover' : undefined; return ( @@ -266,7 +279,7 @@ class AdminAddCourse extends Component { id="term" name="newTerm" variant="outlined" - label="Term" + label="Type your Term name here" fullWidth value={term} error={!!errors.term} @@ -344,8 +357,31 @@ class AdminAddCourse extends Component { } name="newFixedTeams" - label="Fixed Team" - /> + label="Fixed Teams" + /> +
+ + + + + +
+ + Active: Uncheck this box at the end of the term to move it to the Inactive Courses table.
+
Use TA's: + Will you use Teaching or Learning Assistants in this course to fill out rubrics?
+
Fixed teams: Do you assign students to the same team for the entire semester?
+
+ { - render(); - - await waitFor(() => { - expectElementWithAriaLabelToBeInDocument(ct); - }); - - clickElementWithAriaLabel(ac); - - await waitFor(() => { - expectElementWithAriaLabelToBeInDocument(act); - }); - - changeElementWithAriaLabelWithInput(cnami, "Object Oriented Programming"); - - changeElementWithAriaLabelWithInput(cnumi, "CS3423"); - - changeElementWithAriaLabelWithInput(cti, "A"); - - changeElementWithAriaLabelWithInput(cyi, "2025"); - - clickElementWithAriaLabel(aosacb); - - await waitFor(() => { - expectElementWithAriaLabelToBeInDocument(acf); - - expectElementWithAriaLabelToHaveErrorMessage(cti, "Term should be either Spring, Fall, or Summer"); - }); -}); - - -test("AdminAddCourse.test.js Test 9: HelperText error should show for the addCourseYear text field when input is less than 2023", async () => { +test("AdminAddCourse.test.js Test 8: HelperText error should show for the addCourseYear text field when input is less than 2023", async () => { render(); await waitFor(() => { @@ -287,7 +255,7 @@ test("AdminAddCourse.test.js Test 9: HelperText error should show for the addCou }); -test("AdminAddCourse.test.js Test 10: HelperText error should show for the addCourseYear text field when input is not a numeric value", async () => { +test("AdminAddCourse.test.js Test 9: HelperText error should show for the addCourseYear text field when input is not a numeric value", async () => { render(); await waitFor(() => { @@ -318,7 +286,7 @@ test("AdminAddCourse.test.js Test 10: HelperText error should show for the addCo }); -test("AdminAddCourse.test.js Test 11: Filling in valid input and clicking the Add Course button should redirect you to course view page, and should contain the new course you just added", async () => { +test("AdminAddCourse.test.js Test 10: Filling in valid input and clicking the Add Course button should redirect you to course view page, and should contain the new course you just added", async () => { render(); await waitFor(() => { @@ -354,7 +322,7 @@ test("AdminAddCourse.test.js Test 11: Filling in valid input and clicking the Ad }); }); -test("AdminAddCourse.test.js Test 12: HelperText errors should show for the addCourseYear and addCourseTerm text fields when the input year is not numeric and the term is not 'Spring', 'Fall', or 'Summer'", async () => { +test("AdminAddCourse.test.js Test 11: HelperText errors should show for the addCourseYear text field when the input year is not numeric", async () => { render(); await waitFor(() => { @@ -382,6 +350,5 @@ test("AdminAddCourse.test.js Test 12: HelperText errors should show for the addC expectElementWithAriaLabelToHaveErrorMessage(cyi, "Year must be a numeric value"); - expectElementWithAriaLabelToHaveErrorMessage(cti, "Term should be either Spring, Fall, or Summer"); }); }); \ No newline at end of file diff --git a/FrontEndReact/src/View/Admin/Add/AddTask/AdminAddAssessmentTask.js b/FrontEndReact/src/View/Admin/Add/AddTask/AdminAddAssessmentTask.js index 0e99d2c47..5228d0cec 100644 --- a/FrontEndReact/src/View/Admin/Add/AddTask/AdminAddAssessmentTask.js +++ b/FrontEndReact/src/View/Admin/Add/AddTask/AdminAddAssessmentTask.js @@ -386,7 +386,7 @@ class AdminAddAssessmentTask extends Component { id="numberOfTeams" name="newPassword" variant='outlined' - label="Number of teams" + label="Maximum number of teams you will use during class for this assessment" value={this.state.numberOfTeams} error={!!errors.numberOfTeams} helperText={errors.numberOfTeams} @@ -406,7 +406,7 @@ class AdminAddAssessmentTask extends Component { id="maxTeamSize" name="setTeamSize" variant='outlined' - label="Max team size" + label="Max team size allowed for each team in class" value={this.state.maxTeamSize} error={!!errors.maxTeamSize} helperText={errors.maxTeamSize} @@ -534,7 +534,7 @@ class AdminAddAssessmentTask extends Component { id="password" name="newPassword" variant='outlined' - label="Password to switch teams" + label="Password to switch teams (Prevents students from switching teams without instructor approval.)" value={password} error={!!errors.password} helperText={errors.password} @@ -549,7 +549,7 @@ class AdminAddAssessmentTask extends Component { id="notes" name="notes" variant='outlined' - label="Instructions to Students/TA's" + label="Instructions for Students/TA's about the Assessment or particular focus areas" value={notes} error={!!errors.notes} helperText={errors.notes} diff --git a/FrontEndReact/src/View/Admin/Add/AddUsers/AdminAddUser.js b/FrontEndReact/src/View/Admin/Add/AddUsers/AdminAddUser.js index eb7e009d4..fe4bab14c 100644 --- a/FrontEndReact/src/View/Admin/Add/AddUsers/AdminAddUser.js +++ b/FrontEndReact/src/View/Admin/Add/AddUsers/AdminAddUser.js @@ -188,7 +188,7 @@ class AdminAddUser extends Component { "first_name": firstName, "last_name": lastName, "email": email, - "lms_id": lmsId, + "lms_id": lmsId !== "" ? lmsId : null, "consent": null, "owner_id": cookies.get('user')['user_id'], "role_id": navbar.props.isSuperAdmin ? 3 : role diff --git a/FrontEndReact/src/View/Admin/View/Reporting/ViewAssessmentStatus/ViewAssessmentStatus.js b/FrontEndReact/src/View/Admin/View/Reporting/ViewAssessmentStatus/ViewAssessmentStatus.js index 4a1813da7..551de6ec8 100644 --- a/FrontEndReact/src/View/Admin/View/Reporting/ViewAssessmentStatus/ViewAssessmentStatus.js +++ b/FrontEndReact/src/View/Admin/View/Reporting/ViewAssessmentStatus/ViewAssessmentStatus.js @@ -65,7 +65,7 @@ export default function ViewAssessmentStatus(props) { var allRatings = []; var avg = 0; var stdev = 0; - var progress = props.completedAssessmentsPercentage; + var progress = +props.completedAssessmentsPercentage.toFixed(2); if (props.completedAssessments !== null && props.completedAssessments.length > 0) { // Iterate through each completed assessment for chosen assessment task diff --git a/FrontEndReact/src/View/Admin/View/ViewCompleteAssessmentTasks/ViewCompleteIndividualAssessmentTasks.js b/FrontEndReact/src/View/Admin/View/ViewCompleteAssessmentTasks/ViewCompleteIndividualAssessmentTasks.js index c98af7e48..b83652f6f 100644 --- a/FrontEndReact/src/View/Admin/View/ViewCompleteAssessmentTasks/ViewCompleteIndividualAssessmentTasks.js +++ b/FrontEndReact/src/View/Admin/View/ViewCompleteAssessmentTasks/ViewCompleteIndividualAssessmentTasks.js @@ -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"; @@ -23,6 +23,8 @@ class ViewCompleteIndividualAssessmentTasks extends Component { showDialog: false, notes: '', notificationSent: false, + isSingleMsg: false, + compATId: null, errors: { notes:'' @@ -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; @@ -68,21 +72,39 @@ class ViewCompleteIndividualAssessmentTasks extends Component { return; } - - genericResourcePUT( - `/assessment_task?assessment_task_id=${chosenAssessmentTask["assessment_task_id"]}¬ification=${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() { @@ -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 ( + this.handleDialog(true, completedAssessmentId)} + label="Message" + align="center" + isOutlined={true} + disabled={notificationSent} + aria-label="Send individual messages" + /> + ) + }else{ + return( +

{''}

+ ) + } + } + } + }, ]; const options = { @@ -294,7 +347,7 @@ class ViewCompleteIndividualAssessmentTasks extends Component { this.handleDialog(false)} isOutlined={false} disabled={notificationSent} aria-label="viewCompletedAssessmentSendNotificationButton" @@ -303,13 +356,12 @@ class ViewCompleteIndividualAssessmentTasks extends Component {
- + - ); } diff --git a/FrontEndReact/src/View/Admin/View/ViewCompleteAssessmentTasks/ViewCompleteTeamAssessmentTasks.js b/FrontEndReact/src/View/Admin/View/ViewCompleteAssessmentTasks/ViewCompleteTeamAssessmentTasks.js index bf3d6afe5..b2fa72f1b 100644 --- a/FrontEndReact/src/View/Admin/View/ViewCompleteAssessmentTasks/ViewCompleteTeamAssessmentTasks.js +++ b/FrontEndReact/src/View/Admin/View/ViewCompleteAssessmentTasks/ViewCompleteTeamAssessmentTasks.js @@ -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 { genericResourcePUT, genericResourcePOST } from "../../../../utility"; import ResponsiveNotification from "../../../Components/SendNotification"; import CourseInfo from "../../../Components/CourseInfo"; @@ -23,6 +23,8 @@ class ViewCompleteTeamAssessmentTasks extends Component { showDialog: false, notes: '', notificationSent: false, + isSingleMsg: false, + compATId: null, errors: { notes:'' @@ -42,10 +44,12 @@ class ViewCompleteTeamAssessmentTasks extends Component { }); }; - handleDialog = () => { + handleDialog = (isSingleMessage, singleCompletedAT) => { this.setState({ showDialog: this.state.showDialog === false ? true : false, - }) + isSingleMsg: isSingleMessage, + compATId: singleCompletedAT, + }); } handleSendNotification = () => { @@ -69,20 +73,39 @@ class ViewCompleteTeamAssessmentTasks extends Component { return; } - genericResourcePUT( - `/assessment_task?assessment_task_id=${chosenAssessmentTask["assessment_task_id"]}¬ification=${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=${true}&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=${true}`, + this, JSON.stringify({ + "date": date, + "notification_message": notes + }) + ).then((result) => { + if (result !== undefined && result.errorMessage === null) { + this.setState({ + showDialog: false, + notificationSent: date, + }); + } + }); + } + }; render() { @@ -255,7 +278,38 @@ class ViewCompleteTeamAssessmentTasks 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 ( + this.handleDialog(true, completedAssessmentId)} + label="Message" + align="center" + isOutlined={true} + disabled={notificationSent} + aria-label="Send individual messages" + /> + ) + }else{ + return( +

{''}

+ ) + } + } + } + }, ]; const options = { @@ -294,7 +348,7 @@ class ViewCompleteTeamAssessmentTasks extends Component { this.handleDialog(false)} isOutlined={false} disabled={notificationSent} aria-label="viewCompletedAssessmentSendNotificationButton" diff --git a/FrontEndReact/src/View/Admin/View/ViewDashboard/Notifications.js b/FrontEndReact/src/View/Admin/View/ViewDashboard/Notifications.js new file mode 100644 index 000000000..b9195bb92 --- /dev/null +++ b/FrontEndReact/src/View/Admin/View/ViewDashboard/Notifications.js @@ -0,0 +1,125 @@ +import React, { Component } from "react"; +import "bootstrap/dist/css/bootstrap.css"; +import "../../../../SBStyles.css"; +import { Box, Typography } from "@mui/material"; +import CustomButton from "../../../Student/View/Components/CustomButton.js"; +import SendMessageModal from '../../../Components/SendMessageModal.js'; +import CustomDataTable from "../../../Components/CustomDataTable.js"; + +class ViewNotification extends Component { + constructor(props) { + super(props); + + this.state = { + errorMessage: null, + isLoaded: null, + showDialog: false, + emailSubject: '', + emailMessage: '', + notificationSent: false, + + errors: { + emailSubject: '', + emailMessage: '', + } + }; + } + + handleChange = (e) => { + const { id, value } = e.target; + + this.setState({ + [id]: value, + errors: { + ...this.state.errors, + [id]: value.trim() === '' ? `${id.charAt(0).toUpperCase() + id.slice(1)} cannot be empty` : '', + }, + }); + }; + + handleDialog = () => { + this.setState({ + showDialog: this.state.showDialog === false ? true : false, + }) + } + + handleSendNotification = () => { + var emailSubject = this.state.emailSubject; + + var emailMessage = this.state.emailMessage; + + if (emailSubject.trim() === '' && emailMessage.trim() === '') { + this.setState({ + errors: { + emailSubject: 'Subject cannot be empty', + emailMessage: 'Message cannot be empty', + }, + }); + + return; + } + + if (emailMessage.trim() === '') { + this.setState({ + errors: { + emailMessage: 'Message cannot be empty', + }, + }); + + return; + } + + + if (emailSubject.trim() === '') { + this.setState({ + errors: { + emailSubject: 'Subject cannot be empty', + }, + }); + + return; + } + + }; + + render() { + var navbar = this.props.navbar; + + var state = navbar.state; + + var notificationSent = state.notificationSent; + + return ( + + + View Notifications + + + + + + + + + + + ); + } +} + +export default ViewNotification; diff --git a/FrontEndReact/src/View/Components/SendMessageModal.js b/FrontEndReact/src/View/Components/SendMessageModal.js new file mode 100644 index 000000000..51509df5d --- /dev/null +++ b/FrontEndReact/src/View/Components/SendMessageModal.js @@ -0,0 +1,86 @@ +import React from "react"; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import { TextField } from "@mui/material"; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; + +export default function SendMessageModal ( props ) { + return ( + + + + {"Send Message to Admins"} + + + + + Use this form to send a message to all admin users. This notification will be delivered to their registered email addresses. + + + + + + + + + + + + + + {/* */} + + + + + ); +} diff --git a/FrontEndReact/src/View/Navbar/AppState.js b/FrontEndReact/src/View/Navbar/AppState.js index 87fc14b0c..054ddaace 100644 --- a/FrontEndReact/src/View/Navbar/AppState.js +++ b/FrontEndReact/src/View/Navbar/AppState.js @@ -29,7 +29,8 @@ import StudentNavigation from '../Components/StudentNavigation.js'; import ReportingDashboard from '../Admin/View/Reporting/ReportingDashboard.js'; import AdminAddCustomRubric from '../Admin/Add/AddCustomRubric/AdminAddCustomRubric.js'; import AdminViewCustomRubrics from '../Admin/View/ViewCustomRubrics/AdminViewCustomRubrics.js'; -import UserAccount from './UserAccount.js' +import UserAccount from './UserAccount.js'; +import ViewNotification from '../Admin/View/ViewDashboard/Notifications.js'; class AppState extends Component { @@ -468,22 +469,37 @@ class AppState extends Component { Users - - + +
+ + +
+
- @@ -834,6 +850,17 @@ class AppState extends Component { /> } + {this.state.activeTab==="ViewNotification" && + + + + + } ) } diff --git a/FrontEndReact/src/View/Student/View/CompletedAssessmentTask/ViewCompletedAssessmentTasks.js b/FrontEndReact/src/View/Student/View/CompletedAssessmentTask/ViewCompletedAssessmentTasks.js index 9728b156f..fbce13e9b 100644 --- a/FrontEndReact/src/View/Student/View/CompletedAssessmentTask/ViewCompletedAssessmentTasks.js +++ b/FrontEndReact/src/View/Student/View/CompletedAssessmentTask/ViewCompletedAssessmentTasks.js @@ -3,7 +3,7 @@ import 'bootstrap/dist/css/bootstrap.css'; import CustomDataTable from "../../../Components/CustomDataTable"; import { IconButton } from "@mui/material"; import VisibilityIcon from '@mui/icons-material/Visibility'; -import { getHumanReadableDueDate } from "../../../../utility"; +import { genericResourcePOST, getHumanReadableDueDate } from "../../../../utility"; @@ -95,7 +95,19 @@ class ViewCompletedAssessmentTasks extends Component { { navbar.setAssessmentTaskInstructions(assessmentTasks, atId, completedAssessments, { readOnly: true }); - }} + var singluarCompletedAssessment = null; + if (completedAssessments) { + singluarCompletedAssessment = completedAssessments.find(completedAssessment => completedAssessment.assessment_task_id === atId) ?? null; + } + genericResourcePOST( + `/rating`, + this, + JSON.stringify({ + "user_id" : singluarCompletedAssessment.user_id, + "completed_assessment_id": singluarCompletedAssessment.completed_assessment_id, + }), + ); + }} aria-label="completedAssessmentTasksViewIconButton" > diff --git a/FrontEndReact/src/utility.js b/FrontEndReact/src/utility.js index 6de840bb5..4c8f3d36d 100644 --- a/FrontEndReact/src/utility.js +++ b/FrontEndReact/src/utility.js @@ -272,37 +272,27 @@ export function getDueDateString(dueDate) { } export function getHumanReadableDueDate(dueDate, timeZone) { - dueDate = dueDate.substring(5); + const date = new Date(dueDate); - var month = Number(dueDate.substring(0, 2)) - 1; + const month = date.getMonth(); - dueDate = dueDate.substring(3); + const day = date.getDate(); - var day = Number(dueDate.substring(0, 2)); - - dueDate = dueDate.substring(3); - - var hour = Number(dueDate.substring(0, 2)); - - var twelveHourClock = hour < 12 ? "am": "pm"; - - hour = hour > 12 ? (hour % 12) : hour; - - hour = hour === 0 ? 12 : hour; - - dueDate = dueDate.substring(3); - - var minute = Number(dueDate.substring(0, 2)); + const hour = date.getHours(); + const minute = date.getMinutes(); + const monthNames = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; + + const twelveHourClock = hour < 12 ? "am": "pm"; - var minutesString = minute < 10 ? ("0" + minute): minute; - - var timeString = `${hour}:${minutesString}${twelveHourClock}`; - - var dueDateString = `${monthNames[month]} ${day} at ${timeString} ${timeZone ? timeZone : ""}`; - - return dueDateString; + const displayHour = hour > 12 ? (hour % 12) : (hour === 0 ? 12 : hour); + + const minutesString = minute < 10 ? ("0" + minute): minute; + + const timeString = `${displayHour}:${minutesString}${twelveHourClock}`; + + return `${monthNames[month]} ${day} at ${timeString} ${timeZone ? timeZone : ""}`; } /** diff --git a/README.md b/README.md index d46b018d8..acdfa0f07 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,8 @@ for analysis. - Python 3.12 and up. +- MySQL-Server. + - Homebrew 4.2.18 and up. - Redis 7.2.4 and up. @@ -117,6 +119,49 @@ NOTE: - WINDOWS DEVELOPERS ARE NO LONGER SUPPORTED. +## Setting up the MySQL Environment: ## + +- Run the following command to install MySQL-Server +on Linux: + + sudo apt install mysql-server + +- Run the following command to install MySQL-Server +on MacOS: + + brew install mysql + +- Run the following command to start MySQL-Server +on MacOS: + + brew services start mysql + +- Run the following command to start MySQL-Server +in a new terminal: + + sudo mysql -u root + +- Next use these commands to create an account +named skillbuilder and set the password to +"WasPogil1#" + + CREATE DATABASE account; + CREATE USER 'skillbuilder'@'localhost' IDENTIFIED BY 'WasPogil1#'; + GRANT ALL PRIVILEGES ON *.* TO 'skillbuilder'@'localhost'; + FLUSH PRIVILEGES; + exit; + +NOTE: + +- The password should be changed for deployment. + +- Once this is done, you can use: `setupEnv.py` as normal +to create the database. If for any reason you want to +access the database directly, run the following command: + + mysql -u skillbuilder -p + +and then type the password. ## Installing requirements ## diff --git a/compose.yml b/compose.yml index e33bf294c..548cf4c07 100644 --- a/compose.yml +++ b/compose.yml @@ -6,7 +6,10 @@ services: ports: - "127.0.0.1:5050:5000" depends_on: - - redis + redis: + condition: service_started + mysql: + condition: service_healthy volumes: # Mount the source files inside the container to allow for Flask hot reloading to work - "./BackEndFlask/Functions:/app/Functions:rw" @@ -18,6 +21,10 @@ services: environment: - REDIS_HOST=redis - FLASK_DEBUG=1 + - MYSQL_HOST=mysql + - MYSQL_USER=skillbuilder + - MYSQL_PASSWORD=WasPogil1# + - MYSQL_DATABASE=account redis: image: redis:7.2.4 @@ -44,9 +51,30 @@ services: networks: - app-network + mysql: + image: mysql:8.0 + restart: always + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: account + MYSQL_USER: skillbuilder + MYSQL_PASSWORD: WasPogil1# + ports: + - "5551:3306" + volumes: + - mysql-data:/var/lib/mysql + networks: + - app-network + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + networks: app-network: driver: bridge volumes: + mysql-data: frontend-cache: