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, })
def mailing(request, pk=None): """ Mailing list to mail users. :param request: :param pk: optional key of a mailing template :return: """ if request.method == 'POST': form = ChooseMailingList( request.POST, staff_options=mail_staff_options, student_options=mail_student_options, ) if form.is_valid(): recipients_staff = set() recipients_students = set() # staff if form.cleaned_data['SaveTemplate']: t = MailTemplate( RecipientsStaff=json.dumps(form.cleaned_data['Staff']), RecipientsStudents=json.dumps( form.cleaned_data['Students']), Message=form.cleaned_data['Message'], Subject=form.cleaned_data['Subject'], ) t.save() ts = form.cleaned_data['TimeSlot'] for s in form.cleaned_data['Staff']: try: # staff selected by group recipients_staff.update( Group.objects.get(name=s).user_set.all()) if s not in [ 'type3staff', 'type4staff', 'type5staff', 'type6staff' ]: # if user group is not a support type, mail only users with project in this year. for staff in list(recipients_staff): if not staff.proposalsresponsible.filter( TimeSlot=ts).exists( ) and not staff.proposals.filter( TimeSlot=ts).exists(): # user has no project in the selected timeslots. recipients_staff.remove(staff) except Group.DoesNotExist: # not a group object, staff selected by custom options. projs = get_all_proposals(old=True).filter(TimeSlot=ts) if s == 'staffnonfinishedproj': for proj in projs.filter(Status__lt=3).distinct(): recipients_staff.add(proj.ResponsibleStaff) recipients_staff.update(proj.Assistants.all()) elif s == 'distributedstaff': for proj in projs.filter( distributions__isnull=False).distinct(): recipients_staff.add(proj.ResponsibleStaff) recipients_staff.update(proj.Assistants.all()) elif s == 'staffnostudents': for proj in projs.filter( distributions__isnull=True).distinct(): recipients_staff.add(proj.ResponsibleStaff) recipients_staff.update(proj.Assistants.all()) elif s == 'assessors': dists = Distribution.objects.filter( TimeSlot=ts).distinct() for d in dists: try: recipients_staff.update( d.presentationtimeslot.Presentations. Assessors.all()) except PresentationTimeSlot.DoesNotExist: continue recipients_staff.update( User.objects.filter( tracks__isnull=False)) # add trackheads # students students = User.objects.filter( Q(usermeta__TimeSlot=ts) & Q(usermeta__EnrolledBEP=True) & Q(groups=None)) for s in form.cleaned_data['Students']: if s == 'all': recipients_students.update(students) elif s == '10ectsstd': recipients_students.update( students.filter(usermeta__EnrolledExt=False)) elif s == '15ectsstd': recipients_students.update( students.filter(usermeta__EnrolledExt=True)) elif s == 'distributedstd': recipients_students.update( students.filter(distributions__isnull=False, distributions__TimeSlot=ts).distinct()) # always send copy to admins for user in User.objects.filter(is_superuser=True): recipients_staff.add(user) # always send copy to self if request.user not in recipients_students or \ request.user not in recipients_staff: recipients_staff.update([request.user]) mailing_obj = Mailing( Subject=form.cleaned_data['Subject'], Message=form.cleaned_data['Message'], ) mailing_obj.save() mailing_obj.RecipientsStaff.set(recipients_staff) mailing_obj.RecipientsStudents.set(recipients_students) context = { 'form': ConfirmForm(initial={'confirm': True}), 'template': form.cleaned_data['SaveTemplate'], 'mailing': mailing_obj, } return render(request, "support/email_confirm.html", context=context) else: initial = None if pk: template = get_object_or_404(MailTemplate, pk=pk) initial = { 'Message': template.Message, 'Subject': template.Subject, 'Staff': json.loads(template.RecipientsStaff), 'Students': json.loads(template.RecipientsStudents), } form = ChooseMailingList(initial=initial, staff_options=mail_staff_options, student_options=mail_student_options) return render( request, "GenericForm.html", { "form": form, "formtitle": "Send mailing list", "buttontext": "Go to 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' })