.
This commit is contained in:
+12
-3
@@ -7,11 +7,21 @@ from generic.models import CidUser
|
|||||||
|
|
||||||
|
|
||||||
class CidUserFilter(django_filters.FilterSet):
|
class CidUserFilter(django_filters.FilterSet):
|
||||||
active = django_filters.BooleanFilter(field_name="active", label="CID active", initial=True)
|
active = django_filters.BooleanFilter(
|
||||||
|
field_name="active", label="CID active", initial=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CidUser
|
model = CidUser
|
||||||
fields = ("cid", "active", "internal_candidate", "group")
|
# fields = ("cid", "active", "internal_candidate", "group")
|
||||||
|
fields = {
|
||||||
|
"cid": ["contains"],
|
||||||
|
"active": ["exact"],
|
||||||
|
"internal_candidate": ["exact"],
|
||||||
|
"group": ["exact"],
|
||||||
|
"group__name": ["contains"],
|
||||||
|
"email": ["contains"],
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def qs(self):
|
def qs(self):
|
||||||
@@ -22,4 +32,3 @@ class CidUserFilter(django_filters.FilterSet):
|
|||||||
return parent.filter(active=True)
|
return parent.filter(active=True)
|
||||||
|
|
||||||
return parent
|
return parent
|
||||||
|
|
||||||
|
|||||||
@@ -595,6 +595,11 @@ class CidUser(models.Model):
|
|||||||
def get_email_results_resend_url(self):
|
def get_email_results_resend_url(self):
|
||||||
return reverse("generic:candidate_email_results_resend", kwargs={"cid": self.cid})
|
return reverse("generic:candidate_email_results_resend", kwargs={"cid": self.cid})
|
||||||
|
|
||||||
|
def get_next_cid():
|
||||||
|
"""Returns the next available CID"""
|
||||||
|
return CidUser.objects.order_by("-cid").first().cid + 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CidUserExam(models.Model):
|
class CidUserExam(models.Model):
|
||||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<button id="deactivate-selected-cids">Deactivate selected</button>
|
<button id="deactivate-selected-cids">Deactivate selected</button>
|
||||||
<button id="delete-selected-cids">Delete selected</button>
|
<button id="delete-selected-cids">Delete selected</button>
|
||||||
<button id="toggle-internal">Toggle internal</button>
|
<button id="toggle-internal">Toggle internal</button>
|
||||||
|
<button id="add-group">Add Group to users</button>
|
||||||
<p>Select exams below to add user(s) to (or edit existing user(s)).</p>
|
<p>Select exams below to add user(s) to (or edit existing user(s)).</p>
|
||||||
<div class="exam-selectors">
|
<div class="exam-selectors">
|
||||||
<span>
|
<span>
|
||||||
@@ -103,12 +104,12 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</select>
|
</select>
|
||||||
<button id="add-group">Add Group to users</button>
|
|
||||||
</div>
|
</div>
|
||||||
<button id="add-new-cids">Add New (Blank) Users</button>
|
<h3>Create New Users</h3>
|
||||||
|
<button id="add-new-cids" title="This will create N new users">Add New (Blank) Users</button>
|
||||||
<input type="number" id="add-number" value="number to add"><br />
|
<input type="number" id="add-number" value="number to add"><br />
|
||||||
<button id="add-new-emails">Add New Users From Email</button>
|
<button id="add-new-emails" title="This will create users from a list of email addresses">Add New Users From Email</button>
|
||||||
<textarea id="emails-input" name="emails" rows="4" cols="50">Enter emails here</textarea>
|
<textarea id="emails-input" name="emails" rows="4" cols="50" placeholder="Enter emails here (single email per line)"></textarea>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -116,51 +117,51 @@
|
|||||||
{% block js %}
|
{% block js %}
|
||||||
|
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs4/dt-1.11.3/r-2.2.9/datatables.min.css"/>
|
{% comment %} <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs4/dt-1.11.3/r-2.2.9/datatables.min.css"/>
|
||||||
|
|
||||||
<script type="text/javascript" src="https://cdn.datatables.net/v/bs4/dt-1.11.3/r-2.2.9/datatables.min.js"></script>
|
<script type="text/javascript" src="https://cdn.datatables.net/v/bs4/dt-1.11.3/r-2.2.9/datatables.min.js"></script> {% endcomment %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
jQuery.fn.dataTable.render.ellipsis = function ( cutoff, wordbreak, escapeHtml ) {
|
// jQuery.fn.dataTable.render.ellipsis = function ( cutoff, wordbreak, escapeHtml ) {
|
||||||
var esc = function ( t ) {
|
//var esc = function ( t ) {
|
||||||
return t
|
//return t
|
||||||
.replace( /&/g, '&' )
|
//.replace( /&/g, '&' )
|
||||||
.replace( /</g, '<' )
|
//.replace( /</g, '<' )
|
||||||
.replace( />/g, '>' )
|
//.replace( />/g, '>' )
|
||||||
.replace( /"/g, '"' );
|
//.replace( /"/g, '"' );
|
||||||
};
|
//};
|
||||||
|
|
||||||
return function ( d, type, row ) {
|
//return function ( d, type, row ) {
|
||||||
// Order, search and type get the original data
|
//// Order, search and type get the original data
|
||||||
if ( type !== 'display' ) {
|
//if ( type !== 'display' ) {
|
||||||
return d;
|
//return d;
|
||||||
}
|
//}
|
||||||
|
|
||||||
if ( typeof d !== 'number' && typeof d !== 'string' ) {
|
//if ( typeof d !== 'number' && typeof d !== 'string' ) {
|
||||||
return d;
|
//return d;
|
||||||
}
|
//}
|
||||||
|
|
||||||
d = d.toString(); // cast numbers
|
//d = d.toString(); // cast numbers
|
||||||
|
|
||||||
if ( d.length <= cutoff ) {
|
//if ( d.length <= cutoff ) {
|
||||||
return d;
|
//return d;
|
||||||
}
|
//}
|
||||||
|
|
||||||
var shortened = d.substr(0, cutoff-1);
|
//var shortened = d.substr(0, cutoff-1);
|
||||||
|
|
||||||
// Find the last white space character in the string
|
//// Find the last white space character in the string
|
||||||
if ( wordbreak ) {
|
//if ( wordbreak ) {
|
||||||
shortened = shortened.replace(/\s([^\s]*)$/, '');
|
//shortened = shortened.replace(/\s([^\s]*)$/, '');
|
||||||
}
|
//}
|
||||||
|
|
||||||
// Protect against uncontrolled HTML input
|
//// Protect against uncontrolled HTML input
|
||||||
if ( escapeHtml ) {
|
//if ( escapeHtml ) {
|
||||||
shortened = esc( shortened );
|
//shortened = esc( shortened );
|
||||||
}
|
//}
|
||||||
|
|
||||||
return '<span class="ellipsis" title="'+esc(d)+'">'+shortened+'…</span>';
|
//return '<span class="ellipsis" title="'+esc(d)+'">'+shortened+'…</span>';
|
||||||
};
|
//};
|
||||||
};
|
//};
|
||||||
|
|
||||||
function postAjax(data) {
|
function postAjax(data) {
|
||||||
|
|
||||||
@@ -177,6 +178,8 @@
|
|||||||
if (data.status == "success") {
|
if (data.status == "success") {
|
||||||
toastr.info('Added/Updated');
|
toastr.info('Added/Updated');
|
||||||
location.reload();
|
location.reload();
|
||||||
|
} else {
|
||||||
|
toastr.error(data.details)
|
||||||
}
|
}
|
||||||
// show some message according to the response.
|
// show some message according to the response.
|
||||||
// For eg. A message box showing that the status has been changed
|
// For eg. A message box showing that the status has been changed
|
||||||
@@ -215,19 +218,19 @@
|
|||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
|
||||||
{% if exams %}
|
// {% if exams %}
|
||||||
$("table").DataTable({
|
//$("table").DataTable({
|
||||||
columnDefs: [ {
|
//columnDefs: [ {
|
||||||
targets: [6,7,8,9,10],
|
//targets: [6,7,8,9,10],
|
||||||
render: $.fn.dataTable.render.ellipsis( 17, true )
|
//render: $.fn.dataTable.render.ellipsis( 17, true )
|
||||||
} ],
|
//} ],
|
||||||
"paging": false,
|
//"paging": false,
|
||||||
});
|
//});
|
||||||
{% else %}
|
//{% else %}
|
||||||
$("table").DataTable({
|
//$("table").DataTable({
|
||||||
"paging": false,
|
//"paging": false,
|
||||||
});
|
//});
|
||||||
{% endif %}
|
//{% endif %}
|
||||||
|
|
||||||
$("#toggle-internal").click((evt) => {
|
$("#toggle-internal").click((evt) => {
|
||||||
data = getGenericData();
|
data = getGenericData();
|
||||||
@@ -237,6 +240,7 @@
|
|||||||
$("#add-new-emails").click((evt) => {
|
$("#add-new-emails").click((evt) => {
|
||||||
data = getGenericData();
|
data = getGenericData();
|
||||||
data["emails"] = JSON.stringify($("#emails-input").val());
|
data["emails"] = JSON.stringify($("#emails-input").val());
|
||||||
|
data["add_new_emails"] = true;
|
||||||
postAjax(data);
|
postAjax(data);
|
||||||
});
|
});
|
||||||
$("#add-group").click((evt) => {
|
$("#add-group").click((evt) => {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
})
|
})
|
||||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
console.log(data);
|
console.log("done", data);
|
||||||
|
|
||||||
if (data.status == "success") {
|
if (data.status == "success") {
|
||||||
if (data.added) {
|
if (data.added) {
|
||||||
@@ -50,7 +50,14 @@
|
|||||||
toastr.info(`User ${el.target.dataset.cid} removed from exam`)
|
toastr.info(`User ${el.target.dataset.cid} removed from exam`)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
toastr.error(data.status)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.fail(function (data) {
|
||||||
|
console.log("fail", data);
|
||||||
|
toastr.error(data.responseJSON.status)
|
||||||
|
|
||||||
})
|
})
|
||||||
.always(function () {
|
.always(function () {
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.filter(name='has_group')
|
||||||
|
def has_group(user, group_name):
|
||||||
|
return user.groups.filter(name=group_name).exists()
|
||||||
@@ -90,6 +90,7 @@ urlpatterns = [
|
|||||||
name="user_group_create",
|
name="user_group_create",
|
||||||
),
|
),
|
||||||
path("cids/create", views.manage_cid_users, name="manage_cid_users"),
|
path("cids/create", views.manage_cid_users, name="manage_cid_users"),
|
||||||
|
path("cids/create/email", views.create_cid_email, name="create_cid_email"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+38
-6
@@ -51,7 +51,7 @@ from .forms import (
|
|||||||
UserUserGroupForm,
|
UserUserGroupForm,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import CidUser, CidUserGroup, Examination, QuestionNote, UserUserGroup
|
from .models import CidUser, CidUserGroup, Examination, QuestionNote, UserUserGroup, get_next_cid
|
||||||
|
|
||||||
from rapids.models import Rapid as RapidQuestion
|
from rapids.models import Rapid as RapidQuestion
|
||||||
from rapids.models import Exam as RapidExam
|
from rapids.models import Exam as RapidExam
|
||||||
@@ -366,6 +366,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
if exam.authors_only:
|
if exam.authors_only:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if user.groups.filter(name="cid_user_manager").exists():
|
||||||
|
return True
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.app_name == "rapids"
|
self.app_name == "rapids"
|
||||||
and not user.groups.filter(name="rapid_checker").exists()
|
and not user.groups.filter(name="rapid_checker").exists()
|
||||||
@@ -544,7 +547,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
exam_list = self.Exam.objects.all().order_by("name")
|
exam_list = self.Exam.objects.all().order_by("name")
|
||||||
|
|
||||||
if not all:
|
if not all:
|
||||||
exams = exam_list.filter(exam_mode=True, archive=False).order_by("name")
|
exam_list = exam_list.filter(exam_mode=True, archive=False).order_by("name")
|
||||||
|
|
||||||
marking = False
|
marking = False
|
||||||
|
|
||||||
@@ -555,7 +558,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
request,
|
request,
|
||||||
"exam_list.html".format(self.app_name),
|
"exam_list.html".format(self.app_name),
|
||||||
{
|
{
|
||||||
"exams": exams,
|
"exams": exam_list,
|
||||||
"app_name": self.app_name,
|
"app_name": self.app_name,
|
||||||
"view_all": all,
|
"view_all": all,
|
||||||
"marking": marking,
|
"marking": marking,
|
||||||
@@ -947,7 +950,12 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
def exam_json_edit(self, request, pk):
|
def exam_json_edit(self, request, pk):
|
||||||
if request.is_ajax() and request.method == "POST":
|
if request.is_ajax() and request.method == "POST":
|
||||||
|
|
||||||
if not self.check_user_edit_access(request.user, exam_id=pk):
|
|
||||||
|
if request.user.groups.filter(name="cid_user_manager").exists():
|
||||||
|
# This may give more permissions than we actually want
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif not self.check_user_edit_access(request.user, exam_id=pk):
|
||||||
data = {"status": "invalid permisions"}
|
data = {"status": "invalid permisions"}
|
||||||
return JsonResponse(data, status=400)
|
return JsonResponse(data, status=400)
|
||||||
|
|
||||||
@@ -964,6 +972,8 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
app_exam_map["rapids"] = cid_user.rapid_exams
|
app_exam_map["rapids"] = cid_user.rapid_exams
|
||||||
app_exam_map["anatomy"] = cid_user.anatomy_exams
|
app_exam_map["anatomy"] = cid_user.anatomy_exams
|
||||||
app_exam_map["longs"] = cid_user.longs_exams
|
app_exam_map["longs"] = cid_user.longs_exams
|
||||||
|
app_exam_map["physics"] = cid_user.physics_exams
|
||||||
|
app_exam_map["sbas"] = cid_user.sba_exams
|
||||||
app_exam_map["atlas"] = cid_user.casecollection_exams
|
app_exam_map["atlas"] = cid_user.casecollection_exams
|
||||||
|
|
||||||
if add:
|
if add:
|
||||||
@@ -985,6 +995,8 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
app_exam_map["rapids"] = user_user.user_rapid_exams
|
app_exam_map["rapids"] = user_user.user_rapid_exams
|
||||||
app_exam_map["anatomy"] = user_user.user_anatomy_exams
|
app_exam_map["anatomy"] = user_user.user_anatomy_exams
|
||||||
app_exam_map["longs"] = user_user.user_longs_exams
|
app_exam_map["longs"] = user_user.user_longs_exams
|
||||||
|
app_exam_map["physics"] = user_user.user_physics_exams
|
||||||
|
app_exam_map["sbas"] = user_user.user_sba_exams
|
||||||
# app_exam_map["atlas"] = user_user.casecollection_exams
|
# app_exam_map["atlas"] = user_user.casecollection_exams
|
||||||
|
|
||||||
if add:
|
if add:
|
||||||
@@ -2187,6 +2199,9 @@ def candidate_email_results(request, cid, resend=False):
|
|||||||
def candidate_email_results_resend(request, cid, resend=True):
|
def candidate_email_results_resend(request, cid, resend=True):
|
||||||
return candidate_email_results(request, cid, resend=True)
|
return candidate_email_results(request, cid, resend=True)
|
||||||
|
|
||||||
|
@user_is_cid_user_manager
|
||||||
|
def create_cid_email(request):
|
||||||
|
pass
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
def manage_cid_users(request):
|
def manage_cid_users(request):
|
||||||
@@ -2203,6 +2218,7 @@ def manage_cid_users(request):
|
|||||||
|
|
||||||
emails = json.loads(request.POST.get("emails"))
|
emails = json.loads(request.POST.get("emails"))
|
||||||
|
|
||||||
|
|
||||||
number_to_create = int(request.POST.get("number_to_create"))
|
number_to_create = int(request.POST.get("number_to_create"))
|
||||||
|
|
||||||
add_group = request.POST.get("add_group")
|
add_group = request.POST.get("add_group")
|
||||||
@@ -2291,12 +2307,28 @@ def manage_cid_users(request):
|
|||||||
return JsonResponse({"status": "success"})
|
return JsonResponse({"status": "success"})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
new_cid = CidUser.objects.all().order_by("-cid")[0].cid + 1
|
new_cid = get_next_cid()
|
||||||
except IndexError:
|
except IndexError:
|
||||||
new_cid = 10000
|
new_cid = 10000
|
||||||
|
|
||||||
if emails:
|
if "add_new_emails" in request.POST:
|
||||||
email_list = [i.strip() for i in emails.split()]
|
email_list = [i.strip() for i in emails.split()]
|
||||||
|
|
||||||
|
# Verify emails are all valid
|
||||||
|
invalid_emails = []
|
||||||
|
for email in email_list:
|
||||||
|
try:
|
||||||
|
validate_email(email)
|
||||||
|
except ValidationError as e:
|
||||||
|
invalid_emails.append(f"Invalid email: {email}")
|
||||||
|
|
||||||
|
print(f"Email list {email_list}")
|
||||||
|
if not email_list:
|
||||||
|
return JsonResponse({"status": "fail", "details": "No valid emails"})
|
||||||
|
if invalid_emails or not email_list:
|
||||||
|
e = "<br/>".join(invalid_emails)
|
||||||
|
return JsonResponse({"status": "fail", "details": f"Unable to create users<hr>{e}"})
|
||||||
|
|
||||||
for e in email_list:
|
for e in email_list:
|
||||||
passcode = "".join(random.choices(string.ascii_uppercase, k=4))
|
passcode = "".join(random.choices(string.ascii_uppercase, k=4))
|
||||||
c = CidUser(cid=new_cid, passcode=passcode, email=e)
|
c = CidUser(cid=new_cid, passcode=passcode, email=e)
|
||||||
|
|||||||
+7
-3
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
from cProfile import label
|
from cProfile import label
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
@@ -7,15 +6,20 @@ from generic.models import UserGrades
|
|||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
|
||||||
class UserListFilter(django_filters.FilterSet):
|
class UserListFilter(django_filters.FilterSet):
|
||||||
|
|
||||||
userprofile__grade = django_filters.ModelChoiceFilter(label="Grade", queryset=UserGrades.objects.all())
|
userprofile__grade = django_filters.ModelChoiceFilter(
|
||||||
|
label="Grade", queryset=UserGrades.objects.all()
|
||||||
|
)
|
||||||
# test2 = django_filters.CharFilter()
|
# test2 = django_filters.CharFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = {
|
fields = {
|
||||||
"username": ["contains"], "email": ["contains"], "userprofile__grade": ["exact"]
|
"username": ["contains"],
|
||||||
|
"email": ["contains"],
|
||||||
|
"userprofile__grade": ["exact"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
|
# def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
<!DOCTYPE HTML>
|
<!DOCTYPE HTML>
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load django_htmx %}
|
{% load django_htmx %}
|
||||||
|
{% load auth_extras %}
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
@@ -91,7 +92,7 @@
|
|||||||
<span id="atlas-link" class="top-bar-link">
|
<span id="atlas-link" class="top-bar-link">
|
||||||
<a href="{% url 'atlas:index' %}">Atlas</a>
|
<a href="{% url 'atlas:index' %}">Atlas</a>
|
||||||
</span>
|
</span>
|
||||||
{% if perms.generic.can_add_cid_user %}
|
{% if request.user|has_group:"cid_user_manager" %}
|
||||||
<span id="candidate-link" class="top-bar-link">
|
<span id="candidate-link" class="top-bar-link">
|
||||||
<a href="{% url 'generic:manage_cids' %}">Candidates</a>
|
<a href="{% url 'generic:manage_cids' %}">Candidates</a>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user