Skip to content

Commit 7269d35

Browse files
authored
Reference todo lists with slugs (#785)
* Add slug field to the Todo model * Add comment on a new slug field * Update exporting data to spreadsheet to view todo's slug variable * Add slug column to the beginning of the spreadsheet and handle saving this field to the todo * Fix tests * Small fix * Fix tests and add a check for todo slug to be assigned * Update comment text * Add a test for a slug being added through the spreadsheet * Get slug field using get method
1 parent 67fe26e commit 7269d35

File tree

8 files changed

+72
-21
lines changed

8 files changed

+72
-21
lines changed

Diff for: orchestra/migrations/0095_todo_slug.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 2.2.13 on 2021-06-01 10:41
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('orchestra', '0094_rename_team_messages_url'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='todo',
15+
name='slug',
16+
field=models.CharField(blank=True, max_length=255, null=True),
17+
),
18+
]

Diff for: orchestra/models/core/models.py

+5
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,10 @@ class Todo(TodoMixin, BaseModel):
753753
activity_log (str)
754754
A JSON blob that records the user actions
755755
with this todo
756+
slug (str)
757+
A unique identifier for each todo list item.
758+
It is used to refer and retrieve specific
759+
to-do items.
756760
757761
758762
Constraints:
@@ -795,6 +799,7 @@ class Status(ChoicesEnum):
795799
status = models.IntegerField(
796800
default=Status.PENDING.value, choices=Status.choices())
797801
additional_data = JSONField(default=dict)
802+
slug = models.CharField(max_length=255, null=True, blank=True)
798803

799804

800805
class TodoQA(TodoQAMixin, BaseModel):

Diff for: orchestra/tests/helpers/fixtures.py

+21-13
Original file line numberDiff line numberDiff line change
@@ -39,49 +39,54 @@
3939
ITERATION_DURATION = timedelta(hours=1)
4040
PICKUP_DELAY = timedelta(hours=1)
4141

42-
TODO_TEMPLATE_GOOD_CSV_TEXT = """Remove if,Skip if
43-
[],[],the root
44-
[],[],,todo parent 1
45-
[],"[{""prop"": {""value"": true, ""operator"": ""==""}}]",,,todo child 1-1
46-
"[{""prop"": {""value"": true, ""operator"": ""==""}}]",[],,todo parent 2
47-
[],[],,,todo child 2-1
48-
[],[],,,,todo child 2-1-1
49-
[],[],,,todo child 2-2
42+
TODO_TEMPLATE_GOOD_CSV_TEXT = """Slug,Remove if,Skip if
43+
,[],[],the root
44+
todo-parent-slug,[],[],,todo parent 1
45+
,[],"[{""prop"": {""value"": true, ""operator"": ""==""}}]",,,todo child 1-1
46+
,"[{""prop"": {""value"": true, ""operator"": ""==""}}]",[],,todo parent 2
47+
,[],[],,,todo child 2-1
48+
,[],[],,,,todo child 2-1-1
49+
,[],[],,,todo child 2-2
5050
"""
5151

5252
TODO_TEMPLATE_BAD_HEADER_CSV_TEXT = """,Remove if,Skip if
5353
[],[],the root
5454
"""
5555

56-
TODO_TEMPLATE_TWO_ENTRIES_CSV_TEXT = """Remove if,Skip if
57-
[],[],the root,the second root
56+
TODO_TEMPLATE_TWO_ENTRIES_CSV_TEXT = """Slug,Remove if,Skip if
57+
,[],[],the root,the second root
5858
"""
5959

60-
TODO_TEMPLATE_INVALID_PARENT_CSV_TEXT = """Remove if,Skip if
61-
[],[],the root
62-
[],[],,,,,,an impossible child
60+
TODO_TEMPLATE_INVALID_PARENT_CSV_TEXT = """Slug,Remove if,Skip if
61+
,[],[],the root
62+
,[],[],,,,,,an impossible child
6363
"""
6464

6565
TODO_TEMPLATE_NESTED_TODOS = {
6666
'description': 'the root',
67+
'slug': None,
6768
'id': 0,
6869
'items': [
6970
{
7071
'id': 2,
7172
'description': 'todo parent 2',
73+
'slug': None,
7274
'items': [
7375
{
7476
'id': 22,
7577
'description': 'todo child 2-2',
78+
'slug': None,
7679
'items': []
7780
},
7881
{
7982
'id': 21,
8083
'description': 'todo child 2-1',
84+
'slug': None,
8185
'items': [
8286
{
8387
'id': 211,
8488
'description': 'todo child 2-1-1',
89+
'slug': None,
8590
'items': []
8691
}
8792
]
@@ -97,9 +102,11 @@
97102
{
98103
'id': 1,
99104
'description': 'todo parent 1',
105+
'slug': 'todo-parent-slug',
100106
'items': [{
101107
'id': 11,
102108
'description': 'todo child 1-1',
109+
'slug': None,
103110
'items': [],
104111
'skip_if': [{
105112
'prop': {
@@ -246,6 +253,7 @@ class TodoFactory(factory.django.DjangoModelFactory):
246253
lambda n: 'Title {}'.format(n))
247254
start_by_datetime = None
248255
due_datetime = None
256+
slug = None
249257

250258
class Meta:
251259
model = 'orchestra.Todo'

Diff for: orchestra/tests/test_project_api_client.py

+5
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def post(url, *args, **kwargs):
7575
return_value.text = json.dumps(return_value.json())
7676
return return_value
7777

78+
todo_child_slug = 'todo-child-slug'
7879
todolist_template = TodoListTemplateFactory(
7980
slug=self.todolist_template_slug,
8081
name=self.todolist_template_name,
@@ -84,10 +85,12 @@ def post(url, *args, **kwargs):
8485
'description': 'todo parent',
8586
'project': self.project.id,
8687
'step': self.step.slug,
88+
'slug': None,
8789
'items': [{
8890
'id': 2,
8991
'project': self.project.id,
9092
'step': self.step.slug,
93+
'slug': todo_child_slug,
9194
'description': 'todo child',
9295
'items': []
9396
}]
@@ -105,6 +108,8 @@ def post(url, *args, **kwargs):
105108
additional_data)
106109
self.assertEqual(result['success'], True)
107110
self.assertEqual(len(result['todos']), 3)
111+
self.assertEqual(result['todos'][0]['slug'], todo_child_slug)
112+
self.assertEqual(result['todos'][1]['slug'], None)
108113
for t in result['todos']:
109114
self.assertEqual(t['template'], todolist_template.id)
110115
self.assertEqual(t['section'], None)

Diff for: orchestra/tests/test_todos.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ def _todo_data(title, status=Todo.Status.PENDING.value,
4040
start_by=None,
4141
due=None, parent_todo=None, template=None,
4242
activity_log=str({'actions': []}), qa=None,
43-
project=None, step=None, details=None, is_deleted=False):
43+
project=None, step=None, details=None, is_deleted=False,
44+
slug=None):
4445
return {
4546
'title': title,
4647
'template': template,
@@ -55,6 +56,7 @@ def _todo_data(title, status=Todo.Status.PENDING.value,
5556
'section': None,
5657
'status': status,
5758
'step': step,
59+
'slug': slug,
5860
'details': details,
5961
'is_deleted': is_deleted
6062
}
@@ -428,6 +430,7 @@ def setUp(self):
428430
self.step = StepFactory(
429431
slug='step-slug',
430432
workflow_version=self.workflow_version)
433+
self.slug = 'todo-item-slug'
431434
self.project = ProjectFactory(
432435
workflow_version=self.workflow_version)
433436
self.project2 = ProjectFactory()
@@ -538,10 +541,12 @@ def test_update_todos_from_todolist_template_success(self):
538541
'description': 'todo parent',
539542
'project': self.project.id,
540543
'step': self.step.slug,
544+
'slug': None,
541545
'items': [{
542546
'id': 2,
543547
'project': self.project.id,
544548
'step': self.step.slug,
549+
'slug': self.slug,
545550
'description': 'todo child',
546551
'items': []
547552
}]
@@ -563,7 +568,8 @@ def test_update_todos_from_todolist_template_success(self):
563568
template=todolist_template.id,
564569
parent_todo=todos[1]['id'],
565570
project=self.project.id,
566-
step=self.step.slug),
571+
step=self.step.slug,
572+
slug=self.slug),
567573
_todo_data('todo parent',
568574
template=todolist_template.id,
569575
parent_todo=todos[2]['id'],
@@ -645,9 +651,11 @@ def test_conditional_skip_remove_todos_from_template(self):
645651
'id': 1,
646652
'description': 'todo parent 1',
647653
'project': self.project.id,
654+
'slug': None,
648655
'items': [{
649656
'id': 2,
650657
'description': 'todo child 1',
658+
'slug': None,
651659
'project': self.project.id,
652660
'items': []
653661
}],
@@ -661,9 +669,11 @@ def test_conditional_skip_remove_todos_from_template(self):
661669
'id': 3,
662670
'description': 'todo parent 2',
663671
'project': self.project.id,
672+
'slug': None,
664673
'items': [{
665674
'id': 4,
666675
'description': 'todo child 2',
676+
'slug': None,
667677
'project': self.project.id,
668678
'items': [],
669679
'skip_if': [{

Diff for: orchestra/todos/api.py

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def _add_template_todo(
9696
project=project,
9797
step=step,
9898
title=template_todo['description'],
99+
slug=template_todo.get('slug'),
99100
template=todolist_template,
100101
parent_todo=parent_todo,
101102
status=status,

Diff for: orchestra/todos/import_export.py

+9-6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
REMOVE_IF_HEADER = 'Remove if'
1717
SKIP_IF_HEADER = 'Skip if'
18+
SLUG_HEADER = 'Slug'
1819

1920

2021
def _write_template_rows(writer, todo, depth):
@@ -25,7 +26,8 @@ def _write_template_rows(writer, todo, depth):
2526
this row's description.
2627
"""
2728
writer.writerow(
28-
[json.dumps(todo.get('remove_if', [])),
29+
[todo.get('slug', ''),
30+
json.dumps(todo.get('remove_if', [])),
2931
json.dumps(todo.get('skip_if', []))] +
3032
([''] * depth) +
3133
[todo.get('description', '')])
@@ -62,7 +64,7 @@ def export_to_spreadsheet(todo_list_template):
6264
"""
6365
with NamedTemporaryFile(mode='w+', delete=False) as file:
6466
writer = csv.writer(file)
65-
writer.writerow([REMOVE_IF_HEADER, SKIP_IF_HEADER])
67+
writer.writerow([SLUG_HEADER, REMOVE_IF_HEADER, SKIP_IF_HEADER])
6668
_write_template_rows(writer, todo_list_template.todos, 0)
6769
file.flush()
6870
return _upload_csv_to_google(
@@ -84,7 +86,7 @@ def import_from_spreadsheet(todo_list_template, spreadsheet_url, request):
8486
except ValueError as e:
8587
raise TodoListTemplateValidationError(e)
8688
header = next(reader)
87-
if header[:2] != [REMOVE_IF_HEADER, SKIP_IF_HEADER]:
89+
if header[:3] != [SLUG_HEADER, REMOVE_IF_HEADER, SKIP_IF_HEADER]:
8890
raise TodoListTemplateValidationError(
8991
'Unexpected header: {}'.format(header))
9092
# The `i`'th entry in parent_items is current list of child to-dos
@@ -96,12 +98,13 @@ def import_from_spreadsheet(todo_list_template, spreadsheet_url, request):
9698
for rowindex, row in enumerate(reader):
9799
item = {
98100
'id': rowindex,
99-
'remove_if': json.loads(row[0] or '[]'),
100-
'skip_if': json.loads(row[1] or '[]'),
101+
'remove_if': json.loads(row[1] or '[]'),
102+
'skip_if': json.loads(row[2] or '[]'),
103+
'slug': row[0] or None,
101104
'items': []
102105
}
103106
nonempty_columns = [(columnindex, text)
104-
for columnindex, text in enumerate(row[2:])
107+
for columnindex, text in enumerate(row[3:])
105108
if text]
106109
if len(nonempty_columns) == 0:
107110
continue

Diff for: orchestra/todos/serializers.py

+1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ class Meta:
137137
'section',
138138
'project',
139139
'step',
140+
'slug',
140141
'order',
141142
'status',
142143
'additional_data',

0 commit comments

Comments
 (0)