diff --git a/app/controllers/minor/routes.py b/app/controllers/minor/routes.py index d4114c7f4..4ab3b330d 100644 --- a/app/controllers/minor/routes.py +++ b/app/controllers/minor/routes.py @@ -3,19 +3,32 @@ from app.controllers.minor import minor_bp from app.models.user import User -from app.models.attachmentUpload import AttachmentUpload +from app.models.cceMinorProposal import CCEMinorProposal from app.models.term import Term +from app.models.attachmentUpload import AttachmentUpload from app.logic.fileHandler import FileHandler from app.logic.utils import selectSurroundingTerms, getFilesFromRequest -from app.logic.minor import createOtherEngagementRequest, setCommunityEngagementForUser, getSummerExperience, getEngagementTotal, createSummerExperience, getProgramEngagementHistory, getCourseInformation, getCommunityEngagementByTerm, getCCEMinorProposals, createOtherEngagementRequest, removeProposal, getMinorSpreadsheet +from app.logic.minor import ( + changeProposalStatus, + createOtherEngagement, + updateOtherEngagementRequest, + setCommunityEngagementForUser, + getEngagementTotal, + createSummerExperience, + updateSummerExperience, + getProgramEngagementHistory, + getCourseInformation, + getCommunityEngagementByTerm, + getMinorSpreadsheet, + getCCEMinorProposals, + removeProposal +) @minor_bp.route('/profile//cceMinor', methods=['GET']) def viewCceMinor(username): """ Load minor management page with community engagements and summer experience """ - if not (g.current_user.isAdmin): - return abort(403) sustainedEngagementByTerm = getCommunityEngagementByTerm(username) @@ -29,7 +42,7 @@ def viewCceMinor(username): activeTab=activeTab) @minor_bp.route('/cceMinor//otherEngagement', methods=['GET', 'POST']) -def requestOtherEngagement(username): +def createOtherEngagementRequest(username): """ Load minor management page with community engagements and summer experience """ @@ -39,22 +52,68 @@ def requestOtherEngagement(username): # once we submit the form for creation if request.method == "POST": - createdProposal = createOtherEngagementRequest(username, request.form) - attachment = request.files.get("attachmentObject") - if attachment: - addFile = FileHandler(getFilesFromRequest(request), proposalId=createdProposal.id) - addFile.saveFiles() + createOtherEngagement(username, request) return redirect(url_for('minor.viewCceMinor', username=username)) return render_template("minor/requestOtherEngagement.html", + editable = True, user = User.get_by_id(username), selectableTerms = selectSurroundingTerms(g.current_term), - allTerms = getSummerExperience(username)) + postRoute = f"/cceMinor/{username}/otherEngagement", # when form is submitted, what POST route is it being submitted to. + attachmentFilePath = "", + attachmentFileName = "", + proposal = None) + +@minor_bp.route('/cceMinor/viewOtherEngagement/', methods=['GET']) +@minor_bp.route('/cceMinor/viewSummerExperience/', methods=['GET']) +@minor_bp.route('/cceMinor/editOtherEngagement/', methods=['GET', 'POST']) +@minor_bp.route('/cceMinor/editSummerExperience/', methods=['GET', 'POST']) +def editOrViewProposal(proposalID: int): + proposal = CCEMinorProposal.get_by_id(int(proposalID)) + if not (g.current_user.isAdmin or g.current_user.username == proposal.student.username): + return abort(403) + + editProposal = 'view' not in request.path + # if proposal is approved, only admins can edit, but not if the admin is the student + if proposal.isApproved and editProposal: + if g.current_user.username == proposal.student or not g.current_user.isAdmin: + return abort(403) + + attachmentObject = AttachmentUpload.get_or_none(proposal=proposalID) + attachmentFilePath = "" + attachmentFileName = "" + + if attachmentObject: + fileHandler = FileHandler(proposalId=proposalID) + attachmentFilePath = fileHandler.getFileFullPath(attachmentObject.fileName).lstrip("app/") # we need to remove app/ from the url because it prevents it from displaying + attachmentFileName = attachmentObject.fileName + + if request.method == "GET": + selectedTerm = Term.get_by_id(proposal.term) + flash("Once approved, a proposal can only be edited by an admin.", 'warning') + return render_template("minor/requestOtherEngagement.html" if 'OtherEngagement' in request.path else "minor/summerExperience.html", + editable = editProposal, + selectedTerm = selectedTerm, + contentAreas = proposal.contentAreas.split(", ") if proposal.contentAreas else [], + selectableTerms = selectSurroundingTerms(g.current_term, summerOnly=False if 'OtherEngagement' else True), + user = User.get_by_id(proposal.student), + postRoute = f"/cceMinor/editSummerExperience/{proposal.id}" if "SummerExperience" in request.path else f"/cceMinor/editOtherEngagement/{proposal.id}", + proposal = proposal, + attachmentFilePath = attachmentFilePath, + attachmentFileName = attachmentFileName + ) + + if "OtherEngagement" in request.path: + updateOtherEngagementRequest(proposalID, request) + else: + updateSummerExperience(proposalID, request.form) + + return redirect(url_for('minor.viewCceMinor', username=proposal.student)) @minor_bp.route('/cceMinor//summerExperience', methods=['GET', 'POST']) -def requestSummerExperience(username): +def createSummerExperienceRequest(username): """ Load minor management page with community engagements and summer experience """ @@ -69,7 +128,8 @@ def requestSummerExperience(username): summerTerms = selectSurroundingTerms(g.current_term, summerOnly=True) return render_template("minor/summerExperience.html", - summerTerms = summerTerms, + selectableTerms = summerTerms, + contentAreas = [], user = User.get_by_id(username), ) @@ -86,19 +146,38 @@ def getEngagementInformation(username, type, id, term): return information -@minor_bp.route('/cceMinor/withdraw//', methods = ['POST']) -def withdrawProposal(username, proposalID): +@minor_bp.route('/cceMinor///', methods=['POST']) +def updateProposal(action, username, proposalId): try: - if g.current_user.isAdmin or g.current_user.isFaculty or g.current_user == username: - removeProposal(proposalID) - flash("Experience successfully withdrawn", 'success') + if not (g.current_user.isAdmin or g.current_user.isFaculty or g.current_user.username == username): + flash("Unauthorized to perform this action", "warning") + return "" + + actionMap = { + "withdraw": ("Withdrawn", "Proposal successfully withdrawn"), + "complete": ("Completed", "Proposal successfully completed"), + "approve": ("Approved", "Proposal approved"), + "unapprove": ("Submitted", "Proposal unapproved"), + } + + if action not in actionMap: + flash("Invalid action", "warning") + return "" + + newStatus, message = actionMap[action] + + if action == "withdraw": + removeProposal(proposalId) else: - flash("Unauthorized to perform this action", 'warning') + changeProposalStatus(proposalId, newStatus) + flash(message, "success") + except Exception as e: print(e) - flash("Withdrawal Unsuccessful", 'warning') + flash("Proposal status could not be changed", "warning") + return "" - + @minor_bp.route('/cceMinor/getMinorSpreadsheet', methods=['GET']) def returnMinorSpreadsheet(): diff --git a/app/logic/events.py b/app/logic/events.py index db921d149..c030f5758 100644 --- a/app/logic/events.py +++ b/app/logic/events.py @@ -173,7 +173,7 @@ def attemptSaveEvent(eventData, attachmentFiles = None, renewedEvent = False): if attachmentFiles: for event in events: addFile = FileHandler(attachmentFiles, eventId=event.id) - addFile.saveFiles(saveOriginalFile=events[0]) + addFile.saveFiles(parentEvent=events[0]) return events, "" diff --git a/app/logic/fileHandler.py b/app/logic/fileHandler.py index 11ea58cc7..12c2d77a1 100644 --- a/app/logic/fileHandler.py +++ b/app/logic/fileHandler.py @@ -24,11 +24,14 @@ def __init__(self, files=None, courseId=None, eventId=None, programId=None, prop elif programId: self.path = os.path.join(self.path, app.config['files']['program_attachment_path']) elif proposalId: - self.path = os.path.join(self.path, app.config['files']['proposal_attachment_path']) + self.path = os.path.join(self.path, app.config['files']['proposal_attachment_path'], str(proposalId)) def makeDirectory(self): try: - extraDir = str(self.eventId) if self.eventId else "" + extraDir = "" + if self.eventId: + extraDir = str(self.eventId) + os.makedirs(os.path.join(self.path, extraDir)) except OSError as e: if e.errno != 17: @@ -46,57 +49,56 @@ def getFileFullPath(self, newfilename=''): return filePath - def saveFiles(self, saveOriginalFile=None): - try: - for file in self.files: - saveFileToFilesystem = None + def saveFiles(self, parentEvent=None): + """ + Saves attachments for different types and creates DB record for stored attachment + """ + for file in self.files: + saveFileToFilesystem = None + + if self.eventId: + attachmentName = str(parentEvent.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) + if parentEvent and parentEvent.id == self.eventId: + saveFileToFilesystem = attachmentName - if self.eventId: - 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) - 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) - saveFileToFilesystem = file.filename - elif self.programId: + elif self.courseId or self.proposalId: + recordId = self.courseId if self.courseId else self.proposalId + fieldName = 'course' if self.courseId else 'proposal' + + fileExists = AttachmentUpload.select().where( + getattr(AttachmentUpload, fieldName) == recordId, + AttachmentUpload.fileName == file.filename + ).exists() + + if not fileExists: + create_data = {fieldName: recordId, 'fileName': file.filename} + AttachmentUpload.create(**create_data) + saveFileToFilesystem = file.filename - # remove the existing file - deleteFileObject = AttachmentUpload.get_or_none(program=self.programId) - if deleteFileObject: - self.deleteFile(deleteFileObject.id) + elif self.programId: - # add the new file - fileType = file.filename.split('.')[-1] - fileName = f"{self.programId}.{fileType}" - AttachmentUpload.create(program=self.programId, fileName=fileName) - currentProgramID = fileName - saveFileToFilesystem = currentProgramID - - elif self.proposalId: - fileType = file.filename.split('.')[-1] - fileName = f"{self.proposalId}.{fileType}" - isFileInProposal = AttachmentUpload.select().where(AttachmentUpload.proposal == self.proposalId, - AttachmentUpload.fileName == fileName).exists() - if not isFileInProposal: - # add the new file - AttachmentUpload.create(proposal=self.proposalId, fileName=fileName) - saveFileToFilesystem = fileName + # remove the existing file + deleteFileObject = AttachmentUpload.get_or_none(program=self.programId) + if deleteFileObject: + self.deleteFile(deleteFileObject.id) - else: - saveFileToFilesystem = file.filename + # add the new file + fileType = file.filename.split('.')[-1] + fileName = f"{self.programId}.{fileType}" + AttachmentUpload.create(program=self.programId, fileName=fileName) + currentProgramID = fileName + saveFileToFilesystem = currentProgramID - if saveFileToFilesystem: - self.makeDirectory() - file.save(self.getFileFullPath(newfilename=saveFileToFilesystem)) + else: + saveFileToFilesystem = file.filename - except AttributeError as e: - print(e) + if saveFileToFilesystem: + self.makeDirectory() + file.save(self.getFileFullPath(newfilename=saveFileToFilesystem)) def retrievePath(self, files): pathDict = {} diff --git a/app/logic/minor.py b/app/logic/minor.py index 955a124bc..3da351cd1 100644 --- a/app/logic/minor.py +++ b/app/logic/minor.py @@ -5,7 +5,7 @@ from peewee import JOIN, fn, Case, DoesNotExist, SQL import xlsxwriter -from app import app +from app.models import mainDB from app.models.user import User from app.models.term import Term from app.models.event import Event @@ -18,9 +18,8 @@ from app.models.individualRequirement import IndividualRequirement from app.models.certificationRequirement import CertificationRequirement from app.models.cceMinorProposal import CCEMinorProposal -from app.logic.createLogs import createActivityLog from app.logic.fileHandler import FileHandler -from app.logic.serviceLearningCourses import deleteCourseObject +from app.logic.utils import getFilesFromRequest from app.models.attachmentUpload import AttachmentUpload @@ -29,37 +28,31 @@ def createSummerExperience(username, formData): Given the username of the student and the formData which includes all of the SummerExperience information, create a new SummerExperience object. """ - try: - user = User.get(User.username == username) - contentAreas = ', '.join(formData.getlist('contentArea')) # Combine multiple content areas - CCEMinorProposal.create( - student=user, - proposalType = 'Summer Experience', - contentAreas = contentAreas, - status="Pending", - createdBy = g.current_user, - **formData, - ) - except Exception as e: - print(f"Error saving summer experience: {e}") - raise e - -def getCCEMinorProposals(username): - proposalList = [] - - cceMinorProposals = list(CCEMinorProposal.select().where(CCEMinorProposal.student==username)) + user = User.get(User.username == username) + contentAreas = ', '.join(formData.getlist('contentArea')) # Combine multiple content areas + formData = dict(formData) + formData.pop("contentArea") + return CCEMinorProposal.create( + student=user, + proposalType = 'Summer Experience', + contentAreas = contentAreas, + createdBy = g.current_user, + **formData, + ) - for experience in cceMinorProposals: - proposalList.append({ - "id": experience.id, - "type": experience.proposalType, - "createdBy": experience.createdBy, - "supervisor": experience.supervisorName, - "term": experience.term, - "status": experience.status, - }) +def updateSummerExperience(proposalID, formData): + """ + Given the username of the student and the formData which includes all of + the SummerExperience information, create a new SummerExperience object. + """ + contentAreas = ', '.join(formData.getlist('contentArea')) # Combine multiple content areas + formData = dict(formData) + formData.pop("contentArea") + formData.pop("experienceHoursOver300") + CCEMinorProposal.update(contentAreas=contentAreas, **formData).where(CCEMinorProposal.id == proposalID).execute() - return proposalList +def getCCEMinorProposals(username): + return list(CCEMinorProposal.select().where(CCEMinorProposal.student==username)) def getEngagementTotal(engagementData): """ @@ -330,21 +323,64 @@ def getCommunityEngagementByTerm(username): # sorting the communityEngagementByTermDict by the term id return dict(sorted(communityEngagementByTermDict.items(), key=lambda engagement: engagement[0][1])) -def createOtherEngagementRequest(username, formData): +def createOtherEngagement(username, request): """ Create a CCEMinorProposal entry based off of the form data """ user = User.get(User.username == username) - cceObject = CCEMinorProposal.create(proposalType = 'Other Engagement', - createdBy = g.current_user, - status = 'Pending', - student = user, - **formData + createdProposal = CCEMinorProposal.create(proposalType = 'Other Engagement', + createdBy = g.current_user, + student = user, + **request.form ) - - return cceObject - + proposalObject = CCEMinorProposal.get_by_id(createdProposal) + attachment = request.files.get("attachmentObject") + if attachment: + addFile = FileHandler(getFilesFromRequest(request), proposalId=createdProposal.id) + addFile.saveFiles(parentEvent=proposalObject) + +def updateOtherEngagementRequest(proposalID, request): + attachment = request.files.get("attachmentObject") + + with mainDB.atomic(): + # Get proposal safely + proposalObject = CCEMinorProposal.get_by_id(proposalID) + + # ---- HANDLE ATTACHMENT SAFELY ---- + if attachment: + existingAttachment = ( + AttachmentUpload + .select() + .where(AttachmentUpload.proposal == proposalID) + .first() + ) + + if existingAttachment: + deleteFile = FileHandler(proposalId=proposalID) + deleteFile.deleteFile(existingAttachment.id) + + addFile = FileHandler( + getFilesFromRequest(request), + proposalId=proposalID + ) + addFile.saveFiles(parentEvent=proposalObject) + + # ---- CLEAN FORM DATA ---- + update_data = dict(request.form) + + # remove fields that are not DB columns + update_data.pop("contentArea", None) + update_data.pop("attachmentObject", None) + + # ---- UPDATE PROPOSAL ---- + ( + CCEMinorProposal + .update(**update_data) + .where(CCEMinorProposal.id == proposalID) + .execute() + ) + def saveSummerExperience(username, summerExperience, currentUser): """ :param username: username of the student that the summer experience is for @@ -411,3 +447,9 @@ def removeProposal(proposalID) -> None: proposalFileHandler.deleteFile(proposalAttachment.id) CCEMinorProposal.delete().where(CCEMinorProposal.id == proposalID).execute() + +def changeProposalStatus(proposalID, newStatus) -> None: + """ + Changes the status of a proposal. + """ + CCEMinorProposal.update(status=newStatus).where(CCEMinorProposal.id == int(proposalID)).execute() diff --git a/app/models/cceMinorProposal.py b/app/models/cceMinorProposal.py index 9bb4d27e5..2d0ae1763 100644 --- a/app/models/cceMinorProposal.py +++ b/app/models/cceMinorProposal.py @@ -22,13 +22,16 @@ class CCEMinorProposal(baseModel): supervisorEmail = CharField() totalHours = IntegerField(null=True) totalWeeks = IntegerField(null=True) - description = TextField() createdOn = DateTimeField(default=datetime.datetime.now) createdBy = ForeignKeyField(User) - status = CharField(constraints=[Check("status in ('Approved', 'Pending', 'Denied')")]) + status = CharField(constraints=[Check("status in ('Draft', 'Submitted', 'Approved', 'Denied', 'Completed')")]) @property def isOver300Hours(self): if not int(self.totalHours) or (int(self.totalHours) and int(self.totalHours) >= 300): return True return False + + @property + def isApproved(self): + return self.status in ['Approved', 'Completed'] diff --git a/app/static/css/cceMinorManagement.css b/app/static/css/cceMinorManagement.css new file mode 100644 index 000000000..347cbf333 --- /dev/null +++ b/app/static/css/cceMinorManagement.css @@ -0,0 +1,34 @@ +ul.timeline { + list-style-type: none; + position: relative; +} +ul.timeline:before { + content: ' '; + background: #d4d9df; + display: inline-block; + position: absolute; + left: 29px; + width: 2px; + height: 100%; + z-index: 400; +} +ul.timeline > li { + margin: 20px 0; + padding-left: 20px; +} +ul.timeline > li:before { + content: ' '; + background: white; + display: inline-block; + position: absolute; + border-radius: 50%; + border: 2px solid #198754; + left: 20px; + width: 20px; + height: 20px; + z-index: 400; +} + +ul.timeline > li.completed:before { + background: #198754; +} \ No newline at end of file diff --git a/app/static/js/base.js b/app/static/js/base.js index 3ef308142..5dbe8a96e 100644 --- a/app/static/js/base.js +++ b/app/static/js/base.js @@ -205,70 +205,102 @@ function handleFileSelection(fileInputId, single=false){ var attachedObjectContainerId = fileInputId + "Container" $(fileBoxId).after(`
`) var objectContainerId = "#" + attachedObjectContainerId - $(fileBoxId).on('change', function() { - const selectedFiles = $(fileBoxId).prop('files'); - for (let i = 0; i < selectedFiles.length; i++){ - const file = selectedFiles[i]; - if (hasUniqueFileName(file.name)){ - let fileName = (file.name.length > 25) ? file.name.slice(0,10) + '...' + file.name.slice(-10) : file.name; - let fileExtension = file.name.split(".").pop(); - let iconClass = ''; - switch(fileExtension) { - case 'jpg': - case 'png': - case 'jpeg': - iconClass = "bi-file-image"; - break - case 'pdf': - iconClass = 'bi-filetype-pdf'; - break - case 'docx': - iconClass = 'bi-filetype-docx'; - break - case 'xlsx': - iconClass = 'bi-filetype-xlsx'; - break - default: - iconClass = 'bi-file-earmark-arrow-up'; - } - let trashNum = ($(objectContainerId+ " .row").length) - var fullTrashId = "#trash" + trashNum - let fileHTML = " \ -
\ - \ -
" + fileName + "
\ -
\ -
\ - \ -
\ -
\ -
" - if (single) { - $(objectContainerId).html(fileHTML) - } - else { - $(objectContainerId).append(fileHTML) - } - $(fullTrashId).data("file", file); - $(fullTrashId).data("file-container-id", attachedObjectContainerId); - $(fullTrashId).on("click", function() { - let elementFileNum = $(this).data('filenum'); - let attachedObjectContainerId = $(this).data('file-container-id'); - $("#"+ attachedObjectContainerId + " #attachedFilesRow" + elementFileNum).remove(); - $(fileBoxId).prop('files', getSelectedFiles()); - }) - $(fileBoxId).data("file-num", $(fileBoxId).data("file-num") + 1) + + // if we have files that are already saved we can get their information from here + let filePath = $(fileBoxId).data("file-path") + let fileName = $(fileBoxId).data("file-name") + + let existingFile = { + name: fileName, + path: filePath + } + if (filePath && fileName) { + populateSelectedFiles(fileBoxId, attachedObjectContainerId, objectContainerId, single, existingFile) + } + $(fileBoxId).on('change', () => populateSelectedFiles(fileBoxId, attachedObjectContainerId, objectContainerId, single)); +} + +function populateSelectedFiles(fileBoxId, attachedObjectContainerId, objectContainerId, single, existingFile=null) { + const selectedFiles = existingFile ? [existingFile] : $(fileBoxId).prop('files'); + for (let i = 0; i < selectedFiles.length; i++){ + const file = selectedFiles[i]; + if (hasUniqueFileName(file.name)){ + let fileName = (file.name.length > 25) ? file.name.slice(0,10) + '...' + file.name.slice(-10) : file.name; + let trashNum = ($(objectContainerId+ " .row").length) + let iconClass = getIconClass(file) + let fileHTML = generateFileRowHTML(trashNum, iconClass, fileName) + if (existingFile) { + let viewing = $("#isViewing").val() + fileHTML = generateFileRowHTML(trashNum, iconClass, existingFile.name, existingFile.path, viewing) + } + var fullTrashId = "#trash" + trashNum + if (single) { + $(objectContainerId).html(fileHTML) + } + else { + $(objectContainerId).append(fileHTML) + } + $(fullTrashId).data("file", file); + $(fullTrashId).data("file-container-id", attachedObjectContainerId); + $(fullTrashId).on("click", function() { + let elementFileNum = $(this).data('filenum'); + let attachedObjectContainerId = $(this).data('file-container-id'); + $("#"+ attachedObjectContainerId + " #attachedFilesRow" + elementFileNum).remove(); + $(fileBoxId).prop('files', getSelectedFiles()); + }) + $(fileBoxId).data("file-num", $(fileBoxId).data("file-num") + 1) + } + else{ + if (single){ + $(objectContainerId).html(fileHTML) } else{ - if (single){ - $(objectContainerId).html(fileHTML) - } - else{ - msgToast("File with filename '" + file.name + "' has already been added to this event") - } + msgToast("File with filename '" + file.name + "' has already been added to this event") } } + } + if (!existingFile) { $(fileBoxId).prop('files', getSelectedFiles()); - }); + } +} +function generateFileRowHTML(trashNum, iconClass, fileName, filePath=null, showTrash=true) { + return ` +
+ + ${filePath != null ? + `${fileName}` + : `
${fileName}
` + } +
+
+ +
+
+
+ ` } + +function getIconClass(file) { + let iconClass = ''; + let fileExtension = file.name.split(".").pop(); + switch(fileExtension) { + case 'jpg': + case 'png': + case 'jpeg': + iconClass = "bi-file-image"; + break + case 'pdf': + iconClass = 'bi-filetype-pdf'; + break + case 'docx': + iconClass = 'bi-filetype-docx'; + break + case 'xlsx': + iconClass = 'bi-filetype-xlsx'; + break + default: + iconClass = 'bi-file-earmark-arrow-up'; + } + return iconClass +} \ No newline at end of file diff --git a/app/static/js/cceMinorProposalManagement.js b/app/static/js/cceMinorProposalManagement.js new file mode 100644 index 000000000..ae1ca3278 --- /dev/null +++ b/app/static/js/cceMinorProposalManagement.js @@ -0,0 +1,50 @@ +$(document).ready(function() { + $("#withdrawBtn").on("click", function(){ + updateProposalStatus('withdraw') + }) +}); + +function changeAction(element) { + const proposalId = element.id; + const proposalType = $(element).data('type'); + const proposalAction = element.value; + $('#proposalID').val(proposalId); + + if (proposalAction === "Edit") { + location = `/cceMinor/edit${proposalType.replace(/\s+/g, '')}/${proposalId}`; + } else if (proposalAction === "View") { + location = `/cceMinor/view${proposalType.replace(/\s+/g, '')}/${proposalId}`; + } else if (proposalAction === "withdraw") { + $('#withdrawModal').modal('show'); + } else { + updateProposalStatus(proposalAction.toLowerCase()); + } + + resetAllSelections(); +} + + +function resetAllSelections() { + $('.form-select').val('---'); +} + +function updateProposalStatus(action){ + // for withdrawing proposals or marking them as complete + let proposalID = $("#proposalID").val(); + let username = $("#username").val(); + + $.ajax({ + url: `/cceMinor/${action}/${username}/${proposalID}`, + type: "POST", + success: function(res){ + window.location.href = `/profile/${username}/cceMinor?tab=manageProposals`; + }, + error: function(request, status, error) { + console.log(status, error); + } + }); + + resetAllSelections(); +} + +window.changeAction = changeAction; diff --git a/app/static/js/minorProfilePage.js b/app/static/js/minorProfilePage.js index 8792c70cc..1357eb795 100644 --- a/app/static/js/minorProfilePage.js +++ b/app/static/js/minorProfilePage.js @@ -2,45 +2,6 @@ import { validateEmail } from "./emailValidation.mjs"; $(document).ready(function() { $("#supervisorEmail").on('input', validateEmail); - $("#withdrawBtn").on("click", withdrawProposal); - -function changeAction(action){ - let proposalID = action.id; - let proposalAction = action.value; - // decides what to do based on selection - if (proposalAction == "Withdraw"){ - $('#proposalID').val(proposalID); - $('#withdrawModal').modal('show'); - - } - resetAllSelections() - } - - - function resetAllSelections() { - $('.form-select').val('---'); - } - - - function withdrawProposal(){ - // uses hidden label to withdraw course - let proposalID = $("#proposalID").val(); - let username = $("#username").val() - $.ajax({ - url: `/cceMinor/withdraw/${username}/${proposalID}`, - type: "POST", - success: function(s){ - window.location.href = `/profile/${username}/cceMinor?tab=manageProposals` - }, - error: function(request, status, error) { - console.log(status, error); - } - }) - resetAllSelections() - }; - - - window.changeAction = changeAction; $('input.phone-input').inputmask('(999)-999-9999') $('input.phone-input').on('input', function(){ @@ -56,6 +17,60 @@ function changeAction(action){ } }) + handleFileSelection("supervisorAttachment", true) + + $('.submit-proposal').on('click', function(e) { + e.preventDefault(); + let status = $(this).data('status'); + $('#statusField').val(status); + const stats= $('#statusField').val(); + const form = $('#proposalForm')[0]; + + if(stats ==="Submitted"){ + + const fileInput = $('#supervisorAttachment'); + const filePath = fileInput.data('file-path'); + const firstInvalid = form.querySelector(':invalid'); + console.log(filePath,"filePath"); + + if (!filePath) + { + firstInvalid.scrollIntoView({ + block: 'center' }); + firstInvalid.focus(); + return; + } + else{ + $( '#proposalForm').submit() + + } + return; + + } + var formData = new FormData($('#proposalForm')[0]); + var actionURL = $('#proposalForm').attr('action'); + let username = $("#username").val(); + + $.ajax({ + url: actionURL, + type: 'POST', + data: formData, + processData: false, + contentType: false, + success: function(response) { + window.location.href = `/profile/${username}/cceMinor?tab=manageProposals`; + }, + error: function(xhr, status, error) { + console.error('Error:', error); + } + }); + }); + + $('#exitButton').on('click', function() { + let username = $("#username").val() + window.location.href = `/profile/${username}/cceMinor?tab=manageProposals` + }) + // ************** SUSTAINED COMMUNITY ENGAGEMENTS ************** // $('.engagement-row').on("click", function() { showEngagementInformation($(this).data('engagement-data')); @@ -72,28 +87,8 @@ function changeAction(action){ // ************** SUMMER EXPERIENCE ************** // $('#hoursBelow300Container').hide() - $('#otherExperienceDescription').hide() - - $('#summerExperienceForm').on('submit', function(event) { - event.preventDefault(); - var formData = new FormData(this); - var actionUrl = $(this).attr('action'); - let username = $("#username").val() - - $.ajax({ - url: actionUrl, - type: 'POST', - data: formData, - contentType: false, - processData: false, - success: function(response) { - window.location.href = `/profile/${username}/cceMinor?tab=manageProposals` - }, - error: function(xhr, status, error) { - console.error('Error:', error); - } - }); - }); + toggleUnder300HoursTextarea() + toggleOtherExperienceTextarea() $("input[name='experienceHoursOver300']").on("change", function() { toggleUnder300HoursTextarea(); @@ -103,6 +98,8 @@ function changeAction(action){ // when they are hidden $("#yes300hours").on("click", function() { let hoursWeeksBoxes = $("#totalHours, #totalWeeks") + $("#totalWeeks").val(8); + $("#totalHours").val(300) hoursWeeksBoxes.prop('required', false); }) @@ -132,26 +129,6 @@ function changeAction(action){ // ************** END SUMMER EXPERIENCE ************** // // ************** OTHER ENGAGEMENT ************** // - $('#otherEngagementForm').on('submit', function(event) { - event.preventDefault(); - var formData = new FormData(this); - var actionUrl = $(this).attr('action'); - let username = $("#username").val() - $.ajax({ - url: actionUrl, - type: 'POST', - data: formData, - contentType: false, - processData: false, - success: function(response) { - window.location.href = `/profile/${username}/cceMinor?tab=manageProposals` - }, - error: function(xhr, status, error) { - console.error('Error:', error); - } - }); - }); - $("input[name='experienceType']").on("change", function() { toggleOtherExperienceTextarea(); }); @@ -243,12 +220,15 @@ function toggleEngagementCredit(isChecked, engagementData, checkbox){ } function toggleUnder300HoursTextarea() { - var yesRadio = $('#yes300hours'); + var noRadio = $('#no300hours'); var conditionalTextBox = $('#hoursBelow300Container'); - if (yesRadio.is(':checked')) { - conditionalTextBox.hide() + if (noRadio.is(':checked')) { + conditionalTextBox.show(); } else { - conditionalTextBox.show() + conditionalTextBox.hide(); + if ($('#yes300hours').is(':checked')) { + $('#totalHours').val(300); + } } } diff --git a/app/templates/macros/cceMinorStatusMacro.html b/app/templates/macros/cceMinorStatusMacro.html new file mode 100644 index 000000000..a7eff4b5d --- /dev/null +++ b/app/templates/macros/cceMinorStatusMacro.html @@ -0,0 +1,17 @@ +{% macro minorStatusMacro(status) %} +
+
+
    +
  • +
    Proposal Submitted by Student
    +
  • +
  • +
    Proposal Approved by Admin
    +
  • +
  • +
    Experience Marked Completed by Admin
    +
  • +
+
+
+{% endmacro %} \ No newline at end of file diff --git a/app/templates/minor/cceMinorProposalManagement.html b/app/templates/minor/cceMinorProposalManagement.html index 275eaa6c4..e656b160b 100644 --- a/app/templates/minor/cceMinorProposalManagement.html +++ b/app/templates/minor/cceMinorProposalManagement.html @@ -7,30 +7,41 @@ Created By Supervisor Term - Status Action + {% for proposal in proposalList %} - {{ proposal['type'] }} - {{proposal['createdBy'].firstName}} {{proposal['createdBy'].lastName}} - {{proposal['supervisor']}} - {{ proposal['term'].description }} + {{ proposal.proposalType }} + {{proposal.createdBy.firstName}} {{proposal.createdBy.lastName}} + {{proposal.supervisorName}} + {{ proposal.term.description }} - {{proposal['status']}} - - - - - - - + {% if not proposal.isApproved or g.current_user.isCeltsAdmin %} + + {% endif %} + {% if g.current_user.isCeltsAdmin %} + {% if proposal.isApproved %} + + {% else %} + + {% endif %} + {% endif %} + + {% if g.current_user.isCeltsAdmin == True %} + + {% endif %} + + {% from 'macros/cceMinorStatusMacro.html' import minorStatusMacro %} + {{ minorStatusMacro(proposal.status) }} + {% endfor %} diff --git a/app/templates/minor/companyOrganizationInformation.html b/app/templates/minor/companyOrganizationInformation.html index f8c60ae75..087c009c3 100644 --- a/app/templates/minor/companyOrganizationInformation.html +++ b/app/templates/minor/companyOrganizationInformation.html @@ -6,14 +6,18 @@

Company/Organization Information

- +

- +

@@ -21,10 +25,14 @@

Company/Organization Information

- +
- +
\ No newline at end of file diff --git a/app/templates/minor/profile.html b/app/templates/minor/profile.html index 613a8055f..9cdd99c73 100644 --- a/app/templates/minor/profile.html +++ b/app/templates/minor/profile.html @@ -4,6 +4,7 @@ {% block scripts %} {{ super() }} + @@ -14,6 +15,7 @@ {% block styles %} {{ super() }} + {% endblock %} {% block app_content %} diff --git a/app/templates/minor/requestOtherEngagement.html b/app/templates/minor/requestOtherEngagement.html index d342bc18e..d842ba916 100644 --- a/app/templates/minor/requestOtherEngagement.html +++ b/app/templates/minor/requestOtherEngagement.html @@ -17,7 +17,8 @@ {% endblock %} {% block app_content %} -
+ {% set viewing = (not editable) and proposal %} +
@@ -27,16 +28,31 @@

Proposal for Other Community Engaged E Please fill out this form to submit a proposal for another community engaged experience that pertains to at least one of the four following key content areas: power and inequality, community and identity, civic literacy, or civic skills.

- + +

- + + {% if viewing %} + + {% endif %} {% for term in selectableTerms %} - + {% endfor %}
@@ -45,7 +61,9 @@

Proposal for Other Community Engaged E
- +
{% include "minor/companyOrganizationInformation.html" %} @@ -55,8 +73,10 @@

Proposal for Other Community Engaged E {% include "minor/supervisorInformation.html" %}
- - + + +
@@ -67,27 +87,43 @@

Experience Information

- +

- +

- +


-
- +
+
+ +
+
+
+
+ + + +
+
diff --git a/app/templates/minor/summerExperience.html b/app/templates/minor/summerExperience.html index 3f5384352..e34b5f14b 100644 --- a/app/templates/minor/summerExperience.html +++ b/app/templates/minor/summerExperience.html @@ -17,8 +17,10 @@ {% endblock %} {% block app_content %} + {% set viewing = not editable and proposal %}
-
+ +
@@ -27,14 +29,29 @@

Proposal for Community-Engaged Summer Experience

The minor requires an Intensive Community-Engaged Summer Experience that is focused on work that more deeply explores at least one of the four following key content areas: power and inequality, community and identity, civic literacy, or civic skills.

+
- - + + {% if viewing %} + + {% endif %} + {% for term in selectableTerms %} + {% endfor %}
@@ -55,61 +72,108 @@

Experience Information

-
+ +

- +
- +
- +
- +
- +
- +
-
+ {% if viewing and proposal.experienceDescription or not viewing %} + + + {% endif %} +

- +
- +
- +
- +


- +
- +

- + +

- + +


- -
- -
+
+
+ +
+
+
+
+ {% if not viewing %} + + + {% endif %} +
+
diff --git a/app/templates/minor/supervisorInformation.html b/app/templates/minor/supervisorInformation.html index 1df3e9a66..ae20a15d8 100644 --- a/app/templates/minor/supervisorInformation.html +++ b/app/templates/minor/supervisorInformation.html @@ -6,7 +6,9 @@

Supervisor Information

- +
@@ -15,11 +17,15 @@

Supervisor Information

- +
- +
\ No newline at end of file diff --git a/database/prod-backup.sql b/database/prod-backup.sql index 4b3c21cf7..e5becaf74 100644 --- a/database/prod-backup.sql +++ b/database/prod-backup.sql @@ -202,7 +202,7 @@ CREATE TABLE `cceminorproposal` ( CONSTRAINT `cceminorproposal_ibfk_1` FOREIGN KEY (`student_id`) REFERENCES `user` (`username`), CONSTRAINT `cceminorproposal_ibfk_2` FOREIGN KEY (`term_id`) REFERENCES `term` (`id`), CONSTRAINT `cceminorproposal_ibfk_3` FOREIGN KEY (`createdBy_id`) REFERENCES `user` (`username`), - CONSTRAINT `cceminorproposal_chk_1` CHECK ((`status` in (_utf8mb4'Approved',_utf8mb4'Pending',_utf8mb4'Denied'))) + CONSTRAINT `cceminorproposal_chk_1` CHECK ((`status` in (_utf8mb4'Approved',_utf8mb4'Pending',_utf8mb4'Denied',_utf8mb4'Submitted',_utf8mb4'Draft',_utf8mb4'Completed'))) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; diff --git a/tests/code/test_fileHandler.py b/tests/code/test_fileHandler.py index 8b624cf5d..46c796fe8 100644 --- a/tests/code/test_fileHandler.py +++ b/tests/code/test_fileHandler.py @@ -62,16 +62,16 @@ def test_makingdirectory(): def test_saveFiles(): with mainDB.atomic() as transaction: # test event - handledEventFile.saveFiles(saveOriginalFile = Event.get_by_id(15)) + handledEventFile.saveFiles(parentEvent = Event.get_by_id(15)) assert AttachmentUpload.select().where(AttachmentUpload.fileName == '15/eventfile.pdf').exists() # test saving 2nd event in a hypothetical recurring series - handledEventFileRecurring.saveFiles(saveOriginalFile = Event.get_by_id(15)) + handledEventFileRecurring.saveFiles(parentEvent = Event.get_by_id(15)) assert AttachmentUpload.select().where(AttachmentUpload.event_id == 16, AttachmentUpload.fileName == '15/eventfile.pdf').exists() assert 1 == AttachmentUpload.select().where(AttachmentUpload.event_id == 16, AttachmentUpload.fileName == '15/eventfile.pdf').count() - handledEventFileRecurring.saveFiles(saveOriginalFile = Event.get_by_id(15)) + handledEventFileRecurring.saveFiles(parentEvent = Event.get_by_id(15)) assert 1 == AttachmentUpload.select().where(AttachmentUpload.event_id == 16, AttachmentUpload.fileName == '15/eventfile.pdf').count() # test course @@ -122,10 +122,10 @@ def test_retrievePath(): def test_deleteFile(): with mainDB.atomic() as transaction: # creates file in event file directory for deletion - handledEventFile.saveFiles(saveOriginalFile = Event.get_by_id(15)) + handledEventFile.saveFiles(parentEvent = Event.get_by_id(15)) # creates a second file to simulate recurring events - handledEventFileRecurring.saveFiles(saveOriginalFile = Event.get_by_id(15)) + handledEventFileRecurring.saveFiles(parentEvent = Event.get_by_id(15)) # creates a course file for deletion handledCourseFile.saveFiles() diff --git a/tests/code/test_minor.py b/tests/code/test_minor.py index 49eb4e8db..36150e737 100644 --- a/tests/code/test_minor.py +++ b/tests/code/test_minor.py @@ -6,6 +6,7 @@ from collections import OrderedDict from playhouse.shortcuts import model_to_dict from werkzeug.datastructures import ImmutableMultiDict, FileStorage +from types import SimpleNamespace from app import app from app.models import mainDB @@ -14,18 +15,33 @@ from app.models.event import Event from app.models.course import Course from app.models.program import Program +from app.models.attachmentUpload import AttachmentUpload from app.models.courseInstructor import CourseInstructor from app.models.eventParticipant import EventParticipant from app.models.cceMinorProposal import CCEMinorProposal from app.models.courseParticipant import CourseParticipant from app.models.individualRequirement import IndividualRequirement -from app.models.attachmentUpload import AttachmentUpload -from app.logic.minor import createOtherEngagementRequest, getMinorInterest, getMinorProgress, setCommunityEngagementForUser, createSummerExperience, removeProposal -from app.logic.minor import getProgramEngagementHistory, getCourseInformation, toggleMinorInterest, getCommunityEngagementByTerm, getSummerExperience, getEngagementTotal, getCCEMinorProposals -from app.logic.minor import declareMinorInterest, getDeclaredMinorStudents +from app.logic.minor import ( + changeProposalStatus, + createOtherEngagement, + getMinorInterest, + getMinorProgress, + setCommunityEngagementForUser, + createSummerExperience, + getProgramEngagementHistory, + getCourseInformation, + toggleMinorInterest, + getCommunityEngagementByTerm, + getEngagementTotal, + getCCEMinorProposals, + updateOtherEngagementRequest, + updateSummerExperience, + getDeclaredMinorStudents, + declareMinorInterest, + removeProposal +) from app.logic.fileHandler import FileHandler - @pytest.fixture def testUser(request): """Fixture to create a user""" @@ -75,13 +91,12 @@ def testProposal(request): "supervisorEmail": params.get("supervisorEmail", "kafuigle.com"), 'totalHours': params.get("totalHours", 300), 'totalWeeks': params.get("totalWeeks", 10), + 'status': params.get("status", 'Draft'), } else: defaultProposal = { "term": params.get("term", 3), "experienceName": params.get("experienceName", "Assistant to Finn"), - "experienceType": params.get("experienceType", "Internship"), - "contentArea": params.get("contentArea", ["Power and inequality", "Civic literacy"]), "orgName": params.get("orgName", "Finn's Org"), "orgAddress": params.get("orgAddress", "Finn's House"), "orgPhone": params.get("orgPhone", "513-384-FINN"), @@ -92,9 +107,18 @@ def testProposal(request): 'totalHours': params.get("totalHours", 300), 'totalWeeks': params.get("totalWeeks", 10), 'experienceDescription': params.get("experienceDescription", "Working day and night to make sure Finn's needs are met"), - } + 'status': params.get("status", 'Draft'), + } + mockRequestProposalObject = SimpleNamespace( + form=defaultProposal, + files=SimpleNamespace( + getlist=lambda key: [], + get=lambda key: None + ) + ) + # override default values with those put in the parameters. - return defaultProposal + return mockRequestProposalObject @pytest.mark.integration def test_getCourseInformation(testUser): @@ -177,8 +201,6 @@ def test_getProgramEngagementHistory(testUser): @pytest.mark.integration @pytest.mark.parametrize("testProposal", [ {"proposalType": "otherEngagement"}, - {"proposalType": "summerExperience"}, - ], indirect=True) def test_getCCEMinorProposals(testUser, testProposal): @@ -188,22 +210,30 @@ def test_getCCEMinorProposals(testUser, testProposal): with app.app_context(): g.current_user = testUser.username - createOtherEngagementRequest(testUser.username, testProposal) + createOtherEngagement(testUser.username, testProposal) assert len(getCCEMinorProposals(testUser.username)) == 1 + # convert the otherEngagement to a summerExperience proposal type + testProposal.form.pop("experienceName") + testProposal.form.pop("experienceDescription") + + testProposal.form["roleDescription"] = "Assistant to Finn" + testProposal.form["experienceType"] = "Internship" + testProposal.form["contentArea"] = ["Power and inequality", "Civic literacy"] + with app.app_context(): g.current_user = testUser.username - createSummerExperience(testUser.username, ImmutableMultiDict(testProposal)) + createSummerExperience(testUser.username, ImmutableMultiDict(testProposal.form)) assert len(getCCEMinorProposals(testUser.username)) == 2 summerExperienceCount = 0 otherExperienceCount = 0 for experience in getCCEMinorProposals(testUser.username): - if experience["type"] == "Summer Experience": + if experience.proposalType == "Summer Experience": summerExperienceCount+=1 - elif experience["type"] == "Other Engagement": + elif experience.proposalType == "Other Engagement": otherExperienceCount+=1 else: raise AssertionError @@ -488,27 +518,36 @@ def test_getMinorProgress(): "supervisorName": "Finn", "supervisorPhone": "513-384-FINN", "supervisorEmail": "finn@finn.com", + "status": "Draft" }) - khattsRequestedEngagement = ({'term': 3, - 'experienceName': 'Test Experience', - 'orgName': 'Test Company', - 'orgAddress': '123 test ln', - 'orgPhone': '(123)-456-7890', - 'orgPhone': '(123)-456-7890', - 'orgWebsite': "kafui.com", - 'supervisorPhone': '(123)-798-3516', - 'supervisorName': 'kafui', - 'supervisorEmail': 'test@supervisor.com', - 'totalHours': 300, - 'totalWeeks': 10, - 'experienceDescription': 'Test Description', - }) + khattsRequestedEngagement = {'term': 3, + 'experienceName': 'Test Experience', + 'orgName': 'Test Company', + 'orgAddress': '123 test ln', + 'orgPhone': '(123)-456-7890', + 'orgPhone': '(123)-456-7890', + 'orgWebsite': "kafui.com", + 'supervisorPhone': '(123)-798-3516', + 'supervisorName': 'kafui', + 'supervisorEmail': 'test@supervisor.com', + 'totalHours': 300, + 'totalWeeks': 10, + 'experienceDescription': 'Test Description', + "status": "Draft" + } + khattsRequestedEngagementRequest = SimpleNamespace( + form=khattsRequestedEngagement, + files=SimpleNamespace( + getlist=lambda key: [], + get=lambda key: None + ) + ) # verify that Sreynit has a summer, 1 engagement, and an other community engagement request in with app.app_context(): g.current_user = "ramsayb2" - createOtherEngagementRequest("khatts", khattsRequestedEngagement) + createOtherEngagement("khatts", khattsRequestedEngagementRequest) createSummerExperience("khatts", khattsSummerExperience) minorProgressWithSummerAndRequestOther = getMinorProgress() @@ -524,7 +563,7 @@ def test_createSummerExperience(testUser, testTerm, testProposal): with mainDB.atomic() as transaction: # create testing objects - testProposal["term"] = testTerm + testProposal.form["term"] = testTerm User.create(username="glek", firstName="kafui", @@ -540,7 +579,7 @@ def test_createSummerExperience(testUser, testTerm, testProposal): # create the summer experience with the test data and verify FINN has a new entry with app.app_context(): g.current_user = "glek" - createSummerExperience(testUser.username, ImmutableMultiDict(testProposal)) + createSummerExperience(testUser.username, ImmutableMultiDict(testProposal.form)) newSummerExperiences = list(CCEMinorProposal.select().where(CCEMinorProposal.student == testUser.username, CCEMinorProposal.proposalType == 'Summer Experience')) assert len(newSummerExperiences) == 1 @@ -553,7 +592,7 @@ def test_createSummerExperience(testUser, testTerm, testProposal): {"proposalType": "otherEngagement"} ], indirect=True) @pytest.mark.integration -def test_createOtherEngagementRequest(testUser, testProposal): +def test_createOtherEngagement(testUser, testProposal): with mainDB.atomic() as transaction: User.create(username="glek", firstName="kafui", @@ -564,7 +603,7 @@ def test_createOtherEngagementRequest(testUser, testProposal): # Save the requested event to the database with app.app_context(): g.current_user = "glek" - createOtherEngagementRequest(testUser.username, testProposal) + createOtherEngagement(testUser.username, testProposal) # Get the actual saved request from the database (the most recent one) initialOtherExperiences = CCEMinorProposal.select().where(CCEMinorProposal.proposalType == 'Other Engagement', CCEMinorProposal.student == testUser.username) @@ -573,6 +612,52 @@ def test_createOtherEngagementRequest(testUser, testProposal): transaction.rollback() +@pytest.mark.parametrize("testProposal", [ + { + "proposalType": "otherEngagement", + "experienceName": "Assistant to Finn", + "orgName": "Finn's Assistants", + "experienceDescription": "Catering to Finn's every need" + } +], indirect=True) +@pytest.mark.integration +def test_updateOtherEngagementRequest(testUser, testProposal): + with mainDB.atomic() as transaction: + user = testUser + User.create(username="glek", + firstName="kafui", + lastName="gle", + email="kaf@berea.edu", + bnumber="B91111113") + + # Save the requested event to the database + createdOtherEngagementRequest = None + with app.app_context(): + g.current_user = "glek" + createOtherEngagement(user.username, testProposal) + createdOtherEngagementRequest = CCEMinorProposal.select().where( + CCEMinorProposal.student == user, + CCEMinorProposal.proposalType == "Other Engagement" + ).get() + proposalID = createdOtherEngagementRequest.id + + assert createdOtherEngagementRequest.experienceName == "Assistant to Finn" + assert createdOtherEngagementRequest.orgName == "Finn's Assistants" + assert createdOtherEngagementRequest.experienceDescription == "Catering to Finn's every need" + + testProposal.form["experienceName"] = "Opponent to Finn" + testProposal.form["orgName"] = "Finn's Ops" + testProposal.form["experienceDescription"] = "Hating on Finn 24/7" + + updateOtherEngagementRequest(proposalID, testProposal) + + updatedProposal = CCEMinorProposal.get_by_id(proposalID) + assert updatedProposal.experienceName == "Opponent to Finn" + assert updatedProposal.orgName == "Finn's Ops" + assert updatedProposal.experienceDescription == "Hating on Finn 24/7" + + transaction.rollback() + @pytest.mark.parametrize("testProposal", [ {"proposalType": "otherEngagement"} ], indirect=True) @@ -581,33 +666,35 @@ def test_removeProposal(testProposal, testUser): '''creates a test course with all foreign key fields. tests if they can be deleted''' - testProposalId = 999 - with mainDB.atomic() as transaction: - - assert list(CCEMinorProposal.select(CCEMinorProposal.id).where(CCEMinorProposal.id == testProposalId)) == [] - - - testOtherEngagement = CCEMinorProposal.create(id=testProposalId, + testOtherEngagement = CCEMinorProposal.create( student = testUser.username, proposalType = 'Other Engagement', createdBy = testUser.username, - status = 'Pending', - **testProposal + **testProposal.form ) + + testProposalObject = CCEMinorProposal.select().where( + CCEMinorProposal.student == testUser, + CCEMinorProposal.proposalType == "Other Engagement" + ).get() + + testFileName = "proposal.pdf" + testProposalId = testProposalObject.id + assert list(CCEMinorProposal.select().where(CCEMinorProposal.id == testProposalId)) == [testOtherEngagement] # creates a base object for proposal events - proposalFileStorageObject = [FileStorage(filename= "proposal.pdf")] + proposalFileStorageObject = [FileStorage(filename=testFileName)] handledProposalFile = FileHandler(proposalFileStorageObject, proposalId=testProposalId) # uploading a file to proposalattachments - handledProposalFile.saveFiles() + handledProposalFile.saveFiles(testProposalObject) try: - assert AttachmentUpload.select().where(AttachmentUpload.proposal_id == testProposalId, AttachmentUpload.fileName == f"{testProposalId}.pdf").exists() - assert 1 == AttachmentUpload.select().where(AttachmentUpload.proposal_id == testProposalId, AttachmentUpload.fileName == f"{testProposalId}.pdf").count() + assert AttachmentUpload.select().where(AttachmentUpload.proposal_id == testProposalId, AttachmentUpload.fileName == testFileName).exists() + assert 1 == AttachmentUpload.select().where(AttachmentUpload.proposal_id == testProposalId, AttachmentUpload.fileName == testFileName).count() with app.app_context(): g.current_user = testUser.username @@ -615,20 +702,90 @@ def test_removeProposal(testProposal, testUser): assert list(CCEMinorProposal.select().where(CCEMinorProposal.id == testProposalId)) == [] - assert not AttachmentUpload.select().where(AttachmentUpload.proposal_id == testProposalId, AttachmentUpload.fileName == f"{testProposalId}.pdf").exists() - assert 0 == AttachmentUpload.select().where(AttachmentUpload.proposal_id == testProposalId, AttachmentUpload.fileName == f"{testProposalId}.pdf").count() + assert not AttachmentUpload.select().where(AttachmentUpload.proposal_id == testProposalId, AttachmentUpload.fileName == testFileName).exists() + assert 0 == AttachmentUpload.select().where(AttachmentUpload.proposal_id == testProposalId, AttachmentUpload.fileName == testFileName).count() except Exception as e: raise e finally: fileExists = AttachmentUpload.get_or_none(proposal_id = testProposalId) - fullFilePath = handledProposalFile.getFileFullPath(f'{testProposalId}.pdf') + fullFilePath = handledProposalFile.getFileFullPath(testFileName) if fileExists: os.remove(fullFilePath) - transaction.rollback() + transaction.rollback() + +@pytest.mark.parametrize("testProposal", [ + { + "proposalType": "summerExperience", + "experienceType": "Internship", + "totalHours": 301 + } +], indirect=True) +@pytest.mark.integration +def test_updateSummerExperience(testUser, testProposal): + with mainDB.atomic() as transaction: + user = testUser + User.create(username="glek", + firstName="kafui", + lastName="gle", + email="kaf@berea.edu", + bnumber="B91111113") + + # Save the requested event to the database + createdSummerExperience = None + with app.app_context(): + g.current_user = "glek" + createdSummerExperience = createSummerExperience(user.username, ImmutableMultiDict(testProposal.form)) + + proposalID = createdSummerExperience.id + + assert createdSummerExperience.totalHours == 301 + assert createdSummerExperience.experienceType == "Internship" + + testProposal.form["experienceType"] = "Not an internship" + testProposal.form["totalHours"] = 201 + testProposal.form["experienceHoursOver300"] = "" # adding this because the updateSummerExperience tries to pop this key + + updateSummerExperience(proposalID, ImmutableMultiDict(testProposal.form)) + + updatedProposal = CCEMinorProposal.get_by_id(proposalID) + assert updatedProposal.totalHours == 201 + assert updatedProposal.experienceType == "Not an internship" + + transaction.rollback() + +@pytest.mark.parametrize("testProposal", [ + {"proposalType": "otherEngagement"} +], indirect=True) +@pytest.mark.integration +def test_changeProposalStatus(testProposal, testUser): + + with mainDB.atomic() as transaction: + # Create a proposal to update + createdProposal = CCEMinorProposal.create( + student=testUser.username, + proposalType="Other Engagement", + createdBy=testUser.username, + **testProposal.form + ) + + proposalID = createdProposal.id + + newStatus = "Completed" + + with app.app_context(): + g.current_user = testUser.username + changeProposalStatus(proposalID, newStatus) + + updatedProposal = CCEMinorProposal.get_by_id(proposalID) + + assert updatedProposal.status == newStatus + + transaction.rollback() + @pytest.mark.integration def test_declareMinorInterest():