Exemplo n.º 1
0
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
Exemplo n.º 2
0
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,
    })
Exemplo n.º 3
0
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,
    })
Exemplo n.º 4
0
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,
        })
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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,
    })
Exemplo n.º 7
0
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,
                  })
Exemplo n.º 8
0
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
Exemplo n.º 9
0
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"
        })
Exemplo n.º 10
0
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
Exemplo n.º 11
0
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
Exemplo n.º 12
0
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
                  })
Exemplo n.º 13
0
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)
Exemplo n.º 14
0
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,
        })
Exemplo n.º 15
0
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
Exemplo n.º 16
0
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,
        })
Exemplo n.º 17
0
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)