Print msgs from the mgmt command, not the operations module
This commit is contained in:
parent
365435e839
commit
57b99d4d43
3 changed files with 42 additions and 25 deletions
12
README.md
12
README.md
|
@ -168,7 +168,7 @@ The previous `tox` system was removed with the v2 release, since we no longer ai
|
||||||
|
|
||||||
# Importing Tasks via CSV
|
# Importing Tasks via CSV
|
||||||
|
|
||||||
django-todo has the ability to batch-import ("upsert") tasks from a specifically formatted CSV spreadsheet. This ability is provided through both a management command or the web interface.
|
django-todo has the ability to batch-import ("upsert") tasks from a specifically formatted CSV spreadsheet. This ability is provided through both a management command and a web interface.
|
||||||
|
|
||||||
## Management Command
|
## Management Command
|
||||||
|
|
||||||
|
@ -176,13 +176,13 @@ django-todo has the ability to batch-import ("upsert") tasks from a specifically
|
||||||
|
|
||||||
## Web Importer
|
## Web Importer
|
||||||
|
|
||||||
Link from your navigation to `{url todo:import_csv}`
|
Link from your navigation to `{url "todo:import_csv"}`
|
||||||
|
|
||||||
## Import Logic
|
## Import Logic
|
||||||
|
|
||||||
Because data entered via CSV is not going through the same view permissions enforced in the rest of django-todo, and to simplify the logic of when to update vs create a record, etc., the importer will *not* create new users, groups, or task lists. All users, groups, and task lists referenced i your CSV must already exist, and memberships must be correct (if you have a row specifying a user in an incorrect group, the importer will skip that row).
|
Because data entered via CSV is not going through the same view permissions enforced in the rest of django-todo, and to simplify data dependency logic, and to pre-empt disagreements between django-todo users, the importer will *not* create new users, groups, or task lists. All users, groups, and task lists referenced in your CSV must already exist, and group memberships must be correct (if you have a row specifying a user in an incorrect group, the importer will skip that row).
|
||||||
|
|
||||||
Any validation error (e.g. unparse-able dates) results in that row being skipped.
|
Any validation error (e.g. unparse-able dates) will result in that row being skipped.
|
||||||
|
|
||||||
A report of rows upserted and rows skipped (with line numbers and reasons) is provided at the end of the run.
|
A report of rows upserted and rows skipped (with line numbers and reasons) is provided at the end of the run.
|
||||||
|
|
||||||
|
@ -190,12 +190,14 @@ A report of rows upserted and rows skipped (with line numbers and reasons) is pr
|
||||||
|
|
||||||
Copy `todo/data/import_example.csv` to another location on your system and edit in a spreadsheet or directly.
|
Copy `todo/data/import_example.csv` to another location on your system and edit in a spreadsheet or directly.
|
||||||
|
|
||||||
|
**Do not edit the header row!**
|
||||||
|
|
||||||
The "Created By", "Task List" and "Group" columns are required -- all others are optional and should work pretty much exactly like manual task entry via the web UI.
|
The "Created By", "Task List" and "Group" columns are required -- all others are optional and should work pretty much exactly like manual task entry via the web UI.
|
||||||
|
|
||||||
Note: Internally, Tasks are keyed to TaskLists, not to Groups (TaskLists are in Gruops). However, we request the Group in the CSV
|
Note: Internally, Tasks are keyed to TaskLists, not to Groups (TaskLists are in Gruops). However, we request the Group in the CSV
|
||||||
because it's possible to have multiple TaskLists with the same name in different groups; i.e. we need it for namespacing and permissions.
|
because it's possible to have multiple TaskLists with the same name in different groups; i.e. we need it for namespacing and permissions.
|
||||||
|
|
||||||
## Upsert Logic:
|
## Upsert Logic
|
||||||
|
|
||||||
For each valid row, we need to decide whether to create a new task or update an existing one. django-todo matches on the unique combination of Task List, Task Title, and Created By. If we find a task that matches those three, we *update* the rest of the columns. In other words, if you import a CSV once, then edit the Assigned To for a task and import it again, the original task will be updated with a new assignee (and same for the other columns).
|
For each valid row, we need to decide whether to create a new task or update an existing one. django-todo matches on the unique combination of Task List, Task Title, and Created By. If we find a task that matches those three, we *update* the rest of the columns. In other words, if you import a CSV once, then edit the Assigned To for a task and import it again, the original task will be updated with a new assignee (and same for the other columns).
|
||||||
|
|
||||||
|
|
|
@ -28,4 +28,21 @@ class Command(BaseCommand):
|
||||||
filepath = str(options.get("file"))
|
filepath = str(options.get("file"))
|
||||||
|
|
||||||
importer = CSVImporter()
|
importer = CSVImporter()
|
||||||
importer.upsert(filepath)
|
results = importer.upsert(filepath)
|
||||||
|
|
||||||
|
# Report successes, failures and summaries
|
||||||
|
print()
|
||||||
|
for upsert_msg in results.get("upserts"):
|
||||||
|
print(upsert_msg)
|
||||||
|
|
||||||
|
# Stored errors has the form:
|
||||||
|
# self.errors = [{3: ["Incorrect foo", "Non-existent bar"]}, {7: [...]}]
|
||||||
|
for error_dict in results.get("errors"):
|
||||||
|
for k, error_list in error_dict.items():
|
||||||
|
print(f"\nSkipped CSV row {k}:")
|
||||||
|
for msg in error_list:
|
||||||
|
print(f"- {msg}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
for summary_msg in results.get("summaries"):
|
||||||
|
print(summary_msg)
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import csv
|
import csv
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import datetime
|
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from icecream import ic
|
|
||||||
|
|
||||||
from todo.models import Task, TaskList
|
from todo.models import Task, TaskList
|
||||||
|
|
||||||
|
@ -19,7 +18,9 @@ class CSVImporter:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.errors = []
|
self.error_msgs = []
|
||||||
|
self.upsert_msgs = []
|
||||||
|
self.summary_msgs = []
|
||||||
self.line_count = 0
|
self.line_count = 0
|
||||||
self.upsert_count = 0
|
self.upsert_count = 0
|
||||||
|
|
||||||
|
@ -34,7 +35,6 @@ class CSVImporter:
|
||||||
# Header row is:
|
# Header row is:
|
||||||
# Title, Group, Task List, Created Date, Due Date, Completed, Created By, Assigned To, Note, Priority
|
# Title, Group, Task List, Created Date, Due Date, Completed, Created By, Assigned To, Note, Priority
|
||||||
|
|
||||||
print("\n")
|
|
||||||
csv_reader = csv.DictReader(csv_file)
|
csv_reader = csv.DictReader(csv_file)
|
||||||
for row in csv_reader:
|
for row in csv_reader:
|
||||||
self.line_count += 1
|
self.line_count += 1
|
||||||
|
@ -57,22 +57,21 @@ class CSVImporter:
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.upsert_count += 1
|
self.upsert_count += 1
|
||||||
print(
|
msg = (
|
||||||
f"Upserted task {obj.id}: \"{obj.title}\""
|
f'Upserted task {obj.id}: "{obj.title}"'
|
||||||
f"in list \"{obj.task_list}\" (group \"{obj.task_list.group}\")"
|
f' in list "{obj.task_list}" (group "{obj.task_list.group}")'
|
||||||
)
|
)
|
||||||
|
self.upsert_msgs.append(msg)
|
||||||
|
|
||||||
# Report. Stored errors has the form:
|
self.summary_msgs.append(f"\nProcessed {self.line_count} CSV rows")
|
||||||
# self.errors = [{3: ["Incorrect foo", "Non-existent bar"]}, {7: [...]}]
|
self.summary_msgs.append(f"Upserted {self.upsert_count} rows")
|
||||||
print("\n")
|
|
||||||
for error_dict in self.errors:
|
|
||||||
for k, error_list in error_dict.items():
|
|
||||||
print(f"Skipped CSV row {k}:")
|
|
||||||
for msg in error_list:
|
|
||||||
print(f"\t{msg}")
|
|
||||||
|
|
||||||
print(f"\nProcessed {self.line_count} CSV rows")
|
_res = {
|
||||||
print(f"Upserted {self.upsert_count} rows")
|
"errors": self.error_msgs,
|
||||||
|
"upserts": self.upsert_msgs,
|
||||||
|
"summaries": self.summary_msgs,
|
||||||
|
}
|
||||||
|
return _res
|
||||||
|
|
||||||
def validate_row(self, row):
|
def validate_row(self, row):
|
||||||
"""Perform data integrity checks and set default values. Returns a valid object for insertion, or False.
|
"""Perform data integrity checks and set default values. Returns a valid object for insertion, or False.
|
||||||
|
@ -147,7 +146,6 @@ class CSVImporter:
|
||||||
else:
|
else:
|
||||||
row["Created Date"] = None # Override default empty string '' value
|
row["Created Date"] = None # Override default empty string '' value
|
||||||
|
|
||||||
|
|
||||||
# #######################
|
# #######################
|
||||||
# Validate Created Date
|
# Validate Created Date
|
||||||
cd = row.get("Created Date")
|
cd = row.get("Created Date")
|
||||||
|
@ -172,7 +170,7 @@ class CSVImporter:
|
||||||
|
|
||||||
# #######################
|
# #######################
|
||||||
if row_errors:
|
if row_errors:
|
||||||
self.errors.append({self.line_count: row_errors})
|
self.error_msgs.append({self.line_count: row_errors})
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# No errors:
|
# No errors:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue