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