From da15e439e64c8314825ad5f28073fbcbd436946f Mon Sep 17 00:00:00 2001 From: Gertjan van den Burg Date: Thu, 30 May 2019 13:29:31 +0100 Subject: Initial version of demo This commit introduces the demo functionality. The task assignment has been removed at the moment, as this will be changed in a future commit. --- app/admin/forms.py | 2 +- app/admin/routes.py | 15 +- app/auth/routes.py | 12 +- app/decorators.py | 2 + app/main/__init__.py | 2 +- app/main/demo.py | 439 +++++++++++++++++++++ app/main/forms.py | 8 + app/main/routes.py | 31 +- app/models.py | 8 + app/static/annotate.css | 6 +- app/static/css/demo/evaluate.css | 57 +++ app/static/css/demo/learn.css | 11 + app/static/css/global.css | 3 + app/static/js/buttons.js | 39 +- app/static/js/makeChart.js | 146 ++++++- app/static/view_annotation.css | 3 +- app/templates/_partials/modals.html | 21 + app/templates/admin/annotations_by_dataset.html | 2 +- app/templates/admin/manage_datasets.html | 20 +- app/templates/admin/manage_users.html | 116 +++--- app/templates/annotate/index.html | 64 +-- app/templates/base.html | 35 +- app/templates/demo/evaluate.html | 49 +++ app/templates/demo/learn.html | 23 ++ app/templates/index.html | 153 ++++--- app/utils/datasets.py | 33 +- .../versions/43c24fc4dd24_changes_for_demo.py | 30 ++ poetry.lock | 14 +- pyproject.toml | 1 + 29 files changed, 1088 insertions(+), 257 deletions(-) create mode 100644 app/main/demo.py create mode 100644 app/main/forms.py create mode 100644 app/static/css/demo/evaluate.css create mode 100644 app/static/css/demo/learn.css create mode 100644 app/static/css/global.css create mode 100644 app/templates/_partials/modals.html create mode 100644 app/templates/demo/evaluate.html create mode 100644 app/templates/demo/learn.html create mode 100644 migrations/versions/43c24fc4dd24_changes_for_demo.py diff --git a/app/admin/forms.py b/app/admin/forms.py index 1ac9333..bd1dce6 100644 --- a/app/admin/forms.py +++ b/app/admin/forms.py @@ -22,7 +22,7 @@ class AdminAutoAssignForm(FlaskForm): num_per_dataset = IntegerField( "Tasks per Dataset", [NumberRange(min=1, max=20)], default=10 ) - assign = SubmitField("Assign") + submit = SubmitField("Submit") class AdminManageTaskForm(FlaskForm): diff --git a/app/admin/routes.py b/app/admin/routes.py index efb4f57..f0d7817 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -41,7 +41,7 @@ def manage_tasks(): form_manual.username.choices = user_list form_manual.dataset.choices = dataset_list - if form_auto.validate_on_submit() and form_auto.assign.data: + 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 @@ -130,6 +130,8 @@ def manage_users(): flash("User doesn't exist.", "error") return redirect(url_for("admin.manage_users")) + username = user.username + tasks = Task.query.filter_by(annotator_id=user.id).all() for task in tasks: for ann in Annotation.query.filter_by(task_id=task.id).all(): @@ -137,7 +139,7 @@ def manage_users(): db.session.delete(task) db.session.delete(user) db.session.commit() - flash("User deleted successfully.", "success") + flash("User '%s' deleted successfully." % username, "success") return redirect(url_for("admin.manage_users")) return render_template( "admin/manage_users.html", title="Manage Users", users=users, form=form @@ -175,6 +177,7 @@ def manage_datasets(): db.session.commit() os.unlink(filename) flash("Dataset deleted successfully.", "success") + return redirect(url_for("admin.manage_datasets")) overview = [] for dataset in Dataset.query.all(): @@ -187,11 +190,13 @@ def manage_datasets(): entry = { "id": dataset.id, "name": dataset.name, + "demo": dataset.is_demo, "assigned": len(tasks), "completed": n_complete, "percentage": perc, } overview.append(entry) + overview.sort(key=lambda x: x["name"]) return render_template( "admin/manage_datasets.html", title="Manage Datasets", @@ -226,8 +231,10 @@ def add_dataset(): if not os.path.exists(target_filename): flash("Internal error: file moving failed", "error") return redirect(url_for("admin.add_dataset")) - - dataset = Dataset(name=name, md5sum=md5sum(target_filename)) + is_demo = dataset_is_demo(target_filename) + dataset = Dataset( + name=name, md5sum=md5sum(target_filename), is_demo=is_demo + ) db.session.add(dataset) db.session.commit() flash("Dataset %r added successfully." % name, "success") diff --git a/app/auth/routes.py b/app/auth/routes.py index d23bc18..27b0de0 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -21,15 +21,10 @@ from app.auth.email import ( send_email_confirmation_email, ) from app.models import User -from app.utils.tasks import create_initial_user_tasks @bp.route("/login", methods=("GET", "POST")) def login(): - if current_user.is_authenticated: - current_user.last_active = datetime.datetime.utcnow() - db.session.commit() - return redirect(url_for("main.index")) form = LoginForm() if form.validate_on_submit(): user = User.query.filter_by(username=form.username.data).first() @@ -37,6 +32,8 @@ def login(): flash("Invalid username or password", "error") return redirect(url_for("auth.login")) login_user(user, remember=form.remember_me.data) + current_user.last_active = datetime.datetime.utcnow() + db.session.commit() if not user.is_confirmed: return redirect(url_for("auth.not_confirmed")) next_page = request.args.get("next") @@ -122,11 +119,6 @@ def confirm_email(token): else: user.is_confirmed = True db.session.commit() - for task in create_initial_user_tasks(user): - if task is None: - break - db.session.add(task) - db.session.commit() flash("Account confirmed successfully. Thank you!", "success") return redirect(url_for("auth.login")) return redirect(url_for("main.index")) diff --git a/app/decorators.py b/app/decorators.py index df797bd..9fbf1f4 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -16,6 +16,8 @@ def admin_required(func): return func(*args, **kwargs) elif not current_user.is_authenticated: return current_app.login_manager.unauthorized() + elif not current_user.is_confirmed: + return redirect(url_for("auth.not_confirmed")) elif not current_user.is_admin: return current_app.login_manager.unauthorized() return func(*args, **kwargs) diff --git a/app/main/__init__.py b/app/main/__init__.py index 2cb605e..2612509 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -4,6 +4,6 @@ from flask import Blueprint bp = Blueprint('main', __name__) -from app.main import routes +from app.main import routes, demo diff --git a/app/main/demo.py b/app/main/demo.py new file mode 100644 index 0000000..a126fd8 --- /dev/null +++ b/app/main/demo.py @@ -0,0 +1,439 @@ +# -*- coding: utf-8 -*- + +import datetime +import logging +import markdown +import textwrap + +from flask import ( + render_template, + flash, + url_for, + redirect, + request, + session, + abort, +) +from flask_login import current_user + +from app import db +from app.decorators import login_required +from app.models import Annotation, Dataset, Task +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 + +LOGGER = logging.getLogger(__name__) + +# textwrap.dedent is used mostly for code formatting. +DEMO_DATA = { + 1: { + "dataset": {"name": "demo_100"}, + "learn": { + "text": markdown.markdown( + textwrap.dedent( + """ + Welcome to AnnotateChange, an annotation app for change point + detection. + + Our goal with AnnotateChange is to create a dataset of + human-annotated time series to use in the development and + evaluation of change point algorithms. + + We really appreciate that you've agreed to help us with this! + Without your help this project would not be possible. + + In the next few pages, we'll introduce you to the problem of + change point detection. We'll look at a few datasets and see + different types of changes that can occur. + + Thanks again for your help!""" + ) + ) + }, + "annotate": { + "text": RUBRIC + + markdown.markdown( + textwrap.dedent( + """ + Click "Submit" when you have finished marking the change points + or "No change points" when you believe there are none. You can + reset the graph with the "Reset" button.""" + ) + ) + }, + "evaluate": { + "text": markdown.markdown( + textwrap.dedent( + """ + This first example has **one** change point. Not all datasets + that you'll encounter in this program have exactly one change + point. It is up to you to see whether a time series contains a + change point or not, and if it does, to see if there is more + than one. + + Don't worry if you weren't exactly correct on the first try. + The goal of this introduction is to familiarise yourself with + time series data and with change point detection in + particular.""" + ) + ) + }, + }, + 2: { + "dataset": {"name": "demo_200"}, + "learn": { + "text": markdown.markdown( + textwrap.dedent( + """ + In the previous example, you've seen a relatively simple + dataset where a *step change* occurred at a certain point in + time. A step change is one of the simplest types of change + points that can occur. + + Click "Continue" to move on to the next example.""" + ) + ) + }, + "annotate": {"text": RUBRIC}, + "evaluate": { + "text": markdown.markdown( + textwrap.dedent( + """ + The dataset in the previous example shows again a time series + with step changes, but here there are **two** change points. + This is important to keep in mind, as there can be more than + one change point in a dataset.""" + ) + ) + }, + }, + 3: { + "dataset": {"name": "demo_300"}, + "learn": { + "text": markdown.markdown( + textwrap.dedent( + """ + In the previous examples we've introduced *step changes*. + However, these are not the only types of change points that + can occur, as we'll see in the next example.""" + ) + ) + }, + "annotate": {"text": RUBRIC}, + "evaluate": { + "text": markdown.markdown( + textwrap.dedent( + """ + This time series shows an example where a change occurs in the + **variance** of the data. At the change point the variance of + the noise changes abruptly from a relatively low noise variance + to a high noise variance. This is another type of change point + that can occur.""" + ) + ) + }, + }, + 4: { + "dataset": {"name": "demo_400"}, + "learn": { + "text": markdown.markdown( + textwrap.dedent( + """ + So far we have seen two types of change points: step changes + (also known as mean shift) and variance changes.""" + ) + ) + }, + "annotate": {"text": RUBRIC}, + "evaluate": { + "text": markdown.markdown( + textwrap.dedent( + """ + Remember that it's also possible for there to be *no change + points* in a dataset. It can sometimes be difficult to tell + whether a dataset has change points or not. In that case, it's + important to remember that we are looking for points where the + behaviour of the time series changes *abruptly*.""" + ) + ) + }, + }, + 5: { + "dataset": {"name": "demo_500"}, + "learn": { + "text": markdown.markdown( + textwrap.dedent( + """ + Change points mark places in the time series where the + behaviour changes *abruptly*. While **outliers** are data + points that do not adhere to the prevailing behaviour of the + time series, they are not generally considered change points + because the behaviour of the time series before and after the + outlier is the same. """ + ) + ) + }, + "annotate": {"text": RUBRIC}, + "evaluate": { + "text": markdown.markdown( + textwrap.dedent( + """ + Outliers are quite common in real-world time series data, and + not all change point detection methods are robust against these + observations. + + Note that short periods that consist of several consecutive + outlying data points could be considered an abrupt change in + behaviour of the time series. If you see this, use your + intuition to guide you.""" + ) + ) + }, + }, + 6: { + "dataset": {"name": "demo_600"}, + "learn": { + "text": markdown.markdown( + textwrap.dedent( + """ + So far we've seen *step changes*, *variance changes*, and time + series with *outliers*. Can you think of another type of change + that can occur?""" + ) + ) + }, + "annotate": {"text": RUBRIC}, + "evaluate": { + "text": markdown.markdown( + textwrap.dedent( + """ + What we see here is a change in *trend*. For these types of + changes it's not always easy to figure out exactly where the + change occurs, so it's harder to get it exactly right. Use + your intuition and keep in mind that it is normal for the + observations to be noisy.""" + ) + ) + }, + }, + 7: { + "dataset": {"name": "demo_700"}, + "learn": { + "text": markdown.markdown( + textwrap.dedent( + """ + It is not uncommon for time series data from the real world to + display **seasonal variation**, for instance because certain + days of the week are more busy than others. Seasonality can + make it harder to find the change points in the dataset (if + there are any at all). Try to follow the pattern of + seasonality, and check whether the pattern changes in one of + the ways we've seen previously.""" + ) + ) + }, + "annotate": {"text": RUBRIC}, + "evaluate": { + "text": markdown.markdown( + textwrap.dedent( + """ + As you can see from this example, changes in seasonality can + occur as well. We expect that these changes are quite rare, + but it's nevertheless good to be aware of them.""" + ) + ) + }, + }, +} + + +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. + if current_user.is_introduced: + return redirect(url_for("main.index")) + + current_user.is_introduced = True + db.session.commit() + # TODO: Assign real tasks to the user here. + return redirect(url_for("main.index")) + elif phase_id == demo_last_phase_id: + demo_id += 1 + phase_id = 1 + return redirect( + url_for("main.demo", demo_id=demo_id, phase_id=phase_id) + ) + else: + phase_id += 1 + return redirect( + url_for("main.demo", demo_id=demo_id, phase_id=phase_id) + ) + + +def process_annotations(demo_id): + annotation = request.get_json() + if annotation["identifier"] != demo_id: + LOGGER.error( + "User %s returned a task id in the demo that wasn't the demo id." + % current_user.username + ) + flash( + "An internal error occurred, the administrator has been notified.", + "error", + ) + return redirect(url_for("main.index")) + + if annotation["changepoints"] is None: + retval = [] + else: + retval = [cp["x"] for cp in annotation["changepoints"]] + + # If the user is already introduced, we assume that their demo annotations + # are already in the database, and thus we don't put them back in (because + # we want the original ones). + if current_user.is_introduced: + return retval + + dataset = Dataset.query.filter_by( + name=DEMO_DATA[demo_id]["dataset"]["name"] + ).first() + task = Task.query.filter_by( + annotator_id=current_user.id, dataset_id=dataset.id + ).first() + # this happens if the user returns to the same demo page, but hasn't + # completed the full demo yet. Same as above, not updating because we want + # the originals. + if not task is None: + return retval + + # Create a new task + task = Task(annotator_id=current_user.id, dataset_id=dataset.id) + task.done = False + task.annotated_on = None + db.session.add(task) + db.session.commit() + if annotation["changepoints"] is None: + ann = Annotation(cp_index=None, task_id=task.id) + db.session.add(ann) + db.session.commit() + else: + for cp in annotation["changepoints"]: + ann = Annotation(cp_index=cp["x"], task_id=task.id) + db.session.add(ann) + db.session.commit() + + # mark task as done + task.done = True + task.annotated_on = datetime.datetime.utcnow() + db.session.commit() + + return retval + + +def demo_learn(demo_id, form): + demo_data = DEMO_DATA[demo_id]["learn"] + return render_template( + "demo/learn.html", + title="Introduction – %i" % demo_id, + text=demo_data["text"], + form=form, + ) + + +def demo_annotate(demo_id): + demo_data = DEMO_DATA[demo_id]["annotate"] + dataset = Dataset.query.filter_by( + name=DEMO_DATA[demo_id]["dataset"]["name"] + ).first() + if dataset is None: + LOGGER.error( + "Demo requested unavailable dataset: %s" + % demo_data["dataset"]["name"] + ) + flash( + "An internal error occured. The administrator has been notified. We apologise for the inconvenience, please try again later.", + "error", + ) + return redirect(url_for("main.index")) + chart_data = load_data_for_chart(dataset.name, dataset.md5sum) + return render_template( + "annotate/index.html", + title="Introduction – %i" % demo_id, + data=chart_data, + rubric=demo_data["text"], + identifier=demo_id, + ) + + +def demo_evaluate(demo_id, phase_id, form): + demo_data = DEMO_DATA[demo_id]["evaluate"] + user_changepoints = session.get("user_changepoints", "__UNK__") + if user_changepoints == "__UNK__": + flash( + "The previous step of the demo was not completed successfully. Please try again.", + "error", + ) + return redirect( + url_for("main.demo", demo_id=demo_id, phase_id=phase_id - 1) + ) + dataset = Dataset.query.filter_by( + name=DEMO_DATA[demo_id]["dataset"]["name"] + ).first() + chart_data = load_data_for_chart(dataset.name, dataset.md5sum) + true_changepoints = get_demo_true_cps(dataset.name) + if true_changepoints is None: + flash( + "An internal error occurred, the administrator has been notified. We apologise for the inconvenience, please try again later.", + "error", + ) + return redirect(url_for("main.index")) + annotations_true = [dict(index=x) for x in true_changepoints] + annotations_user = [dict(index=x) for x in user_changepoints] + return render_template( + "demo/evaluate.html", + title="Introduction – %i" % demo_id, + data=chart_data, + annotations_user=annotations_user, + annotations_true=annotations_true, + text=demo_data["text"], + form=form, + ) + + +@bp.route( + "/introduction/", + defaults={"demo_id": 1, "phase_id": 1}, + methods=("GET", "POST"), +) +@bp.route( + "/introduction//", + defaults={"phase_id": 1}, + methods=("GET", "POST"), +) +@bp.route( + "/introduction//", methods=("GET", "POST") +) +@login_required +def demo(demo_id, phase_id): + form = NextForm() + + if request.method == "POST": + if form.validate_on_submit(): + return redirect_user(demo_id, phase_id) + else: + user_changepoints = process_annotations(demo_id) + session["user_changepoints"] = user_changepoints + return url_for("main.demo", demo_id=demo_id, phase_id=phase_id + 1) + + if phase_id == 1: + return demo_learn(demo_id, form) + elif phase_id == 2: + return demo_annotate(demo_id) + elif phase_id == 3: + return demo_evaluate(demo_id, phase_id, form) + else: + abort(404) diff --git a/app/main/forms.py b/app/main/forms.py new file mode 100644 index 0000000..dcdffd3 --- /dev/null +++ b/app/main/forms.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from flask_wtf import FlaskForm + +from wtforms import SubmitField + +class NextForm(FlaskForm): + submit = SubmitField("Continue") diff --git a/app/main/routes.py b/app/main/routes.py index d249c5c..11de2f9 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import datetime +import logging from flask import render_template, flash, url_for, redirect, request from flask_login import current_user @@ -11,19 +12,13 @@ from app.main import bp from app.models import Annotation, Task from app.utils.datasets import load_data_for_chart +logger = logging.getLogger(__name__) + RUBRIC = """ -Please mark all the points in the time series where an abrupt change -in - the behaviour of the series occurs. -
-If there are no such points, please click the no changepoints button. -When you're ready, please click the submit button. -
-
-Note: You can zoom and pan the graph if needed. +Please mark the points in the time series where an abrupt change in + the behaviour of the series occurs. The goal is to define segments of the time + series that are separated by places where these abrupt changes occur.
-
-Thank you! """ @@ -35,7 +30,7 @@ def index(): if current_user.is_authenticated: 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] + 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] return render_template( "index.html", @@ -48,14 +43,14 @@ def index(): @bp.route("/annotate/", methods=("GET", "POST")) @login_required -def task(task_id): +def annotate(task_id): if request.method == "POST": # record post time now = datetime.datetime.utcnow() # get the json from the client annotation = request.get_json() - if annotation["task"] != task_id: + if annotation["identifier"] != task_id: flash("Internal error: task id doesn't match.", "error") return redirect(url_for(task_id=task_id)) @@ -100,10 +95,14 @@ def task(task_id): flash("It's not possible to edit annotations at the moment.") return redirect(url_for("main.index")) data = load_data_for_chart(task.dataset.name, task.dataset.md5sum) + if data is None: + flash( + "An internal error occurred loading this dataset, the admin has been notified. Please try again later. We apologise for the inconvenience." + ) return render_template( "annotate/index.html", - title="Annotate %s" % task.dataset.name, - task=task, + title=task.dataset.name.title(), + identifier=task.id, data=data, rubric=RUBRIC, ) diff --git a/app/models.py b/app/models.py index 5016505..f7dd2fa 100644 --- a/app/models.py +++ b/app/models.py @@ -22,8 +22,13 @@ class User(UserMixin, db.Model): db.DateTime(), nullable=False, default=datetime.datetime.utcnow ) is_admin = db.Column(db.Boolean(), default=False) + + # after email is confirmed: is_confirmed = db.Column(db.Boolean(), default=False) + # after all demo tasks completed: + is_introduced = db.Column(db.Boolean(), default=False) + def __repr__(self): return "" % self.username @@ -76,6 +81,9 @@ class Dataset(db.Model): ) md5sum = db.Column(db.String(32), unique=True, nullable=False) + # Whether or not dataset is a demo dataset. + is_demo = db.Column(db.Boolean(), default=True) + def __repr__(self): return "" % self.name diff --git a/app/static/annotate.css b/app/static/annotate.css index 7dc222f..8079053 100644 --- a/app/static/annotate.css +++ b/app/static/annotate.css @@ -20,5 +20,9 @@ rect { } #rubric { - text-align: center; + text-align: left; + padding-bottom: 20px; + padding-top: 10px; + width: 80%; + font-size: 16px; } diff --git a/app/static/css/demo/evaluate.css b/app/static/css/demo/evaluate.css new file mode 100644 index 0000000..0c7dbc2 --- /dev/null +++ b/app/static/css/demo/evaluate.css @@ -0,0 +1,57 @@ +.graph-wrapper { + width: 90%; + margin: 0 auto; +} + +#graph_user > svg { + width: 100%; + height: 100%; +} + +#graph_true > svg { + width: 100%; + height: 100%; +} + +.line { + fill: none; + stroke: blue; + clip-path: url(#clip); +} + +circle { + clip-path: url(#clip); + fill: blue; +} + +rect { + fill: white; + opacity: 0; +} + +.marked { + fill: #fc8d62; + stroke: #fc8d62; +} + +#next-btn { + text-align: right; + padding-bottom: 20px; +} + +#lesson { + padding-top: 10px; + padding-bottom: 10px; +} + +#lesson p { + font-size: 16px; +} + +.ann-line { + stroke-dasharray: 5; + clip-path: url(#clip); + fill: #fc8d62; + stroke: #fc8d62; + stroke-width: 2px; +} diff --git a/app/static/css/demo/learn.css b/app/static/css/demo/learn.css new file mode 100644 index 0000000..13dccd2 --- /dev/null +++ b/app/static/css/demo/learn.css @@ -0,0 +1,11 @@ +#next-btn { + text-align: right; +} + +#lesson { + padding-top: 10px; +} + +#lesson p { + font-size: 16px; +} diff --git a/app/static/css/global.css b/app/static/css/global.css new file mode 100644 index 0000000..6258057 --- /dev/null +++ b/app/static/css/global.css @@ -0,0 +1,3 @@ +h1, h2, h3 { + margin-bottom: 20px; +} diff --git a/app/static/js/buttons.js b/app/static/js/buttons.js index d26a15d..75adad2 100644 --- a/app/static/js/buttons.js +++ b/app/static/js/buttons.js @@ -8,7 +8,7 @@ function resetOnClick() { updateTable(); } -function noCPOnClick(task_id) { +function noCPOnClick(identifier) { var changepoints = document.getElementsByClassName("changepoint"); // validation if (changepoints.length > 0) { @@ -16,27 +16,27 @@ function noCPOnClick(task_id) { return; } - var obj = { - task: task_id, - changepoints: null - }; + var obj = {} + obj["identifier"] = identifier; + obj["changepoints"] = null; var xhr = new XMLHttpRequest(); - xhr.open("POST", ""); + xhr.open("POST", "", false); xhr.withCredentials = true; xhr.setRequestHeader("Content-Type", "application/json"); - xhr.send(JSON.stringify(obj)); + /* Flask's return to this POST must be a URL, not a template!*/ xhr.onreadystatechange = function() { - if (xhr.readyState == XMLHttpRequest.DONE) { - if (xhr.status === 200) - window.location.href = xhr.responseText; + if (xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) { + window.location.href = xhr.responseText; + console.log("XHR Success: " + xhr.responseText); } else { - console.log("Error: " + xhr.status); + console.log("XHR Error: " + xhr.status); } - }; + } + xhr.send(JSON.stringify(obj)); } -function submitOnClick(task_id) { +function submitOnClick(identifier) { var changepoints = document.getElementsByClassName("changepoint"); // validation if (changepoints.length === 0) { @@ -45,7 +45,7 @@ function submitOnClick(task_id) { } var obj = {}; - obj["task"] = task_id; + obj["identifier"] = identifier; obj["changepoints"] = []; var i, cp; for (i=0; i + +{% endmacro %} diff --git a/app/templates/admin/annotations_by_dataset.html b/app/templates/admin/annotations_by_dataset.html index 5ad98b6..eb7a7e8 100644 --- a/app/templates/admin/annotations_by_dataset.html +++ b/app/templates/admin/annotations_by_dataset.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block styles %} -{{super()}} +{{ super() }} {% endblock %} {% block app_content %} diff --git a/app/templates/admin/manage_datasets.html b/app/templates/admin/manage_datasets.html index f14fe81..f68259f 100644 --- a/app/templates/admin/manage_datasets.html +++ b/app/templates/admin/manage_datasets.html @@ -42,18 +42,20 @@ ID Name - Assigned Tasks - Completed Tasks - Percentage + Demo + Assigned Tasks + Completed Tasks + Percentage {% for entry in overview %} - {{ entry['id'] }} - {{ entry['name'] }} - {{ entry['assigned'] }} - {{ entry['completed'] }} - {{ entry['percentage'] }} - + {{ entry['id'] }} + {{ entry['name'] }} + {% if entry['demo'] %}Yes{% else %}No{% endif %} + {{ entry['assigned'] }} + {{ entry['completed'] }} + {{ entry['percentage'] }} + {% endfor %} diff --git a/app/templates/admin/manage_users.html b/app/templates/admin/manage_users.html index 5c44a48..8788958 100644 --- a/app/templates/admin/manage_users.html +++ b/app/templates/admin/manage_users.html @@ -1,70 +1,72 @@ {% extends "base.html" %} {% block app_content %} -

Manage Users

+

Manage Users

-
+
-
- {{ form.hidden_tag() }} - {{ form.user }} - {{ form.delete(hidden='true', id='form-submit') }} - - -
+
+ {{ form.hidden_tag() }} + {{ form.user }} + {{ form.delete(hidden='true', id='form-submit') }} + + +
-
+
- -