Print msgs from the mgmt command, not the operations module

This commit is contained in:
Scot Hacker 2019-03-09 23:22:49 -08:00
parent 365435e839
commit 57b99d4d43
3 changed files with 42 additions and 25 deletions

View file

@ -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).

View file

@ -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)

View file

@ -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: