Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
39efe97
Fixed migrations fr this time
pikachu0542 Mar 1, 2026
d4a719b
Restored all my existing work
pikachu0542 Mar 1, 2026
821ba2b
Made form look decent
pikachu0542 Mar 2, 2026
fc1d5c6
More display changes
pikachu0542 Mar 4, 2026
0dbf574
More display changes
pikachu0542 Mar 4, 2026
30ff53e
Rebased with dev and fixed issues that resulted from that
pikachu0542 Mar 7, 2026
ef1aaf9
Form is almost done i think
pikachu0542 Mar 8, 2026
80e2cf8
Fixed s3 uploads and file display
pikachu0542 Mar 9, 2026
86a2060
Added new MP info to dashboard
pikachu0542 Mar 9, 2026
c603cfe
Uncommented the slackbot ping
pikachu0542 Mar 9, 2026
8fa3f15
Fixed lint
pikachu0542 Mar 9, 2026
52492de
Renamed db column to make lint happy
pikachu0542 Mar 9, 2026
992fd00
Reduced duplicate code byt moving helper funcs to utils
pikachu0542 Mar 9, 2026
e4111a4
Fix a sonarqube issue
pikachu0542 Mar 9, 2026
7286a02
Fixed some more sonarqube issues
pikachu0542 Mar 9, 2026
46b3d03
Added default values for s3 related env vars
pikachu0542 Mar 10, 2026
03311c0
Added S3 creds info to readme
pikachu0542 Mar 10, 2026
4a2c7c1
updated readme info
pikachu0542 Mar 10, 2026
7e55c6c
Added message to migrations
pikachu0542 Mar 10, 2026
4ab2dbf
Fixed bad mobile styling and also s3 issue
pikachu0542 Mar 10, 2026
b2b7b1d
Made the links input and dropzone each be half the full row width
pikachu0542 Mar 10, 2026
ffb22ae
Changed replace to replaceAll
pikachu0542 Mar 10, 2026
ef19ca6
changed loop method to make sonarqube happy
pikachu0542 Mar 10, 2026
4b5f12a
Made form fields required and fixed skill tag error
pikachu0542 Mar 10, 2026
d3e7408
Fixed dashboard divider issue
pikachu0542 Mar 10, 2026
9935e0a
Fixed links not having tall enough line height on mobile
pikachu0542 Mar 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 16 additions & 8 deletions conditional/blueprints/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
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
from conditional.models.models import SpringEval
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, \
Expand Down Expand Up @@ -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'])

Expand Down
148 changes: 119 additions & 29 deletions conditional/blueprints/major_project_submission.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,94 @@
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
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"])
Expand All @@ -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("<!", "<! ")

username = user_dict["username"]
# Connect to S3 bucket
s3 = boto3.client("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'])

# Collect all the locally cached files and put them in the bucket
temp_dir: str = f"/tmp/{user_id}"
if os.path.exists(temp_dir):
for file in os.listdir(temp_dir):
filepath = f"{temp_dir}/{file}"

s3.upload_file(filepath, 'major-project-media', f"{project.id}/{file}")

os.remove(filepath)

# Delete the temp directory once all the files have been stored in S3
os.rmdir(temp_dir)


# Send the slack ping only after we know that the data was properly saved to the DB
send_slack_ping(
{
"text": f"<!subteam^S5XENJJAH> *{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"<!subteam^S5XENJJAH> *{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


Expand All @@ -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()
Expand All @@ -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


Expand All @@ -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
Expand Down
22 changes: 18 additions & 4 deletions conditional/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading