Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finals database support #893

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ courses20.xml
compose-dev.yaml
rpi_data/get-summer-2023-2.sh
rpi_data/summer-20232.csv
.venv/
24 changes: 24 additions & 0 deletions rpi_data/modules/post_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import requests
import base64
import os

url = os.environ.get('yacs_url')
url = "http://localhost:5000"
api_location = url + "/api/bulkCourseUpload"
__location__ = os.path.realpath(os.path.join(
os.getcwd(), os.path.dirname(__file__)))


def csvUpload(fileName):
endpath = os.path.join(__location__, fileName)
endpath = os.path.dirname(os.path.dirname(endpath)) + "\\" + fileName
print(endpath)
uploadFile = {'file': open(endpath, 'rb')}
data = {'isPubliclyVisible': 'on'}

r = requests.post(api_location, files=uploadFile, data=data)
print(r.reason, r.status_code)


if __name__ == "__main__":
csvUpload("spring-2022.csv")
431 changes: 431 additions & 0 deletions rpi_data/out.csv

Large diffs are not rendered by default.

74 changes: 53 additions & 21 deletions src/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,25 @@
import db.connection as connection
import db.classinfo as ClassInfo
import db.courses as Courses
import db.finals as Finals
import db.professor as All_professors
import db.semester_info as SemesterInfo
import db.semester_date_mapping as DateMapping
import db.admin as AdminInfo
import pandas as pd
import db.student_course_selection as CourseSelect
import db.user as UserModel
import controller.user as user_controller
import controller.session as session_controller
import controller.userevent as event_controller
from io import StringIO
from sqlalchemy.orm import Session
import json
import os
import pandas as pd
from constants import Constants

"""
NOTE: on caching
on add of semester of change of data from GET
do a cache.clear() to ensure data integrity
"""
# NOTE: on caching
# on add of semester of change of data from GET
# do a cache.clear() to ensure data integrity

app = FastAPI()
app.add_middleware(SessionMiddleware,
Expand All @@ -43,10 +41,11 @@
db_conn = connection.db
class_info = ClassInfo.ClassInfo(db_conn)
courses = Courses.Courses(db_conn, FastAPICache)
finals = Finals.Finals(db_conn, FastAPICache)
date_range_map = DateMapping.semester_date_mapping(db_conn)
admin_info = AdminInfo.Admin(db_conn)
course_select = CourseSelect.student_course_selection(db_conn)
semester_info = SemesterInfo.semester_info(db_conn)
semester_info = SemesterInfo.semester_info(db_conn, FastAPICache)
professor_info = All_professors.Professor(db_conn, FastAPICache)
users = UserModel.User()

Expand Down Expand Up @@ -143,9 +142,8 @@ def set_defaultSemester(semester_set: DefaultSemesterSetPydantic):
success, error = admin_info.set_semester_default(semester_set.default)
if success:
return Response(status_code=200)
else:
print(error)
return Response(error.__str__(), status_code=500)
print(error)
return Response(error.__str__(), status_code=500)

#Parses the data from the .csv data files
@app.post('/api/bulkCourseUpload')
Expand Down Expand Up @@ -176,13 +174,11 @@ async def uploadHandler(
isSuccess, error = courses.populate_from_csv(csv_file)
if (isSuccess):
return Response(status_code=200)
else:
print(error)
return Response(error.__str__(), status_code=500)
print(error)
return Response(error.__str__(), status_code=500)

@app.post('/api/bulkProfessorUpload')
async def uploadJSON(
isPubliclyVisible: str = Form(...),
file: UploadFile = File(...)):
# Check to make sure the user has sent a file
if not file:
Expand All @@ -208,11 +204,48 @@ async def uploadJSON(
if isSuccess:
print("SUCCESS")
return Response(status_code=200)
else:
print("NOT WORKING")
print(error)
return Response(error.__str__(), status_code=500)
print("NOT WORKING")
print(error)
return Response(error.__str__(), status_code=500)

@app.delete('/api/semester/{semester_id}')
async def remove_semester(semester_id: str):
print(semester_id)
semester, error = semester_info.delete_semester(semester=semester_id)
return Response(status_code=200) if not error else Response(str(error), status_code=500)

@app.post('/api/final')
async def uploadHandler(
file: UploadFile = File(...)):
# check for user files
print("in process")
if not file:
return Response("No file received", 400)
if file.filename.find('.') == -1 or file.filename.rsplit('.', 1)[1].lower() != 'csv':
return Response("File must have csv extension", 400)
# get file
contents = await file.read()
csv_file = StringIO(contents.decode())
# Populate DB from CSV
error = finals.populate_from_csv(csv_file)
return Response(error.__str__(), status_code=500) if error else Response("Upload Successful", status_code=200)

@app.get('/api/final/{semester}')
@cache(expire=Constants.DAY_IN_SECONDS, coder=PickleCoder, namespace="API_CACHE")
async def getHandler(semester: str):
if not semester:
return Response("No semester received", 400)
print(semester)
final, error = finals.get_by_semester(semester)
return final if not error else Response(error, status_code=500)

@app.delete('/api/final/{semester}')
async def deleteHandler(semester: str):
if not semester:
return Response("No semester received", 400)
print(semester)
_, error = finals.delete_by_semester(semester)
return Response(error.__str__(), status_code=500) if error else Response("Delete Successful", status_code=200)

@app.post('/api/mapDateRangeToSemesterPart')
async def map_date_range_to_semester_part_handler(request: Request):
Expand All @@ -234,8 +267,7 @@ async def map_date_range_to_semester_part_handler(request: Request):
semester_info.upsert(semester_title, is_publicly_visible)
if (not error):
return Response(status_code=200)
else:
return Response(error, status_code=500)
return Response(error, status_code=500)
return Response("Did not receive proper form data", status_code=500)

@app.get('/api/user/course')
Expand Down
8 changes: 3 additions & 5 deletions src/api/db/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ def __init__(self, db_conn):
self.interface_name = 'admin_info'

def get_semester_default(self):
# NOTE: COALESCE takes first non-null vaue from the list
# NOTE: COALESCE takes first non-null value from the list
result, error = self.db_conn.execute("""
SELECT admin.semester FROM admin_settings admin
UNION ALL
Expand All @@ -21,8 +21,7 @@ def get_semester_default(self):

if error:
return (None, error)
else:
return (default_semester, error)
return (default_semester, error)

def set_semester_default(self, semester):
try:
Expand All @@ -40,5 +39,4 @@ def set_semester_default(self, semester):

if response != None:
return(True, None)
else:
return (False, error)
return (False, error)
110 changes: 110 additions & 0 deletions src/api/db/finals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import csv
import re
from psycopg2.extras import RealDictCursor
from ast import literal_eval
import asyncio

# https://stackoverflow.com/questions/54839933/importerror-with-from-import-x-on-simple-python-files
if __name__ == "__main__":
import connection
else:
from . import connection


class Finals:
def __init__(self, db_wrapper, cache):
self.db = db_wrapper
self.cache = cache

def populate_from_csv(self, csv_text):
conn = self.db.get_connection()
reader = csv.DictReader(csv_text)
# for each course entry insert sections and course sessions
with conn.cursor(cursor_factory=RealDictCursor) as transaction:
for row in reader:
try:
# finals
transaction.execute(
"""
INSERT INTO
final(
semester,
course,
section,
"start",
"end",
room_assignment
)
VALUES (
%(Semester)s,
%(Course)s,
%(Section)s,
TO_TIMESTAMP(%(Start)s, 'YYYY-MM-DD HH24:MI:SS'),
TO_TIMESTAMP(%(End)s, 'YYYY-MM-DD HH24:MI:SS'),
%(Room_Assignment)s
)
ON CONFLICT (semester, course, section, start, room_assignment) DO NOTHING;
""",
{
"Semester": row['Season'] + ' ' + row['Year'],
"Course": row['Major'] + '-' + row['Course'],
"Section": "1" if row['Section'] == '' else row['Section'],
"Start": row['Start'],
"End": row['End'],
"Room_Assignment": row['Building'] + '-' + row['Room_Number']
}
)
except Exception as e:
print(e)
conn.rollback()
return e
conn.commit()
self.clear_cache()
return None

def get_by_semester(self, semester):
return self.db.execute("""
SELECT * FROM final
WHERE semester=%(Semester)s
ORDER BY start ASC;
""", {
"Semester": semester
}, isSELECT=True)

def delete_by_semester(self, semester):
self.clear_cache()
return self.db.execute("""
BEGIN TRANSACTION;
DELETE FROM final
WHERE semester=%(Semester)s;
COMMIT;
""", {
"Semester": semester
}, isSELECT=False)

def bulk_delete(self, semesters):
for semester in semesters:
_, error = self.delete_by_semester(semester)
if error:
print(error)
return error
self.clear_cache()
return None

def clear_cache(self):
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None

if loop and loop.is_running():
loop.create_task(self.cache.clear(namespace="API_CACHE"))
else:
asyncio.run(self.cache.clear("API_CACHE"))

if __name__ == "__main__":
# os.chdir(os.path.abspath("../rpi_data"))
# fileNames = glob.glob("*.csv")
csv_text = open('../../../rpi_data/out.csv', 'r')
finals = Finals(connection.db)
finals.populate_from_csv(csv_text)
29 changes: 27 additions & 2 deletions src/api/db/semester_info.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
class semester_info:
import asyncio

def __init__(self, db_wrapper):
class semester_info:
def __init__(self, db_wrapper, cache):
self.db = db_wrapper
self.cache = cache

def clear_cache(self):
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None

if loop and loop.is_running():
loop.create_task(self.cache.clear(namespace="API_CACHE"))
else:
asyncio.run(self.cache.clear("API_CACHE"))

def upsert(self, semester, isPublic):
self.db.execute("""
Expand All @@ -28,3 +41,15 @@ def is_public(self, semester):
if data is not None and len(data) > 0:
return data[0]['public']
return False

def delete_semester(self, semester):
# clear cache so this semester does not come up again
self.clear_cache()
return self.db.execute("""
BEGIN TRANSACTION;
DELETE FROM semester_info
WHERE semester=%(Semester)s;
COMMIT;
""", {
"Semester": semester
}, isSELECT=False)
36 changes: 36 additions & 0 deletions src/api/migrations/versions/2024-10-15_adding_finals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""adding finals

Revision ID: 1ff4f05483ad
Revises: 6c9a1ffc58a5
Create Date: 2024-10-15 21:38:59.576120

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '6c9a1ffc58a5'
down_revision = 'c959c263997f'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('final',
sa.Column('semester', sa.VARCHAR(length=255), nullable=False),
sa.Column('course', sa.VARCHAR(length=255), nullable=False),
sa.Column('section', sa.INTEGER(), nullable=False),
sa.Column('start', postgresql.TIMESTAMP(), nullable=False),
sa.Column('end', postgresql.TIMESTAMP(), nullable=False),
sa.Column('room_assignment', sa.VARCHAR(length=255), nullable=False),
sa.PrimaryKeyConstraint('semester', 'course', 'section', 'start', 'room_assignment')
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('final')
# ### end Alembic commands ###
Loading