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)
|
||||
|
||||
# class AnatomyQuestionUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin,
|
||||
class AnatomyQuestionUpdate(UpdateQuestionMixin,AuthorOrCheckerRequiredMixin):
|
||||
class AnatomyQuestionUpdate(UpdateQuestionMixin):
|
||||
model = AnatomyQuestion
|
||||
form_class = AnatomyQuestionForm
|
||||
|
||||
|
||||
@@ -95,6 +95,8 @@ class SupervisorFilter(django_filters.FilterSet):
|
||||
fields = {
|
||||
"name": ["contains"],
|
||||
"email": ["contains"],
|
||||
"active": ["exact"],
|
||||
"site": ["exact"],
|
||||
}
|
||||
|
||||
def __init__(
|
||||
|
||||
+10
-1
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
UpdateQuestionMixin,AuthorOrCheckerRequiredMixin
|
||||
UpdateQuestionMixin
|
||||
):
|
||||
model = Long
|
||||
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?
|
||||
</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
@@ -376,7 +376,7 @@ class RapidAnswerUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView)
|
||||
|
||||
|
||||
class RapidUpdate(
|
||||
UpdateQuestionMixin,AuthorOrCheckerRequiredMixin
|
||||
UpdateQuestionMixin
|
||||
):
|
||||
model = Rapid
|
||||
form_class = RapidForm
|
||||
|
||||
+1
-1
@@ -318,7 +318,7 @@ class QuestionCreate(QuestionCreateBase):
|
||||
return initial
|
||||
|
||||
|
||||
class QuestionUpdate(AuthorOrCheckerRequiredMixin, UpdateQuestionMixin):
|
||||
class QuestionUpdate(UpdateQuestionMixin):
|
||||
model = Question
|
||||
form_class = QuestionForm
|
||||
|
||||
|
||||
Reference in New Issue
Block a user