Skip to content

Commit 4343826

Browse files
authored
Merge pull request #509 from ComputerScienceHouse/develop
Dev -> main
1 parent 3575529 commit 4343826

26 files changed

+1566
-730
lines changed

.github/pull_request_template.md

Lines changed: 0 additions & 19 deletions
This file was deleted.

.github/workflows/sonarqube.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Build
1+
name: Sonarqube
22

33
on:
44
push:
@@ -10,8 +10,9 @@ on:
1010

1111
jobs:
1212
build:
13-
name: Build and analyze
13+
name: Sonarqube Analysis
1414
runs-on: ubuntu-latest
15+
if: ${{ !startsWith(github.head_ref, 'dependabot/') }}
1516

1617
steps:
1718
- uses: actions/checkout@v4

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: 19 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

@@ -82,6 +96,11 @@ It is likely easier to use containers like `podman` or `docker` or the correspon
8296

8397
With podman, I have been using
8498

99+
```sh
100+
podman compose up --watch
101+
```
102+
103+
If you want, you can run without compose support using
85104
```sh
86105
podman compose up --force-recreate --build
87106
```

conditional/blueprints/attendance.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def display_attendance_hm(user_dict=None):
175175
def submit_committee_attendance(user_dict=None):
176176
log = logger.new(request=request, auth_dict=user_dict)
177177

178-
approved = user_dict_is_eval_director(user_dict)
178+
approved = user_dict_is_eboard(user_dict)
179179
post_data = request.get_json()
180180

181181
committee = post_data['committee']

conditional/blueprints/dashboard.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
from flask import Blueprint, request
33

44
from conditional import start_of_year, auth
5-
from conditional.models.models import Conditional
5+
from conditional.models.models import Conditional, MajorProject
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, \
@@ -84,13 +84,19 @@ def display_dashboard(user_dict=None):
8484

8585
data['major_projects'] = [
8686
{
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())]
87+
"id": p.id,
88+
"date": p.date,
89+
"name": p.name,
90+
"proj_name": p.name,
91+
"tldr": p.tldr,
92+
"time_spent": p.time_spent,
93+
"skills": p.skills,
94+
"desc": p.description,
95+
"links": list(filter(None, p.links.split("\n"))),
96+
"status": p.status,
97+
}
98+
for p in get_project_list().filter(MajorProject.uid == uid)
99+
]
94100

95101
data['major_projects_count'] = len(data['major_projects'])
96102

conditional/blueprints/major_project_submission.py

Lines changed: 126 additions & 32 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,83 @@ 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']
70109

71-
if name == "" or description == "":
110+
user_id = user_dict['username']
111+
112+
log.info(user_id)
113+
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
76-
name = name.replace("<!", "<! ")
118+
project: MajorProject = MajorProject(user_id, name, tldr, time_spent, description, links)
77119

78-
username = user_dict["username"]
79-
send_slack_ping(
80-
{
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!"
85-
}
86-
)
120+
# Save the info to the database
87121
db.session.add(project)
88122
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
145+
name = name.replace("<!", "<! ")
146+
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
168+
if app.config['DEV_DISABLE_SLACK_PING']:
169+
log.info("Slack ping skipped due to environment override")
170+
else:
171+
send_slack_ping(
172+
{
173+
"text": f"<!subteam^S5XENJJAH> *{get_member_name(user_id)}* ({user_id})"
174+
f" submitted their major project, *{name}*!"
175+
}
176+
)
177+
178+
89179
return jsonify({"success": True}), 200
90180

91181

@@ -95,7 +185,7 @@ def submit_major_project(user_dict=None):
95185
def major_project_review(user_dict=None):
96186
log = logger.new(request=request, auth_dict=user_dict)
97187

98-
if not ldap_is_eval_director(user_dict["account"]):
188+
if not user_dict_is_eval_director(user_dict["account"]):
99189
return redirect("/dashboard", code=302)
100190

101191
post_data = request.get_json()
@@ -106,8 +196,10 @@ def major_project_review(user_dict=None):
106196

107197
print(post_data)
108198
MajorProject.query.filter(MajorProject.id == pid).update({"status": status})
199+
109200
db.session.flush()
110201
db.session.commit()
202+
111203
return jsonify({"success": True}), 200
112204

113205

@@ -121,10 +213,12 @@ def major_project_delete(pid, user_dict=None):
121213
major_project = MajorProject.query.filter(MajorProject.id == pid).first()
122214
creator = major_project.uid
123215

124-
if creator == user_dict["username"] or ldap_is_eval_director(user_dict["account"]):
216+
if creator == user_dict["username"] or user_dict_is_eval_director(user_dict["account"]):
125217
MajorProject.query.filter(MajorProject.id == pid).delete()
218+
126219
db.session.flush()
127220
db.session.commit()
221+
128222
return jsonify({"success": True}), 200
129223

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

0 commit comments

Comments
 (0)