diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/admin/forms.py | 14 | ||||
| -rw-r--r-- | app/admin/routes.py | 41 | ||||
| -rw-r--r-- | app/auth/routes.py | 26 | ||||
| -rw-r--r-- | app/main/demo.py | 15 | ||||
| -rw-r--r-- | app/main/routes.py | 17 | ||||
| -rw-r--r-- | app/templates/admin/manage.html | 11 | ||||
| -rw-r--r-- | app/utils/tasks.py | 124 |
7 files changed, 106 insertions, 142 deletions
diff --git a/app/admin/forms.py b/app/admin/forms.py index bd1dce6..5ff5361 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, IntegerField -from wtforms.validators import ValidationError, InputRequired, NumberRange +from wtforms import SubmitField, SelectField +from wtforms.validators import ValidationError, InputRequired from werkzeug.utils import secure_filename @@ -15,16 +15,6 @@ from app.models import Dataset from app.utils.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 - ) - submit = SubmitField("Submit") - - class AdminManageTaskForm(FlaskForm): username = SelectField( "Username", coerce=int, validators=[InputRequired()] diff --git a/app/admin/routes.py b/app/admin/routes.py index f0d7817..d244a77 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -10,7 +10,6 @@ from app import db from app.admin import bp from app.decorators import admin_required from app.admin.forms import ( - AdminAutoAssignForm, AdminManageTaskForm, AdminAddDatasetForm, AdminManageDatasetsForm, @@ -24,54 +23,38 @@ from app.utils.datasets import ( dataset_is_demo, load_data_for_chart, ) -from app.utils.tasks import generate_auto_assign_tasks @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.order_by(Dataset.name).all() ] - form_manual = AdminManageTaskForm() - form_manual.username.choices = user_list - form_manual.dataset.choices = dataset_list - - if form_auto.validate_on_submit() and form_auto.submit.data: - max_per_user = form_auto.max_per_user.data - num_per_dataset = form_auto.num_per_dataset.data + form = AdminManageTaskForm() + form.username.choices = user_list + form.dataset.choices = dataset_list - for task, error in generate_auto_assign_tasks( - max_per_user, num_per_dataset - ): - if task is None: - flash(error, "error") - return redirect(url_for("admin.manage_tasks")) - db.session.add(task) - db.session.commit() - flash("Automatic task assignment successful.", "success") - elif form_manual.validate_on_submit(): - user = User.query.filter_by(id=form_manual.username.data).first() + if form.validate_on_submit(): + user = User.query.filter_by(id=form.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_manual.dataset.data).first() + dataset = Dataset.query.filter_by(id=form.dataset.data).first() if dataset is None: flash("Dataset does not exist.", "error") return redirect(url_for("admin.manage_tasks")) action = None - if form_manual.assign.data: + if form.assign.data: action = "assign" - elif form_manual.delete.data: + elif form.delete.data: action = "delete" else: flash( - "Internal error: no button is true but form_manual was submitted.", + "Internal error: no button is true but form was submitted.", "error", ) return redirect(url_for("admin.manage_tasks")) @@ -107,11 +90,7 @@ def manage_tasks(): .all() ) return render_template( - "admin/manage.html", - title="Assign Task", - form_auto=form_auto, - form_manual=form_manual, - tasks=tasks, + "admin/manage.html", title="Assign Task", form=form, tasks=tasks ) diff --git a/app/auth/routes.py b/app/auth/routes.py index 27b0de0..bc5e9b3 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -20,7 +20,8 @@ from app.auth.email import ( send_password_reset_email, send_email_confirmation_email, ) -from app.models import User +from app.models import User, Task +from app.utils.tasks import generate_user_task @bp.route("/login", methods=("GET", "POST")) @@ -32,13 +33,36 @@ def login(): flash("Invalid username or password", "error") return redirect(url_for("auth.login")) login_user(user, remember=form.remember_me.data) + # record last_active time current_user.last_active = datetime.datetime.utcnow() db.session.commit() + + # redirect if not confirmed yet if not user.is_confirmed: return redirect(url_for("auth.not_confirmed")) + + # Get the next page from the request (default to index) next_page = request.args.get("next") if not next_page or url_parse(next_page).netloc != "": next_page = url_for("main.index") + + # redirect if not introduced yet + if not user.is_introduced: + return redirect(url_for("main.index")) + + # assign task if no remaining and not at maximum. + remaining = Task.query.filter_by( + annotator_id=user.id, done=False + ).all() + if remaining: + return redirect(next_page) + + task = generate_user_task(user) + if task is None: + return redirect(next_page) + + db.session.add(task) + db.session.commit() return redirect(next_page) return render_template("auth/login.html", title="Sign In", form=form) diff --git a/app/main/demo.py b/app/main/demo.py index a126fd8..a5dbc0f 100644 --- a/app/main/demo.py +++ b/app/main/demo.py @@ -23,6 +23,7 @@ from app.main import bp from app.main.forms import NextForm from app.main.routes import RUBRIC from app.utils.datasets import load_data_for_chart, get_demo_true_cps +from app.utils.tasks import generate_user_task LOGGER = logging.getLogger(__name__) @@ -253,13 +254,21 @@ def redirect_user(demo_id, phase_id): last_demo_id = max(DEMO_DATA.keys()) demo_last_phase_id = 3 if demo_id == last_demo_id and phase_id == demo_last_phase_id: - # User is introduced. + # User is already introduced (happens if they redo the demo) if current_user.is_introduced: return redirect(url_for("main.index")) + # mark user as introduced current_user.is_introduced = True db.session.commit() - # TODO: Assign real tasks to the user here. + + # assign a task to the user + task = generate_user_task(current_user) + if task is None: + return redirect(url_for("main.index")) + db.session.add(task) + db.session.commit() + return redirect(url_for("main.index")) elif phase_id == demo_last_phase_id: demo_id += 1 @@ -352,7 +361,7 @@ def demo_annotate(demo_id): if dataset is None: LOGGER.error( "Demo requested unavailable dataset: %s" - % demo_data["dataset"]["name"] + % DEMO_DATA[demo_id]["dataset"]["name"] ) flash( "An internal error occured. The administrator has been notified. We apologise for the inconvenience, please try again later.", diff --git a/app/main/routes.py b/app/main/routes.py index 11de2f9..a0033b1 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -11,6 +11,7 @@ from app.decorators import login_required from app.main import bp from app.models import Annotation, Task from app.utils.datasets import load_data_for_chart +from app.utils.tasks import generate_user_task logger = logging.getLogger(__name__) @@ -31,7 +32,9 @@ def index(): user_id = current_user.id tasks = Task.query.filter_by(annotator_id=user_id).all() tasks_done = [t for t in tasks if t.done and not t.dataset.is_demo] - tasks_todo = [t for t in tasks if not t.done] + tasks_todo = [ + t for t in tasks if (not t.done) and (not t.dataset.is_demo) + ] return render_template( "index.html", title="Home", @@ -78,8 +81,18 @@ def annotate(task_id): task.done = True task.annotated_on = now db.session.commit() - flash("Your annotation has been recorded, thank you!", "success") + + # assign a new task if necessary + task = generate_user_task(current_user) + if task is None: + return url_for("main.index") + db.session.add(task) + db.session.commit() + flash( + "A new dataset has been assigned for you to annotate. Thanks for your help!", + "info", + ) return url_for("main.index") task = Task.query.filter_by(id=task_id).first() diff --git a/app/templates/admin/manage.html b/app/templates/admin/manage.html index e0c247a..5c10e02 100644 --- a/app/templates/admin/manage.html +++ b/app/templates/admin/manage.html @@ -2,17 +2,10 @@ {% import 'bootstrap/wtf.html' as wtf %} {% block app_content %} -<h1>Manage Tasks Automatically</h1> +<h1>Manage Tasks</h1> <div class="row"> <div class="col-md-4"> - {{ 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'}) }} + {{ wtf.quick_form(form, button_map={'assign': 'primary', 'delete': 'danger'}) }} </div> </div> <br> diff --git a/app/utils/tasks.py b/app/utils/tasks.py index a5398eb..4297420 100644 --- a/app/utils/tasks.py +++ b/app/utils/tasks.py @@ -4,100 +4,56 @@ """ -import random from flask import current_app from app.models import User, Dataset, Task -def generate_auto_assign_tasks(max_per_user, num_per_dataset): - """Automatically generate random tasks - - This function generates random tasks for the users based on the desired - number of tasks per dataset and the maximum number of tasks per user. The - return value is a tuple (Task, error) where Task is None if an error - occurred. +def generate_user_task(user): """ + Generate new task for a given user. + + This function assigns tasks to a given user and ensures that: + + 1) datasets that are nearly annotated with the desired number of + datasets get priority + 2) users never are given more tasks than max_per_user + 3) users never get the same dataset twice - # create a dictionary of user/num available tasks - 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: - error = ( - "All users already have at least %i tasks assigned to them." - % max_per_user - ) - yield (None, error) - - # create a dictionary of dataset/num tasks desired - 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: - error = ( - "All datasets already have at least the desired number (%i) of tasks." - % num_per_dataset - ) - yield (None, error) - - # shuffle the dataset list - 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]) - - # select a random set of users - selected_users = random.sample(available, tbd) - for user in selected_users: - task = Task(annotator_id=user.id, dataset_id=dataset.id) - yield (task, None) - available_users[user] -= 1 - datasets_tbd[dataset] -= 1 - - if any((datasets_tbd[d] > 0 for d in datasets)): - yield ( - None, - "Insufficient users available for the desired number of tasks per dataset.", - ) - - -def create_initial_user_tasks(user, max_per_user=None, num_per_dataset=None): - """Generate initial tasks for a given user """ - if max_per_user is None: - max_per_user = current_app.config["TASKS_MAX_PER_USER"] - if num_per_dataset is None: - num_per_dataset = current_app.config["TASKS_NUM_PER_DATASET"] + max_per_user = current_app.config["TASKS_MAX_PER_USER"] + num_per_dataset = current_app.config["TASKS_NUM_PER_DATASET"] user_tasks = Task.query.filter_by(annotator_id=user.id).all() - if len(user_tasks) >= max_per_user: - yield None - available_user = max_per_user - len(user_tasks) + user_tasks = [t for t in user_tasks if not t.dataset.is_demo] + n_user_tasks = len(user_tasks) + if n_user_tasks >= max_per_user: + return None - datasets_tbd = {} - for dataset in Dataset.query.all(): + potential_datasets = [] + for dataset in Dataset.query.filter_by(is_demo=False).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: - yield None - - # shuffle the dataset list - datasets = list(datasets_tbd.keys()) - random.shuffle(datasets) - for dataset in datasets: - task = Task(annotator_id=user.id, dataset_id=dataset.id) - yield task - available_user -= 1 - datasets_tbd[dataset] -= 1 - if available_user == 0: - break + + # check that this dataset needs more annotations + n_needed = num_per_dataset - len(dataset_tasks) + if n_needed <= 0: + continue + + # check that this dataset is not already assigned to the user + task = Task.query.filter_by( + dataset_id=dataset.id, annotator_id=user.id + ).first() + if not task is None: + continue + potential_datasets.append((n_needed, dataset)) + + if len(potential_datasets) == 0: + return None + + # sort datasets so that the ones who need the least are at the front. + potential_datasets.sort() + + _, dataset = potential_datasets[0] + task = Task(annotator_id=user.id, dataset_id=dataset.id) + return task |
