diff --git a/.gitignore b/.gitignore index 9af55ef..b891720 100644 --- a/.gitignore +++ b/.gitignore @@ -51,7 +51,7 @@ WAM/.ropeproject/history WAM/.ropeproject/objectdb WAM/local_settings.py .DS_Store -django.log +*.log /WAM/local *THINKINGS.txt* *.key diff --git a/WAM/settings.py b/WAM/settings.py index 6746d05..ba7e341 100644 --- a/WAM/settings.py +++ b/WAM/settings.py @@ -130,6 +130,8 @@ # https://docs.djangoproject.com/en/1.8/howto/static-files/ STATIC_URL = '/static/' +# This ensures local files are searched first +STATICFILES_DIRS = [os.path.join(BASE_DIR, 'loads/static')] # Where do we store media MEDIA_ROOT = os.path.join(BASE_DIR, 'media') diff --git a/loads/forms.py b/loads/forms.py index 520a2a1..9f95235 100644 --- a/loads/forms.py +++ b/loads/forms.py @@ -129,6 +129,13 @@ class LoadsByModulesForm(forms.Form): ) +class LoadChartsForm(forms.Form): + """ This exposes some options for load charts views """ + + show_90_110 = forms.BooleanField(required=False, initial=False, help_text='Show 90% and 110% boundaries') + sort_by_load = forms.BooleanField(required=False, initial=True) + + class ModulesIndexForm(forms.Form): """This prompts for comma separated semesters used for some restrictions""" semesters = forms.CharField( @@ -464,11 +471,14 @@ def save(self): class BaseModuleStaffByStaffFormSet(FancyModelFormSet): + """ Enables altering teaching allocation for a member of staff from staff views """ + def clean(self): """ - Adds validation to check that no two links have the same anchor or URL - and that all links have both an anchor and URL. + Adds validation to check that no module is in the list twice, or that combined percentages exceed 100 """ + + # Don't validate the whole formset (yet) if individual forms have issues if any(self.errors): return @@ -508,19 +518,23 @@ def clean(self): code='invalid_assessment_proportion' ) - if duplicates: - raise forms.ValidationError( - 'Modules should not appear more than once.', - code='duplicate_modules' - ) + if duplicates: + raise forms.ValidationError( + 'Modules should not appear more than once.', + code='duplicate_modules' + ) class BaseModuleStaffByModuleFormSet(FancyModelFormSet): + """ Enables altering teaching allocation for a module from module views """ + def clean(self): """ - Adds validation to check that no two links have the same anchor or URL - and that all links have both an anchor and URL. + Adds overall validation to check that no staff member is in the list twice, + or that combined percentages exceed 100 """ + + # Don't validate the whole formset (yet) if individual forms have issues if any(self.errors): return @@ -567,23 +581,62 @@ def clean(self): code='invalid_assessment_proportion' ) - if duplicates: - raise forms.ValidationError( - 'Staff members should not appear more than once.', - code='duplicate_staff' - ) - if contact_total > 100: - raise forms.ValidationError( - 'Contact proportions are over 100%', - code='invalid_contact_total' - ) - if admin_total > 100: - raise forms.ValidationError( - 'Admin proportions are over 100%', - code='invalid_contact_total' - ) - if assessment_total > 100: - raise forms.ValidationError( - 'Assessment proportions are over 100%', - code='invalid_contact_total' - ) + if duplicates: + raise forms.ValidationError( + 'Staff members should not appear more than once.', + code='duplicate_staff' + ) + + if contact_total > 100: + raise forms.ValidationError( + 'Contact proportions are over 100%', + code='invalid_contact_total' + ) + + if admin_total > 100: + raise forms.ValidationError( + 'Admin proportions are over 100%', + code='invalid_contact_total' + ) + + if assessment_total > 100: + raise forms.ValidationError( + 'Assessment proportions are over 100%', + code='invalid_contact_total' + ) + + +class BaseProjectStaffFormSet(FancyModelFormSet): + """ Enables altering project allocations """ + + def clean(self): + """ + Adds overall validation to check that no staff member is in the list twice + """ + + # Don't validate the whole formset (yet) if individual forms have issues + if any(self.errors): + return + + staff_members = [] + + duplicates = False + + for form in self.forms: + # If the form is deleted, don't validate, its data is about to be nuked + if form in self.deleted_forms: + continue + + if form.cleaned_data: + staff = form.cleaned_data['staff'] + + if staff in staff_members: + duplicates = True + staff_members.append(staff) + + if duplicates: + raise forms.ValidationError( + 'Staff members should not appear more than once.', + code='duplicate_staff' + ) + diff --git a/loads/static/fancy_formset/README.TXT b/loads/static/fancy_formset/README.TXT new file mode 100644 index 0000000..0b74b9c --- /dev/null +++ b/loads/static/fancy_formset/README.TXT @@ -0,0 +1,11 @@ +WARNING! + +This file is here because there is an apparent bug in fancy_formset 1.0.0, which seems to decrement total forms on +a form deletion. This does not appear to be correct behaviour, and can be diagnosed from trying to delete a single +remaining formset that was already in the model layer. + +This file includes a patched version of the library that will be loaded in preference if STATICFILES_DIR is set +up correctly. + +This file / directory needs to be removed if and when the library is patched, or this local copy will continue to +override the main library. \ No newline at end of file diff --git a/loads/static/fancy_formset/formset.js b/loads/static/fancy_formset/formset.js new file mode 100644 index 0000000..75d4c81 --- /dev/null +++ b/loads/static/fancy_formset/formset.js @@ -0,0 +1 @@ +(function(r,o){typeof exports=="object"&&typeof module<"u"?o(exports):typeof define=="function"&&define.amd?define(["exports"],o):(r=typeof globalThis<"u"?globalThis:r||self,o(r.formset={}))})(this,function(r){"use strict";var u=Object.defineProperty;var c=(r,o,n)=>o in r?u(r,o,{enumerable:!0,configurable:!0,writable:!0,value:n}):r[o]=n;var a=(r,o,n)=>(c(r,typeof o!="symbol"?o+"":o,n),n);class o{constructor(t,s,e){a(this,"templatePrefix","__prefix__");a(this,"_namePattern");this.formset=t,this.rootEl=s,this.options=e,this._namePattern=new RegExp(`^${e.prefix}-([^-]+)-(.+)$`),this.isTemplate=this.getFields()[0].name.match(this._namePattern)[1]===this.templatePrefix,this.deleteEl=this.getDeleteEl(),this.render()}getFields(){return Array.from(this.rootEl.querySelectorAll("input,select,textarea"))}getValues(){return this.getFields().reduce((s,e,i)=>(s[e.name.match(this._namePattern)[2]]=e.value,s),{})}hasContent(t){return this.getFields().some((i,m)=>{const h=i.name.match(this._namePattern)[2];if(h===this.options.pkFieldName){if(i.value)return!0}else return i.value!==t[h]})}getDeleteEl(){var s;let t=this.rootEl.querySelector('[name$="-DELETE"]');if(t)return t.addEventListener("click",e=>{(this.isDeleted&&this.formset.atMin&&!this.options.allowDeleteAtMin||!this.isDeleted&&this.formset.atMax&&!this.options.allowAddAtMax)&&(e.preventDefault(),e.stopPropagation())}),t.addEventListener("change",()=>{t.checked?this.deleted():this.undeleted()}),(s=t.closest(this.options.deleteConClosest))==null||s.classList.add(this.options.deleteConCss),t}get isDeleted(){var t;return((t=this.deleteEl)==null?void 0:t.checked)===!0}deleted(){this.formset.deactivateForm(this)}undeleted(){this.formset.activateForm(this)}render(){this.rootEl.classList.toggle(this.options.formDeletedCss,this.isDeleted)}destroy(){this.rootEl.remove(),this.formset.destroyedForm(this),this.formset=null}}class n{constructor(t,s){a(this,"formClass",o);this.rootEl=t;let e=s.prefix||t.getAttribute(s.prefixAttr);if(!e)throw this._e("Formset prefix not found");this.options=s={...s,prefix:e},this.totalFormsEl=document.getElementById(`id_${e}-TOTAL_FORMS`),this.initialFormsEl=document.getElementById(`id_${e}-INITIAL_FORMS`),this.numFormsMin=parseInt(document.getElementById(`id_${e}-MIN_NUM_FORMS`).value,10),this.numFormsMax=parseInt(document.getElementById(`id_${e}-MAX_NUM_FORMS`).value,10),this.addEl=this.getAddEl(),this.collectForms(),this.rootEl.classList.add(this.options.formsetActiveCss),this.render(),this.event("formset:init")}_e(t){return new Error(t,{cause:this.rootEl})}collectForms(){this.forms=Array.from(this.rootEl.querySelectorAll(this.options.formSelector),e=>new this.formClass(this,e,this.options));let t=this.forms.findIndex(e=>e.isTemplate);if(t>-1)this.template=this.forms.splice(t,1)[0];else throw this._e(`Formset ${this.options.prefix} template form not found`);this.numForms=this.forms.length;const s=parseInt(this.initialFormsEl.value,10);if(this.numForms>s){const e=this.template.getValues();this.forms.slice().reverse().forEach(i=>{this.atMin||i.hasContent(e)||i.destroy()})}this._nextId=this.numForms}getAddEl(){let t=document.createElement("button");return t.innerHTML=this.options.addButtonLabel,t.type="button",t.className=this.options.addButtonCss,this.rootEl.appendChild(t),t.onclick=()=>{this.addForm()},t}addForm(){if(this.atMax&&!this.options.allowAddAtMax)return;let t=this._nextId++,s=this.createForm(t),e=this.insertForm(t,s);this.activateForm(e)}render(){this.forms.forEach(t=>{t.render()}),this.rootEl.classList.toggle(this.options.formsetAtMinCss,this.atMin),this.rootEl.classList.toggle(this.options.formsetAtMaxCss,this.atMax)}get numForms(){return parseInt(this.totalFormsEl.value,10)}set numForms(t){this.totalFormsEl.value=t}createForm(t){let e=this.template.rootEl.innerHTML.replace(/__prefix__/g,t),i=this.template.rootEl.cloneNode();return i.removeAttribute("style"),i.classList.add(this.options.formAddedCss),i.innerHTML=e,i}insertForm(t,s){let e=this.template;this.forms.length>0&&(e=this.forms[this.forms.length-1]);let i=e.rootEl;i.parentNode.insertBefore(s,i.nextSibling);let m=new o(this,s,this.options);return this.forms.push(m),this.event("formset:form-add",m),m}event(t,s){const e={formset:this,form:s};this.rootEl.dispatchEvent(new CustomEvent(t,{bubbles:!0,detail:e}))}activateForm(t){this.numForms+=1,this.render(),this.event("formset:form-activate",t)}deactivateForm(t){this.render(),this.event("formset:form-deactivate",t)}destroyedForm(t){this.forms.splice(this.forms.indexOf(t),1),this.numForms-=1,this.render(),this.event("formset:form-destroy",t)}get atMax(){return this.numForms>=this.numFormsMax}get atMin(){return this.numForms<=this.numFormsMin}}const d={formsetSelector:"[data-formset]",formsetClass:n,prefix:null,prefixAttr:"data-formset",formSelector:":scope > fieldset",formClass:o,pkFieldName:"id",formsetActiveCss:"formset-active",formAddedCss:"added",formDeletedCss:"deleted",addButtonLabel:"Add",addButtonCss:"formset-add",deleteConClosest:"p,div,tr,li",deleteConCss:"formset-delete",formsetAtMinCss:"formset-at-min",allowDeleteAtMin:!1,formsetAtMaxCss:"formset-at-max",allowAddAtMax:!1};function f(l={},t=null){l={...d,...l},t=t||l.formsetSelector;let s;return t instanceof Array?s=t:t instanceof HTMLElement?s=[t]:(t&&!(t instanceof NodeList)&&(t=document.querySelectorAll(t)),s=Array.from(t)),s.map(i=>new l.formsetClass(i,l))}r.Form=o,r.Formset=n,r.defaultOptions=d,r.init=f,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"})}); diff --git a/loads/templates/loads/loads_charts.html b/loads/templates/loads/loads_charts.html index b03c7bc..cf6e6c3 100644 --- a/loads/templates/loads/loads_charts.html +++ b/loads/templates/loads/loads_charts.html @@ -40,6 +40,77 @@
No allocations for this group at this time
+