Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into 1963Inverted_Likert…
Browse files Browse the repository at this point in the history
…_Scale
  • Loading branch information
richardebeling committed Sep 18, 2023
2 parents 7d8cba2 + 4e9cb1f commit 03b61c3
Show file tree
Hide file tree
Showing 24 changed files with 356 additions and 337 deletions.
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ not touched by the software license.

```
EvaP – Evaluation Platform
Copyright (C) 2011-2022 by Johannes Wolf, Johannes Linke, Michael Grünewald,
Copyright (C) 2011-2023 by Johannes Wolf, Johannes Linke, Michael Grünewald,
Stefan Richter, and Richard Ebeling
Permission is hereby granted, free of charge, to any person obtaining a copy
Expand Down
8 changes: 7 additions & 1 deletion evap/contributor/templates/contributor_index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<a href="{% url 'contributor:export' %}" class="btn btn-sm btn-light">{% trans 'Export results' %}</a>
</div>
{% if user.is_delegate %}
<div class="btn-switch btn-switch-light mb-auto ms-2 d-print-none">
<div class="btn-switch btn-switch-light ms-2 d-print-none">
<div class="btn-switch-label"><span class="fas fa-people-arrows-left-right"></span> {% trans 'Delegated evaluations' %}</div>
<div class="btn-switch btn-group">
<a href="{% url 'contributor:index' %}?show_delegated=true" role="button" class="btn btn-sm btn-light{% if show_delegated %} active{% endif %}">
Expand All @@ -28,6 +28,12 @@
</div>
</div>
{% endif %}
{% if user.show_startpage_button %}
<div class="ms-2 d-print-none">
{% include 'startpage_button.html' with page='CO' btnClass='btn-sm' %}
</div>
{% endif %}

</div>

{% for semester in semester_list %}
Expand Down
22 changes: 22 additions & 0 deletions evap/evaluation/migrations/0139_userprofile_startpage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.3 on 2023-09-04 20:09

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("evaluation", "0138_votetimestamp"),
]

operations = [
migrations.AddField(
model_name="userprofile",
name="startpage",
field=models.CharField(
choices=[("DE", "default"), ("ST", "student"), ("CO", "contributor"), ("GR", "grades")],
default="DE",
max_length=2,
verbose_name="start page of the user",
),
),
]
23 changes: 20 additions & 3 deletions evap/evaluation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1698,6 +1698,19 @@ class UserProfile(AbstractBaseUser, PermissionsMixin):

is_active = models.BooleanField(default=True, verbose_name=_("active"))

class StartPage(models.TextChoices):
DEFAULT = "DE", _("default")
STUDENT = "ST", _("student")
CONTRIBUTOR = "CO", _("contributor")
GRADES = "GR", _("grades")

startpage = models.CharField(
max_length=2,
choices=StartPage.choices,
verbose_name=_("start page of the user"),
default=StartPage.DEFAULT,
)

class Meta:
# keep in sync with ordering_key
ordering = [
Expand Down Expand Up @@ -1753,9 +1766,7 @@ def full_name_with_additional_info(self):
name = self.full_name
if self.is_external:
return name + " [ext.]"
if "@" in self.email:
return name + " (" + self.email.split("@")[0] + ")"
return name + " (" + self.email + ")"
return f"{name} ({self.email})"

def __str__(self):
return self.full_name
Expand Down Expand Up @@ -1860,6 +1871,12 @@ def is_editor_or_delegate(self):
def is_responsible_or_contributor_or_delegate(self):
return self.is_responsible or self.is_contributor or self.is_delegate

@cached_property
def show_startpage_button(self):
return [self.is_participant, self.is_responsible_or_contributor_or_delegate, self.is_grade_publisher].count(
True
) > 1

@property
def is_external(self):
if not self.email:
Expand Down
4 changes: 2 additions & 2 deletions evap/evaluation/models_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,14 @@ def _get_change_data(self, action_type: InstanceActionType):
if created_value is not None
}
elif action_type == InstanceActionType.CHANGE:
old_dict = type(self).objects.get(pk=self.pk)._as_dict()
old_dict = type(self)._default_manager.get(pk=self.pk)._as_dict()
changes = {
field_name: {FieldActionType.VALUE_CHANGE: [old_value, self_dict[field_name]]}
for field_name, old_value in old_dict.items()
if old_value != self_dict[field_name]
}
elif action_type == InstanceActionType.DELETE:
old_dict = type(self).objects.get(pk=self.pk)._as_dict()
old_dict = type(self)._default_manager.get(pk=self.pk)._as_dict()
changes = {
field_name: {FieldActionType.INSTANCE_DELETE: [deleted_value]}
for field_name, deleted_value in old_dict.items()
Expand Down
1 change: 1 addition & 0 deletions evap/evaluation/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
},
closeAfterSelect: true, // also clears search input on enter.
plugins: {},
hideSelected: false,
};
if(element.hasAttribute("data-tomselect-fullwidth")) {
// span needed to the "remove this icon" button is right-aligned
Expand Down
13 changes: 13 additions & 0 deletions evap/evaluation/templates/startpage_button.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% if user.startpage == page %}
<div data-bs-toggle="tooltip" data-bs-placement="left" title="{% trans 'This is your startpage' %}">
<button type="button" class="btn {{ btnClass }} btn-light" disabled><span class="fas fa-house"></span></button>
</div>
{% else %}
<form id="startpage-form" class="d-flex" method="post" action="{% url 'evaluation:set_startpage' %}">
{% csrf_token %}
<input name="page" type="hidden" value="{{ page }}">
<button type="submit" class="btn {{ btnClass }} btn-light" onclick="setSpinnerIcon('span-set-startpage')" data-bs-toggle="tooltip" data-bs-placement="left" title="{% trans 'Make this my startpage' %}">
<span id="span-set-startpage" class="fas fa-house"></span>
</button>
</form>
{% endif %}
4 changes: 2 additions & 2 deletions evap/evaluation/tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_is_prefetched(self):
baker.make(Contribution, evaluation=evaluation)

def test_logic(cls: type[Model], pk: int, field: str) -> None:
instance = cls.objects.get(pk=pk)
instance = cls._default_manager.get(pk=pk)
self.assertFalse(is_prefetched(instance, field))

prefetch_related_objects([instance], field)
Expand All @@ -76,7 +76,7 @@ def test_logic(cls: type[Model], pk: int, field: str) -> None:
instance.refresh_from_db(fields=[field])
self.assertFalse(is_prefetched(instance, field))

instance = cls.objects.filter(pk=instance.pk).prefetch_related(field).get()
instance = cls._default_manager.filter(pk=instance.pk).prefetch_related(field).get()
self.assertTrue(is_prefetched(instance, field))

test_logic(Evaluation, evaluation.pk, "contributions") # inverse foreign key
Expand Down
17 changes: 17 additions & 0 deletions evap/evaluation/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ def test_send_new_login_key(self):
self.assertEqual(len(mail.outbox[0].cc), 0)


class TestStartpage(WebTest):
def test_default_startpage(self):
result = create_evaluation_with_responsible_and_editor()
responsible = result["responsible"]
evaluation = result["evaluation"]

evaluation.participants.add(responsible)

self.assertRedirects(self.app.get(reverse("evaluation:index"), user=responsible), reverse("student:index"))

page = self.app.get(reverse("contributor:index"), user=responsible)
form = page.forms["startpage-form"]
form.submit()

self.assertRedirects(self.app.get(reverse("evaluation:index"), user=responsible), reverse("contributor:index"))


class TestLegalNoticeView(WebTestWith200Check):
url = "/legal_notice"
test_users = [""]
Expand Down
1 change: 1 addition & 0 deletions evap/evaluation/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
path("contact", views.contact, name="contact"),
path("key/<int:key>", views.login_key_authentication, name="login_key_authentication"),
path("profile", views.profile_edit, name="profile_edit"),
path("set_startpage", views.set_startpage, name="set_startpage"),
]
22 changes: 20 additions & 2 deletions evap/evaluation/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from django.views.i18n import set_language

from evap.evaluation.forms import LoginEmailForm, NewKeyForm, ProfileForm
from evap.evaluation.models import EmailTemplate, FaqSection, Semester
from evap.evaluation.models import EmailTemplate, FaqSection, Semester, UserProfile
from evap.middleware import no_login_required

logger = logging.getLogger(__name__)
Expand All @@ -31,11 +31,17 @@ def redirect_user_to_start_page(user):
return redirect("staff:semester_view", active_semester.id)
return redirect("staff:index")

if user.startpage == UserProfile.StartPage.STUDENT and user.is_participant:
return redirect("student:index")
if user.startpage == UserProfile.StartPage.CONTRIBUTOR and user.is_responsible_or_contributor_or_delegate:
return redirect("contributor:index")
if user.startpage == UserProfile.StartPage.GRADES and user.is_grade_publisher and active_semester is not None:
return redirect("grades:semester_view", active_semester.id)

if user.is_grade_publisher:
if active_semester is not None:
return redirect("grades:semester_view", active_semester.id)
return redirect("grades:index")

if user.is_student:
return redirect("student:index")
if user.is_responsible_or_contributor_or_delegate:
Expand Down Expand Up @@ -215,3 +221,15 @@ def profile_edit(request):
context = {"user": user, "profile_form": profile_form, **(editor_context if user.is_editor else {})}

return render(request, "profile.html", context)


@require_POST
def set_startpage(request):
user = request.user
startpage = request.POST.get("page")
if startpage not in UserProfile.StartPage.values:
return HttpResponseBadRequest()
user.startpage = startpage
user.save()

return redirect("evaluation:index")
9 changes: 7 additions & 2 deletions evap/grades/templates/grades_semester_view.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@
{% show_infotext "grades_pages" %}

<div class="row mb-3 align-items-center">
<h3 class="col-9 mb-0">
<h3 class="col-8 mb-0">
{{ semester.name }}
</h3>
<div class="col-3">
<div class="col-4 d-flex">
{% if user.show_startpage_button and semester.is_active %}
<div class="me-2 d-print-none">
{% include 'startpage_button.html' with page='GR' %}
</div>
{% endif %}
<div class="input-group">
<input type="search" name="search" class="form-control" placeholder="{% trans 'Search...' %}" />
<button class="btn btn-light text-secondary" type="button" data-reset="search" data-bs-toggle="tooltip" data-bs-placement="top" title="{% trans 'Clear search filter' %}">
Expand Down
2 changes: 1 addition & 1 deletion evap/results/templates/results_index.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ <h5 class="card-title mb-lg-0">
</label>
<input id="sort-order-desc" class="btn-check" type="radio" name="result-sort-order" value="desc" />
<label class="btn btn-outline-primary btn-lg" for="sort-order-desc">
<span class="fas fa-arrow-up-z-a-alt"></span>
<span class="fas fa-arrow-up-z-a"></span>
</label>
</div>
<select name="result-sort-column" class="form-select no-tomselect">
Expand Down
28 changes: 23 additions & 5 deletions evap/staff/importers/enrollment.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from collections.abc import Iterable
from dataclasses import dataclass, fields
from datetime import date, datetime
from typing import TypeAlias, TypeGuard, TypeVar
from typing import NoReturn, TypeAlias, TypeGuard, TypeVar

from django.conf import settings
from django.db import transaction
Expand Down Expand Up @@ -39,7 +39,9 @@
@dataclass(frozen=True)
class InvalidValue:
# We make this a dataclass to make sure all instances compare equal.
pass

def __bool__(self) -> NoReturn:
raise NotImplementedError("Bool conversion of InvalidValue is likely a bug")


invalid_value = InvalidValue()
Expand Down Expand Up @@ -326,6 +328,7 @@ def __init__(self, semester: Semester):
@staticmethod
def get_merge_hindrances(course_data: CourseData, merge_candidate: Course) -> list[str]:
hindrances = []

if merge_candidate.type != course_data.course_type:
hindrances.append(_("the course type does not match"))

Expand All @@ -336,9 +339,24 @@ def get_merge_hindrances(course_data: CourseData, merge_candidate: Course) -> li
merge_candidate_evaluations = merge_candidate.evaluations.all()
if len(merge_candidate_evaluations) != 1:
hindrances.append(_("the existing course does not have exactly one evaluation"))
elif merge_candidate_evaluations[0].wait_for_grade_upload_before_publishing != course_data.is_graded:
return hindrances

merge_candidate_evaluation: Evaluation = merge_candidate_evaluations[0]

if merge_candidate_evaluation.wait_for_grade_upload_before_publishing != course_data.is_graded:
hindrances.append(_("the evaluation of the existing course has a mismatching grading specification"))

if merge_candidate_evaluation.is_single_result:
hindrances.append(_("the evaluation of the existing course is a single result"))
return hindrances

if merge_candidate_evaluation.state >= Evaluation.State.IN_EVALUATION:
hindrances.append(
_("the import would add participants to the existing evaluation but the evaluation is already running")
)
else:
assert merge_candidate_evaluation._participant_count is None

return hindrances

def set_course_merge_target(self, course_data: CourseData) -> None:
Expand Down Expand Up @@ -407,7 +425,7 @@ def check_course_data(self, course_data: CourseData, location: ExcelFileLocation
except CourseMergeLogic.NameEnCollisionException:
self.name_en_collision_tracker.add_location_for_key(location, course_data.name_en)

if course_data.merge_into_course:
if course_data.merge_into_course != invalid_value and course_data.merge_into_course:
self.course_merged_tracker.add_location_for_key(location, course_data.name_en)

self.name_en_by_name_de.setdefault(course_data.name_de, course_data.name_en)
Expand Down Expand Up @@ -535,7 +553,7 @@ def finalize(self) -> None:

existing_participation_pairs = [
(participation.evaluation.course.name_en, participation.userprofile.email) # type: ignore
for participation in Evaluation.participants.through.objects.filter(
for participation in Evaluation.participants.through._default_manager.filter(
evaluation__course__name_en__in=seen_evaluation_names, userprofile__email__in=seen_user_emails
).prefetch_related("userprofile", "evaluation__course")
]
Expand Down
2 changes: 1 addition & 1 deletion evap/staff/templates/staff_semester_view.html
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ <h3>
title="{% trans 'Create single result for this course' %}">
<span class="fas fa-square-poll-vertical"></span>
</a>
<a class="btn btn-sm btn-dark" data-toggle="tooltip"
<a class="btn btn-sm btn-dark" data-bs-toggle="tooltip"
href="{% url 'staff:course_copy' course.id %}"
title="{% trans 'Copy course' %}">
<span class="fas fa-copy"></span>
Expand Down
2 changes: 1 addition & 1 deletion evap/staff/templates/staff_user_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<div class="input-group">
<input type="search" name="search" class="form-control" placeholder="{% trans 'Search...' %}" />
<div class="input-group-append">
<button class="btn btn-light text-secondary" type="button" data-reset="search" data-toggle="tooltip" data-placement="top" title="{% trans 'Clear search filter' %}">
<button class="btn btn-light text-secondary" type="button" data-reset="search" data-bs-toggle="tooltip" data-bs-placement="top" title="{% trans 'Clear search filter' %}">
<span class="fas fa-backspace"></span>
</button>
</div>
Expand Down
Loading

0 comments on commit 03b61c3

Please sign in to comment.