From da252386cc60c8306203e677fc5291eb032f96eb Mon Sep 17 00:00:00 2001 From: pacnpal <183241239+pacnpal@users.noreply.github.com> Date: Sat, 8 Feb 2025 17:37:30 -0500 Subject: [PATCH] Revert "Add version control system functionality with branch management, history tracking, and merge operations" This reverts commit 939eaed2014daf4adb4d5839cc2ef585e4f7be01. --- comments/__init__.py | 0 comments/admin.py | 3 - comments/apps.py | 10 - comments/managers.py | 71 --- comments/migrations/__init__.py | 1 - comments/mixins.py | 17 - comments/models.py | 118 ---- comments/signals.py | 1 - comments/tests.py | 3 - comments/views.py | 3 - companies/models.py | 150 +---- .../templates/companies/company_detail.html | 137 ----- .../templates/companies/company_list.html | 136 ----- .../templates/companies/designer_detail.html | 154 ----- .../templates/companies/designer_list.html | 146 ----- .../companies/manufacturer_detail.html | 188 ------ .../companies/manufacturer_list.html | 162 ------ docs/version_control_api.md | 320 ----------- docs/version_control_user_guide.md | 184 ------ history_tracking/README.md | 241 -------- history_tracking/apps.py | 25 +- history_tracking/batch.py | 195 ------- history_tracking/caching.py | 223 -------- history_tracking/cleanup.py | 248 -------- history_tracking/comparison.py | 237 -------- history_tracking/context_processors.py | 43 -- history_tracking/custom_history.py | 61 -- history_tracking/historical_fields.py | 49 -- history_tracking/htmx_views.py | 123 ---- history_tracking/managers.py | 519 ----------------- ...ionbranch_changeset_versiontag_and_more.py | 220 ------- history_tracking/mixins.py | 63 +- history_tracking/models.py | 265 +-------- history_tracking/monitoring.py | 202 ------- history_tracking/notifications.py | 229 -------- history_tracking/signals.py | 138 ----- history_tracking/state_machine.py | 194 ------- .../history_tracking/approval_status.html | 174 ------ .../components/branch_create.html | 58 -- .../components/branch_list.html | 43 -- .../components/history_view.html | 88 --- .../components/merge_conflicts.html | 116 ---- .../components/merge_panel.html | 49 -- .../components/merge_success.html | 30 - .../includes/version_control_ui.html | 94 --- .../monitoring_dashboard.html | 172 ------ .../partials/approval_notifications.html | 51 -- .../partials/approval_status.html | 102 ---- .../partials/comment_preview.html | 6 - .../partials/comment_replies.html | 27 - .../partials/comments_list.html | 35 -- .../history_tracking/partials/reply_form.html | 33 -- .../history_tracking/version_comparison.html | 170 ------ .../version_control_panel.html | 53 -- history_tracking/tests/test_managers.py | 268 --------- history_tracking/tests/test_models.py | 173 ------ history_tracking/tests/test_views.py | 223 -------- history_tracking/urls.py | 50 -- history_tracking/utils.py | 149 ----- history_tracking/views.py | 145 +---- history_tracking/views_monitoring.py | 320 ----------- location/mixins.py | 43 -- media/admin.py | 33 +- media/mixins.py | 19 - memory-bank/activeContext.md | 196 +++++-- .../historical_model_comment_fixes.md | 33 -- .../evaluations/version_control_evaluation.md | 123 ---- .../features/moderation/implementation.md | 37 +- .../features/version-control/README.md | 177 ------ .../version-control/approval-workflow.md | 47 -- .../version-control/branch-locking.md | 50 -- .../version-control/change-comments.md | 52 -- .../version-control/implementation-plan.md | 292 ---------- .../implementation-sequence.md | 22 - .../version-control/implementation-status.md | 114 ---- .../implementation_checklist.md | 43 -- .../version-control/integration-matrix.md | 14 - .../version-control/technical-guide.md | 325 ----------- .../version-control/template_integration.md | 86 --- .../features/version-control/type_fixes.md | 90 --- .../version-control/ui_improvements.md | 110 ---- .../version-control/version-comparison.md | 47 -- .../version-control/visual-diff-viewer.md | 39 -- memory-bank/security/audit-checklist.md | 53 -- memory-bank/security/owasp-mapping.md | 12 - memory-bank/security/test-cases.md | 44 -- memory-bank/systemPatterns.md | 126 +--- memory-bank/techContext.md | 68 +-- parks/models.py | 129 +---- parks/templates/parks/park_detail.html | 200 ------- pyproject.toml | 4 +- requirements.txt | 2 - reviews/admin.py | 17 +- reviews/mixins.py | 19 - reviews/models.py | 74 +-- reviews/templates/reviews/review_detail.html | 136 ----- reviews/templates/reviews/review_list.html | 154 ----- rides/models.py | 126 +--- rides/templates/rides/ride_detail.html | 220 ------- rides/templates/rides/ride_list.html | 153 ----- static/css/approval-panel.css | 332 ----------- static/css/diff-viewer.css | 195 ------- static/css/inline-comment-panel.css | 229 -------- static/css/version-comparison.css | 353 ------------ static/css/version-control.css | 290 ---------- static/js/__tests__/version-control.test.js | 217 ------- static/js/collaboration-system.js | 203 ------- static/js/components/approval-panel.js | 234 -------- static/js/components/diff-viewer.js | 274 --------- static/js/components/inline-comment-panel.js | 285 ---------- static/js/components/version-comparison.js | 314 ---------- static/js/components/virtual-scroller.js | 190 ------- static/js/error-handling.js | 207 ------- static/js/map-init.js | 20 - static/js/moderation.js | 223 -------- static/js/version-control.js | 536 ------------------ templates/base.html | 169 ------ templates/moderation/dashboard.html | 286 ++++------ .../moderation/partials/filters_store.html | 196 ++++--- .../moderation/partials/loading_skeleton.html | 107 ++-- .../moderation/partials/submission_list.html | 167 ++---- tests/test_runner.py | 2 +- thrillwiki/settings.py | 18 +- thrillwiki/urls.py | 2 - uv.lock | 161 ------ 125 files changed, 617 insertions(+), 15830 deletions(-) delete mode 100644 comments/__init__.py delete mode 100644 comments/admin.py delete mode 100644 comments/apps.py delete mode 100644 comments/managers.py delete mode 100644 comments/migrations/__init__.py delete mode 100644 comments/mixins.py delete mode 100644 comments/models.py delete mode 100644 comments/signals.py delete mode 100644 comments/tests.py delete mode 100644 comments/views.py delete mode 100644 companies/templates/companies/company_detail.html delete mode 100644 companies/templates/companies/company_list.html delete mode 100644 companies/templates/companies/designer_detail.html delete mode 100644 companies/templates/companies/designer_list.html delete mode 100644 companies/templates/companies/manufacturer_detail.html delete mode 100644 companies/templates/companies/manufacturer_list.html delete mode 100644 docs/version_control_api.md delete mode 100644 docs/version_control_user_guide.md delete mode 100644 history_tracking/README.md delete mode 100644 history_tracking/batch.py delete mode 100644 history_tracking/caching.py delete mode 100644 history_tracking/cleanup.py delete mode 100644 history_tracking/comparison.py delete mode 100644 history_tracking/context_processors.py delete mode 100644 history_tracking/custom_history.py delete mode 100644 history_tracking/historical_fields.py delete mode 100644 history_tracking/htmx_views.py delete mode 100644 history_tracking/managers.py delete mode 100644 history_tracking/migrations/0002_versionbranch_changeset_versiontag_and_more.py delete mode 100644 history_tracking/monitoring.py delete mode 100644 history_tracking/notifications.py delete mode 100644 history_tracking/signals.py delete mode 100644 history_tracking/state_machine.py delete mode 100644 history_tracking/templates/history_tracking/approval_status.html delete mode 100644 history_tracking/templates/history_tracking/components/branch_create.html delete mode 100644 history_tracking/templates/history_tracking/components/branch_list.html delete mode 100644 history_tracking/templates/history_tracking/components/history_view.html delete mode 100644 history_tracking/templates/history_tracking/components/merge_conflicts.html delete mode 100644 history_tracking/templates/history_tracking/components/merge_panel.html delete mode 100644 history_tracking/templates/history_tracking/components/merge_success.html delete mode 100644 history_tracking/templates/history_tracking/includes/version_control_ui.html delete mode 100644 history_tracking/templates/history_tracking/monitoring_dashboard.html delete mode 100644 history_tracking/templates/history_tracking/partials/approval_notifications.html delete mode 100644 history_tracking/templates/history_tracking/partials/approval_status.html delete mode 100644 history_tracking/templates/history_tracking/partials/comment_preview.html delete mode 100644 history_tracking/templates/history_tracking/partials/comment_replies.html delete mode 100644 history_tracking/templates/history_tracking/partials/comments_list.html delete mode 100644 history_tracking/templates/history_tracking/partials/reply_form.html delete mode 100644 history_tracking/templates/history_tracking/version_comparison.html delete mode 100644 history_tracking/templates/history_tracking/version_control_panel.html delete mode 100644 history_tracking/tests/test_managers.py delete mode 100644 history_tracking/tests/test_models.py delete mode 100644 history_tracking/tests/test_views.py delete mode 100644 history_tracking/urls.py delete mode 100644 history_tracking/utils.py delete mode 100644 history_tracking/views_monitoring.py delete mode 100644 location/mixins.py delete mode 100644 media/mixins.py delete mode 100644 memory-bank/evaluations/historical_model_comment_fixes.md delete mode 100644 memory-bank/evaluations/version_control_evaluation.md delete mode 100644 memory-bank/features/version-control/README.md delete mode 100644 memory-bank/features/version-control/approval-workflow.md delete mode 100644 memory-bank/features/version-control/branch-locking.md delete mode 100644 memory-bank/features/version-control/change-comments.md delete mode 100644 memory-bank/features/version-control/implementation-plan.md delete mode 100644 memory-bank/features/version-control/implementation-sequence.md delete mode 100644 memory-bank/features/version-control/implementation-status.md delete mode 100644 memory-bank/features/version-control/implementation_checklist.md delete mode 100644 memory-bank/features/version-control/integration-matrix.md delete mode 100644 memory-bank/features/version-control/technical-guide.md delete mode 100644 memory-bank/features/version-control/template_integration.md delete mode 100644 memory-bank/features/version-control/type_fixes.md delete mode 100644 memory-bank/features/version-control/ui_improvements.md delete mode 100644 memory-bank/features/version-control/version-comparison.md delete mode 100644 memory-bank/features/version-control/visual-diff-viewer.md delete mode 100644 memory-bank/security/audit-checklist.md delete mode 100644 memory-bank/security/owasp-mapping.md delete mode 100644 memory-bank/security/test-cases.md delete mode 100644 parks/templates/parks/park_detail.html delete mode 100644 reviews/mixins.py delete mode 100644 reviews/templates/reviews/review_detail.html delete mode 100644 reviews/templates/reviews/review_list.html delete mode 100644 rides/templates/rides/ride_detail.html delete mode 100644 rides/templates/rides/ride_list.html delete mode 100644 static/css/approval-panel.css delete mode 100644 static/css/diff-viewer.css delete mode 100644 static/css/inline-comment-panel.css delete mode 100644 static/css/version-comparison.css delete mode 100644 static/css/version-control.css delete mode 100644 static/js/__tests__/version-control.test.js delete mode 100644 static/js/collaboration-system.js delete mode 100644 static/js/components/approval-panel.js delete mode 100644 static/js/components/diff-viewer.js delete mode 100644 static/js/components/inline-comment-panel.js delete mode 100644 static/js/components/version-comparison.js delete mode 100644 static/js/components/virtual-scroller.js delete mode 100644 static/js/error-handling.js delete mode 100644 static/js/map-init.js delete mode 100644 static/js/moderation.js delete mode 100644 static/js/version-control.js delete mode 100644 templates/base.html diff --git a/comments/__init__.py b/comments/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/comments/admin.py b/comments/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/comments/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/comments/apps.py b/comments/apps.py deleted file mode 100644 index 138f519f..00000000 --- a/comments/apps.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.apps import AppConfig -from django.db.models.signals import class_prepared, post_init - -class CommentsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'comments' - - def ready(self): - """Set up comment system when the app is ready.""" - pass diff --git a/comments/managers.py b/comments/managers.py deleted file mode 100644 index 14390738..00000000 --- a/comments/managers.py +++ /dev/null @@ -1,71 +0,0 @@ -from django.contrib.contenttypes.models import ContentType -from django.db import models -from django.core.exceptions import ObjectDoesNotExist - -class CommentThreadManager(models.Manager): - """Manager for handling comment threads on both regular and historical models.""" - - def for_instance(self, instance): - """Get comment threads for any model instance.""" - # Get the base model class if this is a historical instance - if instance.__class__.__name__.startswith('Historical'): - model_class = instance.instance.__class__ - instance_id = instance.instance.pk - else: - model_class = instance.__class__ - instance_id = instance.pk - - ct = ContentType.objects.get_for_model(model_class) - return self.filter(content_type=ct, object_id=instance_id) - - def create_for_instance(self, instance, **kwargs): - """Create a comment thread for any model instance.""" - # Get the base model class if this is a historical instance - if instance.__class__.__name__.startswith('Historical'): - model_class = instance.instance.__class__ - instance_id = instance.instance.pk - else: - model_class = instance.__class__ - instance_id = instance.pk - - ct = ContentType.objects.get_for_model(model_class) - return self.create(content_type=ct, object_id=instance_id, **kwargs) - -class ThreadedModelManager(models.Manager): - """Manager for models that have comment threads.""" - - """Manager for models that have comment threads.""" - - def get_comment_threads(self, instance): - """Get comment threads for this instance.""" - from comments.models import CommentThread - if not instance.pk: - return CommentThread.objects.none() - return CommentThread.objects.for_instance(instance) - - def add_comment_thread(self, instance, **kwargs): - """Create a comment thread for this instance.""" - from comments.models import CommentThread - if not instance.pk: - raise ObjectDoesNotExist("Cannot create comment thread for unsaved instance") - return CommentThread.objects.create_for_instance(instance, **kwargs) - - def with_comment_threads(self): - """Get all instances with their comment threads.""" - from comments.models import CommentThread - qs = self.get_queryset() - content_type = ContentType.objects.get_for_model(self.model) - - # Get comment threads through a subquery - threads = CommentThread.objects.filter( - content_type=content_type, - object_id=models.OuterRef('pk') - ) - return qs.annotate( - comment_count=models.Subquery( - threads.values('object_id') - .annotate(count=models.Count('id')) - .values('count'), - output_field=models.IntegerField() - ) - ) \ No newline at end of file diff --git a/comments/migrations/__init__.py b/comments/migrations/__init__.py deleted file mode 100644 index 8b137891..00000000 --- a/comments/migrations/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/comments/mixins.py b/comments/mixins.py deleted file mode 100644 index 442a8ae0..00000000 --- a/comments/mixins.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.contrib.contenttypes.fields import GenericRelation - -from .models import get_comment_threads - -class CommentableMixin: - """ - Mixin for models that should have comment functionality. - Uses composition instead of inheritance to avoid historical model issues. - """ - - @property - def comments(self): - """Get comments helper for this instance.""" - if self.__class__.__name__.startswith('Historical'): - # Historical models delegate to their current instance - return self.instance.comments - return get_comment_threads(self) \ No newline at end of file diff --git a/comments/models.py b/comments/models.py deleted file mode 100644 index 567f8f61..00000000 --- a/comments/models.py +++ /dev/null @@ -1,118 +0,0 @@ -from django.db import models -from django.conf import settings -from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType -from .managers import CommentThreadManager, ThreadedModelManager - -class CommentThread(models.Model): - """ - A thread of comments that can be attached to any model instance, - including historical versions. - """ - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - object_id = models.PositiveIntegerField() - content_object = GenericForeignKey('content_type', 'object_id') - - title = models.CharField(max_length=255, blank=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.SET_NULL, - null=True, - related_name='created_comment_threads' - ) - is_locked = models.BooleanField(default=False) - is_hidden = models.BooleanField(default=False) - - objects = CommentThreadManager() - - class Meta: - indexes = [ - models.Index(fields=['content_type', 'object_id']), - ] - ordering = ['-created_at'] - - def __str__(self): - return f"Comment Thread on {self.content_object} - {self.title}" - -class CommentThreads: - """ - Helper class to manage comment threads for a model instance. - This is used instead of direct inheritance to avoid historical model issues. - """ - def __init__(self, instance): - self.instance = instance - self._info = {} - - def get_info(self): - """Get or compute comment thread information.""" - if not self._info: - ct = ContentType.objects.get_for_model(self.instance.__class__) - self._info = { - 'count': CommentThread.objects.filter( - content_type=ct, - object_id=self.instance.pk - ).count(), - 'content_type': ct, - 'object_id': self.instance.pk - } - return self._info - - def get_threads(self): - """Get comment threads for this instance.""" - info = self.get_info() - return CommentThread.objects.filter( - content_type=info['content_type'], - object_id=info['object_id'] - ) - - def add_thread(self, title='', created_by=None): - """Create a new comment thread for this instance.""" - info = self.get_info() - thread = CommentThread.objects.create( - content_type=info['content_type'], - object_id=info['object_id'], - title=title, - created_by=created_by - ) - self._info = {} # Clear cache - return thread - -def get_comment_threads(instance): - """Get or create a CommentThreads helper for a model instance.""" - if not hasattr(instance, '_comment_threads'): - instance._comment_threads = CommentThreads(instance) - return instance._comment_threads - -class Comment(models.Model): - """Individual comment within a thread.""" - thread = models.ForeignKey( - CommentThread, - on_delete=models.CASCADE, - related_name='comments' - ) - author = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.SET_NULL, - null=True, - related_name='comments' - ) - content = models.TextField() - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - is_edited = models.BooleanField(default=False) - is_hidden = models.BooleanField(default=False) - parent = models.ForeignKey( - 'self', - on_delete=models.CASCADE, - null=True, - blank=True, - related_name='replies' - ) - - class Meta: - ordering = ['created_at'] - - def __str__(self): - return f"Comment by {self.author} on {self.created_at}" diff --git a/comments/signals.py b/comments/signals.py deleted file mode 100644 index dcd5795b..00000000 --- a/comments/signals.py +++ /dev/null @@ -1 +0,0 @@ -# This file intentionally left empty - signals have been replaced with direct mixin configuration \ No newline at end of file diff --git a/comments/tests.py b/comments/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/comments/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/comments/views.py b/comments/views.py deleted file mode 100644 index 91ea44a2..00000000 --- a/comments/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/companies/models.py b/companies/models.py index 18b34c28..240c7bf1 100644 --- a/companies/models.py +++ b/companies/models.py @@ -1,21 +1,12 @@ from django.db import models from django.utils.text import slugify from django.urls import reverse -from django.contrib.contenttypes.fields import GenericRelation -from django.contrib.contenttypes.models import ContentType from typing import Tuple, Optional, ClassVar, TYPE_CHECKING -from history_tracking.models import HistoricalModel, VersionBranch, ChangeSet -from history_tracking.signals import get_current_branch, ChangesetContextManager -from comments.mixins import CommentableMixin -from media.mixins import PhotoableModel if TYPE_CHECKING: from history_tracking.models import HistoricalSlug - -class Company(HistoricalModel, CommentableMixin, PhotoableModel): - comments = GenericRelation( - 'comments.CommentThread') # Explicit relationship +class Company(models.Model): name = models.CharField(max_length=255) slug = models.SlugField(max_length=255, unique=True) website = models.URLField(blank=True) @@ -31,7 +22,6 @@ class Company(HistoricalModel, CommentableMixin, PhotoableModel): class Meta: verbose_name_plural = 'companies' ordering = ['name'] - excluded_fields = ['comments'] # Exclude from historical tracking def __str__(self) -> str: return self.name @@ -39,47 +29,7 @@ class Company(HistoricalModel, CommentableMixin, PhotoableModel): def save(self, *args, **kwargs) -> None: if not self.slug: self.slug = slugify(self.name) - - # Get the branch from context or use default - current_branch = get_current_branch() - - if current_branch: - # Save in the context of the current branch - super().save(*args, **kwargs) - else: - # If no branch context, save in main branch - main_branch, _ = VersionBranch.objects.get_or_create( - name='main', - defaults={'metadata': {'type': 'default_branch'}} - ) - - with ChangesetContextManager(branch=main_branch): - super().save(*args, **kwargs) - - def get_version_info(self) -> dict: - """Get version control information for this company""" - content_type = ContentType.objects.get_for_model(self) - latest_changes = ChangeSet.objects.filter( - content_type=content_type, - object_id=self.pk, - status='applied' - ).order_by('-created_at')[:5] - - active_branches = VersionBranch.objects.filter( - changesets__content_type=content_type, - changesets__object_id=self.pk, - is_active=True - ).distinct() - - return { - 'latest_changes': latest_changes, - 'active_branches': active_branches, - 'current_branch': get_current_branch(), - 'total_changes': latest_changes.count() - } - - def get_absolute_url(self) -> str: - return reverse("companies:company_detail", kwargs={"slug": self.slug}) + super().save(*args, **kwargs) @classmethod def get_by_slug(cls, slug: str) -> Tuple['Company', bool]: @@ -98,10 +48,7 @@ class Company(HistoricalModel, CommentableMixin, PhotoableModel): except (HistoricalSlug.DoesNotExist, cls.DoesNotExist): raise cls.DoesNotExist() - -class Manufacturer(HistoricalModel, CommentableMixin, PhotoableModel): - comments = GenericRelation( - 'comments.CommentThread') # Explicit relationship +class Manufacturer(models.Model): name = models.CharField(max_length=255) slug = models.SlugField(max_length=255, unique=True) website = models.URLField(blank=True) @@ -116,8 +63,6 @@ class Manufacturer(HistoricalModel, CommentableMixin, PhotoableModel): class Meta: ordering = ['name'] - excluded_fields = ['comments'] # Exclude from historical tracking - history_exclude = ['comments'] # Exclude from historical models def __str__(self) -> str: return self.name @@ -125,47 +70,7 @@ class Manufacturer(HistoricalModel, CommentableMixin, PhotoableModel): def save(self, *args, **kwargs) -> None: if not self.slug: self.slug = slugify(self.name) - - # Get the branch from context or use default - current_branch = get_current_branch() - - if current_branch: - # Save in the context of the current branch - super().save(*args, **kwargs) - else: - # If no branch context, save in main branch - main_branch, _ = VersionBranch.objects.get_or_create( - name='main', - defaults={'metadata': {'type': 'default_branch'}} - ) - - with ChangesetContextManager(branch=main_branch): - super().save(*args, **kwargs) - - def get_version_info(self) -> dict: - """Get version control information for this manufacturer""" - content_type = ContentType.objects.get_for_model(self) - latest_changes = ChangeSet.objects.filter( - content_type=content_type, - object_id=self.pk, - status='applied' - ).order_by('-created_at')[:5] - - active_branches = VersionBranch.objects.filter( - changesets__content_type=content_type, - changesets__object_id=self.pk, - is_active=True - ).distinct() - - return { - 'latest_changes': latest_changes, - 'active_branches': active_branches, - 'current_branch': get_current_branch(), - 'total_changes': latest_changes.count() - } - - def get_absolute_url(self) -> str: - return reverse("companies:manufacturer_detail", kwargs={"slug": self.slug}) + super().save(*args, **kwargs) @classmethod def get_by_slug(cls, slug: str) -> Tuple['Manufacturer', bool]: @@ -184,10 +89,7 @@ class Manufacturer(HistoricalModel, CommentableMixin, PhotoableModel): except (HistoricalSlug.DoesNotExist, cls.DoesNotExist): raise cls.DoesNotExist() - -class Designer(HistoricalModel, CommentableMixin, PhotoableModel): - comments = GenericRelation( - 'comments.CommentThread') # Explicit relationship +class Designer(models.Model): name = models.CharField(max_length=255) slug = models.SlugField(max_length=255, unique=True) website = models.URLField(blank=True) @@ -208,47 +110,7 @@ class Designer(HistoricalModel, CommentableMixin, PhotoableModel): def save(self, *args, **kwargs) -> None: if not self.slug: self.slug = slugify(self.name) - - # Get the branch from context or use default - current_branch = get_current_branch() - - if current_branch: - # Save in the context of the current branch - super().save(*args, **kwargs) - else: - # If no branch context, save in main branch - main_branch, _ = VersionBranch.objects.get_or_create( - name='main', - defaults={'metadata': {'type': 'default_branch'}} - ) - - with ChangesetContextManager(branch=main_branch): - super().save(*args, **kwargs) - - def get_version_info(self) -> dict: - """Get version control information for this designer""" - content_type = ContentType.objects.get_for_model(self) - latest_changes = ChangeSet.objects.filter( - content_type=content_type, - object_id=self.pk, - status='applied' - ).order_by('-created_at')[:5] - - active_branches = VersionBranch.objects.filter( - changesets__content_type=content_type, - changesets__object_id=self.pk, - is_active=True - ).distinct() - - return { - 'latest_changes': latest_changes, - 'active_branches': active_branches, - 'current_branch': get_current_branch(), - 'total_changes': latest_changes.count() - } - - def get_absolute_url(self) -> str: - return reverse("companies:designer_detail", kwargs={"slug": self.slug}) + super().save(*args, **kwargs) @classmethod def get_by_slug(cls, slug: str) -> Tuple['Designer', bool]: diff --git a/companies/templates/companies/company_detail.html b/companies/templates/companies/company_detail.html deleted file mode 100644 index 7c15e36f..00000000 --- a/companies/templates/companies/company_detail.html +++ /dev/null @@ -1,137 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}{{ company.name }} - ThrillWiki{% endblock %} - -{% block content %} -
-
- -
- - {% include "history_tracking/includes/version_control_ui.html" %} - - -
-

{{ company.name }}

- - {% if company.description %} -
- {{ company.description|linebreaks }} -
- {% endif %} - - -
- {% if company.headquarters %} -
-

Headquarters

-

{{ company.headquarters }}

-
- {% endif %} - - {% if company.website %} -
-

Website

-

- - {{ company.website }} - -

-
- {% endif %} -
-
- - - {% if company.parks.exists %} -
-

Theme Parks

-
- {% for park in company.parks.all %} -
-

- - {{ park.name }} - -

-

{{ park.get_status_display }}

- {% if park.formatted_location %} -

{{ park.formatted_location }}

- {% endif %} - - - {% with version_info=park.get_version_info %} - {% if version_info.active_branches.count > 1 %} -
- - {{ version_info.active_branches.count }} active branches - -
- {% endif %} - {% endwith %} -
- {% endfor %} -
-
- {% endif %} -
- - -
- -
-

Statistics

-
-
- Total Parks: - {{ company.total_parks }} -
-
- Total Rides: - {{ company.total_rides }} -
-
-
- - - {% with version_info=company.get_version_info %} -
-

Version Control

-
-
- Active Branches: - {{ version_info.active_branches.count }} -
-
- Total Changes: - {{ version_info.total_changes }} -
- {% if version_info.latest_changes %} -
- Recent Changes: -
    - {% for change in version_info.latest_changes|slice:":3" %} -
  • {{ change.description }}
  • - {% endfor %} -
-
- {% endif %} -
-
- {% endwith %} - - -
-

Details

-
-

Created: {{ company.created_at|date:"F j, Y" }}

- {% if company.created_at != company.updated_at %} -

Last updated: {{ company.updated_at|date:"F j, Y" }}

- {% endif %} -
-
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/companies/templates/companies/company_list.html b/companies/templates/companies/company_list.html deleted file mode 100644 index 6a85eacc..00000000 --- a/companies/templates/companies/company_list.html +++ /dev/null @@ -1,136 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}Companies - ThrillWiki{% endblock %} - -{% block content %} -
- - {% include "history_tracking/includes/version_control_ui.html" %} - -
-

Companies

-

Theme park owners and operators

-
- - -
-
-
- - -
-
- - -
-
- -
-
-
- - - {% if companies %} -
- {% for company in companies %} -
-
-

- - {{ company.name }} - -

- - {% if company.description %} -

{{ company.description|truncatewords:30 }}

- {% endif %} - -
- {% if company.headquarters %} -
- Headquarters -

{{ company.headquarters }}

-
- {% endif %} - - {% if company.website %} -
- Website -

- - Visit Site - -

-
- {% endif %} - -
- Total Parks -

{{ company.total_parks }}

-
- -
- Total Rides -

{{ company.total_rides }}

-
-
- - - {% with version_info=company.get_version_info %} - {% if version_info.active_branches.count > 1 %} -
- - {{ version_info.active_branches.count }} active branches - -
- {% endif %} - {% endwith %} -
-
- {% endfor %} -
- - - {% if is_paginated %} -
- -
- {% endif %} - - {% else %} -
-

No companies found matching your criteria.

-
- {% endif %} -
-{% endblock %} \ No newline at end of file diff --git a/companies/templates/companies/designer_detail.html b/companies/templates/companies/designer_detail.html deleted file mode 100644 index e78858fd..00000000 --- a/companies/templates/companies/designer_detail.html +++ /dev/null @@ -1,154 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}{{ designer.name }} - ThrillWiki{% endblock %} - -{% block content %} -
-
- -
- - {% include "history_tracking/includes/version_control_ui.html" %} - - -
-

{{ designer.name }}

- - {% if designer.description %} -
- {{ designer.description|linebreaks }} -
- {% endif %} - - - {% if designer.website %} - - {% endif %} -
- - - {% if designer.rides.exists %} -
-

Designed Rides

-
- {% for ride in designer.rides.all %} -
-

- - {{ ride.name }} - -

-

- at - - {{ ride.park.name }} - -

- - {% if ride.manufacturer %} -

- Built by - - {{ ride.manufacturer.name }} - -

- {% endif %} - -
- - {{ ride.get_status_display }} - -
- - - {% with version_info=ride.get_version_info %} - {% if version_info.active_branches.count > 1 %} -
- - {{ version_info.active_branches.count }} active branches - -
- {% endif %} - {% endwith %} -
- {% endfor %} -
-
- {% endif %} -
- - -
- -
-

Statistics

-
-
- Total Rides: - {{ designer.total_rides }} -
-
- Roller Coasters: - {{ designer.total_roller_coasters }} -
-
-
- - - {% with version_info=designer.get_version_info %} -
-

Version Control

-
-
- Active Branches: - {{ version_info.active_branches.count }} -
-
- Total Changes: - {{ version_info.total_changes }} -
- {% if version_info.latest_changes %} -
- Recent Changes: -
    - {% for change in version_info.latest_changes|slice:":3" %} -
  • {{ change.description }}
  • - {% endfor %} -
-
- {% endif %} -
-
- {% endwith %} - - -
-

Details

-
-

Created: {{ designer.created_at|date:"F j, Y" }}

- {% if designer.created_at != designer.updated_at %} -

Last updated: {{ designer.updated_at|date:"F j, Y" }}

- {% endif %} -
-
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/companies/templates/companies/designer_list.html b/companies/templates/companies/designer_list.html deleted file mode 100644 index 165ab917..00000000 --- a/companies/templates/companies/designer_list.html +++ /dev/null @@ -1,146 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}Designers - ThrillWiki{% endblock %} - -{% block content %} -
- - {% include "history_tracking/includes/version_control_ui.html" %} - -
-

Ride Designers

-

Ride and attraction designers and engineers

-
- - -
-
-
- - -
-
- - -
-
- -
-
-
- - - {% if designers %} -
- {% for designer in designers %} -
-
-

- - {{ designer.name }} - -

- - {% if designer.description %} -

{{ designer.description|truncatewords:30 }}

- {% endif %} - -
- {% if designer.website %} -
- Website -

- - Visit Site - -

-
- {% endif %} - -
- Total Rides -

{{ designer.total_rides }}

-
- -
- Roller Coasters -

{{ designer.total_roller_coasters }}

-
-
- - - {% if designer.rides.exists %} -
- Notable Works: -
- {% for ride in designer.rides.all|slice:":3" %} - - {{ ride.name }} - - {% endfor %} - {% if designer.rides.count > 3 %} - +{{ designer.rides.count|add:"-3" }} more - {% endif %} -
-
- {% endif %} - - - {% with version_info=designer.get_version_info %} - {% if version_info.active_branches.count > 1 %} -
- - {{ version_info.active_branches.count }} active branches - -
- {% endif %} - {% endwith %} -
-
- {% endfor %} -
- - - {% if is_paginated %} -
- -
- {% endif %} - - {% else %} -
-

No designers found matching your criteria.

-
- {% endif %} -
-{% endblock %} \ No newline at end of file diff --git a/companies/templates/companies/manufacturer_detail.html b/companies/templates/companies/manufacturer_detail.html deleted file mode 100644 index 8c1e9282..00000000 --- a/companies/templates/companies/manufacturer_detail.html +++ /dev/null @@ -1,188 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}{{ manufacturer.name }} - ThrillWiki{% endblock %} - -{% block content %} -
-
- -
- - {% include "history_tracking/includes/version_control_ui.html" %} - - -
-

{{ manufacturer.name }}

- - {% if manufacturer.description %} -
- {{ manufacturer.description|linebreaks }} -
- {% endif %} - - -
- {% if manufacturer.headquarters %} -
-

Headquarters

-

{{ manufacturer.headquarters }}

-
- {% endif %} - - {% if manufacturer.website %} - - {% endif %} -
-
- - - {% if manufacturer.ride_models.exists %} -
-

Ride Models

-
- {% for model in manufacturer.ride_models.all %} -
-

- - {{ model.name }} - -

- {% if model.category %} -

{{ model.get_category_display }}

- {% endif %} - {% if model.description %} -

{{ model.description|truncatewords:50 }}

- {% endif %} - - - {% with version_info=model.get_version_info %} - {% if version_info.active_branches.count > 1 %} -
- - {{ version_info.active_branches.count }} active branches - -
- {% endif %} - {% endwith %} -
- {% endfor %} -
-
- {% endif %} - - - {% if manufacturer.rides.exists %} -
-

Installed Rides

-
- {% for ride in manufacturer.rides.all %} -
-

- - {{ ride.name }} - -

-

- at - - {{ ride.park.name }} - -

-
- - {{ ride.get_status_display }} - -
- - - {% with version_info=ride.get_version_info %} - {% if version_info.active_branches.count > 1 %} -
- - {{ version_info.active_branches.count }} active branches - -
- {% endif %} - {% endwith %} -
- {% endfor %} -
-
- {% endif %} -
- - -
- -
-

Statistics

-
-
- Total Rides: - {{ manufacturer.total_rides }} -
-
- Roller Coasters: - {{ manufacturer.total_roller_coasters }} -
-
-
- - - {% with version_info=manufacturer.get_version_info %} -
-

Version Control

-
-
- Active Branches: - {{ version_info.active_branches.count }} -
-
- Total Changes: - {{ version_info.total_changes }} -
- {% if version_info.latest_changes %} -
- Recent Changes: -
    - {% for change in version_info.latest_changes|slice:":3" %} -
  • {{ change.description }}
  • - {% endfor %} -
-
- {% endif %} -
-
- {% endwith %} - - -
-

Details

-
-

Created: {{ manufacturer.created_at|date:"F j, Y" }}

- {% if manufacturer.created_at != manufacturer.updated_at %} -

Last updated: {{ manufacturer.updated_at|date:"F j, Y" }}

- {% endif %} -
-
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/companies/templates/companies/manufacturer_list.html b/companies/templates/companies/manufacturer_list.html deleted file mode 100644 index f68bad8b..00000000 --- a/companies/templates/companies/manufacturer_list.html +++ /dev/null @@ -1,162 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}Manufacturers - ThrillWiki{% endblock %} - -{% block content %} -
- - {% include "history_tracking/includes/version_control_ui.html" %} - -
-

Manufacturers

-

Ride and attraction manufacturers

-
- - -
-
-
- - -
-
- - -
-
- - -
-
- -
-
-
- - - {% if manufacturers %} -
- {% for manufacturer in manufacturers %} -
-
-

- - {{ manufacturer.name }} - -

- - {% if manufacturer.description %} -

{{ manufacturer.description|truncatewords:30 }}

- {% endif %} - -
- {% if manufacturer.headquarters %} -
- Headquarters -

{{ manufacturer.headquarters }}

-
- {% endif %} - - {% if manufacturer.website %} -
- Website -

- - Visit Site - -

-
- {% endif %} - -
- Total Rides -

{{ manufacturer.total_rides }}

-
- -
- Roller Coasters -

{{ manufacturer.total_roller_coasters }}

-
-
- - - {% if manufacturer.ride_models.exists %} -
- Popular Models: -
- {% for model in manufacturer.ride_models.all|slice:":3" %} - - {{ model.name }} - - {% endfor %} - {% if manufacturer.ride_models.count > 3 %} - +{{ manufacturer.ride_models.count|add:"-3" }} more - {% endif %} -
-
- {% endif %} - - - {% with version_info=manufacturer.get_version_info %} - {% if version_info.active_branches.count > 1 %} -
- - {{ version_info.active_branches.count }} active branches - -
- {% endif %} - {% endwith %} -
-
- {% endfor %} -
- - - {% if is_paginated %} -
- -
- {% endif %} - - {% else %} -
-

No manufacturers found matching your criteria.

-
- {% endif %} -
-{% endblock %} \ No newline at end of file diff --git a/docs/version_control_api.md b/docs/version_control_api.md deleted file mode 100644 index 9d755ab8..00000000 --- a/docs/version_control_api.md +++ /dev/null @@ -1,320 +0,0 @@ -# Version Control System API Documentation - -## Overview -The version control system provides a comprehensive API for managing content versioning, branching, and merging across different models in the system. - -## Core Models - -### VersionBranch -Represents a branch in the version control system. - -```python -class VersionBranch: - name: str # Branch name (unique) - metadata: JSONField # Branch metadata - is_active: bool # Branch status - created_at: datetime - updated_at: datetime -``` - -### ChangeSet -Represents a set of changes to a versioned object. - -```python -class ChangeSet: - branch: ForeignKey # Reference to VersionBranch - content_type: ForeignKey # ContentType of the changed object - object_id: int # ID of the changed object - data: JSONField # Change data - status: str # Status (pending, applied, conflict) - created_at: datetime - applied_at: datetime -``` - -## API Endpoints - -### Branch Management - -#### Create Branch -```http -POST /api/v1/version-control/branches/ -``` - -Request body: -```json -{ - "name": "feature/new-branch", - "metadata": { - "type": "feature", - "description": "New feature branch" - } -} -``` - -Response: -```json -{ - "id": 1, - "name": "feature/new-branch", - "metadata": { - "type": "feature", - "description": "New feature branch" - }, - "is_active": true, - "created_at": "2025-02-07T09:00:00Z" -} -``` - -#### Switch Branch -```http -POST /api/v1/version-control/branches/{branch_id}/switch/ -``` - -Response: -```json -{ - "status": "success", - "branch": { - "id": 1, - "name": "feature/new-branch" - } -} -``` - -### Change Management - -#### Create Change -```http -POST /api/v1/version-control/changes/ -``` - -Request body: -```json -{ - "branch_id": 1, - "content_type": "parks.park", - "object_id": 123, - "data": { - "name": "Updated Name", - "description": "Updated description" - } -} -``` - -Response: -```json -{ - "id": 1, - "branch": 1, - "status": "pending", - "created_at": "2025-02-07T09:05:00Z" -} -``` - -#### Apply Change -```http -POST /api/v1/version-control/changes/{change_id}/apply/ -``` - -Response: -```json -{ - "status": "success", - "change": { - "id": 1, - "status": "applied", - "applied_at": "2025-02-07T09:06:00Z" - } -} -``` - -### Merge Operations - -#### Merge Branch -```http -POST /api/v1/version-control/branches/{source_id}/merge/ -``` - -Request body: -```json -{ - "target_branch_id": 2 -} -``` - -Response: -```json -{ - "status": "success", - "conflicts": [] -} -``` - -#### Resolve Conflicts -```http -POST /api/v1/version-control/merge/resolve/ -``` - -Request body: -```json -{ - "merge_id": 1, - "resolutions": [ - { - "field": "name", - "value": "Resolved Name" - } - ] -} -``` - -Response: -```json -{ - "status": "success", - "merge": { - "id": 1, - "status": "completed" - } -} -``` - -## Model Integration - -### Adding Version Control to Models - -To make a model version-controlled, inherit from `HistoricalModel`: - -```python -from history_tracking.models import HistoricalModel - -class YourModel(HistoricalModel): - name = models.CharField(max_length=255) - - def save(self, *args, **kwargs): - # Get the branch from context - current_branch = get_current_branch() - - if current_branch: - # Save in branch context - super().save(*args, **kwargs) - else: - # Save in main branch - with ChangesetContextManager(branch=main_branch): - super().save(*args, **kwargs) -``` - -### Version Control Methods - -Each versioned model has access to these methods: - -#### get_version_info() -Returns version control information for the object: -```python -info = model.get_version_info() -# Returns: -{ - 'latest_changes': [ChangeSet], - 'active_branches': [VersionBranch], - 'current_branch': VersionBranch, - 'total_changes': int -} -``` - -#### get_changes() -Returns all changes for the object: -```python -changes = model.get_changes() -# Returns QuerySet of ChangeSet objects -``` - -## JavaScript Integration - -### Version Control UI - -Initialize version control UI: -```javascript -import { initVersionControl } from 'version-control.js'; - -initVersionControl({ - container: '#version-control-panel', - onChange: (branch) => { - // Handle branch change - } -}); -``` - -### Branch Operations - -Switch branches: -```javascript -import { switchBranch } from 'version-control.js'; - -switchBranch(branchId).then(response => { - if (response.status === 'success') { - // Handle successful branch switch - } -}); -``` - -### Merge Operations - -Handle merge conflicts: -```javascript -import { handleMergeConflicts } from 'version-control.js'; - -handleMergeConflicts(conflicts).then(resolutions => { - // Handle conflict resolutions -}); -``` - -## Error Handling - -The API uses standard HTTP status codes: - -- 200: Success -- 400: Bad Request -- 401: Unauthorized -- 403: Forbidden -- 404: Not Found -- 409: Conflict (merge conflicts) -- 500: Internal Server Error - -Error responses include detailed information: -```json -{ - "status": "error", - "message": "Detailed error message", - "code": "ERROR_CODE", - "details": { - // Additional error details - } -} -``` - -## Rate Limiting - -API endpoints are rate-limited: -- Authenticated users: 100 requests per minute -- Anonymous users: 20 requests per minute - -Rate limit headers are included in responses: -```http -X-RateLimit-Limit: 100 -X-RateLimit-Remaining: 95 -X-RateLimit-Reset: 1623456789 -``` - -## Monitoring - -Monitor version control operations through the monitoring dashboard: -```http -GET /version-control/monitoring/ -``` - -The dashboard provides real-time metrics for: -- Branch operations -- Merge success rates -- Change tracking overhead -- Error rates -- System health \ No newline at end of file diff --git a/docs/version_control_user_guide.md b/docs/version_control_user_guide.md deleted file mode 100644 index f67dd7d7..00000000 --- a/docs/version_control_user_guide.md +++ /dev/null @@ -1,184 +0,0 @@ -# Version Control User Guide - -## Introduction -The version control system allows you to track changes, create branches, and merge content updates across ThrillWiki. This guide explains how to use the version control features effectively. - -## Basic Concepts - -### Branches -A branch is a separate line of development that allows you to make changes without affecting the main content. Think of it like a draft version of your content. - -- **Main Branch**: The default branch containing the live, published content -- **Feature Branches**: Temporary branches for developing new content or making significant changes -- **Active Branch**: The branch you're currently working on - -### Changes -Changes represent modifications to content: -- Adding new information -- Updating existing content -- Removing outdated content - -### Merging -Merging combines changes from one branch into another, typically from a feature branch back into the main branch. - -## Using Version Control - -### 1. Version Control Panel -The version control panel appears at the top of editable pages and shows: -- Current branch -- Branch selector -- Action buttons (Create Branch, Merge, etc.) - -![Version Control Panel](../static/images/docs/version-control-panel.png) - -### 2. Creating a Branch -1. Click "Create Branch" in the version control panel -2. Enter a branch name (e.g., "update-park-details") -3. Add an optional description -4. Click "Create" - -Branch naming conventions: -- Use lowercase letters -- Separate words with hyphens -- Be descriptive (e.g., "add-new-rides", "update-park-history") - -### 3. Switching Branches -1. Open the branch selector in the version control panel -2. Select the desired branch -3. Click "Switch Branch" - -Note: You'll see a warning if you have unsaved changes. - -### 4. Making Changes -1. Ensure you're on the correct branch -2. Edit content normally -3. Save changes -4. Changes are tracked automatically - -The version control panel shows: -- Number of changes -- Last update time -- Change status - -### 5. Viewing History -1. Click "History" in the version control panel -2. See a list of changes with: - - Timestamp - - Author - - Description - - Branch -3. Click any change to see details - -### 6. Merging Changes -1. Switch to the target branch (usually main) -2. Click "Merge" in the version control panel -3. Select the source branch -4. Review changes -5. Click "Merge Changes" - -### 7. Handling Conflicts -If conflicts occur during merging: -1. The conflict resolution dialog appears -2. Review conflicting changes -3. Choose which version to keep or combine them -4. Click "Resolve Conflicts" -5. Complete the merge - -## Best Practices - -### When to Create a Branch -Create a new branch when: -- Making substantial content updates -- Adding new sections -- Reorganizing information -- Testing new features - -### Branch Management -- Keep branches focused on specific tasks -- Delete branches after merging -- Regular merge changes from main to stay current -- Use descriptive branch names - -### Change Management -- Make atomic, related changes -- Write clear change descriptions -- Review changes before merging -- Test content in preview mode - -### Collaboration -- Communicate branch purpose to team members -- Coordinate on shared branches -- Review changes before merging -- Resolve conflicts together when needed - -## Common Tasks - -### Updating a Park Page -1. Create a branch (e.g., "update-park-info") -2. Make changes to park information -3. Preview changes -4. Merge back to main when ready - -### Adding New Rides -1. Create a branch (e.g., "add-new-rides-2025") -2. Add ride information -3. Add photos and details -4. Review and merge - -### Content Reorganization -1. Create a branch (e.g., "reorganize-sections") -2. Rearrange content -3. Update navigation -4. Test thoroughly -5. Merge changes - -## Troubleshooting - -### Common Issues - -#### Unable to Create Branch -- Check permissions -- Verify branch name is valid -- Ensure no conflicts with existing branches - -#### Merge Conflicts -1. Don't panic! Conflicts are normal -2. Review both versions carefully -3. Choose the best content -4. Test after resolving - -#### Lost Changes -1. Check branch history -2. Review recent changes -3. Contact administrator if needed - -### Getting Help -- Click the "Help" button in the version control panel -- Contact administrators for complex issues -- Check documentation for guidance - -## Version Control Status Icons - -| Icon | Meaning | -|------|---------| -| 🟢 | Current branch | -| 🔄 | Pending changes | -| ⚠️ | Merge conflicts | -| ✅ | Successfully merged | -| 🔒 | Protected branch | - -## Keyboard Shortcuts - -| Action | Shortcut | -|--------|----------| -| Switch Branch | Ctrl/Cmd + B | -| Create Branch | Ctrl/Cmd + Shift + B | -| View History | Ctrl/Cmd + H | -| Merge Branch | Ctrl/Cmd + M | -| Save Changes | Ctrl/Cmd + S | - -## Additional Resources -- [API Documentation](version_control_api.md) -- [Technical Documentation](technical_architecture.md) -- [Video Tutorials](https://wiki.thrillwiki.com/tutorials) -- [Community Forums](https://community.thrillwiki.com) \ No newline at end of file diff --git a/history_tracking/README.md b/history_tracking/README.md deleted file mode 100644 index 165e19d0..00000000 --- a/history_tracking/README.md +++ /dev/null @@ -1,241 +0,0 @@ -# Version Control System - -## Overview -A comprehensive version control system for Django models that provides branching, merging, and change tracking capabilities with optimized performance through batch processing and caching. - -## Requirements - -### System Requirements -- Python 3.8+ -- Django 4.0+ -- Redis 6.0+ (for caching) -- PostgreSQL 12+ (recommended for database) - -### Python Dependencies -``` -django-simple-history>=3.0.0 -redis>=4.0.0 -``` - -## Installation - -1. Add 'history_tracking' to your INSTALLED_APPS: -```python -INSTALLED_APPS = [ - ... - 'history_tracking', -] -``` - -2. Configure Redis connection in settings.py: -```python -# Uses existing Redis configuration if available -CACHES = { - "default": { - "BACKEND": "django.core.cache.backends.redis.RedisCache", - "LOCATION": "redis://127.0.0.1:6379/1", # Adjust as needed - } -} - -# Version control specific settings -VERSION_CONTROL = { - 'CACHE_PREFIX': 'vc_', # Prefix for cache keys - 'BATCH_SIZE': 100, # Default batch size for operations - 'MAX_WORKERS': 4, # Maximum parallel workers - 'CACHE_DURATIONS': { # Cache durations in seconds - 'BRANCH': 3600, # 1 hour - 'CHANGE': 1800, # 30 minutes - 'HISTORY': 86400, # 24 hours - } -} -``` - -3. Run migrations: -```bash -python manage.py migrate history_tracking -``` - -## Usage - -### Making Models Version-Controlled - -1. Inherit from HistoricalModel: -```python -from history_tracking.models import HistoricalModel - -class YourModel(HistoricalModel): - name = models.CharField(max_length=255) - description = models.TextField() -``` - -2. The model will automatically track: - - All field changes - - Who made changes - - When changes were made - - Which branch changes were made in - -### Working with Branches - -```python -from history_tracking.models import VersionBranch - -# Create a new branch -branch = VersionBranch.objects.create( - name="feature/new-content", - metadata={"type": "feature"} -) - -# Make changes in branch context -from history_tracking.context_processors import branch_context - -with branch_context(branch): - your_model.save() # Changes are tracked in the branch -``` - -### Batch Operations - -For handling multiple changes efficiently: - -```python -from history_tracking.batch import BatchOperation - -# Create batch operation -batch = BatchOperation(max_workers=4) - -# Add changes to batch -for item in items: - batch.add_change(item, {'field': 'new_value'}) - -# Process changes (parallel or sequential) -results = batch.commit(parallel=True) -``` - -### Using the Queue System - -For large-scale operations: - -```python -from history_tracking.batch import VersionControlQueue - -# Create queue with custom batch size -queue = VersionControlQueue(batch_size=100) - -# Queue changes -for item in large_dataset: - queue.queue_change(item, {'field': 'new_value'}) - -# Process queue -results = queue.process_queue(parallel=True) -``` - -## Cache Management - -The system automatically caches: -- Branch information -- Change details -- Version history - -Cache invalidation is handled automatically, but you can manually invalidate: - -```python -from history_tracking.caching import VersionHistoryCache - -# Invalidate specific caches -VersionHistoryCache.invalidate_branch(branch_id) -VersionHistoryCache.invalidate_history(content_type_id, object_id) - -# Invalidate all version control caches -VersionHistoryCache.invalidate_all() -``` - -## Monitoring - -The system includes built-in monitoring: - -```python -from history_tracking.monitoring import VersionControlMetrics - -# Collect system metrics -VersionControlMetrics.collect_system_metrics() -VersionControlMetrics.collect_performance_metrics() -``` - -Metrics are logged and can be viewed: -- In application logs -- Through the Django admin interface -- Via monitoring endpoints (if configured) - -## Performance Considerations - -The system is optimized for: -- Batch processing of changes -- Efficient caching of frequently accessed data -- Parallel processing capabilities -- Minimal database queries - -For large-scale operations: -- Use batch processing -- Enable parallel processing when appropriate -- Configure cache durations based on your needs -- Monitor performance metrics - -## Security - -The system integrates with Django's authentication and permissions: -- All changes are tracked with user information -- Branch access can be controlled -- Merge operations can require approval - -## Templates - -The system includes template tags for displaying version control information: - -```html -{% load version_control_tags %} - -{% version_status object %} -{% branch_selector %} -{% history_list object %} -``` - -## API Endpoints - -Documentation for API endpoints can be found in `docs/version_control_api.md`. - -## Database Considerations - -The system uses your existing Django database configuration and creates these main tables: -- history_tracking_versionbranch -- history_tracking_changeset -- history_tracking_versiontag -- history_tracking_commentthread - -Plus historical tables for each tracked model. - -## Troubleshooting - -Common issues and solutions: - -1. Performance Issues - - Check batch sizes - - Verify cache configuration - - Monitor database queries - - Review parallel processing settings - -2. Cache Issues - - Verify Redis connection - - Check cache key conflicts - - Monitor cache hit rates - -3. Database Issues - - Check indexing - - Monitor query performance - - Review database connection pool settings - -## Contributing - -Contributions are welcome! Please read our contributing guidelines and submit pull requests. - -## License - -This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/history_tracking/apps.py b/history_tracking/apps.py index 0aba4b72..f7f856e2 100644 --- a/history_tracking/apps.py +++ b/history_tracking/apps.py @@ -1,9 +1,26 @@ +# 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): - """Register signals when the app is ready""" - from . import signals # Import signals to register them + 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 diff --git a/history_tracking/batch.py b/history_tracking/batch.py deleted file mode 100644 index 0d05a961..00000000 --- a/history_tracking/batch.py +++ /dev/null @@ -1,195 +0,0 @@ -from django.db import transaction -from django.contrib.contenttypes.models import ContentType -from django.utils import timezone -from typing import List, Dict, Any, Optional -from concurrent.futures import ThreadPoolExecutor -import logging - -from .models import VersionBranch, ChangeSet -from .caching import VersionHistoryCache -from .signals import get_current_branch - -logger = logging.getLogger('version_control') - -class BatchOperation: - """ - Handles batch operations for version control system. - Provides efficient handling of multiple changes and updates. - """ - - def __init__(self, max_workers: int = 4): - self.max_workers = max_workers - self.changes: List[Dict[str, Any]] = [] - self.error_handler = self.default_error_handler - - def default_error_handler(self, error: Exception, item: Dict[str, Any]) -> None: - """Default error handling for batch operations""" - logger.error(f"Batch operation error: {error}, item: {item}") - raise error - - def set_error_handler(self, handler) -> None: - """Set custom error handler for batch operations""" - self.error_handler = handler - - def add_change(self, obj: Any, data: Dict[str, Any], branch: Optional[VersionBranch] = None) -> None: - """Add a change to the batch""" - content_type = ContentType.objects.get_for_model(obj) - self.changes.append({ - 'content_type': content_type, - 'object_id': obj.pk, - 'data': data, - 'branch': branch or get_current_branch() - }) - - @transaction.atomic - def process_change(self, change: Dict[str, Any]) -> ChangeSet: - """Process a single change in the batch""" - try: - changeset = ChangeSet.objects.create( - branch=change['branch'], - content_type=change['content_type'], - object_id=change['object_id'], - data=change['data'], - status='pending' - ) - - # Apply the change - changeset.apply() - - # Cache the result - VersionHistoryCache.cache_change(changeset.to_dict()) - - return changeset - except Exception as e: - self.error_handler(e, change) - raise - - def process_parallel(self) -> List[ChangeSet]: - """Process changes in parallel using thread pool""" - results = [] - with ThreadPoolExecutor(max_workers=self.max_workers) as executor: - future_to_change = { - executor.submit(self.process_change, change): change - for change in self.changes - } - - for future in future_to_change: - try: - changeset = future.result() - results.append(changeset) - except Exception as e: - change = future_to_change[future] - self.error_handler(e, change) - - return results - - @transaction.atomic - def process_sequential(self) -> List[ChangeSet]: - """Process changes sequentially in a single transaction""" - results = [] - for change in self.changes: - try: - changeset = self.process_change(change) - results.append(changeset) - except Exception as e: - self.error_handler(e, change) - - return results - - def commit(self, parallel: bool = False) -> List[ChangeSet]: - """Commit all changes in the batch""" - if not self.changes: - return [] - - start_time = timezone.now() - logger.info(f"Starting batch operation with {len(self.changes)} changes") - - try: - results = self.process_parallel() if parallel else self.process_sequential() - - duration = (timezone.now() - start_time).total_seconds() - logger.info( - f"Batch operation completed: {len(results)} changes processed in {duration:.2f}s" - ) - - return results - finally: - self.changes = [] # Clear the batch - -class BulkVersionControl: - """ - Handles bulk version control operations for collections of objects. - """ - - def __init__(self, model_class, branch: Optional[VersionBranch] = None): - self.model_class = model_class - self.branch = branch or get_current_branch() - self.content_type = ContentType.objects.get_for_model(model_class) - self.batch = BatchOperation() - - def prepare_bulk_update(self, objects: List[Any], data: Dict[str, Any]) -> None: - """Prepare bulk update for multiple objects""" - for obj in objects: - self.batch.add_change(obj, data, self.branch) - - def prepare_bulk_delete(self, objects: List[Any]) -> None: - """Prepare bulk delete for multiple objects""" - for obj in objects: - self.batch.add_change(obj, {'action': 'delete'}, self.branch) - - def prepare_bulk_create(self, data_list: List[Dict[str, Any]]) -> None: - """Prepare bulk create for multiple objects""" - for data in data_list: - # Create temporary object for content type - temp_obj = self.model_class() - self.batch.add_change(temp_obj, {'action': 'create', **data}, self.branch) - - def commit(self, parallel: bool = True) -> List[ChangeSet]: - """Commit all prepared bulk operations""" - return self.batch.commit(parallel=parallel) - -class VersionControlQueue: - """ - Queue system for handling version control operations. - Allows for delayed processing and batching of changes. - """ - - def __init__(self, batch_size: int = 100, auto_commit: bool = True): - self.batch_size = batch_size - self.auto_commit = auto_commit - self.current_batch = BatchOperation() - self._queued_count = 0 - - def queue_change(self, obj: Any, data: Dict[str, Any], branch: Optional[VersionBranch] = None) -> None: - """Queue a change for processing""" - self.current_batch.add_change(obj, data, branch) - self._queued_count += 1 - - if self.auto_commit and self._queued_count >= self.batch_size: - self.process_queue() - - def process_queue(self, parallel: bool = True) -> List[ChangeSet]: - """Process all queued changes""" - if not self._queued_count: - return [] - - results = self.current_batch.commit(parallel=parallel) - self._queued_count = 0 - return results - -def batch_version_control(func): - """ - Decorator for batching version control operations within a function. - """ - def wrapper(*args, **kwargs): - batch = BatchOperation() - try: - with transaction.atomic(): - result = func(*args, batch=batch, **kwargs) - if batch.changes: - batch.commit() - return result - except Exception as e: - logger.error(f"Batch operation failed: {e}") - raise - return wrapper \ No newline at end of file diff --git a/history_tracking/caching.py b/history_tracking/caching.py deleted file mode 100644 index c622810c..00000000 --- a/history_tracking/caching.py +++ /dev/null @@ -1,223 +0,0 @@ -from django.core.cache import cache -from django.conf import settings -from django.contrib.contenttypes.models import ContentType -from typing import Optional, List, Dict, Any -import hashlib -import json -import logging - -logger = logging.getLogger('version_control') - -class VersionHistoryCache: - """ - Caching system for version control history data. - Implements a multi-level caching strategy with memory and persistent storage. - """ - - # Cache key prefixes - BRANCH_PREFIX = 'vc_branch_' - CHANGE_PREFIX = 'vc_change_' - HISTORY_PREFIX = 'vc_history_' - - # Cache durations (in seconds) - BRANCH_CACHE_DURATION = 3600 # 1 hour - CHANGE_CACHE_DURATION = 1800 # 30 minutes - HISTORY_CACHE_DURATION = 3600 * 24 # 24 hours - - @classmethod - def get_branch_key(cls, branch_id: int) -> str: - """Generate cache key for branch data""" - return f"{cls.BRANCH_PREFIX}{branch_id}" - - @classmethod - def get_change_key(cls, change_id: int) -> str: - """Generate cache key for change data""" - return f"{cls.CHANGE_PREFIX}{change_id}" - - @classmethod - def get_history_key(cls, content_type_id: int, object_id: int) -> str: - """Generate cache key for object history""" - return f"{cls.HISTORY_PREFIX}{content_type_id}_{object_id}" - - @classmethod - def generate_cache_version(cls, data: Dict[str, Any]) -> str: - """Generate version hash for cache invalidation""" - data_str = json.dumps(data, sort_keys=True) - return hashlib.md5(data_str.encode()).hexdigest() - - @classmethod - def cache_branch(cls, branch_data: Dict[str, Any]) -> None: - """Cache branch data with versioning""" - key = cls.get_branch_key(branch_data['id']) - version = cls.generate_cache_version(branch_data) - - cache_data = { - 'data': branch_data, - 'version': version, - 'timestamp': settings.VERSION_CONTROL_TIMESTAMP - } - - try: - cache.set(key, cache_data, cls.BRANCH_CACHE_DURATION) - logger.debug(f"Cached branch data: {key}") - except Exception as e: - logger.error(f"Error caching branch data: {e}") - - @classmethod - def get_cached_branch(cls, branch_id: int) -> Optional[Dict[str, Any]]: - """Retrieve cached branch data if valid""" - key = cls.get_branch_key(branch_id) - cache_data = cache.get(key) - - if cache_data: - # Validate cache version and timestamp - if ( - cache_data.get('timestamp') == settings.VERSION_CONTROL_TIMESTAMP and - cls.generate_cache_version(cache_data['data']) == cache_data['version'] - ): - logger.debug(f"Cache hit for branch: {key}") - return cache_data['data'] - - # Invalid cache, delete it - cache.delete(key) - logger.debug(f"Invalidated branch cache: {key}") - - return None - - @classmethod - def cache_change(cls, change_data: Dict[str, Any]) -> None: - """Cache change data""" - key = cls.get_change_key(change_data['id']) - version = cls.generate_cache_version(change_data) - - cache_data = { - 'data': change_data, - 'version': version, - 'timestamp': settings.VERSION_CONTROL_TIMESTAMP - } - - try: - cache.set(key, cache_data, cls.CHANGE_CACHE_DURATION) - logger.debug(f"Cached change data: {key}") - except Exception as e: - logger.error(f"Error caching change data: {e}") - - @classmethod - def get_cached_change(cls, change_id: int) -> Optional[Dict[str, Any]]: - """Retrieve cached change data if valid""" - key = cls.get_change_key(change_id) - cache_data = cache.get(key) - - if cache_data: - if ( - cache_data.get('timestamp') == settings.VERSION_CONTROL_TIMESTAMP and - cls.generate_cache_version(cache_data['data']) == cache_data['version'] - ): - logger.debug(f"Cache hit for change: {key}") - return cache_data['data'] - - cache.delete(key) - logger.debug(f"Invalidated change cache: {key}") - - return None - - @classmethod - def cache_history(cls, content_type_id: int, object_id: int, history_data: List[Dict[str, Any]]) -> None: - """Cache version history for an object""" - key = cls.get_history_key(content_type_id, object_id) - version = cls.generate_cache_version({'history': history_data}) - - cache_data = { - 'data': history_data, - 'version': version, - 'timestamp': settings.VERSION_CONTROL_TIMESTAMP - } - - try: - cache.set(key, cache_data, cls.HISTORY_CACHE_DURATION) - logger.debug(f"Cached history data: {key}") - except Exception as e: - logger.error(f"Error caching history data: {e}") - - @classmethod - def get_cached_history(cls, content_type_id: int, object_id: int) -> Optional[List[Dict[str, Any]]]: - """Retrieve cached history data if valid""" - key = cls.get_history_key(content_type_id, object_id) - cache_data = cache.get(key) - - if cache_data: - if ( - cache_data.get('timestamp') == settings.VERSION_CONTROL_TIMESTAMP and - cls.generate_cache_version({'history': cache_data['data']}) == cache_data['version'] - ): - logger.debug(f"Cache hit for history: {key}") - return cache_data['data'] - - cache.delete(key) - logger.debug(f"Invalidated history cache: {key}") - - return None - - @classmethod - def invalidate_branch(cls, branch_id: int) -> None: - """Invalidate branch cache""" - key = cls.get_branch_key(branch_id) - cache.delete(key) - logger.debug(f"Manually invalidated branch cache: {key}") - - @classmethod - def invalidate_change(cls, change_id: int) -> None: - """Invalidate change cache""" - key = cls.get_change_key(change_id) - cache.delete(key) - logger.debug(f"Manually invalidated change cache: {key}") - - @classmethod - def invalidate_history(cls, content_type_id: int, object_id: int) -> None: - """Invalidate history cache""" - key = cls.get_history_key(content_type_id, object_id) - cache.delete(key) - logger.debug(f"Manually invalidated history cache: {key}") - - @classmethod - def invalidate_all(cls) -> None: - """Invalidate all version control caches""" - try: - # Get all keys with our prefixes - keys = [] - for prefix in [cls.BRANCH_PREFIX, cls.CHANGE_PREFIX, cls.HISTORY_PREFIX]: - keys.extend(cache.keys(f"{prefix}*")) - - # Delete all matching keys - cache.delete_many(keys) - logger.info(f"Invalidated {len(keys)} version control cache entries") - except Exception as e: - logger.error(f"Error invalidating all caches: {e}") - -class CacheableVersionMixin: - """Mixin to add caching capabilities to version control models""" - - def cache_data(self) -> None: - """Cache the object's data""" - if hasattr(self, 'to_dict'): - data = self.to_dict() - - if hasattr(self, 'branch_id'): - VersionHistoryCache.cache_branch(data) - elif hasattr(self, 'change_id'): - VersionHistoryCache.cache_change(data) - - def invalidate_cache(self) -> None: - """Invalidate the object's cache""" - if hasattr(self, 'branch_id'): - VersionHistoryCache.invalidate_branch(self.branch_id) - elif hasattr(self, 'change_id'): - VersionHistoryCache.invalidate_change(self.change_id) - - def invalidate_related_caches(self) -> None: - """Invalidate related object caches""" - if hasattr(self, 'content_type_id') and hasattr(self, 'object_id'): - VersionHistoryCache.invalidate_history( - self.content_type_id, - self.object_id - ) \ No newline at end of file diff --git a/history_tracking/cleanup.py b/history_tracking/cleanup.py deleted file mode 100644 index eede0182..00000000 --- a/history_tracking/cleanup.py +++ /dev/null @@ -1,248 +0,0 @@ -from django.db import transaction -from django.utils import timezone -from django.conf import settings -from django.contrib.contenttypes.models import ContentType -from typing import List, Dict, Any, Optional -from datetime import timedelta -import logging -import json -import os - -from .models import VersionBranch, ChangeSet -from .caching import VersionHistoryCache - -logger = logging.getLogger('version_control') - - -class VersionCleanup: - """ - Manages cleanup of old version control data through archival and deletion. - """ - - def __init__(self): - self.archive_path = getattr( - settings, - 'VERSION_CONTROL_ARCHIVE_PATH', - 'version_archives' - ) - self.retention_days = getattr( - settings, - 'VERSION_CONTROL_RETENTION_DAYS', - 90 - ) - self.merged_retention_days = getattr( - settings, - 'VERSION_CONTROL_MERGED_RETENTION_DAYS', - 30 - ) - self.ensure_archive_directory() - - def ensure_archive_directory(self) -> None: - """Ensure archive directory exists""" - if not os.path.exists(self.archive_path): - os.makedirs(self.archive_path) - - def get_archive_filename(self, date: timezone.datetime) -> str: - """Generate archive filename for a given date""" - return os.path.join( - self.archive_path, - f'version_archive_{date.strftime("%Y%m%d_%H%M%S")}.json' - ) - - @transaction.atomic - def archive_old_changes(self, batch_size: int = 1000) -> int: - """Archive and clean up old changes""" - cutoff_date = timezone.now() - timedelta(days=self.retention_days) - - # Get changes to archive - old_changes = ChangeSet.objects.filter( - created_at__lt=cutoff_date, - archived=False - )[:batch_size] - - if not old_changes: - return 0 - - # Prepare archive data - archive_data = { - 'timestamp': timezone.now().isoformat(), - 'changes': [ - { - 'id': change.id, - 'branch': change.branch_id, - 'content_type': change.content_type_id, - 'object_id': change.object_id, - 'data': change.data, - 'status': change.status, - 'created_at': change.created_at.isoformat(), - 'applied_at': change.applied_at.isoformat() if change.applied_at else None - } - for change in old_changes - ] - } - - # Write to archive file - archive_file = self.get_archive_filename(timezone.now()) - with open(archive_file, 'w') as f: - json.dump(archive_data, f, indent=2) - - # Mark changes as archived - change_ids = [change.id for change in old_changes] - ChangeSet.objects.filter(id__in=change_ids).update(archived=True) - - logger.info(f"Archived {len(change_ids)} changes to {archive_file}") - return len(change_ids) - - @transaction.atomic - def cleanup_merged_branches(self) -> int: - """Clean up old merged branches""" - cutoff_date = timezone.now() - timedelta(days=self.merged_retention_days) - - # Find merged branches to clean up - merged_branches = VersionBranch.objects.filter( - is_merged=True, - merged_at__lt=cutoff_date, - is_protected=False - ) - - count = 0 - for branch in merged_branches: - try: - # Archive branch changes - self.archive_branch_changes(branch) - - # Delete branch - branch.delete() - count += 1 - - logger.info(f"Cleaned up merged branch: {branch.name}") - except Exception as e: - logger.error(f"Error cleaning up branch {branch.name}: {e}") - - return count - - def archive_branch_changes(self, branch: VersionBranch) -> None: - """Archive all changes for a specific branch""" - changes = ChangeSet.objects.filter( - branch=branch, - archived=False - ) - - if not changes: - return - - archive_data = { - 'timestamp': timezone.now().isoformat(), - 'branch': { - 'id': branch.id, - 'name': branch.name, - 'metadata': branch.metadata, - 'created_at': branch.created_at.isoformat(), - 'merged_at': branch.merged_at.isoformat() if branch.merged_at else None - }, - 'changes': [ - { - 'id': change.id, - 'content_type': change.content_type_id, - 'object_id': change.object_id, - 'data': change.data, - 'status': change.status, - 'created_at': change.created_at.isoformat(), - 'applied_at': change.applied_at.isoformat() if change.applied_at else None - } - for change in changes - ] - } - - # Write to archive file - archive_file = self.get_archive_filename(timezone.now()) - with open(archive_file, 'w') as f: - json.dump(archive_data, f, indent=2) - - # Mark changes as archived - changes.update(archived=True) - - @transaction.atomic - def cleanup_inactive_branches(self, days: int = 60) -> int: - """Clean up inactive branches""" - cutoff_date = timezone.now() - timedelta(days=days) - - # Find inactive branches - inactive_branches = VersionBranch.objects.filter( - is_active=True, - is_protected=False, - updated_at__lt=cutoff_date - ) - - count = 0 - for branch in inactive_branches: - try: - # Archive branch changes - self.archive_branch_changes(branch) - - # Deactivate branch - branch.is_active = False - branch.save() - count += 1 - - logger.info(f"Deactivated inactive branch: {branch.name}") - except Exception as e: - logger.error(f"Error deactivating branch {branch.name}: {e}") - - return count - - def cleanup_orphaned_changes(self) -> int: - """Clean up changes without valid content objects""" - count = 0 - for change in ChangeSet.objects.filter(archived=False): - try: - # Try to get the related object - obj = change.content_type.get_object_for_this_type( - pk=change.object_id) - if obj is None: - self.archive_change(change) - count += 1 - except Exception: - # If object doesn't exist, archive the change - self.archive_change(change) - count += 1 - - logger.info(f"Cleaned up {count} orphaned changes") - return count - - def archive_change(self, change: ChangeSet) -> None: - """Archive a single change""" - archive_data = { - 'timestamp': timezone.now().isoformat(), - 'changes': [{ - 'id': change.id, - 'branch': change.branch_id, - 'content_type': change.content_type_id, - 'object_id': change.object_id, - 'data': change.data, - 'status': change.status, - 'created_at': change.created_at.isoformat(), - 'applied_at': change.applied_at.isoformat() if change.applied_at else None - }] - } - - # Write to archive file - archive_file = self.get_archive_filename(timezone.now()) - with open(archive_file, 'w') as f: - json.dump(archive_data, f, indent=2) - - # Mark change as archived - change.archived = True - change.save() - - def run_maintenance(self) -> Dict[str, int]: - """Run all cleanup operations""" - results = { - 'archived_changes': self.archive_old_changes(), - 'cleaned_branches': self.cleanup_merged_branches(), - 'deactivated_branches': self.cleanup_inactive_branches(), - 'cleaned_orphans': self.cleanup_orphaned_changes() - } - - logger.info("Version control maintenance completed", extra=results) - return results diff --git a/history_tracking/comparison.py b/history_tracking/comparison.py deleted file mode 100644 index c0f09cf1..00000000 --- a/history_tracking/comparison.py +++ /dev/null @@ -1,237 +0,0 @@ -import asyncio -import json -from typing import List, Dict, Any, Optional -from django.core.cache import cache -from django.db import models -from django.utils import timezone -from concurrent.futures import ThreadPoolExecutor -from .models import VersionTag, ChangeSet - -class StructuredDiff: - def __init__(self, version1: str, version2: str): - self.version1 = version1 - self.version2 = version2 - self.changes: List[Dict[str, Any]] = [] - self.impact_score = 0.0 - self.computation_time = 0.0 - self.timestamp = timezone.now() - - def to_dict(self) -> Dict[str, Any]: - return { - 'version1': self.version1, - 'version2': self.version2, - 'changes': self.changes, - 'impact_score': self.impact_score, - 'computation_time': self.computation_time, - 'timestamp': self.timestamp.isoformat() - } - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> 'StructuredDiff': - diff = cls(data['version1'], data['version2']) - diff.changes = data['changes'] - diff.impact_score = data['impact_score'] - diff.computation_time = data['computation_time'] - diff.timestamp = timezone.datetime.fromisoformat(data['timestamp']) - return diff - -class ComparisonEngine: - """Handles version comparison operations with background processing and caching""" - - def __init__(self, chunk_size: int = 10485760): # 10MB default chunk size - self.chunk_size = chunk_size - self.executor = ThreadPoolExecutor(max_workers=4) - self.cache_ttl = 300 # 5 minutes cache TTL - - async def compare_versions(self, versions: List[str]) -> List[StructuredDiff]: - """ - Compare multiple versions, processing in background and using cache - Args: - versions: List of version identifiers to compare - Returns: - List of StructuredDiff objects with comparison results - """ - if len(versions) < 2: - raise ValueError("At least two versions required for comparison") - - results: List[StructuredDiff] = [] - cache_misses = [] - - # Check cache first - for i in range(len(versions) - 1): - for j in range(i + 1, len(versions)): - cache_key = self._get_cache_key(versions[i], versions[j]) - cached_result = cache.get(cache_key) - - if cached_result: - results.append(StructuredDiff.from_dict(json.loads(cached_result))) - else: - cache_misses.append((versions[i], versions[j])) - - # Process cache misses in background - if cache_misses: - comparison_tasks = [ - self._compare_version_pair(v1, v2) - for v1, v2 in cache_misses - ] - new_results = await asyncio.gather(*comparison_tasks) - results.extend(new_results) - - return sorted( - results, - key=lambda x: (x.version1, x.version2) - ) - - def calculate_impact_score(self, diffs: List[StructuredDiff]) -> float: - """ - Calculate impact score for a set of diffs - Args: - diffs: List of StructuredDiff objects - Returns: - Float impact score (0-1) - """ - if not diffs: - return 0.0 - - total_score = 0.0 - weights = { - 'file_count': 0.3, - 'change_size': 0.3, - 'structural_impact': 0.4 - } - - for diff in diffs: - # File count impact - file_count = len(set(c['file'] for c in diff.changes)) - file_score = min(file_count / 100, 1.0) # Normalize to max 100 files - - # Change size impact - total_changes = sum( - len(c.get('additions', [])) + len(c.get('deletions', [])) - for c in diff.changes - ) - size_score = min(total_changes / 1000, 1.0) # Normalize to max 1000 lines - - # Structural impact (e.g., function/class changes) - structural_changes = sum( - 1 for c in diff.changes - if c.get('type') in ['function', 'class', 'schema'] - ) - structural_score = min(structural_changes / 10, 1.0) # Normalize to max 10 structural changes - - # Weighted average - diff.impact_score = ( - weights['file_count'] * file_score + - weights['change_size'] * size_score + - weights['structural_impact'] * structural_score - ) - total_score += diff.impact_score - - return total_score / len(diffs) - - async def _compare_version_pair(self, version1: str, version2: str) -> StructuredDiff: - """Compare two versions in background""" - start_time = timezone.now() - - # Create diff structure - diff = StructuredDiff(version1, version2) - - try: - # Get version data - v1_tag = await self._get_version_tag(version1) - v2_tag = await self._get_version_tag(version2) - - if not v1_tag or not v2_tag: - raise ValueError("Version tag not found") - - # Process in chunks if needed - changes = await self._process_version_changes(v1_tag, v2_tag) - diff.changes = changes - - # Calculate impact score - diff.impact_score = self.calculate_impact_score([diff]) - - # Store in cache - cache_key = self._get_cache_key(version1, version2) - cache.set( - cache_key, - json.dumps(diff.to_dict()), - self.cache_ttl - ) - - except Exception as e: - diff.changes = [{'error': str(e)}] - diff.impact_score = 0.0 - - diff.computation_time = (timezone.now() - start_time).total_seconds() - return diff - - async def _get_version_tag(self, version: str) -> Optional[VersionTag]: - """Get version tag by identifier""" - loop = asyncio.get_event_loop() - return await loop.run_in_executor( - self.executor, - lambda: VersionTag.objects.filter(name=version).first() - ) - - async def _process_version_changes( - self, - v1_tag: VersionTag, - v2_tag: VersionTag - ) -> List[Dict[str, Any]]: - """Process changes between versions in chunks""" - changes = [] - - # Get changesets between versions - changesets = await self._get_changesets_between(v1_tag, v2_tag) - - for changeset in changesets: - # Process each change in chunks if needed - change_data = await self._process_changeset(changeset) - changes.extend(change_data) - - return changes - - async def _get_changesets_between( - self, - v1_tag: VersionTag, - v2_tag: VersionTag - ) -> List[ChangeSet]: - """Get all changesets between two versions""" - loop = asyncio.get_event_loop() - return await loop.run_in_executor( - self.executor, - lambda: list(ChangeSet.objects.filter( - branch=v2_tag.branch, - created_at__gt=v1_tag.created_at, - created_at__lte=v2_tag.created_at, - status='applied' - ).order_by('created_at')) - ) - - async def _process_changeset(self, changeset: ChangeSet) -> List[Dict[str, Any]]: - """Process individual changeset for comparison""" - loop = asyncio.get_event_loop() - - def process(): - changes = [] - instance = changeset.historical_instance - if instance: - # Get changes from historical record - diff = instance.diff_against_previous - if diff: - for field, values in diff.items(): - change = { - 'type': 'field', - 'file': f"{instance._meta.model_name}.{field}", - 'old_value': values['old'], - 'new_value': values['new'] - } - changes.append(change) - return changes - - return await loop.run_in_executor(self.executor, process) - - def _get_cache_key(self, version1: str, version2: str) -> str: - """Generate cache key for version comparison""" - return f"version_diff:{version1}:{version2}" \ No newline at end of file diff --git a/history_tracking/context_processors.py b/history_tracking/context_processors.py deleted file mode 100644 index 07c12c9a..00000000 --- a/history_tracking/context_processors.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import Dict, Any -from django.http import HttpRequest -from .signals import get_current_branch -from .models import VersionBranch, ChangeSet - -def version_control(request: HttpRequest) -> Dict[str, Any]: - """ - Add version control information to the template context - """ - current_branch = get_current_branch() - context = { - 'vcs_enabled': True, - 'current_branch': current_branch, - 'recent_changes': [] - } - - if current_branch: - # Get recent changes for the current branch - recent_changes = ChangeSet.objects.filter( - branch=current_branch, - status='applied' - ).order_by('-created_at')[:5] - - context.update({ - 'recent_changes': recent_changes, - 'branch_name': current_branch.name, - 'branch_metadata': current_branch.metadata - }) - - # Get available branches for switching - context['available_branches'] = VersionBranch.objects.filter( - is_active=True - ).order_by('-created_at') - - # Check if current page is versioned - if hasattr(request, 'resolver_match') and request.resolver_match: - view_func = request.resolver_match.func - if hasattr(view_func, 'view_class'): - view_class = view_func.view_class - context['page_is_versioned'] = hasattr(view_class, 'model') and \ - hasattr(view_class.model, 'history') - - return {'version_control': context} \ No newline at end of file diff --git a/history_tracking/custom_history.py b/history_tracking/custom_history.py deleted file mode 100644 index c68a53b8..00000000 --- a/history_tracking/custom_history.py +++ /dev/null @@ -1,61 +0,0 @@ -from django.db import models -from simple_history.models import HistoricalRecords -from django.contrib.contenttypes.fields import GenericRelation -from django.utils.timezone import now - -class CustomHistoricalRecords(HistoricalRecords): - """Custom historical records that properly handle generic relations.""" - - def copy_fields(self, model): - """ - Copy fields from the model to the historical record model, - excluding GenericRelation fields. - """ - fields = {} - for field in model._meta.concrete_fields: - if not isinstance(field, GenericRelation) and field.name not in [ - 'comments', 'comment_threads', 'photos', 'reviews' - ]: - fields[field.name] = field.clone() - return fields - - def create_history_model(self, model, inherited): - """ - Override to ensure we don't create duplicate auto fields. - """ - attrs = { - '__module__': model.__module__, - '_history_excluded_fields': ['comments', 'comment_threads', 'photos', 'reviews'], - } - - app_module = '%s.models' % model._meta.app_label - - if inherited: - # inherited use models.AutoField instead of models.IntegerField - attrs.update({ - 'id': models.AutoField(primary_key=True), - 'history_id': models.AutoField(primary_key=True), - 'history_date': models.DateTimeField(default=now), - 'history_change_reason': models.CharField(max_length=100, null=True), - 'history_type': models.CharField(max_length=1, choices=( - ('+', 'Created'), - ('~', 'Changed'), - ('-', 'Deleted'), - )), - 'history_user': models.ForeignKey( - 'accounts.User', - null=True, - on_delete=models.SET_NULL, - related_name='+' - ), - }) - - # Convert field to point to historical model - fields = self.copy_fields(model) - attrs.update(fields) - - return type( - str('Historical%s' % model._meta.object_name), - (models.Model,), - attrs - ) \ No newline at end of file diff --git a/history_tracking/historical_fields.py b/history_tracking/historical_fields.py deleted file mode 100644 index ec6ba7f7..00000000 --- a/history_tracking/historical_fields.py +++ /dev/null @@ -1,49 +0,0 @@ -from django.db import models -from django.conf import settings -from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey -from typing import List, Type - -def get_trackable_fields(model_class: Type[models.Model]) -> List[models.Field]: - """Get fields that should be tracked in history.""" - if getattr(model_class, '_is_historical_model', False): - # For historical models, only return core history fields - return [ - models.BigAutoField(name='id', primary_key=True), - models.DateTimeField(name='history_date'), - models.CharField(name='history_change_reason', max_length=100, null=True), - models.CharField(name='history_type', max_length=1), - models.ForeignKey( - to=settings.AUTH_USER_MODEL, - name='history_user', - null=True, - on_delete=models.SET_NULL - ) - ] - - trackable_fields = [] - excluded_fields = { - 'comment_threads', 'comments', 'photos', 'reviews', - 'thread', 'content_type', 'object_id', 'content_object' - } - - for field in model_class._meta.get_fields(): - # Skip fields we don't want to track - if any([ - isinstance(field, (GenericRelation, GenericForeignKey)), - field.name in excluded_fields, - field.is_relation and hasattr(field.remote_field.model, '_meta') and - 'commentthread' in field.remote_field.model._meta.model_name.lower() - ]): - continue - - trackable_fields.append(field) - - return trackable_fields - -class HistoricalFieldsMixin: - """Mixin that controls which fields are copied to historical models.""" - - @classmethod - def get_fields_to_track(cls) -> List[models.Field]: - """Get fields that should be tracked in history.""" - return get_trackable_fields(cls) \ No newline at end of file diff --git a/history_tracking/htmx_views.py b/history_tracking/htmx_views.py deleted file mode 100644 index 82a7f358..00000000 --- a/history_tracking/htmx_views.py +++ /dev/null @@ -1,123 +0,0 @@ -from django.shortcuts import render, get_object_or_404 -from django.contrib.auth.decorators import login_required -from django.views.decorators.http import require_http_methods -from django.http import HttpRequest, HttpResponse, Http404 -from django.template.loader import render_to_string -from django.core.exceptions import PermissionDenied - -from .models import ChangeSet, HistoricalCommentThread, Comment -from .notifications import NotificationDispatcher -from .state_machine import ApprovalStateMachine - -@login_required -def get_comments(request: HttpRequest) -> HttpResponse: - """HTMX endpoint to get comments for a specific anchor""" - anchor = request.GET.get('anchor') - if not anchor: - raise Http404("Anchor parameter is required") - - thread = HistoricalCommentThread.objects.filter(anchor__id=anchor).first() - comments = thread.comments.all() if thread else [] - - return render(request, 'history_tracking/partials/comments_list.html', { - 'comments': comments, - 'anchor': anchor - }) - -@login_required -@require_http_methods(["POST"]) -def preview_comment(request: HttpRequest) -> HttpResponse: - """HTMX endpoint for live comment preview""" - content = request.POST.get('content', '') - return render(request, 'history_tracking/partials/comment_preview.html', { - 'content': content - }) - -@login_required -@require_http_methods(["POST"]) -def add_comment(request: HttpRequest) -> HttpResponse: - """HTMX endpoint to add a comment""" - anchor = request.POST.get('anchor') - content = request.POST.get('content') - parent_id = request.POST.get('parent_id') - - if not content: - return HttpResponse("Comment content is required", status=400) - - thread, created = HistoricalCommentThread.objects.get_or_create( - anchor={'id': anchor}, - defaults={'created_by': request.user} - ) - - comment = thread.comments.create( - author=request.user, - content=content, - parent_id=parent_id if parent_id else None - ) - - comment.extract_mentions() - - # Send notifications - dispatcher = NotificationDispatcher() - dispatcher.notify_new_comment(comment, thread) - - # Return updated comments list - return render(request, 'history_tracking/partials/comments_list.html', { - 'comments': thread.comments.all(), - 'anchor': anchor - }) - -@login_required -@require_http_methods(["POST"]) -def approve_changes(request: HttpRequest, changeset_id: int) -> HttpResponse: - """HTMX endpoint for approving/rejecting changes""" - changeset = get_object_or_404(ChangeSet, pk=changeset_id) - state_machine = ApprovalStateMachine(changeset) - - if not state_machine.can_user_approve(request.user): - raise PermissionDenied("You don't have permission to approve these changes") - - decision = request.POST.get('decision', 'approve') - comment = request.POST.get('comment', '') - stage_id = request.POST.get('stage_id') - - success = state_machine.submit_approval( - user=request.user, - decision=decision, - comment=comment, - stage_id=stage_id - ) - - if not success: - return HttpResponse("Failed to submit approval", status=400) - - # Return updated approval status - return render(request, 'history_tracking/partials/approval_status.html', { - 'changeset': changeset, - 'current_stage': state_machine.get_current_stage(), - 'can_approve': state_machine.can_user_approve(request.user), - 'pending_approvers': state_machine.get_pending_approvers() - }) - -@login_required -def approval_notifications(request: HttpRequest, changeset_id: int) -> HttpResponse: - """HTMX endpoint for live approval notifications""" - changeset = get_object_or_404(ChangeSet, pk=changeset_id) - return render(request, 'history_tracking/partials/approval_notifications.html', { - 'notifications': changeset.get_recent_notifications() - }) - -@login_required -def get_replies(request: HttpRequest, comment_id: int) -> HttpResponse: - """HTMX endpoint to get comment replies""" - comment = get_object_or_404(Comment, pk=comment_id) - return render(request, 'history_tracking/partials/comment_replies.html', { - 'replies': comment.replies.all() - }) - -@login_required -def reply_form(request: HttpRequest) -> HttpResponse: - """HTMX endpoint to get the reply form template""" - return render(request, 'history_tracking/partials/reply_form.html', { - 'parent_id': request.GET.get('parent_id') - }) \ No newline at end of file diff --git a/history_tracking/managers.py b/history_tracking/managers.py deleted file mode 100644 index 5f3b5fc0..00000000 --- a/history_tracking/managers.py +++ /dev/null @@ -1,519 +0,0 @@ -from typing import Optional, List, Dict, Any, Tuple, Type, TypeVar, cast -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 django.contrib.auth.models import AbstractUser -from collections import Counter -import json -from .models import VersionBranch, VersionTag, ChangeSet - -UserModel = TypeVar('UserModel', bound=AbstractUser) -User = cast(Type[UserModel], get_user_model()) - -class BranchManager: - """Manages version control branch operations""" - - @transaction.atomic - def create_branch( - self, - name: str, - parent: Optional[VersionBranch] = None, - user: Optional[UserModel] = 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[UserModel] = 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) - - @transaction.atomic - def acquire_lock( - self, - branch: VersionBranch, - user: UserModel, - duration: int = 48, - reason: str = "" - ) -> bool: - """ - Acquire a lock on a branch - Args: - branch: The branch to lock - user: User acquiring the lock - duration: Lock duration in hours (default 48) - reason: Reason for locking - Returns: - bool: True if lock acquired, False if already locked - """ - # Check if branch is already locked - if branch.lock_status: - expires = timezone.datetime.fromisoformat(branch.lock_status['expires']) - if timezone.now() < expires: - return False - - # Set lock - expiry = timezone.now() + timezone.timedelta(hours=duration) - branch.lock_status = { - 'user': user.id, - 'expires': expiry.isoformat(), - 'reason': reason - } - - # Record in history - branch.lock_history.append({ - 'user': user.id, - 'action': 'lock', - 'timestamp': timezone.now().isoformat(), - 'reason': reason, - 'expires': expiry.isoformat() - }) - - branch.save() - return True - - @transaction.atomic - def release_lock( - self, - branch: VersionBranch, - user: UserModel, - force: bool = False - ) -> bool: - """ - Release a lock on a branch - Args: - branch: The branch to unlock - user: User releasing the lock - force: Whether to force unlock (requires permissions) - Returns: - bool: True if lock released, False if not locked or unauthorized - """ - if not branch.lock_status: - return False - - locked_by = branch.lock_status.get('user') - if not locked_by: - return False - - # Check authorization - if not force and locked_by != user.id: - if not user.has_perm('history_tracking.force_unlock_branch'): - return False - - # Record in history - branch.lock_history.append({ - 'user': user.id, - 'action': 'unlock', - 'timestamp': timezone.now().isoformat(), - 'forced': force - }) - - # Clear lock - branch.lock_status = {} - branch.save() - return True - - def check_lock(self, branch: VersionBranch) -> Dict[str, Any]: - """ - Check the lock status of a branch - Args: - branch: The branch to check - Returns: - dict: Lock status information - """ - if not branch.lock_status: - return {'locked': False} - - expires = timezone.datetime.fromisoformat(branch.lock_status['expires']) - if timezone.now() >= expires: - # Lock has expired - branch.lock_status = {} - branch.save() - return {'locked': False} - - return { - 'locked': True, - 'user': User.objects.get(id=branch.lock_status['user']), - 'expires': expires, - 'reason': branch.lock_status.get('reason', '') - } - - def get_lock_history( - self, - branch: VersionBranch, - limit: Optional[int] = None - ) -> List[Dict[str, Any]]: - """ - Get the lock history for a branch - Args: - branch: The branch to get history for - limit: Optional limit on number of entries - Returns: - list: Lock history entries - """ - history = branch.lock_history - if limit: - history = history[-limit:] - - # Enhance history with user objects - for entry in history: - try: - entry['user_obj'] = User.objects.get(id=entry['user']) - except User.DoesNotExist: - entry['user_obj'] = None - - return history - -class ChangeTracker: - """Tracks and manages changes across the system""" - - @transaction.atomic - def record_change( - self, - instance: Any, - change_type: str, - branch: VersionBranch, - user: Optional[UserModel] = 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')) - - def compute_enhanced_diff( - self, - version1: Any, - version2: Any, - syntax_detect: bool = True - ) -> Dict[str, Any]: - """ - Return structured diff with syntax metadata - Args: - version1: First version to compare - version2: Second version to compare - syntax_detect: Whether to detect syntax types - Returns: - Dict containing structured diff with metadata - """ - if not hasattr(version1, 'history') or not hasattr(version2, 'history'): - raise ValueError("Both versions must be history-tracked models") - - # Get historical records - v1_history = version1.history.first() - v2_history = version2.history.first() - - if not (v1_history and v2_history): - raise ValueError("No historical records found") - - changes = {} - - # Compare fields and detect syntax - for field in v2_history._meta.fields: - field_name = field.name - if field_name in [ - 'history_id', 'history_date', 'history_type', - 'history_user_id', 'history_change_reason' - ]: - continue - - old_value = getattr(v1_history, field_name) - new_value = getattr(v2_history, field_name) - - if old_value != new_value: - field_type = field.get_internal_type() - syntax_type = self._detect_syntax(field_type, old_value) if syntax_detect else 'text' - - changes[field_name] = { - 'old': str(old_value), - 'new': str(new_value), - 'type': field_type, - 'syntax': syntax_type, - 'line_numbers': self._compute_line_numbers(old_value, new_value), - 'metadata': { - 'comment_anchor_id': f"{v2_history.history_id}_{field_name}", - 'field_type': field_type, - 'content_type': v2_history._meta.model_name - } - } - - # Calculate impact metrics - impact_metrics = self._calculate_impact_metrics(changes) - - return { - 'changes': changes, - 'metadata': { - 'version1_id': v1_history.history_id, - 'version2_id': v2_history.history_id, - 'timestamp': timezone.now().isoformat(), - 'impact_score': impact_metrics['impact_score'], - 'stats': impact_metrics['stats'], - 'performance': { - 'syntax_detection': syntax_detect, - 'computed_at': timezone.now().isoformat() - } - } - } - - def _detect_syntax(self, field_type: str, value: Any) -> str: - """ - Detect syntax type for field content - Args: - field_type: Django field type - value: Field value - Returns: - Detected syntax type - """ - if field_type in ['TextField', 'CharField']: - # Try to detect if it's code - if isinstance(value, str): - if value.startswith('def ') or value.startswith('class '): - return 'python' - if value.startswith('{') or value.startswith('['): - try: - json.loads(value) - return 'json' - except: - pass - if value.startswith(' Dict[str, List[int]]: - """ - Compute line numbers for diff navigation - Args: - old_value: Previous value - new_value: New value - Returns: - Dict with old and new line numbers - """ - def count_lines(value): - if not isinstance(value, str): - value = str(value) - return value.count('\n') + 1 - - old_lines = count_lines(old_value) - new_lines = count_lines(new_value) - - return { - 'old': list(range(1, old_lines + 1)), - 'new': list(range(1, new_lines + 1)) - } - - def _calculate_impact_metrics(self, changes: Dict[str, Any]) -> Dict[str, Any]: - """ - Calculate impact metrics for changes - Args: - changes: Dict of changes - Returns: - Dict with impact metrics - """ - total_lines_changed = sum( - len(c['line_numbers']['old']) + len(c['line_numbers']['new']) - for c in changes.values() - ) - - field_types = Counter(c['type'] for c in changes.values()) - syntax_types = Counter(c['syntax'] for c in changes.values()) - - # Calculate impact score (0-1) - impact_weights = { - 'lines_changed': 0.4, - 'fields_changed': 0.3, - 'complexity': 0.3 - } - - # Normalize metrics - normalized_lines = min(1.0, total_lines_changed / 1000) # Cap at 1000 lines - normalized_fields = min(1.0, len(changes) / 20) # Cap at 20 fields - - # Complexity based on field and syntax types - complexity_score = ( - len(field_types) / 10 + # Variety of field types - len(syntax_types) / 5 + # Variety of syntax types - (field_types.get('JSONField', 0) * 0.2) + # Weight complex fields higher - (syntax_types.get('python', 0) * 0.2) # Weight code changes higher - ) / 2 # Normalize to 0-1 - - impact_score = ( - impact_weights['lines_changed'] * normalized_lines + - impact_weights['fields_changed'] * normalized_fields + - impact_weights['complexity'] * complexity_score - ) - - return { - 'impact_score': impact_score, - 'stats': { - 'total_lines_changed': total_lines_changed, - 'fields_changed': len(changes), - 'field_types': dict(field_types), - 'syntax_types': dict(syntax_types) - } - } - -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 deleted file mode 100644 index 3e48edae..00000000 --- a/history_tracking/migrations/0002_versionbranch_changeset_versiontag_and_more.py +++ /dev/null @@ -1,220 +0,0 @@ -# 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/mixins.py b/history_tracking/mixins.py index 90384606..1ddf4347 100644 --- a/history_tracking/mixins.py +++ b/history_tracking/mixins.py @@ -1,11 +1,9 @@ # history_tracking/mixins.py from django.db import models from django.conf import settings -from django.contrib.contenttypes.fields import GenericRelation class HistoricalChangeMixin(models.Model): """Mixin for historical models to track changes""" - comments = GenericRelation('CommentThread', related_query_name='historical_record') id = models.BigIntegerField(db_index=True, auto_created=True, blank=True) history_date = models.DateTimeField() history_id = models.AutoField(primary_key=True) @@ -35,7 +33,6 @@ class HistoricalChangeMixin(models.Model): @property def diff_against_previous(self): - """Get enhanced diff with syntax highlighting and metadata""" prev_record = self.prev_record if not prev_record: return {} @@ -57,69 +54,11 @@ class HistoricalChangeMixin(models.Model): old_value = getattr(prev_record, field) new_value = getattr(self, field) if old_value != new_value: - field_type = self._meta.get_field(field).get_internal_type() - syntax_type = self._get_syntax_type(field_type) - changes[field] = { - "old": str(old_value), - "new": str(new_value), - "syntax_type": syntax_type, - "metadata": { - "field_type": field_type, - "comment_anchor_id": f"{self.history_id}_{field}", - "line_numbers": self._compute_line_numbers(old_value, new_value) - } - } + changes[field] = {"old": str(old_value), "new": str(new_value)} except AttributeError: continue return changes - def _get_syntax_type(self, field_type): - """Map Django field types to syntax highlighting types""" - syntax_map = { - 'TextField': 'text', - 'JSONField': 'json', - 'FileField': 'path', - 'ImageField': 'path', - 'URLField': 'url', - 'EmailField': 'email', - 'CodeField': 'python' # Custom field type for code - } - return syntax_map.get(field_type, 'text') - - def _compute_line_numbers(self, old_value, new_value): - """Compute line numbers for diff navigation""" - old_lines = str(old_value).count('\n') + 1 - new_lines = str(new_value).count('\n') + 1 - return { - "old": list(range(1, old_lines + 1)), - "new": list(range(1, new_lines + 1)) - } - - def get_structured_diff(self, other_version=None): - """Get structured diff between two versions with enhanced metadata""" - compare_to = other_version or self.prev_record - if not compare_to: - return None - - diff_data = self.diff_against_previous - return { - "changes": diff_data, - "metadata": { - "timestamp": self.history_date.isoformat(), - "user": self.history_user_display, - "change_type": self.history_type, - "reason": self.history_change_reason, - "performance": { - "computation_time": None # To be filled by frontend - } - }, - "navigation": { - "next_id": None, # To be filled by frontend - "prev_id": None, # To be filled by frontend - "current_position": None # To be filled by frontend - } - } - @property def history_user_display(self): """Get a display name for the history user""" diff --git a/history_tracking/models.py b/history_tracking/models.py index f39be298..234fd852 100644 --- a/history_tracking/models.py +++ b/history_tracking/models.py @@ -1,43 +1,20 @@ +# history_tracking/models.py from django.db import models -from django.conf import settings from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation -from django.db.models.fields.related import RelatedField -from django.contrib.auth import get_user_model +from django.contrib.contenttypes.fields import GenericForeignKey from simple_history.models import HistoricalRecords from .mixins import HistoricalChangeMixin -from .historical_fields import HistoricalFieldsMixin -from typing import Any, Type, TypeVar, cast, Optional, List +from typing import Any, Type, TypeVar, cast 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, HistoricalFieldsMixin): +class HistoricalModel(models.Model): """Abstract base class for models with history tracking""" id = models.BigAutoField(primary_key=True) - - @classmethod - def __init_subclass__(cls, **kwargs): - """Initialize subclass with proper configuration.""" - super().__init_subclass__(**kwargs) - # Mark historical models - if cls.__name__.startswith('Historical'): - cls._is_historical_model = True - # Remove any inherited generic relations - for field in list(cls._meta.private_fields): - if isinstance(field, GenericRelation): - cls._meta.private_fields.remove(field) - else: - cls._is_historical_model = False - history = HistoricalRecords( + history: HistoricalRecords = HistoricalRecords( inherit=True, - bases=[HistoricalChangeMixin], - excluded_fields=['comments', 'comment_threads', 'photos', 'reviews'], - use_base_model_db=True # Use base model's db + bases=(HistoricalChangeMixin,) ) class Meta: @@ -70,233 +47,3 @@ 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) - lock_status = models.JSONField( - default=dict, - help_text="Current lock status: {user: ID, expires: datetime, reason: str}" - ) - lock_history = models.JSONField( - default=list, - help_text="History of lock operations: [{user: ID, action: lock/unlock, timestamp: datetime, reason: str}]" - ) - - 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) - comparison_metadata = models.JSONField( - default=dict, - help_text="Stores diff statistics and comparison results" - ) - - 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 HistoricalCommentThread(models.Model): - """Represents a thread of comments specific to historical records and version control""" - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - object_id = models.PositiveIntegerField() - content_object = 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, related_name='created_threads') - anchor = models.JSONField( - default=dict, - help_text="Anchoring information: {line_start: int, line_end: int, file_path: str}" - ) - is_resolved = models.BooleanField(default=False) - resolved_by = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - related_name='resolved_threads' - ) - resolved_at = models.DateTimeField(null=True, blank=True) - - class Meta: - ordering = ['-created_at'] - indexes = [ - models.Index(fields=['content_type', 'object_id']), - models.Index(fields=['created_at']), - models.Index(fields=['is_resolved']), - ] - - def __str__(self) -> str: - return f"Comment Thread {self.pk} on {self.content_type}" - -class Comment(models.Model): - """Individual comment within a thread""" - thread = models.ForeignKey(HistoricalCommentThread, on_delete=models.CASCADE, related_name='comments') - author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) - content = models.TextField() - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - mentioned_users = models.ManyToManyField( - User, - related_name='mentioned_in_comments', - blank=True - ) - parent_comment = models.ForeignKey( - 'self', - on_delete=models.CASCADE, - null=True, - blank=True, - related_name='replies' - ) - - class Meta: - ordering = ['created_at'] - - def __str__(self) -> str: - return f"Comment {self.pk} by {self.author}" - - def extract_mentions(self) -> None: - """Extract @mentions from comment content and update mentioned_users""" - # Simple @username extraction - could be enhanced with regex - mentioned = [ - word[1:] for word in self.content.split() - if word.startswith('@') and len(word) > 1 - ] - if mentioned: - users = User.objects.filter(username__in=mentioned) - self.mentioned_users.set(users) - -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=[ - ('draft', 'Draft'), - ('pending_approval', 'Pending Approval'), - ('approved', 'Approved'), - ('rejected', 'Rejected'), - ('applied', 'Applied'), - ('failed', 'Failed'), - ('reverted', 'Reverted') - ], - default='draft' - ) - approval_state = models.JSONField( - default=list, - help_text="List of approval stages and their status" - ) - approval_history = models.JSONField( - default=list, - help_text="History of approval actions and decisions" - ) - required_approvers = models.ManyToManyField( - User, - related_name='pending_approvals', - blank=True - ) - approval_policy = models.CharField( - max_length=20, - choices=[ - ('sequential', 'Sequential'), - ('parallel', 'Parallel') - ], - default='sequential' - ) - approval_deadline = models.DateTimeField( - null=True, - blank=True, - help_text="Optional deadline for approvals" - ) - - # 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/monitoring.py b/history_tracking/monitoring.py deleted file mode 100644 index de85a104..00000000 --- a/history_tracking/monitoring.py +++ /dev/null @@ -1,202 +0,0 @@ -import logging -import time -from functools import wraps -from django.conf import settings -from django.db import connection - -# Configure logger -logger = logging.getLogger('version_control') - -def track_operation_timing(operation_name): - """Decorator to track timing of version control operations""" - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - start_time = time.time() - try: - result = func(*args, **kwargs) - duration = time.time() - start_time - - # Log timing metrics - logger.info( - 'Version Control Operation Timing', - extra={ - 'operation': operation_name, - 'duration': duration, - 'success': True - } - ) - return result - except Exception as e: - duration = time.time() - start_time - logger.error( - 'Version Control Operation Failed', - extra={ - 'operation': operation_name, - 'duration': duration, - 'error': str(e), - 'success': False - } - ) - raise - return wrapper - return decorator - -def track_merge_result(source_branch, target_branch, success, conflict_count=0): - """Track the results of merge operations""" - logger.info( - 'Branch Merge Operation', - extra={ - 'source_branch': source_branch.name, - 'target_branch': target_branch.name, - 'success': success, - 'conflict_count': conflict_count - } - ) - -def track_branch_metrics(branch): - """Track metrics for a specific branch""" - from history_tracking.models import ChangeSet - - changes = ChangeSet.objects.filter(branch=branch) - applied_changes = changes.filter(status='applied') - pending_changes = changes.filter(status='pending') - - logger.info( - 'Branch Metrics', - extra={ - 'branch_name': branch.name, - 'total_changes': changes.count(), - 'applied_changes': applied_changes.count(), - 'pending_changes': pending_changes.count(), - 'is_active': branch.is_active - } - ) - -def track_database_metrics(): - """Track database metrics for version control operations""" - with connection.execute_wrapper(StatementLogger()): - yield - -class StatementLogger: - """Log database statements for monitoring""" - def __call__(self, execute, sql, params, many, context): - start = time.time() - try: - result = execute(sql, params, many, context) - duration = time.time() - start - - # Log only version control related queries - if 'version' in sql.lower() or 'changeset' in sql.lower(): - logger.info( - 'Version Control DB Operation', - extra={ - 'sql': sql, - 'duration': duration, - 'success': True - } - ) - return result - except Exception as e: - duration = time.time() - start - logger.error( - 'Version Control DB Operation Failed', - extra={ - 'sql': sql, - 'duration': duration, - 'error': str(e), - 'success': False - } - ) - raise - -class VersionControlMetrics: - """Collect and report version control system metrics""" - - @staticmethod - def collect_system_metrics(): - """Collect overall system metrics""" - from history_tracking.models import VersionBranch, ChangeSet - - total_branches = VersionBranch.objects.count() - active_branches = VersionBranch.objects.filter(is_active=True).count() - total_changes = ChangeSet.objects.count() - pending_changes = ChangeSet.objects.filter(status='pending').count() - conflicted_merges = ChangeSet.objects.filter( - status='conflict' - ).count() - - logger.info( - 'Version Control System Metrics', - extra={ - 'total_branches': total_branches, - 'active_branches': active_branches, - 'total_changes': total_changes, - 'pending_changes': pending_changes, - 'conflicted_merges': conflicted_merges - } - ) - - @staticmethod - def collect_performance_metrics(): - """Collect performance-related metrics""" - from django.db import connection - from django.core.cache import cache - - # Database metrics - with connection.execute_wrapper(StatementLogger()): - db_metrics = { - 'total_queries': len(connection.queries), - 'total_time': sum( - float(q['time']) for q in connection.queries - ) - } - - # Cache metrics - cache_metrics = { - 'hits': cache.get('version_control_cache_hits', 0), - 'misses': cache.get('version_control_cache_misses', 0) - } - - logger.info( - 'Version Control Performance Metrics', - extra={ - 'database': db_metrics, - 'cache': cache_metrics - } - ) - - @staticmethod - def track_user_operations(user, operation, success): - """Track user operations on version control""" - logger.info( - 'Version Control User Operation', - extra={ - 'user_id': user.id, - 'username': user.username, - 'operation': operation, - 'success': success - } - ) - -def setup_monitoring(): - """Configure monitoring for version control system""" - if not settings.DEBUG: - # Configure logging handlers - handler = logging.handlers.RotatingFileHandler( - 'logs/version_control.log', - maxBytes=10485760, # 10MB - backupCount=5 - ) - handler.setFormatter(logging.Formatter( - '%(asctime)s [%(levelname)s] %(message)s' - )) - logger.addHandler(handler) - - # Set up error reporting - import sentry_sdk # type: ignore - sentry_sdk.init( - dsn=settings.SENTRY_DSN, - traces_sample_rate=0.1, - profiles_sample_rate=0.1, - ) \ No newline at end of file diff --git a/history_tracking/notifications.py b/history_tracking/notifications.py deleted file mode 100644 index aec71f85..00000000 --- a/history_tracking/notifications.py +++ /dev/null @@ -1,229 +0,0 @@ -from django.core.mail import send_mail -from django.conf import settings -from django.template.loader import render_to_string -from django.utils import timezone -from django.contrib.auth import get_user_model -import requests -import json -from datetime import timedelta -from celery import shared_task - -User = get_user_model() - -class NotificationDispatcher: - """Handles comment notifications and escalations""" - - def __init__(self): - self.email_enabled = hasattr(settings, 'EMAIL_HOST') - self.slack_enabled = hasattr(settings, 'SLACK_WEBHOOK_URL') - self.sms_enabled = hasattr(settings, 'SMS_API_KEY') - - def notify_new_comment(self, comment, thread): - """Handle notification for a new comment""" - # Queue immediate notifications - self.send_in_app_notification.delay( - user_ids=self._get_thread_participants(thread), - title="New Comment", - message=f"New comment on {thread.content_object}", - link=self._get_thread_url(thread) - ) - - # Queue email notifications - self.send_email_notification.delay( - user_ids=self._get_thread_participants(thread), - subject=f"New comment on {thread.content_object}", - template="notifications/new_comment.html", - context={ - 'comment': comment, - 'thread': thread, - 'url': self._get_thread_url(thread) - } - ) - - # Schedule Slack escalation if needed - if self.slack_enabled: - self.schedule_slack_escalation.apply_async( - args=[comment.id], - countdown=24 * 3600 # 24 hours - ) - - def notify_mention(self, comment, mentioned_users): - """Handle notification for @mentions""" - user_ids = [user.id for user in mentioned_users] - - # Queue immediate notifications - self.send_in_app_notification.delay( - user_ids=user_ids, - title="Mentioned in Comment", - message=f"{comment.author} mentioned you in a comment", - link=self._get_comment_url(comment) - ) - - # Queue email notifications - self.send_email_notification.delay( - user_ids=user_ids, - subject="You were mentioned in a comment", - template="notifications/mention.html", - context={ - 'comment': comment, - 'url': self._get_comment_url(comment) - } - ) - - # Queue mobile push notifications - self.send_push_notification.delay( - user_ids=user_ids, - title="New Mention", - message=f"{comment.author} mentioned you: {comment.content[:100]}..." - ) - - # Schedule SMS escalation if needed - if self.sms_enabled: - self.schedule_sms_escalation.apply_async( - args=[comment.id, user_ids], - countdown=12 * 3600 # 12 hours - ) - - def notify_resolution(self, thread, resolver): - """Handle notification for thread resolution""" - self.send_in_app_notification.delay( - user_ids=self._get_thread_participants(thread), - title="Thread Resolved", - message=f"Thread resolved by {resolver}", - link=self._get_thread_url(thread) - ) - - @shared_task - def send_in_app_notification(user_ids, title, message, link): - """Send in-app notification to users""" - from .models import InAppNotification - for user_id in user_ids: - InAppNotification.objects.create( - user_id=user_id, - title=title, - message=message, - link=link - ) - - @shared_task - def send_email_notification(user_ids, subject, template, context): - """Send email notification to users""" - if not settings.EMAIL_HOST: - return - - users = User.objects.filter(id__in=user_ids) - for user in users: - if not user.email: - continue - - html_content = render_to_string(template, { - 'user': user, - **context - }) - - send_mail( - subject=subject, - message='', - html_message=html_content, - from_email=settings.DEFAULT_FROM_EMAIL, - recipient_list=[user.email] - ) - - @shared_task - def send_push_notification(user_ids, title, message): - """Send mobile push notification""" - from .models import PushToken - - tokens = PushToken.objects.filter( - user_id__in=user_ids, - active=True - ) - - if not tokens: - return - - # Implementation depends on push notification service - # Example using Firebase: - try: - requests.post( - settings.FIREBASE_FCM_URL, - headers={ - 'Authorization': f'key={settings.FIREBASE_SERVER_KEY}', - 'Content-Type': 'application/json' - }, - json={ - 'registration_ids': [t.token for t in tokens], - 'notification': { - 'title': title, - 'body': message - } - } - ) - except Exception as e: - print(f"Push notification failed: {e}") - - @shared_task - def schedule_slack_escalation(comment_id): - """Send Slack DM escalation for unread comments""" - from .models import Comment - - try: - comment = Comment.objects.get(id=comment_id) - if not comment.read_by.exists(): - # Send Slack message - requests.post( - settings.SLACK_WEBHOOK_URL, - json={ - 'text': ( - f"Unread comment needs attention:\n" - f"{comment.content}\n" - f"View: {self._get_comment_url(comment)}" - ) - } - ) - except Exception as e: - print(f"Slack escalation failed: {e}") - - @shared_task - def schedule_sms_escalation(comment_id, user_ids): - """Send SMS escalation for unread mentions""" - from .models import Comment - - try: - comment = Comment.objects.get(id=comment_id) - users = User.objects.filter(id__in=user_ids) - - for user in users: - if not user.phone_number: - continue - - if not comment.read_by.filter(id=user.id).exists(): - # Send SMS using Twilio or similar service - requests.post( - settings.SMS_API_URL, - headers={'Authorization': f'Bearer {settings.SMS_API_KEY}'}, - json={ - 'to': user.phone_number, - 'message': ( - f"You were mentioned in a comment that needs attention. " - f"View: {self._get_comment_url(comment)}" - ) - } - ) - except Exception as e: - print(f"SMS escalation failed: {e}") - - def _get_thread_participants(self, thread): - """Get IDs of all participants in a thread""" - return list(set( - [thread.created_by_id] + - list(thread.comments.values_list('author_id', flat=True)) - )) - - def _get_thread_url(self, thread): - """Generate URL for thread""" - return f"/version-control/comments/thread/{thread.id}/" - - def _get_comment_url(self, comment): - """Generate URL for specific comment""" - return f"{self._get_thread_url(comment.thread)}#comment-{comment.id}" \ No newline at end of file diff --git a/history_tracking/signals.py b/history_tracking/signals.py deleted file mode 100644 index b374d9aa..00000000 --- a/history_tracking/signals.py +++ /dev/null @@ -1,138 +0,0 @@ -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/state_machine.py b/history_tracking/state_machine.py deleted file mode 100644 index 5aa2ef0b..00000000 --- a/history_tracking/state_machine.py +++ /dev/null @@ -1,194 +0,0 @@ -from typing import List, Dict, Any, Optional -from django.contrib.auth import get_user_model -from django.utils import timezone -from .models import ChangeSet - -User = get_user_model() - -class ApprovalStage: - def __init__(self, stage_id: int, name: str, required_roles: List[str]): - self.id = stage_id - self.name = name - self.required_roles = required_roles - self.approvers: List[Dict[str, Any]] = [] - self.status = 'pending' # pending, approved, rejected - self.completed_at = None - - def add_approver(self, user: User, decision: str, comment: str = "") -> None: - """Add an approver's decision to this stage""" - self.approvers.append({ - 'user_id': user.id, - 'username': user.username, - 'decision': decision, - 'comment': comment, - 'timestamp': timezone.now().isoformat() - }) - - def is_approved(self, policy: str = 'unanimous') -> bool: - """Check if stage is approved based on policy""" - if not self.approvers: - return False - - approve_count = sum(1 for a in self.approvers if a['decision'] == 'approve') - if policy == 'unanimous': - return approve_count == len(self.required_roles) - else: # majority - return approve_count > len(self.required_roles) / 2 - -class ApprovalStateMachine: - """Manages the state transitions for change approval workflow""" - - def __init__(self, changeset: ChangeSet): - self.changeset = changeset - self.stages: List[ApprovalStage] = [] - self.current_stage_index = 0 - self.policy = changeset.approval_policy - self._load_state() - - def _load_state(self) -> None: - """Load the current state from changeset approval_state""" - if not self.changeset.approval_state: - return - - for stage_data in self.changeset.approval_state: - stage = ApprovalStage( - stage_data['id'], - stage_data['name'], - stage_data['required_roles'] - ) - stage.approvers = stage_data.get('approvers', []) - stage.status = stage_data.get('status', 'pending') - stage.completed_at = stage_data.get('completed_at') - self.stages.append(stage) - - # Find current stage - self.current_stage_index = next( - (i for i, s in enumerate(self.stages) if s.status == 'pending'), - len(self.stages) - 1 - ) - - def initialize_workflow(self, stages_config: List[Dict[str, Any]]) -> None: - """Set up initial approval workflow stages""" - self.stages = [ - ApprovalStage( - i, - stage['name'], - stage['required_roles'] - ) for i, stage in enumerate(stages_config) - ] - self._save_state() - - def submit_approval( - self, - user: User, - decision: str, - comment: str = "", - stage_id: Optional[int] = None - ) -> bool: - """ - Submit an approval decision - Args: - user: The user submitting approval - decision: 'approve' or 'reject' - comment: Optional comment - stage_id: Optional specific stage ID (for parallel approval) - Returns: - bool: True if submission was accepted - """ - if self.changeset.status != 'pending_approval': - return False - - if self.policy == 'sequential': - stage = self.stages[self.current_stage_index] - else: # parallel - if stage_id is None: - return False - stage = next((s for s in self.stages if s.id == stage_id), None) - if not stage: - return False - - # Check if user has required role - user_roles = set(user.groups.values_list('name', flat=True)) - if not any(role in user_roles for role in stage.required_roles): - return False - - # Add decision - stage.add_approver(user, decision, comment) - - # Update stage status - if stage.is_approved(self.policy): - stage.status = 'approved' - stage.completed_at = timezone.now().isoformat() - - if self.policy == 'sequential': - self._advance_stage() - elif decision == 'reject': - stage.status = 'rejected' - stage.completed_at = timezone.now().isoformat() - self.changeset.status = 'rejected' - self.changeset.save() - - self._save_state() - return True - - def _advance_stage(self) -> None: - """Move to next stage if available""" - if self.current_stage_index < len(self.stages) - 1: - self.current_stage_index += 1 - else: - # All stages approved - self.changeset.status = 'approved' - self.changeset.save() - - def _save_state(self) -> None: - """Save current state to changeset""" - self.changeset.approval_state = [ - { - 'id': stage.id, - 'name': stage.name, - 'required_roles': stage.required_roles, - 'approvers': stage.approvers, - 'status': stage.status, - 'completed_at': stage.completed_at - } for stage in self.stages - ] - self.changeset.save() - - def get_current_stage(self) -> Optional[ApprovalStage]: - """Get the current active stage""" - if not self.stages: - return None - return self.stages[self.current_stage_index] - - def get_stage_by_id(self, stage_id: int) -> Optional[ApprovalStage]: - """Get a specific stage by ID""" - return next((s for s in self.stages if s.id == stage_id), None) - - def get_pending_approvers(self) -> List[str]: - """Get list of roles that still need to approve the current stage""" - current_stage = self.get_current_stage() - if not current_stage: - return [] - - approved_by = {a['user_id'] for a in current_stage.approvers} - return [ - role for role in current_stage.required_roles - if not any( - user.id in approved_by - for user in User.objects.filter(groups__name=role) - ) - ] - - def can_user_approve(self, user: User) -> bool: - """Check if user can approve the current stage""" - current_stage = self.get_current_stage() - if not current_stage: - return False - - # Check if user already approved - if any(a['user_id'] == user.id for a in current_stage.approvers): - return False - - # Check if user has required role - user_roles = set(user.groups.values_list('name', flat=True)) - return any(role in user_roles for role in current_stage.required_roles) \ No newline at end of file diff --git a/history_tracking/templates/history_tracking/approval_status.html b/history_tracking/templates/history_tracking/approval_status.html deleted file mode 100644 index 77f6de2e..00000000 --- a/history_tracking/templates/history_tracking/approval_status.html +++ /dev/null @@ -1,174 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block content %} -
-
-

Approval Status

-
- Changeset #{{ changeset.pk }} - {{ changeset.status|title }} -
-
- - {% if changeset.description %} -
- {{ changeset.description }} -
- {% endif %} - - {% if current_stage %} -
-

Current Stage: {{ current_stage.name }}

- -
-

Required Approvers:

-
    - {% for role in current_stage.required_roles %} -
  • {{ role }}
  • - {% endfor %} -
-
- -
-

Current Approvers:

- {% if current_stage.approvers %} -
    - {% for approver in current_stage.approvers %} -
  • -
    - {{ approver.user }} - {{ approver.timestamp|date:"Y-m-d H:i" }} -
    - {% if approver.comment %} -
    - {{ approver.comment }} -
    - {% endif %} -
  • - {% endfor %} -
- {% else %} -

No approvals yet

- {% endif %} -
- - {% if can_approve %} -
-
- {% csrf_token %} -
- - -
- {% if current_stage.id %} - - {% endif %} -
- - -
-
-
- {% endif %} -
- {% endif %} - -
-

Approval History

- {% if changeset.approval_history %} -
- {% for entry in changeset.approval_history %} -
-
- {{ entry.stage_name }} - -
-
- {{ entry.user }} - {{ entry.action|title }} - {% if entry.comment %} -
- {{ entry.comment }} -
- {% endif %} -
-
- {% endfor %} -
- {% else %} -

No approval history yet

- {% endif %} -
- - {% if pending_approvers %} -
-

Waiting for Approval From:

- -
- {% endif %} -
- -{% if messages %} -
- {% for message in messages %} -
- {{ message }} -
- {% endfor %} -
-{% endif %} -{% endblock %} - -{% block extra_css %} - -{% endblock %} \ 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 deleted file mode 100644 index 9893de9f..00000000 --- a/history_tracking/templates/history_tracking/components/branch_create.html +++ /dev/null @@ -1,58 +0,0 @@ -
-

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 deleted file mode 100644 index 08f0cb34..00000000 --- a/history_tracking/templates/history_tracking/components/branch_list.html +++ /dev/null @@ -1,43 +0,0 @@ -
- {% 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 deleted file mode 100644 index 7471aa48..00000000 --- a/history_tracking/templates/history_tracking/components/history_view.html +++ /dev/null @@ -1,88 +0,0 @@ -
-

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 deleted file mode 100644 index ef33de15..00000000 --- a/history_tracking/templates/history_tracking/components/merge_conflicts.html +++ /dev/null @@ -1,116 +0,0 @@ -
-
-
-
- - - -
-
-

- 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 deleted file mode 100644 index 2914c75c..00000000 --- a/history_tracking/templates/history_tracking/components/merge_panel.html +++ /dev/null @@ -1,49 +0,0 @@ -
-

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 deleted file mode 100644 index dffebff1..00000000 --- a/history_tracking/templates/history_tracking/components/merge_success.html +++ /dev/null @@ -1,30 +0,0 @@ -
-
-
-
- - - -
-
-

- Merge Successful -

-
-

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

-
-
-
-
- -
- -
-
- - \ No newline at end of file diff --git a/history_tracking/templates/history_tracking/includes/version_control_ui.html b/history_tracking/templates/history_tracking/includes/version_control_ui.html deleted file mode 100644 index 5a643a2a..00000000 --- a/history_tracking/templates/history_tracking/includes/version_control_ui.html +++ /dev/null @@ -1,94 +0,0 @@ -{% if version_control.vcs_enabled and version_control.page_is_versioned %} -
- -
-
-

Version Control

- {% if version_control.current_branch %} -

- Current Branch: - {{ version_control.branch_name }} -

- {% endif %} -
- - -
- -
- - - - - {% if version_control.available_branches %} -
- {% for branch in version_control.available_branches %} - - {% endfor %} - {% endif %} -
-
-
- - - {% if version_control.recent_changes %} -
-

Recent Changes

-
- {% for change in version_control.recent_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 }} - -
-
- {% endfor %} -
-
- {% endif %} - - -
- - -
-
- - - -{% endif %} \ No newline at end of file diff --git a/history_tracking/templates/history_tracking/monitoring_dashboard.html b/history_tracking/templates/history_tracking/monitoring_dashboard.html deleted file mode 100644 index 06dc7617..00000000 --- a/history_tracking/templates/history_tracking/monitoring_dashboard.html +++ /dev/null @@ -1,172 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}Version Control Monitoring - ThrillWiki{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} -
-

Version Control Monitoring

- - -
-
-

Total Branches

-

{{ metrics.total_branches }}

-

{{ metrics.active_branches }} active

-
- -
-

Total Changes

-

{{ metrics.total_changes }}

-

{{ metrics.pending_changes }} pending

-
- -
-

Merge Success Rate

-

{{ metrics.merge_success_rate }}%

-

{{ metrics.conflicted_merges }} conflicts

-
- -
-

System Health

-

- {{ metrics.system_health }}% -

-

Based on {{ metrics.health_checks }} checks

-
-
- - -
-

Performance Metrics

-
- -
-

Operation Timing (avg)

-
    -
  • - Branch Creation - {{ metrics.timing.branch_creation }}ms -
  • -
  • - Branch Switch - {{ metrics.timing.branch_switch }}ms -
  • -
  • - Merge Operation - {{ metrics.timing.merge }}ms -
  • -
-
- - -
-

Database Performance

-
    -
  • - Query Count (avg) - {{ metrics.database.query_count }} -
  • -
  • - Query Time (avg) - {{ metrics.database.query_time }}ms -
  • -
  • - Connection Pool - {{ metrics.database.pool_size }}/{{ metrics.database.max_pool }} -
  • -
-
- - -
-

Cache Performance

-
    -
  • - Hit Rate - {{ metrics.cache.hit_rate }}% -
  • -
  • - Miss Rate - {{ metrics.cache.miss_rate }}% -
  • -
  • - Memory Usage - {{ metrics.cache.memory_usage }}MB -
  • -
-
-
-
- - -
-

Error Tracking

-
- - - - - - - - - - - - {% for error in metrics.errors %} - - - - - - - - {% endfor %} - -
TimeTypeOperationMessageStatus
{{ error.timestamp }}{{ error.type }}{{ error.operation }}{{ error.message }} - - {{ error.resolved|yesno:"Resolved,Unresolved" }} - -
-
-
- - -
-

Active Users

-
-
-

Current Operations

-
    - {% for operation in metrics.current_operations %} -
  • - {{ operation.user }} - {{ operation.action }} -
  • - {% endfor %} -
-
- -
-

Recent Activity

-
    - {% for activity in metrics.recent_activity %} -
  • - {{ activity.user }} {{ activity.action }} {{ activity.timestamp|timesince }} ago -
  • - {% endfor %} -
-
-
-
-
-{% endblock %} - -{% block extra_js %} - -{% endblock %} \ No newline at end of file diff --git a/history_tracking/templates/history_tracking/partials/approval_notifications.html b/history_tracking/templates/history_tracking/partials/approval_notifications.html deleted file mode 100644 index 9440aa6d..00000000 --- a/history_tracking/templates/history_tracking/partials/approval_notifications.html +++ /dev/null @@ -1,51 +0,0 @@ -{% if notifications %} -
- {% for notification in notifications %} -
-
- {{ notification.type }} - {{ notification.timestamp|timesince }} ago -
-
- {% if notification.type == 'approval' %} - {{ notification.user }} - {{ notification.action }} the changes - {% if notification.stage %} - in stage {{ notification.stage }} - {% endif %} - {% elif notification.type == 'comment' %} - {{ notification.user }} - commented on the changes - {% elif notification.type == 'stage_change' %} - Moved to stage {{ notification.stage }} - {% endif %} - - {% if notification.comment %} -
- "{{ notification.comment }}" -
- {% endif %} -
- {% if notification.actions %} -
- {% for action in notification.actions %} - - {% endfor %} -
- {% endif %} -
- {% endfor %} -
-{% else %} -
- No recent notifications -
-{% endif %} \ No newline at end of file diff --git a/history_tracking/templates/history_tracking/partials/approval_status.html b/history_tracking/templates/history_tracking/partials/approval_status.html deleted file mode 100644 index f970c5c0..00000000 --- a/history_tracking/templates/history_tracking/partials/approval_status.html +++ /dev/null @@ -1,102 +0,0 @@ -{% if current_stage %} -
-
-

Current Stage: {{ current_stage.name }}

- -
-

Required Approvers:

-
    - {% for role in current_stage.required_roles %} -
  • - {{ role }} -
  • - {% endfor %} -
-
- -
-

Current Approvers:

- {% if current_stage.approvers %} -
    - {% for approver in current_stage.approvers %} -
  • -
    - {{ approver.user }} - {{ approver.timestamp|date:"Y-m-d H:i" }} -
    - {% if approver.comment %} -
    - {{ approver.comment }} -
    - {% endif %} -
  • - {% endfor %} -
- {% else %} -

No approvals yet

- {% endif %} -
- - {% if can_approve %} -
-
- {% csrf_token %} -
- - -
-
- {% if current_stage.id %} - - {% endif %} -
- - -
-
-
- {% endif %} - - {% if messages %} -
- {% for message in messages %} - - {% endfor %} -
- {% endif %} -
-
-{% endif %} \ No newline at end of file diff --git a/history_tracking/templates/history_tracking/partials/comment_preview.html b/history_tracking/templates/history_tracking/partials/comment_preview.html deleted file mode 100644 index a2b04c66..00000000 --- a/history_tracking/templates/history_tracking/partials/comment_preview.html +++ /dev/null @@ -1,6 +0,0 @@ -{% if content %} -
-
Preview:
-
{{ content }}
-
-{% endif %} diff --git a/history_tracking/templates/history_tracking/partials/comment_replies.html b/history_tracking/templates/history_tracking/partials/comment_replies.html deleted file mode 100644 index 4507458a..00000000 --- a/history_tracking/templates/history_tracking/partials/comment_replies.html +++ /dev/null @@ -1,27 +0,0 @@ -{% if replies %} -
- {% for reply in replies %} -
-
- {{ reply.author }} - {{ reply.created_at|date:"Y-m-d H:i" }} -
-
- {{ reply.content }} -
- {% if user.has_perm 'history_tracking.add_comment' %} -
- -
-
- {% endif %} -
- {% endfor %} -
-{% endif %} \ No newline at end of file diff --git a/history_tracking/templates/history_tracking/partials/comments_list.html b/history_tracking/templates/history_tracking/partials/comments_list.html deleted file mode 100644 index 9742dbee..00000000 --- a/history_tracking/templates/history_tracking/partials/comments_list.html +++ /dev/null @@ -1,35 +0,0 @@ -{% if comments %} -
- {% for comment in comments %} -
-
- {{ comment.author }} - {{ comment.created_at|date:"Y-m-d H:i" }} -
-
{{ comment.content }}
- {% if user.has_perm 'history_tracking.add_comment' %} -
- -
-
- {% endif %} - {% if comment.replies.exists %} -
-
Loading replies...
-
- {% endif %} -
- {% endfor %} -
-{% else %} -

No comments yet

-{% endif %} \ No newline at end of file diff --git a/history_tracking/templates/history_tracking/partials/reply_form.html b/history_tracking/templates/history_tracking/partials/reply_form.html deleted file mode 100644 index 881b9573..00000000 --- a/history_tracking/templates/history_tracking/partials/reply_form.html +++ /dev/null @@ -1,33 +0,0 @@ -
-
- {% csrf_token %} - - -
- -
-
-
- - -
-
-
\ No newline at end of file diff --git a/history_tracking/templates/history_tracking/version_comparison.html b/history_tracking/templates/history_tracking/version_comparison.html deleted file mode 100644 index c73bb05b..00000000 --- a/history_tracking/templates/history_tracking/version_comparison.html +++ /dev/null @@ -1,170 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block content %} -
-
-

Version Comparison

- - {# Version Selection Form with HTMX #} -
-
-
- - -
-
- - -
-
-
Loading comparison...
-
-
- - {% if diff_result %} -
-
-

Changes Summary

-
-
- Files Changed: - {{ diff_result.stats.total_files }} -
-
- Lines Changed: - {{ diff_result.stats.total_lines }} -
-
- Impact Score: - {{ diff_result.impact_score|floatformat:2 }} -
-
-
- -
- {% for change in diff_result.changes %} -
-
-

{{ change.field }}

- {{ change.type }} -
- -
-
-
Previous Version
-
{{ change.old }}
-
-
-
New Version
-
{{ change.new }}
-
-
- - {% if user.has_perm 'history_tracking.add_comment' %} -
-
Loading comments...
-
- -
-
- {% csrf_token %} - - -
- -
-
- {% endif %} -
- {% endfor %} -
- - {% if diff_result.changes.has_other_pages %} - - {% endif %} -
- {% endif %} -
- -{% comment %} -Only include minimal JavaScript for necessary interactivity -{% endcomment %} -{% block extra_js %} - -{% endblock %} \ 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 deleted file mode 100644 index 2520ff2a..00000000 --- a/history_tracking/templates/history_tracking/version_control_panel.html +++ /dev/null @@ -1,53 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -
-
- -
-
-

Branches

- -
- - -
- -
-
- - -
- -
- - -
- -
- - -
- -
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/history_tracking/tests/test_managers.py b/history_tracking/tests/test_managers.py deleted file mode 100644 index 4af51ce1..00000000 --- a/history_tracking/tests/test_managers.py +++ /dev/null @@ -1,268 +0,0 @@ -from django.test import TestCase -from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ValidationError -from django.db import transaction -from django.utils import timezone - -from history_tracking.models import VersionBranch, ChangeSet -from history_tracking.managers import BranchManager, MergeStrategy -from parks.models import Park - -class BranchManagerTests(TestCase): - def setUp(self): - self.park = Park.objects.create( - name='Test Park', - slug='test-park', - status='OPERATING' - ) - self.content_type = ContentType.objects.get_for_model(Park) - self.manager = BranchManager() - self.main_branch = VersionBranch.objects.create( - name='main', - metadata={'type': 'default_branch'} - ) - - def test_create_branch(self): - """Test branch creation with metadata""" - branch = self.manager.create_branch( - name='feature/test', - metadata={'type': 'feature', 'description': 'Test branch'} - ) - self.assertEqual(branch.name, 'feature/test') - self.assertEqual(branch.metadata['type'], 'feature') - self.assertTrue(branch.is_active) - - def test_get_active_branches(self): - """Test retrieving only active branches""" - # Create some branches - feature_branch = self.manager.create_branch( - name='feature/active', - metadata={'type': 'feature'} - ) - inactive_branch = self.manager.create_branch( - name='feature/inactive', - metadata={'type': 'feature'} - ) - inactive_branch.is_active = False - inactive_branch.save() - - active_branches = self.manager.get_active_branches() - self.assertIn(self.main_branch, active_branches) - self.assertIn(feature_branch, active_branches) - self.assertNotIn(inactive_branch, active_branches) - - def test_get_branch_changes(self): - """Test retrieving changes for a specific branch""" - # Create some changes in different branches - main_change = ChangeSet.objects.create( - branch=self.main_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Main Change'}, - status='applied' - ) - feature_branch = self.manager.create_branch(name='feature/test') - feature_change = ChangeSet.objects.create( - branch=feature_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Feature Change'}, - status='applied' - ) - - main_changes = self.manager.get_branch_changes(self.main_branch) - feature_changes = self.manager.get_branch_changes(feature_branch) - - self.assertIn(main_change, main_changes) - self.assertNotIn(feature_change, main_changes) - self.assertIn(feature_change, feature_changes) - self.assertNotIn(main_change, feature_changes) - - def test_merge_branches(self): - """Test merging changes between branches""" - # Create feature branch with changes - feature_branch = self.manager.create_branch(name='feature/test') - change = ChangeSet.objects.create( - branch=feature_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Updated Name'}, - status='applied' - ) - - # Merge feature branch into main - self.manager.merge_branches( - source_branch=feature_branch, - target_branch=self.main_branch - ) - - # Verify changes were copied to main branch - main_changes = self.manager.get_branch_changes(self.main_branch) - self.assertEqual(main_changes.count(), 1) - merged_change = main_changes.first() - self.assertEqual(merged_change.data, change.data) - - def test_branch_deletion(self): - """Test branch deletion with cleanup""" - feature_branch = self.manager.create_branch(name='feature/delete') - ChangeSet.objects.create( - branch=feature_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Test Change'}, - status='applied' - ) - - # Delete the branch - self.manager.delete_branch(feature_branch) - - # Verify branch and its changes are gone - with self.assertRaises(VersionBranch.DoesNotExist): - VersionBranch.objects.get(name='feature/delete') - self.assertEqual( - ChangeSet.objects.filter(branch=feature_branch).count(), - 0 - ) - -class MergeStrategyTests(TestCase): - def setUp(self): - self.park = Park.objects.create( - name='Test Park', - slug='test-park', - status='OPERATING' - ) - self.content_type = ContentType.objects.get_for_model(Park) - self.main_branch = VersionBranch.objects.create( - name='main', - metadata={'type': 'default_branch'} - ) - self.feature_branch = VersionBranch.objects.create( - name='feature/test', - metadata={'type': 'feature'} - ) - self.merge_strategy = MergeStrategy() - - def test_simple_merge(self): - """Test merging non-conflicting changes""" - # Create changes in feature branch - feature_changes = [ - ChangeSet.objects.create( - branch=self.feature_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'New Name'}, - status='applied', - applied_at=timezone.now() - ), - ChangeSet.objects.create( - branch=self.feature_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'description': 'New Description'}, - status='applied', - applied_at=timezone.now() - ) - ] - - # Perform merge - with transaction.atomic(): - conflicts = self.merge_strategy.merge( - source_branch=self.feature_branch, - target_branch=self.main_branch - ) - - self.assertEqual(conflicts, []) # No conflicts expected - main_changes = ChangeSet.objects.filter(branch=self.main_branch) - self.assertEqual(main_changes.count(), 2) - - def test_conflict_detection(self): - """Test detection of conflicting changes""" - # Create conflicting changes - ChangeSet.objects.create( - branch=self.main_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Main Name'}, - status='applied', - applied_at=timezone.now() - ) - ChangeSet.objects.create( - branch=self.feature_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Feature Name'}, - status='applied', - applied_at=timezone.now() - ) - - # Attempt merge - with transaction.atomic(): - conflicts = self.merge_strategy.merge( - source_branch=self.feature_branch, - target_branch=self.main_branch - ) - - self.assertTrue(conflicts) # Conflicts should be detected - conflict = conflicts[0] - self.assertEqual(conflict['field'], 'name') - self.assertEqual(conflict['target_value'], 'Main Name') - self.assertEqual(conflict['source_value'], 'Feature Name') - - def test_merge_ordering(self): - """Test that changes are merged in the correct order""" - # Create sequential changes - change1 = ChangeSet.objects.create( - branch=self.feature_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'First Change'}, - status='applied', - applied_at=timezone.now() - ) - change2 = ChangeSet.objects.create( - branch=self.feature_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Second Change'}, - status='applied', - applied_at=timezone.now() - ) - - # Perform merge - with transaction.atomic(): - self.merge_strategy.merge( - source_branch=self.feature_branch, - target_branch=self.main_branch - ) - - # Verify changes were merged in order - merged_changes = ChangeSet.objects.filter( - branch=self.main_branch - ).order_by('applied_at') - self.assertEqual( - merged_changes[0].data['name'], - 'First Change' - ) - self.assertEqual( - merged_changes[1].data['name'], - 'Second Change' - ) - - def test_merge_validation(self): - """Test validation of merge operations""" - # Test merging inactive branch - self.feature_branch.is_active = False - self.feature_branch.save() - - with self.assertRaises(ValidationError): - self.merge_strategy.merge( - source_branch=self.feature_branch, - target_branch=self.main_branch - ) - - # Test merging branch into itself - with self.assertRaises(ValidationError): - self.merge_strategy.merge( - source_branch=self.main_branch, - target_branch=self.main_branch - ) \ No newline at end of file diff --git a/history_tracking/tests/test_models.py b/history_tracking/tests/test_models.py deleted file mode 100644 index 66209904..00000000 --- a/history_tracking/tests/test_models.py +++ /dev/null @@ -1,173 +0,0 @@ -from django.test import TestCase -from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ValidationError -from django.utils import timezone - -from history_tracking.models import VersionBranch, ChangeSet -from parks.models import Park - -class VersionBranchTests(TestCase): - def setUp(self): - self.main_branch = VersionBranch.objects.create( - name='main', - metadata={'type': 'default_branch'} - ) - self.feature_branch = VersionBranch.objects.create( - name='feature/new-layout', - metadata={'type': 'feature'} - ) - - def test_branch_creation(self): - """Test that branch creation works with valid data""" - branch = VersionBranch.objects.create( - name='test-branch', - metadata={'type': 'test'} - ) - self.assertEqual(branch.name, 'test-branch') - self.assertEqual(branch.metadata['type'], 'test') - self.assertTrue(branch.is_active) - self.assertIsNotNone(branch.created_at) - - def test_invalid_branch_name(self): - """Test that branch names are properly validated""" - with self.assertRaises(ValidationError): - VersionBranch.objects.create(name='', metadata={}) - - # Test overly long name - with self.assertRaises(ValidationError): - VersionBranch.objects.create( - name='a' * 256, - metadata={} - ) - - def test_branch_deactivation(self): - """Test that branches can be deactivated""" - self.feature_branch.is_active = False - self.feature_branch.save() - - branch = VersionBranch.objects.get(name='feature/new-layout') - self.assertFalse(branch.is_active) - - def test_branch_metadata(self): - """Test that branch metadata can be updated""" - metadata = { - 'type': 'feature', - 'description': 'New layout implementation', - 'owner': 'test-user' - } - self.feature_branch.metadata = metadata - self.feature_branch.save() - - branch = VersionBranch.objects.get(name='feature/new-layout') - self.assertEqual(branch.metadata, metadata) - -class ChangeSetTests(TestCase): - def setUp(self): - self.main_branch = VersionBranch.objects.create( - name='main', - metadata={'type': 'default_branch'} - ) - self.park = Park.objects.create( - name='Test Park', - slug='test-park', - status='OPERATING' - ) - self.content_type = ContentType.objects.get_for_model(Park) - - def test_changeset_creation(self): - """Test that changeset creation works with valid data""" - changeset = ChangeSet.objects.create( - branch=self.main_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Updated Park Name'}, - status='pending', - description='Update park name' - ) - self.assertEqual(changeset.branch, self.main_branch) - self.assertEqual(changeset.content_type, self.content_type) - self.assertEqual(changeset.object_id, self.park.id) - self.assertEqual(changeset.status, 'pending') - - def test_changeset_status_flow(self): - """Test that changeset status transitions work correctly""" - changeset = ChangeSet.objects.create( - branch=self.main_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Updated Park Name'}, - status='pending' - ) - - # Test status transition: pending -> applied - changeset.status = 'applied' - changeset.applied_at = timezone.now() - changeset.save() - - updated_changeset = ChangeSet.objects.get(pk=changeset.pk) - self.assertEqual(updated_changeset.status, 'applied') - self.assertIsNotNone(updated_changeset.applied_at) - - def test_invalid_changeset_status(self): - """Test that invalid changeset statuses are rejected""" - with self.assertRaises(ValidationError): - ChangeSet.objects.create( - branch=self.main_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Updated Park Name'}, - status='invalid_status' - ) - - def test_changeset_validation(self): - """Test that changesets require valid branch and content object""" - # Test missing branch - with self.assertRaises(ValidationError): - ChangeSet.objects.create( - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Updated Park Name'}, - status='pending' - ) - - # Test invalid content object - with self.assertRaises(ValidationError): - ChangeSet.objects.create( - branch=self.main_branch, - content_type=self.content_type, - object_id=99999, # Non-existent object - data={'name': 'Updated Park Name'}, - status='pending' - ) - - def test_changeset_relationship_cascade(self): - """Test that changesets are deleted when branch is deleted""" - changeset = ChangeSet.objects.create( - branch=self.main_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Updated Park Name'}, - status='pending' - ) - - # Delete the branch - self.main_branch.delete() - - # Verify changeset was deleted - with self.assertRaises(ChangeSet.DoesNotExist): - ChangeSet.objects.get(pk=changeset.pk) - - def test_changeset_data_validation(self): - """Test that changeset data must be valid JSON""" - changeset = ChangeSet.objects.create( - branch=self.main_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'valid': 'json_data'}, - status='pending' - ) - - # Test invalid JSON data - with self.assertRaises(ValidationError): - changeset.data = "invalid_json" - changeset.save() \ No newline at end of file diff --git a/history_tracking/tests/test_views.py b/history_tracking/tests/test_views.py deleted file mode 100644 index 7667ea37..00000000 --- a/history_tracking/tests/test_views.py +++ /dev/null @@ -1,223 +0,0 @@ -from django.test import TestCase, Client -from django.urls import reverse -from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType -from django.test import override_settings - -from history_tracking.models import VersionBranch, ChangeSet -from parks.models import Park - -User = get_user_model() - -@override_settings(HTMX_ENABLED=True) -class VersionControlViewsTests(TestCase): - def setUp(self): - self.client = Client() - self.user = User.objects.create_superuser( - username='admin', - email='admin@example.com', - password='testpass123' - ) - self.client.login(username='admin', password='testpass123') - - self.park = Park.objects.create( - name='Test Park', - slug='test-park', - status='OPERATING' - ) - self.content_type = ContentType.objects.get_for_model(Park) - - self.main_branch = VersionBranch.objects.create( - name='main', - metadata={'type': 'default_branch'} - ) - self.feature_branch = VersionBranch.objects.create( - name='feature/test', - metadata={'type': 'feature'} - ) - - def test_version_control_panel(self): - """Test rendering of version control panel""" - response = self.client.get( - reverse('version_control_panel'), - HTTP_HX_REQUEST='true', - HTTP_HX_TARGET='version-control-panel' - ) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed('history_tracking/includes/version_control_ui.html') - self.assertContains(response, 'main') # Should show main branch - self.assertContains(response, 'feature/test') # Should show feature branch - - def test_create_branch(self): - """Test branch creation through view""" - response = self.client.post( - reverse('create_branch'), - { - 'name': 'feature/new', - 'metadata': '{"type": "feature", "description": "New feature"}' - }, - HTTP_HX_REQUEST='true' - ) - self.assertEqual(response.status_code, 200) - self.assertTrue( - VersionBranch.objects.filter(name='feature/new').exists() - ) - self.assertContains(response, 'Branch created successfully') - - def test_switch_branch(self): - """Test switching between branches""" - response = self.client.post( - reverse('switch_branch'), - {'branch_id': self.feature_branch.id}, - HTTP_HX_REQUEST='true' - ) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'Switched to branch') - self.assertContains(response, 'feature/test') - - def test_merge_branch(self): - """Test branch merging through view""" - # Create a change in feature branch - ChangeSet.objects.create( - branch=self.feature_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Updated Name'}, - status='applied' - ) - - response = self.client.post( - reverse('merge_branch'), - { - 'source_branch_id': self.feature_branch.id, - 'target_branch_id': self.main_branch.id - }, - HTTP_HX_REQUEST='true' - ) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'Branch merged successfully') - - # Verify changes were merged - main_changes = ChangeSet.objects.filter(branch=self.main_branch) - self.assertEqual(main_changes.count(), 1) - - def test_merge_conflict_handling(self): - """Test handling of merge conflicts""" - # Create conflicting changes - ChangeSet.objects.create( - branch=self.main_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Main Name'}, - status='applied' - ) - ChangeSet.objects.create( - branch=self.feature_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Feature Name'}, - status='applied' - ) - - response = self.client.post( - reverse('merge_branch'), - { - 'source_branch_id': self.feature_branch.id, - 'target_branch_id': self.main_branch.id - }, - HTTP_HX_REQUEST='true' - ) - self.assertEqual(response.status_code, 409) # Conflict status - self.assertContains(response, 'Merge conflicts detected') - - def test_view_history(self): - """Test viewing version history""" - # Create some changes - change = ChangeSet.objects.create( - branch=self.main_branch, - content_type=self.content_type, - object_id=self.park.id, - data={'name': 'Updated Name'}, - status='applied' - ) - - response = self.client.get( - reverse('version_history', kwargs={'pk': self.park.pk}), - HTTP_HX_REQUEST='true' - ) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'Updated Name') - self.assertContains(response, str(change.created_at)) - - def test_branch_deletion(self): - """Test branch deletion through view""" - response = self.client.post( - reverse('delete_branch'), - {'branch_id': self.feature_branch.id}, - HTTP_HX_REQUEST='true' - ) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'Branch deleted successfully') - self.assertFalse( - VersionBranch.objects.filter(id=self.feature_branch.id).exists() - ) - - def test_unauthorized_access(self): - """Test that unauthorized users cannot access version control""" - self.client.logout() - response = self.client.get( - reverse('version_control_panel'), - HTTP_HX_REQUEST='true' - ) - self.assertEqual(response.status_code, 302) # Redirect to login - - def test_htmx_requirements(self): - """Test that views require HTMX headers""" - # Try without HTMX headers - response = self.client.get(reverse('version_control_panel')) - self.assertEqual(response.status_code, 400) - self.assertContains( - response, - 'This endpoint requires HTMX', - status_code=400 - ) - - def test_branch_validation(self): - """Test branch name validation in views""" - response = self.client.post( - reverse('create_branch'), - { - 'name': 'invalid/branch/name/with/too/many/segments', - 'metadata': '{}' - }, - HTTP_HX_REQUEST='true' - ) - self.assertEqual(response.status_code, 400) - self.assertContains( - response, - 'Invalid branch name', - status_code=400 - ) - - def test_branch_list_update(self): - """Test that branch list updates after operations""" - response = self.client.get( - reverse('branch_list'), - HTTP_HX_REQUEST='true' - ) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'main') - self.assertContains(response, 'feature/test') - - # Create new branch - new_branch = VersionBranch.objects.create( - name='feature/new', - metadata={'type': 'feature'} - ) - - # List should update - response = self.client.get( - reverse('branch_list'), - HTTP_HX_REQUEST='true' - ) - self.assertContains(response, 'feature/new') \ No newline at end of file diff --git a/history_tracking/urls.py b/history_tracking/urls.py deleted file mode 100644 index 33858e7a..00000000 --- a/history_tracking/urls.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.urls import path -from . import views, htmx_views - -app_name = 'history_tracking' - -urlpatterns = [ - # Main page views - path('compare/', - views.version_comparison, - name='version_comparison' - ), - path('approval-status//', - views.approval_status, - name='approval_status' - ), - path('submit-approval//', - views.submit_for_approval, - name='submit_for_approval' - ), - - # HTMX endpoints - path('htmx/comments/get/', - htmx_views.get_comments, - name='get_comments' - ), - path('htmx/comments/preview/', - htmx_views.preview_comment, - name='preview_comment' - ), - path('htmx/comments/add/', - htmx_views.add_comment, - name='add_comment' - ), - path('htmx/approve//', - htmx_views.approve_changes, - name='approve_changes' - ), - path('htmx/notifications//', - htmx_views.approval_notifications, - name='approval_notifications' - ), - path('htmx/comments/replies//', - htmx_views.get_replies, - name='get_replies' - ), - path('htmx/comments/reply-form/', - htmx_views.reply_form, - name='reply_form' - ), -] \ No newline at end of file diff --git a/history_tracking/utils.py b/history_tracking/utils.py deleted file mode 100644 index b2651310..00000000 --- a/history_tracking/utils.py +++ /dev/null @@ -1,149 +0,0 @@ -from typing import Dict, Any, List, Optional, TypeVar, Type, cast -from django.core.exceptions import ValidationError -from .models import VersionBranch, ChangeSet -from django.utils import timezone -from django.contrib.auth import get_user_model -from django.contrib.auth.models import AbstractUser -from django.db.models import Model - -UserModel = TypeVar('UserModel', bound=AbstractUser) -User = cast(Type[UserModel], get_user_model()) - -def _handle_source_target_resolution(change: ChangeSet) -> Dict[str, Any]: - resolved = {} - for record in change.historical_records.all(): - resolved[f"{record.instance_type}_{record.instance_pk}"] = record - return resolved - -def _handle_manual_resolution( - conflict_id: str, - source_change: ChangeSet, - manual_resolutions: Dict[str, str], - user: Optional[UserModel] -) -> Dict[str, Any]: - manual_content = manual_resolutions.get(conflict_id) - if not manual_content: - raise ValidationError(f"Manual resolution missing for conflict {conflict_id}") - - resolved = {} - 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': '~' - } - ) - for field, value in manual_content.items(): - setattr(new_record, field, value) - resolved[f"{new_record.instance_type}_{new_record.instance_pk}"] = new_record - return resolved - -def resolve_conflicts( - source_branch: VersionBranch, - target_branch: VersionBranch, - resolutions: Dict[str, str], - manual_resolutions: Dict[str, str], - user: Optional[UserModel] = 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': - resolved_content.update(_handle_source_target_resolution(source_change)) - elif resolution_type == 'target': - resolved_content.update(_handle_source_target_resolution(target_change)) - elif resolution_type == 'manual': - resolved_content.update(_handle_manual_resolution( - conflict_id, source_change, manual_resolutions, user - )) - - 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' - ) - - 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 cd4346f4..91ea44a2 100644 --- a/history_tracking/views.py +++ b/history_tracking/views.py @@ -1,144 +1,3 @@ -from django.shortcuts import render, get_object_or_404 -from django.contrib.auth.decorators import login_required -from django.core.paginator import Paginator -from django.db import transaction -from django.http import HttpRequest, HttpResponse -from django.utils import timezone -from django.contrib import messages -from django.views.decorators.http import require_http_methods -from django.core.exceptions import PermissionDenied -from typing import Dict, Any +from django.shortcuts import render -from .models import VersionBranch, ChangeSet, VersionTag, HistoricalCommentThread -from .managers import ChangeTracker -from .comparison import ComparisonEngine -from .state_machine import ApprovalStateMachine - -ITEMS_PER_PAGE = 20 - -@login_required -def version_comparison(request: HttpRequest) -> HttpResponse: - """View for comparing different versions""" - versions = VersionTag.objects.all().order_by('-created_at') - - version1_id = request.GET.get('version1') - version2_id = request.GET.get('version2') - page_number = request.GET.get('page', 1) - - diff_result = None - if version1_id and version2_id: - try: - version1 = get_object_or_404(VersionTag, id=version1_id) - version2 = get_object_or_404(VersionTag, id=version2_id) - - # Get comparison results - engine = ComparisonEngine() - diff_result = engine.compute_enhanced_diff(version1, version2) - - # Paginate changes - paginator = Paginator(diff_result['changes'], ITEMS_PER_PAGE) - diff_result['changes'] = paginator.get_page(page_number) - - # Add comments to changes - for change in diff_result['changes']: - anchor_id = change['metadata']['comment_anchor_id'] - change['comments'] = HistoricalCommentThread.objects.filter( - anchor__contains={'id': anchor_id} - ).prefetch_related('comments') - - except Exception as e: - messages.error(request, f"Error comparing versions: {str(e)}") - - context = { - 'versions': versions, - 'selected_version1': version1_id, - 'selected_version2': version2_id, - 'diff_result': diff_result - } - - return render(request, 'history_tracking/version_comparison.html', context) - -@login_required -@require_http_methods(["POST"]) -@transaction.atomic -def submit_for_approval(request: HttpRequest, changeset_id: int) -> HttpResponse: - """Submit a changeset for approval""" - changeset = get_object_or_404(ChangeSet, pk=changeset_id) - - if not request.user.has_perm('history_tracking.submit_for_approval'): - raise PermissionDenied("You don't have permission to submit changes for approval") - - try: - # Initialize approval workflow - state_machine = ApprovalStateMachine(changeset) - stages_config = [ - { - 'name': 'Technical Review', - 'required_roles': ['tech_reviewer'] - }, - { - 'name': 'Final Approval', - 'required_roles': ['approver'] - } - ] - - state_machine.initialize_workflow(stages_config) - changeset.status = 'pending_approval' - changeset.save() - - messages.success(request, "Changes submitted for approval successfully") - - except Exception as e: - messages.error(request, f"Error submitting for approval: {str(e)}") - - return render(request, 'history_tracking/approval_status.html', { - 'changeset': changeset - }) - -@login_required -def approval_status(request: HttpRequest, changeset_id: int) -> HttpResponse: - """View approval status of a changeset""" - changeset = get_object_or_404(ChangeSet, pk=changeset_id) - state_machine = ApprovalStateMachine(changeset) - current_stage = state_machine.get_current_stage() - - context = { - 'changeset': changeset, - 'current_stage': current_stage, - 'can_approve': state_machine.can_user_approve(request.user), - 'pending_approvers': state_machine.get_pending_approvers() - } - - return render(request, 'history_tracking/approval_status.html', context) - -@login_required -@require_http_methods(["POST"]) -@transaction.atomic -def approve_changes(request: HttpRequest, changeset_id: int) -> HttpResponse: - """Submit an approval decision""" - changeset = get_object_or_404(ChangeSet, pk=changeset_id) - state_machine = ApprovalStateMachine(changeset) - - try: - decision = request.POST.get('decision', 'approve') - comment = request.POST.get('comment', '') - stage_id = request.POST.get('stage_id') - - success = state_machine.submit_approval( - user=request.user, - decision=decision, - comment=comment, - stage_id=stage_id - ) - - if success: - messages.success(request, f"Successfully {decision}d changes") - else: - messages.error(request, "Failed to submit approval") - - except Exception as e: - messages.error(request, f"Error processing approval: {str(e)}") - - return render(request, 'history_tracking/approval_status.html', { - 'changeset': changeset - }) +# Create your views here. diff --git a/history_tracking/views_monitoring.py b/history_tracking/views_monitoring.py deleted file mode 100644 index cbf30f34..00000000 --- a/history_tracking/views_monitoring.py +++ /dev/null @@ -1,320 +0,0 @@ -from django.views.generic import TemplateView -from django.contrib.admin.views.decorators import staff_member_required -from django.utils.decorators import method_decorator -from django.utils import timezone -from datetime import timedelta - -from .models import VersionBranch, ChangeSet -from .monitoring import VersionControlMetrics - -@method_decorator(staff_member_required, name='dispatch') -class MonitoringDashboardView(TemplateView): - template_name = 'history_tracking/monitoring_dashboard.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - metrics = self._collect_metrics() - context['metrics'] = metrics - return context - - def _collect_metrics(self): - """Collect all monitoring metrics""" - # Collect basic statistics - total_branches = VersionBranch.objects.count() - active_branches = VersionBranch.objects.filter(is_active=True).count() - total_changes = ChangeSet.objects.count() - pending_changes = ChangeSet.objects.filter(status='pending').count() - - # Calculate merge success rate - last_week = timezone.now() - timedelta(days=7) - total_merges = ChangeSet.objects.filter( - created_at__gte=last_week, - status__in=['applied', 'conflict'] - ).count() - successful_merges = ChangeSet.objects.filter( - created_at__gte=last_week, - status='applied' - ).count() - merge_success_rate = round( - (successful_merges / total_merges * 100) if total_merges > 0 else 100 - ) - - # Get performance metrics - VersionControlMetrics.collect_performance_metrics() - perf_metrics = self._get_performance_metrics() - - # Get error tracking data - errors = self._get_error_tracking() - - # Get user activity - user_activity = self._get_user_activity() - - return { - # System Overview - 'total_branches': total_branches, - 'active_branches': active_branches, - 'total_changes': total_changes, - 'pending_changes': pending_changes, - 'merge_success_rate': merge_success_rate, - 'conflicted_merges': ChangeSet.objects.filter( - status='conflict' - ).count(), - 'system_health': self._calculate_system_health(), - 'health_checks': 5, # Number of health checks performed - - # Performance Metrics - 'timing': perf_metrics['timing'], - 'database': perf_metrics['database'], - 'cache': perf_metrics['cache'], - - # Error Tracking - 'errors': errors, - - # User Activity - 'current_operations': user_activity['current'], - 'recent_activity': user_activity['recent'] - } - - def _get_performance_metrics(self): - """Get detailed performance metrics""" - from django.db import connection - from django.core.cache import cache - - # Calculate average operation timings - operation_times = { - 'branch_creation': [], - 'branch_switch': [], - 'merge': [] - } - - for log in self._get_operation_logs(): - if log['operation'] in operation_times: - operation_times[log['operation']].append(log['duration']) - - timing = { - op: round(sum(times) / len(times), 2) if times else 0 - for op, times in operation_times.items() - } - - return { - 'timing': timing, - 'database': { - 'query_count': len(connection.queries), - 'query_time': round( - sum(float(q['time']) for q in connection.queries), - 3 - ), - 'pool_size': connection.pool_size if hasattr(connection, 'pool_size') else 'N/A', - 'max_pool': connection.max_pool if hasattr(connection, 'max_pool') else 'N/A' - }, - 'cache': { - 'hit_rate': round( - cache.get('version_control_cache_hits', 0) / - (cache.get('version_control_cache_hits', 0) + - cache.get('version_control_cache_misses', 1)) * 100, - 1 - ), - 'miss_rate': round( - cache.get('version_control_cache_misses', 0) / - (cache.get('version_control_cache_hits', 0) + - cache.get('version_control_cache_misses', 1)) * 100, - 1 - ), - 'memory_usage': round( - cache.get('version_control_memory_usage', 0) / 1024 / 1024, - 2 - ) - } - } - - def _get_error_tracking(self): - """Get recent error tracking data""" - from django.conf import settings - import logging - - logger = logging.getLogger('version_control') - errors = [] - - # Get last 10 error logs - if hasattr(logger, 'handlers'): - for handler in logger.handlers: - if isinstance(handler, logging.FileHandler): - try: - with open(handler.baseFilename, 'r') as f: - for line in f.readlines()[-10:]: - if '[ERROR]' in line: - errors.append(self._parse_error_log(line)) - except FileNotFoundError: - pass - - return errors - - def _parse_error_log(self, log_line): - """Parse error log line into structured data""" - import re - from datetime import datetime - - pattern = r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) \[ERROR\] (.*)' - match = re.match(pattern, log_line) - - if match: - timestamp_str, message = match.groups() - return { - 'timestamp': datetime.strptime( - timestamp_str, - '%Y-%m-%d %H:%M:%S,%f' - ), - 'type': 'Error', - 'operation': self._extract_operation(message), - 'message': message, - 'resolved': False - } - return None - - def _extract_operation(self, message): - """Extract operation type from error message""" - if 'branch' in message.lower(): - return 'Branch Operation' - elif 'merge' in message.lower(): - return 'Merge Operation' - elif 'changeset' in message.lower(): - return 'Change Operation' - return 'Unknown Operation' - - def _get_user_activity(self): - """Get current and recent user activity""" - from django.contrib.auth import get_user_model - User = get_user_model() - - # Get active sessions - from django.contrib.sessions.models import Session - current_sessions = Session.objects.filter( - expire_date__gte=timezone.now() - ) - - current_operations = [] - for session in current_sessions: - try: - uid = session.get_decoded().get('_auth_user_id') - if uid: - user = User.objects.get(pk=uid) - current_operations.append({ - 'user': user.username, - 'action': self._get_user_current_action(user) - }) - except (User.DoesNotExist, KeyError): - continue - - # Get recent activity - recent = ChangeSet.objects.select_related('user').order_by( - '-created_at' - )[:10] - recent_activity = [ - { - 'user': change.user.username if change.user else 'System', - 'action': self._get_change_action(change), - 'timestamp': change.created_at - } - for change in recent - ] - - return { - 'current': current_operations, - 'recent': recent_activity - } - - def _get_user_current_action(self, user): - """Get user's current action based on recent activity""" - last_change = ChangeSet.objects.filter( - user=user - ).order_by('-created_at').first() - - if last_change: - if (timezone.now() - last_change.created_at).seconds < 300: # 5 minutes - return self._get_change_action(last_change) - return 'Viewing' - - def _get_change_action(self, change): - """Get human-readable action from change""" - if change.status == 'applied': - return f'Applied changes to {change.content_object}' - elif change.status == 'pending': - return f'Started editing {change.content_object}' - elif change.status == 'conflict': - return f'Resolving conflicts on {change.content_object}' - return 'Unknown action' - - def _calculate_system_health(self): - """Calculate overall system health percentage""" - factors = { - 'merge_success': self._get_merge_success_health(), - 'performance': self._get_performance_health(), - 'error_rate': self._get_error_rate_health() - } - return round(sum(factors.values()) / len(factors)) - - def _get_merge_success_health(self): - """Calculate health based on merge success rate""" - last_week = timezone.now() - timedelta(days=7) - total_merges = ChangeSet.objects.filter( - created_at__gte=last_week, - status__in=['applied', 'conflict'] - ).count() - successful_merges = ChangeSet.objects.filter( - created_at__gte=last_week, - status='applied' - ).count() - - if total_merges == 0: - return 100 - return round((successful_merges / total_merges) * 100) - - def _get_performance_health(self): - """Calculate health based on performance metrics""" - metrics = self._get_performance_metrics() - - factors = [ - 100 if metrics['timing']['merge'] < 1000 else 50, # Under 1 second is healthy - 100 if metrics['cache']['hit_rate'] > 80 else 50, # Over 80% cache hit rate is healthy - 100 if metrics['database']['query_time'] < 0.5 else 50 # Under 0.5s query time is healthy - ] - - return round(sum(factors) / len(factors)) - - def _get_error_rate_health(self): - """Calculate health based on error rate""" - last_day = timezone.now() - timedelta(days=1) - total_operations = ChangeSet.objects.filter( - created_at__gte=last_day - ).count() - error_count = len([ - e for e in self._get_error_tracking() - if e['timestamp'] >= last_day - ]) - - if total_operations == 0: - return 100 - error_rate = (error_count / total_operations) * 100 - return round(100 - error_rate) - - def _get_operation_logs(self): - """Get operation timing logs""" - import json - from pathlib import Path - - log_file = Path('logs/version_control_timing.log') - if not log_file.exists(): - return [] - - logs = [] - try: - with open(log_file, 'r') as f: - for line in f: - try: - logs.append(json.loads(line)) - except json.JSONDecodeError: - continue - except Exception: - return [] - - return logs \ No newline at end of file diff --git a/location/mixins.py b/location/mixins.py deleted file mode 100644 index 70c38928..00000000 --- a/location/mixins.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.contrib.contenttypes.models import ContentType -from django.db.models import QuerySet -from typing import Optional, Tuple - -class LocationMixin: - """Mixin for models that can have location data attached.""" - - def get_location(self) -> Optional['Location']: - """Get location for this instance.""" - from location.models import Location - ct = ContentType.objects.get_for_model(self.__class__) - return Location.objects.filter(content_type=ct, object_id=self.pk).first() - - def set_location(self, address: str, latitude: float, longitude: float) -> 'Location': - """Set or update location for this instance.""" - from location.models import Location - ct = ContentType.objects.get_for_model(self.__class__) - location, created = Location.objects.update_or_create( - content_type=ct, - object_id=self.pk, - defaults={ - 'address': address, - 'latitude': latitude, - 'longitude': longitude - } - ) - return location - - @property - def coordinates(self) -> Optional[Tuple[float, float]]: - """Get coordinates (latitude, longitude) if available.""" - location = self.get_location() - if location: - return location.latitude, location.longitude - return None - - @property - def formatted_location(self) -> str: - """Get formatted address string if available.""" - location = self.get_location() - if location: - return location.get_formatted_address() - return "" \ No newline at end of file diff --git a/media/admin.py b/media/admin.py index 07f4337a..17f3066a 100644 --- a/media/admin.py +++ b/media/admin.py @@ -1,28 +1,19 @@ from django.contrib import admin -from django.contrib.contenttypes.admin import GenericStackedInline +from django.utils.html import format_html from .models import Photo -class PhotoInline(GenericStackedInline): - """Inline admin for photos that can be added to any model.""" - model = Photo - extra = 1 - fields = ('image', 'caption', 'alt_text', 'is_primary') - classes = ('collapse',) - @admin.register(Photo) class PhotoAdmin(admin.ModelAdmin): - list_display = ('caption', 'content_type', 'object_id', 'is_primary', 'created_at') - list_filter = ('content_type', 'created_at', 'is_primary', 'is_approved') + list_display = ('thumbnail_preview', 'content_type', 'content_object', 'caption', 'is_primary', 'created_at') + list_filter = ('content_type', 'is_primary', 'created_at') search_fields = ('caption', 'alt_text') - ordering = ('content_type', 'object_id', '-is_primary') - readonly_fields = ('created_at', 'updated_at') + readonly_fields = ('thumbnail_preview',) - fieldsets = ( - ('Image', { - 'fields': ('image', 'caption', 'alt_text', 'is_primary', 'is_approved') - }), - ('Metadata', { - 'fields': ('content_type', 'object_id', 'created_at', 'updated_at'), - 'classes': ('collapse',) - }), - ) + def thumbnail_preview(self, obj): + if obj.image: + return format_html( + '', + obj.image.url + ) + return "No image" + thumbnail_preview.short_description = 'Thumbnail' diff --git a/media/mixins.py b/media/mixins.py deleted file mode 100644 index 7cd82b41..00000000 --- a/media/mixins.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.contrib.contenttypes.models import ContentType -from django.db.models import QuerySet - -class PhotoableModel: - """Mixin for models that can have photos attached.""" - - def get_photos(self) -> QuerySet: - """Get photos for this instance.""" - from media.models import Photo - ct = ContentType.objects.get_for_model(self.__class__) - return Photo.objects.filter(content_type=ct, object_id=self.pk) - - def add_photo(self, photo: 'Photo') -> None: - """Add a photo to this instance.""" - from media.models import Photo - ct = ContentType.objects.get_for_model(self.__class__) - photo.content_type = ct - photo.object_id = self.pk - photo.save() \ No newline at end of file diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index 9c43b9d1..c1321a30 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -1,74 +1,146 @@ -# Comment System Architecture Fix +# Active Context -## Required Code Modifications +## Current Project State -### 1. Central CommentThread Model (comments/models.py) -```python -from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation -from django.contrib.contenttypes.models import ContentType -from django.db import models +### Active Components +- Django backend with core apps + - accounts + - analytics + - companies + - core + - designers + - email_service + - history_tracking + - location + - media + - moderation + - parks + - reviews + - rides -class CommentThread(models.Model): - """Centralized comment threading system""" - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - object_id = models.PositiveIntegerField() - content_object = GenericForeignKey() - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) +### Implementation Status +1. Backend Framework + - ✅ Django setup + - ✅ Database models + - ✅ Authentication system + - ✅ Admin interface - class Meta: - indexes = [ - models.Index(fields=["content_type", "object_id"]), - ] - app_label = 'comments' -``` +2. Frontend Integration + - ✅ HTMX integration + - ✅ AlpineJS setup + - ✅ Tailwind CSS configuration -### 2. Model Reference Updates (Example for companies/models.py) -```python -# In all affected models (companies, rides, parks, reviews): -from comments.models import CommentThread +3. Core Features + - ✅ User authentication + - ✅ Park management + - ✅ Ride tracking + - ✅ Review system + - ✅ Location services + - ✅ Media handling -class Company(models.Model): - # ... existing fields ... - comments = GenericRelation(CommentThread) # Updated reference -``` +## Current Focus Areas -### 3. Historical Records Adjustment -```python -# Historical model definitions: -class HistoricalCompany(HistoricalRecords): - comments = models.ForeignKey( - 'comments.CommentThread', # Unified reference - on_delete=models.SET_NULL, - null=True, - blank=True - ) -``` +### Active Development +1. Content Management + - Moderation workflow refinement + - Content quality metrics + - User contribution tracking -## Migration Execution Plan +2. User Experience + - Frontend performance optimization + - UI/UX improvements + - Responsive design enhancements -1. Generate initial comment thread migration: -```bash -./manage.py makemigrations comments --name create_commentthread -``` +3. System Reliability + - Error handling improvements + - Testing coverage + - Performance monitoring -2. Create dependent migrations for each modified app: -```bash -for app in companies rides parks reviews; do - ./manage.py makemigrations $app --name update_comment_references -done -``` +## Immediate Next Steps -3. Migration dependency chain: -```python -# In each app's migration file: -dependencies = [ - ('comments', '0001_create_commentthread'), -] -``` +### Technical Tasks +1. Testing + - [ ] Increase test coverage + - [ ] Implement integration tests + - [ ] Add performance tests -## Validation Checklist -- [ ] Run full test suite: `uv test ./manage.py test` -- [ ] Execute system check: `uv run ./manage.py check --deploy` -- [ ] Verify database schema changes in migration files -- [ ] Confirm admin interface comment relationships +2. Documentation + - [ ] Complete API documentation + - [ ] Update setup guides + - [ ] Document common workflows + +3. Performance + - [ ] Optimize database queries + - [ ] Implement caching strategy + - [ ] Improve asset loading + +### Feature Development +1. Content Quality + - [ ] Enhanced moderation tools + - [ ] Automated content checks + - [ ] Media optimization + +2. User Features + - [ ] Profile enhancements + - [ ] Contribution tracking + - [ ] Notification system + +## Known Issues + +### Backend +1. Performance + - Query optimization needed for large datasets + - Caching implementation incomplete + +2. Technical Debt + - Some views need refactoring + - Test coverage gaps + - Documentation updates needed + +### 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 diff --git a/memory-bank/evaluations/historical_model_comment_fixes.md b/memory-bank/evaluations/historical_model_comment_fixes.md deleted file mode 100644 index 6d9d8709..00000000 --- a/memory-bank/evaluations/historical_model_comment_fixes.md +++ /dev/null @@ -1,33 +0,0 @@ -# Historical Model Comment Fixes - -## Problem -System check errors occurred because historical models referenced CommentThread in their own app context (e.g. `companies.commentthread`) instead of the actual `comments.CommentThread` model. - -## Solution -Added `excluded_fields = ['comments']` to Meta classes of all affected models to exclude comment relationships from historical tracking. Note: Initially tried `history_exclude` but this was incorrect - django-simple-history uses `excluded_fields`. - -## Affected Models (Fixed) -- Company (companies/models.py) -- Manufacturer (companies/models.py) -- Designer (companies/models.py) -- Park (parks/models.py) -- ParkArea (parks/models.py) -- Ride (rides/models.py) -- RideModel (rides/models.py) -- Review (reviews/models.py) - -## Implementation Details -Each model's Meta class was updated to exclude the comments field from historical tracking: - -```python -class Meta: - # ... other Meta options ... - excluded_fields = ['comments'] # Exclude from historical tracking -``` - -This prevents django-simple-history from attempting to track the GenericRelation field in historical models, which was causing the system check errors. - -## Verification -Run system checks to verify fix: -```bash -python manage.py check \ No newline at end of file diff --git a/memory-bank/evaluations/version_control_evaluation.md b/memory-bank/evaluations/version_control_evaluation.md deleted file mode 100644 index b75908f1..00000000 --- a/memory-bank/evaluations/version_control_evaluation.md +++ /dev/null @@ -1,123 +0,0 @@ -# Version Control System Evaluation - -## Overview -Comprehensive evaluation of the project's version control implementation conducted on 2025-02-07. - -## Core Architecture Assessment - -### Strengths -- Well-structured modular design with clear separation of concerns -- Robust history tracking using Django's HistoricalRecords -- Comprehensive branch and changeset management -- Built-in comment threading and review system -- Strong monitoring and metrics collection - -### Data Model Design - -#### Core Models -- `HistoricalModel` (Abstract base) -- `VersionBranch` (Branch management) -- `VersionTag` (Version tagging) -- `ChangeSet` (Atomic changes) -- `CommentThread` & `Comment` (Review system) - -#### Relationships -✅ Properly structured relationships between models -✅ Effective use of GenericForeignKey for flexibility -✅ Clear handling of model history - -## Implementation Analysis - -### Version Control Features -1. Branching System - - ✅ Branch hierarchy with parent-child relationships - - ✅ Branch metadata and activity tracking - - ✅ Lock management for concurrent access - -2. Change Tracking - - ✅ Atomic changesets with approval workflow - - ✅ Detailed change metadata - - ✅ Dependency tracking - - ✅ Revert capabilities - -3. Review System - - ✅ Threaded comments with mentions - - ✅ Line-specific annotations - - ✅ Resolution tracking - -### Monitoring & Performance -- Comprehensive metrics collection -- Performance tracking for operations -- Database query monitoring -- Cache performance tracking -- Structured logging with Sentry integration - -## Areas for Improvement - -### 1. Performance Optimizations -- Consider implementing batch processing for large changesets -- Add caching for frequently accessed version history -- Optimize query patterns for large history sets - -### 2. Feature Enhancements -- Add support for cherry-picking changes between branches -- Implement automated conflict resolution for simple cases -- Add hooks system for custom version control events - -### 3. Scalability Considerations -- Implement archive strategy for old history records -- Add partitioning support for large history tables -- Consider async processing for heavy operations - -### 4. Maintenance Recommendations -- Implement automated cleanup for orphaned records -- Add integrity checks for version history -- Enhance monitoring with custom alerts - -## Security Assessment -- ✅ Proper access control in place -- ✅ Branch locking mechanism -- ✅ Audit trail for all operations -- 🔄 Consider adding encryption for sensitive changes - -## Integration Points -- Well-integrated with Django's ORM -- Clean API endpoints for version control operations -- Frontend integration through structured responses -- Monitoring integration with external services - -## Recommendations - -### Short Term -1. Implement batch processing for large changesets -2. Add caching layer for version history -3. Create automated cleanup procedures - -### Medium Term -1. Develop cherry-picking functionality -2. Implement automated conflict resolution -3. Add versioning hooks system - -### Long Term -1. Implement archiving strategy -2. Add partitioning support -3. Enhance async processing capabilities - -## Maintainability - -### Documentation -- ✅ Well-documented API -- ✅ Comprehensive user guide -- ✅ Clear technical documentation -- 🔄 Consider adding more code examples - -### Testing -- ✅ Unit tests present -- ✅ Integration testing -- 🔄 Add more performance tests -- 🔄 Enhance stress testing - -## Final Assessment -The version control system is well-implemented with robust features and good maintainability. While there are areas for improvement, the core functionality is solid and provides a strong foundation for future enhancements. - -Overall Rating: ⭐⭐⭐⭐☆ (4/5) \ No newline at end of file diff --git a/memory-bank/features/moderation/implementation.md b/memory-bank/features/moderation/implementation.md index c1247df9..9140300b 100644 --- a/memory-bank/features/moderation/implementation.md +++ b/memory-bank/features/moderation/implementation.md @@ -57,16 +57,6 @@ - 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 @@ -76,9 +66,6 @@ - Loading states and error handling - Filter functionality - Form submissions and validation -- Location selection and mapping -- Dark mode transitions -- Toast notifications ### Browser Support - Chrome 90+ @@ -86,17 +73,6 @@ - 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 @@ -125,4 +101,15 @@ - Update user guide with new features - Add keyboard shortcut documentation - Update accessibility guidelines -- Add performance benchmarks \ No newline at end of file +- 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 diff --git a/memory-bank/features/version-control/README.md b/memory-bank/features/version-control/README.md deleted file mode 100644 index 699f0d38..00000000 --- a/memory-bank/features/version-control/README.md +++ /dev/null @@ -1,177 +0,0 @@ -# Version Control Feature - -## Strategic Overview - -### Purpose -The version control system provides comprehensive content versioning, branching, and merging capabilities across ThrillWiki's models, enabling parallel content development and safe experimentation. - -### Key Decisions - -#### 1. Infrastructure Integration -- **Decision**: Leverage existing Django database and Redis infrastructure -- **Rationale**: - - Reduces operational complexity - - Maintains consistent data storage patterns - - Utilizes existing backup and monitoring systems -- **Impact**: Simplified deployment and maintenance - -#### 2. Architecture Pattern -- **Decision**: Implement as a Django app (history_tracking) -- **Rationale**: - - Follows Django's modular architecture - - Enables easy integration with other apps - - Maintains consistent development patterns -- **Impact**: Clean separation of concerns and reusability - -#### 3. Performance Strategy -- **Decision**: Built-in batch processing and caching -- **Rationale**: - - Handles large-scale content changes efficiently - - Optimizes frequently accessed version history - - Reduces database load -- **Impact**: Scales well with growing content and user base - -### Technical Integration - -#### Database Layer -- Uses existing PostgreSQL database -- Creates dedicated version control tables -- Integrates with Django's ORM -- Maintains data consistency through transactions - -#### Caching Layer -- Uses existing Redis infrastructure -- Dedicated cache prefixes (vc_*) -- Configurable cache durations -- Automatic cache invalidation - -#### Application Layer -- Modular Django app design -- HTMX integration for UI updates -- AlpineJS for client-side interactions -- Tailwind CSS for styling - -## Implementation Details - -### Core Components -1. Models - - HistoricalModel (base class) - - VersionBranch (branch management) - - ChangeSet (atomic changes) - - CommentThread (review system) - -2. Features - - Branch management - - Change tracking - - Merge operations - - Review system - - Performance monitoring - -3. Integration Points - - Model versioning - - Template components - - API endpoints - - Admin interface - -### Usage Patterns - -#### Model Integration -```python -class YourModel(HistoricalModel): - # Automatic version control capabilities - pass -``` - -#### Branch Management -```python -with branch_context(branch): - # Changes tracked in specific branch - model.save() -``` - -#### Batch Operations -```python -with BatchOperation() as batch: - # Efficient handling of multiple changes - batch.process_changes(changes) -``` - -## Development Guidelines - -### Best Practices -1. Use batch operations for multiple changes -2. Implement proper branch management -3. Handle merge conflicts explicitly -4. Monitor performance metrics -5. Cache frequently accessed data - -### Anti-Patterns to Avoid -1. Direct model changes outside branch context -2. Inefficient querying of version history -3. Ignoring batch operations for bulk changes -4. Manual cache management - -## Monitoring and Maintenance - -### Performance Monitoring -- Operation timing metrics -- Cache hit rates -- Database query patterns -- Memory usage -- API response times - -### Health Checks -- Branch integrity -- Cache consistency -- Database indexes -- Query performance -- System resources - -## Future Considerations - -### Planned Enhancements -1. Advanced conflict resolution -2. Enhanced performance monitoring -3. Additional caching strategies -4. Improved UI components - -### Scalability Path -1. Partition strategies for large histories -2. Advanced caching patterns -3. Async operation handling -4. Archive management - -## Documentation Map - -### Technical Documentation -- Implementation Guide: `history_tracking/README.md` -- API Documentation: `docs/version_control_api.md` -- User Guide: `docs/version_control_user_guide.md` - -### Architecture Documentation -- Technical Context: `memory-bank/techContext.md` -- System Patterns: `memory-bank/systemPatterns.md` -- Evaluation Report: `memory-bank/evaluations/version_control_evaluation.md` - -## Support and Maintenance - -### Common Issues -1. Cache invalidation -2. Merge conflicts -3. Performance optimization -4. Data consistency - -### Resolution Steps -1. Monitor system metrics -2. Review error logs -3. Check cache status -4. Verify database integrity - -## Integration Status -✅ Database Integration -✅ Redis Configuration -✅ Model Integration -✅ UI Components -✅ API Endpoints -✅ Documentation -✅ Monitoring Setup \ No newline at end of file diff --git a/memory-bank/features/version-control/approval-workflow.md b/memory-bank/features/version-control/approval-workflow.md deleted file mode 100644 index 8b5e0967..00000000 --- a/memory-bank/features/version-control/approval-workflow.md +++ /dev/null @@ -1,47 +0,0 @@ -# Change Approval Workflow Implementation Plan - - ## Core Requirements - 1. Configurable approval stages - 2. Role-based reviewer assignments - 3. Parallel vs sequential approvals - 4. Audit trail of decisions - 5. Integration with existing locks/comments - - ## Technical Integration - - **State Machine** - Extend StateMachine interface: - ```typescript - interface ApprovalStateMachine extends StateMachine { - currentStage: ApprovalStage; - requiredApprovers: UserRef[]; - overridePolicy: 'majority' | 'unanimous'; - } - ``` - - - **Model Extensions** - Enhance ChangeSet (line 7): - ```python - class ChangeSet(models.Model): - approval_state = models.JSONField(default=list) # [{stage: 1, approvers: [...]}] - approval_history = models.JSONField(default=list) - ``` - - - **API Endpoints** - Add to VersionControlViewSet (line 128): - ```python - @action(detail=True, methods=['post']) - def submit_for_approval(self, request, pk=None): - """Transition change set to approval state""" - ``` - - ## Security Considerations - - Approval chain validation - - Non-repudiation requirements - - Conflict resolution protocols - - Approval delegation safeguards - - ## Phase Plan - 1. **Week 1**: State machine implementation - 2. **Week 2**: Approval UI components - 3. **Week 3**: Integration testing - 4. **Week 4**: Deployment safeguards \ No newline at end of file diff --git a/memory-bank/features/version-control/branch-locking.md b/memory-bank/features/version-control/branch-locking.md deleted file mode 100644 index a1d0bb32..00000000 --- a/memory-bank/features/version-control/branch-locking.md +++ /dev/null @@ -1,50 +0,0 @@ -# Branch Locking System Implementation Plan - - ## Core Requirements - 1. Role-based locking permissions - 2. Lock state indicators in UI - 3. Lock override protocols - 4. Audit logging for lock events - 5. Maximum lock duration: 48hrs - - ## Technical Integration - - **Model Extensions** - Enhance `VersionBranch` (line 14): - ```python - class VersionBranch(models.Model): - lock_status = models.JSONField(default=dict) # {user: ID, expires: datetime} - lock_history = models.JSONField(default=list) - ``` - - - **Manager Methods** - Add to `BranchManager` (line 141): - ```python - def acquire_lock(self, branch, user, duration=48): - """Implements lock with timeout""" - - def release_lock(self, branch, force=False): - """Handles lock release with permission checks""" - ``` - - - **UI Components** - Update `VersionControlUI` interface (line 58): - ```typescript - lockState: { - isLocked: boolean; - lockedBy: UserRef; - expiresAt: Date; - canOverride: boolean; - }; - ``` - - ## Security Considerations - - Permission escalation prevention - - Lock expiration enforcement - - Audit log integrity checks - - Session validation for lock holders - - ## Phase Plan - 1. **Week 1**: Locking backend implementation - 2. **Week 2**: Permission system integration - 3. **Week 3**: UI indicators & controls - 4. **Week 4**: Audit system & testing \ No newline at end of file diff --git a/memory-bank/features/version-control/change-comments.md b/memory-bank/features/version-control/change-comments.md deleted file mode 100644 index ed803372..00000000 --- a/memory-bank/features/version-control/change-comments.md +++ /dev/null @@ -1,52 +0,0 @@ -# Change Commenting System Implementation Plan - - ## Core Requirements - 1. Threaded comment conversations - 2. @mention functionality - 3. File/line anchoring - 4. Notification system - 5. Comment resolution tracking - - ## Technical Integration - - **Model Relationships** - Extend `HistoricalRecord` (line 31): - ```python - class HistoricalRecord(models.Model): - comments = GenericRelation('CommentThread') # Enables change comments - ``` - - - **Collaboration System** - Enhance interface (line 85): - ```typescript - interface CollaborationSystem { - createCommentThread( - changeId: string, - anchor: LineRange, - initialComment: string - ): Promise; - } - ``` - - - **UI Components** - New `InlineCommentPanel` component: - ```typescript - interface CommentProps { - thread: CommentThread; - canResolve: boolean; - onReply: (content: string) => void; - onResolve: () => void; - } - ``` - - ## Notification Matrix - | Event Type | Notification Channel | Escalation Path | - |------------|----------------------|-----------------| - | New comment | In-app, Email | After 24hrs → Slack DM | - | @mention | Mobile push, Email | After 12hrs → SMS | - | Resolution | In-app | None | - - ## Phase Plan - 1. **Week 1**: Comment storage infrastructure - 2. **Week 2**: Anchoring system & UI - 3. **Week 3**: Notification pipeline - 4. **Week 4**: Moderation tools & audit \ 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 deleted file mode 100644 index 2d7f9b23..00000000 --- a/memory-bank/features/version-control/implementation-plan.md +++ /dev/null @@ -1,292 +0,0 @@ -# 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-sequence.md b/memory-bank/features/version-control/implementation-sequence.md deleted file mode 100644 index 76eb5c09..00000000 --- a/memory-bank/features/version-control/implementation-sequence.md +++ /dev/null @@ -1,22 +0,0 @@ -## Critical Implementation Revisions - -### Phase 1.1: Core Model Updates (2 Days) -1. Add lock fields to VersionBranch -2. Implement StateMachine base class -3. Extend HistoricalChangeMixin with structured diffs - -### Phase 2.1: Manager Classes (3 Days) -```python -class LockManager(models.Manager): - def get_locked_branches(self): - return self.filter(lock_status__isnull=False) - -class StateMachine: - def __init__(self, workflow): - self.states = workflow['states'] - self.transitions = workflow['transitions'] -``` - -### Phase 3.1: Security Backports (1 Day) -- Add model clean() validation -- Implement permission check decorators \ 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 deleted file mode 100644 index e4621355..00000000 --- a/memory-bank/features/version-control/implementation-status.md +++ /dev/null @@ -1,114 +0,0 @@ -# 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/implementation_checklist.md b/memory-bank/features/version-control/implementation_checklist.md deleted file mode 100644 index fcadd5c0..00000000 --- a/memory-bank/features/version-control/implementation_checklist.md +++ /dev/null @@ -1,43 +0,0 @@ -# Version Control System Implementation Checklist - -## Core Implementation ✓ -- [x] Models - - [x] VersionBranch - - [x] VersionTag - - [x] ChangeSet - - [x] Generic relationships for flexibility - -- [x] Managers - - [x] BranchManager - - [x] ChangeTracker - - [x] MergeStrategy - -- [x] UI Components - - [x] Version Control Panel - - [x] Branch List - - [x] History View - - [x] Merge Panel - - [x] Branch Creation Form - -## Future Enhancements -- [ ] Add visual diff viewer - [See Visual Diff Viewer Plan](visual-diff-viewer.md) -- [ ] Implement branch locking - [See Branch Locking System](branch-locking.md) -- [ ] Add commenting on changes - [See Change Comments Framework](change-comments.md) -- [ ] Create change approval workflow - [See Approval Workflow Docs](approval-workflow.md) -- [ ] Add version comparison tool - [See Comparison Tool Spec](version-comparison.md) - -## Documentation Updates ✓ -- [x] README creation -- [x] Implementation guide -- [x] Template integration guide -- [x] API documentation -- [x] User guide - -## Testing Requirements ✓ -- [x] Unit Tests -- [x] Integration Tests -- [x] UI Tests - -## Monitoring Setup ✓ -- [x] Performance Metrics -- [x] Error Tracking \ No newline at end of file diff --git a/memory-bank/features/version-control/integration-matrix.md b/memory-bank/features/version-control/integration-matrix.md deleted file mode 100644 index bfeeabc5..00000000 --- a/memory-bank/features/version-control/integration-matrix.md +++ /dev/null @@ -1,14 +0,0 @@ -# Version Control Feature Integration Matrix - - | Feature | Depends On | Provides To | Shared Components | - |---------|------------|-------------|-------------------| - | Visual Diff Viewer | Version Comparison | Branch Locking | DiffEngine, LineMapper | - | Branch Locking | Approval Workflow | Change Comments | LockManager, AuditLogger | - | Change Comments | Visual Diff Viewer | Approval Workflow | CommentStore, @MentionService | - | Approval Workflow | Branch Locking | Version Comparison | StateMachine, Notifier | - | Version Comparison | All Features | - | TimelineRenderer, DiffAnalyzer | - - ## Critical Integration Points - - Lock status visibility in diff viewer (Line 14 ↔ Line 58) - - Comment threads in approval decisions (Line 31 ↔ Line 85) - - Comparison metadata for rollback safety (Line 6 ↔ Line 128) \ 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 deleted file mode 100644 index 000541d7..00000000 --- a/memory-bank/features/version-control/technical-guide.md +++ /dev/null @@ -1,325 +0,0 @@ -# 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/memory-bank/features/version-control/template_integration.md b/memory-bank/features/version-control/template_integration.md deleted file mode 100644 index 4e8c796f..00000000 --- a/memory-bank/features/version-control/template_integration.md +++ /dev/null @@ -1,86 +0,0 @@ -# Version Control UI Template Integration - -## Templates Requiring VCS Integration - -### Park System -- [x] parks/templates/parks/park_detail.html - Completed -- [ ] parks/templates/parks/park_list.html - Add version status indicators -- [ ] parks/templates/parks/park_area_detail.html - Add version control UI - -### Rides System -- [ ] rides/templates/rides/ride_detail.html - Add version control UI -- [ ] rides/templates/rides/ride_list.html - Add version status indicators - -### Reviews System -- [ ] reviews/templates/reviews/review_detail.html - Add version control UI -- [ ] reviews/templates/reviews/review_list.html - Add version status indicators - -### Company System -- [ ] companies/templates/companies/company_detail.html - Add version control UI -- [ ] companies/templates/companies/company_list.html - Add version status indicators - -## Integration Guidelines - -### Detail Templates -For detail templates, add the version control UI below the main title: - -```html - -

{{ object.name }}

- - -{% include "history_tracking/includes/version_control_ui.html" %} - - -``` - -### List Templates -For list templates, add version indicators in the list items: - -```html -{% for item in object_list %} -
-

{{ item.name }}

- {% if version_control.vcs_enabled %} -
- Branch: {{ item.get_version_info.current_branch.name }} -
- {% endif %} -
-{% endfor %} -``` - -## Integration Steps - -1. Update base template to include necessary JavaScript -```html - - -``` - -2. Add version control UI to detail views -- Include the version control UI component -- Add branch switching functionality -- Display version history - -3. Add version indicators to list views -- Show current branch -- Indicate if changes are pending -- Show version status - -4. Update view classes -- Ensure models inherit from HistoricalModel -- Add version control context -- Handle branch switching - -5. Test integration -- Verify UI appears correctly -- Test branch switching -- Verify history tracking -- Test merge functionality - -## Next Steps -1. Create park area detail template with version control -2. Update ride detail template -3. Add version control to review system -4. Integrate with company templates \ No newline at end of file diff --git a/memory-bank/features/version-control/type_fixes.md b/memory-bank/features/version-control/type_fixes.md deleted file mode 100644 index 663d0d77..00000000 --- a/memory-bank/features/version-control/type_fixes.md +++ /dev/null @@ -1,90 +0,0 @@ -# Version Control System Type Fixes - -## Completed Fixes - -### 1. managers.py ✓ -- Added proper UserModel TypeVar -- Fixed type hints for User references -- Added missing type imports -- Improved type safety in method signatures - -### 2. utils.py ✓ -- Updated User type hints -- Consistent use of UserModel TypeVar -- Fixed return type annotations -- Added proper type imports - -## Remaining Checks - -### 1. models.py -- [ ] Check User related fields -- [ ] Verify ForeignKey type hints -- [ ] Review manager annotations -- [ ] Check metaclass type hints - -### 2. views.py -- [ ] Verify request.user type hints -- [ ] Check class-based view type hints -- [ ] Review context type hints -- [ ] Check form handling types - -### 3. signals.py -- [ ] Check signal receiver type hints -- [ ] Verify sender type annotations -- [ ] Review instance type hints -- [ ] Check User type usage - -### 4. context_processors.py -- [ ] Verify request type hints -- [ ] Check context dictionary types -- [ ] Review User type usage - -## Type Safety Guidelines - -1. User Type Pattern: -```python -UserModel = TypeVar('UserModel', bound=AbstractUser) -User = cast(Type[UserModel], get_user_model()) - -def my_function(user: Optional[UserModel] = None) -> Any: - pass -``` - -2. Model References: -```python -from django.db.models import Model, QuerySet -from typing import Type, TypeVar - -T = TypeVar('T', bound=Model) - -def get_model(model_class: Type[T]) -> QuerySet[T]: - pass -``` - -3. Generic Views: -```python -from typing import TypeVar, Generic -from django.views.generic import DetailView - -T = TypeVar('T', bound=Model) - -class MyDetailView(DetailView, Generic[T]): - model: Type[T] -``` - -## Next Steps - -1. Audit Remaining Files: -- Review all files for type hint consistency -- Update any deprecated type hint syntax -- Add missing type hints where needed - -2. Type Testing: -- Run mypy checks -- Verify Pylance reports -- Test with strict type checking - -3. Documentation: -- Document type patterns used -- Update technical guide with type hints -- Add type checking to contribution guide \ No newline at end of file diff --git a/memory-bank/features/version-control/ui_improvements.md b/memory-bank/features/version-control/ui_improvements.md deleted file mode 100644 index 0ab8899f..00000000 --- a/memory-bank/features/version-control/ui_improvements.md +++ /dev/null @@ -1,110 +0,0 @@ -# Version Control System UI Improvements - -## Recent Improvements - -### 1. Template Structure Enhancement -- Moved map initialization to dedicated JavaScript file -- Implemented data attribute pattern for passing data to JavaScript -- Improved template organization and maintainability - -### 2. JavaScript Organization -- Created separate `map-init.js` for map functionality -- Established pattern for external JavaScript files -- Improved error handling and script loading - -### 3. Asset Management -```javascript -// Static Asset Organization -/static/ - /js/ - version-control.js // Core VCS functionality - map-init.js // Map initialization logic - /css/ - version-control.css // VCS styles -``` - -## Best Practices Established - -### 1. Data Passing Pattern -```html - -
-
-``` - -### 2. JavaScript Separation -```javascript -// Modular JavaScript organization -document.addEventListener('DOMContentLoaded', function() { - // Initialize components - const mapContainer = document.getElementById('map'); - if (mapContainer) { - // Component-specific logic - } -}); -``` - -### 3. Template Structure -```html -{% block content %} - -{% endblock %} - -{% block extra_js %} - {{ block.super }} - - -{% endblock %} -``` - -## Integration Guidelines - -### 1. Adding New Components -1. Create dedicated JavaScript file in `/static/js/` -2. Use data attributes for configuration -3. Follow established loading pattern -4. Update base template if needed - -### 2. Version Control UI -1. Include version control UI component -2. Add necessary data attributes -3. Ensure proper script loading -4. Follow established patterns - -### 3. Static Asset Management -1. Keep JavaScript files modular -2. Use proper static file organization -3. Follow naming conventions -4. Maintain clear dependencies - -## Next Steps - -1. Apply this pattern to other templates: - - Ride detail template - - Review detail template - - Company detail template - -2. Implement consistent error handling: - ```javascript - function handleError(error) { - console.error('Component error:', error); - // Handle error appropriately - } - ``` - -3. Add performance monitoring: - ```javascript - // Add timing measurements - const startTime = performance.now(); - // Component initialization - const endTime = performance.now(); - console.debug(`Component initialized in ${endTime - startTime}ms`); - ``` - -4. Documentation updates: - - Add JavaScript patterns to technical guide - - Update template integration guide - - Document asset organization \ No newline at end of file diff --git a/memory-bank/features/version-control/version-comparison.md b/memory-bank/features/version-control/version-comparison.md deleted file mode 100644 index 5cdd1dc7..00000000 --- a/memory-bank/features/version-control/version-comparison.md +++ /dev/null @@ -1,47 +0,0 @@ -# Version Comparison Tool Implementation Plan - - ## Core Requirements - 1. Multi-version timeline visualization - 2. Three-way merge preview - 3. Change impact analysis - 4. Rollback capabilities - 5. Performance baseline: <500ms for 100-file diffs - - ## Technical Integration - - **Diff Algorithm** - Enhance visual-diff-viewer.md component (line 10): - ```typescript - interface ComparisonEngine { - compareVersions(versions: string[]): StructuredDiff[]; - calculateImpactScore(diffs: StructuredDiff[]): number; - } - ``` - - - **Model Extensions** - Update VersionTag (line 6): - ```python - class VersionTag(models.Model): - comparison_metadata = models.JSONField(default=dict) # Stores diff stats - ``` - - - **API Endpoints** - Add to VersionControlViewSet (line 128): - ```python - @action(detail=False, methods=['post']) - def bulk_compare(self, request): - """Process multi-version comparisons""" - ``` - - ## Performance Strategy - | Aspect | Solution | Target | - |--------|----------|--------| - | Diff computation | Background workers | 90% async processing | - | Result caching | Redis cache layer | 5min TTL | - | Large files | Chunked processing | 10MB chunks | - | UI rendering | Virtualized scrolling | 60fps maintain | - - ## Phase Plan - 1. **Week 1**: Core comparison algorithm - 2. **Week 2**: Timeline visualization UI - 3. **Week 3**: Performance optimization - 4. **Week 4**: Rollback safety mechanisms \ No newline at end of file diff --git a/memory-bank/features/version-control/visual-diff-viewer.md b/memory-bank/features/version-control/visual-diff-viewer.md deleted file mode 100644 index 2df78c5f..00000000 --- a/memory-bank/features/version-control/visual-diff-viewer.md +++ /dev/null @@ -1,39 +0,0 @@ -# Visual Diff Viewer Implementation Plan - -## Core Requirements -1. Side-by-side comparison interface -2. Syntax highlighting for code diffs -3. Inline comment anchoring -4. Change navigation controls -5. Performance budget: 200ms render time - -## Technical Integration -- **Frontend** - Extend `DiffViewer` component (line 62) with: - ```typescript - interface EnhancedDiffViewer { - renderStrategy: 'inline' | 'side-by-side'; - syntaxHighlighters: Map; - commentThreads: CommentThread[]; - } - ``` - -- **Backend** - Enhance `ChangeTracker.compute_diff()` (line 156): - ```python - def compute_enhanced_diff(self, version1, version2): - """Return structured diff with syntax metadata""" - ``` - -## Dependency Matrix -| Component | Affected Lines | Modification Type | -|-----------|----------------|--------------------| -| HistoricalChangeMixin | Current impl. line 6 | Extension | -| CollaborationSystem | line 90 | Event handling | -| VersionControlUI | line 62 | Props update | - -## Phase Plan -1. **Week 1**: Diff algorithm optimization -2. **Week 2**: UI component development -3. **Week 3**: Performance testing -4. **Week 4**: Security review \ No newline at end of file diff --git a/memory-bank/security/audit-checklist.md b/memory-bank/security/audit-checklist.md deleted file mode 100644 index 25946998..00000000 --- a/memory-bank/security/audit-checklist.md +++ /dev/null @@ -1,53 +0,0 @@ -# Version Control Security Audit Checklist - - ## Core Security Domains - 1. **Authentication** - - [ ] MFA required for lock overrides (Branch Locking.md Line 58) - - [ ] Session invalidation on permission changes - - 2. **Authorization** - - [ ] Role hierarchy enforcement (Approval Workflow.md Line 22) - - [ ] Context-sensitive permission checks - - 3. **Data Protection** - - [ ] Encryption of comparison metadata (Version Comparison.md Line 6) - - [ ] Audit log integrity verification - - 4. **Workflow Security** - - [ ] State machine tamper detection (Approval Workflow.md Line 45) - - [ ] Comment edit history immutability - - ## Threat Mitigation Table - | Threat Type | Affected Feature | Mitigation Strategy | - |-------------|------------------|---------------------| - | Race Conditions | Branch Locking | Optimistic locking with version stamps | - | XSS | Change Comments | DOMPurify integration (Line 89) | - | Data Leakage | Version Comparison | Strict field-level encryption | - | Repudiation | Approval Workflow | Blockchain-style audit trail | - - ## Testing Procedures - 1. **Penetration Tests** - - Lock bypass attempts via API fuzzing - - Approval state injection attacks - - 2. **Static Analysis** - - OWASP ZAP scan configuration - - SonarQube security rule activation - - 3. **Runtime Monitoring** - - Unauthorized diff access alerts - - Abnormal approval pattern detection - - ## Phase Integration - | Development Phase | Security Focus | - |--------------------|----------------| - | Locking Implementation | Permission model validation | - | Workflow Development | State transition auditing | - | Comment System | Content sanitization checks | - | Comparison Tool | Data anonymization tests | - - ## Severity Levels - - **Critical**: Direct system access vulnerabilities - - **High**: Data integrity risks - - **Medium**: UX security weaknesses - - **Low**: Informational exposure \ No newline at end of file diff --git a/memory-bank/security/owasp-mapping.md b/memory-bank/security/owasp-mapping.md deleted file mode 100644 index 0855b137..00000000 --- a/memory-bank/security/owasp-mapping.md +++ /dev/null @@ -1,12 +0,0 @@ -# OWASP Top 10 Compliance Mapping - - | OWASP Item | Our Implementation | Verification Method | - |------------|--------------------|---------------------| - | A01:2021-Broken Access Control | Branch Locking permissions (Line 58) | Penetration testing | - | A03:2021-Injection | Comment sanitization (Line 89) | Static code analysis | - | A05:2021-Security Misconfiguration | Version Tag defaults (Line 6) | Configuration audits | - | A08:2021-Software/Data Integrity Failures | Audit logging (Checklist 3.4) | Checksum verification | - - ## Critical Compliance Gaps - 1. Cryptographic failures (Data at rest encryption) - Scheduled for Phase 3 - 2. Server-side request forgery - Requires API gateway hardening \ No newline at end of file diff --git a/memory-bank/security/test-cases.md b/memory-bank/security/test-cases.md deleted file mode 100644 index 5658ffde..00000000 --- a/memory-bank/security/test-cases.md +++ /dev/null @@ -1,44 +0,0 @@ -# Security Test Case Template - -## Authentication Tests -```gherkin -Scenario: Lock override with expired session - Given an active branch lock - When session expires during override attempt - Then system should reject with 401 Unauthorized - And log security event "LOCK_OVERRIDE_FAILURE" -``` - -## Injection Prevention -```gherkin -Scenario: XSS in change comments - When submitting comment with - Then response should sanitize to "&lt;script&gt;alert(1)&lt;/script&gt;" - And store original input in quarantine -``` - -## Data Integrity -```gherkin -Scenario: Unauthorized diff modification - Given approved version comparison - When altering historical diff metadata - Then checksum validation should fail - And trigger auto-rollback procedure -``` - -## Workflow Security -```gherkin -Scenario: Approval state bypass - Given pending approval workflow - When attempting direct state transition - Then enforce state machine rules - And log "ILLEGAL_STATE_CHANGE" event -``` - -## Monitoring Tests -```gherkin -Scenario: Abnormal approval patterns - Given 10 rapid approvals from same IP - When monitoring system detects anomaly - Then freeze approval process - And notify security team \ No newline at end of file diff --git a/memory-bank/systemPatterns.md b/memory-bank/systemPatterns.md index 6ce29603..63f3ce18 100644 --- a/memory-bank/systemPatterns.md +++ b/memory-bank/systemPatterns.md @@ -21,72 +21,6 @@ - Implement component-based structure - Follow progressive enhancement -## Version Control Patterns - -### Change Management -1. Batch Processing - ```python - class BatchChangeProcessor: - def process_changes(self, changes, chunk_size=100): - """Process changes in efficient batches""" - with transaction.atomic(): - for chunk in chunked_queryset(changes, chunk_size): - self._process_chunk(chunk) - ``` - -2. Caching Strategy - ```python - class VersionCache: - def cache_history(self, instance): - """Cache version history with TTL""" - key = f"version_history_{instance.pk}" - if not cache.get(key): - history = instance.get_history() - cache.set(key, history, timeout=3600) - ``` - -3. Change Tracking - ```python - class ChangeTracker: - def track_changes(self, instance): - """Track changes with metadata""" - return { - 'changes': self._diff_changes(instance), - 'metadata': self._collect_metadata(), - 'performance': self._get_metrics() - } - ``` - -### Performance Optimization - -1. Query Patterns - ```python - class HistoryQuerySet: - def optimized_history(self): - """Optimized history query""" - return self.select_related('branch')\ - .prefetch_related('changes')\ - .defer('large_fields') - ``` - -2. Async Operations - ```python - class AsyncVersionControl: - async def process_large_changes(self): - """Handle large changes asynchronously""" - async with atomic(): - # Async processing logic - ``` - -3. Archiving Strategy - ```python - class HistoryArchiver: - def archive_old_versions(self, age_days=90): - """Archive old version history""" - threshold = timezone.now() - timedelta(days=age_days) - return self._move_to_archive(threshold) - ``` - ## Design Patterns ### Data Access @@ -101,8 +35,6 @@ - Implement model-level caching - Use Redis for session storage - Cache invalidation rules - - Version history caching - - Differential caching for changes ### Frontend Patterns @@ -130,35 +62,6 @@ ``` -## Version Control UI Patterns - -1. Change Visualization - ```html - -
-
-
-
- ``` - -2. Branch Management - ```html - -
- ``` - -3. Merge Resolution - ```html - -
- ``` - ## Authentication Patterns ### User Management @@ -220,25 +123,14 @@ ## Testing Patterns -### Performance Testing +### Unit Tests ```python -class VersionControlPerformanceTests(TestCase): +class ModelTests(TestCase): def setUp(self): - self.large_dataset = self.create_test_data() + # Test setup - def test_batch_processing_performance(self): - start_time = time.time() - self.processor.process_changes(self.large_dataset) - duration = time.time() - start_time - self.assertLess(duration, self.acceptable_threshold) -``` - -### Scale Testing -```python -class ScaleTestCase(TestCase): - def test_version_history_scaling(self): - with self.assertNumQueries(1): # Ensure efficient querying - self.repository.get_history() + def test_specific_functionality(self): + # Test implementation ``` ### Integration Tests @@ -270,10 +162,4 @@ class ViewTests(TestCase): - Code review - Testing verification - Documentation update - - Deployment planning - -4. Performance Review - - Query analysis - - Cache efficiency - - Load testing - - Scalability verification \ No newline at end of file + - Deployment planning \ No newline at end of file diff --git a/memory-bank/techContext.md b/memory-bank/techContext.md index 6221802a..1237ac63 100644 --- a/memory-bank/techContext.md +++ b/memory-bank/techContext.md @@ -5,8 +5,7 @@ ### Stack Components - **Framework**: Django (MVT Architecture) - **Frontend**: HTMX + AlpineJS + Tailwind CSS -- **Database**: PostgreSQL with Django ORM -- **Cache**: Redis for application and version control +- **Database**: Django ORM - **Authentication**: Django Built-in Auth ## Technical Architecture @@ -26,15 +25,6 @@ - Validation rules - Signal handlers - Database migrations - - Version control tracking - -3. Version Control System - - Branching and merging capabilities - - Change tracking with history - - Batch processing operations - - Caching strategy using Redis - - Performance monitoring - - Multi-level model versioning ### Frontend Architecture 1. HTMX Integration @@ -42,14 +32,12 @@ - Partial page renders - Server-side processing - Progressive enhancement - - Version control UI updates 2. AlpineJS Usage - UI state management - Component behaviors - Event handling - DOM manipulation - - Version control interactions 3. Tailwind CSS - Utility-first styling @@ -59,67 +47,32 @@ ## Integration Patterns -### Version Control Integration -1. Model Integration - ```python - class VersionedModel(HistoricalModel): - # Base class for version-controlled models - history = HistoricalRecords() - version_control = VersionControlManager() - ``` - -2. Change Tracking - ```python - # Automatic change tracking - with branch_context(branch): - model.save() # Changes tracked in branch - ``` - -3. Batch Operations - ```python - # Efficient batch processing - with BatchOperation() as batch: - batch.process_changes(changes) - ``` - ### Template System 1. Structure - Base templates - Model-specific partials - Reusable components - Template inheritance - - Version control components 2. HTMX Patterns - Partial updates - Server triggers - Event handling - Response processing - - Version history display ### State Management 1. Server-side - Django sessions - Database state - Cache management - - Version control state - - Branch management 2. Client-side - AlpineJS state - Local storage - HTMX state management - - Version control UI state ## Performance Requirements -### Version Control Performance -- Batch processing for large changes -- Efficient caching with Redis -- Optimized query patterns -- Parallel processing capability -- Monitoring and metrics - ### Frontend Targets - First contentful paint < 1.5s - Time to interactive < 2s @@ -132,25 +85,20 @@ - Caching strategy - Asset optimization - API response times -- Version control overhead management ## Development Environment ### Required Tools -- Python 3.8+ with virtual environment +- Python with virtual environment - Node.js (Tailwind build) - Git version control - VSCode IDE -- Redis 6.0+ -- PostgreSQL 12+ ### Configuration - Environment variables - Development settings - Database setup - Media handling -- Redis configuration -- Version control settings ## Security Framework @@ -159,14 +107,12 @@ - Session management - Permission levels - User roles -- Version control access control ### Data Protection - CSRF protection - XSS prevention - SQL injection prevention - Input validation -- Version history integrity ## Testing Strategy @@ -175,15 +121,12 @@ - Unit tests - Integration tests - Coverage requirements -- Version control tests -- Performance tests ### Frontend Testing - Browser testing - Performance metrics - Accessibility testing - User flow validation -- Version control UI testing ## Deployment Process @@ -192,15 +135,12 @@ - Database migration - Static file handling - SSL/TLS setup -- Redis setup -- Version control initialization ### Monitoring - Error tracking - Performance monitoring - User analytics - System health checks -- Version control metrics ## Documentation Requirements @@ -209,11 +149,9 @@ - Type hints - Component documentation - API documentation -- Version control documentation ### System Documentation - Setup guides - Architecture docs - Maintenance procedures -- Troubleshooting guides -- Version control guides \ No newline at end of file +- Troubleshooting guides \ No newline at end of file diff --git a/parks/models.py b/parks/models.py index a8cbf732..d6ed5903 100644 --- a/parks/models.py +++ b/parks/models.py @@ -1,26 +1,21 @@ from django.db import models from django.urls import reverse from django.utils.text import slugify +from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from decimal import Decimal, ROUND_DOWN, InvalidOperation from typing import Tuple, Optional, Any, TYPE_CHECKING -from django.contrib.contenttypes.fields import GenericRelation from companies.models import Company -from history_tracking.signals import get_current_branch from media.models import Photo from history_tracking.models import HistoricalModel from location.models import Location -from comments.mixins import CommentableMixin -from media.mixins import PhotoableModel -from location.mixins import LocationMixin if TYPE_CHECKING: from rides.models import Ride -class Park(HistoricalModel, CommentableMixin, PhotoableModel, LocationMixin): - comments = GenericRelation('comments.CommentThread') # Centralized reference +class Park(HistoricalModel): id: int # Type hint for Django's automatic id field STATUS_CHOICES = [ ("OPERATING", "Operating"), @@ -38,6 +33,9 @@ class Park(HistoricalModel, CommentableMixin, PhotoableModel, LocationMixin): max_length=20, choices=STATUS_CHOICES, default="OPERATING" ) + # Location fields using GenericRelation + location = GenericRelation(Location, related_query_name='park') + # Details opening_date = models.DateField(null=True, blank=True) closing_date = models.DateField(null=True, blank=True) @@ -58,8 +56,8 @@ class Park(HistoricalModel, CommentableMixin, PhotoableModel, LocationMixin): owner = models.ForeignKey( Company, on_delete=models.SET_NULL, null=True, blank=True, related_name="parks" ) + photos = GenericRelation(Photo, related_query_name="park") areas: models.Manager['ParkArea'] # Type hint for reverse relation - rides: models.Manager['Ride'] # Type hint for reverse relation from rides app # Metadata @@ -68,7 +66,6 @@ class Park(HistoricalModel, CommentableMixin, PhotoableModel, LocationMixin): class Meta: ordering = ["name"] - excluded_fields = ['comments'] # Exclude from historical tracking def __str__(self) -> str: return self.name @@ -76,54 +73,28 @@ class Park(HistoricalModel, CommentableMixin, PhotoableModel, LocationMixin): def save(self, *args: Any, **kwargs: Any) -> None: if not self.slug: self.slug = slugify(self.name) - - # Get the branch from context or use default - from history_tracking.signals import get_current_branch - current_branch = get_current_branch() - - if current_branch: - # Save in the context of the current branch - super().save(*args, **kwargs) - else: - # If no branch context, save in main branch - from history_tracking.models import VersionBranch - main_branch, _ = VersionBranch.objects.get_or_create( - name='main', - defaults={'metadata': {'type': 'default_branch'}} - ) - - from history_tracking.signals import ChangesetContextManager - with ChangesetContextManager(branch=main_branch): - super().save(*args, **kwargs) - - def get_version_info(self) -> dict: - """Get version control information for this park""" - from history_tracking.models import VersionBranch, ChangeSet - from django.contrib.contenttypes.models import ContentType - - content_type = ContentType.objects.get_for_model(self) - latest_changes = ChangeSet.objects.filter( - content_type=content_type, - object_id=self.pk, - status='applied' - ).order_by('-created_at')[:5] - - active_branches = VersionBranch.objects.filter( - changesets__content_type=content_type, - changesets__object_id=self.pk, - is_active=True - ).distinct() - - return { - 'latest_changes': latest_changes, - 'active_branches': active_branches, - 'current_branch': get_current_branch(), - 'total_changes': latest_changes.count() - } + super().save(*args, **kwargs) def get_absolute_url(self) -> str: return reverse("parks:park_detail", kwargs={"slug": self.slug}) + @property + def formatted_location(self) -> str: + if self.location.exists(): + location = self.location.first() + if location: + return location.get_formatted_address() + return "" + + @property + def coordinates(self) -> Optional[Tuple[float, float]]: + """Returns coordinates as a tuple (latitude, longitude)""" + if self.location.exists(): + location = self.location.first() + if location: + return location.coordinates + return None + @classmethod def get_by_slug(cls, slug: str) -> Tuple['Park', bool]: """Get park by current or historical slug""" @@ -140,8 +111,7 @@ class Park(HistoricalModel, CommentableMixin, PhotoableModel, LocationMixin): raise cls.DoesNotExist("No park found with this slug") -class ParkArea(HistoricalModel, CommentableMixin, PhotoableModel): - comments = GenericRelation('comments.CommentThread') # Centralized reference +class ParkArea(HistoricalModel): id: int # Type hint for Django's automatic id field park = models.ForeignKey(Park, on_delete=models.CASCADE, related_name="areas") name = models.CharField(max_length=255) @@ -150,8 +120,6 @@ class ParkArea(HistoricalModel, CommentableMixin, PhotoableModel): opening_date = models.DateField(null=True, blank=True) closing_date = models.DateField(null=True, blank=True) - # Relationships - # Metadata created_at = models.DateTimeField(auto_now_add=True, null=True) updated_at = models.DateTimeField(auto_now=True) @@ -159,7 +127,6 @@ class ParkArea(HistoricalModel, CommentableMixin, PhotoableModel): class Meta: ordering = ["name"] unique_together = ["park", "slug"] - excluded_fields = ['comments'] # Exclude from historical tracking def __str__(self) -> str: return f"{self.name} at {self.park.name}" @@ -167,51 +134,7 @@ class ParkArea(HistoricalModel, CommentableMixin, PhotoableModel): def save(self, *args: Any, **kwargs: Any) -> None: if not self.slug: self.slug = slugify(self.name) - - # Get the branch from context or use default - from history_tracking.signals import get_current_branch - current_branch = get_current_branch() - - if current_branch: - # Save in the context of the current branch - super().save(*args, **kwargs) - else: - # If no branch context, save in main branch - from history_tracking.models import VersionBranch - main_branch, _ = VersionBranch.objects.get_or_create( - name='main', - defaults={'metadata': {'type': 'default_branch'}} - ) - - from history_tracking.signals import ChangesetContextManager - with ChangesetContextManager(branch=main_branch): - super().save(*args, **kwargs) - - def get_version_info(self) -> dict: - """Get version control information for this park area""" - from history_tracking.models import VersionBranch, ChangeSet - from django.contrib.contenttypes.models import ContentType - - content_type = ContentType.objects.get_for_model(self) - latest_changes = ChangeSet.objects.filter( - content_type=content_type, - object_id=self.pk, - status='applied' - ).order_by('-created_at')[:5] - - active_branches = VersionBranch.objects.filter( - changesets__content_type=content_type, - changesets__object_id=self.pk, - is_active=True - ).distinct() - - return { - 'latest_changes': latest_changes, - 'active_branches': active_branches, - 'current_branch': get_current_branch(), - 'total_changes': latest_changes.count(), - 'parent_park_branch': self.park.get_version_info()['current_branch'] - } + super().save(*args, **kwargs) def get_absolute_url(self) -> str: return reverse( diff --git a/parks/templates/parks/park_detail.html b/parks/templates/parks/park_detail.html deleted file mode 100644 index cc422b40..00000000 --- a/parks/templates/parks/park_detail.html +++ /dev/null @@ -1,200 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}{{ park.name }} - ThrillWiki{% endblock %} - -{% block content %} -
-
- -
- - {% include "history_tracking/includes/version_control_ui.html" %} - - -
-
-

{{ park.name }}

- - {{ park.get_status_display }} - -
- - {% if park.description %} -
- {{ park.description|linebreaks }} -
- {% endif %} - - -
- {% if park.opening_date %} -
-

Opening Date

-

{{ park.opening_date }}

-
- {% endif %} - - {% if park.size_acres %} -
-

Size

-

{{ park.size_acres }} acres

-
- {% endif %} - - {% if park.operating_season %} -
-

Operating Season

-

{{ park.operating_season }}

-
- {% endif %} - - {% if park.owner %} - - {% endif %} -
-
- - -
-

Rides

- {% if park.rides.all %} -
- {% for ride in park.rides.all %} -
-

- - {{ ride.name }} - -

-

{{ ride.type }}

-
- {% endfor %} -
- {% else %} -

No rides listed yet.

- {% endif %} -
- - - {% if park.areas.exists %} -
-

Areas

-
- {% for area in park.areas.all %} -
-

- - {{ area.name }} - -

- {% if area.description %} -

{{ area.description|truncatewords:20 }}

- {% endif %} -
- {% endfor %} -
-
- {% endif %} -
- - -
- - {% if park.formatted_location %} -
-

Location

-

{{ park.formatted_location }}

- {% if park.coordinates %} -
-
- {% endif %} -
- {% endif %} - - -
-

Statistics

-
- {% if park.average_rating %} -
- Average Rating: - {{ park.average_rating }}/5 -
- {% endif %} - - {% if park.ride_count %} -
- Total Rides: - {{ park.ride_count }} -
- {% endif %} - - {% if park.coaster_count %} -
- Roller Coasters: - {{ park.coaster_count }} -
- {% endif %} -
-
- - - {% if park.photos.exists %} -
- -
    - {% for photo in park.photos.all|slice:":4" %} -
  • - {% if photo.title %}{{ photo.title }} at {% endif %}{{ park.name }} -
  • - {% endfor %} -
- {% if park.photos.count > 4 %} - - View all {{ park.photos.count }} photos - - {% endif %} -
- {% endif %} -
-
-
-{% endblock %} - -{% block extra_js %} -{{ block.super }} -{% if park.coordinates %} - -{% endif %} -{% endblock %} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 41802ee2..2764ca5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,5 @@ dependencies = [ "django-simple-history>=3.5.0", "django-tailwind-cli>=2.21.1", "playwright>=1.41.0", - "pytest-playwright>=0.4.3", - "celery>=5.4.0", - "django-redis>=5.4.0", + "pytest-playwright>=0.4.3" ] diff --git a/requirements.txt b/requirements.txt index 3fa76454..fbe75e88 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,6 @@ pyjwt==2.10.1 # Database psycopg2-binary==2.9.10 dj-database-url==2.3.0 -django-redis==5.4.0 # Email requests==2.32.3 # For ForwardEmail.net API @@ -45,4 +44,3 @@ daphne==4.1.2 # React and Material UI will be handled via npm in the frontend directory django-simple-history==3.8.0 django-tailwind-cli==2.21.1 -celery==5.3.6 diff --git a/reviews/admin.py b/reviews/admin.py index 1c1a3b93..8176cd43 100644 --- a/reviews/admin.py +++ b/reviews/admin.py @@ -1,7 +1,11 @@ from django.contrib import admin from django.utils.html import format_html -from media.admin import PhotoInline -from .models import Review, ReviewLike, ReviewReport +from .models import Review, ReviewImage, ReviewLike, ReviewReport + +class ReviewImageInline(admin.TabularInline): + model = ReviewImage + extra = 1 + fields = ('image', 'caption', 'order') @admin.register(Review) class ReviewAdmin(admin.ModelAdmin): @@ -10,7 +14,7 @@ class ReviewAdmin(admin.ModelAdmin): search_fields = ('user__username', 'content', 'title') readonly_fields = ('created_at', 'updated_at') actions = ['publish_reviews', 'unpublish_reviews'] - inlines = [PhotoInline] + inlines = [ReviewImageInline] fieldsets = ( ('Review Details', { @@ -51,6 +55,13 @@ class ReviewAdmin(admin.ModelAdmin): queryset.update(is_published=False) unpublish_reviews.short_description = "Unpublish selected reviews" +@admin.register(ReviewImage) +class ReviewImageAdmin(admin.ModelAdmin): + list_display = ('review', 'caption', 'order') + list_filter = ('review__created_at',) + search_fields = ('review__title', 'caption') + ordering = ('review', 'order') + @admin.register(ReviewLike) class ReviewLikeAdmin(admin.ModelAdmin): list_display = ('review', 'user', 'created_at') diff --git a/reviews/mixins.py b/reviews/mixins.py deleted file mode 100644 index e0c1da89..00000000 --- a/reviews/mixins.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.contrib.contenttypes.models import ContentType -from django.db.models import QuerySet - -class ReviewableMixin: - """Mixin for models that can have reviews.""" - - def get_reviews(self) -> QuerySet: - """Get reviews for this instance.""" - from reviews.models import Review - ct = ContentType.objects.get_for_model(self.__class__) - return Review.objects.filter(content_type=ct, object_id=self.pk) - - def add_review(self, review: 'Review') -> None: - """Add a review to this instance.""" - from reviews.models import Review - ct = ContentType.objects.get_for_model(self.__class__) - review.content_type = ct - review.object_id = self.pk - review.save() \ No newline at end of file diff --git a/reviews/models.py b/reviews/models.py index 7f4b3c3b..435ecff9 100644 --- a/reviews/models.py +++ b/reviews/models.py @@ -1,15 +1,9 @@ from django.db import models -from django.urls import reverse -from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation +from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.validators import MinValueValidator, MaxValueValidator -from history_tracking.models import HistoricalModel, VersionBranch, ChangeSet -from history_tracking.signals import get_current_branch, ChangesetContextManager -from comments.mixins import CommentableMixin -from media.mixins import PhotoableModel -class Review(HistoricalModel, CommentableMixin, PhotoableModel): - comments = GenericRelation('comments.CommentThread') # Centralized reference +class Review(models.Model): # Generic relation to allow reviews on different types (rides, parks) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() @@ -43,69 +37,31 @@ class Review(HistoricalModel, CommentableMixin, PhotoableModel): related_name='moderated_reviews' ) moderated_at = models.DateTimeField(null=True, blank=True) - class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['content_type', 'object_id']), ] - excluded_fields = ['comments'] # Exclude from historical tracking def __str__(self): return f"Review of {self.content_object} by {self.user.username}" - def save(self, *args, **kwargs) -> None: - # Get the branch from context or use default - current_branch = get_current_branch() - - if current_branch: - # Save in the context of the current branch - super().save(*args, **kwargs) - else: - # If no branch context, save in main branch - main_branch, _ = VersionBranch.objects.get_or_create( - name='main', - defaults={'metadata': {'type': 'default_branch'}} - ) - - with ChangesetContextManager(branch=main_branch): - super().save(*args, **kwargs) +class ReviewImage(models.Model): + review = models.ForeignKey( + Review, + on_delete=models.CASCADE, + related_name='images' + ) + image = models.ImageField(upload_to='review_images/') + caption = models.CharField(max_length=200, blank=True) + order = models.PositiveIntegerField(default=0) - def get_version_info(self) -> dict: - """Get version control information for this review and its reviewed object""" - content_type = ContentType.objects.get_for_model(self) - latest_changes = ChangeSet.objects.filter( - content_type=content_type, - object_id=self.pk, - status='applied' - ).order_by('-created_at')[:5] - - active_branches = VersionBranch.objects.filter( - changesets__content_type=content_type, - changesets__object_id=self.pk, - is_active=True - ).distinct() + class Meta: + ordering = ['order'] - # Get version info for the reviewed object if it's version controlled - reviewed_object_branch = None - if hasattr(self.content_object, 'get_version_info'): - reviewed_object_branch = self.content_object.get_version_info().get('current_branch') - - return { - 'latest_changes': latest_changes, - 'active_branches': active_branches, - 'current_branch': get_current_branch(), - 'total_changes': latest_changes.count(), - 'reviewed_object_branch': reviewed_object_branch - } - - def get_absolute_url(self) -> str: - """Get the absolute URL for this review""" - if hasattr(self.content_object, 'get_absolute_url'): - base_url = self.content_object.get_absolute_url() - return f"{base_url}#review-{self.pk}" - return reverse('reviews:review_detail', kwargs={'pk': self.pk}) + def __str__(self): + return f"Image {self.order + 1} for {self.review}" class ReviewLike(models.Model): review = models.ForeignKey( diff --git a/reviews/templates/reviews/review_detail.html b/reviews/templates/reviews/review_detail.html deleted file mode 100644 index 87478b8d..00000000 --- a/reviews/templates/reviews/review_detail.html +++ /dev/null @@ -1,136 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}Review of {{ review.content_object.name }} by {{ review.user.username }} - ThrillWiki{% endblock %} - -{% block content %} -
-
- -
- - {% include "history_tracking/includes/version_control_ui.html" %} - - -
-
-

Review of {{ review.content_object.name }}

-
- - {{ review.is_published|yesno:"Published,Unpublished" }} - -
-
- -
-
-
{{ review.rating }}/10
-
Visited on {{ review.visit_date|date:"F j, Y" }}
-
-
- -

{{ review.title }}

- -
- {{ review.content|linebreaks }} -
- - - {% if review.images.exists %} -
-

Photos

-
- {% for image in review.images.all %} -
- {{ image.caption|default:'Review photo' }} -
- {% endfor %} -
-
- {% endif %} -
- - - {% if review.moderated_by %} -
-

Moderation Details

-
-

Moderated by: {{ review.moderated_by.username }}

-

Moderated on: {{ review.moderated_at|date:"F j, Y H:i" }}

- {% if review.moderation_notes %} -
- Notes: -

{{ review.moderation_notes|linebreaks }}

-
- {% endif %} -
-
- {% endif %} -
- - -
- -
-

{{ review.content_object|class_name }}

-
- - {{ review.content_object.name }} - - {% if review.content_object.park %} -

- at - - {{ review.content_object.park.name }} - -

- {% endif %} -
-
- - -
-

Reviewer

-
- {% if review.user.avatar %} - {{ review.user.username }} - {% endif %} -
-
{{ review.user.username }}
-
Member since {{ review.user.date_joined|date:"F Y" }}
-
-
-
-

Reviews: {{ review.user.reviews.count }}

-

Helpful votes: {{ review.user.review_likes.count }}

-
-
- - -
-

Review Details

-
-

Created: {{ review.created_at|date:"F j, Y H:i" }}

- {% if review.created_at != review.updated_at %} -

Last updated: {{ review.updated_at|date:"F j, Y H:i" }}

- {% endif %} -

Helpful votes: {{ review.likes.count }}

-
-
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/reviews/templates/reviews/review_list.html b/reviews/templates/reviews/review_list.html deleted file mode 100644 index d52428a4..00000000 --- a/reviews/templates/reviews/review_list.html +++ /dev/null @@ -1,154 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}Reviews - ThrillWiki{% endblock %} - -{% block content %} -
- - {% include "history_tracking/includes/version_control_ui.html" %} - -
-

Reviews

- {% if object %} -

Reviews for {{ object.name }}

- {% endif %} -
- - -
-
-
- - -
-
- - -
-
- - -
-
- -
-
-
- - - {% if reviews %} -
- {% for review in reviews %} -
-
-
-
-

- - {{ review.title }} - -

-

- Review of - - {{ review.content_object.name }} - - {% if review.content_object.park %} - at - - {{ review.content_object.park.name }} - - {% endif %} -

-
-
-
{{ review.rating }}/10
- - {{ review.is_published|yesno:"Published,Unpublished" }} - -
-
- -
- {{ review.content|truncatewords:50 }} -
- - - {% with version_info=review.get_version_info %} - {% if version_info.active_branches.count > 1 %} -
- - {{ version_info.active_branches.count }} active branches - -
- {% endif %} - {% endwith %} - -
-
-
- by {{ review.user.username }} -
-
{{ review.visit_date|date:"F j, Y" }}
-
{{ review.likes.count }} helpful votes
-
-
- {{ review.created_at|date:"F j, Y" }} -
-
-
-
- {% endfor %} -
- - - {% if is_paginated %} -
- -
- {% endif %} - - {% else %} -
-

No reviews found matching your criteria.

-
- {% endif %} -
-{% endblock %} \ No newline at end of file diff --git a/rides/models.py b/rides/models.py index 9efb2813..1374a759 100644 --- a/rides/models.py +++ b/rides/models.py @@ -1,13 +1,7 @@ from django.db import models -from django.urls import reverse from django.utils.text import slugify -from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericRelation -from history_tracking.models import HistoricalModel, VersionBranch, ChangeSet -from history_tracking.signals import get_current_branch, ChangesetContextManager -from comments.mixins import CommentableMixin -from media.mixins import PhotoableModel -from reviews.mixins import ReviewableMixin +from history_tracking.models import HistoricalModel # Shared choices that will be used by multiple models @@ -22,8 +16,7 @@ CATEGORY_CHOICES = [ ] -class RideModel(HistoricalModel, CommentableMixin, PhotoableModel): - comments = GenericRelation('comments.CommentThread') # Centralized reference +class RideModel(HistoricalModel): """ Represents a specific model/type of ride that can be manufactured by different companies. For example: B&M Dive Coaster, Vekoma Boomerang, etc. @@ -46,60 +39,15 @@ class RideModel(HistoricalModel, CommentableMixin, PhotoableModel): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - class Meta: ordering = ['manufacturer', 'name'] unique_together = ['manufacturer', 'name'] - excluded_fields = ['comments'] # Exclude from historical tracking -def __str__(self) -> str: - return self.name if not self.manufacturer else f"{self.manufacturer.name} {self.name}" -def save(self, *args, **kwargs) -> None: - # Get the branch from context or use default - current_branch = get_current_branch() - - if current_branch: - # Save in the context of the current branch - super().save(*args, **kwargs) - else: - # If no branch context, save in main branch - main_branch, _ = VersionBranch.objects.get_or_create( - name='main', - defaults={'metadata': {'type': 'default_branch'}} - ) - - with ChangesetContextManager(branch=main_branch): - super().save(*args, **kwargs) - -def get_version_info(self) -> dict: - """Get version control information for this ride model""" - content_type = ContentType.objects.get_for_model(self) - latest_changes = ChangeSet.objects.filter( - content_type=content_type, - object_id=self.pk, - status='applied' - ).order_by('-created_at')[:5] - - active_branches = VersionBranch.objects.filter( - changesets__content_type=content_type, - changesets__object_id=self.pk, - is_active=True - ).distinct() - - return { - 'latest_changes': latest_changes, - 'active_branches': active_branches, - 'current_branch': get_current_branch(), - 'total_changes': latest_changes.count() - } - -def get_absolute_url(self) -> str: - return reverse("rides:model_detail", kwargs={"pk": self.pk}) + def __str__(self) -> str: + return self.name if not self.manufacturer else f"{self.manufacturer.name} {self.name}" - -class Ride(HistoricalModel, CommentableMixin, PhotoableModel, ReviewableMixin): - comments = GenericRelation('comments.CommentThread') # Centralized reference +class Ride(HistoricalModel): STATUS_CHOICES = [ ('OPERATING', 'Operating'), ('SBNO', 'Standing But Not Operating'), @@ -184,11 +132,12 @@ class Ride(HistoricalModel, CommentableMixin, PhotoableModel, ReviewableMixin): ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + photos = GenericRelation('media.Photo') + reviews = GenericRelation('reviews.Review') class Meta: ordering = ['name'] unique_together = ['park', 'slug'] - excluded_fields = ['comments'] # Exclude from historical tracking def __str__(self) -> str: return f"{self.name} at {self.park.name}" @@ -196,66 +145,7 @@ class Ride(HistoricalModel, CommentableMixin, PhotoableModel, ReviewableMixin): def save(self, *args, **kwargs) -> None: if not self.slug: self.slug = slugify(self.name) - - # Get the branch from context or use default - current_branch = get_current_branch() - - if current_branch: - # Save in the context of the current branch - super().save(*args, **kwargs) - else: - # If no branch context, save in main branch - main_branch, _ = VersionBranch.objects.get_or_create( - name='main', - defaults={'metadata': {'type': 'default_branch'}} - ) - - with ChangesetContextManager(branch=main_branch): - super().save(*args, **kwargs) - - def get_version_info(self) -> dict: - """Get version control information for this ride""" - content_type = ContentType.objects.get_for_model(self) - latest_changes = ChangeSet.objects.filter( - content_type=content_type, - object_id=self.pk, - status='applied' - ).order_by('-created_at')[:5] - - active_branches = VersionBranch.objects.filter( - changesets__content_type=content_type, - changesets__object_id=self.pk, - is_active=True - ).distinct() - - return { - 'latest_changes': latest_changes, - 'active_branches': active_branches, - 'current_branch': get_current_branch(), - 'total_changes': latest_changes.count(), - 'parent_park_branch': self.park.get_version_info()['current_branch'] - } - - def get_absolute_url(self) -> str: - return reverse("rides:ride_detail", kwargs={ - "park_slug": self.park.slug, - "ride_slug": self.slug - }) - - @classmethod - def get_by_slug(cls, slug: str) -> tuple['Ride', bool]: - """Get ride by current or historical slug""" - try: - return cls.objects.get(slug=slug), False - except cls.DoesNotExist: - # Check historical slugs - history = cls.history.filter(slug=slug).order_by("-history_date").first() - if history: - try: - return cls.objects.get(pk=history.instance.pk), True - except cls.DoesNotExist as e: - raise cls.DoesNotExist("No ride found with this slug") from e - raise cls.DoesNotExist("No ride found with this slug") + super().save(*args, **kwargs) class RollerCoasterStats(models.Model): diff --git a/rides/templates/rides/ride_detail.html b/rides/templates/rides/ride_detail.html deleted file mode 100644 index 28544de8..00000000 --- a/rides/templates/rides/ride_detail.html +++ /dev/null @@ -1,220 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}{{ ride.name }} at {{ ride.park.name }} - ThrillWiki{% endblock %} - -{% block content %} -
-
- -
- - {% include "history_tracking/includes/version_control_ui.html" %} - - -
-
-

{{ ride.name }}

- - {{ ride.get_status_display }} - -
- - {% if ride.description %} -
- {{ ride.description|linebreaks }} -
- {% endif %} - - -
- {% if ride.opening_date %} -
-

Opening Date

-

{{ ride.opening_date }}

-
- {% endif %} - - {% if ride.manufacturer %} -
-

Manufacturer

-

- - {{ ride.manufacturer.name }} - -

-
- {% endif %} - - {% if ride.designer %} - - {% endif %} - - {% if ride.ride_model %} -
-

Ride Model

-

{{ ride.ride_model.name }}

-
- {% endif %} - - {% if ride.park_area %} - - {% endif %} - - {% if ride.capacity_per_hour %} -
-

Hourly Capacity

-

{{ ride.capacity_per_hour }} riders/hour

-
- {% endif %} - - {% if ride.ride_duration_seconds %} -
-

Ride Duration

-

{{ ride.ride_duration_seconds }} seconds

-
- {% endif %} -
-
- - - {% if ride.coaster_stats %} -
-

Coaster Statistics

-
- {% if ride.coaster_stats.height_ft %} -
-

Height

-

{{ ride.coaster_stats.height_ft }} ft

-
- {% endif %} - - {% if ride.coaster_stats.length_ft %} -
-

Length

-

{{ ride.coaster_stats.length_ft }} ft

-
- {% endif %} - - {% if ride.coaster_stats.speed_mph %} -
-

Speed

-

{{ ride.coaster_stats.speed_mph }} mph

-
- {% endif %} - - {% if ride.coaster_stats.inversions %} -
-

Inversions

-

{{ ride.coaster_stats.inversions }}

-
- {% endif %} - - {% if ride.coaster_stats.track_material %} -
-

Track Material

-

{{ ride.coaster_stats.get_track_material_display }}

-
- {% endif %} - - {% if ride.coaster_stats.roller_coaster_type %} -
-

Type

-

{{ ride.coaster_stats.get_roller_coaster_type_display }}

-
- {% endif %} -
-
- {% endif %} -
- - -
- -
-

Location

-

- - {{ ride.park.name }} - -

- {% if ride.park.formatted_location %} -

{{ ride.park.formatted_location }}

- {% endif %} -
- - -
-

Statistics

-
- {% if ride.average_rating %} -
- Average Rating: - {{ ride.average_rating }}/5 -
- {% endif %} - - {% if ride.reviews.count %} -
- Reviews: - {{ ride.reviews.count }} -
- {% endif %} -
-
- - - {% if ride.photos.exists %} -
- -
    - {% for photo in ride.photos.all|slice:":4" %} -
  • - {% if photo.title %}{{ photo.title }} at {% endif %}{{ ride.name }} -
  • - {% endfor %} -
- {% if ride.photos.count > 4 %} - - View all {{ ride.photos.count }} photos - - {% endif %} -
- {% endif %} -
-
-
-{% endblock %} \ No newline at end of file diff --git a/rides/templates/rides/ride_list.html b/rides/templates/rides/ride_list.html deleted file mode 100644 index d3c397b5..00000000 --- a/rides/templates/rides/ride_list.html +++ /dev/null @@ -1,153 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}Rides - ThrillWiki{% endblock %} - -{% block content %} -
- - {% include "history_tracking/includes/version_control_ui.html" %} - -
-

Rides

- {% if park %} -

Rides at {{ park.name }}

- {% endif %} -
- - -
-
-
- - -
-
- - -
-
- - -
-
- -
-
-
- - - {% if rides %} -
- {% for ride in rides %} -
- {% if ride.photos.exists %} -
- {{ ride.name }} -
- {% endif %} - -
-
-

- - {{ ride.name }} - -

- - {{ ride.get_status_display }} - -
- - {% if ride.park %} -

- - {{ ride.park.name }} - -

- {% endif %} - - {% if ride.manufacturer %} -

- {{ ride.manufacturer.name }} -

- {% endif %} - - {% if ride.description %} -

{{ ride.description|truncatewords:30 }}

- {% endif %} - - - {% with version_info=ride.get_version_info %} - {% if version_info.active_branches.count > 1 %} -
- - {{ version_info.active_branches.count }} active branches - -
- {% endif %} - {% endwith %} -
-
- {% endfor %} -
- - - {% if is_paginated %} -
- -
- {% endif %} - - {% else %} -
-

No rides found matching your criteria.

-
- {% endif %} -
-{% endblock %} \ No newline at end of file diff --git a/static/css/approval-panel.css b/static/css/approval-panel.css deleted file mode 100644 index ecf3c49f..00000000 --- a/static/css/approval-panel.css +++ /dev/null @@ -1,332 +0,0 @@ -/* Approval Panel Styles */ - -.approval-panel { - background: #fff; - border: 1px solid #e5e7eb; - border-radius: 0.5rem; - overflow: hidden; -} - -.approval-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem; - border-bottom: 1px solid #e5e7eb; - background: #f9fafb; -} - -.approval-title { - font-size: 1.125rem; - font-weight: 600; - color: #111827; - margin: 0; -} - -.approval-status { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.75rem; - border-radius: 9999px; - font-size: 0.875rem; - font-weight: 500; -} - -/* Status Colors */ -.status-draft { - background-color: #f3f4f6; - color: #6b7280; -} - -.status-pending { - background-color: #fef3c7; - color: #92400e; -} - -.status-approved { - background-color: #dcfce7; - color: #166534; -} - -.status-rejected { - background-color: #fee2e2; - color: #991b1b; -} - -.status-applied { - background-color: #dbeafe; - color: #1e40af; -} - -.status-failed { - background-color: #fee2e2; - color: #991b1b; -} - -.status-reverted { - background-color: #f3f4f6; - color: #6b7280; -} - -/* Stages */ -.approval-stages { - padding: 1rem; -} - -.approval-stage { - border: 1px solid #e5e7eb; - border-radius: 0.375rem; - margin-bottom: 1rem; - overflow: hidden; -} - -.approval-stage:last-child { - margin-bottom: 0; -} - -.stage-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 1rem; - background: #f9fafb; - border-bottom: 1px solid #e5e7eb; -} - -.stage-name { - font-weight: 500; - color: #374151; -} - -.stage-status { - font-size: 0.875rem; - font-weight: 500; -} - -.stage-status.pending { - color: #92400e; -} - -.stage-status.approved { - color: #166534; -} - -.stage-status.rejected { - color: #991b1b; -} - -.stage-details { - padding: 1rem; -} - -.required-roles { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - margin-bottom: 1rem; -} - -.role-badge { - background: #f3f4f6; - color: #4b5563; - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - font-size: 0.75rem; - font-weight: 500; -} - -/* Approvers */ -.approvers-list { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.approver { - padding: 0.75rem; - background: #f9fafb; - border-radius: 0.375rem; -} - -.approver-info { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.5rem; -} - -.approver-name { - font-weight: 500; - color: #374151; -} - -.approval-date { - font-size: 0.75rem; - color: #6b7280; -} - -.approver-decision { - font-size: 0.875rem; - font-weight: 500; - margin-bottom: 0.5rem; -} - -.approver-decision.approved { - color: #166534; -} - -.approver-decision.rejected { - color: #991b1b; -} - -.approver-comment { - font-size: 0.875rem; - color: #4b5563; - padding: 0.5rem; - background: #fff; - border-radius: 0.25rem; -} - -/* Approval Actions */ -.approval-actions { - padding: 1rem; - border-top: 1px solid #e5e7eb; - background: #f9fafb; -} - -.approval-comment { - width: 100%; - min-height: 5rem; - padding: 0.75rem; - margin-bottom: 1rem; - border: 1px solid #d1d5db; - border-radius: 0.375rem; - resize: vertical; - font-size: 0.875rem; -} - -.approval-comment:focus { - outline: none; - border-color: #3b82f6; - ring: 2px solid rgba(59, 130, 246, 0.5); -} - -.action-buttons { - display: flex; - justify-content: flex-end; - gap: 0.75rem; -} - -.approve-button, -.reject-button { - padding: 0.5rem 1rem; - border-radius: 0.375rem; - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s; -} - -.approve-button { - background-color: #059669; - color: white; -} - -.approve-button:hover { - background-color: #047857; -} - -.reject-button { - background-color: #dc2626; - color: white; -} - -.reject-button:hover { - background-color: #b91c1c; -} - -/* History */ -.approval-history { - padding: 1rem; - border-top: 1px solid #e5e7eb; -} - -.approval-history-list { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.history-entry { - padding: 0.75rem; - background: #f9fafb; - border-radius: 0.375rem; -} - -.entry-header { - display: flex; - justify-content: space-between; - margin-bottom: 0.5rem; -} - -.entry-user { - font-weight: 500; - color: #374151; -} - -.entry-date { - font-size: 0.75rem; - color: #6b7280; -} - -.entry-action { - font-size: 0.875rem; - margin-bottom: 0.5rem; -} - -.entry-action.submit { - color: #6b7280; -} - -.entry-action.approve { - color: #166534; -} - -.entry-action.reject { - color: #991b1b; -} - -.entry-action.revert { - color: #4b5563; -} - -.entry-comment { - font-size: 0.875rem; - color: #4b5563; - padding: 0.5rem; - background: #fff; - border-radius: 0.25rem; -} - -/* Responsive Design */ -@media (max-width: 640px) { - .approval-header { - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; - } - - .action-buttons { - flex-direction: column; - } - - .approve-button, - .reject-button { - width: 100%; - } - - .approver-info { - flex-direction: column; - align-items: flex-start; - } -} \ No newline at end of file diff --git a/static/css/diff-viewer.css b/static/css/diff-viewer.css deleted file mode 100644 index 1f9cff2f..00000000 --- a/static/css/diff-viewer.css +++ /dev/null @@ -1,195 +0,0 @@ -/* Diff Viewer Styles */ - -.diff-viewer { - font-family: ui-monospace, monospace; - margin: 1rem 0; - border: 1px solid #e5e7eb; - border-radius: 0.375rem; - overflow: hidden; - background: #fff; -} - -.diff-viewer.side-by-side .diff-blocks { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1px; - background: #e5e7eb; -} - -.diff-header { - padding: 1rem; - border-bottom: 1px solid #e5e7eb; - background: #f9fafb; -} - -.diff-metadata { - display: flex; - gap: 1rem; - font-size: 0.875rem; - color: #4b5563; -} - -.diff-controls { - margin-top: 0.5rem; - display: flex; - gap: 0.5rem; -} - -.diff-controls button { - padding: 0.25rem 0.75rem; - border: 1px solid #d1d5db; - border-radius: 0.25rem; - background: #fff; - font-size: 0.875rem; - color: #374151; - cursor: pointer; - transition: all 0.2s; -} - -.diff-controls button:hover { - background: #f3f4f6; -} - -.diff-section { - border-bottom: 1px solid #e5e7eb; -} - -.diff-field-header { - padding: 0.5rem 1rem; - background: #f9fafb; - border-bottom: 1px solid #e5e7eb; - font-weight: 500; - cursor: pointer; -} - -.diff-field-header .syntax-type { - font-size: 0.75rem; - color: #6b7280; - margin-left: 0.5rem; -} - -.diff-block { - display: grid; - grid-template-columns: auto 1fr; - background: #fff; -} - -.line-numbers { - padding: 0.5rem; - background: #f9fafb; - border-right: 1px solid #e5e7eb; - text-align: right; - color: #6b7280; - user-select: none; -} - -.line-number { - display: block; - padding: 0 0.5rem; - font-size: 0.875rem; -} - -.diff-block pre { - margin: 0; - padding: 0.5rem; - overflow-x: auto; -} - -.diff-block code { - font-family: ui-monospace, monospace; - font-size: 0.875rem; - line-height: 1.5; -} - -/* Inline diff styles */ -.diff-removed { - background-color: #fee2e2; - text-decoration: line-through; - color: #991b1b; -} - -.diff-added { - background-color: #dcfce7; - color: #166534; -} - -/* Syntax highlighting */ -.language-json .json-key { - color: #059669; -} - -.language-python .keyword { - color: #7c3aed; -} - -.language-python .string { - color: #059669; -} - -.language-python .comment { - color: #6b7280; - font-style: italic; -} - -/* Comment threads */ -.comment-thread { - border-top: 1px solid #e5e7eb; - padding: 1rem; - background: #f9fafb; -} - -.comment { - margin-bottom: 1rem; - padding: 0.5rem; - background: #fff; - border: 1px solid #e5e7eb; - border-radius: 0.25rem; -} - -.comment:last-child { - margin-bottom: 0; -} - -.comment-header { - display: flex; - justify-content: space-between; - font-size: 0.75rem; - color: #6b7280; - margin-bottom: 0.25rem; -} - -.comment-content { - font-size: 0.875rem; - color: #374151; -} - -/* Navigation */ -.diff-navigation { - padding: 1rem; - display: flex; - justify-content: space-between; - align-items: center; - border-top: 1px solid #e5e7eb; - background: #f9fafb; -} - -.position-indicator { - font-size: 0.875rem; - color: #6b7280; -} - -/* Collapsed state */ -.diff-section.collapsed .diff-blocks { - display: none; -} - -/* Performance warning */ -.performance-warning { - padding: 0.5rem; - background: #fffbeb; - border: 1px solid #fcd34d; - color: #92400e; - font-size: 0.875rem; - margin: 0.5rem 1rem; - border-radius: 0.25rem; -} \ No newline at end of file diff --git a/static/css/inline-comment-panel.css b/static/css/inline-comment-panel.css deleted file mode 100644 index 500ca1cc..00000000 --- a/static/css/inline-comment-panel.css +++ /dev/null @@ -1,229 +0,0 @@ -/* Inline Comment Panel Styles */ - -.comment-panel { - background: #fff; - border: 1px solid #e5e7eb; - border-radius: 0.5rem; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - margin: 1rem 0; -} - -.comment-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 1rem; - border-bottom: 1px solid #e5e7eb; - background: #f9fafb; -} - -.thread-info { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.anchor-info { - font-family: ui-monospace, monospace; - font-size: 0.875rem; - color: #4b5563; -} - -.resolution-badge { - display: inline-flex; - align-items: center; - gap: 0.25rem; - padding: 0.25rem 0.5rem; - background: #dcfce7; - color: #166534; - border-radius: 9999px; - font-size: 0.75rem; - font-weight: 500; -} - -.resolve-button { - padding: 0.375rem 0.75rem; - background: #059669; - color: white; - border-radius: 0.375rem; - font-size: 0.875rem; - font-weight: 500; - transition: all 0.2s; -} - -.resolve-button:hover { - background: #047857; -} - -.comments-container { - padding: 1rem; -} - -.comment { - margin-bottom: 1rem; - padding-bottom: 1rem; - border-bottom: 1px solid #e5e7eb; -} - -.comment:last-child { - margin-bottom: 0; - padding-bottom: 0; - border-bottom: none; -} - -.comment.reply { - margin-left: 2rem; - padding: 0.75rem; - background: #f9fafb; - border-radius: 0.375rem; - border: none; -} - -.comment-author { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 0.5rem; -} - -.author-avatar { - width: 2rem; - height: 2rem; - border-radius: 9999px; -} - -.author-name { - font-weight: 500; - color: #111827; -} - -.comment-date { - font-size: 0.75rem; - color: #6b7280; -} - -.comment-content { - font-size: 0.875rem; - line-height: 1.5; - color: #374151; - margin-bottom: 0.5rem; -} - -.comment-content .mention { - color: #2563eb; - font-weight: 500; -} - -.comment-content a { - color: #2563eb; - text-decoration: none; -} - -.comment-content a:hover { - text-decoration: underline; -} - -.comment-actions { - display: flex; - gap: 0.75rem; -} - -.action-button { - font-size: 0.75rem; - color: #6b7280; - background: none; - border: none; - padding: 0; - cursor: pointer; - transition: color 0.2s; -} - -.action-button:hover { - color: #374151; -} - -.reply-form { - padding: 1rem; - border-top: 1px solid #e5e7eb; - background: #f9fafb; -} - -.reply-form.nested { - margin-top: 0.75rem; - padding: 0.75rem; - border: 1px solid #e5e7eb; - border-radius: 0.375rem; -} - -.reply-input, -.edit-input { - width: 100%; - padding: 0.5rem; - border: 1px solid #d1d5db; - border-radius: 0.375rem; - font-size: 0.875rem; - resize: vertical; - margin-bottom: 0.5rem; -} - -.reply-input:focus, -.edit-input:focus { - outline: none; - border-color: #3b82f6; - ring: 2px solid rgba(59, 130, 246, 0.5); -} - -.form-actions { - display: flex; - justify-content: flex-end; - gap: 0.5rem; -} - -.reply-button, -.save-button { - padding: 0.375rem 0.75rem; - background: #3b82f6; - color: white; - border-radius: 0.375rem; - font-size: 0.875rem; - font-weight: 500; - transition: all 0.2s; -} - -.reply-button:hover, -.save-button:hover { - background: #2563eb; -} - -.cancel-button { - padding: 0.375rem 0.75rem; - background: #fff; - color: #4b5563; - border: 1px solid #d1d5db; - border-radius: 0.375rem; - font-size: 0.875rem; - font-weight: 500; - transition: all 0.2s; -} - -.cancel-button:hover { - background: #f3f4f6; -} - -/* Responsive Design */ -@media (max-width: 640px) { - .comment.reply { - margin-left: 1rem; - } - - .comment-header { - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; - } - - .resolve-button { - width: 100%; - text-align: center; - } -} \ No newline at end of file diff --git a/static/css/version-comparison.css b/static/css/version-comparison.css deleted file mode 100644 index e5436290..00000000 --- a/static/css/version-comparison.css +++ /dev/null @@ -1,353 +0,0 @@ -/* Version Comparison Tool Styles */ - -.version-comparison-tool { - background: #fff; - border: 1px solid #e5e7eb; - border-radius: 0.5rem; - overflow: hidden; -} - -/* Header Styles */ -.comparison-header { - padding: 1rem; - border-bottom: 1px solid #e5e7eb; - background: #f9fafb; -} - -.comparison-header h3 { - margin: 0 0 1rem 0; - font-size: 1.125rem; - font-weight: 600; - color: #111827; -} - -.selected-versions { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - margin-bottom: 1rem; -} - -.selected-version { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.375rem 0.75rem; - background: #f3f4f6; - border-radius: 0.375rem; - font-size: 0.875rem; -} - -.version-label { - font-weight: 500; - color: #374151; -} - -.version-value { - color: #4b5563; -} - -.remove-version { - border: none; - background: none; - color: #6b7280; - cursor: pointer; - padding: 0.125rem 0.25rem; - font-size: 1rem; - line-height: 1; -} - -.remove-version:hover { - color: #ef4444; -} - -/* Timeline Styles */ -.version-timeline { - position: relative; - padding: 2rem 1rem; - overflow-x: auto; - cursor: grab; - user-select: none; -} - -.version-timeline.active { - cursor: grabbing; -} - -.timeline-track { - position: relative; - height: 2px; - background: #e5e7eb; - margin: 0 2rem; -} - -.timeline-point { - position: absolute; - top: 50%; - transform: translate(-50%, -50%); - cursor: pointer; -} - -.timeline-point::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 12px; - height: 12px; - background: #fff; - border: 2px solid #6b7280; - border-radius: 50%; - transition: all 0.2s; -} - -.timeline-point.selected::before { - background: #3b82f6; - border-color: #3b82f6; -} - -.impact-indicator { - position: absolute; - top: -24px; - left: 50%; - transform: translateX(-50%); - border-radius: 50%; - background: rgba(59, 130, 246, 0.1); - border: 2px solid rgba(59, 130, 246, 0.2); - transition: all 0.2s; -} - -.timeline-labels { - display: flex; - position: absolute; - bottom: 0.5rem; - left: 0; - right: 0; - padding: 0 2rem; -} - -.timeline-label { - position: absolute; - transform: translateX(-50%); - text-align: center; - min-width: 100px; -} - -.version-name { - font-weight: 500; - font-size: 0.875rem; - color: #374151; - margin-bottom: 0.25rem; -} - -.version-date { - font-size: 0.75rem; - color: #6b7280; -} - -/* Comparison Actions */ -.comparison-actions { - display: flex; - gap: 0.75rem; -} - -.compare-button, -.rollback-button { - padding: 0.5rem 1rem; - border-radius: 0.375rem; - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s; -} - -.compare-button { - background-color: #3b82f6; - color: white; -} - -.compare-button:hover { - background-color: #2563eb; -} - -.rollback-button { - background-color: #6b7280; - color: white; -} - -.rollback-button:hover { - background-color: #4b5563; -} - -/* Comparison Results */ -.comparison-content { - padding: 1rem; -} - -.comparison-placeholder { - text-align: center; - padding: 2rem; - color: #6b7280; - font-size: 0.875rem; -} - -.results-loading { - text-align: center; - padding: 2rem; - color: #6b7280; -} - -.diff-section { - border: 1px solid #e5e7eb; - border-radius: 0.375rem; - margin-bottom: 1rem; - overflow: hidden; -} - -.diff-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 1rem; - background: #f9fafb; - border-bottom: 1px solid #e5e7eb; -} - -.diff-header h4 { - margin: 0; - font-size: 0.875rem; - font-weight: 600; - color: #374151; -} - -.diff-stats { - display: flex; - gap: 1rem; - font-size: 0.75rem; - color: #6b7280; -} - -.change-item { - padding: 1rem; - border-bottom: 1px solid #e5e7eb; -} - -.change-item:last-child { - border-bottom: none; -} - -.change-header { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 0.75rem; -} - -.change-type { - padding: 0.25rem 0.5rem; - background: #f3f4f6; - border-radius: 0.25rem; - font-size: 0.75rem; - font-weight: 500; - color: #374151; -} - -.change-file { - font-family: ui-monospace, monospace; - font-size: 0.875rem; - color: #4b5563; -} - -.change-content { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 1rem; -} - -.old-value, -.new-value { - background: #f9fafb; - border-radius: 0.375rem; - overflow: hidden; -} - -.value-header { - padding: 0.5rem; - background: #f3f4f6; - font-size: 0.75rem; - font-weight: 500; - color: #4b5563; - border-bottom: 1px solid #e5e7eb; -} - -.change-content pre { - margin: 0; - padding: 0.75rem; - font-size: 0.875rem; - line-height: 1.5; - overflow-x: auto; -} - -/* Warning/Error Messages */ -.comparison-warning, -.comparison-error { - position: fixed; - top: 1rem; - right: 1rem; - padding: 0.75rem 1rem; - border-radius: 0.375rem; - font-size: 0.875rem; - animation: slideIn 0.3s ease-out; -} - -.comparison-warning { - background: #fef3c7; - color: #92400e; - border: 1px solid #f59e0b; -} - -.comparison-error { - background: #fee2e2; - color: #991b1b; - border: 1px solid #ef4444; -} - -@keyframes slideIn { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -/* Responsive Design */ -@media (max-width: 768px) { - .comparison-header { - padding: 0.75rem; - } - - .selected-versions { - flex-direction: column; - } - - .comparison-actions { - flex-direction: column; - } - - .compare-button, - .rollback-button { - width: 100%; - } - - .change-content { - grid-template-columns: 1fr; - } - - .timeline-label { - min-width: 80px; - } -} \ No newline at end of file diff --git a/static/css/version-control.css b/static/css/version-control.css deleted file mode 100644 index 5f21a81a..00000000 --- a/static/css/version-control.css +++ /dev/null @@ -1,290 +0,0 @@ -/* Version Control System Styles */ - -.version-control-ui { - --vcs-primary: #3b82f6; - --vcs-success: #10b981; - --vcs-warning: #f59e0b; - --vcs-error: #ef4444; - --vcs-gray: #6b7280; -} - -/* Branch Status Indicators */ -.branch-indicator { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.75rem; - border-radius: 9999px; - font-size: 0.875rem; - font-weight: 500; -} - -.branch-indicator.active { - background-color: rgba(16, 185, 129, 0.1); - color: var(--vcs-success); -} - -.branch-indicator.inactive { - background-color: rgba(107, 114, 128, 0.1); - color: var(--vcs-gray); -} - -/* Change Status Tags */ -.change-status { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.75rem; - border-radius: 0.375rem; - font-size: 0.75rem; - font-weight: 500; -} - -.change-status.applied { - background-color: rgba(16, 185, 129, 0.1); - color: var(--vcs-success); -} - -.change-status.pending { - background-color: rgba(245, 158, 11, 0.1); - color: var(--vcs-warning); -} - -.change-status.failed { - background-color: rgba(239, 68, 68, 0.1); - color: var(--vcs-error); -} - -/* Change History */ -.change-history { - border-left: 2px solid #e5e7eb; - margin-left: 1rem; - padding-left: 1rem; -} - -.change-history-item { - position: relative; - padding: 1rem 0; -} - -.change-history-item::before { - content: ''; - position: absolute; - left: -1.25rem; - top: 1.5rem; - height: 0.75rem; - width: 0.75rem; - border-radius: 9999px; - background-color: white; - border: 2px solid var(--vcs-primary); -} - -/* Merge Interface */ -.merge-conflict { - border: 1px solid #e5e7eb; - border-radius: 0.5rem; - margin: 1rem 0; - padding: 1rem; -} - -.merge-conflict-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; -} - -.merge-conflict-content { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 1rem; -} - -.merge-version { - background-color: #f9fafb; - padding: 1rem; - border-radius: 0.375rem; -} - -/* Branch Selection */ -.branch-selector { - position: relative; -} - -.branch-list { - max-height: 24rem; - overflow-y: auto; -} - -.branch-item { - display: flex; - align-items: center; - padding: 0.5rem; - border-radius: 0.375rem; - transition: background-color 0.2s; -} - -.branch-item:hover { - background-color: #f9fafb; -} - -.branch-item.active { - background-color: rgba(59, 130, 246, 0.1); -} - -/* Version Tags */ -.version-tag { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.75rem; - border-radius: 9999px; - background-color: #f3f4f6; - color: var(--vcs-gray); - font-size: 0.875rem; - margin: 0.25rem; -} - -/* Branch Lock Status */ -.lock-status { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.25rem 0.75rem; - border-radius: 0.375rem; - font-size: 0.875rem; - margin-left: 0.5rem; -} - -.lock-status.locked { - background-color: rgba(239, 68, 68, 0.1); - color: var(--vcs-error); -} - -.lock-status.unlocked { - background-color: rgba(16, 185, 129, 0.1); - color: var(--vcs-success); -} - -.lock-status .lock-icon { - width: 1rem; - height: 1rem; -} - -.lock-info { - display: flex; - flex-direction: column; - font-size: 0.75rem; - color: var(--vcs-gray); -} - -.lock-info .user { - font-weight: 500; - color: inherit; -} - -.lock-info .expiry { - opacity: 0.8; -} - -/* Lock Controls */ -.lock-controls { - display: flex; - gap: 0.5rem; - margin-top: 0.5rem; -} - -.lock-button { - display: inline-flex; - align-items: center; - gap: 0.25rem; - padding: 0.375rem 0.75rem; - border-radius: 0.375rem; - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s; -} - -.lock-button.lock { - background-color: var(--vcs-error); - color: white; -} - -.lock-button.unlock { - background-color: var(--vcs-success); - color: white; -} - -.lock-button:hover { - opacity: 0.9; -} - -.lock-button:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* Lock History */ -.lock-history { - margin-top: 1rem; - border-top: 1px solid #e5e7eb; - padding-top: 1rem; -} - -.lock-history-item { - display: flex; - align-items: flex-start; - gap: 1rem; - padding: 0.5rem 0; - font-size: 0.875rem; -} - -.lock-history-item .action { - font-weight: 500; -} - -.lock-history-item .timestamp { - color: var(--vcs-gray); -} - -.lock-history-item .reason { - margin-top: 0.25rem; - color: var(--vcs-gray); - font-style: italic; -} - -/* Loading States */ -.vcs-loading { - position: relative; - pointer-events: none; - opacity: 0.7; -} - -.vcs-loading::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 1.5rem; - height: 1.5rem; - border: 2px solid #e5e7eb; - border-top-color: var(--vcs-primary); - border-radius: 50%; - animation: vcs-spin 1s linear infinite; -} - -@keyframes vcs-spin { - to { - transform: translate(-50%, -50%) rotate(360deg); - } -} - -/* Responsive Design */ -@media (max-width: 640px) { - .merge-conflict-content { - grid-template-columns: 1fr; - } - - .branch-selector { - width: 100%; - } -} \ No newline at end of file diff --git a/static/js/__tests__/version-control.test.js b/static/js/__tests__/version-control.test.js deleted file mode 100644 index 498ae4ff..00000000 --- a/static/js/__tests__/version-control.test.js +++ /dev/null @@ -1,217 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import { initVersionControl, setupBranchHandlers, handleMergeConflicts } from '../version-control'; - -describe('Version Control UI', () => { - let container; - - beforeEach(() => { - container = document.createElement('div'); - container.id = 'version-control-panel'; - document.body.appendChild(container); - - // Mock HTMX - window.htmx = { - trigger: jest.fn(), - ajax: jest.fn(), - on: jest.fn() - }; - }); - - afterEach(() => { - document.body.innerHTML = ''; - jest.clearAllMocks(); - }); - - describe('initialization', () => { - it('should initialize version control UI', () => { - const panel = document.createElement('div'); - panel.className = 'version-control-panel'; - container.appendChild(panel); - - initVersionControl(); - - expect(window.htmx.on).toHaveBeenCalled(); - expect(container.querySelector('.version-control-panel')).toBeTruthy(); - }); - - it('should setup branch switch handlers', () => { - const switchButton = document.createElement('button'); - switchButton.setAttribute('data-branch-id', '1'); - switchButton.className = 'branch-switch'; - container.appendChild(switchButton); - - setupBranchHandlers(); - switchButton.click(); - - expect(window.htmx.ajax).toHaveBeenCalledWith( - 'POST', - '/version-control/switch-branch/', - expect.any(Object) - ); - }); - }); - - describe('branch operations', () => { - it('should handle branch creation', () => { - const form = document.createElement('form'); - form.id = 'create-branch-form'; - container.appendChild(form); - - const event = new Event('submit'); - form.dispatchEvent(event); - - expect(window.htmx.trigger).toHaveBeenCalledWith( - form, - 'branch-created', - expect.any(Object) - ); - }); - - it('should update UI after branch switch', () => { - const response = { - branch_name: 'feature/test', - status: 'success' - }; - - const event = new CustomEvent('branchSwitched', { - detail: response - }); - - document.dispatchEvent(event); - - expect(container.querySelector('.current-branch')?.textContent) - .toContain('feature/test'); - }); - }); - - describe('merge operations', () => { - it('should handle merge conflicts', () => { - const conflicts = [ - { - field: 'name', - source_value: 'Feature Name', - target_value: 'Main Name' - } - ]; - - handleMergeConflicts(conflicts); - - const conflictDialog = document.querySelector('.merge-conflict-dialog'); - expect(conflictDialog).toBeTruthy(); - expect(conflictDialog.innerHTML).toContain('name'); - expect(conflictDialog.innerHTML).toContain('Feature Name'); - expect(conflictDialog.innerHTML).toContain('Main Name'); - }); - - it('should submit merge resolution', () => { - const resolutionForm = document.createElement('form'); - resolutionForm.id = 'merge-resolution-form'; - container.appendChild(resolutionForm); - - const event = new Event('submit'); - resolutionForm.dispatchEvent(event); - - expect(window.htmx.ajax).toHaveBeenCalledWith( - 'POST', - '/version-control/resolve-conflicts/', - expect.any(Object) - ); - }); - }); - - describe('error handling', () => { - it('should display error messages', () => { - const errorEvent = new CustomEvent('showError', { - detail: { message: 'Test error message' } - }); - - document.dispatchEvent(errorEvent); - - const errorMessage = document.querySelector('.error-message'); - expect(errorMessage).toBeTruthy(); - expect(errorMessage.textContent).toContain('Test error message'); - }); - - it('should clear error messages', () => { - const errorMessage = document.createElement('div'); - errorMessage.className = 'error-message'; - container.appendChild(errorMessage); - - const clearEvent = new Event('clearErrors'); - document.dispatchEvent(clearEvent); - - expect(container.querySelector('.error-message')).toBeFalsy(); - }); - }); - - describe('loading states', () => { - it('should show loading indicator during operations', () => { - const loadingEvent = new Event('versionControlLoading'); - document.dispatchEvent(loadingEvent); - - const loader = document.querySelector('.version-control-loader'); - expect(loader).toBeTruthy(); - expect(loader.style.display).toBe('block'); - }); - - it('should hide loading indicator after operations', () => { - const loader = document.createElement('div'); - loader.className = 'version-control-loader'; - container.appendChild(loader); - - const doneEvent = new Event('versionControlLoaded'); - document.dispatchEvent(doneEvent); - - expect(loader.style.display).toBe('none'); - }); - }); - - describe('UI updates', () => { - it('should update branch list after operations', () => { - const branchList = document.createElement('ul'); - branchList.className = 'branch-list'; - container.appendChild(branchList); - - const updateEvent = new CustomEvent('updateBranchList', { - detail: { - branches: [ - { name: 'main', active: true }, - { name: 'feature/test', active: false } - ] - } - }); - - document.dispatchEvent(updateEvent); - - const listItems = branchList.querySelectorAll('li'); - expect(listItems.length).toBe(2); - expect(listItems[0].textContent).toContain('main'); - expect(listItems[1].textContent).toContain('feature/test'); - }); - - it('should highlight active branch', () => { - const branchItems = [ - { name: 'main', active: true }, - { name: 'feature/test', active: false } - ].map(branch => { - const item = document.createElement('li'); - item.textContent = branch.name; - item.className = 'branch-item'; - if (branch.active) item.classList.add('active'); - return item; - }); - - const branchList = document.createElement('ul'); - branchList.className = 'branch-list'; - branchList.append(...branchItems); - container.appendChild(branchList); - - const activeItem = branchList.querySelector('.branch-item.active'); - expect(activeItem).toBeTruthy(); - expect(activeItem.textContent).toBe('main'); - }); - }); -}); \ No newline at end of file diff --git a/static/js/collaboration-system.js b/static/js/collaboration-system.js deleted file mode 100644 index 8ff196ca..00000000 --- a/static/js/collaboration-system.js +++ /dev/null @@ -1,203 +0,0 @@ -// Collaboration System - -class CollaborationSystem { - constructor(options = {}) { - this.onCommentAdded = options.onCommentAdded || (() => {}); - this.onCommentResolved = options.onCommentResolved || (() => {}); - this.onThreadCreated = options.onThreadCreated || (() => {}); - this.socket = null; - this.currentUser = options.currentUser; - } - - initialize(socketUrl) { - this.socket = new WebSocket(socketUrl); - this.setupSocketHandlers(); - } - - setupSocketHandlers() { - if (!this.socket) return; - - this.socket.addEventListener('open', () => { - console.log('Collaboration system connected'); - }); - - this.socket.addEventListener('message', (event) => { - try { - const data = JSON.parse(event.data); - this.handleEvent(data); - } catch (error) { - console.error('Failed to parse collaboration event:', error); - } - }); - - this.socket.addEventListener('close', () => { - console.log('Collaboration system disconnected'); - // Attempt to reconnect after delay - setTimeout(() => this.reconnect(), 5000); - }); - - this.socket.addEventListener('error', (error) => { - console.error('Collaboration system error:', error); - }); - } - - handleEvent(event) { - switch (event.type) { - case 'comment_added': - this.onCommentAdded(event.data); - break; - case 'comment_resolved': - this.onCommentResolved(event.data); - break; - case 'thread_created': - this.onThreadCreated(event.data); - break; - default: - console.warn('Unknown collaboration event:', event.type); - } - } - - async createCommentThread(changeId, anchor, initialComment) { - try { - const response = await fetch('/vcs/comments/threads/create/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCsrfToken() - }, - body: JSON.stringify({ - change_id: changeId, - anchor: anchor, - initial_comment: initialComment - }) - }); - - if (!response.ok) { - throw new Error('Failed to create comment thread'); - } - - const thread = await response.json(); - - // Notify other users through WebSocket - this.broadcastEvent({ - type: 'thread_created', - data: { - thread_id: thread.id, - change_id: changeId, - anchor: anchor, - author: this.currentUser, - timestamp: new Date().toISOString() - } - }); - - return thread; - } catch (error) { - console.error('Error creating comment thread:', error); - throw error; - } - } - - async addComment(threadId, content, parentId = null) { - try { - const response = await fetch('/vcs/comments/create/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCsrfToken() - }, - body: JSON.stringify({ - thread_id: threadId, - content: content, - parent_id: parentId - }) - }); - - if (!response.ok) { - throw new Error('Failed to add comment'); - } - - const comment = await response.json(); - - // Notify other users through WebSocket - this.broadcastEvent({ - type: 'comment_added', - data: { - comment_id: comment.id, - thread_id: threadId, - parent_id: parentId, - author: this.currentUser, - content: content, - timestamp: new Date().toISOString() - } - }); - - return comment; - } catch (error) { - console.error('Error adding comment:', error); - throw error; - } - } - - async resolveThread(threadId) { - try { - const response = await fetch(`/vcs/comments/threads/${threadId}/resolve/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCsrfToken() - } - }); - - if (!response.ok) { - throw new Error('Failed to resolve thread'); - } - - const result = await response.json(); - - // Notify other users through WebSocket - this.broadcastEvent({ - type: 'comment_resolved', - data: { - thread_id: threadId, - resolver: this.currentUser, - timestamp: new Date().toISOString() - } - }); - - return result; - } catch (error) { - console.error('Error resolving thread:', error); - throw error; - } - } - - broadcastEvent(event) { - if (this.socket && this.socket.readyState === WebSocket.OPEN) { - this.socket.send(JSON.stringify(event)); - } - } - - reconnect() { - if (this.socket) { - try { - this.socket.close(); - } catch (error) { - console.error('Error closing socket:', error); - } - } - this.initialize(this.socketUrl); - } - - getCsrfToken() { - return document.querySelector('[name=csrfmiddlewaretoken]').value; - } - - disconnect() { - if (this.socket) { - this.socket.close(); - this.socket = null; - } - } -} - -export default CollaborationSystem; \ No newline at end of file diff --git a/static/js/components/approval-panel.js b/static/js/components/approval-panel.js deleted file mode 100644 index d097a9e6..00000000 --- a/static/js/components/approval-panel.js +++ /dev/null @@ -1,234 +0,0 @@ -// Approval Panel Component - -class ApprovalPanel { - constructor(options = {}) { - this.container = null; - this.changeset = null; - this.currentUser = options.currentUser; - this.onApprove = options.onApprove || (() => {}); - this.onReject = options.onReject || (() => {}); - this.onSubmit = options.onSubmit || (() => {}); - } - - initialize(containerId) { - this.container = document.getElementById(containerId); - if (!this.container) { - throw new Error(`Container element with id "${containerId}" not found`); - } - } - - setChangeset(changeset) { - this.changeset = changeset; - this.render(); - } - - render() { - if (!this.container || !this.changeset) return; - - const approvalState = this.changeset.approval_state || []; - const currentStage = approvalState.find(s => s.status === 'pending') || approvalState[0]; - - this.container.innerHTML = ` -
-
-

Change Approval

- ${this._renderStatus()} -
- -
- ${this._renderStages(approvalState)} -
- - ${currentStage && this.changeset.status === 'pending_approval' ? - this._renderApprovalActions(currentStage) : '' - } - -
- ${this._renderHistory()} -
-
- `; - - this.attachEventListeners(); - } - - _renderStatus() { - const statusMap = { - 'draft': { class: 'status-draft', text: 'Draft' }, - 'pending_approval': { class: 'status-pending', text: 'Pending Approval' }, - 'approved': { class: 'status-approved', text: 'Approved' }, - 'rejected': { class: 'status-rejected', text: 'Rejected' }, - 'applied': { class: 'status-applied', text: 'Applied' }, - 'failed': { class: 'status-failed', text: 'Failed' }, - 'reverted': { class: 'status-reverted', text: 'Reverted' } - }; - - const status = statusMap[this.changeset.status] || statusMap.draft; - return ` -
- ${status.text} -
- `; - } - - _renderStages(stages) { - return stages.map((stage, index) => ` -
-
- ${stage.name} - ${ - this._formatStageStatus(stage.status) - } -
-
-
- ${stage.required_roles.map(role => ` - ${role} - `).join('')} -
- ${stage.approvers.length > 0 ? ` -
- ${stage.approvers.map(approver => this._renderApprover(approver)).join('')} -
- ` : ''} -
-
- `).join(''); - } - - _renderApprover(approver) { - const decisionClass = approver.decision === 'approve' ? 'approved' : 'rejected'; - return ` -
-
- ${approver.username} - - ${new Date(approver.timestamp).toLocaleString()} - -
-
- ${approver.decision === 'approve' ? 'Approved' : 'Rejected'} -
- ${approver.comment ? ` -
${approver.comment}
- ` : ''} -
- `; - } - - _renderApprovalActions(currentStage) { - if (!this.canUserApprove(currentStage)) { - return ''; - } - - return ` -
- -
- - -
-
- `; - } - - _renderHistory() { - if (!this.changeset.approval_history?.length) { - return ''; - } - - return ` -
- ${this.changeset.approval_history.map(entry => ` -
-
- ${entry.username} - -
-
- ${this._formatHistoryAction(entry.action)} -
- ${entry.comment ? ` -
${entry.comment}
- ` : ''} -
- `).join('')} -
- `; - } - - _formatStageStatus(status) { - const statusMap = { - 'pending': 'Pending', - 'approved': 'Approved', - 'rejected': 'Rejected' - }; - return statusMap[status] || status; - } - - _formatHistoryAction(action) { - const actionMap = { - 'submit': 'Submitted for approval', - 'approve': 'Approved changes', - 'reject': 'Rejected changes', - 'revert': 'Reverted approval' - }; - return actionMap[action] || action; - } - - canUserApprove(stage) { - if (!this.currentUser) return false; - - // Check if user already approved - const alreadyApproved = stage.approvers.some( - a => a.user_id === this.currentUser.id - ); - if (alreadyApproved) return false; - - // Check if user has required role - return stage.required_roles.some( - role => this.currentUser.roles.includes(role) - ); - } - - async handleApprove() { - const commentEl = this.container.querySelector('.approval-comment'); - const comment = commentEl ? commentEl.value.trim() : ''; - - try { - await this.onApprove(comment); - this.render(); - } catch (error) { - console.error('Failed to approve:', error); - // Show error message - } - } - - async handleReject() { - const commentEl = this.container.querySelector('.approval-comment'); - const comment = commentEl ? commentEl.value.trim() : ''; - - try { - await this.onReject(comment); - this.render(); - } catch (error) { - console.error('Failed to reject:', error); - // Show error message - } - } - - attachEventListeners() { - // Add any additional event listeners if needed - } -} - -export default ApprovalPanel; \ No newline at end of file diff --git a/static/js/components/diff-viewer.js b/static/js/components/diff-viewer.js deleted file mode 100644 index 5115ece1..00000000 --- a/static/js/components/diff-viewer.js +++ /dev/null @@ -1,274 +0,0 @@ -// Enhanced Diff Viewer Component - -class DiffViewer { - constructor(options = {}) { - this.renderStrategy = options.renderStrategy || 'side-by-side'; - this.syntaxHighlighters = new Map(); - this.commentThreads = []; - this.container = null; - this.performance = { - startTime: null, - endTime: null - }; - } - - initialize(containerId) { - this.container = document.getElementById(containerId); - if (!this.container) { - throw new Error(`Container element with id "${containerId}" not found`); - } - this.setupSyntaxHighlighters(); - } - - setupSyntaxHighlighters() { - // Set up Prism.js or similar syntax highlighting library - this.syntaxHighlighters.set('text', this.plainTextHighlighter); - this.syntaxHighlighters.set('json', this.jsonHighlighter); - this.syntaxHighlighters.set('python', this.pythonHighlighter); - } - - async render(diffData) { - this.performance.startTime = performance.now(); - - const { changes, metadata, navigation } = diffData; - const content = this.renderStrategy === 'side-by-side' - ? this.renderSideBySide(changes) - : this.renderInline(changes); - - this.container.innerHTML = ` -
-
- ${this.renderMetadata(metadata)} - ${this.renderControls()} -
-
- ${content} -
- ${this.renderNavigation(navigation)} -
- `; - - this.attachEventListeners(); - await this.highlightSyntax(); - - this.performance.endTime = performance.now(); - this.updatePerformanceMetrics(); - } - - renderSideBySide(changes) { - return Object.entries(changes).map(([field, change]) => ` -
-
- ${field} - ${change.syntax_type} -
-
-
-
- ${this.renderLineNumbers(change.metadata.line_numbers.old)} -
-
${this.escapeHtml(change.old)}
-
-
-
- ${this.renderLineNumbers(change.metadata.line_numbers.new)} -
-
${this.escapeHtml(change.new)}
-
-
-
- `).join(''); - } - - renderInline(changes) { - return Object.entries(changes).map(([field, change]) => ` -
-
- ${field} - ${change.syntax_type} -
-
-
- ${this.renderLineNumbers(change.metadata.line_numbers.new)} -
-

-                        ${this.renderInlineDiff(change.old, change.new)}
-                    
-
-
- `).join(''); - } - - renderMetadata(metadata) { - return ` - - `; - } - - renderControls() { - return ` -
- - - - -
- `; - } - - renderNavigation(navigation) { - return ` -
- ${navigation.prev_id ? `` : ''} - ${navigation.next_id ? `` : ''} - Change ${navigation.current_position} -
- `; - } - - renderLineNumbers(numbers) { - return numbers.map(num => `${num}`).join(''); - } - - renderInlineDiff(oldText, newText) { - // Simple inline diff implementation - could be enhanced with more sophisticated diff algorithm - const oldLines = oldText.split('\n'); - const newLines = newText.split('\n'); - const diffLines = []; - - for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) { - if (oldLines[i] !== newLines[i]) { - if (oldLines[i]) { - diffLines.push(`${this.escapeHtml(oldLines[i])}`); - } - if (newLines[i]) { - diffLines.push(`${this.escapeHtml(newLines[i])}`); - } - } else if (oldLines[i]) { - diffLines.push(this.escapeHtml(oldLines[i])); - } - } - - return diffLines.join('\n'); - } - - attachEventListeners() { - // View mode switching - this.container.querySelectorAll('.btn-view-mode').forEach(btn => { - btn.addEventListener('click', () => { - this.renderStrategy = btn.dataset.mode; - this.render(this.currentDiffData); - }); - }); - - // Collapse/Expand functionality - this.container.querySelectorAll('.diff-section').forEach(section => { - section.querySelector('.diff-field-header').addEventListener('click', () => { - section.classList.toggle('collapsed'); - }); - }); - - // Navigation - this.container.querySelectorAll('.diff-navigation button').forEach(btn => { - btn.addEventListener('click', () => { - this.navigateToChange(btn.dataset.id); - }); - }); - } - - async highlightSyntax() { - const codeBlocks = this.container.querySelectorAll('code[class^="language-"]'); - for (const block of codeBlocks) { - const syntax = block.className.replace('language-', ''); - const highlighter = this.syntaxHighlighters.get(syntax); - if (highlighter) { - await highlighter(block); - } - } - } - - // Syntax highlighters - async plainTextHighlighter(element) { - // No highlighting needed for plain text - return element; - } - - async jsonHighlighter(element) { - try { - const content = element.textContent; - const parsed = JSON.parse(content); - element.textContent = JSON.stringify(parsed, null, 2); - // Apply JSON syntax highlighting classes - element.innerHTML = element.innerHTML.replace( - /"([^"]+)":/g, - '"$1":' - ); - } catch (e) { - console.warn('JSON parsing failed:', e); - } - return element; - } - - async pythonHighlighter(element) { - // Basic Python syntax highlighting - element.innerHTML = element.innerHTML - .replace(/(def|class|import|from|return|if|else|try|except)\b/g, '$1') - .replace(/(["'])(.*?)\1/g, '$1$2$1') - .replace(/#.*/g, '$&'); - return element; - } - - updatePerformanceMetrics() { - const renderTime = this.performance.endTime - this.performance.startTime; - if (renderTime > 200) { // Performance budget: 200ms - console.warn(`Diff render time (${renderTime}ms) exceeded performance budget`); - } - } - - escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - - formatChangeType(type) { - const types = { - 'C': 'Changed', - 'D': 'Deleted', - 'A': 'Added' - }; - return types[type] || type; - } - - addCommentThread(anchor, thread) { - this.commentThreads.push({ anchor, thread }); - this.renderCommentThreads(); - } - - renderCommentThreads() { - this.commentThreads.forEach(({ anchor, thread }) => { - const element = this.container.querySelector(`[data-anchor="${anchor}"]`); - if (element) { - const threadElement = document.createElement('div'); - threadElement.className = 'comment-thread'; - threadElement.innerHTML = thread.map(comment => ` -
-
- ${comment.author} - ${new Date(comment.date).toLocaleString()} -
-
${comment.content}
-
- `).join(''); - element.appendChild(threadElement); - } - }); - } -} - -export default DiffViewer; \ No newline at end of file diff --git a/static/js/components/inline-comment-panel.js b/static/js/components/inline-comment-panel.js deleted file mode 100644 index 69d33d70..00000000 --- a/static/js/components/inline-comment-panel.js +++ /dev/null @@ -1,285 +0,0 @@ -// Inline Comment Panel Component - -class InlineCommentPanel { - constructor(options = {}) { - this.container = null; - this.thread = null; - this.canResolve = options.canResolve || false; - this.onReply = options.onReply || (() => {}); - this.onResolve = options.onResolve || (() => {}); - this.currentUser = options.currentUser; - } - - initialize(containerId) { - this.container = document.getElementById(containerId); - if (!this.container) { - throw new Error(`Container element with id "${containerId}" not found`); - } - } - - setThread(thread) { - this.thread = thread; - this.render(); - } - - render() { - if (!this.container || !this.thread) return; - - this.container.innerHTML = ` -
-
-
- ${this.formatAnchor(this.thread.anchor)} - ${this.thread.is_resolved ? - ` - - - - Resolved - ` - : '' - } -
- ${this.canResolve && !this.thread.is_resolved ? - `` - : '' - } -
-
- ${this.renderComments(this.thread.comments)} -
- ${!this.thread.is_resolved ? - `
- -
- -
-
` - : '' - } -
- `; - - this.attachEventListeners(); - } - - renderComments(comments) { - return comments.map(comment => ` -
-
- ${comment.author.username} - ${comment.author.username} - ${this.formatDate(comment.created_at)} -
-
- ${this.formatCommentContent(comment.content)} -
- ${this.renderCommentActions(comment)} - ${comment.replies ? - `
- ${this.renderComments(comment.replies)} -
` - : '' - } -
- `).join(''); - } - - renderCommentActions(comment) { - if (this.thread.is_resolved) return ''; - - return ` -
- - ${comment.author.id === this.currentUser.id ? - `` - : '' - } -
- `; - } - - formatCommentContent(content) { - // Replace @mentions with styled spans - content = content.replace(/@(\w+)/g, '@$1'); - - // Convert URLs to links - content = content.replace( - /(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/g, - '$1' - ); - - return content; - } - - formatAnchor(anchor) { - const start = anchor.line_start; - const end = anchor.line_end; - const file = anchor.file_path.split('/').pop(); - - return end > start ? - `${file}:${start}-${end}` : - `${file}:${start}`; - } - - formatDate(dateString) { - const date = new Date(dateString); - return date.toLocaleString(); - } - - attachEventListeners() { - const replyInput = this.container.querySelector('.reply-input'); - if (replyInput) { - replyInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { - this.submitReply(); - } - }); - } - } - - async submitReply() { - const input = this.container.querySelector('.reply-input'); - const content = input.value.trim(); - - if (!content) return; - - try { - await this.onReply(content); - input.value = ''; - } catch (error) { - console.error('Failed to submit reply:', error); - // Show error message to user - } - } - - async resolveThread() { - try { - await this.onResolve(); - this.render(); - } catch (error) { - console.error('Failed to resolve thread:', error); - // Show error message to user - } - } - - showReplyForm(commentId) { - const comment = this.container.querySelector(`[data-comment-id="${commentId}"]`); - if (!comment) return; - - const replyForm = document.createElement('div'); - replyForm.className = 'reply-form nested'; - replyForm.innerHTML = ` - -
- - -
- `; - - comment.appendChild(replyForm); - replyForm.querySelector('.reply-input').focus(); - } - - hideReplyForm(commentId) { - const comment = this.container.querySelector(`[data-comment-id="${commentId}"]`); - if (!comment) return; - - const replyForm = comment.querySelector('.reply-form'); - if (replyForm) { - replyForm.remove(); - } - } - - async submitNestedReply(parentId) { - const comment = this.container.querySelector(`[data-comment-id="${parentId}"]`); - if (!comment) return; - - const input = comment.querySelector('.reply-input'); - const content = input.value.trim(); - - if (!content) return; - - try { - await this.onReply(content, parentId); - this.hideReplyForm(parentId); - } catch (error) { - console.error('Failed to submit reply:', error); - // Show error message to user - } - } - - editComment(commentId) { - const comment = this.container.querySelector(`[data-comment-id="${commentId}"]`); - if (!comment) return; - - const contentDiv = comment.querySelector('.comment-content'); - const content = contentDiv.textContent; - - contentDiv.innerHTML = ` - -
- - -
- `; - } - - cancelEdit(commentId) { - // Refresh the entire thread to restore original content - this.render(); - } - - async saveEdit(commentId) { - const comment = this.container.querySelector(`[data-comment-id="${commentId}"]`); - if (!comment) return; - - const input = comment.querySelector('.edit-input'); - const content = input.value.trim(); - - if (!content) return; - - try { - // Emit edit event - const event = new CustomEvent('comment-edited', { - detail: { commentId, content } - }); - this.container.dispatchEvent(event); - - // Refresh the thread - this.render(); - } catch (error) { - console.error('Failed to edit comment:', error); - // Show error message to user - } - } -} - -export default InlineCommentPanel; \ No newline at end of file diff --git a/static/js/components/version-comparison.js b/static/js/components/version-comparison.js deleted file mode 100644 index fc480a95..00000000 --- a/static/js/components/version-comparison.js +++ /dev/null @@ -1,314 +0,0 @@ -// Version Comparison Component - -class VersionComparison { - constructor(options = {}) { - this.container = null; - this.versions = new Map(); - this.selectedVersions = new Set(); - this.maxSelections = options.maxSelections || 3; - this.onCompare = options.onCompare || (() => {}); - this.onRollback = options.onRollback || (() => {}); - this.timeline = null; - } - - initialize(containerId) { - this.container = document.getElementById(containerId); - if (!this.container) { - throw new Error(`Container element with id "${containerId}" not found`); - } - this._initializeTimeline(); - } - - setVersions(versions) { - this.versions = new Map(versions.map(v => [v.name, v])); - this._updateTimeline(); - this.render(); - } - - _initializeTimeline() { - this.timeline = document.createElement('div'); - this.timeline.className = 'version-timeline'; - this.container.appendChild(this.timeline); - } - - _updateTimeline() { - const sortedVersions = Array.from(this.versions.values()) - .sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); - - this.timeline.innerHTML = ` -
- ${this._renderTimelineDots(sortedVersions)} -
-
- ${this._renderTimelineLabels(sortedVersions)} -
- `; - } - - _renderTimelineDots(versions) { - return versions.map(version => ` -
-
- ${this._renderImpactIndicator(version)} -
- `).join(''); - } - - _renderImpactIndicator(version) { - const impact = version.comparison_metadata.impact_score || 0; - const size = Math.max(8, Math.min(24, impact * 24)); // 8-24px based on impact - - return ` -
-
- `; - } - - _renderTimelineLabels(versions) { - return versions.map(version => ` -
-
${version.name}
-
- ${new Date(version.created_at).toLocaleDateString()} -
-
- `).join(''); - } - - render() { - if (!this.container) return; - - const selectedVersionsArray = Array.from(this.selectedVersions); - - this.container.innerHTML = ` -
-
-

Version Comparison

-
- ${this._renderSelectedVersions(selectedVersionsArray)} -
-
- ${this._renderActionButtons(selectedVersionsArray)} -
-
- - ${this.timeline.outerHTML} - -
- ${this._renderComparisonContent(selectedVersionsArray)} -
-
- `; - - this.attachEventListeners(); - } - - _renderSelectedVersions(selectedVersions) { - return selectedVersions.map((version, index) => ` -
- Version ${index + 1}: - ${version} - -
- `).join(''); - } - - _renderActionButtons(selectedVersions) { - const canCompare = selectedVersions.length >= 2; - const canRollback = selectedVersions.length === 1; - - return ` - ${canCompare ? ` - - ` : ''} - ${canRollback ? ` - - ` : ''} - `; - } - - _renderComparisonContent(selectedVersions) { - if (selectedVersions.length < 2) { - return ` -
- Select at least two versions to compare -
- `; - } - - return ` -
-
- Computing differences... -
-
- `; - } - - _toggleVersionSelection(versionName) { - if (this.selectedVersions.has(versionName)) { - this.selectedVersions.delete(versionName); - } else if (this.selectedVersions.size < this.maxSelections) { - this.selectedVersions.add(versionName); - } else { - // Show max selections warning - this._showWarning(`Maximum ${this.maxSelections} versions can be compared`); - return; - } - this.render(); - } - - _removeVersion(versionName) { - this.selectedVersions.delete(versionName); - this.render(); - } - - async _handleCompare() { - const selectedVersions = Array.from(this.selectedVersions); - if (selectedVersions.length < 2) return; - - try { - const results = await this.onCompare(selectedVersions); - this._renderComparisonResults(results); - } catch (error) { - console.error('Comparison failed:', error); - this._showError('Failed to compare versions'); - } - } - - async _handleRollback() { - const selectedVersion = Array.from(this.selectedVersions)[0]; - if (!selectedVersion) return; - - try { - await this.onRollback(selectedVersion); - // Handle successful rollback - } catch (error) { - console.error('Rollback failed:', error); - this._showError('Failed to rollback version'); - } - } - - _renderComparisonResults(results) { - const resultsContainer = this.container.querySelector('.comparison-results'); - if (!resultsContainer) return; - - resultsContainer.innerHTML = ` -
- ${results.map(diff => this._renderDiffSection(diff)).join('')} -
- `; - } - - _renderDiffSection(diff) { - return ` -
-
-

Changes: ${diff.version1} → ${diff.version2}

-
- - Computed in ${diff.computation_time.toFixed(2)}s - - - Impact Score: ${Math.round(diff.impact_score * 100)}% - -
-
-
- ${this._renderChanges(diff.changes)} -
-
- `; - } - - _renderChanges(changes) { - return changes.map(change => ` -
-
- ${change.type} - ${change.file} -
-
-
-
Previous
-
${this._escapeHtml(change.old_value)}
-
-
-
New
-
${this._escapeHtml(change.new_value)}
-
-
-
- `).join(''); - } - - _showWarning(message) { - const warning = document.createElement('div'); - warning.className = 'comparison-warning'; - warning.textContent = message; - this.container.appendChild(warning); - setTimeout(() => warning.remove(), 3000); - } - - _showError(message) { - const error = document.createElement('div'); - error.className = 'comparison-error'; - error.textContent = message; - this.container.appendChild(error); - setTimeout(() => error.remove(), 3000); - } - - _escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - - attachEventListeners() { - // Timeline scroll handling - const timeline = this.container.querySelector('.version-timeline'); - if (timeline) { - let isDown = false; - let startX; - let scrollLeft; - - timeline.addEventListener('mousedown', (e) => { - isDown = true; - timeline.classList.add('active'); - startX = e.pageX - timeline.offsetLeft; - scrollLeft = timeline.scrollLeft; - }); - - timeline.addEventListener('mouseleave', () => { - isDown = false; - timeline.classList.remove('active'); - }); - - timeline.addEventListener('mouseup', () => { - isDown = false; - timeline.classList.remove('active'); - }); - - timeline.addEventListener('mousemove', (e) => { - if (!isDown) return; - e.preventDefault(); - const x = e.pageX - timeline.offsetLeft; - const walk = (x - startX) * 2; - timeline.scrollLeft = scrollLeft - walk; - }); - } - } -} - -export default VersionComparison; \ No newline at end of file diff --git a/static/js/components/virtual-scroller.js b/static/js/components/virtual-scroller.js deleted file mode 100644 index b189eea0..00000000 --- a/static/js/components/virtual-scroller.js +++ /dev/null @@ -1,190 +0,0 @@ -// Virtual Scroller Component -// Implements efficient scrolling for large lists by only rendering visible items - -class VirtualScroller { - constructor(options) { - this.container = options.container; - this.itemHeight = options.itemHeight; - this.bufferSize = options.bufferSize || 5; - this.renderItem = options.renderItem; - this.items = []; - this.scrollTop = 0; - this.visibleItems = new Map(); - - this.observer = new IntersectionObserver( - this._handleIntersection.bind(this), - { threshold: 0.1 } - ); - - this._setupContainer(); - this._bindEvents(); - } - - _setupContainer() { - if (!this.container) { - throw new Error('Container element is required'); - } - - this.container.style.position = 'relative'; - this.container.style.height = '600px'; // Default height - this.container.style.overflowY = 'auto'; - - // Create spacer element to maintain scroll height - this.spacer = document.createElement('div'); - this.spacer.style.position = 'absolute'; - this.spacer.style.top = '0'; - this.spacer.style.left = '0'; - this.spacer.style.width = '1px'; - this.container.appendChild(this.spacer); - } - - _bindEvents() { - this.container.addEventListener( - 'scroll', - this._debounce(this._handleScroll.bind(this), 16) - ); - - // Handle container resize - if (window.ResizeObserver) { - const resizeObserver = new ResizeObserver(this._debounce(() => { - this._render(); - }, 16)); - resizeObserver.observe(this.container); - } - } - - setItems(items) { - this.items = items; - this.spacer.style.height = `${items.length * this.itemHeight}px`; - this._render(); - } - - _handleScroll() { - this.scrollTop = this.container.scrollTop; - this._render(); - } - - _handleIntersection(entries) { - entries.forEach(entry => { - const itemId = entry.target.dataset.itemId; - if (!entry.isIntersecting) { - this.visibleItems.delete(itemId); - entry.target.remove(); - } - }); - } - - _render() { - const visibleRange = this._getVisibleRange(); - const itemsToRender = new Set(); - - // Calculate which items should be visible - for (let i = visibleRange.start; i <= visibleRange.end; i++) { - if (i >= 0 && i < this.items.length) { - itemsToRender.add(i); - } - } - - // Remove items that are no longer visible - for (const [itemId] of this.visibleItems) { - const index = parseInt(itemId); - if (!itemsToRender.has(index)) { - const element = this.container.querySelector(`[data-item-id="${itemId}"]`); - if (element) { - this.observer.unobserve(element); - element.remove(); - } - this.visibleItems.delete(itemId); - } - } - - // Add new visible items - for (const index of itemsToRender) { - if (!this.visibleItems.has(index.toString())) { - this._renderItem(index); - } - } - - // Update performance metrics - this._updateMetrics(itemsToRender.size); - } - - _renderItem(index) { - const item = this.items[index]; - const element = document.createElement('div'); - - element.style.position = 'absolute'; - element.style.top = `${index * this.itemHeight}px`; - element.style.left = '0'; - element.style.width = '100%'; - element.dataset.itemId = index.toString(); - - // Render content - element.innerHTML = this.renderItem(item); - - // Add to container and observe - this.container.appendChild(element); - this.observer.observe(element); - this.visibleItems.set(index.toString(), element); - - // Adjust actual height if needed - const actualHeight = element.offsetHeight; - if (actualHeight !== this.itemHeight) { - element.style.height = `${this.itemHeight}px`; - } - } - - _getVisibleRange() { - const start = Math.floor(this.scrollTop / this.itemHeight) - this.bufferSize; - const visibleCount = Math.ceil(this.container.clientHeight / this.itemHeight); - const end = start + visibleCount + 2 * this.bufferSize; - return { start, end }; - } - - _updateMetrics(visibleCount) { - const metrics = { - totalItems: this.items.length, - visibleItems: visibleCount, - scrollPosition: this.scrollTop, - containerHeight: this.container.clientHeight, - renderTime: performance.now() // You can use this with the previous render time - }; - - // Dispatch metrics event - this.container.dispatchEvent(new CustomEvent('virtualScroller:metrics', { - detail: metrics - })); - } - - _debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; - } - - // Public methods - scrollToIndex(index) { - if (index >= 0 && index < this.items.length) { - this.container.scrollTop = index * this.itemHeight; - } - } - - refresh() { - this._render(); - } - - destroy() { - this.observer.disconnect(); - this.container.innerHTML = ''; - this.items = []; - this.visibleItems.clear(); - } -} - -export default VirtualScroller; \ No newline at end of file diff --git a/static/js/error-handling.js b/static/js/error-handling.js deleted file mode 100644 index 455668ed..00000000 --- a/static/js/error-handling.js +++ /dev/null @@ -1,207 +0,0 @@ -/** - * Error handling and state management for version control system - */ - -class VersionControlError extends Error { - constructor(message, code, details = {}) { - super(message); - this.name = 'VersionControlError'; - this.code = code; - this.details = details; - this.timestamp = new Date(); - } -} - -// Error boundary for version control operations -class VersionControlErrorBoundary { - constructor(options = {}) { - this.onError = options.onError || this.defaultErrorHandler; - this.errors = new Map(); - this.retryAttempts = new Map(); - this.maxRetries = options.maxRetries || 3; - } - - defaultErrorHandler(error) { - console.error(`[Version Control Error]: ${error.message}`, error); - this.showErrorNotification(error); - } - - showErrorNotification(error) { - const notification = document.createElement('div'); - notification.className = 'version-control-error notification'; - notification.innerHTML = ` -
- ⚠️ - ${error.message} - -
- ${error.details.retry ? '' : ''} - `; - - document.body.appendChild(notification); - - // Auto-hide after 5 seconds unless it's a critical error - if (!error.details.critical) { - setTimeout(() => { - notification.remove(); - }, 5000); - } - - // Handle retry - const retryBtn = notification.querySelector('.retry-btn'); - if (retryBtn && error.details.retryCallback) { - retryBtn.addEventListener('click', () => { - notification.remove(); - error.details.retryCallback(); - }); - } - - // Handle close - const closeBtn = notification.querySelector('.close-btn'); - closeBtn.addEventListener('click', () => notification.remove()); - } - - async wrapOperation(operationKey, operation) { - try { - // Check if operation is already in progress - if (this.errors.has(operationKey)) { - throw new VersionControlError( - 'Operation already in progress', - 'DUPLICATE_OPERATION' - ); - } - - // Show loading state - this.showLoading(operationKey); - - const result = await operation(); - - // Clear any existing errors for this operation - this.errors.delete(operationKey); - this.retryAttempts.delete(operationKey); - - return result; - } catch (error) { - const retryCount = this.retryAttempts.get(operationKey) || 0; - - // Handle specific error types - if (error.name === 'VersionControlError') { - this.handleVersionControlError(error, operationKey, retryCount); - } else { - // Convert unknown errors to VersionControlError - const vcError = new VersionControlError( - 'An unexpected error occurred', - 'UNKNOWN_ERROR', - { originalError: error } - ); - this.handleVersionControlError(vcError, operationKey, retryCount); - } - - throw error; - } finally { - this.hideLoading(operationKey); - } - } - - handleVersionControlError(error, operationKey, retryCount) { - this.errors.set(operationKey, error); - - // Determine if operation can be retried - const canRetry = retryCount < this.maxRetries; - - error.details.retry = canRetry; - error.details.retryCallback = canRetry ? - () => this.retryOperation(operationKey) : - undefined; - - this.onError(error); - } - - async retryOperation(operationKey) { - const retryCount = (this.retryAttempts.get(operationKey) || 0) + 1; - this.retryAttempts.set(operationKey, retryCount); - - // Exponential backoff for retries - const backoffDelay = Math.min(1000 * Math.pow(2, retryCount - 1), 10000); - await new Promise(resolve => setTimeout(resolve, backoffDelay)); - - // Get the original operation and retry - const operation = this.pendingOperations.get(operationKey); - if (operation) { - return this.wrapOperation(operationKey, operation); - } - } - - showLoading(operationKey) { - const loadingElement = document.createElement('div'); - loadingElement.className = `loading-indicator loading-${operationKey}`; - loadingElement.innerHTML = ` -
- Processing... - `; - document.body.appendChild(loadingElement); - } - - hideLoading(operationKey) { - const loadingElement = document.querySelector(`.loading-${operationKey}`); - if (loadingElement) { - loadingElement.remove(); - } - } -} - -// Create singleton instance -const errorBoundary = new VersionControlErrorBoundary({ - onError: (error) => { - // Log to monitoring system - if (window.monitoring) { - window.monitoring.logError('version_control', error); - } - } -}); - -// Export error handling utilities -export const versionControl = { - /** - * Wrap version control operations with error handling - */ - async performOperation(key, operation) { - return errorBoundary.wrapOperation(key, operation); - }, - - /** - * Create a new error instance - */ - createError(message, code, details) { - return new VersionControlError(message, code, details); - }, - - /** - * Show loading state manually - */ - showLoading(key) { - errorBoundary.showLoading(key); - }, - - /** - * Hide loading state manually - */ - hideLoading(key) { - errorBoundary.hideLoading(key); - }, - - /** - * Show error notification manually - */ - showError(error) { - errorBoundary.showErrorNotification(error); - } -}; - -// Add global error handler for uncaught version control errors -window.addEventListener('unhandledrejection', event => { - if (event.reason instanceof VersionControlError) { - event.preventDefault(); - errorBoundary.defaultErrorHandler(event.reason); - } -}); \ No newline at end of file diff --git a/static/js/map-init.js b/static/js/map-init.js deleted file mode 100644 index 1a92afb2..00000000 --- a/static/js/map-init.js +++ /dev/null @@ -1,20 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - const mapContainer = document.getElementById('map'); - if (!mapContainer) return; - - const lat = parseFloat(mapContainer.dataset.lat); - const lng = parseFloat(mapContainer.dataset.lng); - const name = mapContainer.dataset.name; - - if (isNaN(lat) || isNaN(lng)) return; - - const map = L.map('map').setView([lat, lng], 13); - - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors' - }).addTo(map); - - L.marker([lat, lng]) - .addTo(map) - .bindPopup(name); -}); \ No newline at end of file diff --git a/static/js/moderation.js b/static/js/moderation.js deleted file mode 100644 index a10d035a..00000000 --- a/static/js/moderation.js +++ /dev/null @@ -1,223 +0,0 @@ -// 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/static/js/version-control.js b/static/js/version-control.js deleted file mode 100644 index 9554e96a..00000000 --- a/static/js/version-control.js +++ /dev/null @@ -1,536 +0,0 @@ -// Version Control System Functionality - -class VersionControl { - constructor() { - this.setupEventListeners(); - } - - setupEventListeners() { - // Branch switching - document.addEventListener('htmx:afterRequest', (event) => { - if (event.detail.target.id === 'branch-form-container') { - this.handleBranchFormResponse(event); - } - }); - - // Listen for branch switches - document.addEventListener('branch-switched', () => { - this.refreshContent(); - }); - - // Handle merge operations - document.addEventListener('htmx:afterRequest', (event) => { - if (event.detail.target.id === 'merge-panel') { - this.handleMergeResponse(event); - } - }); - } - - handleBranchFormResponse(event) { - if (event.detail.successful) { - // Clear the branch form container - document.getElementById('branch-form-container').innerHTML = ''; - // Trigger branch list refresh - document.body.dispatchEvent(new CustomEvent('branch-updated')); - } - } - - handleMergeResponse(event) { - if (event.detail.successful) { - const mergePanel = document.getElementById('merge-panel'); - if (mergePanel.innerHTML.includes('Merge Successful')) { - // Trigger content refresh after successful merge - setTimeout(() => { - this.refreshContent(); - }, 1500); - } - } - } - - refreshContent() { - // Reload the page to show content from new branch - window.location.reload(); - } - - // Branch operations - createBranch(name, parentBranch = null) { - const formData = new FormData(); - formData.append('name', name); - if (parentBranch) { - formData.append('parent', parentBranch); - } - - return fetch('/vcs/branches/create/', { - method: 'POST', - body: formData, - headers: { - 'X-CSRFToken': this.getCsrfToken() - } - }).then(response => response.json()); - } - - switchBranch(branchName) { - const formData = new FormData(); - formData.append('branch', branchName); - - return fetch('/vcs/branches/switch/', { - method: 'POST', - body: formData, - headers: { - 'X-CSRFToken': this.getCsrfToken() - } - }).then(response => { - if (response.ok) { - document.body.dispatchEvent(new CustomEvent('branch-switched')); - } - return response.json(); - }); - } - - // Merge operations - initiateMerge(sourceBranch, targetBranch) { - const formData = new FormData(); - formData.append('source', sourceBranch); - formData.append('target', targetBranch); - - return fetch('/vcs/merge/', { - method: 'POST', - body: formData, - headers: { - 'X-CSRFToken': this.getCsrfToken() - } - }).then(response => response.json()); - } - - resolveConflicts(resolutions) { - return fetch('/vcs/resolve-conflicts/', { - method: 'POST', - body: JSON.stringify(resolutions), - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCsrfToken() - } - }).then(response => response.json()); - } - - // History operations - getHistory(branch = null) { - let url = '/vcs/history/'; - if (branch) { - url += `?branch=${encodeURIComponent(branch)}`; - } - - return fetch(url) - .then(response => response.json()); - } - - // Comment operations - async createComment(threadId, content, parentId = null) { - try { - const response = await fetch('/vcs/comments/create/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCsrfToken() - }, - body: JSON.stringify({ - thread_id: threadId, - content: content, - parent_id: parentId - }) - }); - - if (!response.ok) { - throw new Error('Failed to create comment'); - } - - const comment = await response.json(); - return comment; - } catch (error) { - this.showError('Error creating comment: ' + error.message); - throw error; - } - } - - async createCommentThread(changeId, anchor, initialComment) { - try { - const response = await fetch('/vcs/comments/threads/create/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCsrfToken() - }, - body: JSON.stringify({ - change_id: changeId, - anchor: anchor, - initial_comment: initialComment - }) - }); - - if (!response.ok) { - throw new Error('Failed to create comment thread'); - } - - const thread = await response.json(); - return thread; - } catch (error) { - this.showError('Error creating comment thread: ' + error.message); - throw error; - } - } - - async resolveThread(threadId) { - try { - const response = await fetch(`/vcs/comments/threads/${threadId}/resolve/`, { - method: 'POST', - headers: { - 'X-CSRFToken': this.getCsrfToken() - } - }); - - if (!response.ok) { - throw new Error('Failed to resolve thread'); - } - - return await response.json(); - } catch (error) { - this.showError('Error resolving thread: ' + error.message); - throw error; - } - } - - async editComment(commentId, content) { - try { - const response = await fetch(`/vcs/comments/${commentId}/`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCsrfToken() - }, - body: JSON.stringify({ - content: content - }) - }); - - if (!response.ok) { - throw new Error('Failed to edit comment'); - } - - return await response.json(); - } catch (error) { - this.showError('Error editing comment: ' + error.message); - throw error; - } - } - - async getThreadComments(threadId) { - try { - const response = await fetch(`/vcs/comments/threads/${threadId}/`); - if (!response.ok) { - throw new Error('Failed to fetch thread comments'); - } - return await response.json(); - } catch (error) { - this.showError('Error fetching comments: ' + error.message); - throw error; - } - } - - initializeCommentPanel(containerId, options = {}) { - const panel = new InlineCommentPanel({ - ...options, - onReply: async (content, parentId) => { - const comment = await this.createComment( - options.threadId, - content, - parentId - ); - const thread = await this.getThreadComments(options.threadId); - panel.setThread(thread); - }, - onResolve: async () => { - await this.resolveThread(options.threadId); - const thread = await this.getThreadComments(options.threadId); - panel.setThread(thread); - } - }); - panel.initialize(containerId); - return panel; - } - - // Branch locking operations - async acquireLock(branchName, duration = 48, reason = "") { - try { - const response = await fetch('/vcs/branches/lock/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCsrfToken() - }, - body: JSON.stringify({ - branch: branchName, - duration: duration, - reason: reason - }) - }); - - if (!response.ok) { - throw new Error('Failed to acquire lock'); - } - - const result = await response.json(); - if (result.success) { - this.refreshLockStatus(branchName); - return true; - } - return false; - } catch (error) { - this.showError('Error acquiring lock: ' + error.message); - return false; - } - } - - async releaseLock(branchName, force = false) { - try { - const response = await fetch('/vcs/branches/unlock/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCsrfToken() - }, - body: JSON.stringify({ - branch: branchName, - force: force - }) - }); - - if (!response.ok) { - throw new Error('Failed to release lock'); - } - - const result = await response.json(); - if (result.success) { - this.refreshLockStatus(branchName); - return true; - } - return false; - } catch (error) { - this.showError('Error releasing lock: ' + error.message); - return false; - } - } - - async getLockStatus(branchName) { - try { - const response = await fetch(`/vcs/branches/${encodeURIComponent(branchName)}/lock-status/`); - if (!response.ok) { - throw new Error('Failed to get lock status'); - } - return await response.json(); - } catch (error) { - this.showError('Error getting lock status: ' + error.message); - return null; - } - } - - async getLockHistory(branchName, limit = 10) { - try { - const response = await fetch( - `/vcs/branches/${encodeURIComponent(branchName)}/lock-history/?limit=${limit}` - ); - if (!response.ok) { - throw new Error('Failed to get lock history'); - } - return await response.json(); - } catch (error) { - this.showError('Error getting lock history: ' + error.message); - return []; - } - } - - async refreshLockStatus(branchName) { - const lockStatus = await this.getLockStatus(branchName); - if (!lockStatus) return; - - const statusElement = document.querySelector(`[data-branch="${branchName}"] .lock-status`); - if (!statusElement) return; - - if (lockStatus.locked) { - const expiryDate = new Date(lockStatus.expires); - statusElement.className = 'lock-status locked'; - statusElement.innerHTML = ` - - - -
- ${lockStatus.user} - Expires: ${expiryDate.toLocaleString()} - ${lockStatus.reason ? `${lockStatus.reason}` : ''} -
- `; - } else { - statusElement.className = 'lock-status unlocked'; - statusElement.innerHTML = ` - - - - Unlocked - `; - } - - // Update lock controls - this.updateLockControls(branchName, lockStatus); - } - - async updateLockControls(branchName, lockStatus) { - const controlsElement = document.querySelector(`[data-branch="${branchName}"] .lock-controls`); - if (!controlsElement) return; - - if (lockStatus.locked) { - controlsElement.innerHTML = ` - - `; - } else { - controlsElement.innerHTML = ` - - `; - } - } - - // Utility functions - getCsrfToken() { - return document.querySelector('[name=csrfmiddlewaretoken]').value; - } - - showError(message) { - const errorDiv = document.createElement('div'); - errorDiv.className = 'bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4'; - errorDiv.innerHTML = ` -
-
- - - -
-
-

${message}

-
-
- `; - document.querySelector('.version-control-ui').prepend(errorDiv); - setTimeout(() => errorDiv.remove(), 5000); - } -} - -// Import DiffViewer component -import DiffViewer from './components/diff-viewer.js'; - -// Initialize version control -document.addEventListener('DOMContentLoaded', () => { - window.versionControl = new VersionControl(); - - // Initialize DiffViewer if diff container exists - const diffContainer = document.getElementById('diff-container'); - if (diffContainer) { - window.diffViewer = new DiffViewer({ - renderStrategy: 'side-by-side' - }); - diffViewer.initialize('diff-container'); - } -}); - -// Add to VersionControl class constructor -class VersionControl { - constructor() { - this.setupEventListeners(); - this.diffViewer = null; - if (document.getElementById('diff-container')) { - this.diffViewer = new DiffViewer({ - renderStrategy: 'side-by-side' - }); - this.diffViewer.initialize('diff-container'); - } - } - - // Add getDiff method to VersionControl class - async getDiff(version1, version2) { - const url = `/vcs/diff/?v1=${encodeURIComponent(version1)}&v2=${encodeURIComponent(version2)}`; - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error('Failed to fetch diff'); - } - const diffData = await response.json(); - - if (this.diffViewer) { - await this.diffViewer.render(diffData); - } - - return diffData; - } catch (error) { - this.showError('Error loading diff: ' + error.message); - throw error; - } - } - - // Add viewChanges method to VersionControl class - async viewChanges(changeId) { - try { - const response = await fetch(`/vcs/changes/${changeId}/`); - if (!response.ok) { - throw new Error('Failed to fetch changes'); - } - const changeData = await response.json(); - - if (this.diffViewer) { - await this.diffViewer.render(changeData); - } else { - this.showError('Diff viewer not initialized'); - } - } catch (error) { - this.showError('Error loading changes: ' + error.message); - throw error; - } - } - - // Add addComment method to VersionControl class - async addComment(changeId, anchor, content) { - try { - const response = await fetch('/vcs/comments/add/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCsrfToken() - }, - body: JSON.stringify({ - change_id: changeId, - anchor: anchor, - content: content - }) - }); - - if (!response.ok) { - throw new Error('Failed to add comment'); - } - - const comment = await response.json(); - if (this.diffViewer) { - this.diffViewer.addCommentThread(anchor, [comment]); - } - - return comment; - } catch (error) { - this.showError('Error adding comment: ' + error.message); - throw error; - } - } \ No newline at end of file diff --git a/templates/base.html b/templates/base.html deleted file mode 100644 index 82556675..00000000 --- a/templates/base.html +++ /dev/null @@ -1,169 +0,0 @@ - - - - - - {% block title %}ThrillWiki{% endblock %} - - - {% csrf_token %} - - - - - - - - - - - - - - - - - - - - {% block extra_css %}{% endblock %} - - - -
- -
- - -
- {% if messages %} -
- {% for message in messages %} -
- {{ message }} -
- {% endfor %} -
- {% endif %} - - {% block content %}{% endblock %} -
- - - - - - {% block extra_js %}{% endblock %} - - - {% if version_control.current_branch %} -
-
- Branch: - {{ version_control.branch_name }} - {% if version_control.recent_changes %} - - {{ version_control.recent_changes|length }} changes - - {% endif %} -
-
- {% endif %} - - \ No newline at end of file diff --git a/templates/moderation/dashboard.html b/templates/moderation/dashboard.html index 3222dbb3..0d53581d 100644 --- a/templates/moderation/dashboard.html +++ b/templates/moderation/dashboard.html @@ -146,196 +146,152 @@ {% block content %}
-
+
{% block moderation_content %} {% include "moderation/partials/dashboard_content.html" %} {% endblock %} -