def list_students_xlsx(request): """ Same as liststudents but as XLSX. The combination of students and grades is done in general_excel. :param request: """ if get_timephase_number() < 0: if get_timeslot() is None: raise PermissionDenied("System is closed.") else: if get_timephase_number() < 4: raise PermissionDenied("Students are not yet distributed") if get_timephase_number() < 5 and not get_grouptype( "3") in request.user.groups.all(): return render( request, "base.html", { 'Message': "When the phase 'Distribution of projects is " "finished, you can view your students here." }) typ = GradeCategory.objects.filter(TimeSlot=get_timeslot()) des = get_distributions(request.user) file = get_list_students_xlsx(des, typ) response = HttpResponse(content=file) response[ 'Content-Type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' response[ 'Content-Disposition'] = 'attachment; filename=students-grades.xlsx' return response
def list_missing_of_type(request, pk): """ Lists all students that did not hand in a file with specified type. :param request: :param pk: filetype to show missing students for :return: """ ftype = get_object_or_404(FileType, pk=pk) missing_students = [] dists = get_distributions(request.user) if not dists: # raise PermissionDenied('You do not have any distributed students at this moment.') return render( request, 'base.html', context={ 'Message': 'You do not have any distributed students at this moment.' }) for dist in dists: if dist.files.filter(Type=ftype).count() == 0: missing_students.append(dist) return render(request, 'professionalskills/list_missing_students.html', { 'type': ftype, 'distributions': missing_students, })
def list_files_of_type(request, pk): """ Lists all files of one type of professional skill. :param request: :param pk: filetype to show delivered files for :return: """ ftype = get_object_or_404(FileType, pk=pk) if get_grouptype('3') in request.user.groups.all() or \ get_grouptype('6') in request.user.groups.all(): files = StudentFile.objects.filter(Type=ftype).distinct() elif get_grouptype('1') in request.user.groups.all() or \ get_grouptype('2') in request.user.groups.all(): # type1 or type2 dists = get_distributions(request.user, timeslot=get_timeslot()) if not dists: # raise PermissionDenied('You do not have any distributed students at this moment.') return render( request, 'base.html', context={ 'Message': 'You do not have any distributed students at this moment.' }) files = StudentFile.objects.filter(Type=ftype, Distribution__in=dists) # elif not request.user.groups.exists(): # files = StudentFile.objects.filter(Type=ftype, Distribution=request.user.distribution.get(TimeSlot=get_timeslot())) else: raise PermissionDenied('Not allowed.') return render(request, 'professionalskills/list_files.html', { 'type': ftype, 'files': files, })
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 download_all_of_type(request, pk): """ Download all files for a given filetype :param request: :param pk: id of the filetype :return: """ ftype = get_object_or_404(FileType, pk=pk) if ftype.TimeSlot == get_timeslot(): # current year download if get_timephase_number() < 5: # only in phase 5, 6 and 7 raise PermissionDenied( "This page is not available in the current time phase.") in_memory = BytesIO() dists = get_distributions(request.user, timeslot=ftype.TimeSlot) if not dists: raise PermissionDenied('You do not have any students.') with zipfile.ZipFile(in_memory, 'w') as archive: for file in ftype.files.filter(Distribution__in=dists): trck = file.Distribution.Proposal.Track fname = file.Distribution.Student.usermeta.get_nice_name().split( )[-1] + "".join(file.Distribution.Student.usermeta.get_nice_name(). split(' ')[:-1]) try: with open(file.File.path, 'rb') as fstream: archive.writestr( '{}/{}.{}'.format(str(trck), fname, file.File.name.split('.')[-1]), fstream.read()) except ( IOError, ValueError ): # happens if a file is referenced from database but does not exist on disk. return render( request, 'base.html', { 'Message': 'These files cannot be downloaded, please contact support staff. (Error on file: "{}")' .format(file) }) in_memory.seek(0) response = HttpResponse(content_type="application/zip") response['Content-Disposition'] = 'attachment; filename="{}.zip"'.format( str(ftype.Name)) response.write(in_memory.read()) return response
def list_missing_of_type(request, pk): """ Lists all students that did not hand in a file with specified type. :param request: :param pk: filetype to show missing students for :return: """ ftype = get_object_or_404(FileType, pk=pk) missing_students = [] for dist in get_distributions(request.user): if dist.files.filter(Type=ftype).count() == 0: missing_students.append(dist) return render(request, 'professionalskills/list_missing_students.html', { 'type': ftype, 'distributions': missing_students, })
def student(request, pk=None): """ view student prvs and files handins. :param request: :param pk: pk of distribution of student to view. Or none if viewing for self. :return: """ timeslot = get_timeslot() if not pk: # Student try: dist = request.user.distributions.get(TimeSlot=timeslot) except Distribution.DoesNotExist: raise PermissionDenied('You are not distributed to a project.') else: # staff try: dist = get_distributions(request.user, timeslot=timeslot).get(pk=pk) except (Distribution.DoesNotExist, AttributeError): raise PermissionDenied( "You are not allowed to view this students files.") if not can_view_files(request.user, dist): raise PermissionDenied('You are not allowed to view these files.') prvs = FileType.objects.filter(TimeSlot=timeslot) lst = [] for prv in prvs: try: f = prv.files.get(Distribution=dist) except StudentFile.DoesNotExist: f = None except StudentFile.MultipleObjectsReturned: raise PermissionDenied( f'Multiple files are uploaded for {dist} for {prv}. Please contact support staff to remove one.' ) lst.append([prv, f]) # will crash on multiple files return render(request, 'professionalskills/list_professional_skills_students.html', { 'filetypes': lst, 'timeslot': timeslot, 'dist': dist, })
def get_prv_todo(user): """ get to-do list for prv staff :param user: :return: """ if not user.is_authenticated: return False # check if not student if not user.groups.exists(): return False ts = get_timeslot() des = get_distributions(user, timeslot=ts) if not des or not des.exists(): return 'Nothing to do, you don\'t have students assigned.' prvs = FileType.objects.filter(TimeSlot=ts) if not prvs.exists(): return 'Nothing to do, there are no professional skills defined yet.' des = des.prefetch_related('Student__usermeta') html = '<ul>' done = True for d in des: url = reverse('professionalskills:student', kwargs={'pk': d.pk}) html += format_html( '<li><a href="{}" title="View files">{}:</a><br />', url, d.Student.usermeta.get_nice_name()) if not d.missing_files().exists() and not d.missing_file_gradings( ).exists(): html += f'Finished' else: html += f'{d.missing_files().count()} files missing, {d.missing_file_gradings().count()} gradings missing.' done = False html += '</li>' html += '</ul>' if done: html = 'No remaining open tasks.' + html else: html = "Please review the following items:" + html html += '<a href="{}" class ="button primary">{}</a>' url = reverse('students:liststudents', kwargs={'timeslot': ts.pk}) title = "View all" html = format_html(html, url, title) return html
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 export_filetype_xlsx(request, pk): """ xlsx export for osiris prv grading :param request: :param pk: :return: """ prv = get_object_or_404(FileType, pk=pk) if not prv.CheckedBySupervisor or not prv.aspects.exists(): raise PermissionDenied("This file has no grading aspects.") dists = get_distributions(request.user, timeslot=prv.TimeSlot) if not dists: raise PermissionDenied('There are no students') file = get_prv_type_xlsx(prv=prv, des=dists) response = HttpResponse(content=file) response[ 'Content-Type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' response[ 'Content-Disposition'] = f'attachment; filename=students {prv}.xlsx' return response
def print_forms(request): """ Export PDF of all distributed students :param request: :return: """ template = get_template('professionalskills/print_results.html') pages = [] for dstr in get_distributions(request.user, timeslot=get_timeslot()): obj = { 'student': dstr.Student, 'proposal': dstr.Proposal, 'files': StaffResponse.objects.filter( File__Distribution=dstr).order_by('File__Type__id'), } try: obj['room'] = dstr.presentationtimeslot.Presentations.AssessmentRoom obj['time'] = dstr.presentationtimeslot.DateTime except: pass pages.append(obj) htmlblock = template.render({'pages': pages}) buffer = BytesIO() pisa_status = pisa.CreatePDF(htmlblock.encode('utf-8'), dest=buffer, encoding='utf-8') if pisa_status.err: raise Exception("Pisa Failed PDF creation in print PRV results") buffer.seek(0) response = HttpResponse(buffer, 'application/pdf') response[ 'Content-Disposition'] = 'attachment; filename="Professional-Skills-export.pdf"' return response
def list_filetypes(request): """ For students to view a list of all profskills they have to hand in. For type3/type6 staff also shows edit and download buttons :param request: :return: """ timeslot = get_timeslot() if not timeslot: raise PermissionDenied( 'There is no timeslot defined. This page is not available.') prvs = FileType.objects.filter(TimeSlot=timeslot) lst = [] # list with [prv, nfiles, ngraded] dists = get_distributions(request.user, timeslot) if dists: cnt = dists.count() dists = dists.prefetch_related('files') for prv in prvs: files = prv.files.filter( Distribution__in=dists).prefetch_related('staffresponse') lst.append([ prv, files.count(), files.filter(staffresponse__isnull=False).count() ]) else: cnt = 0 for prv in prvs: lst.append([prv, 0, 0]) return render(request, 'professionalskills/list_professional_skills_staff.html', { 'filetypes': lst, 'timeslot': timeslot, 'nstud': cnt })
def detail_project(request, pk): """ Detailview page for a given proposal. Displays all information for the proposal. Used for students to choose a proposal from, and for staff to check. For staff it shows edit and up/downgrade buttons. For students it shows a apply or retract button. The proposal is cached after the create phase (phase>4). The apply/retract button is not cached but inserted using a .format(). Proposals are not cached for staff Private proposals don't have a apply/retract button, because the template doesn't have the {} in it then. :param request: :param pk: pk of the project :return: """ prop = get_cached_project(pk) # if student if not request.user.groups.exists(): # make apply / retract button. if get_timephase_number() != 3: # phase 3 is for students choosing projects. button = '' else: button = '<a href="{}" class="button {}">{}</a>' if get_all_applications(request.user).filter( Proposal=prop).exists(): # if user has applied to this proposal button = button.format(reverse('students:retractapplication', args=[get_all_applications(request.user).filter(Proposal=prop)[0].id]), 'danger', 'Retract Application') else: # show apply button button = button.format(reverse('students:confirmapply', args=[prop.id]), 'primary', 'Apply') # get proposal from cache, or put in cache cdata = cache.get("proposaldetail{}".format(pk)) if cdata is None: data = {"proposal": prop, "project": prop, "user": request.user } cdata = render_block_to_string("proposals/detail_project.html", 'body', data) cache.set('proposaldetail{}'.format(pk), cdata, None) tracking_visit_project(prop, request.user) # always log visits from students return render(request, "proposals/detail_project.html", { "bodyhtml": cdata.format(button), 'project': prop, 'fav': prop.favorites.filter(User=request.user).exists() }) # send project for if statement in scripts. # if staff: else: data = {"proposal": prop, "project": prop, "Editlock": "Editing not possible"} if prop.Status == 4: # published proposal in this timeslot # support staff can see applications if get_grouptype("3") in request.user.groups.all() and get_timephase_number() > 2: data['applications'] = prop.applications.all() # responsible / assistants can see distributions in distribution phase if get_timephase_number() >= 4: data['distributions'] = get_distributions(request.user).filter(Proposal=prop) allowed = can_edit_project_fn(request.user, prop, False) if allowed[0]: data['Editlock'] = False else: data['Editlock'] = allowed[1] data['fav'] = prop.favorites.filter(User=request.user).exists() data['cpv'] = cache.get('cpv_proj_{}'.format(prop.id)) # if cpv is not in cache, ignore return render(request, "proposals/detail_project.html", data)
def list_students(request, timeslot=None): """ For support staff, responsibles and assistants to view their students. List all students with distributions that the current user is allowed to see. Including a button to view the students files. Shows the grades as well. :param request: :param timeslot: the timeslot to look at. None for current timeslot (Future distributions do not exist) :return: """ ts = None show_grades = False error = None # if error set, don't show students but do show notify and tabcontrol current_ts = False if not timeslot: error = 'There is currently not time slot active in the system.' else: ts = get_object_or_404(TimeSlot, pk=timeslot) if ts.Begin > timezone.now().date(): error = 'Future students are not yet known.' if ts == get_timeslot(): current_ts = True if current_ts: # for current timeslot, check time phases. if get_timephase_number() < 0: # no timephase if get_timeslot() is None: # no timeslot error = "System is closed." else: if get_timephase_number() < 4: error = "Students are not yet distributed" if get_timephase_number() < 5 and not get_grouptype( "3") in request.user.groups.all(): error = "When the phase 'Distribution of projects' is finished, you can view your students here." if get_timephase_number() == -1 or get_timephase_number( ) >= 6: # also show grades when timeslot but no timephase. show_grades = True else: # historic grades show_grades = True if error: # show error but not permissiondenied so still the tab view is shown. return render( request, "students/list_students.html", { 'error': error, 'hide_sidebar': True, 'timeslots': get_recent_timeslots(), 'timeslot': ts, 'is_current': current_ts, }) # show students and possibly grades des = get_distributions(request.user, ts).select_related( 'Proposal__ResponsibleStaff', 'Proposal__Track', 'Student__usermeta').prefetch_related('Proposal__Assistants', 'files__staffresponse') cats = None if show_grades: cats = GradeCategory.objects.filter(TimeSlot=ts) des.prefetch_related('results__Category') deslist = [] # make grades for d in des: reslist = [] if show_grades: for c in cats: try: reslist.append(d.results.get(Category=c).Grade) except CategoryResult.DoesNotExist: reslist.append('-') deslist.append([d, reslist]) return render( request, "students/list_students.html", { 'des': deslist, 'typ': cats, 'show_grades': show_grades, 'hide_sidebar': True, 'timeslots': get_recent_timeslots(), 'timeslot': ts, 'is_current': current_ts, })
def download_files(request, timeslot): """ all student files of PRV of timeslot :param request: :param timeslot: timeslot id. :return: """ timeslot = get_object_or_404(TimeSlot, pk=timeslot) if timeslot == get_timeslot(): # current year if 4 > get_timephase_number( ) >= 0: # only in phase 4, 5, 6 and 7 (although 4 is not useful) raise PermissionDenied( "This page is not available in the current time phase.") elif timeslot.End > datetime.now().date(): # future ts. raise PermissionDenied('Future timeslot.') in_memory = BytesIO() des = get_distributions(request.user, timeslot=timeslot) if not des: return render(request, 'base.html', context={ 'Message': 'You do not have students.', 'return': 'students:liststudents', 'returnget': timeslot.pk }) empty = True with zipfile.ZipFile(in_memory, 'w') as archive: for d in des: files = d.files.all() for file in files: empty = False try: with open(file.File.path, 'rb') as fstream: name, ext = path.splitext(file.OriginalName) name = path.basename(truncatechars(name, 25)) ext = get_ext(file.File.name) archive.writestr( '{}-({})/{}/{}-{}.{}'.format( d.Student.usermeta.Fullname, d.Student.username, file.Type, timezone.localtime( file.TimeStamp).strftime("%y%m%d%H%M%S"), name, ext), fstream.read()) except ( IOError, ValueError ): # happens if a file is referenced from database but does not exist on disk. return render( request, 'base.html', { 'Message': 'These files cannot be downloaded, please contact support staff. (Error on file: "{}")' .format(file) }) if empty: return render( request, 'base.html', { 'Message': 'Your students did not upload any files yet.', 'return': 'students:liststudents', 'returnget': get_timeslot().pk }) in_memory.seek(0) response = HttpResponse(content_type="application/zip") response[ 'Content-Disposition'] = f'attachment; filename="studentfiles {timeslot}.zip"' response.write(in_memory.read()) return response
def list_students(request, timeslot): """ For support staff, responsibles and assistants to view their students. List all students with distributions that the current user is allowed to see. Including a button to view the students files. In later timephase shows the grades as well. :param request: :param timeslot: the timeslot to look at. None for current timeslot (Future distributions do not exist) :return: """ ts = get_object_or_404(TimeSlot, pk=timeslot) if ts.Begin > timezone.now().date(): raise PermissionDenied("Future students are not yet known.") if ts == get_timeslot(): current_ts = True else: current_ts = False if current_ts: # for current timeslot, check time phases. if get_timephase_number() < 0: # no timephase if get_timeslot() is None: # no timeslot raise PermissionDenied("System is closed.") else: if get_timephase_number() < 4: raise PermissionDenied("Students are not yet distributed") if get_timephase_number() < 5 and not get_grouptype( "3") in request.user.groups.all(): return render( request, "base.html", { 'Message': "When the phase 'Distribution of projects' is finished, you can view your students here." }) if get_timephase_number() == -1 or get_timephase_number( ) >= 6: # also show grades when timeslot but no timephase. show_grades = True else: show_grades = False else: # historic view of distributions. Hide grades, as they might have changed outside the system. show_grades = False des = get_distributions(request.user, ts).select_related( 'Proposal__ResponsibleStaff', 'Proposal__Track', 'Student__usermeta').prefetch_related('Proposal__Assistants') cats = None if show_grades: cats = GradeCategory.objects.filter(TimeSlot=get_timeslot()) des.prefetch_related('results__Category') deslist = [] # make grades for d in des: reslist = [] if show_grades: for c in cats: try: reslist.append(d.results.get(Category=c).Grade) except CategoryResult.DoesNotExist: reslist.append('-') deslist.append([d, reslist]) return render( request, "support/list_students.html", { 'des': deslist, 'typ': cats, 'show_grades': show_grades, 'hide_sidebar': True, 'timeslots': get_recent_timeslots(), 'timeslot': ts, 'is_current': current_ts, })
def detail_project(request, pk): """ Detailview page for a given proposal. Displays all information for the proposal. Used for students to choose a proposal from, and for staff to check. For staff it shows edit and up/downgrade buttons. For students it shows a apply or retract button. The proposal is cached after the create phase (phase>4). The apply/retract button is not cached but inserted using a .format(). Proposals are not cached for staff Private proposals don't have a apply/retract button, because the template doesn't have the {} in it then. :param request: :param pk: pk of the project :return: """ proj = get_cached_project(pk) # if student if not request.user.groups.exists(): # make apply / retract button. if request.user.personal_proposal.filter( TimeSlot=proj.TimeSlot).exists() or not proj.can_apply(): button = '' else: button = '<a href="{}" class="button {}">{}</a>' if get_all_applications( request.user, timeslot=proj.TimeSlot).filter(Proposal=proj).exists( ): # if user has applied to this proposal button = button.format( reverse('students:retractapplication', args=[ get_all_applications( request.user, timeslot=proj.TimeSlot).filter( Proposal=proj)[0].id ]), 'danger', 'Retract Application') else: # show apply button button = button.format( reverse('students:confirmapply', args=[proj.id]), 'primary', 'Apply') # get proposal from cache, or put in cache cdata = cache.get("proposaldetail{}".format(pk)) if cdata is None: data = { "proposal": proj, "project": proj, "user": request.user, 'cache_string_render': True } cdata = render_block_to_string("proposals/detail_project.html", 'body', data, request=request) cache.set('proposaldetail{}'.format(pk), cdata, settings.PROJECT_OBJECT_CACHE_DURATION) tracking_visit_project(proj, request.user) # always log visits from students # applications counter if proj.applications.exists(): applications_count = proj.applications.count() applications_count_txt = '{} student'.format(applications_count) if applications_count > 1: applications_count_txt += 's' else: applications_count_txt = 'No one applied to this project yet.' return render( request, "proposals/detail_project.html", { "bodyhtml": cdata.format(apply_buttons=button, applications_counter=applications_count_txt), 'project': proj, }) # send project for if statement in scripts. # if staff: else: data = { "proposal": proj, "project": proj, "Editlock": "Editing not possible" } if proj.Status == 4: # support staff can see applications and distributions always if get_grouptype("3") in request.user.groups.all(): data['applications'] = proj.applications.all() data['distributions'] = get_distributions( request.user, timeslot=proj.TimeSlot).filter(Proposal=proj) # other staff users can see old distributions and current in phase 4+ elif (get_grouptype('2u') not in request.user.groups.all()) and \ proj.prevyear() or (proj.curyear() and get_timephase_number() >= 4): data['distributions'] = get_distributions( request.user, timeslot=proj.TimeSlot).filter(Proposal=proj) allowed = can_edit_project_fn(request.user, proj, False) if allowed[0]: data['Editlock'] = False else: data['Editlock'] = allowed[1] data['cpv'] = cache.get('cpv_proj_{}'.format( proj.id)) # if cpv is not in cache, ignore return render(request, "proposals/detail_project.html", data)