Skip to content

Commit

Permalink
basic bulk assignment page
Browse files Browse the repository at this point in the history
Takes an excel sheet with specific columns and attempts to automatically
assign users to sections and authorities for the current session.
  • Loading branch information
struan committed May 15, 2024
1 parent 55658b7 commit c2719f7
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 2 deletions.
5 changes: 5 additions & 0 deletions ceuk-marking/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@
volunteers.AvailableAssignmentAuthorities.as_view(),
name="available_authorities",
),
path(
"volunteers/bulk/",
volunteers.BulkAssignVolunteer.as_view(),
name="bulk_assign_volunteer",
),
path(
"volunteers/<user_id>/assign/",
volunteers.VolunteerAssignentView.as_view(),
Expand Down
135 changes: 135 additions & 0 deletions crowdsourcer/forms.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
from collections import defaultdict

from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import validate_comma_separated_integer_list
from django.db.models.query import QuerySet
from django.forms import (
BaseFormSet,
BooleanField,
CharField,
CheckboxSelectMultiple,
ChoiceField,
FileField,
Form,
HiddenInput,
IntegerField,
ModelForm,
Select,
Textarea,
Expand All @@ -13,12 +22,16 @@
inlineformset_factory,
)

import pandas as pd

from crowdsourcer.models import (
Assigned,
Marker,
MarkingSession,
Option,
PublicAuthority,
Response,
ResponseType,
Section,
)

Expand Down Expand Up @@ -424,3 +437,125 @@ class Meta:
VolunteerAssignmentFormset = inlineformset_factory(
User, Assigned, form=VolunteerAssignentForm, extra=1
)


class VolunteerBulkAssignForm(Form):
volunteer_list = FileField(
required=True,
label="Volunteer list (Excel file)",
help_text="Volunteers will be loaded from sheet 'Volunteers' using column headers 'First Name', 'Last Name', 'Email', 'Assigned Section'",
)
num_assignments = IntegerField(
required=True,
label="Number of assignments",
help_text="Number of assigments to make per volunteer",
)
response_type = ChoiceField(
required=True, choices=[(rt.type, rt.type) for rt in ResponseType.objects.all()]
)
session = CharField(required=True, widget=HiddenInput)
always_assign = BooleanField(
required=False, help_text="Override checks and assign as much as possible"
)

def clean(self):
data = self.cleaned_data.get("volunteer_list")

try:
df = pd.read_excel(
data,
usecols=[
"First Name",
"Last Name",
"Email",
"Council Area",
"Assigned Section",
],
sheet_name="Volunteers",
)
except ValueError as v:
raise ValidationError(f"Problem processing excel file: {v}")

errors = []
section_names = pd.unique(df["Assigned Section"])
for section in section_names:
try:
s = Section.objects.get(title=section)
except s.DoesNotExist:
errors.append(
ValidationError(
f"Cannot assign to section '{section}', it does not exist"
)
)

# need to correct this before we can do any further processing because otherwise
# the assigment maths will be off
if errors:
raise ValidationError(errors)

if not self.cleaned_data["always_assign"]:
rt = ResponseType.objects.get(type=self.cleaned_data["response_type"])
ms = MarkingSession.objects.get(label=self.cleaned_data["session"])
num_assignments = self.cleaned_data.get("num_assignments")

max_assignments = {}
sections = Section.objects.filter(marking_session=ms)
for section in sections:
assigned = Assigned.objects.filter(
marking_session=ms,
section=section,
response_type=rt,
).values("authority")
max_assignments[section.title] = (
PublicAuthority.objects.exclude(id__in=assigned)
.filter(questiongroup__marking_session=ms)
.count()
)

section_assignments = defaultdict(list)
users_assigned = Assigned.objects.filter(
marking_session=ms, response_type=rt
).values_list("user__email", flat=True)
for _, row in df.iterrows():
if row["Email"] in users_assigned:
continue

section_assignments[row["Assigned Section"]].append(
{
"first_name": row["First Name"],
"last_name": row["Last Name"],
"email": row["Email"],
}
)

if not section_assignments.keys():
errors.append(
"No assignments will be made, all volunteers must already have assignements"
)

for section, volunteers in section_assignments.items():
required_volunteer_assignments = len(volunteers) * num_assignments

if required_volunteer_assignments > max_assignments[section]:
assignments_required = max_assignments[section] / len(volunteers)
errors.append(
ValidationError(
f"Too many volunteers for {section}, not all will volunteers will get assignments. Need {assignments_required} per volunteer"
)
)

num_assignments_required = max_assignments[section] / num_assignments
if num_assignments_required > len(volunteers):
volunteers_required = (
max_assignments[section] - len(volunteers)
) / num_assignments
errors.append(
ValidationError(
f"Not enough volunteers for {section}, not all entities will have volunteers - {volunteers_required} more volunteers needed"
)
)

if errors:
raise ValidationError(errors)

self.volunteer_df = df
20 changes: 20 additions & 0 deletions crowdsourcer/templates/crowdsourcer/volunteers/bulk_assign.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% extends 'crowdsourcer/base.html' %}

{% load crowdsourcer_tags django_bootstrap5 %}

{% block content %}
{% if show_login %}
<h1 class="mb-4">Sign in</h1>
<a href="{% url 'login' %}">Sign in</a>
{% else %}
<h1 class="mb-4">Bulk Volunteer Assignment</h1>


<form enctype="multipart/form-data" action="" method="post">
{% csrf_token %}
{% bootstrap_form form %}
<input type="submit" value="Assign">
</form>

{% endif %}
{% endblock %}
76 changes: 74 additions & 2 deletions crowdsourcer/views/volunteers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@
from django.urls import reverse
from django.views.generic import FormView, ListView

from crowdsourcer.forms import MarkerFormset, UserForm, VolunteerAssignmentFormset
from crowdsourcer.models import Assigned, PublicAuthority
from crowdsourcer.forms import (
MarkerFormset,
UserForm,
VolunteerAssignmentFormset,
VolunteerBulkAssignForm,
)
from crowdsourcer.models import Assigned, Marker, PublicAuthority, ResponseType, Section

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -158,3 +163,70 @@ def render_to_response(self, context, **response_kwargs):
data.append({"name": a.name, "id": a.id})

return JsonResponse({"results": data})


class BulkAssignVolunteer(VolunteerAccessMixin, FormView):
template_name = "crowdsourcer/volunteers/bulk_assign.html"
form_class = VolunteerBulkAssignForm

def get_initial(self):
kwargs = super().get_initial()
kwargs["session"] = self.request.current_session.label
return kwargs

def get_success_url(self):
return reverse(
"session_urls:list_volunteers",
kwargs={"marking_session": self.request.current_session.label},
)

def form_valid(self, form):
ms = self.request.current_session
rt = ResponseType.objects.get(type=form.cleaned_data.get("response_type"))

for _, row in form.volunteer_df.iterrows():
u, c = User.objects.update_or_create(
username=row["Email"],
defaults={
"email": row["Email"],
"first_name": row["First Name"],
"last_name": row["Last Name"],
},
)
u.save()

m, c = Marker.objects.update_or_create(user=u, response_type=rt)
m.marking_session.add(ms)

max_assignments = form.cleaned_data["num_assignments"]
num_assignments = max_assignments
existing_assignments = Assigned.objects.filter(
user=u, marking_session=ms, response_type=rt
).count()

if existing_assignments >= max_assignments:
continue

num_assignments = max_assignments - existing_assignments

section = Section.objects.get(title=row["Assigned Section"])
assigned = Assigned.objects.filter(
marking_session=ms,
section=section,
response_type=rt,
).values("authority")

to_assign = PublicAuthority.objects.filter(
questiongroup__marking_session=ms
).exclude(id__in=assigned)[:num_assignments]

for a in to_assign:
a, c = Assigned.objects.update_or_create(
user=u,
marking_session=ms,
response_type=rt,
section=section,
authority=a,
)

return super().form_valid(form)

0 comments on commit c2719f7

Please sign in to comment.