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 distribute_personal(distribution_proposals, students_done=[]): """ Distribute all students with a private proposal to the private proposal. :param distribution_proposals: List of distributions :param students_done: list of distributed students :return: """ stds = get_all_students().filter( personal_proposal__isnull=False, personal_proposal__TimeSlot=get_timeslot()).distinct( ).prefetch_related('personal_proposal') for s in stds: try: p = s.personal_proposal.filter(TimeSlot=get_timeslot()).get() except MultipleObjectsReturned: raise Exception( "User {} has multiple private proposals ({}). Please resolve this!" .format( s, print_list( s.personal_proposal.filter( TimeSlot=get_timeslot()).all()))) if p.TimeSlot != get_timeslot(): raise Exception( "User {} of this timeslot has private proposal {} of other timeslot {}" .format(s, p, p.TimeSlot)) distribution_proposals.append(DistributionProposal(s.id, p.id, 0)) students_done.append(s) return [distribution_proposals, students_done]
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 api_undistribute(request): """ AJAX call from manual distribute to undistribute :param request: :return: """ if get_timephase_number() < 4 or get_timephase_number() > 5: # Not in phase 6, because projects already started. raise PermissionDenied('Undistribution is not possible in this timephase') if request.method == 'POST': try: student = get_all_students(undistributed=True).get(pk=request.POST['student']) except User.DoesNotExist: return JsonResponse({'type': 'warning', 'txt': warningString + ' (User cannot be found)'}) try: dist = student.distributions.get(TimeSlot=get_timeslot()) except Distribution.DoesNotExist: return JsonResponse({'type': 'warning', 'txt': warningString + ' (Distribution cannot be found)'}) try: n = dist.delete() if n[0] == 1: return JsonResponse( {'type': 'success', 'txt': 'Undistributed Student ' + dist.Student.usermeta.get_nice_name()}) else: return JsonResponse({'type': 'warning', 'txt': warningString + ' (distributions not deleted)'}) except Exception as e: return JsonResponse({'type': 'warning', 'txt': warningString, 'exception': str(e)}) else: raise PermissionDenied('You don\'t know what you\'re doing!')
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 get_valid_students(): """ All students with applications, with personal, in this timeslot. Personal proposals are distributed as first, so personal prop students will not get to the other distribution. :return: list of user objects. """ return get_all_students().filter(applications__isnull=False).filter(applications__Proposal__TimeSlot=get_timeslot()).distinct()
def visitorsMenu(request): """ List of all students, click on the student to view the proposals that he/she visited. :param request: :return: """ return render(request, 'godpowers/visitorsmenu.html', {'students': get_all_students()})
def get_valid_students(): """ All students with applications, without personal, in this timeslot. :return: list of user objects. """ # TODO personal_proposal should be filtered by timeslot return get_all_students().filter( personal_proposal__isnull=True, applications__isnull=False).filter( applications__Proposal__TimeSlot=get_timeslot()).distinct()
def get_cohorts(): """ Get all cohorts that have students in this year. :return: """ cohorts = [] for s in get_all_students().select_related('usermeta'): c = s.usermeta.Cohort if c is not None: if c not in cohorts: cohorts.append(c) cohorts.sort(reverse=True) return cohorts
def api_redistribute(request): """ AJAX call from manual distribute to change a distribution :param request: :return: """ if get_timephase_number() < 4 or get_timephase_number() > 6: raise PermissionDenied('Distribution is not possible in this timephase') if request.method == 'POST': try: student = get_all_students(undistributed=True).get(pk=request.POST['student']) except User.DoesNotExist: return JsonResponse({'type': 'warning', 'txt': warningString + ' (User cannot be found)'}) try: dist = student.distributions.get(TimeSlot=get_timeslot()) except Distribution.DoesNotExist: return JsonResponse({'type': 'warning', 'txt': warningString + ' (Distribution cannot be found)'}) try: # change Proposal dist.Proposal = get_cached_project(request.POST['propTo']) # change Application if user has Application try: dist.Application = get_all_applications(dist.Student).get(Proposal=dist.Proposal) appl_prio = dist.Application.Priority except Application.DoesNotExist: dist.Application = None appl_prio = -1 dist.full_clean() dist.save() except Exception as e: return JsonResponse({'type': 'warning', 'txt': warningString, 'exception': str(e)}) return JsonResponse( {'type': 'success', 'txt': 'Changed distributed Student ' + dist.Student.usermeta.get_nice_name() + ' to Proposal ' + dist.Proposal.Title, 'prio': appl_prio}) else: raise PermissionDenied("You don't know what you're doing!")
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, })