Source code for jot.core._delete_mixin
"""Delete mixin, archive, permanent delete, notes export."""
import os
import re
from datetime import datetime
DELETED_NOTES_DIR = ".jot-deleted-notes"
_DELETED_NOTES_FILENAME_RE = re.compile(
r"^TASK-\d+-notes-\d{8}_\d{6}\.org$"
)
[docs]
class DeleteMixin:
"""Task removal, archival, and notes preservation."""
def _migrate_legacy_deleted_notes(self):
"""Move stray TASK-<id>-notes-<ts>.org files from project_dir
into the DELETED_NOTES_DIR subdirectory. Idempotent: a no-op
when no matching files exist at the project root.
"""
try:
entries = list(self.project_dir.iterdir())
except (OSError, AttributeError):
return 0
matches = [
p for p in entries
if p.is_file() and _DELETED_NOTES_FILENAME_RE.match(p.name)
]
if not matches:
return 0
dest_dir = self.project_dir / DELETED_NOTES_DIR
try:
dest_dir.mkdir(exist_ok=True)
except OSError as e:
print(f"Warning: could not create {dest_dir}: {e}")
return 0
moved = 0
for src in matches:
dest = dest_dir / src.name
if dest.exists():
continue
try:
src.rename(dest)
moved += 1
except OSError as e:
print(f"Warning: could not move {src.name}: {e}")
return moved
def _unlink_children(self, task_id):
"""Remove parent:{task_id} labels from all children."""
parent_label = f"parent:{task_id}"
for task in self.tasks:
labels = task.get('labels', [])
if parent_label in labels:
task['labels'] = [l for l in labels if l != parent_label]
[docs]
def remove_task(self, task_id):
"""Archive a task by ID and set previous task as current"""
self._unlink_children(task_id)
org_file = self.save_notes_to_org_file(task_id)
if org_file:
print(f"\n\u2713 Notes saved to {os.path.basename(org_file)}")
task_to_archive = None
task_index = -1
for i, task in enumerate(self.tasks):
if task['id'] == task_id:
task_to_archive = task
task_index = i
break
if not task_to_archive:
return False
self.tasks = [t for t in self.tasks if t['id'] != task_id]
self.archived.append(task_to_archive)
# Set previous task as current
if self.tasks:
for task in self.tasks:
task['current'] = False
if task_index == 0:
self.tasks[0]['current'] = True
else:
new_index = min(task_index - 1, len(self.tasks) - 1)
self.tasks[new_index]['current'] = True
self._save_tasks()
return True
[docs]
def delete_task_permanently(self, task_id):
"""Permanently delete a task and release its ID for reuse"""
self._unlink_children(task_id)
org_file = self.save_notes_to_org_file(task_id)
if org_file:
print(f"\n\u2713 Notes saved to {os.path.basename(org_file)}")
deleted = False
original_count = len(self.tasks)
self.tasks = [t for t in self.tasks if t['id'] != task_id]
if len(self.tasks) < original_count:
deleted = True
original_archived = len(self.archived)
self.archived = [t for t in self.archived if t['id'] != task_id]
if len(self.archived) < original_archived:
deleted = True
if deleted:
self.id_manager.release_id(task_id)
self._save_tasks()
return True
return False
[docs]
def archive_task(self, task_id):
"""Archive a task by ID (wrapper around remove_task)"""
return self.remove_task(task_id)
[docs]
def save_notes_to_org_file(self, task_id):
"""Save task notes to an org file before deletion.
Only creates file if task has non-empty notes.
Returns:
str: Path to created org file, or None if no notes
"""
task = None
for t in self.tasks:
if t['id'] == task_id:
task = t
break
if not task:
for t in self.archived:
if t['id'] == task_id:
task = t
break
if not task:
return None
notes = task.get('notes', '').strip()
if not notes:
return None
# Format created_at for display
created_at = task.get('created_at', 'unknown')
if created_at != 'unknown':
try:
created_dt = datetime.fromisoformat(
created_at.replace('Z', '+00:00'))
created_at = created_dt.strftime('%Y-%m-%d %H:%M:%S')
except Exception:
pass
now = datetime.now()
org_content = (
f"#+TITLE: Task #{task_id} Notes (Deleted)\n"
f"#+DATE: {now.strftime('%Y-%m-%d')}\n\n"
f"* Task Information\n"
f" - ID: {task_id}\n"
f" - Text: {task['text']}\n"
f" - Status: {task.get('status', 'todo')}\n"
f" - Priority: {task.get('priority', 'none')}\n"
f" - Created: {created_at}\n\n"
f"* Notes\n{notes}\n\n"
f"* Metadata\n"
f" - Deleted: {now.strftime('%Y-%m-%d %H:%M:%S')}\n"
f" - Original file: {self.storage_file.name}\n"
)
filename = f"TASK-{task_id}-notes-{now.strftime('%Y%m%d_%H%M%S')}.org"
dest_dir = self.project_dir / DELETED_NOTES_DIR
try:
dest_dir.mkdir(exist_ok=True)
except OSError as e:
print(f"Warning: Could not create {dest_dir}: {e}")
return None
org_file_path = dest_dir / filename
try:
with open(org_file_path, 'w') as f:
f.write(org_content)
return str(org_file_path)
except Exception as e:
print(f"Warning: Could not save notes to org file: {e}")
return None