- ```
-
-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 %}
-
- {% endfor %}
-
- {% else %}
-
No rides listed yet.
- {% endif %}
-
-
-
- {% if park.areas.exists %}
-
-
Areas
-
- {% for area in park.areas.all %}
-
-
- {% 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 %}
-
- {% 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 %}
-
-
-
- {% 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 }}
-
-
-
-
-
-
Reviewer
-
- {% if review.user.avatar %}
-
- {% 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.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 %}
-
-
- {% if page_obj.has_previous %}
-
- Previous
-
- {% endif %}
-
- {% if page_obj.has_next %}
-
- Next
-
- {% endif %}
-
-
- {% 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 %}
-
- {% 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 %}
-
- {% 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 %}
-
-
-
- {% endif %}
-
-
-
-
-
- {{ 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 %}
-
-
- {% if page_obj.has_previous %}
-
- Previous
-
- {% endif %}
-
- {% if page_obj.has_next %}
-
- Next
-
- {% endif %}
-
-
- {% 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 = `
-
-
-
-
- ${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.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 ? `
-
- ` : ''}
-
- `;
- }
-
- _renderApprovalActions(currentStage) {
- if (!this.canUserApprove(currentStage)) {
- return '';
- }
-
- return `
-
-
-
-
- Reject Changes
-
-
- Approve Changes
-
-
-
- `;
- }
-
- _renderHistory() {
- if (!this.changeset.approval_history?.length) {
- return '';
- }
-
- return `
-
- ${this.changeset.approval_history.map(entry => `
-
-
-
- ${this._formatHistoryAction(entry.action)}
-
- ${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 = `
-
-
-
- ${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]) => `
-
-
-
-
-
- ${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]) => `
-
-
-
-
- ${this.renderLineNumbers(change.metadata.line_numbers.new)}
-
-
- ${this.renderInlineDiff(change.old, change.new)}
-
-
-
- `).join('');
- }
-
- renderMetadata(metadata) {
- return `
-
- ${new Date(metadata.timestamp).toLocaleString()}
- ${metadata.user || 'Anonymous'}
- ${this.formatChangeType(metadata.change_type)}
- ${metadata.reason ? `${metadata.reason} ` : ''}
-
- `;
- }
-
- renderControls() {
- return `
-
- Side by Side
- Inline
- Collapse All
- Expand All
-
- `;
- }
-
- renderNavigation(navigation) {
- return `
-
- ${navigation.prev_id ? `Previous ` : ''}
- ${navigation.next_id ? `Next ` : ''}
- 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 => `
-
- `).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.attachEventListeners();
- }
-
- renderComments(comments) {
- return comments.map(comment => `
-
- `).join('');
- }
-
- renderCommentActions(comment) {
- if (this.thread.is_resolved) return '';
-
- return `
-
- `;
- }
-
- 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 = `
-
-
-
- Cancel
-
-
- Reply
-
-
- `;
-
- 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 = `
-
-
-
- Cancel
-
-
- Save
-
-
- `;
- }
-
- 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 = `
-
- `;
-
- 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 ? `
-
- Compare Versions
-
- ` : ''}
- ${canRollback ? `
-
- Rollback to Version
-
- ` : ''}
- `;
- }
-
- _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 `
-
-
-
- ${this._renderChanges(diff.changes)}
-
-
- `;
- }
-
- _renderChanges(changes) {
- return changes.map(change => `
-
-
-
-
-
-
${this._escapeHtml(change.old_value)}
-
-
-
-
${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 ? '
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 = '
Select area ';
- 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 = `
-
-
-
-
- Unlock Branch
-
- `;
- } else {
- controlsElement.innerHTML = `
-
-
-
-
- Lock Branch
-
- `;
- }
- }
-
- // 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 = `
-
- `;
- 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 %}
-
-
-
-
-
-
-
-
About
-
- ThrillWiki is your source for theme park and attraction information.
-
-
-
-
-
-
-
-
© {% now "Y" %} ThrillWiki. All rights reserved.
-
-
-
-
-
- {% 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 %}
-
+
{% include "moderation/partials/loading_skeleton.html" %}
-
-
+
+
-
+
Something went wrong
-
-
-
-
- Retrying...
-
-
-
- Retry
-
+
+ There was a problem loading the content. Please try again.
+
+
+
+ Retry
-
-
-
-
-
-
{% endblock %}
{% block extra_js %}
-
-
-
-
-
-
-
-
-
-
-
{% endblock %}
diff --git a/templates/moderation/partials/filters_store.html b/templates/moderation/partials/filters_store.html
index 06122d3f..35e1be75 100644
--- a/templates/moderation/partials/filters_store.html
+++ b/templates/moderation/partials/filters_store.html
@@ -1,69 +1,129 @@
-{% load static %}
+{% comment %}
+This template contains the Alpine.js store for managing filter state in the moderation dashboard
+{% endcomment %}
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/templates/moderation/partials/loading_skeleton.html b/templates/moderation/partials/loading_skeleton.html
index 3376534d..082ffe94 100644
--- a/templates/moderation/partials/loading_skeleton.html
+++ b/templates/moderation/partials/loading_skeleton.html
@@ -1,69 +1,66 @@
{% load static %}
-
-
-
+
+
+
- {% for i in '1234'|make_list %}
+ {% for i in "1234" %}
{% endfor %}
-
-
-
-
- {% for i in '123'|make_list %}
-
- {% endfor %}
-
-
-
-
-
- {% for i in '123'|make_list %}
-
-
-
-
-
-
- {% for j in '1234'|make_list %}
-
- {% endfor %}
-
-
-
-
-
-
-
- {% for j in '1234'|make_list %}
-
- {% endfor %}
-
-
-
-
- {% for j in '1234'|make_list %}
-
- {% endfor %}
-
-
-
+
+
+
+ {% for i in "123" %}
+
{% endfor %}
+
+
+ {% for i in "123" %}
+
+
+
+
+
+
+ {% for i in "1234" %}
+
+ {% endfor %}
+
+
+
+
+
+ {% for i in "12" %}
+
+ {% endfor %}
+
+
+ {% for i in "1234" %}
+
+ {% endfor %}
+
+
+
+
+ {% endfor %}
\ No newline at end of file
diff --git a/templates/moderation/partials/submission_list.html b/templates/moderation/partials/submission_list.html
index f41928a3..c5737242 100644
--- a/templates/moderation/partials/submission_list.html
+++ b/templates/moderation/partials/submission_list.html
@@ -6,10 +6,8 @@
{% endblock %}
{% for submission in submissions %}
-
@@ -231,23 +224,13 @@
Permanently Closed
{% elif field == 'opening_date' or field == 'closing_date' or field == 'status_since' %}
-
+
{% else %}
{% if field == 'park' %}
@@ -356,24 +339,12 @@
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
placeholder="General description and notable features">{{ value }}
{% elif field == 'min_height_in' or field == 'max_height_in' %}
-
+
{% elif field == 'capacity_per_hour' %}
-
+
Notes (required):
-
+
@@ -462,93 +424,52 @@
rows="3">
-
{% endif %}
diff --git a/tests/test_runner.py b/tests/test_runner.py
index 6fc6ca7b..d088257c 100644
--- a/tests/test_runner.py
+++ b/tests/test_runner.py
@@ -4,7 +4,7 @@ import sys
import django
from django.conf import settings
from django.test.runner import DiscoverRunner
-import coverage # type: ignore
+import coverage
import unittest
def setup_django():
diff --git a/thrillwiki/settings.py b/thrillwiki/settings.py
index 27011ec2..ed356cdf 100644
--- a/thrillwiki/settings.py
+++ b/thrillwiki/settings.py
@@ -53,7 +53,6 @@ INSTALLED_APPS = [
"designers",
"analytics",
"location",
- "comments",
]
MIDDLEWARE = [
@@ -87,7 +86,6 @@ TEMPLATES = [
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"moderation.context_processors.moderation_access",
- "history_tracking.context_processors.version_control",
],
},
},
@@ -110,22 +108,14 @@ DATABASES = {
# Cache settings
CACHES = {
"default": {
- "BACKEND": "django_redis.cache.RedisCache",
- "LOCATION": "redis://192.168.86.3:6379/1",
- "OPTIONS": {
- "CLIENT_CLASS": "django_redis.client.DefaultClient",
- "SOCKET_CONNECT_TIMEOUT": 5,
- "SOCKET_TIMEOUT": 5,
- "RETRY_ON_TIMEOUT": True,
- "MAX_CONNECTIONS": 1000,
- "PARSER_CLASS": "redis.connection.HiredisParser",
- },
- "KEY_PREFIX": "thrillwiki",
+ "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
+ "LOCATION": "unique-snowflake",
"TIMEOUT": 300, # 5 minutes
+ "OPTIONS": {"MAX_ENTRIES": 1000},
}
}
-CACHE_MIDDLEWARE_SECONDS = 300 # 5 minutes
+CACHE_MIDDLEWARE_SECONDS = 1 # 5 minutes
CACHE_MIDDLEWARE_KEY_PREFIX = "thrillwiki"
# Password validation
diff --git a/thrillwiki/urls.py b/thrillwiki/urls.py
index e3e14eb5..1dc28f9f 100644
--- a/thrillwiki/urls.py
+++ b/thrillwiki/urls.py
@@ -52,8 +52,6 @@ urlpatterns = [
path("user/", accounts_views.user_redirect_view, name="user_redirect"),
# Moderation URLs - placed after other URLs but before static/media serving
path("moderation/", include("moderation.urls", namespace="moderation")),
- # Version Control System URLs
- path("vcs/", include("history_tracking.urls", namespace="history")),
path(
"env-settings/",
views.environment_and_settings_view,
diff --git a/uv.lock b/uv.lock
index bb736581..f2747247 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,18 +1,6 @@
version = 1
requires-python = ">=3.13"
-[[package]]
-name = "amqp"
-version = "5.3.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "vine" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944 },
-]
-
[[package]]
name = "asgiref"
version = "3.8.1"
@@ -55,15 +43,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/af/cc/55a32a2c98022d88812b5986d2a92c4ff3ee087e83b712ebc703bba452bf/Automat-24.8.1-py3-none-any.whl", hash = "sha256:bf029a7bc3da1e2c24da2343e7598affaa9f10bf0ab63ff808566ce90551e02a", size = 42585 },
]
-[[package]]
-name = "billiard"
-version = "4.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7c/58/1546c970afcd2a2428b1bfafecf2371d8951cc34b46701bea73f4280989e/billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f", size = 155031 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/30/da/43b15f28fe5f9e027b41c539abc5469052e9d48fd75f8ff094ba2a0ae767/billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb", size = 86766 },
-]
-
[[package]]
name = "black"
version = "24.10.0"
@@ -84,26 +63,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898 },
]
-[[package]]
-name = "celery"
-version = "5.4.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "billiard" },
- { name = "click" },
- { name = "click-didyoumean" },
- { name = "click-plugins" },
- { name = "click-repl" },
- { name = "kombu" },
- { name = "python-dateutil" },
- { name = "tzdata" },
- { name = "vine" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/8a/9c/cf0bce2cc1c8971bf56629d8f180e4ca35612c7e79e6e432e785261a8be4/celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706", size = 1575692 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/90/c4/6a4d3772e5407622feb93dd25c86ce3c0fee746fa822a777a627d56b4f2a/celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64", size = 425983 },
-]
-
[[package]]
name = "certifi"
version = "2024.12.14"
@@ -197,43 +156,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
]
-[[package]]
-name = "click-didyoumean"
-version = "0.3.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631 },
-]
-
-[[package]]
-name = "click-plugins"
-version = "1.1.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5f/1d/45434f64ed749540af821fd7e42b8e4d23ac04b1eda7c26613288d6cd8a8/click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", size = 8164 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e9/da/824b92d9942f4e472702488857914bdd50f73021efea15b4cad9aca8ecef/click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8", size = 7497 },
-]
-
-[[package]]
-name = "click-repl"
-version = "0.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "prompt-toolkit" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289 },
-]
-
[[package]]
name = "colorama"
version = "0.4.6"
@@ -406,19 +328,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/40/e556bc19ba65356fe5f0e48ca01c50e81f7c630042fa7411b6ab428ecf68/django_oauth_toolkit-3.0.1-py3-none-any.whl", hash = "sha256:3ef00b062a284f2031b0732b32dc899e3bbf0eac221bbb1cffcb50b8932e55ed", size = 77299 },
]
-[[package]]
-name = "django-redis"
-version = "5.4.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "django" },
- { name = "redis" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/83/9d/2272742fdd9d0a9f0b28cd995b0539430c9467a2192e4de2cea9ea6ad38c/django-redis-5.4.0.tar.gz", hash = "sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42", size = 52567 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/f1/63caad7c9222c26a62082f4f777de26389233b7574629996098bf6d25a4d/django_redis-5.4.0-py3-none-any.whl", hash = "sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b", size = 31119 },
-]
-
[[package]]
name = "django-simple-history"
version = "3.7.0"
@@ -574,20 +483,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/58/4a1880ea64032185e9ae9f63940c9327c6952d5584ea544a8f66972f2fda/jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789", size = 92520 },
]
-[[package]]
-name = "kombu"
-version = "5.4.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "amqp" },
- { name = "tzdata" },
- { name = "vine" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/38/4d/b93fcb353d279839cc35d0012bee805ed0cf61c07587916bfc35dbfddaf1/kombu-5.4.2.tar.gz", hash = "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf", size = 442858 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/87/ec/7811a3cf9fdfee3ee88e54d08fcbc3fabe7c1b6e4059826c59d7b795651c/kombu-5.4.2-py3-none-any.whl", hash = "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763", size = 201349 },
-]
-
[[package]]
name = "mccabe"
version = "0.7.0"
@@ -715,18 +610,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
]
-[[package]]
-name = "prompt-toolkit"
-version = "3.0.50"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "wcwidth" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 },
-]
-
[[package]]
name = "psycopg2-binary"
version = "2.9.10"
@@ -743,7 +626,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712 },
{ url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155 },
{ url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356 },
- { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 },
]
[[package]]
@@ -891,18 +773,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/72/38/e95090d0a26114a650f7d8847425f21757fc740de965df94de47b439fe67/pytest_playwright-0.6.2-py3-none-any.whl", hash = "sha256:0eff73bebe497b0158befed91e2f5fe94cfa17181f8b3acf575beed84e7e9043", size = 16436 },
]
-[[package]]
-name = "python-dateutil"
-version = "2.9.0.post0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "six" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
-]
-
[[package]]
name = "python-dotenv"
version = "1.0.1"
@@ -981,15 +851,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
]
-[[package]]
-name = "six"
-version = "1.17.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
-]
-
[[package]]
name = "sqlparse"
version = "0.5.3"
@@ -1014,7 +875,6 @@ version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "black" },
- { name = "celery" },
{ name = "channels" },
{ name = "channels-redis" },
{ name = "daphne" },
@@ -1027,7 +887,6 @@ dependencies = [
{ name = "django-filter" },
{ name = "django-htmx" },
{ name = "django-oauth-toolkit" },
- { name = "django-redis" },
{ name = "django-simple-history" },
{ name = "django-tailwind-cli" },
{ name = "django-webpack-loader" },
@@ -1049,7 +908,6 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "black", specifier = ">=24.1.0" },
- { name = "celery", specifier = ">=5.4.0" },
{ name = "channels", specifier = ">=4.2.0" },
{ name = "channels-redis", specifier = ">=4.2.1" },
{ name = "daphne", specifier = ">=4.1.2" },
@@ -1062,7 +920,6 @@ requires-dist = [
{ name = "django-filter", specifier = ">=23.5" },
{ name = "django-htmx", specifier = ">=1.17.2" },
{ name = "django-oauth-toolkit", specifier = ">=3.0.1" },
- { name = "django-redis", specifier = ">=5.4.0" },
{ name = "django-simple-history", specifier = ">=3.5.0" },
{ name = "django-tailwind-cli", specifier = ">=2.21.1" },
{ name = "django-webpack-loader", specifier = ">=3.1.1" },
@@ -1155,24 +1012,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
]
-[[package]]
-name = "vine"
-version = "5.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636 },
-]
-
-[[package]]
-name = "wcwidth"
-version = "0.2.13"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
-]
-
[[package]]
name = "whitenoise"
version = "6.8.2"
Preview:
-