diff --git a/app/controllers/admin/routes.py b/app/controllers/admin/routes.py index 64ae34764..d4495f79f 100644 --- a/app/controllers/admin/routes.py +++ b/app/controllers/admin/routes.py @@ -1,9 +1,10 @@ -from flask import request, render_template, url_for, g, Flask, redirect +from flask import request, render_template, url_for, g, redirect from flask import flash, abort, jsonify, session, send_file from peewee import DoesNotExist, fn, IntegrityError -from playhouse.shortcuts import model_to_dict, dict_to_model +from playhouse.shortcuts import model_to_dict import json -from datetime import datetime, date +from datetime import datetime +import os from app import app from app.models.program import Program @@ -21,13 +22,14 @@ from app.logic.userManagement import getAllowedPrograms, getAllowedTemplates from app.logic.createLogs import createAdminLog from app.logic.certification import getCertRequirements, updateCertRequirements -from app.logic.volunteers import getEventLengthInHours from app.logic.utils import selectSurroundingTerms, getFilesFromRequest from app.logic.events import deleteEvent, attemptSaveEvent, preprocessEventData, calculateRecurringEventFrequency, deleteEventAndAllFollowing, deleteAllRecurringEvents, getBonnerEvents,addEventView, getEventRsvpCountsForTerm -from app.logic.participants import getEventParticipants, getUserParticipatedTrainingEvents, checkUserRsvp, checkUserVolunteer +from app.logic.participants import getUserParticipatedTrainingEvents, checkUserRsvp from app.logic.fileHandler import FileHandler from app.logic.bonner import getBonnerCohorts, makeBonnerXls, rsvpForBonnerCohort from app.controllers.admin import admin_bp +from app.logic.serviceLearningCoursesData import parseUploadedFile, courseParticipantPreviewSessionCleaner + @admin_bp.route('/switch_user', methods=['POST']) @@ -337,6 +339,24 @@ def deleteEventFile(): eventfile.deleteFile(fileData["fileId"]) return "" +@admin_bp.route("/uploadCourseParticipant", methods= ["POST"]) +def addCourseFile(): + fileData = request.files['addCourseParticipants'] + filePath = os.path.join(app.config["files"]["base_path"], fileData.filename) + fileData.save(filePath) + (session['errorFlag'], session['courseParticipantPreview'], session['previewCourseDisplayList']) = parseUploadedFile(filePath) + os.remove(filePath) + return redirect(url_for("main.getAllCourseInstructors", showPreviewModal = True)) + +@admin_bp.route("/deleteUploadedFile", methods= ["POST"]) +def deleteCourseFile(): + try: + courseParticipantPreviewSessionCleaner() + except KeyError: + pass + + return "" + @admin_bp.route("/manageBonner") def manageBonner(): if not g.current_user.isCeltsAdmin: diff --git a/app/controllers/admin/userManagement.py b/app/controllers/admin/userManagement.py index 80ff1ed18..224304752 100644 --- a/app/controllers/admin/userManagement.py +++ b/app/controllers/admin/userManagement.py @@ -1,14 +1,12 @@ -from flask import Flask, make_response, render_template,request, flash, g, json, abort, redirect, url_for +from flask import render_template,request, flash, g, abort, redirect, url_for import re from app.controllers.admin import admin_bp from app.models.user import User from app.models.program import Program from app.logic.userManagement import addCeltsAdmin,addCeltsStudentStaff,removeCeltsAdmin,removeCeltsStudentStaff -from app.logic.userManagement import changeCurrentTerm from app.logic.userManagement import changeProgramInfo from app.logic.utils import selectSurroundingTerms -from app.logic.userManagement import addNextTerm -from app.models.term import Term +from app.logic.term import addNextTerm, changeCurrentTerm @admin_bp.route('/admin/manageUsers', methods = ['POST']) def manageUsers(): diff --git a/app/controllers/main/routes.py b/app/controllers/main/routes.py index a20661b9f..a457e57c2 100644 --- a/app/controllers/main/routes.py +++ b/app/controllers/main/routes.py @@ -33,6 +33,7 @@ from app.logic.courseManagement import unapprovedCourses, approvedCourses from app.logic.utils import selectSurroundingTerms from app.logic.certification import getCertRequirementsWithCompletion +from app.logic.serviceLearningCoursesData import saveCourseParticipantsToDatabase,courseParticipantPreviewSessionCleaner from app.logic.createLogs import createRsvpLog, createAdminLog @main_bp.route('/logout', methods=['GET']) @@ -378,25 +379,44 @@ def getAllCourseInstructors(term=None): """ This function selects all the Instructors Name and the previous courses """ + showPreviewModal = request.args.get('showPreviewModal', default=False, type=bool) + + if showPreviewModal and 'courseParticipantPreview' in session: + courseParticipantPreview = session['courseParticipantPreview'] + else: + courseParticipantPreview = [] + + errorFlag = session.get('errorFlag') + previewParticipantDisplayList = session.get('previewCourseDisplayList') + if g.current_user.isCeltsAdmin: setRedirectTarget(request.full_path) courseDict = getCourseDict() - term = Term.get_or_none(Term.id == term) or g.current_term unapproved = unapprovedCourses(term) approved = approvedCourses(term) terms = selectSurroundingTerms(g.current_term) + if request.method =='POST' and "submitParticipant" in request.form: + saveCourseParticipantsToDatabase(session['courseParticipantPreview']) + courseParticipantPreviewSessionCleaner() + flash('File saved successfully!', 'success') + return redirect(url_for('main.getAllCourseInstructors')) + return render_template('/main/manageServiceLearningFaculty.html', courseInstructors = courseDict, unapprovedCourses = unapproved, approvedCourses = approved, terms = terms, term = term, - CourseStatus = CourseStatus) + CourseStatus = CourseStatus, + previewParticipantsErrorFlag = errorFlag, + courseParticipantPreview= courseParticipantPreview, + previewParticipantDisplayList = previewParticipantDisplayList + ) else: - abort(403) + abort(403) def getRedirectTarget(popTarget=False): """ diff --git a/app/logic/serviceLearningCoursesData.py b/app/logic/serviceLearningCoursesData.py index 2511b02b2..5a9efacd7 100644 --- a/app/logic/serviceLearningCoursesData.py +++ b/app/logic/serviceLearningCoursesData.py @@ -1,3 +1,6 @@ +from flask import session, g +import re as regex +from openpyxl import load_workbook from app.models.course import Course from app.models.user import User from app.models.term import Term @@ -12,6 +15,7 @@ from app.models import DoesNotExist from app.logic.createLogs import createAdminLog from app.logic.fileHandler import FileHandler +from app.logic.term import addPastTerm def getServiceLearningCoursesData(user): """Returns dictionary with data used to populate Service-Learning proposal table""" @@ -29,13 +33,12 @@ def getServiceLearningCoursesData(user): faculty = [f"{instructor.user.firstName} {instructor.user.lastName}" for instructor in otherInstructors] - courseDict[course.id] = { - "id":course.id, - "creator":f"{course.createdBy.firstName} {course.createdBy.lastName}", - "name":course.courseName, - "faculty": faculty, - "term": course.term, - "status": course.status.status} + courseDict[course.id] = {"id":course.id, + "creator":f"{course.createdBy.firstName} {course.createdBy.lastName}", + "name":course.courseName, + "faculty": faculty, + "term": course.term, + "status": course.status.status} return courseDict def withdrawProposal(courseID): @@ -57,9 +60,9 @@ def withdrawProposal(courseID): courseName = course.courseName questions = CourseQuestion.select().where(CourseQuestion.course == course) notes = list(Note.select(Note.id) - .join(QuestionNote) - .where(QuestionNote.question.in_(questions)) - .distinct()) + .join(QuestionNote) + .where(QuestionNote.question.in_(questions)) + .distinct()) course.delete_instance(recursive=True) for note in notes: note.delete_instance() @@ -90,3 +93,109 @@ def renewProposal(courseID, term): user=instructor.user) return newCourse + +def parseUploadedFile(filePath): + excelData = load_workbook(filename=filePath) + excelSheet = excelData.active + errorFlag = False + courseParticipantPreview= {} + previewTerm = '' + previewCourse = '' + studentValue= '' + cellRow = 1 + previewCourseDisplayList = [] + + + for row in excelSheet.iter_rows(): + cellVal = row[0].value + displayRow = '' + termReg = r"\b[a-zA-Z]{3,}( [AB])? \d{4}\b" # Checks for 3 or more letters followed by zero or one space and A or B followed by a single space character followed by 4 digits. For Example: Fall 2020 or Spring B 2021 + courseReg = r"\b[A-Z]{2,4} \d{3}\b" # Checks for between 2-4 capital letters followed by a single space followed by 3 digits. For Example: CSC 226 + bnumberReg = r"\b[B]\d{8}\b" # Checks for a capital B followed by 8 digits. For Example B00123456 + + if regex.search(termReg, str(cellVal)): + if "Spring A" in cellVal or "Spring B" in cellVal: + cellVal = "Spring " + cellVal.split()[-1] + if "Fall A" in cellVal or "Fall B" in cellVal: + cellVal = "Fall " + cellVal.split()[-1] + + if cellVal.split()[0] not in ["Summer", "Spring", "Fall", "May"]: + previewTerm = f"ERROR: {cellVal} is not valid." + errorFlag = True + displayRow = f"ERROR-{previewTerm}" + else: + previousTerm = list(Term.select().order_by(Term.termOrder))[-1].termOrder > Term.convertDescriptionToTermOrder(cellVal) + hasTerm = Term.get_or_none(Term.description == cellVal) + if hasTerm or previousTerm: + previewTerm = cellVal + displayRow = f"TERM-{previewTerm}" + else: + previewTerm = f"ERROR: The term {cellVal} does not exist and cannot be automatically created." + errorFlag = True + displayRow = f"ERROR-{previewTerm}" + courseParticipantPreview[previewTerm]= {} + + elif regex.search(courseReg, str(cellVal)): + hasCourse = Course.get_or_none(Course.courseAbbreviation == cellVal) + previewCourse = f'{cellVal} will be created' + if hasCourse and hasCourse.courseName: + previewCourse = f"{cellVal} matched to the course {hasCourse.courseName}" + if not courseParticipantPreview.get(previewTerm): + courseParticipantPreview[previewTerm]= {} + courseParticipantPreview[previewTerm][cellVal]=[] + displayRow = f"COURSE-{previewCourse}" + + elif regex.search(bnumberReg, str(cellVal)): + hasUser = User.get_or_none(User.bnumber == cellVal) + if hasUser: + studentValue = f"{hasUser.firstName} {hasUser.lastName}" + displayRow = f"STUDENT-{studentValue}" + else: + studentValue = f'ERROR: {row[1].value} with B# "{row[0].value}" does not exist.' + errorFlag = True + displayRow = f"ERROR-{studentValue}" + if not courseParticipantPreview.get(previewTerm): + courseParticipantPreview[previewTerm]= {} + if not courseParticipantPreview[previewTerm].get(previewCourse): + courseParticipantPreview[previewTerm][previewCourse]=[] + courseParticipantPreview[previewTerm][previewCourse].append([studentValue, cellVal]) + + elif cellVal != '' and cellVal != None: + errorText = f'ERROR: {cellVal} in row {cellRow} of the Excel document is not a valid value.' + errorFlag = True + displayRow = f"ERROR-{errorText}" + + if displayRow: + previewCourseDisplayList.append(displayRow) + + cellRow += 1 + + return errorFlag, courseParticipantPreview, previewCourseDisplayList + +def saveCourseParticipantsToDatabase(courseParticipantPreview): + for term in courseParticipantPreview: + termObj = Term.get_or_none(description = term) or addPastTerm(term) + + for course in courseParticipantPreview[term]: + courseObj = Course.get_or_create(courseAbbreviation = course, + term = termObj, + defaults = {"CourseName" : "", + "sectionDesignation" : "", + "courseCredit" : "1", + "term" : termObj, + "status" : 4, + "createdBy" : g.current_user, + "serviceLearningDesignatedSections" : "", + "previouslyApprovedDescription" : "" }) + + for student, bNumber in courseParticipantPreview[term][course]: + userObj = User.get(User.bnumber == bNumber) + CourseParticipant.get_or_create(user = userObj, + course = courseObj[0], + defaults = {"course" : courseObj[0]}) + + +def courseParticipantPreviewSessionCleaner(): + session.pop('errorFlag') + session.pop('courseParticipantPreview') + session.pop('previewCourseDisplayList') \ No newline at end of file diff --git a/app/logic/term.py b/app/logic/term.py new file mode 100644 index 000000000..a5aa8c2f7 --- /dev/null +++ b/app/logic/term.py @@ -0,0 +1,62 @@ +from flask import session +from playhouse.shortcuts import model_to_dict +from app.logic.createLogs import createAdminLog +from app.models.term import Term + +def addNextTerm(): + newSemesterMap = {"Spring":"Summer", + "Summer":"Fall", + "Fall":"Spring"} + terms = list(Term.select().order_by(Term.termOrder)) + prevTerm = terms[-1] + prevSemester, prevYear = prevTerm.description.split() + + newYear = int(prevYear) + 1 if prevSemester == "Fall" else int(prevYear) + + newDescription = newSemesterMap[prevSemester] + " " + str(newYear) + newAY = prevTerm.academicYear + if prevSemester == "Summer ": # we only change academic year when the latest term in the table is Summer + year1, year2 = prevTerm.academicYear.split("-") + newAY = year2 + "-" + str(int(year2)+1) + + semester = newDescription.split()[0] + summer= "Summer" in semester + newTerm = Term.create(description=newDescription, + year=newYear, + academicYear=newAY, + isSummer= summer, + termOrder=Term.convertDescriptionToTermOrder(newDescription)) + newTerm.save() + + return newTerm + +def addPastTerm(description): + semester, year = description.split() + if 'May' in semester: + semester = "Summer" + if semester == "Fall": + academicYear = year + "-" + str(int(year) + 1) + elif semester == "Summer" or "Spring": + academicYear= str(int(year) - 1) + "-" + year + + isSummer = "Summer" in semester + newDescription=f"{semester} {year}" + orderTerm = Term.convertDescriptionToTermOrder(newDescription) + + createdOldTerm = Term.create(description= newDescription, + year=year, + academicYear=academicYear, + isSummer=isSummer, + termOrder=orderTerm) + createdOldTerm.save() + return createdOldTerm + +def changeCurrentTerm(term): + oldCurrentTerm = Term.get_by_id(g.current_term) + oldCurrentTerm.isCurrentTerm = False + oldCurrentTerm.save() + newCurrentTerm = Term.get_by_id(term) + newCurrentTerm.isCurrentTerm = True + newCurrentTerm.save() + session["current_term"] = model_to_dict(newCurrentTerm) + createAdminLog(f"Changed Current Term from {oldCurrentTerm.description} to {newCurrentTerm.description}") \ No newline at end of file diff --git a/app/logic/userManagement.py b/app/logic/userManagement.py index de3555f38..8b3df68c9 100644 --- a/app/logic/userManagement.py +++ b/app/logic/userManagement.py @@ -34,42 +34,6 @@ def removeCeltsStudentStaff(user): user.save() createAdminLog(f'Removed {user.firstName} {user.lastName} from a CELTS student staff member.') - -def changeCurrentTerm(term): - oldCurrentTerm = Term.get_by_id(g.current_term) - oldCurrentTerm.isCurrentTerm = False - oldCurrentTerm.save() - newCurrentTerm = Term.get_by_id(term) - newCurrentTerm.isCurrentTerm = True - newCurrentTerm.save() - session["current_term"] = model_to_dict(newCurrentTerm) - createAdminLog(f"Changed Current Term from {oldCurrentTerm.description} to {newCurrentTerm.description}") - -def addNextTerm(): - newSemesterMap = {"Spring":"Summer", - "Summer":"Fall", - "Fall":"Spring"} - terms = list(Term.select().order_by(Term.id)) - prevTerm = terms[-1] - prevSemester, prevYear = prevTerm.description.split() - - newYear = int(prevYear) + 1 if prevSemester == "Fall" else int(prevYear) - newDescription = newSemesterMap[prevSemester] + " " + str(newYear) - newAY = prevTerm.academicYear - - if prevSemester == "Summer": # we only change academic year when the latest term in the table is Summer - year1, year2 = prevTerm.academicYear.split("-") - newAY = year2 + "-" + str(int(year2)+1) - - newTerm = Term.create( - description=newDescription, - year=newYear, - academicYear=newAY, - isSummer="Summer" in newDescription.split()) - newTerm.save() - - return newTerm - def changeProgramInfo(newProgramName, newContactEmail, newContactName, newLocation, programId): """Updates the program info with a new sender and email.""" program = Program.get_by_id(programId) @@ -100,4 +64,4 @@ def getAllowedTemplates(currentUser): if currentUser.isCeltsAdmin: return EventTemplate.select().where(EventTemplate.isVisible==True).order_by(EventTemplate.name) else: - return [] + return [] \ No newline at end of file diff --git a/app/logic/utils.py b/app/logic/utils.py index b555ac427..bc7b8b678 100644 --- a/app/logic/utils.py +++ b/app/logic/utils.py @@ -51,4 +51,4 @@ def getFilesFromRequest(request): if fileDoesNotExist: attachmentFiles = None - return attachmentFiles + return attachmentFiles \ No newline at end of file diff --git a/app/models/courseStatus.py b/app/models/courseStatus.py index c9f656e89..d73b85152 100644 --- a/app/models/courseStatus.py +++ b/app/models/courseStatus.py @@ -5,3 +5,4 @@ class CourseStatus(baseModel): IN_PROGRESS = 1 SUBMITTED = 2 APPROVED = 3 + IMPORTED = 4 \ No newline at end of file diff --git a/app/models/term.py b/app/models/term.py index beed1bfba..34f85261b 100644 --- a/app/models/term.py +++ b/app/models/term.py @@ -5,6 +5,7 @@ class Term(baseModel): academicYear = CharField() isSummer = BooleanField(default=False) isCurrentTerm = BooleanField(default=False) + termOrder = CharField() _cache = None @@ -48,3 +49,13 @@ def isFutureTerm(self): elif ("Spring" in currentTerm.description): return True return False + + @staticmethod + def convertDescriptionToTermOrder(description): + semester,year = description.split() + if semester == "Spring": + return year + "-1" + elif semester == "Summer" or semester == "May": + return year + "-2" + elif semester == "Fall": + return year + '-3' \ No newline at end of file diff --git a/app/static/js/manageServiceLearningFaculty.js b/app/static/js/manageServiceLearningFaculty.js index 209bd0192..120c5018b 100644 --- a/app/static/js/manageServiceLearningFaculty.js +++ b/app/static/js/manageServiceLearningFaculty.js @@ -10,20 +10,62 @@ $(document).ready( function () { $('.dataTables_filter').addClass('float-start'); } } + + + }); + + $('#modalPreview button[data-bs-dismiss="modal"]').click(function () { + $('#modalPreview').removeClass('show d-block'); }); - $("#downloadApprovedCoursesBtn").click(function(){ - let termID = $("#downloadApprovedCoursesBtn").val(); - $.ajax({ - url:`/serviceLearning/downloadApprovedCourses/${termID}`, - type:"GET", - success: function(response){ - callback(response); - }, - error: function(response){ - console.log(response) - }, - - - }) + + $('#modalSubmit').on('hidden.bs.modal', function () { + $('#addCourseParticipant').val(''); + + }) + + $("#downloadApprovedCoursesBtn").click(function () { + let termID = $("#downloadApprovedCoursesBtn").val(); + $.ajax({ + url: `/serviceLearning/downloadApprovedCourses/${termID}`, + type: "GET", + success: function (response) { + callback(response); + }, + error: function (response) { + console.log(response) + }, + + }) + }); +}); + +$("#modalCourseParticipant").on("click", function () { + $("#modalSubmit").modal("toggle"); }); + +$('#closeAddCourseParticipants').on('click', function () { + $('#addCourseParticipants')[0].form.reset() + $('#previewButton').prop('disabled', true) +}) + +const fileInput= $("#addCourseParticipants") +fileInput.on('change', handleFileSelect) + +function handleFileSelect(event){ + const selectedFile = event.target.files[0]; + + if (selectedFile){ + $("#previewButton").prop('disabled', false); + } +} + +$("#cancelModalPreview").click(function(){ + $.ajax({ + url: "/deleteUploadedFile", + type: 'POST', + error: function(error, status){ + console.log(error, status) + } + }); +}) \ No newline at end of file diff --git a/app/templates/main/manageServiceLearningFaculty.html b/app/templates/main/manageServiceLearningFaculty.html index f5796ac46..08286e2d4 100644 --- a/app/templates/main/manageServiceLearningFaculty.html +++ b/app/templates/main/manageServiceLearningFaculty.html @@ -6,12 +6,19 @@ + {% if previewParticipantDisplayList %} + + {% endif %} {% endblock %} {% block styles %} - {{super()}} - - +{{super()}} + + {% endblock %} {% block app_content %} @@ -20,137 +27,246 @@

Designated Service-Learning Courses

-
-
-
-
-
-
-

Unapproved

-
-
-
-
- -
-
+ {% for t in terms %} + + {% endfor %} + + +
+
+
+
+
+

Unapproved

+
+
+
+
+ +
- {% if unapprovedCourses | length == 0 %} -
There are no unapproved courses for {{ term.description }}.
- {% else %} - - - - - - - - - - - {% for course in unapprovedCourses %} - - - - - - - {% endfor %} - -
CoursesFacultyStatusAction
{{course.courseName}}{{ course.instructors }} - {{course.status.status}} - -
- {% endif %} +
+ {% if unapprovedCourses | length == 0 %} +
There are no unapproved courses for {{ term.description }}.
+ {% else %} + + + + + + + + + + + {% for course in unapprovedCourses %} + + + + + + + {% endfor %} + +
CoursesFacultyStatusAction
{{course.courseName}}{{ course.instructors }} + {{course.status.status}} + +
+ {% endif %}

Approved

- {% if approvedCourses | length == 0 %} -
There are no approved courses for {{ term.description }}.
- {% else %} - - - - - - - - - - - {% for course in approvedCourses %} - - - - - - - {% endfor %} - -
CoursesFacultyStatusAction
{{course.courseName}}{{ course.instructors }} - {{course.status.status}} - -
- {% endif %} + {% if approvedCourses | length == 0 %} +
There are no approved courses for {{ term.description }}.
+ {% else %} + + + + + + + + + + + {% for course in approvedCourses %} + + + + + + + {% endfor %} + +
CoursesFacultyStatusAction
{{course.courseName}}{{ course.instructors }} + {{course.status.status}} + +
+ {% endif %} {% set downloadAndSend = "disabled" if approvedCourses | length ==0 else "" %} {% if term.isCurrentTerm or term.isFutureTerm %}
- +
- -
- {% endif %} + + +
+
-
-

Service-Learning Faculty

-
- {% set instructorEmails = [] %} - {% for instructor in courseInstructors%} - {{ instructorEmails.append(instructor.email) or "" }} - {% endfor %} -
- Email All + + +