def get_valid_proposals(): """ Get all proposals that (non-private) students can be distributed to. :return: """ return get_all_proposals().filter(Q(Status=4) & Q(Private__isnull=True))
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 list_second_choice(request): """ list all students with a random distribution, without project and all non-full projects :param request: :return: """ props = get_all_proposals().filter( Status=4, Private__isnull=True).distinct().annotate( num_distr=Count('distributions')).filter( TimeSlot=get_timeslot(), num_distr__lt=F('NumStudentsMax')).order_by('Title') prop_obj = [[prop, get_share_link(prop.pk)] for prop in props] no_dist = get_all_students(undistributed=True).filter( distributions__isnull=True, applications__isnull=False).distinct() # filter students in this year with only applications in other year no_dist = [ s for s in no_dist if s.applications.filter(Proposal__TimeSlot=get_timeslot()).exists() ] return render( request, 'distributions/list_second_choice.html', { 'distributions': Distribution.objects.filter( TimeSlot=get_timeslot(), Application__isnull=True, Proposal__Private__isnull=True).order_by('Student'), 'no_dist': no_dist, 'proposals': prop_obj, })
def mail_track_heads_pending(stdout=False): """ Mail track heads with their pending proposals. Can be used in an automated script with logging. In that case stdout can be set to True. :param stdout: :return: """ for track in Track.objects.all(): if stdout: print("Track: " + track.Name) if track.Head is not None: proposals = get_all_proposals().filter( Q(Status=3) & Q(Track=track)) if proposals.count() > 0: e = track.Head.email context = { 'proposals': proposals, 'track': track, } send_mail("action required for track head", "email/action_required_trackhead_email.html", context, e) if stdout: print("Sending proposals: " + str(list(proposals))) else: if stdout: print("Track has no status 3 pending") else: if stdout: print("Track has no head")
def manual(request): """ Support page to distribute students manually to projects. Uses ajax calls to change distributions. Same table included as in list appls/dists :param request: """ if get_timephase_number() < 4 or get_timephase_number() > 6: raise PermissionDenied('Distribution is not possible in this timephase') props = get_all_proposals().filter(Q(Status__exact=4)) \ .select_related('ResponsibleStaff__usermeta', 'Track', 'TimeSlot') \ .prefetch_related('Assistants__usermeta', 'Private__usermeta', 'applications__Student__usermeta', 'distributions__Student__usermeta') # includes students without applications. # also show undistributed in phase 6 studs = get_all_students(undistributed=True).exclude(distributions__TimeSlot=get_timeslot()) \ .select_related('usermeta') \ .prefetch_related('applications__Proposal').distinct() dists = Distribution.objects.filter(TimeSlot=get_timeslot()) \ .select_related('Student__usermeta', 'Proposal', 'Application__Student__usermeta') return render(request, 'distributions/manual_distribute.html', {'proposals': props, 'undistributedStudents': studs, 'distributions': dists, 'hide_sidebar': True})
def list_published_api(request): """ JSON list of all published proposals with some detail info. :param request: :return: """ props = get_all_proposals().filter(Q(Status=4) & Q(Private__isnull=True)) prop_list = [] for prop in props: prop_list.append({ "id": prop.id, "detaillink": reverse("proposals:details", args=[prop.id]), "title": prop.Title, "group": prop.Group.ShortName, "track": str(prop.Track), "reponsible": str(prop.ResponsibleStaff), "assistants": [str(u) for u in list(prop.Assistants.all())], }) return JsonResponse(prop_list, safe=False)
def list_distributions_xlsx(request): """ Same as supportListApplications but as XLSX """ if get_timephase_number() < 3: raise PermissionDenied("There are no applications yet") elif get_timephase_number() > 4: projects = get_all_proposals().filter(Q(Status=4) & Q(distributions__isnull=False)).distinct() else: projects = get_all_proposals().filter(Status=4) # projects = projects.select_related('ResponsibleStaff', 'Track').prefetch_related('Assistants', # 'distributions__Student__usermeta') file = get_list_distributions_xlsx(projects) response = HttpResponse(content=file) response['Content-Type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' response['Content-Disposition'] = 'attachment; filename=marketplace-projects-distributions.xlsx' return response
def list_applications_distributions(request, timeslot=None): """ Show a list of all active proposals with the applications and possibly distributions of students. Used for support staff as an overview. Same table include as in manual distribute """ if not timeslot: if get_timephase_number() > 5: projects = get_all_proposals().filter( Q(Status=4) & Q(distributions__isnull=False)).distinct() projects = projects.select_related( 'ResponsibleStaff', 'Track').prefetch_related('Assistants', 'Private', 'distributions__Application', 'distributions__Student__usermeta') else: # phase 3 & 4 & 5 projects = get_all_proposals().filter(Status=4) projects = projects.select_related( 'ResponsibleStaff', 'Track').prefetch_related('Assistants', 'Private', 'applications__Student__usermeta', 'distributions__Application', 'distributions__Student__usermeta') return render(request, 'distributions/list_applications_distributions.html', {"proposals": projects}) else: # future ts = get_object_or_404(TimeSlot, pk=timeslot) projects = Proposal.objects.filter(TimeSlot=ts, Status=4) projects = projects.select_related( 'ResponsibleStaff', 'Track').prefetch_related('Assistants', 'Private', 'applications__Student__usermeta', 'distributions__Application', 'distributions__Student__usermeta') return render(request, 'distributions/list_applications_distributions.html', { "proposals": projects, 'timeslot': ts })
def list_public_projects_titles_api(request): """ Get all public proposals (=status 4) titles as JSON :param request: :return: JSON response """ data = {} for prop in get_all_proposals().filter(Q(Status=4) & Q(Private__isnull=True)): data[prop.id] = prop.Title return JsonResponse(data)
def get_pending_tag(user): """ :param user: :return: """ # <button> inside <a> is invalid HTML5. MetroUI does not work well with loading-pulse on non-button, so keep it. html = "<a href='" + reverse( "proposals:pending" ) + "'><button class=\"button danger loading-pulse\">Pending: {}</button></a>" num = 0 if get_grouptype("2") in user.groups.all(): num += get_all_proposals().filter( Q(Assistants__id=user.id) & Q(Status__exact=1)).count() if get_grouptype("1") in user.groups.all(): num += get_all_proposals().filter( Q(ResponsibleStaff=user.id) & Q(Status__exact=2)).count() num += get_all_proposals().filter( Q(Track__Head=user.id) & Q(Status__exact=3)).count() if num == 0: return "No pending projects for your attention" else: return format_html(html.format(num))
def list_applications_distributions(request): """ Show a list of all active proposals with the applications and possibly distributions of students. Used for support staff as an overview. Same table include as in manual distribute """ if get_timephase_number() < 3: raise PermissionDenied("There are no applications or distributions yet.") elif get_timephase_number() > 5: projects = get_all_proposals().filter(Q(Status=4) & Q(distributions__isnull=False)).distinct() projects = projects.select_related('ResponsibleStaff', 'Track').prefetch_related('Assistants', 'Private', 'distributions__Application', 'distributions__Student__usermeta') else: # phase 3 & 4 & 5 projects = get_all_proposals().filter(Status=4) projects = projects.select_related('ResponsibleStaff', 'Track').prefetch_related('Assistants', 'Private', 'applications__Student__usermeta', 'distributions__Application', 'distributions__Student__usermeta') return render(request, 'distributions/list_applications_distributions.html', {"proposals": projects})
def list_public_projects_api(request): """ Return all public proposals (=type 4) ordered by group as JSON :param request: :return: JSON response """ data = {} for group in CapacityGroup.objects.all(): data[group.ShortName] = { "name": group.ShortName, "projects": [prop.id for prop in get_all_proposals().filter(Q(Status=4) & Q(Group=group) & Q(Private__isnull=True))] } return JsonResponse(data)
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 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 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' })
p.StudentsTaskDescription = "students have to do stuff woop woop" p.Track = random.choice(trackobjs) # p.Private = None # p.Image = random.choice(["niels.png", "crying.png"]) # p.Status = random.choice(Proposal.StatusOptions)[0] p.Status = 4 p.TimeSlot = get_timeslot() p.save() # save already to activate the manytomany field of assistants numphd = random.choice([1, 2]) ass1 = random.choice(phds) p.Assistants.add(ass1) if numphd == 2: phds.remove(ass1) ass2 = random.choice(phds) p.Assistants.add(ass2) phds.append(ass1) p.save() print('{} created'.format(p)) except: print(str(i) + "th proposal not created") print("generating applications") secure_random = random.SystemRandom() projects = get_all_proposals() for i in range(0, NUMSTDS): for c in range(1, settings.MAX_NUM_APPLICATIONS + 1): app = Application(Priority=c, Proposal=secure_random.choice(projects), Student=User.objects.get(username='******'.format(i))) app.save()