Skip to content

Commit b5a0e7a

Browse files
authored
New major project form 🔥 🔥 🔥 (#503)
* Fixed migrations fr this time * Restored all my existing work * Made form look decent * More display changes * More display changes * Rebased with dev and fixed issues that resulted from that * Form is almost done i think * Fixed s3 uploads and file display * Added new MP info to dashboard * Uncommented the slackbot ping * Fixed lint * Renamed db column to make lint happy * Reduced duplicate code byt moving helper funcs to utils * Fix a sonarqube issue * Fixed some more sonarqube issues * Added default values for s3 related env vars * Added S3 creds info to readme * updated readme info * Added message to migrations * Fixed bad mobile styling and also s3 issue * Made the links input and dropzone each be half the full row width * Changed replace to replaceAll * changed loop method to make sonarqube happy * Made form fields required and fixed skill tag error * Fixed dashboard divider issue * Fixed links not having tall enough line height on mobile
1 parent b50024e commit b5a0e7a

22 files changed

+1389
-622
lines changed

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ ENV PORT=${PORT}
3535
EXPOSE ${PORT}
3636

3737
COPY conditional /opt/conditional/conditional
38-
COPY *.py package.json /opt/conditional
39-
COPY --from=build-frontend /opt/conditional/conditional/static /opt/conditional/conditional/static
38+
COPY *.py package.json /opt/conditional/
39+
COPY --from=build-frontend /opt/conditional/conditional/static/ /opt/conditional/conditional/static/
4040

4141
RUN ln -sf /usr/share/zoneinfo/America/New_York /etc/localtime
4242

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ OIDC_CLIENT_CONFIG = {
2424
}
2525
```
2626

27+
#### Add S3 Config
28+
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.
29+
30+
There are 2 ways that you can get the needed credentials.
31+
1. Reach out to an RTP for creds to the dev bucket
32+
2. Create your own bucket using [DEaDASS](https://deadass.csh.rit.edu/), and the site will give you the credentials you need.
33+
34+
```py
35+
S3_URI = env.get("S3_URI", "https://s3.csh.rit.edu")
36+
S3_BUCKET_ID = env.get("S3_BUCKET_ID", "major-project-media")
37+
AWS_ACCESS_KEY_ID = env.get("AWS_ACCESS_KEY_ID", "")
38+
AWS_SECRET_ACCESS_KEY = env.get("AWS_SECRET_ACCESS_KEY", "")
39+
```
40+
2741
#### Database
2842
You can either develop using the dev database, or use the local database provided in the docker compose file
2943

conditional/blueprints/dashboard.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
from conditional import start_of_year, auth
55
from conditional.models.models import Conditional
66
from conditional.models.models import HouseMeeting
7-
from conditional.models.models import MajorProject
87
from conditional.models.models import MemberHouseMeetingAttendance
98
from conditional.models.models import MemberSeminarAttendance
109
from conditional.models.models import TechnicalSeminar
1110
from conditional.models.models import SpringEval
1211
from conditional.util.auth import get_user
1312
from conditional.util.flask import render_template
1413
from conditional.util.housing import get_queue_position
14+
from conditional.util.major_project import get_project_list
1515
from conditional.util.member import gatekeep_values, get_active_members, get_freshman_data, get_voting_members, \
1616
get_cm, get_hm, is_gatekeep_active, req_cm
1717
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):
8282

8383
data['housing'] = housing
8484

85+
proj_list = get_project_list()
86+
8587
data['major_projects'] = [
8688
{
87-
'id': p.id,
88-
'name': p.name,
89-
'status': p.status,
90-
'description': p.description
91-
} for p in
92-
MajorProject.query.filter(MajorProject.uid == uid,
93-
MajorProject.date > start_of_year())]
89+
"id": p.id,
90+
"date": p.date,
91+
"name": p.name,
92+
"proj_name": p.name,
93+
"tldr": p.tldr,
94+
"time_spent": p.time_spent,
95+
"skills": p.skills,
96+
"desc": p.description,
97+
"links": list(filter(None, p.links.split("\n"))),
98+
"status": p.status,
99+
}
100+
for p in proj_list
101+
]
94102

95103
data['major_projects_count'] = len(data['major_projects'])
96104

conditional/blueprints/major_project_submission.py

Lines changed: 119 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,94 @@
1+
import collections
12
import json
2-
import requests
3+
import os
34

45
from flask import Blueprint
56
from flask import request
67
from flask import jsonify
78
from flask import redirect
89

9-
from sqlalchemy import desc
10-
10+
import requests
11+
import boto3
1112
import structlog
1213

13-
from conditional.util.context_processors import get_member_name
14+
from werkzeug.utils import secure_filename
1415

16+
from conditional import db, get_user, auth, app
1517
from conditional.models.models import MajorProject
18+
from conditional.models.models import MajorProjectSkill
1619

17-
from conditional.util.ldap import ldap_is_eval_director
20+
from conditional.util.context_processors import get_member_name
1821
from conditional.util.ldap import ldap_get_member
1922
from conditional.util.flask import render_template
23+
from conditional.util.s3 import list_files_in_folder
24+
from conditional.util.user_dict import user_dict_is_eval_director
25+
from conditional.util.major_project import get_project_list
2026

21-
from conditional import db, start_of_year, get_user, auth, app
27+
collections.Callable = collections.abc.Callable
2228

2329
logger = structlog.get_logger()
2430

2531
major_project_bp = Blueprint("major_project_bp", __name__)
2632

27-
2833
@major_project_bp.route("/major_project/")
2934
@auth.oidc_auth("default")
3035
@get_user
3136
def display_major_project(user_dict=None):
3237
log = logger.new(request=request, auth_dict=user_dict)
3338
log.info("Display Major Project Page")
3439

35-
major_projects = [
40+
# There is probably a better way to do this, but it does work
41+
proj_list: list = get_project_list()
42+
43+
bucket: str = app.config['S3_BUCKET_ID']
44+
45+
major_projects: list[dict] = [
3646
{
47+
"id": p.id,
48+
"date": p.date,
3749
"username": p.uid,
3850
"name": ldap_get_member(p.uid).cn,
3951
"proj_name": p.name,
52+
"tldr": p.tldr,
53+
"time_spent": p.time_spent,
54+
"skills": p.skills,
55+
"desc": p.description,
56+
"links": list(filter(None, p.links.split("\n"))),
4057
"status": p.status,
41-
"description": p.description,
42-
"id": p.id,
4358
"is_owner": bool(user_dict["username"] == p.uid),
59+
"files": list_files_in_folder(bucket, f"{p.id}/")
4460
}
45-
for p in MajorProject.query.filter(
46-
MajorProject.date > start_of_year()
47-
).order_by(desc(MajorProject.id))
61+
for p in proj_list
4862
]
4963

50-
major_projects_len = len(major_projects)
5164
# return names in 'first last (username)' format
5265
return render_template(
5366
"major_project_submission.html",
5467
major_projects=major_projects,
55-
major_projects_len=major_projects_len,
56-
username=user_dict["username"],
57-
)
68+
major_projects_len=len(major_projects),
69+
username=user_dict["username"])
70+
71+
@major_project_bp.route("/major_project/upload", methods=["POST"])
72+
@auth.oidc_auth("default")
73+
@get_user
74+
def upload_major_project_files(user_dict=None):
75+
log = logger.new(request=request, auth_dict=user_dict)
76+
log.info('Uploading Major Project File(s)')
77+
78+
if len(list(request.files.keys())) <1:
79+
return "No file", 400
80+
81+
# Temporarily save files to a place, to be uploaded on submit
82+
for _, file in request.files.lists():
83+
file = file[0]
84+
safe_name: str = secure_filename(file.filename)
85+
filename = f"/tmp/{user_dict['username']}/{safe_name}"
86+
87+
os.makedirs(os.path.dirname(filename), exist_ok=True)
88+
file.save(filename)
89+
90+
return jsonify({"success": True}), 200
91+
5892

5993

6094
@major_project_bp.route("/major_project/submit", methods=["POST"])
@@ -65,27 +99,79 @@ def submit_major_project(user_dict=None):
6599
log.info("Submit Major Project")
66100

67101
post_data = request.get_json()
102+
68103
name = post_data["projectName"]
104+
tldr = post_data['projectTldr']
105+
time_spent = post_data['projectTimeSpent']
106+
skills = post_data['projectSkills']
69107
description = post_data["projectDescription"]
108+
links = post_data['projectLinks']
109+
110+
user_id = user_dict['username']
111+
112+
log.info(user_id)
70113

71-
if name == "" or description == "":
114+
# All fields are required in order to be able to submit the form
115+
if not name or not tldr or not time_spent or not description:
72116
return jsonify({"success": False}), 400
73-
project = MajorProject(user_dict["username"], name, description)
74117

75-
# Don't you dare try pinging @channel
118+
project: MajorProject = MajorProject(user_id, name, tldr, time_spent, description, links)
119+
120+
# Save the info to the database
121+
db.session.add(project)
122+
db.session.commit()
123+
124+
project = MajorProject.query.filter(
125+
MajorProject.name == name,
126+
MajorProject.uid == user_id
127+
).first()
128+
129+
skills_list: list = list(filter(lambda x: x != 'None', skills))
130+
131+
for skill in skills_list:
132+
skill = skill.strip()
133+
134+
if skill not in ("", 'None'):
135+
mp_skill = MajorProjectSkill(project.id, skill)
136+
db.session.add(mp_skill)
137+
138+
db.session.commit()
139+
140+
# Fail if attempting to retreive non-existent project
141+
if project is None:
142+
return jsonify({"success": False}), 500
143+
144+
# Sanitize input so that the Slackbot cannot ping @channel
76145
name = name.replace("<!", "<! ")
77146

78-
username = user_dict["username"]
147+
# Connect to S3 bucket
148+
s3 = boto3.client("s3",
149+
aws_access_key_id=app.config['AWS_ACCESS_KEY_ID'],
150+
aws_secret_access_key=app.config['AWS_SECRET_ACCESS_KEY'],
151+
endpoint_url=app.config['S3_URI'])
152+
153+
# Collect all the locally cached files and put them in the bucket
154+
temp_dir: str = f"/tmp/{user_id}"
155+
if os.path.exists(temp_dir):
156+
for file in os.listdir(temp_dir):
157+
filepath = f"{temp_dir}/{file}"
158+
159+
s3.upload_file(filepath, 'major-project-media', f"{project.id}/{file}")
160+
161+
os.remove(filepath)
162+
163+
# Delete the temp directory once all the files have been stored in S3
164+
os.rmdir(temp_dir)
165+
166+
167+
# Send the slack ping only after we know that the data was properly saved to the DB
79168
send_slack_ping(
80169
{
81-
"text": f"<!subteam^S5XENJJAH> *{get_member_name(username)}* ({username})"
82-
f" submitted their major project, *{name}*! Please be sure to reach out"
83-
f" to E-Board members to answer any questions they may have regarding"
84-
f" your project!"
170+
"text": f"<!subteam^S5XENJJAH> *{get_member_name(user_id)}* ({user_id})"
171+
f" submitted their major project, *{name}*!"
85172
}
86173
)
87-
db.session.add(project)
88-
db.session.commit()
174+
89175
return jsonify({"success": True}), 200
90176

91177

@@ -95,7 +181,7 @@ def submit_major_project(user_dict=None):
95181
def major_project_review(user_dict=None):
96182
log = logger.new(request=request, auth_dict=user_dict)
97183

98-
if not ldap_is_eval_director(user_dict["account"]):
184+
if not user_dict_is_eval_director(user_dict["account"]):
99185
return redirect("/dashboard", code=302)
100186

101187
post_data = request.get_json()
@@ -106,8 +192,10 @@ def major_project_review(user_dict=None):
106192

107193
print(post_data)
108194
MajorProject.query.filter(MajorProject.id == pid).update({"status": status})
195+
109196
db.session.flush()
110197
db.session.commit()
198+
111199
return jsonify({"success": True}), 200
112200

113201

@@ -121,10 +209,12 @@ def major_project_delete(pid, user_dict=None):
121209
major_project = MajorProject.query.filter(MajorProject.id == pid).first()
122210
creator = major_project.uid
123211

124-
if creator == user_dict["username"] or ldap_is_eval_director(user_dict["account"]):
212+
if creator == user_dict["username"] or user_dict_is_eval_director(user_dict["account"]):
125213
MajorProject.query.filter(MajorProject.id == pid).delete()
214+
126215
db.session.flush()
127216
db.session.commit()
217+
128218
return jsonify({"success": True}), 200
129219

130220
return "Must be project owner to delete!", 401

conditional/models/models.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,27 +127,41 @@ def __init__(self, fid, seminar_id):
127127
self.fid = fid
128128
self.seminar_id = seminar_id
129129

130-
131130
class MajorProject(db.Model):
132131
__tablename__ = 'major_projects'
133132
id = Column(Integer, primary_key=True)
134133
date = Column(Date, nullable=False)
135134
uid = Column(String(32), nullable=False, index=True)
136135
name = Column(String(64), nullable=False)
137-
description = Column(Text)
136+
tldr = Column(String(128), nullable=True)
137+
time_spent = Column(Text, nullable=True)
138+
description = Column(Text, nullable=False)
139+
links = Column(Text, nullable=True)
138140
active = Column(Boolean, nullable=False)
139141
status = Column(Enum('Pending', 'Passed', 'Failed',
140142
name="major_project_enum"),
141143
nullable=False)
142144

143-
def __init__(self, uid, name, desc):
145+
def __init__(self, uid, name, tldr, time_spent, description, links): # pylint: disable=too-many-positional-arguments
144146
self.uid = uid
145147
self.date = datetime.now()
146148
self.name = name
147-
self.description = desc
149+
self.tldr = tldr
150+
self.time_spent = time_spent
151+
self.description = description
152+
self.links = links
148153
self.status = 'Pending'
149154
self.active = True
150155

156+
class MajorProjectSkill(db.Model):
157+
__tablename__ = "major_project_skills"
158+
project_id = Column(Integer, ForeignKey('major_projects.id', ondelete="cascade"), nullable=False, primary_key=True)
159+
skill = Column(Text, nullable=False, primary_key=True)
160+
161+
def __init__(self, project_id, skill):
162+
self.project_id = project_id
163+
self.skill = skill
164+
151165

152166
class HouseMeeting(db.Model):
153167
__tablename__ = 'house_meetings'

0 commit comments

Comments
 (0)