Compare commits
3
Commits
46165a47ec
...
4d8d6b2a06
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d8d6b2a06 | ||
|
|
ebf215b3d5 | ||
|
|
97d59d338f |
+1
-1
@@ -598,7 +598,7 @@ class AnatomyQuestionAnswerUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, U
|
|||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
# class AnatomyQuestionUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin,
|
# class AnatomyQuestionUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin,
|
||||||
class AnatomyQuestionUpdate(UpdateQuestionMixin,AuthorOrCheckerRequiredMixin):
|
class AnatomyQuestionUpdate(UpdateQuestionMixin):
|
||||||
model = AnatomyQuestion
|
model = AnatomyQuestion
|
||||||
form_class = AnatomyQuestionForm
|
form_class = AnatomyQuestionForm
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ class SupervisorFilter(django_filters.FilterSet):
|
|||||||
fields = {
|
fields = {
|
||||||
"name": ["contains"],
|
"name": ["contains"],
|
||||||
"email": ["contains"],
|
"email": ["contains"],
|
||||||
|
"active": ["exact"],
|
||||||
|
"site": ["exact"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
+10
-1
@@ -307,9 +307,18 @@ class ExaminationTable(tables.Table):
|
|||||||
sequence = ("examination",)
|
sequence = ("examination",)
|
||||||
|
|
||||||
class SupervisorTable(tables.Table):
|
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:
|
class Meta:
|
||||||
model = Supervisor
|
model = Supervisor
|
||||||
template_name = "django_tables2/bootstrap4.html"
|
template_name = "django_tables2/bootstrap4.html"
|
||||||
|
|
||||||
sequence = ("name", "email")
|
sequence = ("name", "email")
|
||||||
|
exclude = ("id",)
|
||||||
@@ -18,13 +18,13 @@
|
|||||||
|
|
||||||
{% render_table table %}
|
{% render_table table %}
|
||||||
|
|
||||||
<ul id="supervisor-list">
|
{% comment %} <ul id="supervisor-list">
|
||||||
{% for supervisor in object_list %}
|
{% for supervisor in object_list %}
|
||||||
<li class="supervisor"><a href="{% url 'generic:supervisor_detail' pk=supervisor.pk %}">{{supervisor.name}}</a>
|
<li class="supervisor"><a href="{% url 'generic:supervisor_detail' pk=supervisor.pk %}">{{supervisor.name}}</a>
|
||||||
<br/>{{supervisor.email}} [{{supervisor.site}}]
|
<br/>{{supervisor.email}} [{{supervisor.site}}]
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul> {% endcomment %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -16,42 +16,62 @@
|
|||||||
{{grade}}
|
{{grade}}
|
||||||
{% endif %}Trainees
|
{% endif %}Trainees
|
||||||
</h2>
|
</h2>
|
||||||
<table>
|
<form>
|
||||||
<tr class="header"><th>Name</th><th>Grade</th><th>Email</th><th>Supervisor</th><th>Edit</th></tr>
|
<table>
|
||||||
|
<tr class="header">
|
||||||
{% for trainee in trainees %}
|
<th>Name</th>
|
||||||
<tr class="trainee">
|
<th>Grade</th><th>Email</th><th>Supervisor</th><th>Edit</th>
|
||||||
<td>{{trainee.user.first_name}} {{trainee.user.last_name}}</td>
|
<th class="trainee-bulk-edit">Bulk Edit<br/>
|
||||||
<td
|
<span _="on click log 'test' then log 'hello' then repeat for i in <input/>
|
||||||
{% if not trainee.grade %}
|
set i.checked to not i.checked
|
||||||
class="no-grade"
|
">Toggle All</span>
|
||||||
{% endif %}
|
</th>
|
||||||
>{{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>
|
|
||||||
|
|
||||||
</tr>
|
</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>
|
<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>
|
<style>
|
||||||
.add-supervisor, .add-grade {
|
.add-supervisor, .add-grade {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
@@ -61,6 +81,14 @@
|
|||||||
opacity: 50%;
|
opacity: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.remove-trainee, .trainee-bulk-edit {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -3625,9 +3625,22 @@ class UpdateQuestionMixin(RedirectMixin, RevisionMixin, UpdateView):
|
|||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
kwargs.update({"user": self.request.user})
|
kwargs.update({"user": self.request.user})
|
||||||
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["question"] = context["object"]
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -506,7 +506,7 @@ class LongCreate(LongCreateBase):
|
|||||||
|
|
||||||
|
|
||||||
class LongUpdate(
|
class LongUpdate(
|
||||||
UpdateQuestionMixin,AuthorOrCheckerRequiredMixin
|
UpdateQuestionMixin
|
||||||
):
|
):
|
||||||
model = Long
|
model = Long
|
||||||
form_class = LongForm
|
form_class = LongForm
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from longs.models import Long
|
||||||
|
|
||||||
|
|
||||||
@@ -12,10 +12,24 @@
|
|||||||
This questions has unmarked answers. Are you sure you want to review?
|
This questions has unmarked answers. Are you sure you want to review?
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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 %}
|
{% if question.normal %}
|
||||||
<h3>This question is normal</h3>
|
<h3>This question is normal</h3>
|
||||||
Answers will be automatically marked.<br />
|
|
||||||
|
|
||||||
{% if incorrect_answers %}
|
{% if incorrect_answers %}
|
||||||
<p>The following answers have been submitted for this question</p>
|
<p>The following answers have been submitted for this question</p>
|
||||||
@@ -31,14 +45,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<h3>This question is abnormal</h3>
|
<h3>This question is abnormal</h3>
|
||||||
Answers given as normal will be automatically marked.<br />
|
|
||||||
Region: {{ question.get_regions }}, Abnormalities: {{ question.get_abnormalities }}<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>
|
Primary answer: <span id="primary-answer" title="The primary answer of the question">{{question.get_primary_answer}}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="marking">
|
<div class="marking">
|
||||||
<form method="POST" class="post-form">{% csrf_token %}
|
<form method="POST" class="post-form">{% csrf_token %}
|
||||||
{% if not question.normal %}
|
{% if not question.normal %}
|
||||||
Click each answer to toggle through marks awarded (as per colour)<br/>
|
|
||||||
{% if not review %}
|
{% if not review %}
|
||||||
{% if unmarked_exam_answers_only %}
|
{% 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>
|
Showing unmarked exam answers only <a href="{% url 'rapids:mark_all' exam.id question_number %}">(view all answers for question)</a>
|
||||||
|
|||||||
+1
-1
@@ -376,7 +376,7 @@ class RapidAnswerUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView)
|
|||||||
|
|
||||||
|
|
||||||
class RapidUpdate(
|
class RapidUpdate(
|
||||||
UpdateQuestionMixin,AuthorOrCheckerRequiredMixin
|
UpdateQuestionMixin
|
||||||
):
|
):
|
||||||
model = Rapid
|
model = Rapid
|
||||||
form_class = RapidForm
|
form_class = RapidForm
|
||||||
|
|||||||
+1
-1
@@ -318,7 +318,7 @@ class QuestionCreate(QuestionCreateBase):
|
|||||||
return initial
|
return initial
|
||||||
|
|
||||||
|
|
||||||
class QuestionUpdate(AuthorOrCheckerRequiredMixin, UpdateQuestionMixin):
|
class QuestionUpdate(UpdateQuestionMixin):
|
||||||
model = Question
|
model = Question
|
||||||
form_class = QuestionForm
|
form_class = QuestionForm
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user