diff --git a/README.md b/README.md index 222426359..b26f85461 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ If you want to test with actual emails, use an email other than outlook to test 1. Set up two factor authentication on your Gmail (Security Settings) 2. Create an App Password through your Gmail. This 16 character password can only be viewed once, so make sure to save it. (NOTE: You won't have the option to create an app password unless step one is completed) 3. Inside of your secret_config.yaml file set the MAIL_USERNAME and MAIL_DEFAULT_SENDER as your Gmail, set the MAIL_PASSWORD as your new app password as, and set ALWAYS_SEND_MAIL as True. If you want emails to go to their real recipients, remove MAIL_OVERRIDE_ALL from your config or set it to "". -4. For testing purposes, change the email of the student and supervisor to match another email that can receive your test emails (or you can use MAIL_OVERRIDE_ALL to send everything to the address specified. +4. For testing purposes, change the email of the student and supervisor to match another email that can receive your test emails or you can use MAIL_OVERRIDE_ALL to send everything to the address specified. ### SSDT Documentation This is SSDT Documentation that contains details, references, workflow, system administration, etc. You are welcome to contribute to it and/or review it: diff --git a/app/__init__.py b/app/__init__.py index af22e6334..aaac89445 100755 --- a/app/__init__.py +++ b/app/__init__.py @@ -84,6 +84,11 @@ def load_currentTerm(): session['current_term'] = model_to_dict(term) g.current_term = term +import datetime +@app.before_request +def load_currentDateTime(): + g.currentDateTime = datetime.datetime.now() + from flask import request @app.context_processor def load_visibleAccordion(): diff --git a/app/controllers/admin/minor.py b/app/controllers/admin/minor.py index 527631057..4a8ebadfd 100644 --- a/app/controllers/admin/minor.py +++ b/app/controllers/admin/minor.py @@ -1,20 +1,36 @@ -from flask import render_template, g, abort +from flask import render_template, g, abort, request, redirect, url_for + +from app.models.user import User from app.controllers.admin import admin_bp -from app.logic.minor import getMinorInterest, getMinorProgress +from app.logic.minor import getMinorInterest, getMinorProgress, toggleMinorInterest -@admin_bp.route('/admin/cceMinor', methods=['GET']) +@admin_bp.route('/admin/cceMinor', methods=['POST','GET']) def manageMinor(): + if not g.current_user.isAdmin: abort(403) + + if request.method == 'POST': + interested_students = request.form.getlist('interestedStudents[]') + + for i in interested_students: + user = User.get(username=i) + if not user.minorInterest: + toggleMinorInterest(i) + interestedStudentsList = getMinorInterest() interestedStudentEmailString = ';'.join([student['email'] for student in interestedStudentsList]) sustainedEngagement = getMinorProgress() + return render_template('/admin/cceMinor.html', interestedStudentsList = interestedStudentsList, interestedStudentEmailString = interestedStudentEmailString, sustainedEngagement = sustainedEngagement, ) + + + diff --git a/app/controllers/admin/routes.py b/app/controllers/admin/routes.py index bf7de956f..d2a31d4ba 100644 --- a/app/controllers/admin/routes.py +++ b/app/controllers/admin/routes.py @@ -34,7 +34,7 @@ from app.logic.participants import getParticipationStatusForTrainings, checkUserRsvp from app.logic.minor import getMinorInterest from app.logic.fileHandler import FileHandler -from app.logic.bonner import getBonnerCohorts, makeBonnerXls, rsvpForBonnerCohort +from app.logic.bonner import getBonnerCohorts, makeBonnerXls, rsvpForBonnerCohort, addBonnerCohortToRsvpLog from app.logic.serviceLearningCourses import parseUploadedFile, saveCourseParticipantsToDatabase, unapprovedCourses, approvedCourses, getImportedCourses, getInstructorCourses, editImportedCourses from app.controllers.admin import admin_bp @@ -110,6 +110,8 @@ def createEvent(templateid, programid): rsvpcohorts = request.form.getlist("cohorts[]") for year in rsvpcohorts: rsvpForBonnerCohort(int(year), savedEvents[0].id) + addBonnerCohortToRsvpLog(int(year), savedEvents[0].id) + noun = (eventData['isRecurring'] == 'on' and "Events" or "Event") # pluralize flash(f"{noun} successfully created!", 'success') @@ -149,7 +151,7 @@ def createEvent(templateid, programid): def rsvpLogDisplay(eventId): event = Event.get_by_id(eventId) if g.current_user.isCeltsAdmin or (g.current_user.isCeltsStudentStaff and g.current_user.isProgramManagerFor(event.program)): - allLogs = EventRsvpLog.select(EventRsvpLog, User).join(User).where(EventRsvpLog.event_id == eventId).order_by(EventRsvpLog.createdOn.desc()) + allLogs = EventRsvpLog.select(EventRsvpLog, User).join(User, on=(EventRsvpLog.createdBy == User.username)).where(EventRsvpLog.event_id == eventId).order_by(EventRsvpLog.createdOn.desc()) return render_template("/events/rsvpLog.html", event = event, allLogs = allLogs) @@ -253,6 +255,7 @@ def eventDisplay(eventId): rsvpcohorts = request.form.getlist("cohorts[]") for year in rsvpcohorts: rsvpForBonnerCohort(int(year), event.id) + addBonnerCohortToRsvpLog(int(year), event.id) flash("Event successfully updated!", "success") return redirect(url_for("admin.eventDisplay", eventId = event.id)) @@ -292,7 +295,7 @@ def eventDisplay(eventId): eventData['timeEnd'] = event.timeEnd.strftime("%-I:%M %p") eventData['startDate'] = event.startDate.strftime("%m/%d/%Y") eventCountdown = getCountdownToEvent(event) - + # Identify the next event in a recurring series if event.recurringId: @@ -317,7 +320,8 @@ def eventDisplay(eventId): filepaths=filepaths, image=image, pageViewsCount=pageViewsCount, - eventCountdown=eventCountdown) + eventCountdown=eventCountdown + ) @@ -588,7 +592,8 @@ def saveRequirements(certid): @admin_bp.route("/displayEventFile", methods=["POST"]) def displayEventFile(): - fileData= request.form - eventfile=FileHandler(eventId=fileData["id"]) - eventfile.changeDisplay(fileData['id']) + fileData = request.form + eventfile = FileHandler(eventId=fileData["id"]) + isChecked = fileData.get('checked') == 'true' + eventfile.changeDisplay(fileData['id'], isChecked) return "" diff --git a/app/controllers/main/routes.py b/app/controllers/main/routes.py index 9dbcf8710..e72f9291c 100644 --- a/app/controllers/main/routes.py +++ b/app/controllers/main/routes.py @@ -494,6 +494,10 @@ def getDietInfo(): @main_bp.route('/profile//indicateInterest', methods=['POST']) def indicateMinorInterest(username): - toggleMinorInterest(username) + if g.current_user.isCeltsAdmin or g.current_user.username == username: + toggleMinorInterest(username) + + else: + abort(403) return "" diff --git a/app/logic/bonner.py b/app/logic/bonner.py index 556d2dbdd..1fe11c5bd 100644 --- a/app/logic/bonner.py +++ b/app/logic/bonner.py @@ -1,6 +1,6 @@ from collections import defaultdict from datetime import date -from peewee import IntegrityError, SQL +from peewee import IntegrityError, SQL, fn import xlsxwriter @@ -8,6 +8,7 @@ from app.models.bonnerCohort import BonnerCohort from app.models.eventRsvp import EventRsvp from app.models.user import User +from app.logic.createLogs import createRsvpLog def makeBonnerXls(): """ @@ -72,8 +73,21 @@ def getBonnerCohorts(limit=None, currentYear=date.today().year): return cohorts + + def rsvpForBonnerCohort(year, event): """ Adds an EventRsvp record to the given event for each user in the given Bonner year. """ - EventRsvp.insert_from(BonnerCohort.select(BonnerCohort.user, event, SQL('NOW()')).where(BonnerCohort.year == year),[EventRsvp.user, EventRsvp.event, EventRsvp.rsvpTime]).on_conflict(action='IGNORE').execute() + EventRsvp.insert_from(BonnerCohort.select(BonnerCohort.user, event, SQL('NOW()')) + .where(BonnerCohort.year == year), + [EventRsvp.user, EventRsvp.event, EventRsvp.rsvpTime]).on_conflict(action='IGNORE').execute() + +def addBonnerCohortToRsvpLog(year, event): + """ This method adds the table information in the RSVP Log page""" + bonnerCohort = list(BonnerCohort.select(fn.CONCAT(User.firstName, ' ', User.lastName).alias("fullName")) + .join(User, on=(User.username == BonnerCohort.user)) + .where(BonnerCohort.year == year)) + for bonner in bonnerCohort: + fullName = bonner.fullName + createRsvpLog(eventId=event, content=f"Added {fullName} to RSVP list.") \ No newline at end of file diff --git a/app/logic/fileHandler.py b/app/logic/fileHandler.py index f4dbed562..18746e78e 100644 --- a/app/logic/fileHandler.py +++ b/app/logic/fileHandler.py @@ -4,101 +4,78 @@ from app.models.attachmentUpload import AttachmentUpload class FileHandler: - def __init__(self,files=None, courseId=None, eventId=None): - self.files=files + def __init__(self, files=None, courseId=None, eventId=None): + self.files = files self.path = app.config['files']['base_path'] self.courseId = courseId self.eventId = eventId if courseId: self.path = os.path.join(self.path, app.config['files']['course_attachment_path'], str(courseId)) elif eventId: - # eventID is not included in the path, because it is now a part of the attachment filename. self.path = os.path.join(self.path, app.config['files']['event_attachment_path']) - + def makeDirectory(self): - # This creates a directory. - # Created to remove duplicates when an event is recurring. try: extraDir = str(self.eventId) if self.eventId else "" os.makedirs(os.path.join(self.path, extraDir)) - # Error 17 Occurs when we try to create a directory that already exists except OSError as e: if e.errno != 17: print(f'Fail to create directory: {e}') raise e - - def getFileFullPath(self, newfilename = ''): - """ - This creates the directory/path for the object from the "Choose File" input in the create event and edit event. - :returns: directory path for attachment - """ - - # Added the eventID of the first recurring event to track the file path for subsequent recurring events. + + def getFileFullPath(self, newfilename=''): try: - # tries to create the full path of the files location and passes if - # the directories already exist or there is no attachment - filePath=(os.path.join(self.path, newfilename)) - except AttributeError: # will pass if there is no attachment to save + filePath = (os.path.join(self.path, newfilename)) + except AttributeError: pass except FileExistsError: pass - return filePath def saveFiles(self, saveOriginalFile=None): - """ Saves the attachment in the app/static/files/eventattachments/ or courseattachements/ directory """ try: for file in self.files: saveFileToFilesystem = None if self.eventId: - attachmentName = str(saveOriginalFile.id) + "/" + file.filename - - # isFileInEvent checks if the attachment exists in the database under that eventId and filename. + attachmentName = str(saveOriginalFile.id) + "/" + file.filename isFileInEvent = AttachmentUpload.select().where(AttachmentUpload.event_id == self.eventId, AttachmentUpload.fileName == attachmentName).exists() if not isFileInEvent: - AttachmentUpload.create(event = self.eventId, fileName = attachmentName) - - # Only save the file if our event is on its own, or the first of a recurring series + AttachmentUpload.create(event=self.eventId, fileName=attachmentName) if saveOriginalFile and saveOriginalFile.id == self.eventId: saveFileToFilesystem = attachmentName - elif self.courseId: isFileInCourse = AttachmentUpload.select().where(AttachmentUpload.course == self.courseId, AttachmentUpload.fileName == file.filename).exists() if not isFileInCourse: - AttachmentUpload.create(course = self.courseId, fileName = file.filename) + AttachmentUpload.create(course=self.courseId, fileName=file.filename) saveFileToFilesystem = file.filename - else: + else: saveFileToFilesystem = file.filename - if saveFileToFilesystem: self.makeDirectory() - file.save(self.getFileFullPath(newfilename = saveFileToFilesystem)) - - except AttributeError: # will pass if there is no attachment to save + file.save(self.getFileFullPath(newfilename=saveFileToFilesystem)) + except AttributeError: pass - def retrievePath(self,files): - pathDict={} + def retrievePath(self, files): + pathDict = {} for file in files: - pathDict[file.fileName] = ((self.path+"/"+ file.fileName)[3:], file) - + pathDict[file.fileName] = ((self.path + "/" + file.fileName)[3:], file) return pathDict def deleteFile(self, fileId): - """ - Deletes attachmant from the app/static/files/eventattachments/ or courseattachments/ directory - """ file = AttachmentUpload.get_by_id(fileId) file.delete_instance() - - # checks if there are other instances with the same filename in the AttachmentUpload table if not AttachmentUpload.select().where(AttachmentUpload.fileName == file.fileName).exists(): path = os.path.join(self.path, file.fileName) os.remove(path) - - def changeDisplay(self, fileId): + + def changeDisplay(self, fileId, isDisplayed): file = AttachmentUpload.get_by_id(fileId) - AttachmentUpload.update(isDisplayed=False).where(AttachmentUpload.event == file.event, AttachmentUpload.isDisplayed==True).execute() - AttachmentUpload.update(isDisplayed=True).where(AttachmentUpload.id == fileId).execute() - return "" + + # Uncheck all other checkboxes for the same event + AttachmentUpload.update(isDisplayed=False).where(AttachmentUpload.event == file.event).execute() + + # Check the selected checkbox + AttachmentUpload.update(isDisplayed=isDisplayed).where(AttachmentUpload.id == fileId).execute() + return "" diff --git a/app/logic/serviceLearningCourses.py b/app/logic/serviceLearningCourses.py index e4b0f3d96..d99cfaad4 100644 --- a/app/logic/serviceLearningCourses.py +++ b/app/logic/serviceLearningCourses.py @@ -43,12 +43,28 @@ def getSLProposalInfoForUser(user: User) -> Dict[int, Dict[str, Any]]: courseDict[course.id] = {"id":course.id, "creator":f"{course.createdBy.firstName} {course.createdBy.lastName}", - "name":course.courseName if course.courseName else course.courseAbbreviation, + "name": course.courseName, + "abbr": course.courseAbbreviation, + "courseDisplayName": createCourseDisplayName(course.courseName, course.courseAbbreviation), "faculty": faculty, "term": course.term, "status": course.status.status} return courseDict +def createCourseDisplayName(name, abbreviation): + ''' + This function combines course name and numbers with conditions + inputs: course name, course abbreviation + ''' + if name and abbreviation: + return f"{abbreviation} - {name}" + elif not name and not abbreviation: + return '' + elif not name: + return abbreviation + elif not abbreviation: + return name + def saveCourseParticipantsToDatabase(cpPreview: Dict[str, Dict[str, Dict[str, List[Dict[str, Any]]]]]) -> None: for term, terminfo in cpPreview.items(): termObj: Term = Term.get_or_none(description = term) or addPastTerm(term) diff --git a/app/static/css/base.css b/app/static/css/base.css index 74fcddcc6..cd4d3e3ca 100644 --- a/app/static/css/base.css +++ b/app/static/css/base.css @@ -102,4 +102,12 @@ select.empty { .rsvp-btn{ padding-top: 4em; +} +.required::after { + content: " *"; + color: red; + font-size: 1.3em; /* Adjust the font size as needed */ + font-weight: bolder; /* make it bold */ + vertical-align: middle; /* Align vertically with the text */ + line-height: 1; /* Match the line height of the parent text */ } \ No newline at end of file diff --git a/app/static/css/eventList.css b/app/static/css/eventList.css index 801935a19..8f78db8b9 100644 --- a/app/static/css/eventList.css +++ b/app/static/css/eventList.css @@ -1,3 +1,7 @@ .accordion-button::after { margin-left: 1.5rem; -} \ No newline at end of file +} + +.icon { + margin-right: 20px; /* Adjust the value as needed */ + } \ No newline at end of file diff --git a/app/static/css/slcNewProposal.css b/app/static/css/slcNewProposal.css index f37a27f44..644af5145 100644 --- a/app/static/css/slcNewProposal.css +++ b/app/static/css/slcNewProposal.css @@ -28,24 +28,5 @@ background-color: #0d6efd; } -@media (max-width: 1500px){ - .btn{ - height:60px; - } - .step{ - width: 13px; - height:13px; - } -} - -@media (max-width: 1100px){ - .btn{ - height:80px; - } - .step{ - width: 10px; - height:10px; - } -} diff --git a/app/static/js/createEvents.js b/app/static/js/createEvents.js index d72701977..cb93a98b3 100644 --- a/app/static/js/createEvents.js +++ b/app/static/js/createEvents.js @@ -1,41 +1,40 @@ -import searchUser from './searchUser.js' - +import searchUser from './searchUser.js'; // updates max and min dates of the datepickers as the other datepicker changes function updateDate(obj) { - // we need to replace "-" with "/" because firefox cannot turn a date with "-" to a datetime object - var selectedDate = ($(obj).val()).replaceAll("-", "/") - var dateToChange = new Date(selectedDate); - var newMonth = dateToChange.getMonth(); - var newYear = dateToChange.getFullYear(); - var newDay = dateToChange.getDate(); - if(obj.className.includes("startDatePicker")) { - $("#endDatePicker-"+$(obj).data("page-location")).datepicker({minDate: new Date(newYear, newMonth, newDay)}); - $("#endDatePicker-"+$(obj).data("page-location")).datepicker("option", "minDate", new Date(newYear, newMonth, newDay)); + var selectedDate = $(obj).datepicker("getDate"); // No need for / for Firefox compatiblity + var newMonth = selectedDate.getMonth(); + var newYear = selectedDate.getFullYear(); + var newDay = selectedDate.getDate(); + + if (obj.className.includes("startDatePicker")) { + $("#endDatePicker-" + $(obj).data("page-location")).datepicker("option", "minDate", new Date(newYear, newMonth, newDay)); + } else if (obj.className.includes("endDatePicker")) { + $("#startDatePicker-" + $(obj).data("page-location")).datepicker("option", "maxDate", new Date(newYear, newMonth, newDay)); } + if (obj.className.includes("endDatePicker")) { $("#startDatePicker-"+$(obj).data("page-location")).datepicker({maxDate: new Date(newYear, newMonth, newDay)}); $("#startDatePicker-"+$(obj).data("page-location")).datepicker("option", "maxDate", new Date(newYear, newMonth, newDay)); } } + // turns a string with a time with HH:mm format to %I:%M %p format // used to display 12 hour format but still use 24 hour format in the backend -function format24to12HourTime(timeStr){ +function format24to12HourTime(timeStr) { var formattedTime; - if (parseInt(timeStr.slice(0, 2)) > 12){ - formattedTime = "0" + String(parseInt(timeStr.slice(0, 2)) - 12) + timeStr.slice(2) + " PM"; - } - else if (parseInt(timeStr.slice(0, 2)) < 12 ){ - formattedTime = timeStr + " AM"; - } - else { - formattedTime = timeStr + " PM"; - } - return formattedTime; + if (parseInt(timeStr.slice(0, 2)) > 12) { + formattedTime = "0" + String(parseInt(timeStr.slice(0, 2)) - 12) + timeStr.slice(2) + " PM"; + } else if (parseInt(timeStr.slice(0, 2)) < 12) { + formattedTime = timeStr + " AM"; + } else { + formattedTime = timeStr + " PM"; } + return formattedTime; +} - function calculateRecurringEventFrequency(){ +function calculateRecurringEventFrequency(){ var eventDatesAndName = {name:$("#inputEventName").val(), isRecurring: true, startDate:$(".startDatePicker")[0].value, @@ -48,7 +47,6 @@ function format24to12HourTime(timeStr){ var recurringEvents = JSON.parse(jsonData) var recurringTable = $("#recurringEventsTable") $("#recurringEventsTable tbody tr").remove(); - for (var event of recurringEvents){ var eventdate = new Date(event.date).toLocaleDateString() recurringTable.append(""+event.name+""+eventdate+""); @@ -58,166 +56,175 @@ function format24to12HourTime(timeStr){ console.log(error) } }); - } + +$(document).ready(function () { + // Initialize datepicker with proper options + $.datepicker.setDefaults({ + dateFormat: 'yy/mm/dd', // Ensures compatibility across browsers + minDate: new Date() + }); + + $(".datePicker").datepicker({ + dateFormat: 'mm/dd/yy', + minDate: new Date() + }); + + $(".datePicker").each(function() { + var dateStr = $(this).val(); + if (dateStr) { + var dateObj = new Date(dateStr); + if (!isNaN(dateObj.getTime())) { + $(this).datepicker("setDate", dateObj); + } + } + }); -/* - * Run when the webpage is ready for javascript - */ -$(document).ready(function() { - if ( $(".startDatePicker")[0].value != $(".endDatePicker")[0].value){ + // Update datepicker min and max dates on change + $(".startDatePicker, .endDatePicker").change(function () { + updateDate(this); + }); + + if ($(".startDatePicker")[0].value != $(".endDatePicker")[0].value) { calculateRecurringEventFrequency(); } - handleFileSelection("attachmentObject") + handleFileSelection("attachmentObject"); + + $("#checkRSVP").on("click", function () { + if ($("#checkRSVP").is(":checked")) { + $("#limitGroup").show(); + } else { + $("#limitGroup").hide(); + } + }); - $("#checkRSVP").on("click", function() { - if ($("#checkRSVP").is(":checked")) { - $("#limitGroup").show(); - } - else{ - $("#limitGroup").hide(); - } - }) // Disable button when we are ready to submit - $("#saveEvent").on('submit',function(event) { + $("#saveEvent").on('submit', function (event) { $(this).find("input[type=submit]").prop("disabled", true); }); - $("#checkIsRecurring").click(function() { - var recurringStatus = $("input[name='isRecurring']:checked").val() + $("#checkIsRecurring").click(function () { + var recurringStatus = $("input[name='isRecurring']:checked").val(); if (recurringStatus == 'on') { - $(".endDateStyle, #recurringTableDiv").removeClass('d-none') + $(".endDateStyle, #recurringTableDiv").removeClass('d-none'); $(".endDatePicker").prop('required', true); } else { - $(".endDateStyle, #recurringTableDiv").addClass('d-none') + $(".endDateStyle, #recurringTableDiv").addClass('d-none'); $(".endDatePicker").prop('required', false); } }); - - $("#allowPastStart").click(function() { - var allowPast = $("#allowPastStart:checked").val() + $("#allowPastStart").click(function () { + var allowPast = $("#allowPastStart:checked").val(); if (allowPast == 'on') { $.datepicker.setDefaults({ - minDate: new Date('1999/10/25'), - dateFormat:'mm-dd-yy' + minDate: new Date('1999/10/25'), + dateFormat: 'yy-mm-dd' // Ensures compatibility across browsers }); } else { $.datepicker.setDefaults({ - minDate: new Date($.now()), - dateFormat:'mm-dd-yy' + minDate: new Date(), + dateFormat: 'yy/mm/dd' // Ensures compatibility across browsers }); } }); + // everything except Chrome if (navigator.userAgent.indexOf("Chrome") == -1) { $('input.timepicker').timepicker({ - timeFormat : 'hh:mm p', - scrollbar: true, - dropdown: true, - dynamic: true, - minTime: "08:00am", - maxTime: "10:00pm" + timeFormat: 'hh:mm p', + scrollbar: true, + dropdown: true, + dynamic: true, + minTime: "08:00am", + maxTime: "10:00pm" }); $(".timepicker").prop("type", "text"); $(".timeIcons").prop("hidden", false); var formattedStartTime = format24to12HourTime($(".startTime").prop("defaultValue")); var formattedEndTime = format24to12HourTime($(".endTime").prop("defaultValue")); - $(".startTime"[0]).val(formattedStartTime); - $(".endTime"[0]).val(formattedEndTime); - } - else { + $(".startTime").val(formattedStartTime); + $(".endTime").val(formattedEndTime); + } else { $(".timepicker").prop("type", "time"); $(".timeIcons").prop("hidden", true); } if ($(".datePicker").is("readonly")) { - $(".datePicker" ).datepicker( "option", "disabled", true ) - }; - - //makes the input fields act like readonly (readonly doesn't work with required) - $(".readonly").on('keydown paste', function(e){ - if(e.keyCode != 9) // ignore tab - e.preventDefault(); - }); + $(".datePicker").datepicker("option", "disabled", true); + } - $.datepicker.setDefaults({ - minDate: new Date($.now()), - dateFormat:'mm-dd-yy' + $(".readonly").on('keydown paste', function (e) { + if (e.keyCode != 9) // ignore tab + e.preventDefault(); }); - $(".startDate").click(function() { - $("#startDatePicker-" + $(this).data("page-location")).datepicker().datepicker("show"); + $(".startDate").click(function () { + $("#startDatePicker-" + $(this).data("page-location")).datepicker("show"); }); - $(".endDate").click(function() { - $("#endDatePicker-" + $(this).data("page-location")).datepicker().datepicker("show"); + $(".endDate").click(function () { + $("#endDatePicker-" + $(this).data("page-location")).datepicker("show"); }); - - $(".startDatePicker, .endDatePicker").change(function(){ - if ( $(this).val() && $("#endDatePicker-" + $(this).data("page-location")).val()){ + $(".startDatePicker, .endDatePicker").change(function () { + if ($(this).val() && $("#endDatePicker-" + $(this).data("page-location")).val()) { calculateRecurringEventFrequency(); } }); - $("#checkRSVP").click(function(){ - if ($("input[name='isRsvpRequired']:checked").val() == 'on'){ + $("#checkRSVP").click(function () { + if ($("input[name='isRsvpRequired']:checked").val() == 'on') { $("#checkFood").prop('checked', true); - - } else{ + } else { $("#checkFood").prop('disabled', false); } }); - - var facilitatorArray = [] + var facilitatorArray = []; function callback(selectedFacilitator) { - // JSON.parse is required to de-stringify the search results into a dictionary. - let facilitator = (selectedFacilitator["firstName"]+" "+selectedFacilitator["lastName"]+" ("+selectedFacilitator["username"]+")"); + let facilitator = (selectedFacilitator["firstName"] + " " + selectedFacilitator["lastName"] + " (" + selectedFacilitator["username"] + ")"); let username = selectedFacilitator["username"]; - if (!facilitatorArray.includes(username)){ - facilitatorArray.push(username); - let tableBody = $("#facilitatorTable").find("tbody"); - let lastRow = tableBody.find("tr:last"); - let newRow = lastRow.clone(); - newRow.find("td:eq(0) p").text(facilitator); - newRow.find("td:eq(0) div button").data("id", username); - newRow.find("td:eq(0) div input").attr("id", username); - newRow.attr("id", username); - newRow.prop("hidden", false); - lastRow.after(newRow); - $("#hiddenFacilitatorArray").attr("value", facilitatorArray); + if (!facilitatorArray.includes(username)) { + facilitatorArray.push(username); + let tableBody = $("#facilitatorTable").find("tbody"); + let lastRow = tableBody.find("tr:last"); + let newRow = lastRow.clone(); + newRow.find("td:eq(0) p").text(facilitator); + newRow.find("td:eq(0) div button").data("id", username); + newRow.find("td:eq(0) div input").attr("id", username); + newRow.attr("id", username); + newRow.prop("hidden", false); + lastRow.after(newRow); + $("#hiddenFacilitatorArray").attr("value", facilitatorArray); } } - $("#eventFacilitator").on('input', function() { - // To retrieve specific columns into a dict, create a [] list and put columns inside + $("#eventFacilitator").on('input', function () { searchUser("eventFacilitator", callback, true, undefined, "instructor"); }); - $("#facilitatorTable").on("click", "#remove", function() { - let username = $(this).closest("tr")[0].id - const index = facilitatorArray.indexOf(username) - facilitatorArray.splice(index, 1); - $("#hiddenFacilitatorArray").attr("value", facilitatorArray); - $(this).closest("tr").remove(); + $("#facilitatorTable").on("click", "#remove", function () { + let username = $(this).closest("tr")[0].id; + const index = facilitatorArray.indexOf(username); + facilitatorArray.splice(index, 1); + $("#hiddenFacilitatorArray").attr("value", facilitatorArray); + $(this).closest("tr").remove(); }); - $(".endDatePicker").change(function(){ - updateDate(this) + $(".endDatePicker").change(function () { + updateDate(this); }); - $(".startDatePicker").change(function(){ - updateDate(this) + $(".startDatePicker").change(function () { + updateDate(this); }); - $("#inputCharacters").keyup(function(event){ - setCharacterLimit(this, "#remainingCharacters") - }); + $("#inputCharacters").keyup(function (event) { + setCharacterLimit(this, "#remainingCharacters"); + }); setCharacterLimit($("#inputCharacters"), "#remainingCharacters"); - }); diff --git a/app/static/js/displayFilesMacro.js b/app/static/js/displayFilesMacro.js index 336fcf11b..49c04951c 100644 --- a/app/static/js/displayFilesMacro.js +++ b/app/static/js/displayFilesMacro.js @@ -1,35 +1,45 @@ $(document).ready(function(){ - $("a.fileName").tooltip() + $("a.fileName").tooltip(); + $(".removeAttachment").on("click", function(){ - let fileId= $(this).data("id") - let deleteLink = $(this).data("delete-url") - let fileData = {fileId : fileId, - databaseId:$(this).data('database-id')} + let fileId = $(this).data("id"); + let deleteLink = $(this).data("delete-url"); + let fileData = { + fileId: fileId, + databaseId: $(this).data('database-id') + }; + $.ajax({ - type:"POST", + type: "POST", url: deleteLink, - data: fileData, //get the startDate, endDate and name as a dictionary + data: fileData, success: function(){ - msgFlash("Attachment removed successfully", "success") - $("#attachment_"+fileId).remove() - + msgFlash("Attachment removed successfully", "success"); + $("#attachment_" + fileId).remove(); }, - error: function(error){ - msgFlash(error) + error: function(error){ + msgFlash(error); } - }); }); + }); $('.attachmentCheck').change(function() { + // Store the current checkbox state + var isChecked = $(this).is(':checked'); + // Uncheck all checkboxes $('.attachmentCheck').prop('checked', false); - // Check the selected checkbox - $(this).prop('checked', true); + + // Toggle the current checkbox + if (!isChecked) { + $(this).prop('checked', true); + } else { + $(this).prop('checked', false); + } var attachmentId = $(this).data('id'); var isChecked = $(this).is(':checked'); - $.ajax({ url: '/displayEventFile', method: 'POST', @@ -38,16 +48,31 @@ $(document).ready(function(){ checked: isChecked }, success: function(response) { - msgToast("Event Cover ", "Successfully updated the event cover.") + msgToast("Event Cover", "Successfully updated the event cover."); }, error: function(xhr, status, error) { - msgFlash(error) + msgFlash(error); + } + }); + }); + + // Additional functionality to handle click events + document.querySelectorAll('.attachmentCheck').forEach(item => { + item.addEventListener('click', event => { + if (item.checked) { + item.checked = false; // This toggles the checkbox off if it was already checked + } else { + uncheckAll(); + item.checked = true; } }); }); - - -}) + function uncheckAll() { + document.querySelectorAll('.attachmentCheck').forEach(checkbox => { + checkbox.checked = false; + }); + } +}); diff --git a/app/static/js/eventKiosk.js b/app/static/js/eventKiosk.js index 32b1c30b8..b4195b975 100644 --- a/app/static/js/eventKiosk.js +++ b/app/static/js/eventKiosk.js @@ -1,10 +1,86 @@ -$(document).keydown(function(e){ - if (e.key === "Escape") { - $("#fullscreenCheck").prop("checked", false) +var elem = document.getElementById("show"); + +$(document).on("fullscreenchange", function() { + if (!document.fullscreenElement && !document.webkitFullscreenElement && !document.mozFullScreenElement && !document.msFullscreenElement) { + closeFullscreen(true); + } +}); + +$(document).keydown(function(e) { + if (e.key === "F11") { + e.preventDefault(); toggleFullscreen(); } }); +function toggleFullscreen() { + if (document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement) { + closeFullscreen(true); + } else { + openFullscreen(); + } +} + +function openFullscreen() { + $("#show").css({ + 'background-color': 'white', + 'position': 'absolute', + 'top': '50%', + 'left': '50%', + 'transform': 'translate(-50%, -50%)', + 'height': '100%', + 'width': '100%', + 'box-sizing': 'border-box' + }); + + if (elem.requestFullscreen) { + elem.requestFullscreen(); + } else if (elem.webkitRequestFullscreen) { /* Safari */ + elem.webkitRequestFullscreen(); + } else if (elem.msRequestFullscreen) { /* IE11 */ + elem.msRequestFullscreen(); + } + ensureFocus(); + + + $("#fullscreenCheck").attr("onclick", "toggleFullscreen()").text("Close Full Screen"); +} + +function ensureFocus() { + if (!$("#submitScannerData").is(":focus")) { + $("#submitScannerData").focus(); + } +} + +function closeFullscreen(toggleButton) { + $("#show").css({ + 'background-color': 'white', + 'position': 'static', + 'top': 'auto', + 'left': 'auto', + 'transform': 'none', + 'height': 'auto', + 'width': 'auto', + 'box-sizing': 'content-box' + }); + + if (document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { /* Safari */ + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { /* IE11 */ + document.msExitFullscreen(); + } + } + + ensureFocus(); + + if (toggleButton) { + $("#fullscreenCheck").attr("onclick", "toggleFullscreen()").text("Open Full Screen"); + } +} + $(document).ready(function(e) { $("#submitScannerData").focus(); @@ -13,125 +89,82 @@ $(document).ready(function(e) { submitData(); } }); + + // Click event for the Enter button + $("#enter-button").click(function() { + submitData(); + }); - // Opens the camera to scan the ID + + // Opens the camera to scan the ID $('.qr-reader-button').on("click", function() { - $('#qr-reader').toggle() - let lastResult, countResults = 0; - let onScanSuccess = function(decodedText, decodedResult) { - if (decodedText && decodedText.length > 9 && decodedText !== lastResult) { - lastResult = decodedText; - - $("#submitScannerData").val(decodedText) - submitData(); - } else { - message = decodedText + " Invalid B-number" - flasherStatus = "danger" - } - } - let qrboxFunction = function(viewfinderWidth, viewfinderHeight) { - let minEdgePercentage = 0.9; // 90% - let minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight); - let qrboxSize = Math.floor(minEdgeSize * minEdgePercentage); - return { - width: qrboxSize, - height: qrboxSize + $('#qr-reader').toggle(); + let lastResult, countResults = 0; + let onScanSuccess = function(decodedText, decodedResult) { + if (decodedText && decodedText.length > 9 && decodedText !== lastResult) { + lastResult = decodedText; + + $("#submitScannerData").val(decodedText); + submitData(); + } else { + message = decodedText + " Invalid B-number"; + flasherStatus = "danger"; + } }; - } - let scanner = new Html5QrcodeScanner("qr-reader", { - fps: 2, - qrbox: qrboxFunction, - preferFrontCamera: false, - facingMode: { exact: "environment" }, - useBarCodeDetectorIfSupported: true, - }, true); - scanner.render(onScanSuccess); - - - // we have to delay this so that the element exists before we try to add the event - window.setTimeout(function() { - $('#html5-qrcode-button-camera-stop').on("click", function() { - $('#qr-reader').toggle() - })}, 500); - }) + let qrboxFunction = function(viewfinderWidth, viewfinderHeight) { + let minEdgePercentage = 0.9; // 90% + let minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight); + let qrboxSize = Math.floor(minEdgeSize * minEdgePercentage); + return { + width: qrboxSize, + height: qrboxSize + }; + }; + let scanner = new Html5QrcodeScanner("qr-reader", { + fps: 2, + qrbox: qrboxFunction, + preferFrontCamera: false, + facingMode: { exact: "environment" }, + useBarCodeDetectorIfSupported: true, + }, true); + scanner.render(onScanSuccess); + + // Delay to ensure the element exists before adding the event + window.setTimeout(function() { + $('#html5-qrcode-button-camera-stop').on("click", function() { + $('#qr-reader').toggle(); + }); + }, 500); + }); }); -function submitData(){ - $(".alert").remove() +function submitData() { + $(".alert").remove(); $.ajax({ - method: "POST", - url: '/signintoEvent', - data: { - "eventid": $("#eventid").val(), - "bNumber": $("#submitScannerData").val() - }, - - success: function(resultID) { - if (resultID.status == "already signed in") { - msgFlash(`${resultID.user} already signed in!`, "warning"); - } else if (resultID.status === "banned") { - msgFlash(`${resultID.user} is ineligible!`, "danger"); - } else if (resultID.status === "does not exist") { - msgFlash("User does not exist", "danger"); - } else { - msgFlash(`${resultID.user} successfully signed in!`, "success"); + method: "POST", + url: '/signintoEvent', + data: { + "eventid": $("#eventid").val(), + "bNumber": $("#submitScannerData").val() + }, + success: function(resultID) { + if (resultID.status == "already signed in") { + msgFlash(`${resultID.user} already signed in!`, "warning"); + } else if (resultID.status === "banned") { + msgFlash(`${resultID.user} is ineligible!`, "danger"); + } else if (resultID.status === "does not exist") { + msgFlash("User does not exist", "danger"); + } else { + msgFlash(`${resultID.user} successfully signed in!`, "success"); + } + $("#submitScannerData").val("").blur(); + }, + error: function(request, status, error) { + console.log(status, error); + msgFlash("See Attendant; Unable to sign in.", "danger"); + $("#submitScannerData").val("").blur(); } - $("#submitScannerData").val("").blur(); - }, - - error: function(request, status, error) { - console.log(status, error); - msgFlash("See Attendant; Unable to sign in.", "danger"); - $("#submitScannerData").val("").blur(); - } - }) -} - -function hideElements(hide) { - if (hide == true) { - - $("footer").hide(); - $("kiosk-hide").animate({ opacity: 0 }, 1); - $("kiosk-hide").css("width", "0"); - $("kiosk-hide").prop("disabled", true); - $("a").hide(); - $("nav").css("width", "0"); - } else { - $("footer").show(); - $("kiosk-hide").css("width", "inherit"); - $("kiosk-hide").animate({ opacity: 1 }, 1); - $("kiosk-hide").prop("disabled", false); - $("a").show(); - $("nav").css("width", "inherit"); - } + }); } -// Source: https://stackoverflow.com/questions/1125084/how-to-make-the-window-full-screen-with-javascript-stretching-all-over-the-screen -function toggleFullscreen() { - if($("#fullscreenCheck").prop("checked") == false){ - hideElements(false); - document.exitFullscreen() || document.webkitExitFullscreen() || document.msExitFullscreen() - } else { - hideElements(true); - - var el = document.documentElement - , rfs = // for newer Webkit and Firefox - el.requestFullscreen - || el.webkitRequestFullScreen - || el.mozRequestFullScreen - || el.msRequestFullscreen - ; - if(typeof rfs!="undefined" && rfs){ - rfs.call(el); - } else if(typeof window.ActiveXObject!="undefined"){ - // for Internet Explorer - var wscript = new ActiveXObject("WScript.Shell"); - if (wscript!=null) { - wscript.SendKeys("{F11}"); - } - } - } - $('#submitScannerData').focus(); -}; - diff --git a/app/static/js/manageVolunteers.js b/app/static/js/manageVolunteers.js index 5446f085e..693aa7968 100644 --- a/app/static/js/manageVolunteers.js +++ b/app/static/js/manageVolunteers.js @@ -91,7 +91,8 @@ $(document).ready(function() { url: `/addVolunteersToEvent/${eventId}`, type: "POST", data: {"selectedVolunteers": selectedVolunteers, "ajax": true}, - success: function(s){ + success: + function(s){ location.reload() }, error: function(request, status, error){ diff --git a/app/static/js/minorAdminPage.js b/app/static/js/minorAdminPage.js index fd0e03cb2..f145bd1af 100644 --- a/app/static/js/minorAdminPage.js +++ b/app/static/js/minorAdminPage.js @@ -1,3 +1,4 @@ +import searchUser from './searchUser.js' function emailAllInterested(){ // Read all student emails from the input as a string and put them in mailto format let interestedStudentEmails = $("#interestedStudentEmails").val(); @@ -14,6 +15,87 @@ function emailAllInterested(){ msgFlash("No interested students to email.", "info") } } + + +$(document).ready(function() { + $(document).on('click', '.remove_interested_student', function() { + let username = $(this).attr('id'); + + + $.ajax({ + type: 'POST', + url: '/profile/' + username + '/indicateInterest', + success: function(response) { + msgToast("Student successfully removed") + location.reload(); + }, + error: function(error) { + console.log("error") + } + }); + }); +}); + + + + +function getInterestedStudents() { + // get all the checkboxes and return a list of users who's + // checkboxes are selected + let checkboxesDisplayedInModal = $("#addInterestedStudentsModal input[type=checkbox]:checked") + let interestedStudentsList = [] + checkboxesDisplayedInModal.each(function(index, checkbox){ + interestedStudentsList.push(checkbox["value"]) + }) + return interestedStudentsList +} + +function updateInterestedStudents(){ + let interestedStudentList = getInterestedStudents() + let buttonContent = $("#addInterestedStudentsbtn").html() + if (interestedStudentList.length > 1) { + if (buttonContent.charAt(buttonContent.length-1) != "s") { + // make the button text plural if there are multiple users selected + $("#addInterestedStudentsbtn").html(buttonContent + "s") + } + } else if (buttonContent.charAt(buttonContent.length-1) == "s") { + // remove the s if it is plural and we have less than 2 volunteers + $("#addInterestedStudentsbtn").html(buttonContent.slice(0, -1)) + } + // disable the submit button if there are no selectedCheckboxes + if (interestedStudentList.length == 0) { + + $("#addInterestedStudentsbtn").prop("disabled", true) + } else { + $("#addInterestedStudentsbtn").prop("disabled", false) + } +} + +var userlist = [] +function callback(selected) { + let user = $("#addStudentInput").val() + if (userlist.includes(selected["username"]) == false){ + userlist.push(user) + let i = userlist.length; + $("#interestedStudentList").prepend("
  • ") + $("#interestedStudentElement"+i).append(" ", + "") + $("#userlistCheckbox"+i).click(updateInterestedStudents) + updateInterestedStudents() + } + else { + msgFlash("User already selected.") + } +} +$("#addInterestedStudentsbtn").prop('disabled', true); ++ +$("#addInterestedStudentsModal").on("shown.bs.modal", function() { + $('#addStudentInput').focus(); +}); + +$("#addStudentInput").on("input", function() { +searchUser("addStudentInput", callback, true, "addInterestedStudentsModal"); +}); $(document).ready(function() { $('#engagedStudentsTable').DataTable(); diff --git a/app/templates/admin/cceMinor.html b/app/templates/admin/cceMinor.html index 1f46ed4b7..aa0272c8d 100644 --- a/app/templates/admin/cceMinor.html +++ b/app/templates/admin/cceMinor.html @@ -12,7 +12,6 @@ {% endblock %} {% endblock %} {% block app_content %} -

    CCE Minor Management

    CCE Minor Progress

    @@ -28,7 +27,7 @@

    CCE Minor Progress

    {% for student in sustainedEngagement %} @@ -50,6 +49,7 @@

    CCE Minor Progress

    Interested Students

    + Add Students
    @@ -57,17 +57,59 @@

    Interested Students

    + {% for student in interestedStudentsList %} - + {% endfor %}
    - {{ student.firstName }} {{ student.lastName }} + {{ student.firstName }} {{ student.lastName }} {{ student.engagementCount }}/4 {{ student.hasSummer }}
    Name Remove Student
    + {{student.firstName}} {{student.lastName}} + +
    + + + + {% endblock %} diff --git a/app/templates/admin/createEvent.html b/app/templates/admin/createEvent.html index 22e88b3c6..bb6f84be5 100644 --- a/app/templates/admin/createEvent.html +++ b/app/templates/admin/createEvent.html @@ -3,7 +3,7 @@ {% set isNewEvent = 'create' in request.path %} {% if isNewEvent %} {% if eventData["program"].programName == 'CELTS-Sponsored Event' %} - {% set page_title = 'Create A ' + eventData["program"].programName %} + {% set page_title = 'Create a ' + eventData["program"].programName %} {% elif template.tag == 'single-program' %} {%set page_title = 'Create Event for ' + eventData["program"].programName %} @@ -23,7 +23,7 @@ {% extends "eventNav.html"%} {% endif %} -{% set eventPast = event.isPastStart if not isNewEvent else False %} +{% set eventPast = event.isPastEnd if not isNewEvent else False %} {% set tabName = 'edit' %} @@ -58,22 +58,21 @@

    {{page_title}}

    {% macro locationTimeMacro(eventData, isNewEvent, pageLocation) %}
    - - Location + {% if eventData.location %} value="{{eventData.location}}" {% endif %} - />
    - +
    -
    +
    @@ -82,10 +81,10 @@

    {{page_title}}

    - +
    @@ -106,7 +105,7 @@

    {{page_title}}

    {% else %} {% set startTime = "12:00" %} {% endif %} - @@ -120,9 +119,9 @@

    {{page_title}}

    {%else%} {% set endTime = "13:00" %} {% endif %} - + class = "startTime" id="endTime-{{pageLocation}}" data-page-location="{{pageLocation}}" required />
    @@ -138,7 +137,7 @@

    {{page_title}}

    {% endif %}
    - +
    @@ -216,7 +215,7 @@

    {{page_title}}

    - +
    diff --git a/app/templates/admin/manageServiceLearningFaculty.html b/app/templates/admin/manageServiceLearningFaculty.html index bdefed76b..c0bded3cf 100644 --- a/app/templates/admin/manageServiceLearningFaculty.html +++ b/app/templates/admin/manageServiceLearningFaculty.html @@ -22,6 +22,7 @@ {{super()}} + {% endblock %} {% block app_content %} @@ -71,7 +72,14 @@
    There are no unapproved courses for {{ term.description }}.
    {{ course.instructors }} {{course.status.status}} + title="status key" data-content="{{ course.status.status }}"> + {% if course.status.status == 'In Progress' %} + + {% elif course.status.status == 'Submitted' %} + + {% endif %} + {{course.status.status}} + diff --git a/app/templates/base.html b/app/templates/base.html index a5e7d7fd0..9460ece7c 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -23,9 +23,9 @@ {% block content %}
    -
    +
    -