diff --git a/Dockerfile b/Dockerfile index d6b15b39..c7acca14 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,8 +35,8 @@ ENV PORT=${PORT} EXPOSE ${PORT} COPY conditional /opt/conditional/conditional -COPY *.py package.json /opt/conditional -COPY --from=build-frontend /opt/conditional/conditional/static /opt/conditional/conditional/static +COPY *.py package.json /opt/conditional/ +COPY --from=build-frontend /opt/conditional/conditional/static/ /opt/conditional/conditional/static/ RUN ln -sf /usr/share/zoneinfo/America/New_York /etc/localtime diff --git a/README.md b/README.md index d174bdeb..96314d0b 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,20 @@ OIDC_CLIENT_CONFIG = { } ``` +#### Add S3 Config +An S3 bucket is used to store files that users upload (currently just for major project submissions). In order to have this work properly, you need to provide some credentials to the app. + +There are 2 ways that you can get the needed credentials. +1. Reach out to an RTP for creds to the dev bucket +2. Create your own bucket using [DEaDASS](https://deadass.csh.rit.edu/), and the site will give you the credentials you need. + +```py +S3_URI = env.get("S3_URI", "https://s3.csh.rit.edu") +S3_BUCKET_ID = env.get("S3_BUCKET_ID", "major-project-media") +AWS_ACCESS_KEY_ID = env.get("AWS_ACCESS_KEY_ID", "") +AWS_SECRET_ACCESS_KEY = env.get("AWS_SECRET_ACCESS_KEY", "") +``` + #### Database You can either develop using the dev database, or use the local database provided in the docker compose file diff --git a/conditional/blueprints/dashboard.py b/conditional/blueprints/dashboard.py index 32587a77..39cf8265 100644 --- a/conditional/blueprints/dashboard.py +++ b/conditional/blueprints/dashboard.py @@ -4,7 +4,6 @@ from conditional import start_of_year, auth from conditional.models.models import Conditional from conditional.models.models import HouseMeeting -from conditional.models.models import MajorProject from conditional.models.models import MemberHouseMeetingAttendance from conditional.models.models import MemberSeminarAttendance from conditional.models.models import TechnicalSeminar @@ -12,6 +11,7 @@ from conditional.util.auth import get_user from conditional.util.flask import render_template from conditional.util.housing import get_queue_position +from conditional.util.major_project import get_project_list from conditional.util.member import gatekeep_values, get_active_members, get_freshman_data, get_voting_members, \ get_cm, get_hm, is_gatekeep_active, req_cm from conditional.util.user_dict import user_dict_is_active, user_dict_is_bad_standing, user_dict_is_intromember, \ @@ -82,15 +82,23 @@ def display_dashboard(user_dict=None): data['housing'] = housing + proj_list = get_project_list() + data['major_projects'] = [ { - 'id': p.id, - 'name': p.name, - 'status': p.status, - 'description': p.description - } for p in - MajorProject.query.filter(MajorProject.uid == uid, - MajorProject.date > start_of_year())] + "id": p.id, + "date": p.date, + "name": p.name, + "proj_name": p.name, + "tldr": p.tldr, + "time_spent": p.time_spent, + "skills": p.skills, + "desc": p.description, + "links": list(filter(None, p.links.split("\n"))), + "status": p.status, + } + for p in proj_list + ] data['major_projects_count'] = len(data['major_projects']) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index aa3ba66c..0bebff28 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -1,30 +1,35 @@ +import collections import json -import requests +import os from flask import Blueprint from flask import request from flask import jsonify from flask import redirect -from sqlalchemy import desc - +import requests +import boto3 import structlog -from conditional.util.context_processors import get_member_name +from werkzeug.utils import secure_filename +from conditional import db, get_user, auth, app from conditional.models.models import MajorProject +from conditional.models.models import MajorProjectSkill -from conditional.util.ldap import ldap_is_eval_director +from conditional.util.context_processors import get_member_name from conditional.util.ldap import ldap_get_member from conditional.util.flask import render_template +from conditional.util.s3 import list_files_in_folder +from conditional.util.user_dict import user_dict_is_eval_director +from conditional.util.major_project import get_project_list -from conditional import db, start_of_year, get_user, auth, app +collections.Callable = collections.abc.Callable logger = structlog.get_logger() major_project_bp = Blueprint("major_project_bp", __name__) - @major_project_bp.route("/major_project/") @auth.oidc_auth("default") @get_user @@ -32,29 +37,58 @@ def display_major_project(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info("Display Major Project Page") - major_projects = [ + # There is probably a better way to do this, but it does work + proj_list: list = get_project_list() + + bucket: str = app.config['S3_BUCKET_ID'] + + major_projects: list[dict] = [ { + "id": p.id, + "date": p.date, "username": p.uid, "name": ldap_get_member(p.uid).cn, "proj_name": p.name, + "tldr": p.tldr, + "time_spent": p.time_spent, + "skills": p.skills, + "desc": p.description, + "links": list(filter(None, p.links.split("\n"))), "status": p.status, - "description": p.description, - "id": p.id, "is_owner": bool(user_dict["username"] == p.uid), + "files": list_files_in_folder(bucket, f"{p.id}/") } - for p in MajorProject.query.filter( - MajorProject.date > start_of_year() - ).order_by(desc(MajorProject.id)) + for p in proj_list ] - major_projects_len = len(major_projects) # return names in 'first last (username)' format return render_template( "major_project_submission.html", major_projects=major_projects, - major_projects_len=major_projects_len, - username=user_dict["username"], - ) + major_projects_len=len(major_projects), + username=user_dict["username"]) + +@major_project_bp.route("/major_project/upload", methods=["POST"]) +@auth.oidc_auth("default") +@get_user +def upload_major_project_files(user_dict=None): + log = logger.new(request=request, auth_dict=user_dict) + log.info('Uploading Major Project File(s)') + + if len(list(request.files.keys())) <1: + return "No file", 400 + + # Temporarily save files to a place, to be uploaded on submit + for _, file in request.files.lists(): + file = file[0] + safe_name: str = secure_filename(file.filename) + filename = f"/tmp/{user_dict['username']}/{safe_name}" + + os.makedirs(os.path.dirname(filename), exist_ok=True) + file.save(filename) + + return jsonify({"success": True}), 200 + @major_project_bp.route("/major_project/submit", methods=["POST"]) @@ -65,27 +99,79 @@ def submit_major_project(user_dict=None): log.info("Submit Major Project") post_data = request.get_json() + name = post_data["projectName"] + tldr = post_data['projectTldr'] + time_spent = post_data['projectTimeSpent'] + skills = post_data['projectSkills'] description = post_data["projectDescription"] + links = post_data['projectLinks'] + + user_id = user_dict['username'] + + log.info(user_id) - if name == "" or description == "": + # All fields are required in order to be able to submit the form + if not name or not tldr or not time_spent or not description: return jsonify({"success": False}), 400 - project = MajorProject(user_dict["username"], name, description) - # Don't you dare try pinging @channel + project: MajorProject = MajorProject(user_id, name, tldr, time_spent, description, links) + + # Save the info to the database + db.session.add(project) + db.session.commit() + + project = MajorProject.query.filter( + MajorProject.name == name, + MajorProject.uid == user_id + ).first() + + skills_list: list = list(filter(lambda x: x != 'None', skills)) + + for skill in skills_list: + skill = skill.strip() + + if skill not in ("", 'None'): + mp_skill = MajorProjectSkill(project.id, skill) + db.session.add(mp_skill) + + db.session.commit() + + # Fail if attempting to retreive non-existent project + if project is None: + return jsonify({"success": False}), 500 + + # Sanitize input so that the Slackbot cannot ping @channel name = name.replace(" *{get_member_name(username)}* ({username})" - f" submitted their major project, *{name}*! Please be sure to reach out" - f" to E-Board members to answer any questions they may have regarding" - f" your project!" + "text": f" *{get_member_name(user_id)}* ({user_id})" + f" submitted their major project, *{name}*!" } ) - db.session.add(project) - db.session.commit() + return jsonify({"success": True}), 200 @@ -95,7 +181,7 @@ def submit_major_project(user_dict=None): def major_project_review(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eval_director(user_dict["account"]): + if not user_dict_is_eval_director(user_dict["account"]): return redirect("/dashboard", code=302) post_data = request.get_json() @@ -106,8 +192,10 @@ def major_project_review(user_dict=None): print(post_data) MajorProject.query.filter(MajorProject.id == pid).update({"status": status}) + db.session.flush() db.session.commit() + return jsonify({"success": True}), 200 @@ -121,10 +209,12 @@ def major_project_delete(pid, user_dict=None): major_project = MajorProject.query.filter(MajorProject.id == pid).first() creator = major_project.uid - if creator == user_dict["username"] or ldap_is_eval_director(user_dict["account"]): + if creator == user_dict["username"] or user_dict_is_eval_director(user_dict["account"]): MajorProject.query.filter(MajorProject.id == pid).delete() + db.session.flush() db.session.commit() + return jsonify({"success": True}), 200 return "Must be project owner to delete!", 401 diff --git a/conditional/models/models.py b/conditional/models/models.py index 2683a599..f3a52072 100644 --- a/conditional/models/models.py +++ b/conditional/models/models.py @@ -127,27 +127,41 @@ def __init__(self, fid, seminar_id): self.fid = fid self.seminar_id = seminar_id - class MajorProject(db.Model): __tablename__ = 'major_projects' id = Column(Integer, primary_key=True) date = Column(Date, nullable=False) uid = Column(String(32), nullable=False, index=True) name = Column(String(64), nullable=False) - description = Column(Text) + tldr = Column(String(128), nullable=True) + time_spent = Column(Text, nullable=True) + description = Column(Text, nullable=False) + links = Column(Text, nullable=True) active = Column(Boolean, nullable=False) status = Column(Enum('Pending', 'Passed', 'Failed', name="major_project_enum"), nullable=False) - def __init__(self, uid, name, desc): + def __init__(self, uid, name, tldr, time_spent, description, links): # pylint: disable=too-many-positional-arguments self.uid = uid self.date = datetime.now() self.name = name - self.description = desc + self.tldr = tldr + self.time_spent = time_spent + self.description = description + self.links = links self.status = 'Pending' self.active = True +class MajorProjectSkill(db.Model): + __tablename__ = "major_project_skills" + project_id = Column(Integer, ForeignKey('major_projects.id', ondelete="cascade"), nullable=False, primary_key=True) + skill = Column(Text, nullable=False, primary_key=True) + + def __init__(self, project_id, skill): + self.project_id = project_id + self.skill = skill + class HouseMeeting(db.Model): __tablename__ = 'house_meetings' diff --git a/conditional/templates/dashboard.html b/conditional/templates/dashboard.html index 397c8ef4..6fba54ef 100644 --- a/conditional/templates/dashboard.html +++ b/conditional/templates/dashboard.html @@ -15,21 +15,21 @@

{{ get_member_name(username) }}

{{username}}@csh.rit.edu
{% if active %} - Active + Active {% else %} - Inactive + Inactive {% endif %} - + {% if onfloor %} - On-floor Status + On-floor Status {% else %} - Off-floor Status + Off-floor Status {% endif %} - + {% if voting %} - Voting + Voting {% else %} - Non-Voting + Non-Voting {% endif %}
@@ -38,7 +38,7 @@
{{username}}@csh.rit.edu
- + {% if freshman %}
@@ -55,16 +55,16 @@

Freshman Evaluations
- - - - - - - + + + + + + @@ -74,378 +74,415 @@

Freshman Evaluations {% if freshman['committee_meetings'] >= 6 %} {% else %} {% endif %} {{freshman['committee_meetings']}} / 6 - - - -

- - - - -
Evaluations Date{{freshman['eval_date']}}
Signatures Missed - {% if freshman['sig_missed'] == 0 %} - None {% else %} - {{freshman['sig_missed']}} {% endif %} +
Evaluations Date{{freshman['eval_date']}}
Signatures Missed + {% if freshman['sig_missed'] == 0 %} + None {% else %} + {{freshman['sig_missed']}} {% endif %}
House Meetings Missed - {% if freshman['hm_missed'] == 0 %} - None {% else %} - {{ freshman['hm_missed'] }} {% endif %} - -
-
- Technical Seminars {% if freshman['ts_total'] == 0 %} -
- -
- {% else %} -
-
    - {% for ts in freshman['ts_list'] %} -
  • {{ts}}
  • - {% endfor %} -
-
- {% endif %} -
-
+ + + + + House Meetings Missed + + {% if freshman['hm_missed'] == 0 %} + None {% else %} + {{ freshman['hm_missed'] }} {% endif %} + + + + + +
+ Technical Seminars {% if freshman['ts_total'] == 0 %} +
+
- {% endif %} - {% if spring['status'] != None %} -
-
-

Membership Evaluations - {% if spring['status'] == "Passed" %} - Passed - {% elif spring['status'] == "Failed" %} - Failed - {% elif active %} - Pending - {% endif %} -

-
-
- Technical Seminars {% if ts_total == 0 %} -
- -
- {% else %} -
-
    - {% for ts in ts_list %} -
  • {{ts}}
  • - {% endfor %} -
-
- {% endif %} -
-
- - - - - - - - - - - - - - - -
Directorship Meetings - {% if spring['committee_meetings'] >= spring['req_meetings'] %} - {% else %} - {% endif %} {{ spring['committee_meetings'] }} / {{ spring['req_meetings'] }} -
House Meetings Missed - {% if spring['hm_missed'] == 0 %} - None {% else %} - {{spring['hm_missed']}} - {% endif %} - -
Major Project - - {% if spring['mp_status'] == "Passed" %} - Passed {% elif spring['mp_status'] == "Pending" %} - Pending {% else %} - None {% endif %} - -
-
+ {% else %} +
+
    + {% for ts in freshman['ts_list'] %} +
  • {{ts}}
  • + {% endfor %} +
{% endif %} - {% if conditionals_len != 0 %} -
-
-

Conditionals

-
-
-
- - - - - - - - - {% for c in conditionals %} - - - - - - - {% endfor %} - -
Date AssignedDate DueDescriptionStatus
{{c['date_created']}}{{c['date_due']}}{{c['description']}} - {% if c['status'] == "Passed" %} - {% elif c['status'] == "Pending" %} - Pending {% else %} - Failed {% endif %} -
- -
-
-
+
+
+

+{% endif %} +{% if spring['status'] != None %} +
+
+

Membership Evaluations + {% if spring['status'] == "Passed" %} + Passed + {% elif spring['status'] == "Failed" %} + Failed + {% elif active %} + Pending {% endif %} +

+
+
+ Technical Seminars {% if ts_total == 0 %} +
+ +
+ {% else %} +
+
    + {% for ts in ts_list %} +
  • {{ts}}
  • + {% endfor %} +
+
+ {% endif %} +
+
+ + + + + + + + + + + + + + + +
Directorship Meetings + {% if spring['committee_meetings'] >= spring['req_meetings'] %} + {% else %} + {% endif %} {{ spring['committee_meetings'] }} / {{ spring['req_meetings'] }} +
House Meetings Missed + {% if spring['hm_missed'] == 0 %} + None {% else %} + {{spring['hm_missed']}} + {% endif %} + +
Major Project + + {% if spring['mp_status'] == "Passed" %} + Passed {% elif spring['mp_status'] == "Pending" %} + Pending {% else %} + None {% endif %} + +
+
+
+{% endif %} +{% if conditionals_len != 0 %} +
+
+

Conditionals

+
+
+
+ + + + + + + + + {% for c in conditionals %} + + + + + + + {% endfor %} + +
Date AssignedDate DueDescriptionStatus
{{c['date_created']}}{{c['date_due']}}{{c['description']}} + {% if c['status'] == "Passed" %} + {% elif c['status'] == "Pending" %} + Pending {% else %} + Failed {% endif %} +
+ +
+
+
+{% endif %} -
-
-

Member Statistics

-
-
-
- - - - - - - {% for title in voting_count %} - - - - - {% endfor %} - -
CategoryMembers
{{ title }}{{ voting_count[title] }}
+
+
+

Member Statistics

+
+
+
+ + + + + + + {% for title in voting_count %} + + + + + {% endfor %} + +
CategoryMembers
{{ title }}{{ voting_count[title] }}
+ +
+
+
-
-
+{% if major_projects_count == 0 and not active%} + +{% elif major_projects_count > 0 %} +
+
+

Major Projects

+
+
+ {% for p in major_projects %} +
+ {% if p['status'] == "Passed" %} +
{{p['name']}}
+ {% elif p['status'] == "Pending" %} +
+
{{p['name']}}
+
- - {% if major_projects_count == 0 and not active%} - - {% elif major_projects_count > 0 %} -
-
-

Major Projects

+ {% else %} +
{{p['name']}}
+ {% endif %} +
+ TLDR + +

{{p['tldr']}}

+
+ +
+ Time Spent + +

{{p['time_spent']}}

+
+ +
+ Skills Applied + +
+ {% for s in p['skills'] %} +

{{s}}

+ {% endfor %}
-
- {% for p in major_projects %} -
- {% if p['status'] == "Passed" %} -
{{p['name']}}
- {% elif p['status'] == "Pending" %} -
- {{p['name']}} - -
- {% else %} -
{{p['name']}}
- {% endif %} -
{{p['description']}}
-
- +
+ +
- {% endif %} - - {% if accepting_dues and check_current_student(username) and not active and not bad_standing %} -
-
-

Become Active

-
-
- Hey there, you're eligible to become an active member! Click the button below if you'd like to become active and pay dues. -
- -
- {% endif %} - +
+ Description + +

{{p['desc']}}

+
+
+ {% endfor %} +
+
+{% endif %} -
- {% if housing %} -
-
-

Housing Status

-
-
+{% if accepting_dues and check_current_student(username) and not active and not bad_standing %} +
+
+

Become Active

+
+
+ Hey there, you're eligible to become an active member! Click the button below if you'd like to become active and pay dues. +
+ +
+{% endif %} - - - - - - + - {% if housing['room'] != None %} - - - - - {% endif %} - {% if housing['queue_pos'][0] != None and housing['room'] == None %} - - - - - {% endif %} - -
Housing Points{{housing['points']}}
Room Number{{housing['room']}}
Housing Queue Position - - {{housing['queue_pos'][0]}} / {{housing['queue_pos'][1]}} - -
+
+ {% if housing %} +
+
+

Housing Status

+
+
+ + + + + + + + + {% if housing['room'] != None %} + + + + {% endif %} - - - - {% if active %} -
-
-

Gatekeep - {% if gatekeep['status'] == "passing" %} - Ok! - {% elif gatekeep['status'] == "disenfranchised" and gatekeep_active%} - Disenfranchised - {% elif active %} - Pending - {% endif %} -

-
-
-
Housing Points{{housing['points']}}
Room Number{{housing['room']}}
- - {% if not gatekeep_active %} - - - - - {% endif %} - - - - - - - - - - - - - -
Status - - Gatekeep Inactive Until 6 Weeks - -
Technical Seminars - - {% if gatekeep['technical_seminars'] >= 2 %} - - {% else %} - - {% endif %} - {{ gatekeep['technical_seminars'] }} / 2 - -
Directorship Meetings - - {% if gatekeep['committee_meetings'] >= 6 %} - - {% else %} - - {% endif %} - {{ gatekeep['committee_meetings']}} / 6 - -
House Meetings Missed - {% if gatekeep['hm_missed'] == 0 %} - None - {% elif gatekeep['hm_missed'] <= 1 %} - {{ gatekeep['hm_missed'] }} Missed - {% else %} - {{gatekeep['hm_missed']}} - {% endif %} - -
-
-
- {% endif %} - - {% if hm_attendance_len == 0 and active%} -
- You haven't missed any house meetings. -
- {% elif hm_attendance_len > 0 %} -
-
-

Missed House Meetings

-
-
- - - - - - - {% for a in hm_attendance %} - - - - - {% endfor %} - -
DateReason
{{a['datetime']}}{{a['reason']}}
-
-
+ {% if housing['queue_pos'][0] != None and housing['room'] == None %} + + Housing Queue Position + + + {{housing['queue_pos'][0]}} / {{housing['queue_pos'][1]}} + + + + {% endif %} + + {% endif %} +
+
+ + {% if active %} +
+
+

Gatekeep + {% if gatekeep['status'] == "passing" %} + Ok! + {% elif gatekeep['status'] == "disenfranchised" and gatekeep_active%} + Disenfranchised + {% elif active %} + Pending + {% endif %} +

+
+
+ + + {% if not gatekeep_active %} + + + + + {% endif %} + + + + + + + + + + + + + +
Status + + Gatekeep Inactive Until 6 Weeks + +
Technical Seminars + + {% if gatekeep['technical_seminars'] >= 2 %} + + {% else %} + + {% endif %} + {{ gatekeep['technical_seminars'] }} / 2 + +
Directorship Meetings + + {% if gatekeep['committee_meetings'] >= 6 %} + + {% else %} + + {% endif %} + {{ gatekeep['committee_meetings']}} / 6 + +
House Meetings Missed + {% if gatekeep['hm_missed'] == 0 %} + None + {% elif gatekeep['hm_missed'] <= 1 %} + {{ gatekeep['hm_missed'] }} Missed + {% else %} + {{gatekeep['hm_missed']}} + {% endif %} + +
+
+
+{% endif %} - {% if cm_attendance_len == 0 and active%} -
You have not attended any directorship meetings.
- {% elif cm_attendance_len > 0 %} -
-
-

Directorship Meeting Attendance

-
+{% if hm_attendance_len == 0 and active%} +
+ You haven't missed any house meetings. +
+{% elif hm_attendance_len > 0 %} +
+
+

Missed House Meetings

+
+
+ + + + + + + {% for a in hm_attendance %} + + + + + {% endfor %} + +
DateReason
{{a['datetime']}}{{a['reason']}}
+
+
+{% endif %} -
- - - - - - - - - {% for meeting in cm_attendance %} - - - - - {% endfor %} - -
EventDate
{{meeting['committee']}}{{meeting['timestamp'].strftime('%Y-%m-%d')}}
- -
-
- {% endif %} -
+{% if cm_attendance_len == 0 and active%} +
You have not attended any directorship meetings.
+{% elif cm_attendance_len > 0 %} +
+
+

Directorship Meeting Attendance

+ +
+ + + + + + + + + {% for meeting in cm_attendance %} + + + + + {% endfor %} + +
EventDate
{{meeting['committee']}}{{meeting['timestamp'].strftime('%Y-%m-%d')}}
+ +
+
+{% endif %} +
+
{% endblock %} diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index 6c5446f2..ba9bcf95 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -1,96 +1,345 @@ {% extends "nav.html" %} + +{% block extraHeader %} + + +{% endblock %} + {% block title %} Major Project Form {% endblock %} + {% block body %} -
-

Major Project Form

+
+ +

Major Project Form

+ +
+
+

+ Welcome to the Major Project submission form! We're excited to read about what you've + been working on. For us (E-Board) to best evaluate your project, please give us as much detail as + possible. Don't feel pressured to write full paragraphs though, good bullet points are plenty! +

+ Generally, a major project is something that you make with the goal of challenging yourself, + learning new things, and doing something you would be proud of. Major projects are most likely to + pass when they meet at least 2 of the 3 + + Major Project Pillars + + - considerable time on your project, benefiting House, and meaningfully applying skills. + And of course, after you submit, please try to talk to E-Board members (in-person or over Slack) + so we are familiar with your project and can ask you questions! +

+
+
+
-
-
-
- - +
+
+ + + + + + + + +
+
+ + + +
+ + + + List what skills you meaningfully used while working on this project (at least 2!) + +
+
+ +
+ + + +
-
-
-
-
-
- - + + + + + +
+
+ + + +
+ +
+
+
+ + +
+ Upload Media +
+ + + Drag files here or click to upload. + +
+
+
- + + -

All Major Projects

{% if major_projects_len - <=0 %}
-
-

No Pending Major Projects

+ +
+ +

All Major Projects

+ + {% if major_projects_len <= 0 %} + +
+
+
+
+ No Pending Major Projects +
+
-
-{% else %} - +
+ + {% else %} + {% for p in major_projects %} -
-
-
-

{{p['proj_name']}}

- - {{p['name']}} ({{p['username']}}) + +
+
+ +
+ +
+ +
+

+ {{p['proj_name']}} +

+ + User profile picture of the submitter + + + {{p['name']}} ({{p['username']}}) + +
+ + +
+ {% if is_eval_director %} + + + {% else %} +
+ + {% if p['status']=='Passed' %} +
+ {% elif p['status']=='Failed' %} +
+ {% else %} +
+ {% endif %} +
+ {% endif %} +
-
- - {% if is_eval_director %} - -
- - + + +
+
+
- {% else %} - {% if p['status'] == 'Passed' %} -
- {% elif p['status'] == 'Failed' %} -
- {% else %} -
- {% endif %} - {% if p.is_owner and p['status'] == 'Pending' %} - - {% endif %} - {% endif %}
- -
- {{p['description']}} + +
+ + +
+
+
+
+
TLDR
+

{{p['tldr']}}

+
+ +
+
Time Commitment
+

{{p['time_spent']}}

+
+
+ +
+
+
Skills
+
+ {% for s in p['skills'] %} + + {{s}} + + {% endfor %} +
+
+ +
+
Links
+ {% for l in p['links'] %} + {% set href = l %} + {% if not href.startswith('http://') and not href.startswith('https://') %} + {% set href = 'https://' + href %} + {% endif %} +

{{ href }}

+ {% endfor %} +
+
+ +
+
+
Description
+

{{p['desc']}}

+
+
+ +
+
Images
+ +
+ {% if p['files']|length == 0 %} +

No images uploaded

+ {% else %} + {% for f in p['files'] %} + User submitted image for the project + {% endfor %} + {% endif %} +
+ +
- {% endfor %} +
+{% endfor %} {% endif %} -
-{% endblock %} + +{% endblock %} \ No newline at end of file diff --git a/conditional/util/major_project.py b/conditional/util/major_project.py new file mode 100644 index 00000000..7aa53221 --- /dev/null +++ b/conditional/util/major_project.py @@ -0,0 +1,26 @@ +from sqlalchemy import desc, func + +from conditional import db, start_of_year +from conditional.models.models import MajorProject +from conditional.models.models import MajorProjectSkill + + +def get_project_list(): + proj_list = db.session.query( + MajorProject.id, + MajorProject.date, + MajorProject.uid, + MajorProject.name, + MajorProject.tldr, + MajorProject.time_spent, + MajorProject.description, + MajorProject.links, + MajorProject.status, + func.array_agg(MajorProjectSkill.skill).label("skills") + ).outerjoin(MajorProjectSkill, + MajorProject.id == MajorProjectSkill.project_id + ).group_by(MajorProject.id + ).where(MajorProject.date >= start_of_year() + ).order_by(desc(MajorProject.date), desc(MajorProject.id)) + + return proj_list diff --git a/conditional/util/s3.py b/conditional/util/s3.py new file mode 100644 index 00000000..7ce3a999 --- /dev/null +++ b/conditional/util/s3.py @@ -0,0 +1,24 @@ +import boto3 +import botocore +from conditional import app + + +def list_files_in_folder(bucket_name, folder_prefix): + + s3 = boto3.client( + service_name="s3", + aws_access_key_id=app.config['AWS_ACCESS_KEY_ID'], + aws_secret_access_key=app.config['AWS_SECRET_ACCESS_KEY'], + endpoint_url=app.config['S3_URI'] + ) + + try: + response = s3.list_objects(Bucket=bucket_name, Prefix=folder_prefix) + if 'Contents' in response: + return [obj['Key'] for obj in response['Contents']] + + return [] + + except botocore.exceptions.ClientError as e: + print(f"Error listing files in the folder: {e}") + return [] diff --git a/config.env.py b/config.env.py index 554f8e08..d1a39bb5 100644 --- a/config.env.py +++ b/config.env.py @@ -26,6 +26,13 @@ LDAP_BIND_DN = env.get("CONDITIONAL_LDAP_BIND_DN", "cn=conditional,ou=Apps,dc=csh,dc=rit,dc=edu") LDAP_BIND_PW = env.get("CONDITIONAL_LDAP_BIND_PW", "") +# S3 information +S3_URI = env.get("S3_URI", "https://s3.csh.rit.edu") # URL for where the s3 bucket is hosted +S3_BUCKET_ID = env.get("S3_BUCKET_ID", "major-project-media") # name of the bucket +AWS_ACCESS_KEY_ID = env.get("AWS_ACCESS_KEY_ID", "") +AWS_SECRET_ACCESS_KEY = env.get("AWS_SECRET_ACCESS_KEY", "") + + # Sentry config # Not required for local development, but if you set it, make sure the # SENTRY_ENV is 'local-development' diff --git a/frontend/images/photo_video.svg b/frontend/images/photo_video.svg new file mode 100644 index 00000000..2324ec73 --- /dev/null +++ b/frontend/images/photo_video.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/frontend/javascript/modules/majorProjectForm.js b/frontend/javascript/modules/majorProjectForm.js index fc9969c1..a19da72d 100644 --- a/frontend/javascript/modules/majorProjectForm.js +++ b/frontend/javascript/modules/majorProjectForm.js @@ -1,30 +1,77 @@ import FetchUtil from "../utils/fetchUtil"; export default class MajorProjectForm { - constructor(form) { - this.form = form; - this.endpoint = '/major_project/submit'; - this.render(); - } - - render() { - this.form.querySelector('input[type=submit]') - .addEventListener('click', e => this._submitForm(e)); - } - - _submitForm(e) { - e.preventDefault(); - - let payload = { - projectName: this.form.querySelector('input[name=name]').value, - projectDescription: - this.form.querySelector('textarea[name=description]').value - }; - - FetchUtil.postWithWarning(this.endpoint, payload, { - warningText: "You will not be able to edit your " + - "project once it has been submitted.", - successText: "Your project has been submitted." - }); - } -} + + + constructor(form) { + this.form = form; + this.endpoint = '/major_project/submit'; + this.tags_written = false; + this.tag_keys = ["Enter", "Comma", "Tab"]; + this.render(); + } + + render() { + this.form.querySelector('input[type=submit]') + .addEventListener('click', e => this._submitForm(e)); + this.form.querySelector('input[id=skill-input]') + .addEventListener('focusout', e => this.onWriteSkill(e)); + this.form.querySelector('input[id=skill-input]') + .addEventListener('keypress', e => this.onKeyPress(e)); + } + + onKeyPress(e) { + if (this.tag_keys.includes(e.code)) { + e.preventDefault(); + this.onWriteSkill(e); + } + return false; + } + + onWriteSkill(e) { + let input = document.getElementById("skill-input") + if (!this.tags_written) { + this.tags_written = true + + const firstTag = document.getElementsByClassName("skill-tag").item(0); + if (firstTag) firstTag.remove(); + } + + let txt = input.value.replaceAll(/[^a-zA-Z0-9\+\-\.\# ]/g, ''); // allowed characters list + if (txt) input.insertAdjacentHTML("beforebegin", '' + txt + ''); + let skills = this.form.getElementsByClassName("skill-tag") + skills.item(skills.length - 1).addEventListener('click', e => this.onRemoveTag(e)); + input.value = ""; + } + + onRemoveTag(e) { + e.target.remove(); + } + + _submitForm(e) { + e.preventDefault(); + + let skills = []; + + for (const tag of this.form.getElementsByClassName('skill-tag')) { + skills.push(tag.textContent); + } + + let payload = { + projectName: this.form.querySelector('input[name=name]').value, + projectTldr: this.form.querySelector('input[name=tldr]').value, + projectTimeSpent: this.form.querySelector('textarea[name=time-commitment]').value, + projectSkills: skills, + projectDescription: this.form.querySelector('textarea[name=description]').value, + projectLinks: this.form.querySelector('textarea[name=links]').value + }; + + console.log(payload) + + FetchUtil.postWithWarning(this.endpoint, payload, { + warningText: "You will not be able to edit your " + + "project once it has been submitted.", + successText: "Your project has been submitted." + }); + } +} \ No newline at end of file diff --git a/frontend/javascript/modules/majorProjectStatus.js b/frontend/javascript/modules/majorProjectStatus.js index 97a68551..7ca87ad3 100644 --- a/frontend/javascript/modules/majorProjectStatus.js +++ b/frontend/javascript/modules/majorProjectStatus.js @@ -44,7 +44,7 @@ export default class MajorProjectStatus { $(dashboardContainer).hide(); } else { // Major projects page button - $(this.control.closest(".panel")).fadeOut(); + $(this.control.closest(".card")).fadeOut(); } }); } else { diff --git a/frontend/stylesheets/pages/_dashboard.scss b/frontend/stylesheets/pages/_dashboard.scss index debdba53..8cc1bc29 100644 --- a/frontend/stylesheets/pages/_dashboard.scss +++ b/frontend/stylesheets/pages/_dashboard.scss @@ -41,3 +41,11 @@ margin: 5px 0; } } + +.section-header { + font-size: 1.25em; +} + +div.col { + margin-bottom: 1em; +} \ No newline at end of file diff --git a/frontend/stylesheets/pages/_major-project.scss b/frontend/stylesheets/pages/_major-project.scss index 01438510..d9e70f24 100644 --- a/frontend/stylesheets/pages/_major-project.scss +++ b/frontend/stylesheets/pages/_major-project.scss @@ -1,3 +1,135 @@ -.major-project-desc { - white-space: pre-line; +.bg-white { + background-color: #fff; } + +.px-extra { + padding-left: 1em; + padding-right: 1em; +} + +.mp-form-intro { + background-color: #fff; + box-shadow: 1px 1px 2px grey; + padding: 1em; + padding-bottom: 0.25em; + margin-bottom: 2em; +} + +.mp-form { + background-color: #fff; + box-shadow: 1px 1px 2px grey; + margin-bottom: 2em; + padding: 1em; +} + +.form-control { + margin-bottom: 1em; +} +.form-label { + margin-top: 1.25rem; + font-size: 2rem; +} + +.form-label span { + font-size: 1rem; +} + +.form-textarea { + resize: vertical; +} + +.form-skilltags { + box-shadow: inset 0 -1px 0 #ddd; + border: none; + border-radius: 0; + padding: 0 0 10px; + height: fit-content; +} + +.mb-extra { + margin-bottom: 2em; +} + +.proj-listing { + background-color: #fff; + box-shadow: 1px 1px 2px grey; +} + +.rounded-circle { + border-radius: 50%; +} + +.placeholder { + color: #bbb; + font-size: 16px; +} + +.skill-tag { + display: block; + float: left; + background: #b0197e; + padding: 4px 30px 4px 8px; + margin: 2px 3px; + color: #fff; + border-radius: 5px; + transition: .5s all; +} + +.skill-tag:after { + position: absolute; + content: "×"; + border: 1px solid; + border-radius: 10px; + padding: 0 4px; + margin: 3px 0 10px 7px; + font-size: 10px; +} + +.skill-display { + display: block; + float: left; + background: #b0197e; + padding: 4px 8px 4px 8px; + margin: 2px 3px; + color: #fff; + font-weight: 600; + border-radius: 5px; + transition: .5s all; +} + +.skill-tag:hover { + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); +} + +.skill-group { + display: flex; + flex-wrap: wrap; +} + +.line-short { + line-height: 0.5em; +} + +.line-med { + line-height: 1.5em; +} + +.proj-img { + max-width: 30%; + margin-left: 0.5em; + margin-right: 0.5em; +} + +.img-container { + display: flex; + flex-wrap: wrap; + margin-bottom: 1em; +} + +.img-header { + margin-left: 1em; +} + +.no-imgs { + margin-left: 0.2em; +} \ No newline at end of file diff --git a/migrations/versions/6ae578b76143_.py b/migrations/versions/6ae578b76143_add_date_to_.mp_submission.py similarity index 100% rename from migrations/versions/6ae578b76143_.py rename to migrations/versions/6ae578b76143_add_date_to_.mp_submission.py diff --git a/migrations/versions/6c4cf35d7c0c_update_mp_table.py b/migrations/versions/6c4cf35d7c0c_update_mp_table.py new file mode 100644 index 00000000..93a45f86 --- /dev/null +++ b/migrations/versions/6c4cf35d7c0c_update_mp_table.py @@ -0,0 +1,43 @@ +"""Add new Major Project data for improved MP form + +Revision ID: 6c4cf35d7c0c +Revises: f1d08673b870 +Create Date: 2026-03-06 15:42:50.323042 + +""" + +# revision identifiers, used by Alembic. +revision = '6c4cf35d7c0c' +down_revision = 'f1d08673b870' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('major_project_skills', + sa.Column('project_id', sa.Integer(), nullable=False), + sa.Column('skill', sa.Text(), nullable=False), + sa.ForeignKeyConstraint(['project_id'], ['major_projects.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('project_id', 'skill') + ) + op.add_column('major_projects', sa.Column('tldr', sa.String(length=128), nullable=True)) + op.add_column('major_projects', sa.Column('timeSpent', sa.Text(), nullable=True)) + op.add_column('major_projects', sa.Column('links', sa.Text(), nullable=True)) + op.alter_column('major_projects', 'description', + existing_type=sa.TEXT(), + nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('major_projects', 'description', + existing_type=sa.TEXT(), + nullable=True) + op.drop_column('major_projects', 'links') + op.drop_column('major_projects', 'timeSpent') + op.drop_column('major_projects', 'tldr') + op.drop_table('major_project_skills') + # ### end Alembic commands ### diff --git a/migrations/versions/95538b39976f_mp_time_spent.py b/migrations/versions/95538b39976f_mp_time_spent.py new file mode 100644 index 00000000..a2dbf0bb --- /dev/null +++ b/migrations/versions/95538b39976f_mp_time_spent.py @@ -0,0 +1,28 @@ +"""Add time spent to mp table + +Revision ID: 95538b39976f +Revises: 6c4cf35d7c0c +Create Date: 2026-03-09 15:20:23.072283 + +""" + +# revision identifiers, used by Alembic. +revision = '95538b39976f' +down_revision = '6c4cf35d7c0c' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('major_projects', sa.Column('time_spent', sa.Text(), nullable=True)) + op.drop_column('major_projects', 'timeSpent') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('major_projects', sa.Column('timeSpent', sa.TEXT(), autoincrement=False, nullable=True)) + op.drop_column('major_projects', 'time_spent') + # ### end Alembic commands ### diff --git a/package-lock.json b/package-lock.json index 0f69d912..6084610f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "sass": "^1.93.3", "sass-loader": "^16.0.6", "style-loader": "^4.0.0", - "webpack": "^5.102.1", + "webpack": "^5.105.3", "webpack-cli": "^6.0.1" }, "engines": { @@ -2563,13 +2563,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", - "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "version": "25.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", + "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@webassemblyjs/ast": { @@ -2795,9 +2795,9 @@ "license": "Apache-2.0" }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -2981,13 +2981,16 @@ "peer": true }, "node_modules/baseline-browser-mapping": { - "version": "2.8.23", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.23.tgz", - "integrity": "sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bootstrap": { @@ -3077,9 +3080,9 @@ } }, "node_modules/browserslist": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", - "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -3097,11 +3100,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.19", - "caniuse-lite": "^1.0.30001751", - "electron-to-chromium": "^1.5.238", - "node-releases": "^2.0.26", - "update-browserslist-db": "^1.1.4" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -3114,7 +3117,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/callsites": { "version": "3.1.0", @@ -3128,9 +3132,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001753", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz", - "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==", + "version": "1.0.30001775", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", + "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", "dev": true, "funding": [ { @@ -3206,7 +3210,8 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", @@ -3382,21 +3387,21 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.244", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", - "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", "dev": true, "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -3422,9 +3427,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, @@ -3600,7 +3605,6 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -4212,22 +4216,6 @@ "node": ">= 10.13.0" } }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jquery": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", @@ -5114,6 +5102,16 @@ "resolved": "https://registry.npmjs.org/simple-masonry/-/simple-masonry-1.0.5.tgz", "integrity": "sha1-tU6BpzIniokgnbXuPAkYFzIpHmU=" }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5124,6 +5122,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/strip-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", @@ -5165,6 +5174,22 @@ "webpack": "^5.27.0" } }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -5193,9 +5218,9 @@ } }, "node_modules/terser": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", - "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5245,27 +5270,6 @@ } } }, - "node_modules/terser/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/terser/node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -5329,9 +5333,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, @@ -5380,9 +5384,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -5429,9 +5433,9 @@ "license": "MIT" }, "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "license": "MIT", "dependencies": { @@ -5443,9 +5447,9 @@ } }, "node_modules/webpack": { - "version": "5.102.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", - "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", + "version": "5.105.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.3.tgz", + "integrity": "sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==", "dev": true, "license": "MIT", "dependencies": { @@ -5455,25 +5459,25 @@ "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.26.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.4", - "webpack-sources": "^3.3.3" + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" @@ -5560,9 +5564,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index b39cdc23..7c07ceef 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "sass": "^1.93.3", "sass-loader": "^16.0.6", "style-loader": "^4.0.0", - "webpack": "^5.102.1", + "webpack": "^5.105.3", "webpack-cli": "^6.0.1" } } diff --git a/requirements.in b/requirements.in index c1753f85..0ad7c5f0 100644 --- a/requirements.in +++ b/requirements.in @@ -1,6 +1,9 @@ alembic~=1.15.1 astroid~=3.3.9 blinker~=1.4 +boto3==1.35.13 +botocore==1.35.13 +click~=8.1.8 csh_ldap>=2.5.3 ddtrace~=4.4.0 Flask~=3.1.0 diff --git a/requirements.txt b/requirements.txt index 9a54f1ef..b3e1fe19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,9 @@ -# This file was autogenerated by uv via the following command: -# uv pip compile requirements.in +# +# This file is autogenerated by pip-compile with Python 3.14 +# by the following command: +# +# pip-compile --cert=None --client-cert=None --index-url=None --pip-args=None requirements.in +# alembic==1.15.2 # via # -r requirements.in @@ -15,6 +19,13 @@ blinker==1.9.0 # -r requirements.in # flask # sentry-sdk +boto3==1.35.13 + # via -r requirements.in +botocore==1.35.13 + # via + # -r requirements.in + # boto3 + # s3transfer build==1.4.0 # via pip-tools bytecode==0.17.0 @@ -88,7 +99,11 @@ jinja2==3.1.6 # via # -r requirements.in # flask -lazy-object-proxy==1.12.0 +jmespath==1.1.0 + # via + # boto3 + # botocore +lazy-object-proxy==1.4.3 # via -r requirements.in mako==1.3.10 # via @@ -153,6 +168,13 @@ pyproject-hooks==1.2.0 # build # pip-tools python-dotenv==1.2.2 +python-dateutil==2.6.1 + # via + # -r requirements.in + # botocore + # via + # -r requirements.in + # botocore # via pydantic-settings python-editor==1.0.4 # via -r requirements.in @@ -163,7 +185,7 @@ requests==2.32.5 # flask-pyoidc # oic # pyjwkest -sentry-sdk==2.24.1 +sentry-sdk[flask]==2.24.1 # via -r requirements.in setuptools==82.0.0 # via pip-tools @@ -196,6 +218,7 @@ typing-inspection==0.4.2 # pydantic-settings urllib3==2.6.3 # via + # botocore # requests # sentry-sdk werkzeug==3.1.6 @@ -210,3 +233,7 @@ wrapt==1.17.3 # ddtrace zipp==3.23.0 # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools