Compare commits

...
3 Commits
Author SHA1 Message Date
Ross 4d8d6b2a06 fix question edit permissions 2024-09-07 11:03:44 +01:00
Ross ebf215b3d5 add some help to the rapid marking pages 2024-09-07 11:03:32 +01:00
Ross 97d59d338f update trainee management 2024-09-07 08:50:04 +01:00
12 changed files with 161 additions and 42 deletions
+1 -1
View File
@@ -598,7 +598,7 @@ class AnatomyQuestionAnswerUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, U
return super().form_invalid(form)
# class AnatomyQuestionUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin,
class AnatomyQuestionUpdate(UpdateQuestionMixin,AuthorOrCheckerRequiredMixin):
class AnatomyQuestionUpdate(UpdateQuestionMixin):
model = AnatomyQuestion
form_class = AnatomyQuestionForm
+2
View File
@@ -95,6 +95,8 @@ class SupervisorFilter(django_filters.FilterSet):
fields = {
"name": ["contains"],
"email": ["contains"],
"active": ["exact"],
"site": ["exact"],
}
def __init__(
+10 -1
View File
@@ -307,9 +307,18 @@ class ExaminationTable(tables.Table):
sequence = ("examination",)
class SupervisorTable(tables.Table):
name = tables.Column(
linkify=("generic:supervisor_detail", {"pk": tables.A("pk")}),
verbose_name="Name",
)
edit = tables.LinkColumn(
"generic:supervisor_edit", text="Edit", args=[A("pk")], orderable=False
)
class Meta:
model = Supervisor
template_name = "django_tables2/bootstrap4.html"
sequence = ("name", "email")
sequence = ("name", "email")
exclude = ("id",)
@@ -18,13 +18,13 @@
{% render_table table %}
<ul id="supervisor-list">
{% comment %} <ul id="supervisor-list">
{% for supervisor in object_list %}
<li class="supervisor"><a href="{% url 'generic:supervisor_detail' pk=supervisor.pk %}">{{supervisor.name}}</a>
<br/>{{supervisor.email}} [{{supervisor.site}}]
</li>
{% endfor %}
</ul>
</ul> {% endcomment %}
{% endblock %}
+59 -31
View File
@@ -16,42 +16,62 @@
{{grade}}
{% endif %}Trainees
</h2>
<table>
<tr class="header"><th>Name</th><th>Grade</th><th>Email</th><th>Supervisor</th><th>Edit</th></tr>
{% for trainee in trainees %}
<tr class="trainee">
<td>{{trainee.user.first_name}} {{trainee.user.last_name}}</td>
<td
{% if not trainee.grade %}
class="no-grade"
{% endif %}
>{{trainee.grade}}{% if not trainee.grade %}
<a href="{% url 'account_profile_update' trainee.user.username %}" class="add-grade">add</a>
{% endif %}
</td>
<td>{{trainee.user.email}}</td>
<td
{% if not trainee.supervisor %}
class="no-supervisor"
{% endif %}
>{{trainee.supervisor}}{% if not trainee.supervisor %}
<a href="{% url 'account_profile_update' trainee.user.username %}" class="add-supervisor">add</a>
{% endif %}
</td>
<td><a href="{% url 'account_update' trainee.user.username %}">User</a>/<a href="{% url 'account_profile_update' trainee.user.username %}">Profile</a>
<span class="hover-highlight"><i class="bi bi-x-circle" title="Click to remove trainee status"
hx-get = "{% url 'generic:user_not_trainee' trainee.user.pk %}"
></i></span>
</td>
<form>
<table>
<tr class="header">
<th>Name</th>
<th>Grade</th><th>Email</th><th>Supervisor</th><th>Edit</th>
<th class="trainee-bulk-edit">Bulk Edit<br/>
<span _="on click log 'test' then log 'hello' then repeat for i in <input/>
set i.checked to not i.checked
">Toggle All</span>
</th>
</tr>
{% endfor %}
</table>
{% for trainee in trainees %}
<tr class="trainee">
<td>{{trainee.user.first_name}} {{trainee.user.last_name}}</td>
<td
{% if not trainee.grade %}
class="no-grade"
{% endif %}
>{{trainee.grade}}{% if not trainee.grade %}
<a href="{% url 'account_profile_update' trainee.user.username %}" class="add-grade">add</a>
{% endif %}
</td>
<td>{{trainee.user.email}}</td>
<td
{% if not trainee.supervisor %}
class="no-supervisor"
{% endif %}
>{{trainee.supervisor}}{% if not trainee.supervisor %}
<a href="{% url 'account_profile_update' trainee.user.username %}" class="add-supervisor">add</a>
{% endif %}
</td>
<td><a href="{% url 'account_update' trainee.user.username %}">User</a>/<a href="{% url 'account_profile_update' trainee.user.username %}">Profile</a>
<span class="hover-highlight remove-trainee"><i class="bi bi-x-circle" title="Click to remove trainee status"
hx-get = "{% url 'generic:user_not_trainee' trainee.user.pk %}"
></i></span>
</td>
<td class="trainee-bulk-edit">
<input type="checkbox" name="trainee" value="{{trainee.user.pk}}">
</td>
</tr>
{% endfor %}
</table>
</form>
<div class="small-gray">Count: {{trainees|length}}</div>
<button class="btn btn-sm"
_="on click toggle .show on .remove-trainee"
>Remove trainees</button>
<button class="btn btn-sm"
_="on click toggle .show on .trainee-bulk-edit"
>Bulk edit trainees</button>
<style>
.add-supervisor, .add-grade {
font-size: small;
@@ -61,6 +81,14 @@
opacity: 50%;
}
.remove-trainee, .trainee-bulk-edit {
display: none;
}
.show {
display: inline;
}
td {
padding-right: 8px;
}
+14 -1
View File
@@ -3625,9 +3625,22 @@ class UpdateQuestionMixin(RedirectMixin, RevisionMixin, UpdateView):
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update({"user": self.request.user})
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["question"] = context["object"]
return context
# Do permission checks here
obj = context["object"]
if (
self.request.user.groups.filter(name="long_checker").exists()
or self.request.user.is_superuser
):
return context
if self.request.user in obj.get_author_objects():
return context
raise PermissionDenied() # or Http404
+49
View File
@@ -0,0 +1,49 @@
from django.urls import reverse
import pytest
from longs.models import Long
def create_question():
return Long.objects.create(
description="Test Question",
)
def create_user(db, django_user_model):
return django_user_model.objects.create_user(
"user1", "user1@user.com", "password"
)
@pytest.mark.django_db
def test_question_creation(client, db, django_user_model):
question = create_question()
assert question.description == "Test Question"
assert question.get_absolute_url() == f"/longs/question/{question.pk}/"
# Test access
user = create_user(db, django_user_model)
client.force_login(user)
# View access
response = client.get(reverse("longs:question_detail", kwargs={"pk": question.pk}), follow=True)
assert response.status_code == 403
response = client.get(reverse("longs:long_update", kwargs={"pk": question.pk}), follow=True)
assert response.status_code == 403
# Add author
question.add_author(user)
# Test author access
response = client.get(reverse("longs:question_detail", kwargs={"pk": question.pk}), follow=True)
assert response.status_code == 200
response = client.get(reverse("longs:long_update", kwargs={"pk": question.pk}), follow=True)
assert response.status_code == 200
+1 -1
View File
@@ -506,7 +506,7 @@ class LongCreate(LongCreateBase):
class LongUpdate(
UpdateQuestionMixin,AuthorOrCheckerRequiredMixin
UpdateQuestionMixin
):
model = Long
form_class = LongForm
+6
View File
@@ -0,0 +1,6 @@
import pytest
from longs.models import Long
+15 -3
View File
@@ -12,10 +12,24 @@
This questions has unmarked answers. Are you sure you want to review?
</div>
{% endif %}
<details>
<summary><i class="bi bi-info-circle"></i> Help</summary>
<p>Unmarked questions will be shown below. To mark the question, click on the answer to toggle through the markers.</p>
<p>The colour of the answer will change to reflect the mark given.</p>
<p>Answers will be automatically marked.</p>
<p>By default only answers for the current exam will be shown. Click the link to view all answers.</p>
<h4>Buttons</h4>
<ul>
<li>Previous: Go to the previous question</li>
<li>Save: Save your changes and stay on the same question</li>
<li>Next: Save your changes and go to the next question (will warn if not all answers have been marked)</li>
<li>Skip: Skip the current question allowing you to move on without marking all answers</li>
</ul>
</details>
{% if question.normal %}
<h3>This question is normal</h3>
Answers will be automatically marked.<br />
{% if incorrect_answers %}
<p>The following answers have been submitted for this question</p>
@@ -31,14 +45,12 @@
{% endif %}
{% else %}
<h3>This question is abnormal</h3>
Answers given as normal will be automatically marked.<br />
Region: {{ question.get_regions }}, Abnormalities: {{ question.get_abnormalities }}<br />
Primary answer: <span id="primary-answer" title="The primary answer of the question">{{question.get_primary_answer}}</span>
{% endif %}
<div class="marking">
<form method="POST" class="post-form">{% csrf_token %}
{% if not question.normal %}
Click each answer to toggle through marks awarded (as per colour)<br/>
{% if not review %}
{% if unmarked_exam_answers_only %}
Showing unmarked exam answers only <a href="{% url 'rapids:mark_all' exam.id question_number %}">(view all answers for question)</a>
+1 -1
View File
@@ -376,7 +376,7 @@ class RapidAnswerUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView)
class RapidUpdate(
UpdateQuestionMixin,AuthorOrCheckerRequiredMixin
UpdateQuestionMixin
):
model = Rapid
form_class = RapidForm
+1 -1
View File
@@ -318,7 +318,7 @@ class QuestionCreate(QuestionCreateBase):
return initial
class QuestionUpdate(AuthorOrCheckerRequiredMixin, UpdateQuestionMixin):
class QuestionUpdate(UpdateQuestionMixin):
model = Question
form_class = QuestionForm