diff --git a/BackEndFlask/controller/Routes/Assessment_task_routes.py b/BackEndFlask/controller/Routes/Assessment_task_routes.py index 934d6bc27..9aedd5a59 100644 --- a/BackEndFlask/controller/Routes/Assessment_task_routes.py +++ b/BackEndFlask/controller/Routes/Assessment_task_routes.py @@ -22,7 +22,9 @@ get_assessment_tasks, get_assessment_task, create_assessment_task, - replace_assessment_task + replace_assessment_task, + toggle_lock_status, + toggle_published_status, ) from models.completed_assessment import ( @@ -101,8 +103,7 @@ def get_all_assessment_tasks(): user_course.course_id ) - for assessment_task in assessment_tasks: - all_assessment_tasks.append(assessment_task) + for assessment_task in assessment_tasks: all_assessment_tasks.append(assessment_task) return create_good_response( assessment_tasks_schema.dump(all_assessment_tasks), @@ -253,6 +254,47 @@ def update_assessment_task(): ) +@bp.route('/assessment_task_toggle_lock', methods=['PUT']) +@jwt_required() +@bad_token_check() +@AuthCheck() +def toggle_lock_status_route(): + try: + assessmentTaskId = request.args.get('assessmentTaskId') + + toggle_lock_status(assessmentTaskId) + + return create_good_response( + assessment_task_schema.dump(get_assessment_task(assessmentTaskId)), + 201, + "assessment_tasks" + ) + except Exception as e: + return create_bad_response( + f"An error occurred toggling the lock status for assessment {e}", "assessment_tasks", 400 + ) + + +@bp.route('/assessment_task_toggle_published', methods=['PUT']) +@jwt_required() +@bad_token_check() +@AuthCheck() +def toggle_published_status_route(): + try: + assessmentTaskId = request.args.get('assessmentTaskId') + + toggle_published_status(assessmentTaskId) + + return create_good_response( + assessment_task_schema.dump(get_assessment_task(assessmentTaskId)), + 201, + "assessment_tasks" + ) + except Exception as e: + return create_bad_response( + f"An error occurred toggling the published status for assessment {e}", "assessment_tasks", 400 + ) + # /assessment_task/ POST # copies over assessment_tasks from an existing course to another course @@ -314,9 +356,11 @@ class Meta: "comment", "number_of_teams", "max_team_size", - "notification_sent" + "notification_sent", + "locked", + "published", ) assessment_task_schema = AssessmentTaskSchema() -assessment_tasks_schema = AssessmentTaskSchema(many=True) \ No newline at end of file +assessment_tasks_schema = AssessmentTaskSchema(many=True) diff --git a/BackEndFlask/controller/Routes/Completed_assessment_routes.py b/BackEndFlask/controller/Routes/Completed_assessment_routes.py index 5b44a59f3..c41c21870 100644 --- a/BackEndFlask/controller/Routes/Completed_assessment_routes.py +++ b/BackEndFlask/controller/Routes/Completed_assessment_routes.py @@ -15,7 +15,10 @@ create_completed_assessment, replace_completed_assessment, completed_assessment_exists, - get_completed_assessment_count + get_completed_assessment_count, + toggle_lock_status, + make_complete_assessment_locked, + make_complete_assessment_unlocked, ) from models.queries import ( @@ -29,6 +32,51 @@ from models.assessment_task import get_assessment_tasks_by_course_id +@bp.route('/completed_assessment_toggle_lock', methods=['PUT']) +@jwt_required() +@bad_token_check() +@AuthCheck() +def toggle_complete_assessment_lock_status(): + try: + assessmentTaskId = request.args.get('assessment_task_id') + toggle_lock_status(assessmentTaskId) + return create_good_response(None, 201, "completed_assessments") + + except Exception as e: + return create_bad_response( + f"An error occurred toggling CAT lock: {e}", "completed_assessments", 400 + ) + +@bp.route('/completed_assessment_lock', methods=['PUT']) +@jwt_required() +@bad_token_check() +@AuthCheck() +def lock_complete_assessment(): + try: + assessmentTaskId = request.args.get('assessment_task_id') + make_complete_assessment_locked(assessmentTaskId) + return create_good_response(None, 201, "completed_assessments") + + except Exception as e: + return create_bad_response( + f"An error occurred locking a CAT: {e}", "completed_assessments", 400 + ) + +@bp.route('/completed_assessment_unlock', methods=['PUT']) +@jwt_required() +@bad_token_check() +@AuthCheck() +def unlock_complete_assessment(): + try: + assessmentTaskId = request.args.get('assessment_task_id') + make_complete_assessment_unlocked(assessmentTaskId) + return create_good_response(None, 201, "completed_assessments") + + except Exception as e: + return create_bad_response( + f"An error occurred unlocking a CAT: {e}", "completed_assessments", 400 + ) + @bp.route('/completed_assessment', methods=['GET']) @jwt_required() @bad_token_check() @@ -96,7 +144,7 @@ def get_all_completed_assessments(): completed_assessments = get_completed_assessment_with_team_name(assessment_task_id) else: 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} @@ -106,7 +154,7 @@ def get_all_completed_assessments(): if request.args and request.args.get("assessment_task_id"): assessment_task_id = int(request.args.get("assessment_task_id")) - + get_assessment_task(assessment_task_id) # Trigger an error if not exists. completed_assessments = get_completed_assessment_with_team_name(assessment_task_id) @@ -119,7 +167,7 @@ def get_all_completed_assessments(): for assessment in completed_assessments ] return create_good_response(result, 200, "completed_assessments") - + if request.args and request.args.get("completed_assessment_task_id"): completed_assessment_task_id = int(request.args.get("completed_assessment_task_id")) one_completed_assessment = get_completed_assessment_with_team_name(completed_assessment_task_id) @@ -222,9 +270,10 @@ class Meta: 'team_name', 'user_id', 'first_name', - 'last_name', + 'last_name', 'initial_time', 'done', + 'locked', 'last_update', 'rating_observable_characteristics_suggestions_data', 'course_id', @@ -234,4 +283,4 @@ class Meta: completed_assessment_schema = CompletedAssessmentSchema() -completed_assessment_schemas = CompletedAssessmentSchema(many=True) \ No newline at end of file +completed_assessment_schemas = CompletedAssessmentSchema(many=True) diff --git a/BackEndFlask/controller/Routes/Rating_routes.py b/BackEndFlask/controller/Routes/Rating_routes.py index b4bd18895..6c0b5f314 100644 --- a/BackEndFlask/controller/Routes/Rating_routes.py +++ b/BackEndFlask/controller/Routes/Rating_routes.py @@ -94,4 +94,4 @@ class Meta: ) student_feedback_schema = StudentFeedbackSchema() -student_feedbacks_schema = StudentFeedbackSchema(many=True) \ No newline at end of file +student_feedbacks_schema = StudentFeedbackSchema(many=True) diff --git a/BackEndFlask/controller/Routes/Team_routes.py b/BackEndFlask/controller/Routes/Team_routes.py index 9e7de4e66..f8ae68c76 100644 --- a/BackEndFlask/controller/Routes/Team_routes.py +++ b/BackEndFlask/controller/Routes/Team_routes.py @@ -22,7 +22,9 @@ from models.queries import ( get_team_by_course_id_and_user_id, - get_all_nonfull_adhoc_teams + get_all_nonfull_adhoc_teams, + get_students_by_team_id, + get_team_users, ) @bp.route('/team', methods = ['GET']) @@ -45,6 +47,7 @@ def get_all_teams(): except Exception as e: return create_bad_response(f"An error occurred retrieving all teams: {e}", "teams", 400) +# WIP @bp.route('/team_by_user', methods = ['GET']) @jwt_required() @bad_token_check() @@ -57,11 +60,79 @@ def get_all_teams_by_user(): teams = get_team_by_course_id_and_user_id(course_id, user_id) - return create_good_response(teams_schema.dump(teams), 200, "teams") + json = [] + + for i in range(0, len(teams)): + team_id = teams[i].team_id + team_name = teams[i].team_name + team_users = get_team_users(course_id, team_id, user_id) + users = [] + + # Get the names of each team member w/ the LName shortened. + for user in team_users: + # users.append((user[1]+' '+user[2][0]+'.')) + users.append(user[1]) + + data = { + 'team_id': teams[i].team_id, + 'team_name': teams[i].team_name, + 'observer_id': teams[i].observer_id, + 'course_id': teams[i].course_id, + 'date_created': teams[i].date_created, + 'active_until': teams[i].active_until, + 'team_users': users, + } + json.append(data) + + return create_good_response(json, 200, "teams") + # return create_good_response(teams_schema.dump(teams), 200, "teams") except Exception as e: return create_bad_response(f"An error occurred retrieving all teams: {e}", "teams", 400) +# @bp.route('/team_by_user', methods=['GET']) +# @jwt_required() +# @bad_token_check() +# @AuthCheck() +# def get_all_teams_by_user(): +# try: +# if request.args and request.args.get("course_id"): +# course_id = int(request.args.get("course_id")) +# user_id = int(request.args.get("user_id")) +# +# # Get the teams that the user is associated with +# teams = get_team_by_course_id_and_user_id(course_id, user_id) +# +# # Prepare a list to store teams and their users +# teams_with_users = [] +# +# for team in teams: +# # Access team data directly since 'team' is a Team object +# team_id = team.team_id +# team_name = team.team_name +# +# # Get the users of the current team +# team_users = get_team_users(course_id, team_id, user_id) +# +# # Add team information along with its users to the result +# teams_with_users.append({ +# "team": { +# "team_id": team_id, +# "team_name": team_name, +# "course_id": team.course_id, +# "observer_id": team.observer_id, +# "date_created": team.date_created, +# "active_until": team.active_until +# }, +# "users": team_users +# }) +# +# # Return the teams along with their users +# return create_good_response(teams_with_users, 200, "teams_with_users") +# +# except Exception as e: +# return create_bad_response(f"An error occurred retrieving all teams: {e}", "teams", 400) + @bp.route('/team_by_observer', methods = ['GET']) @jwt_required() @bad_token_check() @@ -207,6 +278,21 @@ def delete_selected_teams(): except Exception as e: return create_bad_response(f"An error occurred deleting a team: {e}", "teams", 400) +@bp.route('/get_all_team_users', methods=['GET']) +@jwt_required() +@bad_token_check() +@AuthCheck() +def get_all_team_users(): + try: + course_id = request.args.get("course_id") + team_id = request.args.get("team_id") + users = get_students_by_team_id(course_id, team_id) + users_json = [{"name": user[1]} for user in users] + return create_good_response(users_json, 200, "teams") + except Exception as e: + return create_bad_response(f"An error occurred getting team users: {e}", "teams", 400) + + class TeamSchema(ma.Schema): class Meta: fields = ( diff --git a/BackEndFlask/core/.cron_setup_complete b/BackEndFlask/core/.cron_setup_complete new file mode 100644 index 000000000..774815e9e --- /dev/null +++ b/BackEndFlask/core/.cron_setup_complete @@ -0,0 +1 @@ +Cron setup completed at: Wed Jan 1 02:58:56 PM CST 2025 diff --git a/BackEndFlask/core/__init__.py b/BackEndFlask/core/__init__.py index d70f6939a..73acc7310 100644 --- a/BackEndFlask/core/__init__.py +++ b/BackEndFlask/core/__init__.py @@ -1,4 +1,5 @@ from flask_jwt_extended import JWTManager +from flask_migrate import Migrate from flask_marshmallow import Marshmallow from flask_sqlalchemy import SQLAlchemy from models.tests import testing @@ -96,6 +97,7 @@ def setup_cron_jobs(): db = SQLAlchemy(app) ma = Marshmallow(app) +migrate = Migrate(app, db) redis_host = os.environ.get('REDIS_HOST', 'localhost') diff --git a/BackEndFlask/dbinsert.py b/BackEndFlask/dbinsert.py new file mode 100644 index 000000000..71d034dee --- /dev/null +++ b/BackEndFlask/dbinsert.py @@ -0,0 +1,83 @@ +#!/usr/local/bin/python3 + +from core import app, db +from models.user import create_user +import argparse +import sys + + +def insert_student(fname, lname, email, password, lms_id): + print("Inserting new student:") + print(f" fname: {fname}") + print(f" lname: {lname}") + print(f" email: {email}") + print(f" password: {password}") + print(f" lms_id: {lms_id}") + + with app.app_context(): + create_user({ + "first_name": fname, + "last_name": lname, + "email": email, + "password": password, + "lms_id": lms_id, + "consent": None, + "owner_id": 2, + "role_id": 5 + }) + + +def insert_admin(fname, lname, email, password): + print("Inserting new admin:") + print(f" fname: {fname}") + print(f" lname: {lname}") + print(f" email: {email}") + print(f" password: {password}") + + with app.app_context(): + create_user({ + "first_name": fname, + "last_name": lname, + "email": email, + "password": password, + "lms_id": 1, + "consent": None, + "owner_id": 1, + "role_id": 3 + }) + + +def main(): + parser = argparse.ArgumentParser(description="Insert user information into the database.") + + parser.add_argument("--new-student", action="store_true", help="Indicates that a new student is being added") + parser.add_argument("--fname", type=str, help="First name of the user", required=False) + parser.add_argument("--lname", type=str, help="Last name of the user", required=False) + parser.add_argument("--email", type=str, help="Email of the user", required=False) + parser.add_argument("--password", type=str, help="Password of the user", required=False) + parser.add_argument("--lms", type=str, help="LMS ID of the student", required=False) + + parser.add_argument("--new-admin", action="store_true", help="Indicates that a new admin is being added") + + args = parser.parse_args() + + if not args.new_student and not args.new_admin: + print("Error: You must specify either --new-student or --new-admin.") + sys.exit(1) + + if args.new_student: + if not (args.fname and args.lname and args.email and args.password and args.lms): + print("Error: Missing required fields for student (fname, lname, email, password, lms).") + sys.exit(1) + insert_student(args.fname, args.lname, args.email, args.password, args.lms) + + if args.new_admin: + if not (args.fname and args.lname and args.email and args.password): + print("Error: Missing required fields for admin (fname, lname, email, password).") + sys.exit(1) + insert_admin(args.fname, args.lname, args.email, args.password) + + +if __name__ == "__main__": + main() + diff --git a/BackEndFlask/migrations/README b/BackEndFlask/migrations/README new file mode 100644 index 000000000..0e0484415 --- /dev/null +++ b/BackEndFlask/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/BackEndFlask/migrations/alembic.ini b/BackEndFlask/migrations/alembic.ini new file mode 100644 index 000000000..ec9d45c26 --- /dev/null +++ b/BackEndFlask/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/BackEndFlask/migrations/env.py b/BackEndFlask/migrations/env.py new file mode 100644 index 000000000..4c9709271 --- /dev/null +++ b/BackEndFlask/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/BackEndFlask/migrations/script.py.mako b/BackEndFlask/migrations/script.py.mako new file mode 100644 index 000000000..2c0156303 --- /dev/null +++ b/BackEndFlask/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/BackEndFlask/migrations/versions/9820393e9e55_initial_migration.py b/BackEndFlask/migrations/versions/9820393e9e55_initial_migration.py new file mode 100644 index 000000000..e55e26df7 --- /dev/null +++ b/BackEndFlask/migrations/versions/9820393e9e55_initial_migration.py @@ -0,0 +1,40 @@ +"""Initial migration + +Revision ID: 9820393e9e55 +Revises: +Create Date: 2025-01-01 15:49:53.243298 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9820393e9e55' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('AssessmentTask', schema=None) as batch_op: + batch_op.add_column(sa.Column('locked', sa.Boolean(), nullable=False)) + batch_op.add_column(sa.Column('published', sa.Boolean(), nullable=False)) + + with op.batch_alter_table('CompletedAssessment', schema=None) as batch_op: + batch_op.add_column(sa.Column('locked', sa.Boolean(), nullable=False)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('CompletedAssessment', schema=None) as batch_op: + batch_op.drop_column('locked') + + with op.batch_alter_table('AssessmentTask', schema=None) as batch_op: + batch_op.drop_column('published') + batch_op.drop_column('locked') + + # ### end Alembic commands ### diff --git a/BackEndFlask/models/assessment_task.py b/BackEndFlask/models/assessment_task.py index 243e201df..018a455df 100644 --- a/BackEndFlask/models/assessment_task.py +++ b/BackEndFlask/models/assessment_task.py @@ -23,11 +23,11 @@ def __init__(self): def __str__(self): return self.message - + class InvalidMaxTeamSize(Exception): def __init__(self): self.message = "Number of people on a team must be greater than 0." - + def __str__(self): return self.message @@ -39,7 +39,7 @@ def validate_number_of_teams(number_of_teams): raise InvalidNumberOfTeams() except ValueError: raise InvalidNumberOfTeams() - + def validate_max_team_size(max_team_size): if max_team_size is not None: try: @@ -75,10 +75,10 @@ def get_assessment_tasks_by_team_id(team_id): @error_log def get_assessment_task(assessment_task_id): one_assessment_task = AssessmentTask.query.filter_by(assessment_task_id=assessment_task_id).first() - + if one_assessment_task is None: - raise InvalidAssessmentTaskID(assessment_task_id) - + raise InvalidAssessmentTaskID(assessment_task_id) + return one_assessment_task @error_log @@ -100,13 +100,15 @@ def create_assessment_task(assessment_task): due_date=datetime.strptime(assessment_task["due_date"], '%Y-%m-%dT%H:%M:%S.%fZ'), time_zone=assessment_task["time_zone"], show_suggestions=assessment_task["show_suggestions"], + locked=False, + published=True, show_ratings=assessment_task["show_ratings"], unit_of_assessment=assessment_task["unit_of_assessment"], create_team_password=assessment_task["create_team_password"], comment=assessment_task["comment"], number_of_teams=assessment_task["number_of_teams"], max_team_size=assessment_task["max_team_size"], - notification_sent=None + notification_sent=None, ) db.session.add(new_assessment_task) @@ -120,7 +122,7 @@ def load_demo_admin_assessment_task(): "assessment_task_name": "Critical Thinking Assessment", "comment": "An example comment", "create_team_password": "at_cta", - "due_date": "2023-04-24T08:30:00", + "due_date": "2027-04-24T08:30:00", "number_of_teams": None, "max_team_size": None, "role_id": 4, @@ -128,7 +130,9 @@ def load_demo_admin_assessment_task(): "show_ratings": True, "show_suggestions": True, "time_zone": "EST", - "unit_of_assessment": False + "unit_of_assessment": False, + "locked": False, + "published": True, }, { # Assessment Task 2 "assessment_task_name": "Formal Communication Assessment", @@ -142,13 +146,15 @@ def load_demo_admin_assessment_task(): "show_ratings": True, "show_suggestions": False, "time_zone": "EST", - "unit_of_assessment": False + "unit_of_assessment": False, + "locked": False, + "published": True, }, { # Assessment Task 3 "assessment_task_name": "Information Processing Assessment", "comment": None, "create_team_password": "at_ipa", - "due_date": "2023-02-14T08:00:00", + "due_date": "2027-02-14T08:00:00", "number_of_teams": None, "max_team_size": None, "role_id": 5, @@ -156,13 +162,15 @@ def load_demo_admin_assessment_task(): "show_ratings": False, "show_suggestions": True, "time_zone": "EST", - "unit_of_assessment": False + "unit_of_assessment": False, + "locked": False, + "published": True, }, { # Assessment Task 4 "assessment_task_name": "Interpersonal Communication", "comment": None, "create_team_password": "at_ic", - "due_date": "2023-03-05T09:30:00", + "due_date": "2025-03-05T09:30:00", "number_of_teams": None, "max_team_size": None, "role_id": 5, @@ -170,7 +178,9 @@ def load_demo_admin_assessment_task(): "show_ratings": False, "show_suggestions": False, "time_zone": "EST", - "unit_of_assessment": False + "unit_of_assessment": False, + "locked": True, + "published": True, }, { # Assessment Task 5 "assessment_task_name": "Management Assessment", @@ -184,7 +194,9 @@ def load_demo_admin_assessment_task(): "show_ratings": True, "show_suggestions": True, "time_zone": "EST", - "unit_of_assessment": True + "unit_of_assessment": True, + "locked": False, + "published": True, }, { # Assessment Task 6 "assessment_task_name": "Problem Solving Assessment", @@ -198,7 +210,9 @@ def load_demo_admin_assessment_task(): "show_ratings": False, "show_suggestions": False, "time_zone": "EST", - "unit_of_assessment": False + "unit_of_assessment": False, + "locked": False, + "published": True, }, { # Assessment Task 7 "assessment_task_name": "Teamwork Assessment", @@ -212,7 +226,9 @@ def load_demo_admin_assessment_task(): "show_ratings": False, "show_suggestions": True, "time_zone": "EST", - "unit_of_assessment": True + "unit_of_assessment": True, + "locked": False, + "published": True, }, { # Assessment Task 8 "assessment_task_name": "Critical Thinking Assessment 2", @@ -226,7 +242,9 @@ def load_demo_admin_assessment_task(): "show_ratings": True, "show_suggestions": True, "time_zone": "CST", - "unit_of_assessment": True + "unit_of_assessment": True, + "locked": False, + "published": True, }, { # Assessment Task 9 "assessment_task_name": "AAAAAAAAAAAA", @@ -240,7 +258,9 @@ def load_demo_admin_assessment_task(): "show_ratings": True, "show_suggestions": True, "time_zone": "EST", - "unit_of_assessment": True + "unit_of_assessment": True, + "locked": False, + "published": True, }, { # Assessment Task 10 "assessment_task_name": "CCCCCCCCCCCCC", @@ -254,7 +274,9 @@ def load_demo_admin_assessment_task(): "show_ratings": True, "show_suggestions": True, "time_zone": "PST", - "unit_of_assessment": True + "unit_of_assessment": True, + "locked": False, + "published": True, }, { # Assessment Task 11 "assessment_task_name": "DDDDDDDDDDDDDD", @@ -268,7 +290,9 @@ def load_demo_admin_assessment_task(): "show_ratings": True, "show_suggestions": True, "time_zone": "PST", - "unit_of_assessment": True + "unit_of_assessment": True, + "locked": False, + "published": True, }, { # Assessment Task 12 "assessment_task_name": "Student 1", @@ -282,7 +306,9 @@ def load_demo_admin_assessment_task(): "show_ratings": True, "show_suggestions": True, "time_zone": "EST", - "unit_of_assessment": False + "unit_of_assessment": False, + "locked": False, + "published": True, }, { # Assessment Task 13 "assessment_task_name": "Student 2 Individ", @@ -296,7 +322,9 @@ def load_demo_admin_assessment_task(): "show_ratings": True, "show_suggestions": True, "time_zone": "PST", - "unit_of_assessment": False + "unit_of_assessment": False, + "locked": False, + "published": True, }, { # Assessment Task 14 "assessment_task_name": "UI 1", @@ -310,7 +338,9 @@ def load_demo_admin_assessment_task(): "show_ratings": True, "show_suggestions": True, "time_zone": "PST", - "unit_of_assessment": False + "unit_of_assessment": False, + "locked": False, + "published": True, }, { # Assessment Task 15 "assessment_task_name": "UI 2", @@ -324,7 +354,9 @@ def load_demo_admin_assessment_task(): "show_ratings": True, "show_suggestions": True, "time_zone": "PST", - "unit_of_assessment": False + "unit_of_assessment": False, + "locked": False, + "published": True, }, { # Assessment Task 16 "assessment_task_name": "Calc 1", @@ -338,7 +370,9 @@ def load_demo_admin_assessment_task(): "show_ratings": True, "show_suggestions": True, "time_zone": "PST", - "unit_of_assessment": False + "unit_of_assessment": False, + "locked": False, + "published": True, }, { # Assessment Task 17 "assessment_task_name": "Calc 2", @@ -352,7 +386,9 @@ def load_demo_admin_assessment_task(): "show_ratings": True, "show_suggestions": True, "time_zone": "PST", - "unit_of_assessment": False + "unit_of_assessment": False, + "locked": False, + "published": True, }, { # Assessment Task 18 "assessment_task_name": "Phys 1", @@ -366,7 +402,9 @@ def load_demo_admin_assessment_task(): "show_ratings": True, "show_suggestions": True, "time_zone": "MST", - "unit_of_assessment": False + "unit_of_assessment": False, + "locked": False, + "published": True, }, { # Assessment Task 19 "assessment_task_name": "Phys 2", @@ -380,8 +418,10 @@ def load_demo_admin_assessment_task(): "show_ratings": True, "show_suggestions": True, "time_zone": "MST", - "unit_of_assessment": False - } + "unit_of_assessment": False, + "locked": False, + "published": True, + }, ] for assessment in list_of_assessment_tasks: @@ -393,12 +433,14 @@ def load_demo_admin_assessment_task(): "rubric_id": assessment["rubric_id"], "role_id": assessment["role_id"], "show_suggestions": assessment["show_suggestions"], + "locked": assessment["locked"], + "published": assessment["published"], "show_ratings": assessment["show_ratings"], "unit_of_assessment": assessment["unit_of_assessment"], "create_team_password": assessment["create_team_password"], "comment": assessment["comment"], "number_of_teams": assessment["number_of_teams"], - "max_team_size": assessment["max_team_size"] + "max_team_size": assessment["max_team_size"], }) @error_log @@ -451,4 +493,18 @@ def toggle_notification_sent_to_true(assessment_task_id, date): db.session.commit() - return one_assessment_task \ No newline at end of file + return one_assessment_task + +@error_log +def toggle_lock_status(assessment_task_id): + one_assessment_task = AssessmentTask.query.filter_by(assessment_task_id=assessment_task_id).first() + one_assessment_task.locked = not one_assessment_task.locked + db.session.commit() + return one_assessment_task + +@error_log +def toggle_published_status(assessment_task_id): + one_assessment_task = AssessmentTask.query.filter_by(assessment_task_id=assessment_task_id).first() + one_assessment_task.published = not one_assessment_task.published + db.session.commit() + return one_assessment_task diff --git a/BackEndFlask/models/completed_assessment.py b/BackEndFlask/models/completed_assessment.py index 2603e6f74..763d6fa1f 100644 --- a/BackEndFlask/models/completed_assessment.py +++ b/BackEndFlask/models/completed_assessment.py @@ -84,7 +84,8 @@ def create_completed_assessment(completed_assessment_data): initial_time=datetime.strptime(completed_assessment_data["initial_time"], '%Y-%m-%dT%H:%M:%S.%fZ'), last_update=None if completed_assessment_data["last_update"] is None else datetime.strptime(completed_assessment_data["last_update"], '%Y-%m-%dT%H:%M:%S.%fZ'), rating_observable_characteristics_suggestions_data=completed_assessment_data["rating_observable_characteristics_suggestions_data"], - done=completed_assessment_data["done"] + done=completed_assessment_data["done"], + locked=False, ) db.session.add(completed_assessment_data) @@ -92,11 +93,48 @@ def create_completed_assessment(completed_assessment_data): return completed_assessment_data +@error_log +def toggle_lock_status(completed_assessment_id): + one_completed_assessment = CompletedAssessment.query.filter_by(completed_assessment_id=completed_assessment_id).first() + + if one_completed_assessment is None: + raise InvalidCRID(completed_assessment_id) + + one_completed_assessment.locked = not one_completed_assessment.locked + db.session.commit() + + return one_completed_assessment + +@error_log +def make_complete_assessment_locked(completed_assessment_id): + one_completed_assessment = CompletedAssessment.query.filter_by(completed_assessment_id=completed_assessment_id).first() + + if one_completed_assessment is None: + raise InvalidCRID(completed_assessment_id) + + one_completed_assessment.locked = True + db.session.commit() + + return one_completed_assessment + +@error_log +def make_complete_assessment_unlocked(completed_assessment_id): + one_completed_assessment = CompletedAssessment.query.filter_by(completed_assessment_id=completed_assessment_id).first() + + if one_completed_assessment is None: + raise InvalidCRID(completed_assessment_id) + + one_completed_assessment.locked = False + db.session.commit() + + return one_completed_assessment + def load_demo_completed_assessment(): list_of_completed_assessments = [ { # Completed Assessment id 1 "assessment_task_id": 1, "done": True, + "locked": False, "initial_time": "2024-01-28T21:08:36.376000", "last_update": "2024-02-01T21:01:33.458000", "rating_observable_characteristics_suggestions_data": { @@ -200,6 +238,7 @@ def load_demo_completed_assessment(): { # Completed Assessment id 2 "assessment_task_id": 2, "done": True, + "locked": False, "initial_time": "2024-01-28T21:08:55.755000", "last_update": "2024-02-01T21:02:45.652000", "rating_observable_characteristics_suggestions_data": { @@ -318,6 +357,7 @@ def load_demo_completed_assessment(): { # Completed Assessment id 3 "assessment_task_id": 5, "done": True, + "locked": False, "initial_time": "2024-01-28T21:09:24.685000", "last_update": "2024-02-01T21:03:25.208000", "rating_observable_characteristics_suggestions_data": { @@ -391,6 +431,7 @@ def load_demo_completed_assessment(): { # Completed Assessment id 4 "assessment_task_id": 8, "done": True, + "locked": False, "initial_time": "2024-01-28T21:22:03.218000", "last_update": "2024-02-01T21:04:16.909000", "rating_observable_characteristics_suggestions_data": { @@ -494,6 +535,7 @@ def load_demo_completed_assessment(): { # Completed Assessment id 5 "assessment_task_id": 9, "done": True, + "locked": False, "initial_time": "2024-01-28T21:26:21.901000", "last_update": "2024-02-01T21:05:39.666000", "rating_observable_characteristics_suggestions_data": { @@ -612,6 +654,7 @@ def load_demo_completed_assessment(): { # Completed Assessment id 6 "assessment_task_id": 10, "done": True, + "locked": False, "initial_time": "2024-01-30T15:11:00.760000", "last_update": "2024-02-01T21:06:49.714000", "rating_observable_characteristics_suggestions_data": { @@ -730,6 +773,7 @@ def load_demo_completed_assessment(): { # Completed Assessment id 7 "assessment_task_id": 11, "done": True, + "locked": False, "initial_time": "2024-01-30T15:12:56.525000", "last_update": "2024-02-05T16:26:42.377000", "rating_observable_characteristics_suggestions_data": { @@ -803,6 +847,7 @@ def load_demo_completed_assessment(): { # Completed Assessment id 8 "assessment_task_id": 12, "done": True, + "locked": False, "initial_time": "2024-02-05T17:04:36.368000", "last_update": "2024-02-05T17:04:38.112000", "rating_observable_characteristics_suggestions_data": { @@ -891,6 +936,7 @@ def load_demo_completed_assessment(): { # Completed Assessment id 9 "assessment_task_id": 13, "done": True, + "locked": False, "initial_time": "2024-02-05T17:07:57.768000", "last_update": "2024-02-05T17:08:00.783000", "rating_observable_characteristics_suggestions_data": { @@ -988,6 +1034,7 @@ def load_demo_completed_assessment(): "last_update": comp_assessment["last_update"], "rating_observable_characteristics_suggestions_data": comp_assessment["rating_observable_characteristics_suggestions_data"], "done": comp_assessment["done"], + "locked": comp_assessment["locked"], }) def replace_completed_assessment(completed_assessment_data, completed_assessment_id): @@ -1011,4 +1058,4 @@ def replace_completed_assessment(completed_assessment_data, completed_assessment db.session.commit() - return one_completed_assessment \ No newline at end of file + return one_completed_assessment diff --git a/BackEndFlask/models/queries.py b/BackEndFlask/models/queries.py index d82338284..95306c7b1 100644 --- a/BackEndFlask/models/queries.py +++ b/BackEndFlask/models/queries.py @@ -192,6 +192,7 @@ def get_role_in_course(user_id: int, course_id: int): return role +# WIP @error_log def get_team_by_course_id_and_user_id(course_id, user_id): """ @@ -216,6 +217,39 @@ def get_team_by_course_id_and_user_id(course_id, user_id): return teams +# HERE +@error_log +def get_team_users(course_id: int, team_id: int, user_id: int): + """ + Description: + Gets all users associated with the given team in the given course. + + Parameters: + course_id: int (The id of the course) + team_id: int (The id of the team) + user_id: int (The id of the logged-in user) + """ + users_in_team = db.session.query( + User.user_id, + User.first_name, + User.last_name, + User.email, + Team.team_id, + Team.team_name + ).join( + TeamUser, TeamUser.team_id == Team.team_id + ).join( + User, User.user_id == TeamUser.user_id + ).filter( + and_( + Team.course_id == course_id, + Team.team_id == team_id + ) + ).all() + + # Return the users in the team + return users_in_team + @error_log def get_team_by_course_id_and_observer_id(course_id, observer_id): """ @@ -787,6 +821,7 @@ def get_completed_assessment_with_team_name(assessment_task_id): CompletedAssessment.last_update, CompletedAssessment.rating_observable_characteristics_suggestions_data, CompletedAssessment.done, + CompletedAssessment.locked, Team.team_name ).join( Team, Team.team_id == CompletedAssessment.team_id @@ -816,6 +851,7 @@ def get_completed_assessment_with_user_name(assessment_task_id): CompletedAssessment.last_update, CompletedAssessment.rating_observable_characteristics_suggestions_data, CompletedAssessment.done, + CompletedAssessment.locked, User.first_name, User.last_name ).join( @@ -871,6 +907,7 @@ def get_completed_assessment_by_user_id(course_id, user_id): CompletedAssessment.last_update, CompletedAssessment.rating_observable_characteristics_suggestions_data, CompletedAssessment.done, + CompletedAssessment.locked, AssessmentTask.assessment_task_name, AssessmentTask.rubric_id, AssessmentTask.unit_of_assessment @@ -894,6 +931,7 @@ def get_completed_assessment_by_user_id(course_id, user_id): CompletedAssessment.last_update, CompletedAssessment.rating_observable_characteristics_suggestions_data, CompletedAssessment.done, + CompletedAssessment.locked, AssessmentTask.assessment_task_name, AssessmentTask.rubric_id, AssessmentTask.unit_of_assessment @@ -929,6 +967,7 @@ def get_completed_assessment_by_ta_user_id(course_id, user_id): CompletedAssessment.last_update, CompletedAssessment.rating_observable_characteristics_suggestions_data, CompletedAssessment.done, + CompletedAssessment.locked, AssessmentTask.assessment_task_name, AssessmentTask.rubric_id, AssessmentTask.unit_of_assessment diff --git a/BackEndFlask/models/schemas.py b/BackEndFlask/models/schemas.py index 433602e6d..2d08858c2 100644 --- a/BackEndFlask/models/schemas.py +++ b/BackEndFlask/models/schemas.py @@ -22,7 +22,7 @@ Blacklist(id, token) """ -class Role(db.Model): +class Role(db.Model): __tablename__ = "Role" role_id = db.Column(db.Integer, primary_key=True, autoincrement=True) role_name = db.Column(db.Text, nullable=False) @@ -57,13 +57,13 @@ class Category(db.Model): description = db.Column(db.String(255), nullable=False) rating_json = db.Column(db.JSON, nullable=False) -class RubricCategory(db.Model): +class RubricCategory(db.Model): __tablename__ = "RubricCategories" __table_args__ = {'sqlite_autoincrement': True} rubric_category_id = db.Column(db.Integer, primary_key=True) rubric_id = db.Column(db.Integer, ForeignKey(Rubric.rubric_id), nullable=False) category_id = db.Column(db.Integer, ForeignKey(Category.category_id), nullable=False) - + class ObservableCharacteristic(db.Model): __tablename__ = "ObservableCharacteristic" __table_args__ = {'sqlite_autoincrement': True} @@ -96,10 +96,10 @@ class UserCourse(db.Model): user_course_id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, ForeignKey(User.user_id), nullable=False) course_id = db.Column(db.Integer, ForeignKey(Course.course_id), nullable=False) - active = db.Column(db.Boolean) + active = db.Column(db.Boolean) role_id = db.Column(db.Integer, ForeignKey(Role.role_id), nullable=False) -class Team(db.Model): # keeps track of default teams for a fixed team scenario +class Team(db.Model): # keeps track of default teams for a fixed team scenario __tablename__ = "Team" team_id = db.Column(db.Integer, primary_key=True, autoincrement=True) team_name = db.Column(db.Text, nullable=False) @@ -113,7 +113,7 @@ class TeamUser(db.Model): 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" assessment_task_id = db.Column(db.Integer, primary_key=True, autoincrement=True) @@ -131,6 +131,8 @@ class AssessmentTask(db.Model): 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) + locked = db.Column(db.Boolean, nullable=False) + published = db.Column(db.Boolean, nullable=False) class Checkin(db.Model): # keeps students checking to take a specific AT __tablename__ = "Checkin" @@ -153,10 +155,11 @@ class CompletedAssessment(db.Model): last_update = db.Column(db.DateTime(timezone=True), nullable=True) rating_observable_characteristics_suggestions_data = db.Column(db.JSON, nullable=True) done = db.Column(db.Boolean, nullable=False) + locked = db.Column(db.Boolean, nullable=False) class Feedback(db.Model): __tablename__ = "Feedback" 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 + feedback_time = db.Column(DateTime(timezone=True), nullable=True) # time the student viewed their feedback diff --git a/BackEndFlask/models/utility.py b/BackEndFlask/models/utility.py index 6f9a7d2f3..cbbda6e29 100644 --- a/BackEndFlask/models/utility.py +++ b/BackEndFlask/models/utility.py @@ -81,4 +81,4 @@ def wrapper(*args, **kwargs): logger.error(f"{e.__traceback__.tb_frame.f_code.co_filename} { e.__traceback__.tb_lineno} Error Type: {type(e).__name__} Message: {e}") raise e - return wrapper \ No newline at end of file + return wrapper diff --git a/BackEndFlask/requirements.txt b/BackEndFlask/requirements.txt index 423cf8d5b..eace85f65 100644 --- a/BackEndFlask/requirements.txt +++ b/BackEndFlask/requirements.txt @@ -1,5 +1,6 @@ flask >= 2.2.2 flask-sqlalchemy >= 3.0.3 +Flask-Migrate >= 4.0.7 sqlalchemy >= 2.0.13 flask-marshmallow >= 0.15.0 marshmallow-sqlalchemy >= 0.29.0 @@ -16,4 +17,4 @@ redis >= 4.5.5 python-dotenv >= 1.0.0 yagmail >= 0.15.293 openpyxl >= 3.1.2 -cryptography >= 43.0.1 \ No newline at end of file +cryptography >= 43.0.1 diff --git a/BackEndFlask/run.py b/BackEndFlask/run.py index 58643d83c..8d992afb8 100644 --- a/BackEndFlask/run.py +++ b/BackEndFlask/run.py @@ -1,4 +1,5 @@ from core import app + if __name__ == '__main__': app.run(host="0.0.0.0") diff --git a/Dockerfile.backend b/Dockerfile.backend index 386c23bc2..45d07c55f 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -15,7 +15,7 @@ RUN apt-get update WORKDIR /app # Copy only requirements and setup scripts first to leverage Docker cache -COPY BackEndFlask/requirements.txt BackEndFlask/setupEnv.py /app/ +COPY BackEndFlask/requirements.txt BackEndFlask/setupEnv.py BackEndFlask/dbinsert.py /app/ # Install Python dependencies RUN pip install --no-cache-dir --upgrade pip \ diff --git a/FrontEndReact/src/View/Admin/View/CompleteAssessmentTask/CompleteAssessmentTask.js b/FrontEndReact/src/View/Admin/View/CompleteAssessmentTask/CompleteAssessmentTask.js index 6075aee97..3a76ed0de 100644 --- a/FrontEndReact/src/View/Admin/View/CompleteAssessmentTask/CompleteAssessmentTask.js +++ b/FrontEndReact/src/View/Admin/View/CompleteAssessmentTask/CompleteAssessmentTask.js @@ -222,23 +222,23 @@ class CompleteAssessmentTask extends Component { ); } - + const roleName = currentUserRole["role_name"]; - + if (roleName === "Student" && this.state.usingTeams && !userFixedTeam){ return ( ); } - + if (roleName !== "Student" && this.state.usingTeams && !teamsUsers) { return ( - ); + ); } - + const unitList = this.state.unitList; - + if (!unitList) { return ( diff --git a/FrontEndReact/src/View/Admin/View/ViewAssessmentTask/ViewAssessmentTasks.js b/FrontEndReact/src/View/Admin/View/ViewAssessmentTask/ViewAssessmentTasks.js index 02e3e0aad..ab4ef0f4f 100644 --- a/FrontEndReact/src/View/Admin/View/ViewAssessmentTask/ViewAssessmentTasks.js +++ b/FrontEndReact/src/View/Admin/View/ViewAssessmentTask/ViewAssessmentTasks.js @@ -1,14 +1,17 @@ import React, { Component } from 'react'; import 'bootstrap/dist/css/bootstrap.css'; import CustomDataTable from '../../../Components/CustomDataTable.js'; -import { IconButton } from '@mui/material'; import { Button } from '@mui/material'; import { Tooltip } from '@mui/material'; import EditIcon from '@mui/icons-material/Edit'; import VisibilityIcon from '@mui/icons-material/Visibility'; -import { formatDueDate, genericResourceGET, getHumanReadableDueDate } from '../../../../utility.js'; +import { formatDueDate, genericResourceGET, genericResourcePUT, getHumanReadableDueDate } from '../../../../utility.js'; import Loading from '../../../Loading/Loading.js'; - +import { IconButton } from '@mui/material'; +import LockIcon from '@mui/icons-material/Lock'; +import LockOpenIcon from '@mui/icons-material/LockOpen'; +import PublishIcon from '@mui/icons-material/Publish'; +import UnpublishedIcon from '@mui/icons-material/Unpublished'; class ViewAssessmentTasks extends Component { constructor(props) { @@ -21,7 +24,9 @@ class ViewAssessmentTasks extends Component { downloadedAssessment: null, exportButtonId: {}, completedAssessments: null, - assessmentTasks: null + assessmentTasks: null, + lockStatus: {}, + publishedStatus: {}, } this.handleDownloadCsv = (atId, exportButtonId, assessmentTaskIdToAssessmentTaskName) => { @@ -37,15 +42,49 @@ class ViewAssessmentTasks extends Component { var assessmentName = assessmentTaskIdToAssessmentTaskName[atId]; var newExportButtonJSON = this.state.exportButtonId; - + newExportButtonJSON[assessmentName] = exportButtonId; - + this.setState({ downloadedAssessment: assessmentName, exportButtonId: newExportButtonJSON - }); } + }); + } }); } + + this.handleLockToggle = (assessmentTaskId, task) => { + this.setState((prevState) => { + const newLockStatus = { ...prevState.lockStatus }; + newLockStatus[assessmentTaskId] = !newLockStatus[assessmentTaskId]; + return { lockStatus: newLockStatus }; + }, () => { + const lockStatus = this.state.lockStatus[assessmentTaskId]; + + genericResourcePUT( + `/assessment_task_toggle_lock?assessmentTaskId=${assessmentTaskId}`, + this, + JSON.stringify({ locked: lockStatus }) + ); + }); + }; + + this.handlePublishToggle = (assessmentTaskId, task) => { + this.setState((prevState) => { + const newPublishedStatus = { ...prevState.publishedStatus }; + newPublishedStatus[assessmentTaskId] = !newPublishedStatus[assessmentTaskId]; + return { publishedStatus: newPublishedStatus }; + }, () => { + const publishedStatus = this.state.publishedStatus[assessmentTaskId]; + + genericResourcePUT( + `/assessment_task_toggle_published?assessmentTaskId=${assessmentTaskId}`, + this, + JSON.stringify({ published: publishedStatus }) + ); + }); + }; + } componentDidUpdate () { @@ -62,9 +101,9 @@ class ViewAssessmentTasks extends Component { link.click(); var assessmentName = this.state.downloadedAssessment; - + const exportAssessmentTask = document.getElementById(this.state.exportButtonId[assessmentName]) - + setTimeout(() => { if(exportAssessmentTask) { exportAssessmentTask.removeAttribute("disabled"); @@ -87,22 +126,32 @@ class ViewAssessmentTasks extends Component { this, {dest: "assessmentTasks"} ); - + genericResourceGET( `/completed_assessment?course_id=${courseId}&only_course=true`, "completed_assessments", this, {dest: "completedAssessments"} ); + + const assessmentTasks = this.props.navbar.adminViewAssessmentTask.assessmentTasks; + const initialLockStatus = {}; + const initialPublishedStatus = {}; + + assessmentTasks.forEach((task) => { + initialLockStatus[task.assessment_task_id] = task.locked; + initialPublishedStatus[task.assessment_task_id] = task.published; + }); + + this.setState({ lockStatus: initialLockStatus, publishedStatus: initialPublishedStatus }); } render() { - if (this.state.assessmentTasks === null || this.state.completedAssessments === null) { return ; } const fixedTeams = this.props.navbar.state.chosenCourse["use_fixed_teams"]; - + var navbar = this.props.navbar; var adminViewAssessmentTask = navbar.adminViewAssessmentTask; @@ -250,6 +299,52 @@ class ViewAssessmentTasks extends Component { } } }, + { + name: "assessment_task_id", + label: "Publish", + options: { + filter: false, + sort: false, + setCellHeaderProps: () => { return { align:"center", width:"70px", className:"button-column-alignment"}}, + setCellProps: () => { return { align:"center", width:"70px", className:"button-column-alignment"} }, + customBodyRender: (atId) => { + const task = assessmentTasks.find((task) => task["assessment_task_id"] === atId); + const isPublished = this.state.publishedStatus[atId] !== undefined ? this.state.publishedStatus[atId] : (task ? task.published : false); + + return ( + this.handlePublishToggle(atId, task)} + > + {isPublished ? : } + + ); + } + } + }, + { + name: "assessment_task_id", + label: "Lock", + options: { + filter: false, + sort: false, + setCellHeaderProps: () => { return { align:"center", width:"70px", className:"button-column-alignment"}}, + setCellProps: () => { return { align:"center", width:"70px", className:"button-column-alignment"} }, + customBodyRender: (atId) => { + const task = assessmentTasks.find((task) => task["assessment_task_id"] === atId); + const isLocked = this.state.lockStatus[atId] !== undefined ? this.state.lockStatus[atId] : (task ? task.locked : false); + + return ( + this.handleLockToggle(atId, task)} + > + {isLocked ? : } + + ); + } + } + }, { name: "assessment_task_id", label: "Edit", @@ -299,21 +394,23 @@ class ViewAssessmentTasks extends Component { customBodyRender: (assessmentTaskId) => { if (assessmentTaskId && assessmentTasks) { const selectedTask = assessmentTasks.find(task => task.assessment_task_id === assessmentTaskId); - + if (selectedTask) { return ( - { - setCompleteAssessmentTaskTabWithID(selectedTask); - }} - aria-label='viewCompletedAssessmentIconButton' - > - - + <> + { + setCompleteAssessmentTaskTabWithID(selectedTask); + }} + aria-label='viewCompletedAssessmentIconButton' + > + + + ); } - } + } return( <> {"N/A"} @@ -334,7 +431,7 @@ class ViewAssessmentTasks extends Component { const assessmentTask = assessmentTasks.find(task => task.assessment_task_id === atId); const isTeamAssessment = assessmentTask && assessmentTask.unit_of_assessment; const teamsExist = this.props.teams && this.props.teams.length > 0; - + if (isTeamAssessment && (fixedTeams && !teamsExist)) { return ( @@ -351,7 +448,7 @@ class ViewAssessmentTasks extends Component { ); } - + return ( - - - - - - ) - } +
+

+ {"Rubric for " + rubricName} +

+ +
+ Rubric Description: {rubricDescription} +
+ +
+
+
+

+ Assessment Categories: {categoryList} +

+
+

+ Instructions +

+ +
+
+ +
+
+
+ + + ) + } } export default ViewAssessmentTaskInstructions; diff --git a/FrontEndReact/src/View/Student/View/AssessmentTask/ViewAssessmentTasks.js b/FrontEndReact/src/View/Student/View/AssessmentTask/ViewAssessmentTasks.js index 364fc3c12..21e4cd142 100644 --- a/FrontEndReact/src/View/Student/View/AssessmentTask/ViewAssessmentTasks.js +++ b/FrontEndReact/src/View/Student/View/AssessmentTask/ViewAssessmentTasks.js @@ -10,7 +10,6 @@ class ViewAssessmentTasks extends Component { constructor(props) { super(props); - this.isObjectFound = (atId) => { var completedAssessments = this.props.completedAssessments; @@ -36,7 +35,7 @@ class ViewAssessmentTasks extends Component { if (assessmentTasks["number_of_teams"] !== null) // If the number of teams is specified, use that { count = assessmentTasks["number_of_teams"] - } else { // Otherwise, use the number of fixed teams + } else { // Otherwise, use the number of fixed teams count = this.props.counts[1]; } } else { @@ -72,7 +71,9 @@ class ViewAssessmentTasks extends Component { if (role["role_id"] === 5) { chosenCAT = this.props.completedAssessments; } + var assessmentTasks = this.props.assessmentTasks; + // var completedAssessmentTasks = this.props.completedAssessments; const columns = [ { @@ -84,6 +85,22 @@ class ViewAssessmentTasks extends Component { setCellProps: () => { return { width:"300px"} }, } }, + { + name: "unit_of_assessment", + label: "Unit of Assessment", + options: { + filter: true, + setCellHeaderProps: () => { return { width:"270px"}}, + setCellProps: () => { return { width:"270px"} }, + customBodyRender: (isTeam) => { + return ( +

+ {isTeam ? "Team" : "Individual"} +

+ ) + } + }, + }, { name: "due_date", label: "Due Date", @@ -127,6 +144,10 @@ class ViewAssessmentTasks extends Component { setCellHeaderProps: () => { return { align:"center", width:"140px", className:"button-column-alignment"}}, setCellProps: () => { return { align:"center", width:"140px", className:"button-column-alignment"} }, customBodyRender: (atId) => { + // let at = assessmentTasks.find((at) => at["assessment_task_id"] === atId); + // let filledByStudent = at.completed_by_role_id === 5; + let filledByStudent = true; + return ( at["assessment_task_id"] === atId)["unit_of_assessment"])) || - this.isObjectFound(atId) === true + + disabled={role["role_id"] === 5 ? + ((this.props.checkin.indexOf(atId) === -1 && + (assessmentTasks.find((at) => at["assessment_task_id"] === atId)["unit_of_assessment"])) || + this.isObjectFound(atId) === true || !filledByStudent) : this.areAllATsComplete(atId) === true } @@ -185,7 +206,6 @@ class ViewAssessmentTasks extends Component { ) - } } } diff --git a/FrontEndReact/src/View/Student/View/CompletedAssessmentTask/StudentCompletedAssessmentTasks.js b/FrontEndReact/src/View/Student/View/CompletedAssessmentTask/StudentCompletedAssessmentTasks.js index e9bed4e28..e8a5a7913 100644 --- a/FrontEndReact/src/View/Student/View/CompletedAssessmentTask/StudentCompletedAssessmentTasks.js +++ b/FrontEndReact/src/View/Student/View/CompletedAssessmentTask/StudentCompletedAssessmentTasks.js @@ -54,6 +54,9 @@ class StudentCompletedAssessmentTasks extends Component { completedAssessments, } = this.state; + // const filteredATs = this.props.filteredAssessments; // Currently unused, but may be in the future. + const filteredCATs = this.props.filteredCompleteAssessments; + if (errorMessage) { return(
@@ -74,7 +77,7 @@ class StudentCompletedAssessmentTasks extends Component {
@@ -83,4 +86,4 @@ class StudentCompletedAssessmentTasks extends Component { } } -export default StudentCompletedAssessmentTasks; \ No newline at end of file +export default StudentCompletedAssessmentTasks; diff --git a/FrontEndReact/src/View/Student/View/CompletedAssessmentTask/ViewCompletedAssessmentTasks.js b/FrontEndReact/src/View/Student/View/CompletedAssessmentTask/ViewCompletedAssessmentTasks.js index fbce13e9b..82e3bb496 100644 --- a/FrontEndReact/src/View/Student/View/CompletedAssessmentTask/ViewCompletedAssessmentTasks.js +++ b/FrontEndReact/src/View/Student/View/CompletedAssessmentTask/ViewCompletedAssessmentTasks.js @@ -8,96 +8,101 @@ import { genericResourcePOST, getHumanReadableDueDate } from "../../../../utilit class ViewCompletedAssessmentTasks extends Component { - render() { - var navbar = this.props.navbar; + render() { + // var navbar = this.props.navbar; - var completedAssessments = this.props.completedAssessments; - var assessmentTasks = this.props.assessmentTasks; + var completedAssessments = this.props.completedAssessments; + var assessmentTasks = this.props.assessmentTasks; - const columns = [ - { - name: "assessment_task_name", - label: "Task Name", - options: { - filter: true, - setCellHeaderProps: () => { return { width:"250px" } }, - setCellProps: () => { return { width:"250px" } }, - } - }, - { - name: "initial_time", - label: "Initial Time", - options: { - filter: true, - setCellHeaderProps: () => { return { width:"150px" } }, - setCellProps: () => { return { width:"150px" } }, - customBodyRender: (initial_time) => { - return( - <> - {initial_time ? getHumanReadableDueDate(initial_time) : "N/A"} - - ); - } - } - }, - { - name: "last_update", - label: "Last Update", - options: { - filter: true, - setCellHeaderProps: () => { return { width:"150px" } }, - setCellProps: () => { return { width:"150px" } }, - customBodyRender: (last_update) => { - return( - <> - {last_update ? getHumanReadableDueDate(last_update) : "N/A"} - - ); - } - } - }, - { - name: "assessment_task_id", - label: "Unit of Assessment", - options: { - filter: true, - setCellHeaderProps: () => { return { width:"170px" } }, - setCellProps: () => { return { width:"140px" } }, - customBodyRender: (atId) => { - const assessmentTask = assessmentTasks.find(at => at.assessment_task_id === atId); - return <>{assessmentTask.unit_of_assessment ? "Team" : "Individual"}; - } - } - }, - { - name: "completed_by_role_id", - label: "Completed By", - options: { - filter: true, - setCellHeaderProps: () => { return { width:"140px" } }, - setCellProps: () => { return { width:"140px" } }, - customBodyRender: (roleId) => { - return <>{roleId === 5 ? "Student" : "TA/Instructor"}; - } - } - }, - { - name: "assessment_task_id", - label: "View", - options: { - filter: false, - sort: false, - setCellHeaderProps: () => { return { align:"center", width:"100px", className:"button-column-alignment" } }, - setCellProps: () => { return { align:"center", width:"100px", className:"button-column-alignment" } }, - customBodyRender: (atId) => { - return ( -
- { - navbar.setAssessmentTaskInstructions(assessmentTasks, atId, completedAssessments, { readOnly: true }); + const columns = [ + { + name: "assessment_task_name", + label: "Task Name", + options: { + filter: true, + setCellHeaderProps: () => { return { width:"250px" } }, + setCellProps: () => { return { width:"250px" } }, + } + }, + { + name: "initial_time", + label: "Initial Time", + options: { + filter: true, + setCellHeaderProps: () => { return { width:"150px" } }, + setCellProps: () => { return { width:"150px" } }, + customBodyRender: (initial_time) => { + return( + <> + {initial_time ? getHumanReadableDueDate(initial_time) : "N/A"} + + ); + } + } + }, + { + name: "last_update", + label: "Last Update", + options: { + filter: true, + setCellHeaderProps: () => { return { width:"150px" } }, + setCellProps: () => { return { width:"150px" } }, + customBodyRender: (last_update) => { + return( + <> + {last_update ? getHumanReadableDueDate(last_update) : "N/A"} + + ); + } + } + }, + { + name: "assessment_task_id", + label: "Unit of Assessment", + options: { + filter: true, + setCellHeaderProps: () => { return { width:"170px" } }, + setCellProps: () => { return { width:"140px" } }, + customBodyRender: (atId) => { + const assessmentTask = assessmentTasks.find(at => at.assessment_task_id === atId); + if (assessmentTask === undefined) { + return <>UNDEFINED + } + return <>{assessmentTask.unit_of_assessment ? "Team" : "Individual"}; + } + } + }, + { + name: "completed_by_role_id", + label: "Completed By", + options: { + filter: true, + setCellHeaderProps: () => { return { width:"140px" } }, + setCellProps: () => { return { width:"140px" } }, + customBodyRender: (roleId) => { + return <>{roleId === 5 ? "Student" : "TA/Instructor"}; + } + } + }, + { + name: "assessment_task_id", + label: "View", + options: { + filter: false, + sort: false, + setCellHeaderProps: () => { return { align:"center", width:"100px", className:"button-column-alignment" } }, + setCellProps: () => { return { align:"center", width:"100px", className:"button-column-alignment" } }, + customBodyRender: (atId) => { + return ( +
+ { var singluarCompletedAssessment = null; if (completedAssessments) { - singluarCompletedAssessment = completedAssessments.find(completedAssessment => completedAssessment.assessment_task_id === atId) ?? null; + singluarCompletedAssessment + = completedAssessments.find( + completedAssessment => completedAssessment.assessment_task_id === atId + ) ?? null; } genericResourcePOST( `/rating`, @@ -106,39 +111,44 @@ class ViewCompletedAssessmentTasks extends Component { "user_id" : singluarCompletedAssessment.user_id, "completed_assessment_id": singluarCompletedAssessment.completed_assessment_id, }), - ); - }} - aria-label="completedAssessmentTasksViewIconButton" - > - - -
- ) - - } - } - }, - ]; + ); + this.props.navbar.setAssessmentTaskInstructions( + assessmentTasks, + atId, + completedAssessments, + { readOnly: true, skipInstructions: true } + ); + }} + aria-label="completedAssessmentTasksViewIconButton" + > + +
+
+ ) + } + } + }, + ]; - const options = { - onRowsDelete: false, - download: false, - print: false, - viewColumns: false, - selectableRows: "none", - selectableRowsHeader: false, - responsive: "vertical", - tableBodyMaxHeight: "21rem" - }; + const options = { + onRowsDelete: false, + download: false, + print: false, + viewColumns: false, + selectableRows: "none", + selectableRowsHeader: false, + responsive: "vertical", + tableBodyMaxHeight: "21rem" + }; - return ( - - ) - } + return ( + + ) + } } -export default ViewCompletedAssessmentTasks; \ No newline at end of file +export default ViewCompletedAssessmentTasks; diff --git a/FrontEndReact/src/View/Student/View/StudentViewTeams.js b/FrontEndReact/src/View/Student/View/StudentViewTeams.js index cb1d7fed0..02ae67347 100644 --- a/FrontEndReact/src/View/Student/View/StudentViewTeams.js +++ b/FrontEndReact/src/View/Student/View/StudentViewTeams.js @@ -23,13 +23,14 @@ class StudentViewTeams extends Component { var navbar = this.props.navbar; var state = navbar.state; var chosenCourse = state.chosenCourse; + var chosenCourseId = chosenCourse["course_id"]; genericResourceGET( - `/team_by_user?course_id=${chosenCourse["course_id"]}`, "teams", this); + `/team_by_user?course_id=${chosenCourseId}`, "teams", this); var url = ( chosenCourse["use_tas"] ? - `/user?course_id=${chosenCourse["course_id"]}&role_id=4` : + `/user?course_id=${chosenCourseId}&role_id=4` : `/user?uid=${chosenCourse["admin_id"]}` ); @@ -73,4 +74,4 @@ class StudentViewTeams extends Component { } } -export default StudentViewTeams; \ No newline at end of file +export default StudentViewTeams; diff --git a/FrontEndReact/src/View/Student/View/ViewTeams.js b/FrontEndReact/src/View/Student/View/ViewTeams.js index 909df27e3..9a84b665d 100644 --- a/FrontEndReact/src/View/Student/View/ViewTeams.js +++ b/FrontEndReact/src/View/Student/View/ViewTeams.js @@ -3,79 +3,89 @@ import 'bootstrap/dist/css/bootstrap.css'; import CustomDataTable from "../../Components/CustomDataTable"; import { getHumanReadableDueDate } from "../../../utility"; - - class ViewTeams extends Component{ - render() { - var teams = this.props.teams; - - var users = this.props.users; - - var navbar = this.props.navbar; + render() { + var teams = this.props.teams; + var users = this.props.users; + var navbar = this.props.navbar; - const columns = [ - { - name: "team_name", - label: "Team Name", - options: { - filter: true, - setCellHeaderProps: () => { return { width:"230px" } }, - setCellProps: () => { return { width:"230px" } }, - } - }, - { - name: "observer_id", - label: navbar.state.chosenCourse["use_tas"] ? "TA Name" : "Instructor Name", - options: { - filter: true, - setCellHeaderProps: () => { return { width:"230px" } }, - setCellProps: () => { return { width:"230px" } }, - customBodyRender: (observerId) => { - return( -

{users[observerId]}

- ) - } - } - }, - { - name: "date_created", - label: "Date Created", - options: { - filter: true, - setCellHeaderProps: () => { return { width:"160px" } }, - setCellProps: () => { return { width:"160px" } }, - customBodyRender: (date_created) => { - let dateCreatedString = getHumanReadableDueDate(date_created); + const columns = [ + { + name: "team_name", + label: "Team Name", + options: { + filter: true, + setCellHeaderProps: () => { return { width:"230px" } }, + setCellProps: () => { return { width:"230px" } }, + } + }, + { + name: "observer_id", + label: navbar.state.chosenCourse["use_tas"] ? "TA Name" : "Instructor Name", + options: { + filter: true, + setCellHeaderProps: () => { return { width:"230px" } }, + setCellProps: () => { return { width:"230px" } }, + customBodyRender: (observerId) => { + return( +

{users[observerId]}

+ ) + } + } + }, + { + name: "team_users", + label: "Members", + options: { + filter: true, + setCellHeaderProps: () => { return { width:"230px" } }, + setCellProps: () => { return { width:"230px" } }, + customBodyRender: (user) => { + return( + <>{user + " "} + ); + } + }, + }, + { + name: "date_created", + label: "Date Created", + options: { + filter: true, + setCellHeaderProps: () => { return { width:"160px" } }, + setCellProps: () => { return { width:"160px" } }, + customBodyRender: (date_created) => { + let dateCreatedString = getHumanReadableDueDate(date_created); - return( -

- {date_created ? dateCreatedString : "N/A"} -

- ) - } - } - }, - ]; + return( +

+ {date_created ? dateCreatedString : "N/A"} +

+ ) + } + } + }, + ]; - const options = { - onRowsDelete: false, - download: false, - print: false, - viewColumns: false, - selectableRows: "none", - selectableRowsHeader: false, - responsive: "vertical", - tableBodyMaxHeight: "21rem" - }; + const options = { + onRowsDelete: false, + download: false, + print: false, + viewColumns: false, + selectableRows: "none", + selectableRowsHeader: false, + responsive: "vertical", + tableBodyMaxHeight: "21rem" + }; - return ( - - ) - } + return ( + + ) + } } -export default ViewTeams; \ No newline at end of file +export default ViewTeams; diff --git a/README.md b/README.md index acdfa0f07..ccc979314 100644 --- a/README.md +++ b/README.md @@ -8,360 +8,251 @@ research-based or custom rubrics. Instructors can email students their results, as well as download the data for analysis. +# Setup +The following shows how to get SkillBuilder running on your operating system. -## SkillBuilder is implemented in three parts: ## +## Requirements -- A Back End Flask server. +The following technologies are required: +1. `Python >= 3.12` +2. `Redis` +3. `Docker/Docker Desktop` +4. `Node >= v21.6.1` -- A Caching Redis server. +Find your operating system below and follow the instructions +on installing them. -- A Front End React server. +### Linux +#### Debian/Ubuntu (and its derivatives) +1. Perform any system upgrades. -## Setting up and running with Docker and Docker Compose: ## +``` +sudo apt update -y +sudo apt upgrade -y +``` -- UPDATE: Using Docker and Docker Compose should become the sole - method for running this application locally as it solves - dependency issues across all platforms! Also makes developing - easier as there are now only two commands to worry about. +2. Install `Python3`: +``` +sudo apt install python3 +python3 --version +``` -- Follow the link for instructions on downloading Docker Desktop: - https://www.docker.com/products/docker-desktop/ +Ensure that the version is `>= 3.12`. -- NOTE: If you have an intel chip with Windows OS, you will need - to go to the following link to install Docker Desktop: - https://docs.docker.com/desktop/install/windows-install/ +*Note*: Debian uses the last _stable_ release of Python (which is not 3.12), but +from testing, it seems to work just fine. -- NOTE: Make sure that there are no running frontend, - redis, or backend processes as there will be port - conflicts. To view if you have processes running - on important ports, run the following and expect - no output: +3. Install `Redis`: - lsof -i :3000,5050,6379 - - If output, there is a chance you still have processes - running and you need to use the following command to - kill them off: +Using the following link to install: - kill - - There is a chance that your OS has an important process - running on one of these ports that should not be terminated. - In that case, change the port for conflicting processes in the - compose.yml file. Make sure that you also update changed - ports in the frontend or backend .env and anywhere else - needed! +https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/ - Step 1: - After following the instructions, ensure you have Docker - Desktop open and running. +*Note*: Ubuntu and Debian typically use `systemctl` as the init system, but if using +something different, the docs will not cover those. - Step 2: - Open a new terminal and navigate to where you have this - repository cloned. +4. Install `Node`: - Step 3: - Run the following command to ensure you have docker running: +``` +sudo apt install nodejs +node -v +``` - docker ps +5. Install Docker/Docker Desktop: - Step 4: - Run the following command to build the images: +Use the following link for the instuctions for Ubuntu: - docker compose build - - NOTE: To rebuild with new changes applied and ignore cached - build run the following: +https://docs.docker.com/desktop/setup/install/linux/ubuntu/ - docker compose build --no-cache +Use the following link for the instuctions for Debian: - NOTE: To view all of the build logs instead of the default - summary run the following: +https://docs.docker.com/desktop/setup/install/linux/debian/ - docker compose build --process=plain +### MacOS - Step 5: - Run the following command to run containers from the images: +MacOS will require some kind of package manager (this document will +use `homebrew`). - docker compose up +You can find `homebrew` here: https://brew.sh/ - Step 6: - Open a browser with the link http://localhost:3000 to see the frontend. +1. Install `Python3` +You can find the downloads here: +https://www.python.org/downloads/macos/ -## REQUIREMENTS: ## +2. Install `Redis` -- Python 3.12 and up. +``` +brew install redis +``` -- MySQL-Server. +3. Install `Node` -- Homebrew 4.2.18 and up. +Either download prebuilt binaries directly, or use a package manager: -- Redis 7.2.4 and up. +https://nodejs.org/en/download/package-manager -- Node.js v21.6.1 and up. +4. Install Docker/Docker Desktop -NOTE: +The following link will walk you through it: -- You WILL encounter issues when running both the -Back End and Front End servers if you do NOT have -installed the REQUIRED versions of Python and -Node.js. +https://docs.docker.com/desktop/setup/install/mac-install/ -NOTE: +### Windows -- Linux, Mac, and WSL Developers use `python3`. +Running this project on bare metal Windows is no longer supported. +You will need to get WSL (Windows Subsystem for Linux) or preferably WSL2. -- WINDOWS DEVELOPERS ARE NO LONGER SUPPORTED. +The following shows you how to set it up: +https://learn.microsoft.com/en-us/windows/wsl/install -## Setting up the MySQL Environment: ## +Once this is install and set up, open Windows Terminal, Powershell, Command Prompt +(or whatever terminal emulator you use) and do: -- Run the following command to install MySQL-Server -on Linux: +``` +wsl +``` - sudo apt install mysql-server +If this is working correctly, follow the installation instructions in the *Linux* +section of this README to get all dependencies. -- Run the following command to install MySQL-Server -on MacOS: +## Running Rubricapp in a Docker container - brew install mysql +1. Perform a build: -- Run the following command to start MySQL-Server -on MacOS: +``` +docker compose build +``` - brew services start mysql +This step is needed for whenever Docker files are modified. -- Run the following command to start MySQL-Server -in a new terminal: +_Note_: Docker will cache during build time. If you need to rebuild without the +cache, run: - sudo mysql -u root +``` +docker compose build --no-cache +``` -- Next use these commands to create an account -named skillbuilder and set the password to -"WasPogil1#" +2. To run the container, do: - CREATE DATABASE account; - CREATE USER 'skillbuilder'@'localhost' IDENTIFIED BY 'WasPogil1#'; - GRANT ALL PRIVILEGES ON *.* TO 'skillbuilder'@'localhost'; - FLUSH PRIVILEGES; - exit; +``` +docker compose up +``` -NOTE: +_Note_: if changes are required for the database, you can reset the database with: -- The password should be changed for deployment. +``` +docker compose down -v +``` -- 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: +When the front end is finished compiling, it should show a link, namely: `http://localhost:3000`. +Simply open this link in your browser and use an appropriate login. - mysql -u skillbuilder -p +# Not using Docker -and then type the password. +You can also run rubricapp without Docker, but you will need to manually run the setup yourself. -## Installing requirements ## +1. Create a virtual environment -- Follow the link for instructions on downloading Python: +``` +python3 -m venv +source /bin/activate +``` - https://www.python.org/downloads/ +This is where all of the Python dependencies will be stored instead of being +stored globally on your system. -- Follow the link for instructions on downloading Node.js: +2. Start Redis - https://nodejs.org/en/download +Enable the Redis service using your appropriate `init system` (`systemctl` in this example). -- Follow the link for instructions on downloading brew: +``` +systemctl start redis +``` - https://brew.sh/ +Make sure that it is running: -- Once installed, run the following command with Homebrew - to install redis: +``` +systemctl status redis +``` - brew install redis +3. Launch the backend: +``` +user@/(project root)$ cd BackendFlask +user@/(project root)/BackendFlask$ python3 ./setupEnv.py -irds +``` +The setup flags are as follows: +* `-i, --install` - install all depencencies +* `-r, --reset` - reset the database +* `-d, --demo` - load demo data into the database +* `-s, --start` - start the backend server -## Setting up the Back End environment: ## +Later iterations of using `setupEnv.py` only requires the `-s` flag +(unless new depencencies are added or if the database needs to be reset etc). -- Follow the instructions for setting up the virtual environment: - - Step 1: - Ensure you are in the BackEndFlask directory by running - the command: +4. Launch the Frontend Server - cd BackEndFlask +``` +user@/(project root)$ cd FrontendReact +user@/(project root)/FrontendReact$ npm install # only do this once +user@/(project root)/FrontendReact$ npm start +``` - Step 2: - Create the virtual environment by running the command: +This will launch the server on port 3000. Access it by navigating to `http://localhost:3000` in your browser and logging in with appropriate credentials. - python3 -m venv BackEndFlaskVenv +# Other - Step 3: - Activate the virtual environment by running the command: +If you are testing with adding students/TAs/admins, it may be time consuming to +manually do it via the website. There is a script that will automatically insert new +users into the database straight from the command line. It is important to note +that this script only works if the backend _is currently running inside docker_. - source BackEndFlaskVenv/bin/activate +Run this script with: - To Deactivate the virtual environment, run the command: +``` +./dbinsert.sh +``` - deactivate +Run this and follow the on-screen instructions. - To Remove the virtual environment, run the command: +# Troubleshooting - rm -r BackEndFlaskVenv +## Redis issues -- In order to setup the environment for the first time, - you will need to be in the `/rubricapp/BackEndFlask/` - directory and run the following command: +If it does not start correctly, there could be a multitude of reasons. I suggest +using `journalctl` to investigate it (systemctl will give out the full command). - python3 setupEnv.py -id +But a good starting point is seeing if it is already running: -- This command will install all the requirements from - requirements.txt, create a new database, and load - the database with demo data. +``` +ps aux | grep redis +``` -Flag Meanings: +This will give the PIDs of all processess with `redis` in its name. Try killing them +with `kill ..., ` and then rerunning `systemctl start redis`. -- `-i` install -- `-d` demo +_Note_: if `redis` is not considered a service, try using `redis-server` or `redis-server.service`. -NOTE: -- If you DO NOT run the above command with the - `-i` and `-d` flags once, then the Back End server - WILL NOT be initialized properly. If the Back End - server is NOT initialized properly, then the Back - End server WILL NOT run. IF the Back End server - is NOT running, then the Front End server WILL NOT - run properly either. +## Port conflicts -- In the case where you want to restart with a fresh - new database, add the flag `-r` to reset the existing - database. You WILL then have to rerun the command with - the `-d` flag to load demo data. +The backend runs on port 5000 and the frontend runs on port 3000. You may already have processes running +on those ports. If this is the case, you will have conflicts and the server(s) will not run normally. +You can check what is running on those ports with: +``` +lsof -i :5000 +lsof -i :3000 +``` -## Setting up the Front End environment: ## -- Follow the link for instructions on downloading Node.js: +If any output appears here, you may either want to kill them with `kill`, or run those processes on different ports. - https://nodejs.org/en/download -- In order to install the required packages you WILL need - to be in the directory `/rubricapp/FrontEndReact/`. -- Inside the Front End React directory run the following - command to install all the Node packages for the project: - - npm install - -NOTE: -- If you run `npm install` outside of the - `/rubricapp/FrontEndReact/` directory, it WILL cause - issues. - -- In the case where you run `npm install` outside - of the `/rubricapp/FrontEndReact/` directory, - simply remove the created files `package.json` and - `package-lock.json` and the directory `node_modules`. - Ensure that you have correctly changed the current - working directory to `/rubricapp/FrontEndReact/` - before attempting to run the command to install - the Node packages. - - - -## Running the Servers after setup: ## - -NOTE: - -- You WILL need to run the Back End server first, - the Redis server second, then the Front End server - third. - -- You WILL need to run the Back End, Redis, and - Front End servers in different terminal windows. - - - -## Running the Back End server of the application: ## -- Use the following command for running the Back End - server in the `/rubricapp/BackEndFlask/` directory - during regular use: - - python3 setupEnv.py -s - -Flag meaning: - -- `-s` start - - - -## Running the Redis server: ## - -- Use the following command for running the Redis server: - - brew services start redis - -NOTE: -- Run the following command to restart redis with - Homebrew: - - brew services restart redis - -- Run the following command to stop redis with - Homewbrew: - - brew services stop redis - - - -## Running the Front End server of the application: ## - -- Use the following command for running the Front End - Server in the `/rubricapp/FrontEndReact/` directory: - - npm start - -- This command runs the Front End server in development mode. - Open http://localhost:3000 or http://127.0.0.1:3000 to view - it in your browser. - -- Any changes made in the `/rubricapp/FrontEndReact/` - directory will be caught by the running Front End - server, thus rerendering any opened tabs in your - browser. - -- You will also be able to see any compile warnings - and errors in the console. - - - -## Running Pytest: ## - -- For running pytests on the Back End server - you will use the following command: - - python3 setupEnv.py -t - -Flag meaning: - -- `-t` test - - - -## Running Jest tests: ## - -- For running Jest tests on the Front End server - you will use the following command: - - npm test - -- This command launches the test runner in the interactive - watch mode. Make sure the version of react is - 'react-scripts@0.3.0' or higher. - -- Here is a link for learning more information about running tests: - - https://facebook.github.io/create-react-app/docs/running-tests diff --git a/README.old b/README.old new file mode 100644 index 000000000..e58c1873e --- /dev/null +++ b/README.old @@ -0,0 +1,372 @@ +# SkillBuilder + +A web application for evaluating students' professional +skills, such as teamwork and communication. The purpose +of the SkillBuilder application is to allow instructors +to assess teams of students in real-time using +research-based or custom rubrics. Instructors can email +students their results, as well as download the data +for analysis. + + + +## SkillBuilder is implemented in three parts: ## + +- A Back End Flask server. + +- A Caching Redis server. + +- A Front End React server. + + + +## Setting up and running with Docker and Docker Compose: ## + +- UPDATE: Using Docker and Docker Compose should become the sole + method for running this application locally as it solves + dependency issues across all platforms! Also makes developing + easier as there are now only two commands to worry about. + +- Follow the link for instructions on downloading Docker Desktop: + https://www.docker.com/products/docker-desktop/ + +- NOTE: If you have an intel chip with Windows OS, you will need + to go to the following link to install Docker Desktop: + https://docs.docker.com/desktop/install/windows-install/ + +- NOTE: Make sure that there are no running frontend, + redis, or backend processes as there will be port + conflicts. To view if you have processes running + on important ports, run the following and expect + no output: + + lsof -i :3000,5050,6379 + + If output, there is a chance you still have processes + running and you need to use the following command to + kill them off: + + kill + + or optionally, send with a forced kill (not recommended as it does + not allow the process to shut down gracefully). + + sudo kill -9 + + There is a chance that your OS has an important process + running on one of these ports that should not be terminated. + In that case, change the port for conflicting processes in the + compose.yml file. Make sure that you also update changed + ports in the frontend or backend .env and anywhere else + needed! + + Step 1: + After following the instructions, ensure you have Docker + Desktop open and running. + + Step 2: + Open a new terminal and navigate to where you have this + repository cloned. + + Step 3: + Run the following command to ensure you have docker running: + + docker ps + + Step 4: + Run the following command to build the images: + + docker compose build + + NOTE: To rebuild with new changes applied and ignore cached + build run the following: + + docker compose build --no-cache + + NOTE: To view all of the build logs instead of the default + summary run the following: + + docker compose build --process=plain + + Step 5: + Run the following command to run containers from the images: + + docker compose up + + Step 6: + Open a browser with the link http://localhost:3000 to see the frontend and log in + with one of the demo students/TAs/admins. + + +## REQUIREMENTS: ## + +- Python 3.12 and up. + +- MySQL-Server. + +- Homebrew 4.2.18 and up. + +- Redis 7.2.4 and up. + +- Node.js v21.6.1 and up. + +NOTE: + +- You WILL encounter issues when running both the +Back End and Front End servers if you do NOT have +installed the REQUIRED versions of Python and +Node.js. + +NOTE: + +- Linux, Mac, and WSL Developers use `python3`. + +- 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 ## + +- Follow the link for instructions on downloading Python: + + https://www.python.org/downloads/ + +- Follow the link for instructions on downloading Node.js: + + https://nodejs.org/en/download + +- Follow the link for instructions on downloading brew: + + https://brew.sh/ + +- Once installed, run the following command with Homebrew + to install redis: + + brew install redis + + + +## Setting up the Back End environment: ## + +- Follow the instructions for setting up the virtual environment: + + Step 1: + Ensure you are in the BackEndFlask directory by running + the command: + + cd BackEndFlask + + Step 2: + Create the virtual environment by running the command: + + python3 -m venv BackEndFlaskVenv + + Step 3: + Activate the virtual environment by running the command: + + source BackEndFlaskVenv/bin/activate + + To Deactivate the virtual environment, run the command: + + deactivate + + To Remove the virtual environment, run the command: + + rm -r BackEndFlaskVenv + +- In order to setup the environment for the first time, + you will need to be in the `/rubricapp/BackEndFlask/` + directory and run the following command: + + python3 setupEnv.py -id + +- This command will install all the requirements from + requirements.txt, create a new database, and load + the database with demo data. + +Flag Meanings: + +- `-i` install +- `-d` demo + +NOTE: +- If you DO NOT run the above command with the + `-i` and `-d` flags once, then the Back End server + WILL NOT be initialized properly. If the Back End + server is NOT initialized properly, then the Back + End server WILL NOT run. IF the Back End server + is NOT running, then the Front End server WILL NOT + run properly either. + +- In the case where you want to restart with a fresh + new database, add the flag `-r` to reset the existing + database. You WILL then have to rerun the command with + the `-d` flag to load demo data. + + + +## Setting up the Front End environment: ## +- Follow the link for instructions on downloading Node.js: + + https://nodejs.org/en/download + +- In order to install the required packages you WILL need + to be in the directory `/rubricapp/FrontEndReact/`. + +- Inside the Front End React directory run the following + command to install all the Node packages for the project: + + npm install + +NOTE: +- If you run `npm install` outside of the + `/rubricapp/FrontEndReact/` directory, it WILL cause + issues. + +- In the case where you run `npm install` outside + of the `/rubricapp/FrontEndReact/` directory, + simply remove the created files `package.json` and + `package-lock.json` and the directory `node_modules`. + Ensure that you have correctly changed the current + working directory to `/rubricapp/FrontEndReact/` + before attempting to run the command to install + the Node packages. + + + +## Running the Servers after setup: ## + +NOTE: + +- You WILL need to run the Back End server first, + the Redis server second, then the Front End server + third. + +- You WILL need to run the Back End, Redis, and + Front End servers in different terminal windows. + + + +## Running the Back End server of the application: ## +- Use the following command for running the Back End + server in the `/rubricapp/BackEndFlask/` directory + during regular use: + + python3 setupEnv.py -s + +Flag meaning: + +- `-s` start + + + +## Running the Redis server: ## + +- Use the following command for running the Redis server: + + brew services start redis + +NOTE: +- Run the following command to restart redis with + Homebrew: + + brew services restart redis + +- Run the following command to stop redis with + Homewbrew: + + brew services stop redis + + + +## Running the Front End server of the application: ## + +- Use the following command for running the Front End + Server in the `/rubricapp/FrontEndReact/` directory: + + npm start + +- This command runs the Front End server in development mode. + Open http://localhost:3000 or http://127.0.0.1:3000 to view + it in your browser. + +- Any changes made in the `/rubricapp/FrontEndReact/` + directory will be caught by the running Front End + server, thus rerendering any opened tabs in your + browser. + +- You will also be able to see any compile warnings + and errors in the console. + + + +## Running Pytest: ## + +- For running pytests on the Back End server + you will use the following command: + + python3 setupEnv.py -t + +Flag meaning: + +- `-t` test + + + +## Running Jest tests: ## + +- For running Jest tests on the Front End server + you will use the following command: + + npm test + +- This command launches the test runner in the interactive + watch mode. Make sure the version of react is + 'react-scripts@0.3.0' or higher. + +- Here is a link for learning more information about running tests: + + https://facebook.github.io/create-react-app/docs/running-tests diff --git a/dbinsert.sh b/dbinsert.sh new file mode 100755 index 000000000..8a10df196 --- /dev/null +++ b/dbinsert.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +function help() { + echo "This script is inteded to be ran *while* the backend is running in Docker." + echo -e "It will insert a new user into the DB so you don't have to do it in the website.\n" + echo "dbinsert.sh