Skip to content

Commit

Permalink
Two pane skip todo interface (#451)
Browse files Browse the repository at this point in the history
* Revert "Checklist models (#449)"

This reverts commit a9de904.

* [WIP] Create a 2-pane interface that allows you to skip a todo

* [WIP] refactored code

* [WIP] refactored code

* [WIP] refactored code

* [WIP] refactored code

* Revert "Revert "Checklist models (#449)""

This reverts commit 4352de6.

* Renamed checklistTemplate to TodoListTemplates and other changes

* Updated the migration script and fixed lint errors

* Changed the name from checklist to todolist

* Removed unused code

* added the json schema for todolist

* added the json schema for todolist

* Fixed the tests for skipped_datetime field

* Fixed the tests for skipped_datetime field

* added the json schema reference in todo serializer

* Adding jsl and jsonschema to requirement.tx

* Removed the skipped field

* Add the skipped_datetime field doc

* Fixed the todo models on_delete cascade

* Fixed the todo models on_delete cascade
  • Loading branch information
adbharadwaj committed Jun 18, 2018
1 parent ae5cab3 commit cd20f48
Show file tree
Hide file tree
Showing 18 changed files with 360 additions and 69 deletions.
11 changes: 11 additions & 0 deletions orchestra/json_schemas/todos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import jsl


class TodoSchema(jsl.Document):
id = jsl.IntField(required=True)
description = jsl.StringField(required=True)
items = jsl.ArrayField(jsl.DocumentField('TodoSchema'))


class TodoListSchema(jsl.Document):
items = jsl.ArrayField(jsl.DocumentField('TodoSchema'))
17 changes: 17 additions & 0 deletions orchestra/json_schemas/validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from orchestra.utils.json_schema import DefaultValidatingDraft4Validator


def validate_json(blob_key, json_schema, data):
"""
Validate json blobs against their json schema.
:param blob_key: key name for data in model
:param json_schema: JSON schema object
:param data: JSON data, defaults are added to it.
"""
if not data:
# Don't validate when data is empty. Otherwise, won't be able to
# create empty database objects.
return data
schema = json_schema.get_schema()
DefaultValidatingDraft4Validator(schema).validate(data)
return data
60 changes: 60 additions & 0 deletions orchestra/migrations/0076_auto_20180615_1606.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.10 on 2018-06-15 16:06
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import jsonfield.fields
import orchestra.models.core.mixins
import orchestra.utils.models


class Migration(migrations.Migration):

dependencies = [
('orchestra', '0075_add_sanity_checks'),
]

operations = [
migrations.CreateModel(
name='TodoListTemplate',
fields=[
('id', models.AutoField(auto_created=True,
primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(
default=django.utils.timezone.now)),
('is_deleted', models.BooleanField(default=False)),
('slug', models.CharField(max_length=200, unique=True)),
('name', models.CharField(max_length=200)),
('description', models.TextField()),
('todos', jsonfield.fields.JSONField(default={'items': []})),
('creator', models.ForeignKey(blank=True, null=True,
on_delete=django.db.models.deletion.SET_NULL, related_name='creator', to='orchestra.Worker')),
],
bases=(orchestra.models.core.mixins.TodoListTemplateMixin,
orchestra.utils.models.DeleteMixin, models.Model),
),
migrations.AddField(
model_name='todo',
name='activity_log',
field=jsonfield.fields.JSONField(default={'actions': []}),
),
migrations.AddField(
model_name='todo',
name='parent_todo',
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent', to='orchestra.Todo'),
),
migrations.AddField(
model_name='todo',
name='skipped_datetime',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='todo',
name='template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name='template', to='orchestra.TodoListTemplate'),
),
]
7 changes: 7 additions & 0 deletions orchestra/models/core/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,13 @@ def __str__(self):
self.completed)


class TodoListTemplateMixin(object):

def __str__(self):
return '{} - {}'.format(
self.name, self.description)


class PayRateMixin(object):

def __str__(self):
Expand Down
50 changes: 48 additions & 2 deletions orchestra/models/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from djmoney.models.fields import MoneyField
from jsonfield import JSONField
from phonenumber_field.modelfields import PhoneNumberField

from orchestra.models.core.mixins import CertificationMixin
from orchestra.models.core.mixins import TodoListTemplateMixin
from orchestra.models.core.mixins import PayRateMixin
from orchestra.models.core.mixins import ProjectMixin
from orchestra.models.core.mixins import StepMixin
Expand Down Expand Up @@ -576,6 +576,35 @@ class PayRate(PayRateMixin, models.Model):
end_date = models.DateField(null=True, blank=True)


class TodoListTemplate (TodoListTemplateMixin, BaseModel):
"""
A todo template
Attributes:
slug (str):
A unique key for the todo list
name (str):
A human-readable name for the todo list template
description (str):
A longer description of the todo list template
creator (orchestra.models.Worker)
The worker who created the todo list template. This field is null
if it is generated by the system.
todos (str)
A JSON blob that describe the todos in the todo list template.
"""
class Meta:
app_label = 'orchestra'

slug = models.CharField(max_length=200, unique=True)
name = models.CharField(max_length=200)
description = models.TextField()
creator = models.ForeignKey(
Worker, null=True, blank=True,
related_name='creator', on_delete=models.SET_NULL)
todos = JSONField(default={'items': []})


class Todo(TodoMixin, BaseModel):
"""
A todo on a task.
Expand All @@ -590,7 +619,17 @@ class Todo(TodoMixin, BaseModel):
start_by_datetime (datetime.datetime):
The time to start the todo. (inclusive)
due_datetime (datetime.datetime):
The time the todo is due.
The time the todo is due
skipped_datetime (datetime.datetime):
The time the todo was skipped
parent_todo (orchestra.models.Todo):
The parent todo item
template (orchestra.models.TodoListTemplate)
The template the todo is based on
activity_log (str)
A JSON blob that records the user actions
with this todo
Constraints:
`task` and `assignment_counter` are taken to be unique_together.
Expand All @@ -607,6 +646,13 @@ class Meta:
completed = models.BooleanField(default=False)
start_by_datetime = models.DateTimeField(null=True, blank=True)
due_datetime = models.DateTimeField(null=True, blank=True)
skipped_datetime = models.DateTimeField(null=True, blank=True)
parent_todo = models.ForeignKey(
'self', null=True, related_name='parent', on_delete=models.CASCADE)
template = models.ForeignKey(
TodoListTemplate, null=True, blank=True, related_name='template',
on_delete=models.SET_NULL)
activity_log = JSONField(default={'actions': []})


class SanityCheck(SanityCheckMixin, BaseModel):
Expand Down
12 changes: 10 additions & 2 deletions orchestra/static/dist/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -2301,14 +2301,22 @@ a.logo {
padding-top: 1.5rem;
padding-bottom: 1.5rem; }

.todo-list .todolist-pane {
border: 1px solid black;
padding: 1.5rem; }
.todo-list .todolist-pane__heading {
margin-top: -2.8rem;
background-color: white;
text-align: center; }

.todo-list .new-todo {
float: left; }
.todo-list .new-todo__box {
display: inline-block; }
.todo-list .new-todo__box-details {
width: 600px; }
width: 550px; }
.todo-list .new-todo__description {
width: 600px; }
width: 550px; }
.todo-list .new-todo__description__datetime {
display: inline-block;
padding-left: 10px; }
Expand Down
16 changes: 14 additions & 2 deletions orchestra/static/dist/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -61446,6 +61446,16 @@ function todoList(orchestraApi) {
todoApi.update(todo);
};

todoList.toggleSkipTodo = function (todo) {
if (todo.skipped_datetime) {
todo.skipped_datetime = null;
} else {
var datetimeUtc = _momentTimezone2.default.tz((0, _momentTimezone2.default)(), _momentTimezone2.default.tz.guess()).utc();
todo.skipped_datetime = datetimeUtc.format('YYYY-MM-DD HH:mm');
}
todoApi.update(todo);
};

todoList.setTimeOfDate = function (datetime) {
$scope.$apply();
};
Expand Down Expand Up @@ -61482,7 +61492,7 @@ function todoList(orchestraApi) {
/* 209 */
/***/ (function(module, exports) {

module.exports = "<section class=\"section-panel todo-list\">\n <div class=\"container-fluid\">\n <div class=\"row section-header\">\n <div class=\"col-lg-12 col-md-12 col-sm-12\">\n <h3>\n Todo List\n <a class=\"btn\"\n ng-if=\"todoList.canSendToPending()\"\n ng-click=\"todoList.sendToPending()\">\n Send to pending\n </a>\n </h3>\n </div>\n </div>\n <div class=\"row section-body\" ng-if=\"todoList.ready\">\n <div class=\"col-lg-12 col-md-12 col-sm-12\">\n <form class=\"new-todo\">\n <div class=\"new-todo__box\">\n <select name=\"todoList\" id=\"todoList\" ng-model=\"todoList.newTodoTaskId\">\n <option value=\"\" selected>Select owner</option>\n <option value=\"{{task.id}}\" ng-repeat=\"task in todoList.possibleTasks\">{{todoList.steps[task.step_slug].name}}</option>\n </select>\n </div>\n <div class=\"new-todo__box new-todo__box-details\">\n <input class=\"new-todo__description\"\n type=\"text\"\n ng-model=\"todoList.newTodoDescription\"\n placeholder=\"Description\">\n <div class=\"pull-right\">\n <div class=\"new-todo__description__datetime\">\n <label>Start</label>\n <date-picker\n date=\"todoList.newTodoStartDate\"\n callback=\"todoList.setTimeOfDate\"></date-picker>\n <time-input\n datetime=\"todoList.newTodoStartDate\"\n default-hour=\"8\"\n ></time-input>\n </div>\n <div class=\"new-todo__description__datetime\">\n <label>Due</label>\n <date-picker\n date=\"todoList.newTodoDueDate\"\n callback=\"todoList.setTimeOfDate\"></date-picker>\n <time-input\n datetime=\"todoList.newTodoDueDate\"\n default-hour=\"18\"\n ></time-input>\n </div>\n </div>\n </div>\n <div class=\"new-todo__box\">\n <button type=\"submit\"\n class=\"btn btn-primary btn-sm edit-save-handle\"\n ng-disabled=\"!todoList.canAddTodo()\"\n ng-click=\"todoList.addTodo()\">\n Add\n </button>\n </div>\n </form>\n </div>\n\n <div class=\"col-lg-12 col-md-12 col-sm-12\">\n <div class=\"existing-todos\">\n <todo-checklist\n title=\"Todos\"\n todos=\"todoList.todos\"\n show-checked=\"false\"\n update-todo=\"todoList.updateTodo\"\n steps=\"todoList.steps\"\n task-slugs=\"todoList.taskSlugs\"\n ></todo-checklist>\n <todo-checklist\n title=\"Completed\"\n todos=\"todoList.todos\"\n show-checked=\"true\"\n update-todo=\"todoList.updateTodo\"\n steps=\"todoList.steps\"\n task-slugs=\"todoList.taskSlugs\"\n ></todo-checklist>\n </div>\n </div>\n </div>\n </div>\n</section>\n";
module.exports = "<section class=\"section-panel todo-list\">\n <div class=\"container-fluid\">\n <div class=\"row section-header\">\n <div class=\"col-lg-12 col-md-12 col-sm-12\">\n <h3>\n Todo List\n </h3>\n </div>\n </div>\n <div class=\"row section-body\" ng-if=\"todoList.ready\">\n <div class=\"col-lg-6 col-md-6 col-sm-12\">\n <div class=\"todolist-pane\">\n <div class=\"new-todo__box todolist-pane__heading col-sm-3\">\n <select name=\"todoList\" id=\"todoList\" ng-model=\"todoList.newTodoTaskId\">\n <option value=\"\" selected>Select owner</option>\n <option value=\"{{task.id}}\" ng-repeat=\"task in todoList.possibleTasks\">{{todoList.steps[task.step_slug].name}}</option>\n </select>\n </div>\n\n <form class=\"new-todo\">\n <div class=\"new-todo__box new-todo__box-details\">\n <input class=\"new-todo__description\"\n type=\"text\"\n ng-model=\"todoList.newTodoDescription\"\n placeholder=\"Add a todo item\">\n <div class=\"pull-right\">\n <div class=\"new-todo__description__datetime\">\n <label>Start</label>\n <date-picker\n date=\"todoList.newTodoStartDate\"\n callback=\"todoList.setTimeOfDate\"></date-picker>\n <time-input\n datetime=\"todoList.newTodoStartDate\"\n default-hour=\"8\"\n ></time-input>\n </div>\n <div class=\"new-todo__description__datetime\">\n <label>Due</label>\n <date-picker\n date=\"todoList.newTodoDueDate\"\n callback=\"todoList.setTimeOfDate\"></date-picker>\n <time-input\n datetime=\"todoList.newTodoDueDate\"\n default-hour=\"18\"\n ></time-input>\n </div>\n </div>\n </div>\n <div class=\"new-todo__box\">\n <button type=\"submit\"\n class=\"btn btn-primary btn-sm edit-save-handle\"\n ng-disabled=\"!todoList.canAddTodo()\"\n ng-click=\"todoList.addTodo()\">\n Add\n </button>\n </div>\n </form>\n\n <todo-checklist\n title=\"Todos\"\n todos=\"todoList.todos\"\n show-skipped=\"false\"\n update-todo=\"todoList.updateTodo\"\n toggle-skip-todo=\"todoList.toggleSkipTodo\"\n steps=\"todoList.steps\"\n task-slugs=\"todoList.taskSlugs\"\n ></todo-checklist>\n </div>\n </div>\n <div class=\"col-lg-6 col-md-6 col-sm-12\">\n <div class=\"todolist-pane\">\n <p class=\"todolist-pane__heading col-sm-4\">Skipped todo items</p>\n <todo-checklist\n title=\"Todos\"\n todos=\"todoList.todos\"\n show-skipped=\"true\"\n update-todo=\"todoList.updateTodo\"\n steps=\"todoList.steps\"\n toggle-skip-todo=\"todoList.toggleSkipTodo\"\n task-slugs=\"todoList.taskSlugs\"\n ></todo-checklist>\n </div>\n </div>\n </div>\n\n </div>\n</section>\n";

/***/ }),
/* 210 */
Expand Down Expand Up @@ -61522,7 +61532,9 @@ function todoChecklist() {
title: '@',
todos: '<',
showChecked: '=',
showSkipped: '=',
updateTodo: '=',
toggleSkipTodo: '=',
steps: '<',
taskSlugs: '<'
},
Expand All @@ -61543,7 +61555,7 @@ function todoChecklist() {
/* 212 */
/***/ (function(module, exports) {

module.exports = "<div class=\"list-by-status\" ng-if=\"(todos | filter: {completed: showChecked}).length > 0\">\n <h4>{{title}}</h4>\n <div class=\"todo\"\n ng-repeat=\"todo in todos | filter: {completed: showChecked}\">\n <label ng-class=\"{'text-danger': isInDanger(todo)}\">\n <input type=\"checkbox\"\n ng-model=\"todo.completed\"\n ng-change=\"updateTodo(todo)\">\n <span class=\"todo__role\">{{steps[taskSlugs[todo.task]].name}}:&nbsp;</span>\n <span class=\"todo__description\">{{todo.description}}</span>\n <span ng-class=\"{'todo__dates': true, 'todo__dates-danger': isInDanger(todo)}\">\n <span ng-if=\"isNonEmptyString(todo.start_by_datetime)\">\n Start <datetime-display datetime=\"todo.start_by_datetime\" show-time=\"true\"/>\n </span>\n <span ng-if=\"isNonEmptyString(todo.start_by_datetime) && isNonEmptyString(todo.due_datetime)\" class=\"todo__dates-separator\">|</span>\n <span ng-if=\"isNonEmptyString(todo.due_datetime)\">\n Due <datetime-display datetime=\"todo.due_datetime\" show-time=\"true\"/></span>\n </span>\n </label>\n </div>\n</div>\n";
module.exports = "<div class=\"list-by-status\" ng-if=\"(todos | filter: {skipped_datetime: showSkipped?'!!':'!'}).length > 0\">\n <!-- <h4>{{title}}</h4> -->\n <div class=\"todo\"\n ng-repeat=\"todo in todos | filter: {skipped_datetime: showSkipped?'!!':'!'}\">\n <label ng-class=\"{'text-danger': isInDanger(todo)}\">\n <input type=\"checkbox\"\n ng-hide=\"showSkipped\"\n ng-model=\"todo.completed\"\n ng-change=\"updateTodo(todo)\">\n <span class=\"todo__role\">{{steps[taskSlugs[todo.task]].name}}:&nbsp;</span>\n <span class=\"todo__description\">{{todo.description}}</span>\n <span ng-class=\"{'todo__dates': true, 'todo__dates-danger': isInDanger(todo)}\">\n <span ng-if=\"isNonEmptyString(todo.start_by_datetime)\">\n Start <datetime-display datetime=\"todo.start_by_datetime\" show-time=\"true\"/>\n </span>\n <span ng-if=\"isNonEmptyString(todo.start_by_datetime) && isNonEmptyString(todo.due_datetime)\" class=\"todo__dates-separator\">|</span>\n <span ng-if=\"isNonEmptyString(todo.due_datetime)\">\n Due <datetime-display datetime=\"todo.due_datetime\" show-time=\"true\"/></span>\n </span>\n <span>\n &nbsp;&nbsp;<a href=\"#\" ng-click=\"toggleSkipTodo(todo)\">[{{showSkipped?\"add\":\"skip\"}}]</a>\n </span>\n </label>\n </div>\n</div>\n";

/***/ }),
/* 213 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export default function todoChecklist () {
title: '@',
todos: '<',
showChecked: '=',
showSkipped: '=',
updateTodo: '=',
toggleSkipTodo: '=',
steps: '<',
taskSlugs: '<'
},
Expand Down
10 changes: 7 additions & 3 deletions orchestra/static/orchestra/todos/todo-checklist.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<div class="list-by-status" ng-if="(todos | filter: {completed: showChecked}).length > 0">
<h4>{{title}}</h4>
<div class="list-by-status" ng-if="(todos | filter: {skipped_datetime: showSkipped?'!!':'!'}).length > 0">
<!-- <h4>{{title}}</h4> -->
<div class="todo"
ng-repeat="todo in todos | filter: {completed: showChecked}">
ng-repeat="todo in todos | filter: {skipped_datetime: showSkipped?'!!':'!'}">
<label ng-class="{'text-danger': isInDanger(todo)}">
<input type="checkbox"
ng-hide="showSkipped"
ng-model="todo.completed"
ng-change="updateTodo(todo)">
<span class="todo__role">{{steps[taskSlugs[todo.task]].name}}:&nbsp;</span>
Expand All @@ -16,6 +17,9 @@ <h4>{{title}}</h4>
<span ng-if="isNonEmptyString(todo.due_datetime)">
Due <datetime-display datetime="todo.due_datetime" show-time="true"/></span>
</span>
<span>
&nbsp;&nbsp;<a href="#" ng-click="toggleSkipTodo(todo)">[{{showSkipped?"add":"skip"}}]</a>
</span>
</label>
</div>
</div>
10 changes: 10 additions & 0 deletions orchestra/static/orchestra/todos/todo-list.directive.es6.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ export default function todoList (orchestraApi) {
todoApi.update(todo)
}

todoList.toggleSkipTodo = (todo) => {
if (todo.skipped_datetime) {
todo.skipped_datetime = null
} else {
const datetimeUtc = moment.tz(moment(), moment.tz.guess()).utc()
todo.skipped_datetime = datetimeUtc.format('YYYY-MM-DD HH:mm')
}
todoApi.update(todo)
}

todoList.setTimeOfDate = (datetime) => {
$scope.$apply()
}
Expand Down

0 comments on commit cd20f48

Please sign in to comment.