diff --git a/history_tracking/apps.py b/history_tracking/apps.py index f7f856e2..0aba4b72 100644 --- a/history_tracking/apps.py +++ b/history_tracking/apps.py @@ -1,26 +1,9 @@ -# history_tracking/apps.py from django.apps import AppConfig - class HistoryTrackingConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "history_tracking" + default_auto_field = 'django.db.models.BigAutoField' + name = 'history_tracking' def ready(self): - from django.apps import apps - from .mixins import HistoricalChangeMixin - - # Get the Park model - try: - Park = apps.get_model('parks', 'Park') - ParkArea = apps.get_model('parks', 'ParkArea') - - # Apply mixin to historical models - if HistoricalChangeMixin not in Park.history.model.__bases__: - Park.history.model.__bases__ = (HistoricalChangeMixin,) + Park.history.model.__bases__ - - if HistoricalChangeMixin not in ParkArea.history.model.__bases__: - ParkArea.history.model.__bases__ = (HistoricalChangeMixin,) + ParkArea.history.model.__bases__ - except LookupError: - # Models might not be loaded yet - pass + """Register signals when the app is ready""" + from . import signals # Import signals to register them diff --git a/history_tracking/managers.py b/history_tracking/managers.py new file mode 100644 index 00000000..c4ef39c8 --- /dev/null +++ b/history_tracking/managers.py @@ -0,0 +1,177 @@ +from typing import Optional, List, Dict, Any, Tuple +from django.db import transaction +from django.core.exceptions import ValidationError +from django.utils import timezone +from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType +from .models import VersionBranch, VersionTag, ChangeSet + +User = get_user_model() + +class BranchManager: + """Manages version control branch operations""" + + @transaction.atomic + def create_branch(self, name: str, parent: Optional[VersionBranch] = None, + user: Optional[User] = None) -> VersionBranch: + """Create a new version branch""" + branch = VersionBranch.objects.create( + name=name, + parent=parent, + created_by=user, + metadata={ + 'created_from': parent.name if parent else 'root', + 'created_at': timezone.now().isoformat() + } + ) + branch.full_clean() + return branch + + @transaction.atomic + def merge_branches(self, source: VersionBranch, target: VersionBranch, + user: Optional[User] = None) -> Tuple[bool, List[Dict[str, Any]]]: + """ + Merge source branch into target branch + Returns: (success, conflicts) + """ + if not source.is_active or not target.is_active: + raise ValidationError("Cannot merge inactive branches") + + merger = MergeStrategy() + success, conflicts = merger.auto_merge(source, target) + + if success: + # Record successful merge + ChangeSet.objects.create( + branch=target, + created_by=user, + description=f"Merged branch '{source.name}' into '{target.name}'", + metadata={ + 'merge_source': source.name, + 'merge_target': target.name, + 'merged_at': timezone.now().isoformat() + }, + status='applied' + ) + + return success, conflicts + + def list_branches(self, include_inactive: bool = False) -> List[VersionBranch]: + """Get all branches with their relationships""" + queryset = VersionBranch.objects.select_related('parent') + if not include_inactive: + queryset = queryset.filter(is_active=True) + return list(queryset) + +class ChangeTracker: + """Tracks and manages changes across the system""" + + @transaction.atomic + def record_change(self, instance: Any, change_type: str, + branch: VersionBranch, user: Optional[User] = None, + metadata: Optional[Dict] = None) -> ChangeSet: + """Record a change in the system""" + if not hasattr(instance, 'history'): + raise ValueError("Instance must be a model with history tracking enabled") + + # Create historical record by saving the instance + instance.save() + historical_record = instance.history.first() + + if not historical_record: + raise ValueError("Failed to create historical record") + + # Create changeset + content_type = ContentType.objects.get_for_model(historical_record) + changeset = ChangeSet.objects.create( + branch=branch, + created_by=user, + description=f"{change_type} operation on {instance._meta.model_name}", + metadata=metadata or {}, + status='pending', + content_type=content_type, + object_id=historical_record.pk + ) + + return changeset + + def get_changes(self, branch: VersionBranch) -> List[ChangeSet]: + """Get all changes in a branch ordered by creation time""" + return list(ChangeSet.objects.filter(branch=branch).order_by('created_at')) + +class MergeStrategy: + """Handles merge operations and conflict resolution""" + + def auto_merge(self, source: VersionBranch, + target: VersionBranch) -> Tuple[bool, List[Dict[str, Any]]]: + """ + Attempt automatic merge between branches + Returns: (success, conflicts) + """ + conflicts = [] + + # Get all changes since branch creation + source_changes = ChangeSet.objects.filter( + branch=source, + status='applied' + ).order_by('created_at') + + target_changes = ChangeSet.objects.filter( + branch=target, + status='applied' + ).order_by('created_at') + + # Detect conflicts + for source_change in source_changes: + for target_change in target_changes: + if self._detect_conflict(source_change, target_change): + conflicts.append({ + 'source_change': source_change.pk, + 'target_change': target_change.pk, + 'type': 'content_conflict', + 'description': 'Conflicting changes detected' + }) + + if conflicts: + return False, conflicts + + # No conflicts, apply source changes to target + for change in source_changes: + self._apply_change_to_branch(change, target) + + return True, [] + + def _detect_conflict(self, change1: ChangeSet, change2: ChangeSet) -> bool: + """Check if two changes conflict with each other""" + # Get historical instances + instance1 = change1.historical_instance + instance2 = change2.historical_instance + + if not (instance1 and instance2): + return False + + # Same model and instance ID indicates potential conflict + return ( + instance1._meta.model == instance2._meta.model and + instance1.id == instance2.id + ) + + @transaction.atomic + def _apply_change_to_branch(self, change: ChangeSet, + target_branch: VersionBranch) -> None: + """Apply a change from one branch to another""" + # Create new changeset in target branch + new_changeset = ChangeSet.objects.create( + branch=target_branch, + description=f"Applied change from '{change.branch.name}'", + metadata={ + 'source_change': change.pk, + 'source_branch': change.branch.name + }, + status='pending', + content_type=change.content_type, + object_id=change.object_id + ) + + new_changeset.status = 'applied' + new_changeset.save() \ No newline at end of file diff --git a/history_tracking/migrations/0002_versionbranch_changeset_versiontag_and_more.py b/history_tracking/migrations/0002_versionbranch_changeset_versiontag_and_more.py new file mode 100644 index 00000000..3e48edae --- /dev/null +++ b/history_tracking/migrations/0002_versionbranch_changeset_versiontag_and_more.py @@ -0,0 +1,220 @@ +# Generated by Django 5.1.6 on 2025-02-06 22:00 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ("history_tracking", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="VersionBranch", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255, unique=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("metadata", models.JSONField(blank=True, default=dict)), + ("is_active", models.BooleanField(default=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "parent", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="children", + to="history_tracking.versionbranch", + ), + ), + ], + options={ + "ordering": ["-created_at"], + }, + ), + migrations.CreateModel( + name="ChangeSet", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("description", models.TextField(blank=True)), + ("metadata", models.JSONField(blank=True, default=dict)), + ("dependencies", models.JSONField(blank=True, default=dict)), + ( + "status", + models.CharField( + choices=[ + ("pending", "Pending"), + ("applied", "Applied"), + ("failed", "Failed"), + ("reverted", "Reverted"), + ], + default="pending", + max_length=20, + ), + ), + ("object_id", models.PositiveIntegerField()), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="contenttypes.contenttype", + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "branch", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="changesets", + to="history_tracking.versionbranch", + ), + ), + ], + options={ + "ordering": ["-created_at"], + }, + ), + migrations.CreateModel( + name="VersionTag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255, unique=True)), + ("object_id", models.PositiveIntegerField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("metadata", models.JSONField(blank=True, default=dict)), + ( + "branch", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tags", + to="history_tracking.versionbranch", + ), + ), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="contenttypes.contenttype", + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["-created_at"], + }, + ), + migrations.AddIndex( + model_name="versionbranch", + index=models.Index(fields=["name"], name="history_tra_name_cf8692_idx"), + ), + migrations.AddIndex( + model_name="versionbranch", + index=models.Index( + fields=["parent"], name="history_tra_parent__c645fa_idx" + ), + ), + migrations.AddIndex( + model_name="versionbranch", + index=models.Index( + fields=["created_at"], name="history_tra_created_6f9fc9_idx" + ), + ), + migrations.AddIndex( + model_name="changeset", + index=models.Index( + fields=["branch"], name="history_tra_branch__0c1728_idx" + ), + ), + migrations.AddIndex( + model_name="changeset", + index=models.Index( + fields=["created_at"], name="history_tra_created_c0fe58_idx" + ), + ), + migrations.AddIndex( + model_name="changeset", + index=models.Index(fields=["status"], name="history_tra_status_93e04d_idx"), + ), + migrations.AddIndex( + model_name="changeset", + index=models.Index( + fields=["content_type", "object_id"], + name="history_tra_content_9f97ff_idx", + ), + ), + migrations.AddIndex( + model_name="versiontag", + index=models.Index(fields=["name"], name="history_tra_name_38da60_idx"), + ), + migrations.AddIndex( + model_name="versiontag", + index=models.Index( + fields=["branch"], name="history_tra_branch__0a9a55_idx" + ), + ), + migrations.AddIndex( + model_name="versiontag", + index=models.Index( + fields=["created_at"], name="history_tra_created_7a1501_idx" + ), + ), + migrations.AddIndex( + model_name="versiontag", + index=models.Index( + fields=["content_type", "object_id"], + name="history_tra_content_0892f3_idx", + ), + ), + ] diff --git a/history_tracking/models.py b/history_tracking/models.py index 234fd852..52e2ae6c 100644 --- a/history_tracking/models.py +++ b/history_tracking/models.py @@ -1,14 +1,18 @@ -# history_tracking/models.py from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.auth import get_user_model from simple_history.models import HistoricalRecords from .mixins import HistoricalChangeMixin -from typing import Any, Type, TypeVar, cast +from typing import Any, Type, TypeVar, cast, Optional from django.db.models import QuerySet +from django.core.exceptions import ValidationError +from django.utils import timezone T = TypeVar('T', bound=models.Model) +User = get_user_model() + class HistoricalModel(models.Model): """Abstract base class for models with history tracking""" id = models.BigAutoField(primary_key=True) @@ -47,3 +51,124 @@ class HistoricalSlug(models.Model): def __str__(self) -> str: return f"{self.content_type} - {self.object_id} - {self.slug}" + +class VersionBranch(models.Model): + """Represents a version control branch for tracking parallel development""" + name = models.CharField(max_length=255, unique=True) + parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, related_name='children') + created_at = models.DateTimeField(auto_now_add=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) + metadata = models.JSONField(default=dict, blank=True) + is_active = models.BooleanField(default=True) + + class Meta: + ordering = ['-created_at'] + indexes = [ + models.Index(fields=['name']), + models.Index(fields=['parent']), + models.Index(fields=['created_at']), + ] + + def __str__(self) -> str: + return f"{self.name} ({'active' if self.is_active else 'inactive'})" + + def clean(self) -> None: + # Prevent circular references + if self.parent and self.pk: + branch = self.parent + while branch: + if branch.pk == self.pk: + raise ValidationError("Circular branch reference detected") + branch = branch.parent + +class VersionTag(models.Model): + """Tags specific versions for reference (releases, milestones, etc)""" + name = models.CharField(max_length=255, unique=True) + branch = models.ForeignKey(VersionBranch, on_delete=models.CASCADE, related_name='tags') + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + historical_instance = GenericForeignKey('content_type', 'object_id') + created_at = models.DateTimeField(auto_now_add=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) + metadata = models.JSONField(default=dict, blank=True) + + class Meta: + ordering = ['-created_at'] + indexes = [ + models.Index(fields=['name']), + models.Index(fields=['branch']), + models.Index(fields=['created_at']), + models.Index(fields=['content_type', 'object_id']), + ] + + def __str__(self) -> str: + return f"{self.name} ({self.branch.name})" + +class ChangeSet(models.Model): + """Groups related changes together for atomic version control operations""" + branch = models.ForeignKey(VersionBranch, on_delete=models.CASCADE, related_name='changesets') + created_at = models.DateTimeField(auto_now_add=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) + description = models.TextField(blank=True) + metadata = models.JSONField(default=dict, blank=True) + dependencies = models.JSONField(default=dict, blank=True) + status = models.CharField( + max_length=20, + choices=[ + ('pending', 'Pending'), + ('applied', 'Applied'), + ('failed', 'Failed'), + ('reverted', 'Reverted') + ], + default='pending' + ) + + # Instead of directly relating to HistoricalRecord, use GenericForeignKey + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + historical_instance = GenericForeignKey('content_type', 'object_id') + + class Meta: + ordering = ['-created_at'] + indexes = [ + models.Index(fields=['branch']), + models.Index(fields=['created_at']), + models.Index(fields=['status']), + models.Index(fields=['content_type', 'object_id']), + ] + + def __str__(self) -> str: + return f"ChangeSet {self.pk} ({self.branch.name} - {self.status})" + + def apply(self) -> None: + """Apply the changeset to the target branch""" + if self.status != 'pending': + raise ValidationError(f"Cannot apply changeset with status: {self.status}") + + try: + # Apply changes through the historical instance + if self.historical_instance: + instance = self.historical_instance.instance + if instance: + instance.save() + self.status = 'applied' + except Exception as e: + self.status = 'failed' + self.metadata['error'] = str(e) + self.save() + + def revert(self) -> None: + """Revert the changes in this changeset""" + if self.status != 'applied': + raise ValidationError(f"Cannot revert changeset with status: {self.status}") + + try: + # Revert changes through the historical instance + if self.historical_instance: + instance = self.historical_instance.instance + if instance: + instance.save() + self.status = 'reverted' + except Exception as e: + self.metadata['revert_error'] = str(e) + self.save() diff --git a/history_tracking/signals.py b/history_tracking/signals.py new file mode 100644 index 00000000..b374d9aa --- /dev/null +++ b/history_tracking/signals.py @@ -0,0 +1,138 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from simple_history.signals import post_create_historical_record +from django.contrib.auth import get_user_model +from django.db import transaction +from .models import VersionBranch, ChangeSet, HistoricalModel +from .managers import ChangeTracker +import threading + +User = get_user_model() + +# Thread-local storage for tracking active changesets +_changeset_context = threading.local() + +def get_current_branch(): + """Get the currently active branch for the thread""" + return getattr(_changeset_context, 'current_branch', None) + +def set_current_branch(branch): + """Set the active branch for the current thread""" + _changeset_context.current_branch = branch + +def clear_current_branch(): + """Clear the active branch for the current thread""" + if hasattr(_changeset_context, 'current_branch'): + del _changeset_context.current_branch + +class ChangesetContextManager: + """Context manager for tracking changes in a specific branch""" + + def __init__(self, branch, user=None): + self.branch = branch + self.user = user + self.previous_branch = None + + def __enter__(self): + self.previous_branch = get_current_branch() + set_current_branch(self.branch) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + set_current_branch(self.previous_branch) + +@receiver(post_create_historical_record) +def handle_history_record(sender, instance, history_instance, **kwargs): + """Handle creation of historical records by adding them to changesets""" + # Only handle records from HistoricalModel subclasses + if not isinstance(instance, HistoricalModel): + return + + branch = get_current_branch() + if not branch: + # If no branch is set, use the default branch + branch, _ = VersionBranch.objects.get_or_create( + name='main', + defaults={ + 'metadata': { + 'type': 'default_branch', + 'created_automatically': True + } + } + ) + + # Create or get active changeset for the current branch + changeset = getattr(_changeset_context, 'active_changeset', None) + if not changeset: + changeset = ChangeSet.objects.create( + branch=branch, + created_by=history_instance.history_user, + description=f"Automatic change tracking: {history_instance.history_type}", + metadata={ + 'auto_tracked': True, + 'model': instance._meta.model_name, + 'history_type': history_instance.history_type + }, + status='applied' + ) + _changeset_context.active_changeset = changeset + + # Add the historical record to the changeset + changeset.historical_records.add(history_instance) + +@receiver(post_save, sender=ChangeSet) +def handle_changeset_save(sender, instance, created, **kwargs): + """Handle changeset creation by updating related objects""" + if created and instance.status == 'applied': + # Clear the active changeset if this is the one we were using + active_changeset = getattr(_changeset_context, 'active_changeset', None) + if active_changeset and active_changeset.id == instance.id: + delattr(_changeset_context, 'active_changeset') + + # Update branch metadata + branch = instance.branch + if not branch.metadata.get('first_change'): + branch.metadata['first_change'] = instance.created_at.isoformat() + branch.metadata['last_change'] = instance.created_at.isoformat() + branch.metadata['change_count'] = branch.changesets.count() + branch.save() + +def start_changeset(branch, user=None, description=None): + """Start a new changeset in the given branch""" + changeset = ChangeSet.objects.create( + branch=branch, + created_by=user, + description=description or "Manual changeset", + status='pending' + ) + _changeset_context.active_changeset = changeset + return changeset + +def commit_changeset(success=True): + """Commit the current changeset""" + changeset = getattr(_changeset_context, 'active_changeset', None) + if changeset: + changeset.status = 'applied' if success else 'failed' + changeset.save() + delattr(_changeset_context, 'active_changeset') + return changeset + +class ChangesetManager: + """Context manager for handling changesets""" + + def __init__(self, branch, user=None, description=None): + self.branch = branch + self.user = user + self.description = description + self.changeset = None + + def __enter__(self): + self.changeset = start_changeset( + self.branch, + self.user, + self.description + ) + return self.changeset + + def __exit__(self, exc_type, exc_val, exc_tb): + commit_changeset(success=exc_type is None) \ No newline at end of file diff --git a/history_tracking/templates/history_tracking/components/branch_create.html b/history_tracking/templates/history_tracking/components/branch_create.html new file mode 100644 index 00000000..9893de9f --- /dev/null +++ b/history_tracking/templates/history_tracking/components/branch_create.html @@ -0,0 +1,58 @@ +
+

Create New Branch

+ +
+ {% csrf_token %} + +
+ + +
+ +
+ + +

+ Leave empty to create from root +

+
+ +
+ + +
+
+
+ + \ No newline at end of file diff --git a/history_tracking/templates/history_tracking/components/branch_list.html b/history_tracking/templates/history_tracking/components/branch_list.html new file mode 100644 index 00000000..08f0cb34 --- /dev/null +++ b/history_tracking/templates/history_tracking/components/branch_list.html @@ -0,0 +1,43 @@ +
+ {% for branch in branches %} +
+
+ {{ branch.name }} + {% if branch.is_active %} + Active + {% endif %} +
+ {% if branch.parent %} +
+ from: {{ branch.parent.name }} +
+ {% endif %} +
+ + +
+
+ {% endfor %} +
+ + \ No newline at end of file diff --git a/history_tracking/templates/history_tracking/components/history_view.html b/history_tracking/templates/history_tracking/components/history_view.html new file mode 100644 index 00000000..7471aa48 --- /dev/null +++ b/history_tracking/templates/history_tracking/components/history_view.html @@ -0,0 +1,88 @@ +
+

Change History

+ + {% if changes %} +
+ {% for change in changes %} +
+
+
+

{{ change.description }}

+
+ {{ change.created_at|date:"M d, Y H:i" }} + {% if change.created_by %} + by {{ change.created_by.username }} + {% endif %} +
+
+ + {{ change.status|title }} + +
+ + {% if change.historical_records.exists %} +
+ {% for record in change.historical_records.all %} +
+
+ {{ record.instance_type|title }} + {% if record.history_type == '+' %} + created + {% elif record.history_type == '-' %} + deleted + {% else %} + modified + {% endif %} +
+ {% if record.history_type == '~' and record.diff_to_prev %} +
+ Changes: +
    + {% for field, values in record.diff_to_prev.items %} +
  • {{ field }}: {{ values.old }} → {{ values.new }}
  • + {% endfor %} +
+
+ {% endif %} +
+ {% endfor %} +
+ {% endif %} + + {% if change.metadata %} +
+
+ Additional Details +
{{ change.metadata|pprint }}
+
+
+ {% endif %} + + {% if change.status == 'applied' %} +
+ +
+ {% endif %} +
+ {% endfor %} +
+ {% else %} +
+ No changes recorded for this branch yet. +
+ {% endif %} +
\ No newline at end of file diff --git a/history_tracking/templates/history_tracking/components/merge_conflicts.html b/history_tracking/templates/history_tracking/components/merge_conflicts.html new file mode 100644 index 00000000..ef33de15 --- /dev/null +++ b/history_tracking/templates/history_tracking/components/merge_conflicts.html @@ -0,0 +1,116 @@ +
+
+
+
+ + + +
+
+

+ Merge Conflicts Detected +

+
+

Conflicts were found while merging '{{ source.name }}' into '{{ target.name }}'.

+
+
+
+
+ +
+

Conflicts to Resolve:

+ +
+ {% csrf_token %} + + + + {% for conflict in conflicts %} +
+
+
+ Conflict #{{ forloop.counter }} +
+ + Type: {{ conflict.type }} + +
+ +
+ {{ conflict.description }} +
+ + {% if conflict.type == 'content_conflict' %} +
+
+ +
+
{{ conflict.source_content }}
+
+
+ +
+ +
+
{{ conflict.target_content }}
+
+
+ +
+ + +
+ + +
+ {% endif %} +
+ {% endfor %} + +
+ + +
+
+
+
+ + \ No newline at end of file diff --git a/history_tracking/templates/history_tracking/components/merge_panel.html b/history_tracking/templates/history_tracking/components/merge_panel.html new file mode 100644 index 00000000..2914c75c --- /dev/null +++ b/history_tracking/templates/history_tracking/components/merge_panel.html @@ -0,0 +1,49 @@ +
+

Merge Branches

+ +
+ {% csrf_token %} + +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+
+
\ No newline at end of file diff --git a/history_tracking/templates/history_tracking/components/merge_success.html b/history_tracking/templates/history_tracking/components/merge_success.html new file mode 100644 index 00000000..dffebff1 --- /dev/null +++ b/history_tracking/templates/history_tracking/components/merge_success.html @@ -0,0 +1,30 @@ +
+
+
+
+ + + +
+
+

+ Merge Successful +

+
+

Successfully merged branch '{{ source.name }}' into '{{ target.name }}'.

+
+
+
+
+ +
+ +
+
+ + \ No newline at end of file diff --git a/history_tracking/templates/history_tracking/version_control_panel.html b/history_tracking/templates/history_tracking/version_control_panel.html new file mode 100644 index 00000000..2520ff2a --- /dev/null +++ b/history_tracking/templates/history_tracking/version_control_panel.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} + +{% block content %} +
+
+ +
+
+

Branches

+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/history_tracking/urls.py b/history_tracking/urls.py new file mode 100644 index 00000000..ab447e33 --- /dev/null +++ b/history_tracking/urls.py @@ -0,0 +1,22 @@ +from django.urls import path +from . import views + +app_name = 'history' + +urlpatterns = [ + # Main VCS interface + path('vcs/', views.VersionControlPanel.as_view(), name='vcs-panel'), + + # Branch operations + path('vcs/branches/', views.BranchListView.as_view(), name='branch-list'), + path('vcs/branches/create/', views.BranchCreateView.as_view(), name='branch-create'), + + # History views + path('vcs/history/', views.HistoryView.as_view(), name='history-view'), + + # Merge operations + path('vcs/merge/', views.MergeView.as_view(), name='merge-view'), + + # Tag operations + path('vcs/tags/create/', views.TagCreateView.as_view(), name='tag-create'), +] \ No newline at end of file diff --git a/history_tracking/utils.py b/history_tracking/utils.py new file mode 100644 index 00000000..e3f11ef6 --- /dev/null +++ b/history_tracking/utils.py @@ -0,0 +1,139 @@ +from typing import Dict, Any, List, Optional +from django.core.exceptions import ValidationError +from .models import VersionBranch, ChangeSet +from django.utils import timezone +from django.contrib.auth import get_user_model + +User = get_user_model() + +def resolve_conflicts( + source_branch: VersionBranch, + target_branch: VersionBranch, + resolutions: Dict[str, str], + manual_resolutions: Dict[str, str], + user: Optional[User] = None +) -> ChangeSet: + """ + Resolve merge conflicts between branches + + Args: + source_branch: Source branch of the merge + target_branch: Target branch of the merge + resolutions: Dict mapping conflict IDs to resolution type ('source', 'target', 'manual') + manual_resolutions: Dict mapping conflict IDs to manual resolution content + user: User performing the resolution + + Returns: + ChangeSet: The changeset recording the conflict resolution + """ + if not resolutions: + raise ValidationError("No resolutions provided") + + resolved_content = {} + + for conflict_id, resolution_type in resolutions.items(): + source_id, target_id = conflict_id.split('_') + source_change = ChangeSet.objects.get(pk=source_id) + target_change = ChangeSet.objects.get(pk=target_id) + + if resolution_type == 'source': + # Use source branch version + for record in source_change.historical_records.all(): + resolved_content[f"{record.instance_type}_{record.instance_pk}"] = record + + elif resolution_type == 'target': + # Use target branch version + for record in target_change.historical_records.all(): + resolved_content[f"{record.instance_type}_{record.instance_pk}"] = record + + elif resolution_type == 'manual': + # Use manual resolution + manual_content = manual_resolutions.get(conflict_id) + if not manual_content: + raise ValidationError(f"Manual resolution missing for conflict {conflict_id}") + + # Create new historical record with manual content + base_record = source_change.historical_records.first() + if base_record: + new_record = base_record.__class__( + **{ + **base_record.__dict__, + 'id': base_record.id, + 'history_date': timezone.now(), + 'history_user': user, + 'history_change_reason': 'Manual conflict resolution', + 'history_type': '~' + } + ) + # Apply manual changes + for field, value in manual_content.items(): + setattr(new_record, field, value) + resolved_content[f"{new_record.instance_type}_{new_record.instance_pk}"] = new_record + + # Create resolution changeset + resolution_changeset = ChangeSet.objects.create( + branch=target_branch, + created_by=user, + description=f"Resolved conflicts from '{source_branch.name}'", + metadata={ + 'resolution_type': 'conflict_resolution', + 'source_branch': source_branch.name, + 'resolved_conflicts': list(resolutions.keys()) + }, + status='applied' + ) + + # Add resolved records to changeset + for record in resolved_content.values(): + resolution_changeset.historical_records.add(record) + + return resolution_changeset + +def get_change_diff(change: ChangeSet) -> List[Dict[str, Any]]: + """ + Get a structured diff of changes in a changeset + + Args: + change: The changeset to analyze + + Returns: + List of diffs for each changed record + """ + diffs = [] + + for record in change.historical_records.all(): + diff = { + 'model': record.instance_type.__name__, + 'id': record.instance_pk, + 'type': record.history_type, + 'date': record.history_date, + 'user': record.history_user_display, + 'changes': {} + } + + if record.history_type == '~': # Modified + previous = record.prev_record + if previous: + diff['changes'] = record.diff_against_previous + elif record.history_type == '+': # Added + diff['changes'] = { + field: {'old': None, 'new': str(getattr(record, field))} + for field in record.__dict__ + if not field.startswith('_') and field not in [ + 'history_date', 'history_id', 'history_type', + 'history_user_id', 'history_change_reason' + ] + } + elif record.history_type == '-': # Deleted + diff['changes'] = { + field: {'old': str(getattr(record, field)), 'new': None} + for field in record.__dict__ + if not field.startswith('_') and field not in [ + 'history_date', 'history_id', 'history_type', + 'history_user_id', 'history_change_reason' + ] + } + + diffs.append(diff) + + return diffs \ No newline at end of file diff --git a/history_tracking/views.py b/history_tracking/views.py index 91ea44a2..c6bb27a6 100644 --- a/history_tracking/views.py +++ b/history_tracking/views.py @@ -1,3 +1,237 @@ -from django.shortcuts import render +from django.views.generic import TemplateView, View +from django.http import HttpResponse, JsonResponse +from django.shortcuts import get_object_or_404 +from django.template.loader import render_to_string +from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.exceptions import ValidationError +from django.db import transaction -# Create your views here. +from .models import VersionBranch, VersionTag, ChangeSet +from .managers import BranchManager, ChangeTracker, MergeStrategy + +class VersionControlPanel(LoginRequiredMixin, TemplateView): + """Main version control interface""" + template_name = 'history_tracking/version_control_panel.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + branch_manager = BranchManager() + + context.update({ + 'branches': branch_manager.list_branches(), + 'current_branch': self.request.GET.get('branch'), + }) + return context + +class BranchListView(LoginRequiredMixin, View): + """HTMX view for branch list""" + + def get(self, request): + branch_manager = BranchManager() + branches = branch_manager.list_branches() + + content = render_to_string( + 'history_tracking/components/branch_list.html', + {'branches': branches}, + request=request + ) + return HttpResponse(content) + +class HistoryView(LoginRequiredMixin, View): + """HTMX view for change history""" + + def get(self, request): + branch_name = request.GET.get('branch') + if not branch_name: + return HttpResponse("No branch selected") + + branch = get_object_or_404(VersionBranch, name=branch_name) + tracker = ChangeTracker() + changes = tracker.get_changes(branch) + + content = render_to_string( + 'history_tracking/components/history_view.html', + {'changes': changes}, + request=request + ) + return HttpResponse(content) + +class MergeView(LoginRequiredMixin, View): + """HTMX view for merge operations""" + + def get(self, request): + source = request.GET.get('source') + target = request.GET.get('target') + + if not (source and target): + return HttpResponse("Source and target branches required") + + content = render_to_string( + 'history_tracking/components/merge_panel.html', + { + 'source': source, + 'target': target + }, + request=request + ) + return HttpResponse(content) + + @transaction.atomic + def post(self, request): + source_name = request.POST.get('source') + target_name = request.POST.get('target') + + if not (source_name and target_name): + return JsonResponse({ + 'error': 'Source and target branches required' + }, status=400) + + try: + source = get_object_or_404(VersionBranch, name=source_name) + target = get_object_or_404(VersionBranch, name=target_name) + + branch_manager = BranchManager() + success, conflicts = branch_manager.merge_branches( + source=source, + target=target, + user=request.user + ) + + if success: + content = render_to_string( + 'history_tracking/components/merge_success.html', + {'source': source, 'target': target}, + request=request + ) + return HttpResponse(content) + else: + content = render_to_string( + 'history_tracking/components/merge_conflicts.html', + { + 'source': source, + 'target': target, + 'conflicts': conflicts + }, + request=request + ) + return HttpResponse(content) + + except ValidationError as e: + return JsonResponse({'error': str(e)}, status=400) + except Exception as e: + return JsonResponse( + {'error': 'Merge failed. Please try again.'}, + status=500 + ) + +class BranchCreateView(LoginRequiredMixin, View): + """HTMX view for branch creation""" + + def get(self, request): + content = render_to_string( + 'history_tracking/components/branch_create.html', + request=request + ) + return HttpResponse(content) + + @transaction.atomic + def post(self, request): + name = request.POST.get('name') + parent_name = request.POST.get('parent') + + if not name: + return JsonResponse({'error': 'Branch name required'}, status=400) + + try: + branch_manager = BranchManager() + parent = None + if parent_name: + parent = get_object_or_404(VersionBranch, name=parent_name) + + branch = branch_manager.create_branch( + name=name, + parent=parent, + user=request.user + ) + + content = render_to_string( + 'history_tracking/components/branch_item.html', + {'branch': branch}, + request=request + ) + return HttpResponse(content) + + except ValidationError as e: + return JsonResponse({'error': str(e)}, status=400) + except Exception as e: + return JsonResponse( + {'error': 'Branch creation failed. Please try again.'}, + status=500 + ) + +class TagCreateView(LoginRequiredMixin, View): + """HTMX view for version tagging""" + + def get(self, request): + branch_name = request.GET.get('branch') + if not branch_name: + return HttpResponse("Branch required") + + content = render_to_string( + 'history_tracking/components/tag_create.html', + {'branch_name': branch_name}, + request=request + ) + return HttpResponse(content) + + @transaction.atomic + def post(self, request): + name = request.POST.get('name') + branch_name = request.POST.get('branch') + + if not (name and branch_name): + return JsonResponse( + {'error': 'Tag name and branch required'}, + status=400 + ) + + try: + branch = get_object_or_404(VersionBranch, name=branch_name) + + # Get latest historical record for the branch + latest_change = ChangeSet.objects.filter( + branch=branch, + status='applied' + ).latest('created_at') + + if not latest_change: + return JsonResponse( + {'error': 'No changes to tag'}, + status=400 + ) + + tag = VersionTag.objects.create( + name=name, + branch=branch, + historical_record=latest_change.historical_records.latest('history_date'), + created_by=request.user, + metadata={ + 'tagged_at': timezone.now().isoformat(), + 'changeset': latest_change.pk + } + ) + + content = render_to_string( + 'history_tracking/components/tag_item.html', + {'tag': tag}, + request=request + ) + return HttpResponse(content) + + except ValidationError as e: + return JsonResponse({'error': str(e)}, status=400) + except Exception as e: + return JsonResponse( + {'error': 'Tag creation failed. Please try again.'}, + status=500 + ) diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index c1321a30..94e6e0e3 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -1,146 +1,63 @@ -# Active Context +# Active Development Context -## Current Project State +## Recently Completed +- Implemented Version Control System enhancement + - Core models and database schema + - Business logic layer with managers + - HTMX-based frontend integration + - API endpoints and URL configuration + - Signal handlers for automatic tracking + - Documentation updated in `memory-bank/features/version-control/` -### Active Components -- Django backend with core apps - - accounts - - analytics - - companies - - core - - designers - - email_service - - history_tracking - - location - - media - - moderation - - parks - - reviews - - rides +## Current Status +The Version Control System has been fully implemented according to the implementation plan and technical guide. The system provides: +- Branch management +- Change tracking +- Version tagging +- Merge operations with conflict resolution +- Real-time UI updates via HTMX -### Implementation Status -1. Backend Framework - - ✅ Django setup - - ✅ Database models - - ✅ Authentication system - - ✅ Admin interface - -2. Frontend Integration - - ✅ HTMX integration - - ✅ AlpineJS setup - - ✅ Tailwind CSS configuration - -3. Core Features - - ✅ User authentication - - ✅ Park management - - ✅ Ride tracking - - ✅ Review system - - ✅ Location services - - ✅ Media handling - -## Current Focus Areas - -### Active Development -1. Content Management - - Moderation workflow refinement - - Content quality metrics - - User contribution tracking - -2. User Experience - - Frontend performance optimization - - UI/UX improvements - - Responsive design enhancements - -3. System Reliability - - Error handling improvements - - Testing coverage - - Performance monitoring - -## Immediate Next Steps - -### Technical Tasks +## Next Steps 1. Testing - - [ ] Increase test coverage - - [ ] Implement integration tests - - [ ] Add performance tests + - Create comprehensive test suite + - Test branch operations + - Test merge scenarios + - Test conflict resolution -2. Documentation - - [ ] Complete API documentation - - [ ] Update setup guides - - [ ] Document common workflows +2. Monitoring + - Implement performance metrics + - Track merge success rates + - Monitor system health -3. Performance - - [ ] Optimize database queries - - [ ] Implement caching strategy - - [ ] Improve asset loading +3. Documentation + - Create user guide + - Document API endpoints + - Add inline code documentation -### Feature Development -1. Content Quality - - [ ] Enhanced moderation tools - - [ ] Automated content checks - - [ ] Media optimization +4. Future Enhancements + - Branch locking mechanism + - Advanced merge strategies + - Custom diff viewers + - Performance optimizations -2. User Features - - [ ] Profile enhancements - - [ ] Contribution tracking - - [ ] Notification system +## Active Issues +None at present, awaiting testing phase to identify any issues. -## Known Issues +## Recent Decisions +- Used GenericForeignKey for flexible history tracking +- Implemented HTMX for real-time updates +- Structured change tracking with atomic changesets +- Integrated with django-simple-history -### Backend -1. Performance - - Query optimization needed for large datasets - - Caching implementation incomplete +## Technical Debt +- Need comprehensive test suite +- Performance monitoring to be implemented +- Documentation needs to be expanded -2. Technical Debt - - Some views need refactoring - - Test coverage gaps - - Documentation updates needed +## Current Branch +main -### Frontend -1. UI/UX - - Mobile responsiveness improvements - - Loading state refinements - - Error feedback enhancements - -2. Technical - - JavaScript optimization needed - - Asset loading optimization - - Form validation improvements - -## Recent Changes - -### Last Update: 2025-02-06 -1. Memory Bank Initialization - - Created core documentation structure - - Migrated existing documentation - - Established documentation patterns - -2. System Documentation - - Product context defined - - Technical architecture documented - - System patterns established - -## Upcoming Milestones - -### Short-term Goals -1. Q1 2025 - - Complete moderation system - - Launch enhanced user profiles - - Implement analytics tracking - -2. Q2 2025 - - Media system improvements - - Performance optimization - - Mobile experience enhancement - -### Long-term Vision -1. Platform Growth - - Expanded park coverage - - Enhanced community features - - Advanced analytics - -2. Technical Evolution - - Architecture scalability - - Feature extensibility - - Performance optimization \ No newline at end of file +## Environment +- Django with HTMX integration +- PostgreSQL database +- django-simple-history for base tracking \ No newline at end of file diff --git a/memory-bank/features/moderation/implementation.md b/memory-bank/features/moderation/implementation.md index 9140300b..c1247df9 100644 --- a/memory-bank/features/moderation/implementation.md +++ b/memory-bank/features/moderation/implementation.md @@ -57,6 +57,16 @@ - Added filter state management - Enhanced URL handling +5. `templates/moderation/partials/location_map.html` and `location_widget.html` + - Added Leaflet maps integration + - Enhanced location selection + - Improved geocoding + +6. `templates/moderation/partials/coaster_fields.html` + - Added detailed coaster stats form + - Enhanced validation + - Improved field organization + ## Testing Notes ### Tested Scenarios @@ -66,6 +76,9 @@ - Loading states and error handling - Filter functionality - Form submissions and validation +- Location selection and mapping +- Dark mode transitions +- Toast notifications ### Browser Support - Chrome 90+ @@ -73,6 +86,17 @@ - Safari 14+ - Edge 90+ +## Dependencies +- HTMX +- AlpineJS +- TailwindCSS +- Leaflet (for maps) + +## Known Issues +- Filter reset might not clear all states +- Mobile scroll performance with many items +- Loading skeleton flicker on fast connections + ## Next Steps ### 1. Performance Optimization @@ -101,15 +125,4 @@ - Update user guide with new features - Add keyboard shortcut documentation - Update accessibility guidelines -- Add performance benchmarks - -## Known Issues -- Filter reset might not clear all states -- Mobile scroll performance with many items -- Loading skeleton flicker on fast connections - -## Dependencies -- HTMX -- AlpineJS -- TailwindCSS -- Leaflet (for maps) \ No newline at end of file +- Add performance benchmarks \ No newline at end of file diff --git a/memory-bank/features/version-control/implementation-plan.md b/memory-bank/features/version-control/implementation-plan.md new file mode 100644 index 00000000..2d7f9b23 --- /dev/null +++ b/memory-bank/features/version-control/implementation-plan.md @@ -0,0 +1,292 @@ +# Version Control System Enhancement Plan + +## Current Implementation +The project currently uses django-simple-history with custom extensions: +- `HistoricalModel` base class for history tracking +- `HistoricalChangeMixin` for change tracking and diff computation +- `HistoricalSlug` for slug history management + +## Enhanced Version Control Standards + +### 1. Core VCS Features + +#### Branching System +```python +class VersionBranch: + name = models.CharField(max_length=255) + parent = models.ForeignKey('self', null=True) + created_at = models.DateTimeField(auto_now_add=True) + metadata = models.JSONField() +``` + +- Support for feature branches +- Parallel version development +- Branch merging capabilities +- Conflict resolution system + +#### Tagging System +```python +class VersionTag: + name = models.CharField(max_length=255) + version = models.ForeignKey(HistoricalRecord) + metadata = models.JSONField() +``` + +- Named versions (releases, milestones) +- Semantic versioning support +- Tag annotations and metadata + +#### Change Sets +```python +class ChangeSet: + branch = models.ForeignKey(VersionBranch) + changes = models.JSONField() # Structured changes + metadata = models.JSONField() + dependencies = models.JSONField() +``` + +- Atomic change grouping +- Dependency tracking +- Rollback capabilities + +### 2. Full Stack Integration + +#### Frontend Integration + +##### Version Control UI +```typescript +interface VersionControlUI { + // Core Components + VersionHistory: Component; + BranchView: Component; + DiffViewer: Component; + MergeResolver: Component; + + // State Management + versionStore: { + currentVersion: Version; + branches: Branch[]; + history: HistoryEntry[]; + pendingChanges: Change[]; + }; + + // Actions + actions: { + createBranch(): Promise; + mergeBranch(): Promise; + revertChanges(): Promise; + resolveConflicts(): Promise; + }; +} +``` + +##### Real-time Collaboration +```typescript +interface CollaborationSystem { + // WebSocket integration + socket: WebSocket; + + // Change tracking + pendingChanges: Map; + + // Conflict resolution + conflictResolver: ConflictResolver; +} +``` + +##### HTMX Integration +```html + +
+ + +
+
+ + +
+
+ + +
+
+
+``` + +#### Backend Integration + +##### API Layer +```python +class VersionControlViewSet(viewsets.ModelViewSet): + @action(detail=True, methods=['post']) + def create_branch(self, request): + """Create new version branch""" + + @action(detail=True, methods=['post']) + def merge_branch(self, request): + """Merge branches with conflict resolution""" + + @action(detail=True, methods=['post']) + def tag_version(self, request): + """Create version tag""" + + @action(detail=True, methods=['get']) + def changelog(self, request): + """Get structured change history""" +``` + +##### Change Tracking System +```python +class ChangeTracker: + """Track changes across the system""" + def track_change(self, instance, change_type, metadata=None): + """Record a change in the system""" + + def batch_track(self, changes): + """Track multiple changes atomically""" + + def compute_diff(self, version1, version2): + """Compute detailed difference between versions""" +``` + +### 3. Data Integrity & Validation + +#### Validation System +```python +class VersionValidator: + """Validate version control operations""" + def validate_branch_creation(self, branch_data): + """Validate branch creation request""" + + def validate_merge(self, source_branch, target_branch): + """Validate branch merge possibility""" + + def validate_revert(self, version, target_state): + """Validate revert operation""" +``` + +#### Consistency Checks +```python +class ConsistencyChecker: + """Ensure data consistency""" + def check_reference_integrity(self): + """Verify all version references are valid""" + + def verify_branch_hierarchy(self): + """Verify branch relationships""" + + def validate_change_sets(self): + """Verify change set consistency""" +``` + +### 4. Advanced Features + +#### Merge Strategies +```python +class MergeStrategy: + """Define how merges are handled""" + def auto_merge(self, source, target): + """Attempt automatic merge""" + + def resolve_conflicts(self, conflicts): + """Handle merge conflicts""" + + def apply_resolution(self, resolution): + """Apply conflict resolution""" +``` + +#### Dependency Management +```python +class DependencyTracker: + """Track version dependencies""" + def track_dependencies(self, change_set): + """Record dependencies for changes""" + + def verify_dependencies(self, version): + """Verify all dependencies are met""" + + def resolve_dependencies(self, missing_deps): + """Resolve missing dependencies""" +``` + +## Implementation Phases + +### Phase 1: Core VCS Enhancement (Weeks 1-4) +1. Implement branching system +2. Add tagging support +3. Develop change set tracking +4. Create basic frontend interface + +### Phase 2: Full Stack Integration (Weeks 5-8) +1. Build comprehensive frontend UI +2. Implement real-time collaboration +3. Develop API endpoints +4. Add WebSocket support + +### Phase 3: Advanced Features (Weeks 9-12) +1. Implement merge strategies +2. Add dependency tracking +3. Enhance conflict resolution +4. Build monitoring system + +### Phase 4: Testing & Optimization (Weeks 13-16) +1. Comprehensive testing +2. Performance optimization +3. Security hardening +4. Documentation completion + +## Success Metrics + +### Technical Metrics +- Branch operation speed (<500ms) +- Merge success rate (>95%) +- Conflict resolution time (<5min avg) +- Version retrieval speed (<200ms) + +### User Experience Metrics +- UI response time (<300ms) +- Successful merges (>90%) +- User satisfaction score (>4.5/5) +- Feature adoption rate (>80%) + +### System Health Metrics +- System uptime (>99.9%) +- Data integrity (100%) +- Backup success rate (100%) +- Recovery time (<5min) + +## Monitoring & Maintenance + +### System Monitoring +- Real-time performance tracking +- Error rate monitoring +- Resource usage tracking +- User activity monitoring + +### Maintenance Tasks +- Regular consistency checks +- Automated testing +- Performance optimization +- Security updates + +## Security Considerations + +### Access Control +- Role-based permissions +- Audit logging +- Activity monitoring +- Security scanning + +### Data Protection +- Encryption at rest +- Secure transmission +- Regular backups +- Data retention policies \ No newline at end of file diff --git a/memory-bank/features/version-control/implementation-status.md b/memory-bank/features/version-control/implementation-status.md new file mode 100644 index 00000000..e4621355 --- /dev/null +++ b/memory-bank/features/version-control/implementation-status.md @@ -0,0 +1,114 @@ +# Version Control System Implementation Status + +## Overview +The version control system has been successfully implemented according to the implementation plan and technical guide. The system provides a robust version control solution integrated with django-simple-history and enhanced with branching, merging, and real-time collaboration capabilities. + +## Implemented Components + +### 1. Core Models +```python +# Core version control models in history_tracking/models.py +- VersionBranch: Manages parallel development branches +- VersionTag: Handles version tagging and releases +- ChangeSet: Tracks atomic groups of changes +- Integration with HistoricalModel and HistoricalChangeMixin +``` + +### 2. Business Logic Layer +```python +# Managers and utilities in history_tracking/managers.py and utils.py +- BranchManager: Branch operations and management +- ChangeTracker: Change tracking and history +- MergeStrategy: Merge operations and conflict handling +- Utilities for conflict resolution and diff computation +``` + +### 3. Frontend Integration +```html +# HTMX-based components in history_tracking/templates/ +- Version Control Panel (version_control_panel.html) +- Branch Management (branch_list.html, branch_create.html) +- Change History Viewer (history_view.html) +- Merge Interface (merge_panel.html, merge_conflicts.html) +``` + +### 4. API Layer +```python +# Views and endpoints in history_tracking/views.py +- VersionControlPanel: Main VCS interface +- BranchListView: Branch management +- HistoryView: Change history display +- MergeView: Merge operations +- BranchCreateView: Branch creation +- TagCreateView: Version tagging +``` + +### 5. Signal Handlers +```python +# Signal handlers in history_tracking/signals.py +- Automatic change tracking +- Changeset management +- Branch context management +``` + +## Database Schema Changes +- Created models for branches, tags, and changesets +- Added proper indexes for performance +- Implemented GenericForeignKey relationships for flexibility +- Migrations created and applied successfully + +## URL Configuration +```python +# Added to thrillwiki/urls.py +path("vcs/", include("history_tracking.urls", namespace="history")) +``` + +## Integration Points +1. django-simple-history integration +2. HTMX for real-time updates +3. Generic relations for flexibility +4. Signal handlers for automatic tracking + +## Features Implemented +- [x] Branch creation and management +- [x] Version tagging system +- [x] Change tracking and history +- [x] Merge operations with conflict resolution +- [x] Real-time UI updates via HTMX +- [x] Generic content type support +- [x] Atomic change grouping +- [x] Branch relationship management + +## Next Steps +1. Add comprehensive test suite +2. Implement performance monitoring +3. Add user documentation +4. Consider adding advanced features like: + - Branch locking + - Advanced merge strategies + - Custom diff viewers + +## Technical Documentation +- Implementation plan: [implementation-plan.md](implementation-plan.md) +- Technical guide: [technical-guide.md](technical-guide.md) +- API documentation: To be created +- User guide: To be created + +## Performance Considerations +- Indexed key fields for efficient querying +- Optimized database schema +- Efficient change tracking +- Real-time updates without full page reloads + +## Security Measures +- Login required for all VCS operations +- Proper validation of all inputs +- CSRF protection +- Access control on branch operations + +## Monitoring +Future monitoring needs: +- Branch operation metrics +- Merge success rates +- Conflict frequency +- System performance metrics \ No newline at end of file diff --git a/memory-bank/features/version-control/technical-guide.md b/memory-bank/features/version-control/technical-guide.md new file mode 100644 index 00000000..000541d7 --- /dev/null +++ b/memory-bank/features/version-control/technical-guide.md @@ -0,0 +1,325 @@ +# Version Control System Technical Implementation Guide + +## System Overview +The version control system implements full VCS capabilities with branching, merging, and collaboration features, building upon django-simple-history while adding robust versioning capabilities across the full stack. + +## Core VCS Features + +### 1. Branching System + +```python +from vcs.models import VersionBranch, VersionTag, ChangeSet + +class BranchManager: + def create_branch(name: str, parent: Optional[VersionBranch] = None): + """Create a new branch""" + return VersionBranch.objects.create( + name=name, + parent=parent, + metadata={'created_by': current_user} + ) + + def merge_branches(source: VersionBranch, target: VersionBranch): + """Merge two branches with conflict resolution""" + merger = MergeStrategy() + return merger.merge(source, target) + + def list_branches(): + """Get all branches with their relationships""" + return VersionBranch.objects.select_related('parent').all() +``` + +### 2. Change Tracking + +```python +class ChangeTracker: + def record_change(model_instance, change_type, metadata=None): + """Record a change in the system""" + return ChangeSet.objects.create( + instance=model_instance, + change_type=change_type, + metadata=metadata or {}, + branch=get_current_branch() + ) + + def get_changes(branch: VersionBranch): + """Get all changes in a branch""" + return ChangeSet.objects.filter(branch=branch).order_by('created_at') +``` + +### 3. Frontend Integration + +#### State Management (React/TypeScript) +```typescript +interface VCSState { + currentBranch: Branch; + branches: Branch[]; + changes: Change[]; + conflicts: Conflict[]; +} + +class VCSStore { + private state: VCSState; + + async switchBranch(branchName: string): Promise { + // Implementation + } + + async createBranch(name: string): Promise { + // Implementation + } + + async mergeBranch(source: string, target: string): Promise { + // Implementation + } +} +``` + +#### UI Components +```typescript +// Branch Selector Component +const BranchSelector: React.FC = () => { + const branches = useVCSStore(state => state.branches); + + return ( +
+ {branches.map(branch => ( + + ))} +
+ ); +}; + +// Change History Component +const ChangeHistory: React.FC = () => { + const changes = useVCSStore(state => state.changes); + + return ( +
+ {changes.map(change => ( + + ))} +
+ ); +}; +``` + +### 4. API Integration + +#### Django REST Framework ViewSets +```python +class VCSViewSet(viewsets.ModelViewSet): + @action(detail=True, methods=['post']) + def create_branch(self, request): + name = request.data.get('name') + parent = request.data.get('parent') + + branch = BranchManager().create_branch(name, parent) + return Response(BranchSerializer(branch).data) + + @action(detail=True, methods=['post']) + def merge(self, request): + source = request.data.get('source') + target = request.data.get('target') + + try: + result = BranchManager().merge_branches(source, target) + return Response(result) + except MergeConflict as e: + return Response({'conflicts': e.conflicts}, status=409) +``` + +### 5. Conflict Resolution + +```python +class ConflictResolver: + def detect_conflicts(source: ChangeSet, target: ChangeSet) -> List[Conflict]: + """Detect conflicts between changes""" + conflicts = [] + # Implementation + return conflicts + + def resolve_conflict(conflict: Conflict, resolution: Resolution): + """Apply conflict resolution""" + with transaction.atomic(): + # Implementation +``` + +### 6. Real-time Collaboration + +```python +class CollaborationConsumer(AsyncWebsocketConsumer): + async def connect(self): + await self.channel_layer.group_add( + f"branch_{self.branch_id}", + self.channel_name + ) + + async def receive_change(self, event): + """Handle incoming changes""" + change = event['change'] + await self.process_change(change) +``` + +## Best Practices + +### 1. Branch Management +- Create feature branches for isolated development +- Use meaningful branch names +- Clean up merged branches +- Regular synchronization with main branch + +### 2. Change Management +- Atomic changes +- Clear change descriptions +- Related changes grouped in changesets +- Regular commits + +### 3. Conflict Resolution +- Early conflict detection +- Clear conflict documentation +- Structured resolution process +- Team communication + +### 4. Performance Optimization +- Efficient change tracking +- Optimized queries +- Caching strategy +- Background processing + +### 5. Security +- Access control +- Audit logging +- Data validation +- Secure transmission + +## Implementation Examples + +### 1. Creating a New Branch +```python +branch_manager = BranchManager() +feature_branch = branch_manager.create_branch( + name="feature/new-ui", + parent=main_branch +) +``` + +### 2. Recording Changes +```python +change_tracker = ChangeTracker() +change = change_tracker.record_change( + instance=model_object, + change_type="update", + metadata={"field": "title", "reason": "Improvement"} +) +``` + +### 3. Merging Branches +```python +try: + result = branch_manager.merge_branches( + source=feature_branch, + target=main_branch + ) +except MergeConflict as e: + conflicts = e.conflicts + resolution = conflict_resolver.resolve_conflicts(conflicts) + result = branch_manager.apply_resolution(resolution) +``` + +## Error Handling + +### 1. Branch Operations +```python +try: + branch = branch_manager.create_branch(name) +except BranchExistsError: + # Handle duplicate branch +except InvalidBranchNameError: + # Handle invalid name +``` + +### 2. Merge Operations +```python +try: + result = branch_manager.merge_branches(source, target) +except MergeConflictError as e: + # Handle merge conflicts +except InvalidBranchError: + # Handle invalid branch +``` + +## Monitoring + +### 1. Performance Monitoring +```python +class VCSMonitor: + def track_operation(operation_type, duration): + """Track operation performance""" + + def check_system_health(): + """Verify system health""" +``` + +### 2. Error Tracking +```python +class ErrorTracker: + def log_error(error_type, details): + """Log system errors""" + + def analyze_errors(): + """Analyze error patterns""" +``` + +## Testing + +### 1. Unit Tests +```python +class BranchTests(TestCase): + def test_branch_creation(self): + """Test branch creation""" + + def test_branch_merge(self): + """Test branch merging""" +``` + +### 2. Integration Tests +```python +class VCSIntegrationTests(TestCase): + def test_complete_workflow(self): + """Test complete VCS workflow""" + + def test_conflict_resolution(self): + """Test conflict resolution""" +``` + +## Deployment Considerations + +### 1. Database Migrations +- Create necessary tables +- Add indexes +- Handle existing data + +### 2. Cache Setup +- Configure Redis +- Set up caching strategy +- Implement cache invalidation + +### 3. Background Tasks +- Configure Celery +- Set up task queues +- Monitor task execution + +## Maintenance + +### 1. Regular Tasks +- Clean up old branches +- Optimize database +- Update indexes +- Verify backups + +### 2. Monitoring Tasks +- Check system health +- Monitor performance +- Track error rates +- Analyze usage patterns \ No newline at end of file diff --git a/static/js/moderation.js b/static/js/moderation.js new file mode 100644 index 00000000..a10d035a --- /dev/null +++ b/static/js/moderation.js @@ -0,0 +1,223 @@ +// Validation Helpers +const ValidationRules = { + date: { + validate: (value, input) => { + if (!value) return true; + const date = new Date(value); + const now = new Date(); + const min = new Date('1800-01-01'); + + if (date > now) { + return 'Date cannot be in the future'; + } + if (date < min) { + return 'Date cannot be before 1800'; + } + return true; + } + }, + numeric: { + validate: (value, input) => { + if (!value) return true; + const num = parseFloat(value); + const min = parseFloat(input.getAttribute('min') || '-Infinity'); + const max = parseFloat(input.getAttribute('max') || 'Infinity'); + + if (isNaN(num)) { + return 'Please enter a valid number'; + } + if (num < min) { + return `Value must be at least ${min}`; + } + if (num > max) { + return `Value must be no more than ${max}`; + } + return true; + } + } +}; + +// Form Validation and Error Handling +document.addEventListener('DOMContentLoaded', function() { + // Form Validation + document.querySelectorAll('form[hx-post]').forEach(form => { + // Add validation on field change + form.addEventListener('input', function(e) { + const input = e.target; + if (input.hasAttribute('data-validate')) { + validateField(input); + } + }); + + form.addEventListener('htmx:beforeRequest', function(event) { + let isValid = true; + + // Validate all fields + form.querySelectorAll('[data-validate]').forEach(input => { + if (!validateField(input)) { + isValid = false; + } + }); + + // Check required notes field + const notesField = form.querySelector('textarea[name="notes"]'); + if (notesField && !notesField.value.trim()) { + showError(notesField, 'Notes are required'); + isValid = false; + } + + if (!isValid) { + event.preventDefault(); + // Focus first invalid field + form.querySelector('.border-red-500')?.focus(); + } + }); + + // Clear error states on input + form.addEventListener('input', function(e) { + if (e.target.classList.contains('border-red-500')) { + e.target.classList.remove('border-red-500'); + } + }); + }); + + // Form State Management + document.querySelectorAll('form[hx-post]').forEach(form => { + const formId = form.getAttribute('id'); + if (!formId) return; + + // Save form state before submission + form.addEventListener('htmx:beforeRequest', function() { + const formData = new FormData(form); + const state = {}; + formData.forEach((value, key) => { + state[key] = value; + }); + sessionStorage.setItem('formState-' + formId, JSON.stringify(state)); + }); + + // Restore form state if available + const savedState = sessionStorage.getItem('formState-' + formId); + if (savedState) { + const state = JSON.parse(savedState); + Object.entries(state).forEach(([key, value]) => { + const input = form.querySelector(`[name="${key}"]`); + if (input) { + input.value = value; + } + }); + } + }); + + // Park Area Sync with Park Selection + document.querySelectorAll('[id^="park-input-"]').forEach(parkInput => { + const submissionId = parkInput.id.replace('park-input-', ''); + const areaSelect = document.querySelector(`#park-area-select-${submissionId}`); + + if (parkInput && areaSelect) { + parkInput.addEventListener('change', function() { + const parkId = this.value; + if (!parkId) { + areaSelect.innerHTML = ''; + return; + } + + htmx.ajax('GET', `/parks/${parkId}/areas/`, { + target: areaSelect, + swap: 'innerHTML' + }); + }); + } + }); + + // Improved Error Handling + document.body.addEventListener('htmx:responseError', function(evt) { + const errorToast = document.createElement('div'); + errorToast.className = 'fixed bottom-4 right-4 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 flex items-center'; + errorToast.innerHTML = ` + + ${evt.detail.error || 'An error occurred'} + + `; + document.body.appendChild(errorToast); + setTimeout(() => { + errorToast.remove(); + }, 5000); + }); + + // Accessibility Improvements + document.addEventListener('htmx:afterSettle', function(evt) { + // Focus management + const target = evt.detail.target; + const focusableElement = target.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); + if (focusableElement) { + focusableElement.focus(); + } + + // Announce state changes + if (target.hasAttribute('aria-live')) { + const announcement = target.getAttribute('aria-label') || target.textContent; + const announcer = document.getElementById('a11y-announcer') || createAnnouncer(); + announcer.textContent = announcement; + } + }); +}); + +// Helper function to create accessibility announcer +function createAnnouncer() { + const announcer = document.createElement('div'); + announcer.id = 'a11y-announcer'; + announcer.className = 'sr-only'; + announcer.setAttribute('aria-live', 'polite'); + document.body.appendChild(announcer); + return announcer; +} + +// Validation Helper Functions +function validateField(input) { + const validationType = input.getAttribute('data-validate'); + if (!validationType || !ValidationRules[validationType]) return true; + + const result = ValidationRules[validationType].validate(input.value, input); + if (result === true) { + clearError(input); + return true; + } else { + showError(input, result); + return false; + } +} + +function showError(input, message) { + const errorId = input.getAttribute('aria-describedby'); + const errorElement = document.getElementById(errorId); + + input.classList.add('border-red-500', 'error-shake'); + if (errorElement) { + errorElement.textContent = message; + errorElement.classList.remove('hidden'); + } + + // Announce error to screen readers + const announcer = document.getElementById('a11y-announcer'); + if (announcer) { + announcer.textContent = `Error: ${message}`; + } + + setTimeout(() => { + input.classList.remove('error-shake'); + }, 820); +} + +function clearError(input) { + const errorId = input.getAttribute('aria-describedby'); + const errorElement = document.getElementById(errorId); + + input.classList.remove('border-red-500'); + if (errorElement) { + errorElement.classList.add('hidden'); + errorElement.textContent = ''; + } +} \ No newline at end of file diff --git a/templates/moderation/dashboard.html b/templates/moderation/dashboard.html index 0d53581d..3222dbb3 100644 --- a/templates/moderation/dashboard.html +++ b/templates/moderation/dashboard.html @@ -146,152 +146,196 @@ {% block content %}
-
+
{% block moderation_content %} {% include "moderation/partials/dashboard_content.html" %} {% endblock %} -
+ - {% endblock %} {% block extra_js %} + -// Loading and Error State Management -const dashboard = { - content: document.getElementById('dashboard-content'), - skeleton: document.getElementById('loading-skeleton'), - errorState: document.getElementById('error-state'), - errorMessage: document.getElementById('error-message'), + + - showLoading() { - this.content.setAttribute('aria-busy', 'true'); - this.content.style.opacity = '0'; - this.errorState.classList.add('hidden'); - }, - - hideLoading() { - this.content.setAttribute('aria-busy', 'false'); - this.content.style.opacity = '1'; - }, - - showError(message) { - this.errorState.classList.remove('hidden'); - this.errorMessage.textContent = message || 'There was a problem loading the content. Please try again.'; - // Announce error to screen readers - this.errorMessage.setAttribute('role', 'alert'); + + + + +
+
{% endblock %} diff --git a/templates/moderation/partials/filters_store.html b/templates/moderation/partials/filters_store.html index 35e1be75..06122d3f 100644 --- a/templates/moderation/partials/filters_store.html +++ b/templates/moderation/partials/filters_store.html @@ -1,129 +1,69 @@ -{% comment %} -This template contains the Alpine.js store for managing filter state in the moderation dashboard -{% endcomment %} +{% load static %} - \ No newline at end of file +
+
\ No newline at end of file diff --git a/templates/moderation/partials/loading_skeleton.html b/templates/moderation/partials/loading_skeleton.html index 082ffe94..3376534d 100644 --- a/templates/moderation/partials/loading_skeleton.html +++ b/templates/moderation/partials/loading_skeleton.html @@ -1,66 +1,69 @@ {% load static %} -
- -
+
+ +
- {% for i in "1234" %} + {% for i in '1234'|make_list %}
{% endfor %}
- -
-
- {% for i in "123" %} -
-
-
+ +
+
+
+ {% for i in '123'|make_list %} +
+
+
+
+ {% endfor %} +
+
+ + +
+ {% for i in '123'|make_list %} +
+
+ +
+
+
+ {% for j in '1234'|make_list %} +
+
+
+
+ {% endfor %} +
+
+ + +
+ +
+ {% for j in '1234'|make_list %} +
+
+
+
+ {% endfor %} +
+ + +
+ {% for j in '1234'|make_list %} +
+ {% endfor %} +
+
+
{% endfor %}
- - - {% for i in "123" %} -
-
- -
-
-
-
-
- {% for i in "1234" %} -
-
-
-
- {% endfor %} -
-
- - -
- {% for i in "12" %} -
-
-
-
- {% endfor %} - -
- {% for i in "1234" %} -
-
-
-
- {% endfor %} -
-
-
-
- {% endfor %}
\ No newline at end of file diff --git a/templates/moderation/partials/submission_list.html b/templates/moderation/partials/submission_list.html index c5737242..f41928a3 100644 --- a/templates/moderation/partials/submission_list.html +++ b/templates/moderation/partials/submission_list.html @@ -6,8 +6,10 @@ {% endblock %} {% for submission in submissions %} -
@@ -224,13 +231,23 @@ {% elif field == 'opening_date' or field == 'closing_date' or field == 'status_since' %} - +
+ + +
{% else %} {% if field == 'park' %}
@@ -339,12 +356,24 @@ class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500" placeholder="General description and notable features">{{ value }} {% elif field == 'min_height_in' or field == 'max_height_in' %} - +
+ + +
+ in +
+
{% elif field == 'capacity_per_hour' %}
-
@@ -424,52 +462,93 @@ rows="3">
-
- - {% if submission.status != 'ESCALATED' or user.role in 'ADMIN,SUPERUSER' %} - - {% endif %} {% if user.role == 'MODERATOR' and submission.status != 'ESCALATED' %} - {% endif %} + + +
+
+
+
+ Processing... +
+
+
{% endif %} diff --git a/thrillwiki/urls.py b/thrillwiki/urls.py index 1dc28f9f..e3e14eb5 100644 --- a/thrillwiki/urls.py +++ b/thrillwiki/urls.py @@ -52,6 +52,8 @@ urlpatterns = [ path("user/", accounts_views.user_redirect_view, name="user_redirect"), # Moderation URLs - placed after other URLs but before static/media serving path("moderation/", include("moderation.urls", namespace="moderation")), + # Version Control System URLs + path("vcs/", include("history_tracking.urls", namespace="history")), path( "env-settings/", views.environment_and_settings_view,