def delete_filetype(request, pk): """ Delete a file type. :param request: :param pk: id of the file type :return: """ obj = get_object_or_404(FileType, pk=pk) if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): obj.delete() return render( request, 'base.html', { 'Message': 'File type deleted.', 'return': 'professionalskills:filetypelist', }) else: form = ConfirmForm() return render( request, 'GenericForm.html', { 'form': form, 'formtitle': 'Confirm deletion of File type {}'.format(obj), 'buttontext': 'Confirm' })
def terms_form(request): """ Form for a user to accept the terms of use. :param request: :return: """ try: # already accepted, redirect user to login obj = request.user.termsaccepted if obj.Stamp <= timezone.now(): return HttpResponseRedirect('/') except UserAcceptedTerms.DoesNotExist: pass if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): # user accepted terms. Possible already accepted terms in a parallel session, so get_or_create. UserAcceptedTerms.objects.get_or_create(User=request.user) return HttpResponseRedirect('/') else: form = ConfirmForm() return render(request, 'index/terms.html', { 'form': form, 'formtitle': 'I have read and accepted the Terms of Services', 'buttontext': 'Confirm', 'terms': Term.objects.all() })
def delete_filetype_aspect(request, pk): """ Delete a file type grade aspect. :param request: :param pk: id of the file type aspect :return: """ obj = get_object_or_404(StaffResponseFileAspect, pk=pk) pk = obj.File.pk # store original linked File if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): obj.delete() return render( request, 'base.html', { 'Message': 'File type aspect deleted.', 'return': 'professionalskills:filetypeaspects', 'returnget': pk, }) else: form = ConfirmForm() return render( request, 'GenericForm.html', { 'form': form, 'formtitle': 'Confirm deletion of file type aspect {}'.format(obj), 'buttontext': 'Confirm' })
def delete_aspect(request, pk): """ :param request: :param pk: :return: """ aspect = get_object_or_404(GradeCategoryAspect, pk=pk) cat = aspect.Category.id if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): aspect.delete() return render( request, 'base.html', { 'Message': 'Grade category aspect deleted.', 'return': 'results:list_aspects', 'returnget': cat }) else: form = ConfirmForm() return render( request, 'GenericForm.html', { 'form': form, 'formtitle': 'Delete grade category aspect?', 'buttontext': 'Confirm' })
def mail_track_heads(request): """ Mail all track heads with their actions :param request: :return: """ if get_timephase_number() > 2: return render(request, "base.html", {"Message": "Only possible in first two phases"}) if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): mail_track_heads_pending() return render(request, "base.html", {"Message": "Track Heads mailed!"}) else: form = ConfirmForm() trackstats = {} for track in Track.objects.all(): trackstats[str(track)] = { 'pending': get_all_proposals().filter(Q(Status=3) & Q(Track=track)).count(), 'head': track.Head.email, } return render(request, "support/TrackHeadSendConfirm.html", { 'trackstats': trackstats, 'form': form })
def copy(request, pk=None): """ Show a list of timeslots to import grades from. :param request: :param pk: :return: """ # do a copy if pk: ts = get_object_or_404(TimeSlot, pk=pk) if ts == get_timeslot(): raise PermissionDenied( "It is not possible to copy the grades from the current timeslot." ) if get_timeslot().gradecategories.exists(): return render( request, 'base.html', { 'Message': "The current timeslot already has grade categories." " Importing is not possible. " "Please remove the categories in the current timeslot before copying.", 'return': 'results:list_categories' }) if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): for cat in ts.gradecategories.all(): old_id = cat.id old_aspects = cat.aspects.all() cat.id = None cat.TimeSlot = get_timeslot() cat.save() for aspect in old_aspects: aspect.id = None aspect.Category = cat aspect.save() return render( request, 'base.html', { 'Message': 'Finished importing!', 'return': 'results:list_categories' }) else: form = ConfirmForm() return render( request, 'GenericForm.html', { 'form': form, 'formtitle': 'Confirm copy grade categories and aspects', 'buttontext': 'Confirm' }) # list possible timeslots to copy from else: tss = TimeSlot.objects.filter(gradecategories__isnull=False).distinct() return render(request, "results/list_copy.html", { "tss": tss, 'ts': get_timeslot(), })
def confirm_mailing(request): if request.method == 'POST': mailing_obj = get_object_or_404(Mailing, id=request.POST.get('mailingid', None)) form = ConfirmForm(request.POST) if form.is_valid(): # loop over all collected email addresses to create a message mails = [] for recipient in mailing_obj.RecipientsStaff.all( ) | mailing_obj.RecipientsStudents.all(): mails.append({ 'template': 'email/supportstaff_email.html', 'email': recipient.email, 'subject': mailing_obj.Subject, 'context': { 'message': mailing_obj.Message, 'name': recipient.usermeta.get_nice_name(), } }) EmailThreadTemplate(mails).start() mailing_obj.Sent = True mailing_obj.save() return render(request, "support/email_progress.html") raise PermissionDenied('The confirm checkbox was unchecked.') raise PermissionDenied("No post data supplied!")
def delete_file(request, pk): """ Delete a public file :param request: :param pk: pk of the proposal to delete :return: """ obj = get_object_or_404(PublicFile, pk=pk) if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): obj.delete() return render(request, "base.html", { "Message": "Public file removed!", "return": "index:index" }) else: form = ConfirmForm() return render( request, 'GenericForm.html', { 'form': form, 'formtitle': 'Confirm deleting public file {}'.format(obj), 'buttontext': 'Confirm' })
def delete_random_distributions(request): """ Delete all distributions who have had a random assigned project :param request: :return: """ dists = Distribution.objects.filter(TimeSlot=get_timeslot(), Application__isnull=True, Proposal__Private__isnull=True).order_by('Student') if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): dists.delete() return render(request, 'base.html', { 'Message': 'Distributions deleted!' }) else: form = ConfirmForm() return render(request, 'distributions/delete_random_dists.html', { 'form': form, 'buttontext': 'Confirm', 'formtitle': 'Confirm deletion distributions of random assigned projects', 'distributions': dists, })
def students_confirm(request): """ Add a list of students to a specific timeslot :param request: :return: """ if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): data = json.loads(request.POST.get('jsondata', None)) if not data: raise PermissionDenied('Invalid data.') ts = get_object_or_404(TimeSlot, pk=data.get('ts')) cnt = 0 for student in data['students']: s = get_object_or_404(User, pk=student, groups=None) s.usermeta.TimeSlot.add(ts) s.usermeta.save() cnt += 1 return render(request, 'base.html', { 'Message': f'{cnt} students set to {ts}.', }) else: return render(request, 'base.html', { 'Message': 'Please check confirm.', }) else: raise PermissionDenied('Invalid request.')
def delete_timephase(request, timephase): """ :param request: :param timephase: pk of timephase to delete :return: """ name = 'Time phase' obj = get_object_or_404(TimePhase, pk=timephase) if obj.End < datetime.now().date(): raise PermissionDenied('This TimePhase has already finished.') if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): obj.delete() return render( request, 'base.html', { 'Message': '{} deleted.'.format(name), 'return': 'timeline:list_timephases', 'returnget': obj.TimeSlot.pk }) else: form = ConfirmForm() return render( request, 'GenericForm.html', { 'form': form, 'formtitle': 'Delete {}?'.format(name), 'buttontext': 'Delete' })
def delete_category(request, pk): """ :param request: :param pk: :return: """ cat = get_object_or_404(GradeCategory, pk=pk) if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): for aspect in cat.aspects.all(): aspect.delete() cat.delete() return render(request, 'base.html', { 'Message': 'Grade category deleted.', 'return': 'results:list_categories', }) else: form = ConfirmForm() return render(request, 'GenericForm.html', { 'form': form, 'formtitle': 'Delete grade category?', 'buttontext': 'Confirm' })
def delete_mailing_template(request, pk): """ :param request: :param pk: pk of template :return: """ name = 'Mailing template' obj = get_object_or_404(MailTemplate, pk=pk) if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): obj.delete() return render( request, 'base.html', { 'Message': '{} deleted.'.format(name), 'return': 'support:mailingtemplates' }) else: form = ConfirmForm() return render( request, 'GenericForm.html', { 'form': form, 'formtitle': 'Delete {}?'.format(name), 'buttontext': 'Delete' })
def osirisToMeta(request): write_errors = [] try: data, log = read_osiris_xlsx() except: return render( request, 'base.html', { 'Message': 'Retrieving Osirisdata failed. Please upload a valid file.', 'return': 'index:index', }) if request.method == 'POST': count = 0 form = ConfirmForm(request.POST) if form.is_valid(): for p in data: try: user = User.objects.get(email=p.email) except User.DoesNotExist: write_errors.append('User {} skipped'.format(p.email)) continue try: meta = user.usermeta except UserMeta.DoesNotExist: meta = UserMeta() user.usermeta = meta meta.save() user.save() if p.automotive: meta.Study = 'Automotive' else: meta.Study = 'Electrical Engineering' meta.Cohort = p.cohort meta.ECTS = p.ects meta.Studentnumber = p.idnumber meta.save() count += 1 return render( request, 'base.html', { 'Message': mark_safe('User meta updated for {} users. <br />'.format( count) + print_list(write_errors)), 'return': 'osirisdata:list' }) else: form = ConfirmForm() return render( request, 'osirisdata/osiris_to_meta_form.html', { 'form': form, 'formtitle': 'Confirm write to usermeta', 'buttontext': 'Confirm' })
def assign(request, pk): """ Assign all distributed students to one of the prv groups. :param request: :param pk: :return: """ filetype = get_object_or_404(FileType, pk=pk) if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): if filetype.groups.all().aggregate( Sum('Max'))['Max__sum'] < get_all_students().count(): return render( request, 'base.html', { 'Message': 'Groups capacity not sufficient. Groups are not changed.', 'return': 'professionalskills:listgroups', 'returnget': filetype.id }) for group in filetype.groups.all(): group.Members.clear() group.save() students = list(get_all_students()) totalstudents = len(students) random.shuffle(students) groups = list(filetype.groups.all()) totaldistributed = 0 while totaldistributed < totalstudents: for g in [g for g in groups if g.Members.count() < g.Max]: try: g.Members.add(students.pop(0)) totaldistributed += 1 except IndexError: break for g in groups: g.save() return render( request, 'base.html', { 'Message': 'Students divided over the groups.', 'return': 'professionalskills:listgroups', 'returnget': filetype.id, }) else: form = ConfirmForm() return render( request, 'GenericForm.html', { 'form': form, 'formtitle': 'Confirm reshuffling students for {}'.format(filetype), 'buttontext': 'Confirm' })
def mail_overdue_students(request): """ Mail students that didn't handin file before the deadline :param request: :return: """ timeslot = get_timeslot() prvs = FileType.objects.filter(TimeSlot=timeslot) dists = get_distributions(request.user, timeslot=timeslot) if request.method == "POST": form = ConfirmForm(request.POST) if form.is_valid(): mails = [] for dist in dists: missingtypes = [] for ftype in prvs: if ftype.deadline_passed() and not dist.files.filter( Type=ftype).exists(): missingtypes.append(ftype) if len(missingtypes) > 0: mails.append({ 'template': 'email/overdueprvstudent.html', 'email': dist.Student.email, 'subject': 'Overdue professional skill delivery', 'context': { 'student': dist.Student, 'project': dist.Proposal, 'types': missingtypes, } }) EmailThreadTemplate(mails).start() return render(request, "support/email_progress.html") else: form = ConfirmForm() # preview list of students. students = [] for dist in dists: missing = False for ftype in prvs: if ftype.deadline_passed() and not dist.files.filter( Type=ftype).exists(): missing = True break if missing: students.append(dist.Student) return render( request, 'professionalskills/overdueprvform.html', { 'form': form, 'formtitle': 'Confirm mailing overdue students', 'buttontext': 'Confirm', 'students': students, })
def copy(request, pk, from_pk=None): """ Show a list of timeslots to import rubrics from. :param request: :param pk: prv to copy grades to :param from_pk: prv to copy grades from :return: """ prv = get_object_or_404(FileType, pk=pk) if prv.TimeSlot.is_finished(): raise PermissionDenied('Old prvs cannot be changed.') if from_pk: # do copy from_prv = get_object_or_404(FileType, pk=from_pk) if not from_prv.CheckedBySupervisor or not from_prv.aspects.exists(): raise PermissionDenied("This file has no grading aspects.") if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): for aspect in from_prv.aspects.all(): aspect.id = None aspect.File = prv aspect.save() return render( request, 'base.html', { 'Message': 'Finished importing!', 'return': 'professionalskills:list_aspects', 'returnget': prv.pk }) else: form = ConfirmForm() return render( request, 'GenericForm.html', { 'form': form, 'formtitle': f'Confirm import aspcects to {prv.Name}', 'buttontext': 'Confirm' }) else: # show options list prvs = FileType.objects.filter( CheckedBySupervisor=True, aspects__isnull=False).exclude(pk=pk).distinct() return render(request, "professionalskills/list_copy.html", { "prvs": prvs, 'prv': prv })
def delete_capacity_group(request, pk): obj = get_object_or_404(CapacityGroup, pk=pk) if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): obj.delete() return render( request, 'base.html', { 'Message': 'Capacity group {} deleted.'.format(obj), 'return': 'support:listcapacitygroups' }) else: form = ConfirmForm() return render( request, 'GenericForm.html', { 'form': form, 'formtitle': 'Confirm deletion of {}'.format(obj), 'buttontext': 'Delete' })
def mail_overdue_students(request): """ Mail students that didn't handin file before the deadline :param request: :return: """ if request.method == "POST": form = ConfirmForm(request.POST) if form.is_valid(): mails = [] for dist in get_distributions(request.user): missingtypes = [] for ftype in FileType.objects.all(): if ftype.Deadline >= datetime.today().date(): continue # only mail if the deadline has passed. if dist.files.filter(Type=ftype).count() == 0: missingtypes.append(ftype) if len(missingtypes) > 0: mails.append({ 'template': 'email/overdueprvstudent.html', 'email': dist.Student.email, 'subject': 'Overdue professional skill delivery', 'context': { 'student': dist.Student, 'project': dist.Proposal, 'types': missingtypes, } }) EmailThreadTemplate(mails).start() return render(request, "support/email_progress.html") else: form = ConfirmForm() return render( request, 'GenericForm.html', { "form": form, "formtitle": "Confirm mailing overdue students", "buttontext": "Confirm" })
def students_applied(request): """ Add students who applied in this timeslot to this timeslot :param request: :return: """ timeslot = get_timeslot() if not timeslot: return render( request, 'base.html', context={ 'Message': 'There is no current active timeslot. This page is not available.' }) students = User.objects.filter( groups=None, applications__Proposal__TimeSlot=timeslot).exclude( usermeta__TimeSlot=timeslot).distinct() if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): for s in students: s.usermeta.TimeSlot.add(timeslot) s.usermeta.save() return render(request, 'base.html', context={'Message': 'Students added to timeslot.'}) else: form = ConfirmForm() return render( request, 'timeline/timeslot_applied_users_form.html', { 'form': form, 'formtitle': f'Add students to timeslot {timeslot}', 'students': students, 'timeslot': timeslot, 'buttontext': 'Confirm' })
def osirisToMeta(request): if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): data = osirisData() for p in data.getalldata(): try: user = User.objects.get(email=p.email) except User.DoesNotExist: continue try: meta = user.usermeta except UserMeta.DoesNotExist: meta = UserMeta() user.usermeta = meta meta.save() user.save() if p.automotive: meta.Study = 'Automotive' else: meta.Study = 'Eletrical Engineering' meta.Cohort = p.cohort meta.ECTS = p.ects meta.Studentnumber = p.idnumber meta.save() return render(request, 'base.html', { 'Message': 'usermeta updated!', 'return': 'osirisdata:list' }) else: form = ConfirmForm() return render( request, 'GenericForm.html', { 'form': form, 'formtitle': 'Confirm rewrite usermeta', 'buttontext': 'Confirm' })
def mail_distributions(request): """ Mail all distributions to affected users :param request: :return: """ if get_timephase_number() < 4 or get_timephase_number() > 5: # mailing is possible in phase 4 or 5 raise PermissionDenied('Mailing distributions is not possible in this timephase') if request.method == 'POST': form = ConfirmForm(request.POST) if form.is_valid(): mails = [] ts = get_timeslot() # iterate through projects, put students directly in the mail list for prop in get_all_proposals().filter(Q(distributions__isnull=False)): for dist in prop.distributions.filter(TimeSlot=ts): mails.append({ 'template': 'email/studentdistribution.html', 'email': dist.Student.email, 'subject': 'distribution', 'context': { 'project': prop, 'student': dist.Student, } }) # iterate through all assistants and responsible for usr in get_all_staff().filter(Q(groups__name='type1staff') | Q(groups__name='type2staff')): if usr.proposals.filter(TimeSlot=get_timeslot()).exists(): mails.append({ 'template': 'email/assistantdistribution.html', 'email': usr.email, 'subject': 'distribution', 'context': { 'supervisor': usr, 'projects': usr.proposals.filter(TimeSlot=get_timeslot()).distinct(), } }) if usr.proposalsresponsible.filter(TimeSlot=get_timeslot()).exists(): mails.append({ 'template': 'email/supervisordistribution.html', 'email': usr.email, 'subject': 'distribution', 'context': { 'supervisor': usr, 'projects': usr.proposalsresponsible.filter(TimeSlot=get_timeslot()).distinct(), } }) # UNCOMMENT THIS TO MAIL NOT_DISTRIBUTED STUDENTS WITH 'action required' # for usr in get_all_students().filter(distributions__isnull=True): # mails.append({ # 'template': 'email/studentnodistribution.html', # 'email': usr.email, # 'subject': 'action required', # 'context': { # 'student': usr, # } # }) EmailThreadTemplate(mails).start() return render(request, 'support/email_progress.html') else: form = ConfirmForm() return render(request, 'GenericForm.html', { 'form': form, 'formtitle': 'Confirm mailing distributions', 'buttontext': 'Confirm' })
def automatic(request, dist_type, distribute_random=1, automotive_preference=1): """ After automatic distribution, this pages shows how good the automatic distribution is. At this point a type3staff member can choose to apply the distributions. Later, this distribution can be edited using manual distributions. :param request: :param dist_type: which type automatic distribution is used. :param distribute_random: Whether to distribute leftover students to random projects :param automotive_preference: Distribute automotive students first to automotive people :return: """ if get_timephase_number() < 4 or get_timephase_number() > 5: # 4 or 5 raise PermissionDenied('Distribution is not possible in this timephase') if int(dist_type) == 1: typename = 'calculated by student' elif int(dist_type) == 2: typename = 'calculated by project' else: raise PermissionDenied("Invalid type") if distribute_random not in [0, 1]: raise PermissionDenied("Invalid option type random") if automotive_preference not in [0, 1]: raise PermissionDenied("Invalid option type automotive") dists = [] # list to store actual user and proposal objects, instead of just ID's like distobjs. if request.method == 'POST': jsondata = request.POST.get('jsondata', None) if jsondata is None: return render(request, 'base.html', {'Message': 'Invalid POST data'}) distobjs = json.loads(jsondata) # json blob with all dists with studentID projectID and preference for obj in distobjs: dists.append({ 'student': User.objects.get(pk=obj['StudentID']), 'proposal': get_all_proposals().get(pk=obj['ProjectID']), 'preference': obj['Preference'], }) form = ConfirmForm(request.POST) if form.is_valid(): # delete all old distributions of this timeslot Distribution.objects.filter(TimeSlot=get_timeslot()).delete() # save the stuff for dist in dists: dstdbobj = Distribution() dstdbobj.Student = dist['student'] dstdbobj.Proposal = dist['proposal'] dstdbobj.TimeSlot = get_timeslot() if dist['preference'] > 0: try: dstdbobj.Application = \ Application.objects.filter(Q(Student=dist['student']) & Q(Proposal=dist['proposal']) & Q(Priority=dist['preference']) & Q(Proposal__TimeSlot=get_timeslot()))[0] except Application.DoesNotExist: dstdbobj.Application = None else: dstdbobj.Application = None dstdbobj.save() return render(request, 'base.html', { 'Message': 'Distributions saved!', 'return': 'distributions:SupportListApplicationsDistributions', }) else: # Handle the most common errors beforehand with a nice message error_list = '' stds = get_all_students().filter(personal_proposal__isnull=False, personal_proposal__TimeSlot=get_timeslot()).distinct().prefetch_related('personal_proposal') for u in stds: # all private students ps = u.personal_proposal.filter(TimeSlot=get_timeslot()) if ps.count() > 1: # more than one private proposal. error_list += "<li>User {} has multiple private proposals ({}). Please resolve this!</li>" \ .format(u, print_list(ps)) if ps.first().Status != 4: error_list += "<li>User {} has private proposals which is not yet public. Please upgrade proposal {} to public!</li>" \ .format(u, ps.first()) if error_list: return render(request, 'base.html', { 'Message': '<h1>Automatic distribution cannot start</h1><p>The following error(s) occurred:</p><ul>{}</ul>' .format(error_list), 'return': 'distributions:SupportListApplicationsDistributions', }) form = ConfirmForm() # run the algorithms # catch any remaining errors of the algorithm with a broad exception exception. try: if int(dist_type) == 1: # from student distobjs = distribution.calculate_1_from_student( distribute_random=distribute_random, automotive_preference=automotive_preference) elif int(dist_type) == 2: # from project distobjs = distribution.calculate_2_from_project( distribute_random=distribute_random, automotive_preference=automotive_preference) # invalid types are catched at the begin of the function except Exception as e: return render(request, "base.html", { 'Message': '<h1>Automatic distribution cannot start</h1><p>The following error(s) occurred:</p>{}' .format(e)}) # convert to django models from db # and make scatter chart data scatter = [] undistributed = list(get_all_students()) # get all students and remove all distributed students later. for obj in distobjs: # distobjs is saved as json blob in the view, to use later on for distributions. student = get_all_students().get(pk=obj.StudentID) undistributed.remove(student) # remove distributed student from undistributed students list. scatter.append({ # data for scatterplot ECTS vs Preference 'x': student.usermeta.ECTS, 'y': obj.Preference, }) try: proposal = get_all_proposals().get(pk=obj.ProjectID) except Proposal.DoesNotExist: raise Exception("Proposal id {} cannot be found in all_proposals!".format(obj.ProjectID)) dists.append({ 'student': student, 'proposal': proposal, 'preference': obj.Preference, }) # show undistributed students also in the table. (Not added to json blob) for obj in undistributed: dists.append({ 'student': obj, 'proposal': None, 'preference': 'Not distributed. ' + ('With' if obj.applications.filter(Proposal__TimeSlot=get_timeslot()).exists() else 'No') + ' applications.', }) cohorts = distribution.get_cohorts() # make headers for table and find cohort for each student. columns = ['Total'] # calculate stats per cohort prefs = { # first column with total 'Total': [obj['preference'] for obj in dists if obj['proposal'] is not None], } for c in cohorts: # all next columns in table for each cohort. First column is totals. columns.append(c) prefs[c] = [] for obj in dists: if obj['proposal'] is not None: # do not count not-distributed students in the stats. if obj['student'].usermeta.Cohort == int(c): prefs[c].append(obj['preference']) # list of all individual preferences in this cohort # make a table table = [] pref_options = list(range(-1, settings.MAX_NUM_APPLICATIONS + 1)) # all options for preference. for pref in pref_options: # each preference, each row. # set first column if pref == -1: # random distributed students. this_row = ['Random'] elif pref == 0: this_row = ['Private'] # private proposals else: this_row = ['#' + str(pref), ] # application preference # set other columns for c in columns: # add columns to the row. num = prefs[c].count(pref) try: this_row.append('{}% ({})'.format(round(num / len(prefs[c]) * 100), num)) except ZeroDivisionError: this_row.append('{}% ({})'.format(0, 0)) # add row the the table table.append(this_row) # one but last row with totals. this_row = ['Total Distributed'] for c in columns: this_row.append(len(prefs[c])) table.append(this_row) # last row with undistributed. this_row = ['Not Distributed', len(undistributed)] for c in columns[1:]: # skip total column, is already added. this_row.append(len([u for u in undistributed if u.usermeta.Cohort == c])) table.append(this_row) # show the tables for testing. if settings.TESTING: return columns, table, dists data = [obj.as_json() for obj in distobjs] return render(request, 'distributions/automatic_distribute.html', { 'typename': typename, 'distributions': dists, 'form': form, 'stats': table, 'stats_header': columns, 'scatter': scatter, 'jsondata': json.dumps(data), 'distribute_random': distribute_random, 'automotive_preference': automotive_preference, })