diff options
| author | Gertjan van den Burg <gertjanvandenburg@gmail.com> | 2019-03-27 18:03:45 +0000 |
|---|---|---|
| committer | Gertjan van den Burg <gertjanvandenburg@gmail.com> | 2019-03-27 18:03:45 +0000 |
| commit | 839a428d2bf1f13ce2d7629146d3b6b59caa776c (patch) | |
| tree | 1a1059da734efc967194f53d81e166077ba0b3f3 | |
| parent | bugfixes for dockerfile (diff) | |
| download | AnnotateChange-839a428d2bf1f13ce2d7629146d3b6b59caa776c.tar.gz AnnotateChange-839a428d2bf1f13ce2d7629146d3b6b59caa776c.zip | |
Add support for automatic task assignment
| -rw-r--r-- | app/admin/forms.py | 16 | ||||
| -rw-r--r-- | app/admin/routes.py | 81 | ||||
| -rw-r--r-- | app/templates/admin/manage.html | 15 |
3 files changed, 95 insertions, 17 deletions
diff --git a/app/admin/forms.py b/app/admin/forms.py index 30070d2..3d76b5d 100644 --- a/app/admin/forms.py +++ b/app/admin/forms.py @@ -6,8 +6,8 @@ from flask import current_app from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileRequired -from wtforms import SubmitField, SelectField -from wtforms.validators import ValidationError, InputRequired +from wtforms import SubmitField, SelectField, IntegerField +from wtforms.validators import ValidationError, InputRequired, NumberRange from werkzeug.utils import secure_filename @@ -15,6 +15,18 @@ from app.models import Dataset from app.admin.datasets import validate_dataset, get_name_from_dataset +class AdminAutoAssignForm(FlaskForm): + max_per_user = IntegerField( + "Maximum Tasks per User", [NumberRange(min=0, max=10)], + default=5 + ) + num_per_dataset = IntegerField( + "Tasks per Dataset", [NumberRange(min=1, max=20)], + default=10 + ) + assign = SubmitField("Assign") + + class AdminManageTaskForm(FlaskForm): username = SelectField( "Username", coerce=int, validators=[InputRequired()] diff --git a/app/admin/routes.py b/app/admin/routes.py index 0317e44..0af4bb1 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os +import random from flask import render_template, flash, redirect, url_for, current_app @@ -11,6 +12,7 @@ from app.admin import bp from app.admin.datasets import get_name_from_dataset, md5sum from app.decorators import admin_required from app.admin.forms import ( + AdminAutoAssignForm, AdminManageTaskForm, AdminAddDatasetForm, AdminManageDatasetsForm, @@ -21,31 +23,79 @@ from app.models import User, Dataset, Task, Annotation @bp.route("/manage/tasks", methods=("GET", "POST")) @admin_required def manage_tasks(): + form_auto = AdminAutoAssignForm() + user_list = [(u.id, u.username) for u in User.query.all()] dataset_list = [(d.id, d.name) for d in Dataset.query.all()] - form = AdminManageTaskForm() - form.username.choices = user_list - form.dataset.choices = dataset_list + form_manual = AdminManageTaskForm() + form_manual.username.choices = user_list + form_manual.dataset.choices = dataset_list - if form.validate_on_submit(): - user = User.query.filter_by(id=form.username.data).first() + if form_auto.validate_on_submit(): + max_per_user = form_auto.max_per_user.data + num_per_dataset = form_auto.num_per_dataset.data + + available_users = {} + for user in User.query.all(): + user_tasks = Task.query.filter_by(annotator_id=user.id).all() + if len(user_tasks) < max_per_user: + available_users[user] = max_per_user - len(user_tasks) + + if not available_users: + flash( + "All users already have at least %i tasks assigned to them." + % max_per_user, + "error", + ) + return redirect(url_for("admin.manage_tasks")) + + datasets_tbd = {} + for dataset in Dataset.query.all(): + dataset_tasks = Task.query.filter_by(dataset_id=dataset.id).all() + if len(dataset_tasks) < num_per_dataset: + datasets_tbd[dataset] = num_per_dataset - len(dataset_tasks) + + if not datasets_tbd: + flash( + "All datasets have at least the desired number (%i) of assigned tasks." + % num_per_dataset, + "info", + ) + return redirect(url_for("admin.manage_tasks")) + + datasets = list(datasets_tbd.keys()) + random.shuffle(datasets) + for dataset in datasets: + available = [u for u, v in available_users.items() if v > 0] + tbd = min(len(available), datasets_tbd[dataset]) + selected_users = random.sample(available, tbd) + for user in selected_users: + task = Task(annotator_id=user.id, dataset_id=dataset.id) + db.session.add(task) + db.session.commit() + available_users[user] -= 1 + datasets_tbd[dataset] -= 1 + flash("Automatic task assignment successful.", "success") + + elif form_manual.validate_on_submit(): + user = User.query.filter_by(id=form_manual.username.data).first() if user is None: flash("User does not exist.", "error") return redirect(url_for("admin.manage_tasks")) - dataset = Dataset.query.filter_by(id=form.dataset.data).first() + dataset = Dataset.query.filter_by(id=form_manual.dataset.data).first() if dataset is None: flash("Dataset does not exist.", "error") return redirect(url_for("admin.manage_tasks")) action = None - if form.assign.data: + if form_manual.assign.data: action = "assign" - elif form.delete.data: + elif form_manual.delete.data: action = "delete" else: flash( - "Internal error: no button is true but form was submitted.", + "Internal error: no button is true but form_manual was submitted.", "error", ) return redirect(url_for("admin.manage_tasks")) @@ -74,9 +124,18 @@ def manage_tasks(): db.session.commit() flash("Task deleted successfully.", "success") - tasks = Task.query.join(User, Task.user).order_by(User.username).all() + tasks = ( + Task.query.join(User, Task.user) + .join(Dataset, Task.dataset) + .order_by(Dataset.name, User.username) + .all() + ) return render_template( - "admin/manage.html", title="Assign Task", form=form, tasks=tasks + "admin/manage.html", + title="Assign Task", + form_auto=form_auto, + form_manual=form_manual, + tasks=tasks, ) diff --git a/app/templates/admin/manage.html b/app/templates/admin/manage.html index 92aa6a5..e0c247a 100644 --- a/app/templates/admin/manage.html +++ b/app/templates/admin/manage.html @@ -2,10 +2,17 @@ {% import 'bootstrap/wtf.html' as wtf %} {% block app_content %} -<h1>Manage Tasks</h1> +<h1>Manage Tasks Automatically</h1> <div class="row"> <div class="col-md-4"> - {{ wtf.quick_form(form, button_map={'assign': 'primary', 'delete': 'danger'}) }} + {{ wtf.quick_form(form_auto, button_map={'assign': 'success'}) }} + </div> +</div> + +<h1>Manage Tasks Manually</h1> +<div class="row"> + <div class="col-md-4"> + {{ wtf.quick_form(form_manual, button_map={'assign': 'primary', 'delete': 'danger'}) }} </div> </div> <br> @@ -14,16 +21,16 @@ <table class="table table-striped"> <thead class="thead-dark"> <th scope="col">Task ID</th> - <th scope="col">Username</th> <th scope="col">Dataset Name</th> + <th scope="col">Username</th> <th scope="col">Status</th> <th scope="col">Completed On</th> </thead> {% for task in tasks %} <tr> <th scope="row">{{ task.id }}</th> - <td>{{ task.user.username }}</td> <td>{{ task.dataset.name }}</td> + <td>{{ task.user.username }}</td> <td>{% if task.done %}Completed{% else %}Pending{% endif %}</td> <td>{% if task.done %}{{ task.annotated_on }}{% else %}{% endif %}</td> </tr> |
