Add version control context processor and integrate map functionality with dedicated JavaScript

This commit is contained in:
pacnpal
2025-02-06 20:06:10 -05:00
parent f3d28817a5
commit ecf94bf84e
16 changed files with 1671 additions and 89 deletions

View File

@@ -0,0 +1,43 @@
from typing import Dict, Any
from django.http import HttpRequest
from .signals import get_current_branch
from .models import VersionBranch, ChangeSet
def version_control(request: HttpRequest) -> Dict[str, Any]:
"""
Add version control information to the template context
"""
current_branch = get_current_branch()
context = {
'vcs_enabled': True,
'current_branch': current_branch,
'recent_changes': []
}
if current_branch:
# Get recent changes for the current branch
recent_changes = ChangeSet.objects.filter(
branch=current_branch,
status='applied'
).order_by('-created_at')[:5]
context.update({
'recent_changes': recent_changes,
'branch_name': current_branch.name,
'branch_metadata': current_branch.metadata
})
# Get available branches for switching
context['available_branches'] = VersionBranch.objects.filter(
is_active=True
).order_by('-created_at')
# Check if current page is versioned
if hasattr(request, 'resolver_match') and request.resolver_match:
view_func = request.resolver_match.func
if hasattr(view_func, 'view_class'):
view_class = view_func.view_class
context['page_is_versioned'] = hasattr(view_class, 'model') and \
hasattr(view_class.model, 'history')
return {'version_control': context}

View File

@@ -1,8 +1,9 @@
from typing import Optional, List, Dict, Any, Tuple
from typing import Optional, List, Dict, Any, Tuple, Union
from django.db import transaction
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractUser
from django.contrib.contenttypes.models import ContentType
from .models import VersionBranch, VersionTag, ChangeSet
@@ -11,10 +12,8 @@ User = get_user_model()
class BranchManager:
"""Manages version control branch operations"""
@transaction.atomic
def create_branch(self, name: str, parent: Optional[VersionBranch] = None,
user: Optional[User] = None) -> VersionBranch:
"""Create a new version branch"""
user: Optional['User'] = None) -> VersionBranch:
branch = VersionBranch.objects.create(
name=name,
parent=parent,
@@ -29,7 +28,7 @@ class BranchManager:
@transaction.atomic
def merge_branches(self, source: VersionBranch, target: VersionBranch,
user: Optional[User] = None) -> Tuple[bool, List[Dict[str, Any]]]:
user: Optional['User'] = None) -> Tuple[bool, List[Dict[str, Any]]]:
"""
Merge source branch into target branch
Returns: (success, conflicts)
@@ -66,9 +65,8 @@ class BranchManager:
class ChangeTracker:
"""Tracks and manages changes across the system"""
@transaction.atomic
def record_change(self, instance: Any, change_type: str,
branch: VersionBranch, user: Optional[User] = None,
branch: VersionBranch, user: Optional['User'] = None,
metadata: Optional[Dict] = None) -> ChangeSet:
"""Record a change in the system"""
if not hasattr(instance, 'history'):

View File

@@ -0,0 +1,94 @@
{% if version_control.vcs_enabled and version_control.page_is_versioned %}
<div class="version-control-ui bg-white shadow-sm rounded-lg p-4 mb-4">
<!-- Branch Information -->
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-lg font-semibold">Version Control</h3>
{% if version_control.current_branch %}
<p class="text-sm text-gray-600">
Current Branch:
<span class="font-medium">{{ version_control.branch_name }}</span>
</p>
{% endif %}
</div>
<!-- Branch Selection -->
<div class="relative" x-data="{ open: false }">
<button @click="open = !open"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
Branch Actions
</button>
<div x-show="open"
@click.away="open = false"
class="absolute right-0 mt-2 py-2 w-48 bg-white rounded-lg shadow-xl z-50">
<!-- Create Branch -->
<button class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 w-full text-left"
hx-get="{% url 'history:branch-create' %}"
hx-target="#branch-form-container">
Create Branch
</button>
<!-- Switch Branch -->
{% if version_control.available_branches %}
<div class="border-t border-gray-100 my-2"></div>
{% for branch in version_control.available_branches %}
<button class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 w-full text-left"
hx-post="{% url 'history:switch-branch' %}"
hx-vals='{"branch": "{{ branch.name }}"}'
hx-target="body">
Switch to {{ branch.name }}
</button>
{% endfor %}
{% endif %}
</div>
</div>
</div>
<!-- Recent Changes -->
{% if version_control.recent_changes %}
<div class="mt-4">
<h4 class="text-sm font-semibold text-gray-700 mb-2">Recent Changes</h4>
<div class="space-y-2">
{% for change in version_control.recent_changes %}
<div class="bg-gray-50 p-2 rounded text-sm">
<div class="flex justify-between items-start">
<div>
<span class="font-medium">{{ change.description }}</span>
<p class="text-xs text-gray-500">
{{ change.created_at|date:"M d, Y H:i" }}
{% if change.created_by %}
by {{ change.created_by.username }}
{% endif %}
</p>
</div>
<span class="px-2 py-1 text-xs rounded
{% if change.status == 'applied' %}
bg-green-100 text-green-800
{% elif change.status == 'pending' %}
bg-yellow-100 text-yellow-800
{% elif change.status == 'failed' %}
bg-red-100 text-red-800
{% endif %}">
{{ change.status|title }}
</span>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Branch Form Container -->
<div id="branch-form-container"></div>
<!-- Merge Panel -->
<div id="merge-panel"></div>
</div>
<!-- Scripts -->
<script>
document.body.addEventListener('branch-switched', function(e) {
location.reload();
});
</script>
{% endif %}

View File

@@ -1,17 +1,53 @@
from typing import Dict, Any, List, Optional
from typing import Dict, Any, List, Optional, TypeVar, Type, Union, cast
from django.core.exceptions import ValidationError
from .models import VersionBranch, ChangeSet
from django.utils import timezone
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractUser
from django.db.models import Model
User = get_user_model()
UserModel = TypeVar('UserModel', bound=AbstractUser)
User = cast(Type[UserModel], get_user_model())
def _handle_source_target_resolution(change: ChangeSet) -> Dict[str, Any]:
resolved = {}
for record in change.historical_records.all():
resolved[f"{record.instance_type}_{record.instance_pk}"] = record
return resolved
def _handle_manual_resolution(
conflict_id: str,
source_change: ChangeSet,
manual_resolutions: Dict[str, str],
user: Optional[UserModel]
) -> Dict[str, Any]:
manual_content = manual_resolutions.get(conflict_id)
if not manual_content:
raise ValidationError(f"Manual resolution missing for conflict {conflict_id}")
resolved = {}
base_record = source_change.historical_records.first()
if base_record:
new_record = base_record.__class__(
**{
**base_record.__dict__,
'id': base_record.id,
'history_date': timezone.now(),
'history_user': user,
'history_change_reason': 'Manual conflict resolution',
'history_type': '~'
}
)
for field, value in manual_content.items():
setattr(new_record, field, value)
resolved[f"{new_record.instance_type}_{new_record.instance_pk}"] = new_record
return resolved
def resolve_conflicts(
source_branch: VersionBranch,
target_branch: VersionBranch,
resolutions: Dict[str, str],
manual_resolutions: Dict[str, str],
user: Optional[User] = None
user: Optional[UserModel] = None
) -> ChangeSet:
"""
Resolve merge conflicts between branches
@@ -37,40 +73,14 @@ def resolve_conflicts(
target_change = ChangeSet.objects.get(pk=target_id)
if resolution_type == 'source':
# Use source branch version
for record in source_change.historical_records.all():
resolved_content[f"{record.instance_type}_{record.instance_pk}"] = record
resolved_content.update(_handle_source_target_resolution(source_change))
elif resolution_type == 'target':
# Use target branch version
for record in target_change.historical_records.all():
resolved_content[f"{record.instance_type}_{record.instance_pk}"] = record
resolved_content.update(_handle_source_target_resolution(target_change))
elif resolution_type == 'manual':
# Use manual resolution
manual_content = manual_resolutions.get(conflict_id)
if not manual_content:
raise ValidationError(f"Manual resolution missing for conflict {conflict_id}")
resolved_content.update(_handle_manual_resolution(
conflict_id, source_change, manual_resolutions, user
))
# Create new historical record with manual content
base_record = source_change.historical_records.first()
if base_record:
new_record = base_record.__class__(
**{
**base_record.__dict__,
'id': base_record.id,
'history_date': timezone.now(),
'history_user': user,
'history_change_reason': 'Manual conflict resolution',
'history_type': '~'
}
)
# Apply manual changes
for field, value in manual_content.items():
setattr(new_record, field, value)
resolved_content[f"{new_record.instance_type}_{new_record.instance_pk}"] = new_record
# Create resolution changeset
resolution_changeset = ChangeSet.objects.create(
branch=target_branch,
created_by=user,
@@ -83,7 +93,6 @@ def resolve_conflicts(
status='applied'
)
# Add resolved records to changeset
for record in resolved_content.values():
resolution_changeset.historical_records.add(record)

View File

@@ -1,58 +1,99 @@
# Active Development Context
## Recently Completed
- Implemented Version Control System enhancement
- Core models and database schema
- Business logic layer with managers
- HTMX-based frontend integration
- API endpoints and URL configuration
- Signal handlers for automatic tracking
- Documentation updated in `memory-bank/features/version-control/`
## Current Implementation Status
Version Control System has been implemented with core functionality and initial integration:
## Current Status
The Version Control System has been fully implemented according to the implementation plan and technical guide. The system provides:
- Branch management
- Change tracking
- Version tagging
- Merge operations with conflict resolution
- Real-time UI updates via HTMX
### Completed
1. Core VCS Components:
- Base models (VersionBranch, VersionTag, ChangeSet)
- Business logic (BranchManager, ChangeTracker, MergeStrategy)
- UI components and templates
- Asset integration (JS/CSS)
## Next Steps
1. Testing
- Create comprehensive test suite
- Test branch operations
- Test merge scenarios
- Test conflict resolution
2. Initial Integration:
- Park model VCS integration
- ParkArea model VCS integration
- Base template VCS support
- Park detail template integration
- Version control context processor
2. Monitoring
- Implement performance metrics
- Track merge success rates
- Monitor system health
3. Documentation:
- Technical implementation guide
- Template integration guide
- Implementation checklist
- Base README
3. Documentation
- Create user guide
- Document API endpoints
- Add inline code documentation
### In Progress
1. Model Integration:
- [ ] Rides system
- [ ] Reviews system
- [ ] Companies system
4. Future Enhancements
- Branch locking mechanism
- Advanced merge strategies
- Custom diff viewers
- Performance optimizations
2. Template Updates:
- [ ] Park list view
- [ ] Ride detail/list views
- [ ] Review detail/list views
- [ ] Company detail/list views
## Immediate Next Steps
1. Model Integration (Priority)
```python
# Add to rides/models.py:
class Ride(HistoricalModel):
# Update save method
def save(self, *args, **kwargs):
from history_tracking.signals import get_current_branch, ChangesetContextManager
# Add version control logic
```
2. Template Updates
```html
<!-- Add to each list template -->
{% if version_control.vcs_enabled %}
{% include "history_tracking/includes/version_status.html" %}
{% endif %}
```
3. Testing Setup
- Create test cases for model integration
- Verify UI functionality
- Test version control operations
## Active Issues
None at present, awaiting testing phase to identify any issues.
1. Need to ensure consistent version control behavior across models
2. Must handle relationships between versioned models
3. Need to implement proper cleanup for old versions
## Recent Decisions
- Used GenericForeignKey for flexible history tracking
- Implemented HTMX for real-time updates
- Structured change tracking with atomic changesets
- Integrated with django-simple-history
## Technical Dependencies
- django-simple-history: Base history tracking
- HTMX: UI interactions
- Alpine.js: Frontend reactivity
- Custom VCS components
## Technical Debt
- Need comprehensive test suite
- Performance monitoring to be implemented
- Documentation needs to be expanded
## Integration Strategy
1. Roll out model integration one app at a time
2. Update templates to include version control UI
3. Add list view version indicators
4. Implement relationship handling
## Monitoring Points
- Track version control operation performance
- Monitor database size with version history
- Watch for merge conflicts
- Track user interaction patterns
## Code Standards
- All versioned models inherit from HistoricalModel
- Consistent save method implementation
- Proper branch context management
- Standard version control UI components
## Documentation Status
- [x] Technical implementation
- [x] Template integration guide
- [ ] API documentation
- [ ] User guide
- [ ] Admin documentation
## Current Branch
main
@@ -60,4 +101,5 @@ main
## Environment
- Django with HTMX integration
- PostgreSQL database
- django-simple-history for base tracking
- django-simple-history
- Custom VCS extensions

View File

@@ -0,0 +1,228 @@
# ThrillWiki Version Control System
## Overview
The ThrillWiki Version Control System (VCS) provides comprehensive version tracking, branching, and merging capabilities for all content in the system. It builds upon django-simple-history while adding powerful versioning features.
## Features
- Full version history tracking
- Branch-based development
- Version tagging
- Merge operations with conflict resolution
- Real-time collaborative editing
- Automatic change tracking
## Model Integration
### Making Models Version-Controlled
To add version control to any model, inherit from `HistoricalModel`:
```python
from history_tracking.models import HistoricalModel
class YourModel(HistoricalModel):
# Your model fields here
name = models.CharField(max_length=255)
class Meta:
# Your meta options
```
This automatically provides:
- Full version history
- Change tracking
- Branch support
- Merge capabilities
### Example Integration (from parks/models.py)
```python
from history_tracking.models import HistoricalModel
class Park(HistoricalModel):
name = models.CharField(max_length=255)
description = models.TextField()
def save(self, *args, **kwargs):
# Changes will be automatically tracked
super().save(*args, **kwargs)
```
## Usage Guide
### Basic Version Control Operations
1. Creating a Branch
```python
from history_tracking.managers import BranchManager
# Create a new feature branch
branch_manager = BranchManager()
feature_branch = branch_manager.create_branch(
name="feature/new-park-details",
user=request.user
)
```
2. Recording Changes
```python
from history_tracking.signals import ChangesetContextManager
# Making changes in a specific branch
with ChangesetContextManager(branch=feature_branch, user=request.user):
park = Park.objects.get(id=1)
park.description = "Updated description"
park.save() # Change is automatically tracked in the branch
```
3. Merging Changes
```python
# Merge feature branch back to main
success, conflicts = branch_manager.merge_branches(
source=feature_branch,
target=main_branch,
user=request.user
)
if not success:
# Handle merge conflicts
for conflict in conflicts:
# Resolve conflicts through UI or programmatically
pass
```
4. Working with Tags
```python
from history_tracking.models import VersionTag
# Tag a specific version
VersionTag.objects.create(
name="v1.0.0",
branch=main_branch,
content_type=ContentType.objects.get_for_model(park),
object_id=park.id,
created_by=user
)
```
## UI Integration
### HTMX Components
The system provides HTMX-powered components for real-time version control:
1. Version Control Panel
```html
{% include "history_tracking/version_control_panel.html" %}
```
2. Branch Selection
```html
<div hx-get="{% url 'history:branch-list' %}"
hx-trigger="load, branch-updated from:body">
</div>
```
3. Change History
```html
<div hx-get="{% url 'history:history-view' %}?branch={{ branch.name }}"
hx-trigger="load, branch-selected from:body">
</div>
```
## Best Practices
1. Branch Management
- Create feature branches for significant changes
- Use descriptive branch names (e.g., "feature/new-park-system")
- Clean up merged branches
- Regularly sync with main branch
2. Change Tracking
- Make atomic, related changes
- Provide clear change descriptions
- Group related changes in a single changeset
- Review changes before merging
3. Conflict Resolution
- Resolve conflicts promptly
- Communicate with team members about overlapping changes
- Test after resolving conflicts
- Document resolution decisions
4. Performance
- Use changesets for bulk operations
- Index frequently queried fields
- Clean up old branches and tags
- Monitor system performance
## Error Handling
1. Common Issues
```python
try:
branch_manager.merge_branches(source, target)
except ValidationError as e:
# Handle validation errors
except MergeConflict as e:
# Handle merge conflicts
```
2. Conflict Resolution
```python
from history_tracking.utils import resolve_conflicts
resolved = resolve_conflicts(
source_branch=source,
target_branch=target,
resolutions={
'conflict_id': 'resolution_type', # 'source', 'target', or 'manual'
},
manual_resolutions={
'conflict_id': 'manual resolution content'
},
user=request.user
)
```
## System Maintenance
1. Regular Tasks
- Clean up old branches
- Archive old versions
- Verify data integrity
- Monitor system health
2. Monitoring
```python
from history_tracking.utils import get_system_metrics
metrics = get_system_metrics()
# Check branch counts, merge success rates, etc.
```
## Security Considerations
1. Access Control
- All VCS operations require authentication
- Branch operations are logged
- Merge operations require proper permissions
- Changes are tracked with user attribution
2. Data Protection
- Historical data is preserved
- Audit logs are maintained
- Sensitive data is handled securely
- Backups include version history
## Support and Troubleshooting
For issues or questions:
1. Check the logs for detailed error messages
2. Review the conflict resolution documentation
3. Verify branch and change permissions
4. Contact the development team for assistance
## Contributing
When contributing to the VCS:
1. Follow the established branching pattern
2. Document significant changes
3. Add tests for new features
4. Update technical documentation

View File

@@ -0,0 +1,159 @@
# Version Control System Implementation Checklist
## Core Implementation ✓
- [x] Models
- [x] VersionBranch
- [x] VersionTag
- [x] ChangeSet
- [x] Generic relationships for flexibility
- [x] Managers
- [x] BranchManager
- [x] ChangeTracker
- [x] MergeStrategy
- [x] UI Components
- [x] Version Control Panel
- [x] Branch List
- [x] History View
- [x] Merge Panel
- [x] Branch Creation Form
## Asset Integration ✓
- [x] JavaScript
- [x] Version Control core functionality
- [x] HTMX integration
- [x] Event handling
- [x] CSS
- [x] Version control styles
- [x] Component styles
- [x] Responsive design
## Template Integration
- [x] Base Template Updates
- [x] Required JS/CSS includes
- [x] Version control status bar
- [x] HTMX setup
- [x] Park System
- [x] Park detail template
- [ ] Park list template
- [ ] Area detail template
- [ ] Rides System
- [ ] Ride detail template
- [ ] Ride list template
- [ ] Reviews System
- [ ] Review detail template
- [ ] Review list template
- [ ] Companies System
- [ ] Company detail template
- [ ] Company list template
## Model Integration
- [x] Park Model
- [x] VCS integration
- [x] Save method override
- [x] Version info methods
- [x] ParkArea Model
- [x] VCS integration
- [x] Save method override
- [x] Version info methods
- [ ] Ride Model
- [ ] VCS integration
- [ ] Save method override
- [ ] Version info methods
- [ ] Review Model
- [ ] VCS integration
- [ ] Save method override
- [ ] Version info methods
- [ ] Company Model
- [ ] VCS integration
- [ ] Save method override
- [ ] Version info methods
## Documentation
- [x] README creation
- [x] Implementation guide
- [x] Template integration guide
- [ ] API documentation
- [ ] User guide
## Testing Requirements
- [ ] Unit Tests
- [ ] Model tests
- [ ] Manager tests
- [ ] View tests
- [ ] Form tests
- [ ] Integration Tests
- [ ] Branch operations
- [ ] Merge operations
- [ ] Change tracking
- [ ] UI interactions
- [ ] UI Tests
- [ ] Component rendering
- [ ] User interactions
- [ ] Responsive design
- [ ] Browser compatibility
## Monitoring Setup
- [ ] Performance Metrics
- [ ] Branch operation timing
- [ ] Merge success rates
- [ ] Change tracking overhead
- [ ] UI responsiveness
- [ ] Error Tracking
- [ ] Operation failures
- [ ] Merge conflicts
- [ ] UI errors
- [ ] Performance issues
## Next Steps
1. Complete model integrations:
- Update Ride model
- Update Review model
- Update Company model
2. Template implementations:
- Create remaining detail templates
- Add version control to list views
- Implement version indicators
3. Testing:
- Write comprehensive test suite
- Set up CI/CD integration
- Perform load testing
4. Documentation:
- Complete API documentation
- Create user guide
- Add examples and tutorials
5. Monitoring:
- Set up performance monitoring
- Configure error tracking
- Create dashboards
## Known Issues
1. Need to implement proper error handling in JavaScript
2. Add loading states to UI components
3. Implement proper caching for version history
4. Add batch operations for multiple changes
5. Implement proper cleanup for old versions
## Future Enhancements
1. Add visual diff viewer
2. Implement branch locking
3. Add commenting on changes
4. Create change approval workflow
5. Add version comparison tool

View File

@@ -0,0 +1,86 @@
# Version Control UI Template Integration
## Templates Requiring VCS Integration
### Park System
- [x] parks/templates/parks/park_detail.html - Completed
- [ ] parks/templates/parks/park_list.html - Add version status indicators
- [ ] parks/templates/parks/park_area_detail.html - Add version control UI
### Rides System
- [ ] rides/templates/rides/ride_detail.html - Add version control UI
- [ ] rides/templates/rides/ride_list.html - Add version status indicators
### Reviews System
- [ ] reviews/templates/reviews/review_detail.html - Add version control UI
- [ ] reviews/templates/reviews/review_list.html - Add version status indicators
### Company System
- [ ] companies/templates/companies/company_detail.html - Add version control UI
- [ ] companies/templates/companies/company_list.html - Add version status indicators
## Integration Guidelines
### Detail Templates
For detail templates, add the version control UI below the main title:
```html
<!-- Title Section -->
<h1>{{ object.name }}</h1>
<!-- Version Control UI -->
{% include "history_tracking/includes/version_control_ui.html" %}
<!-- Rest of the content -->
```
### List Templates
For list templates, add version indicators in the list items:
```html
{% for item in object_list %}
<div class="item">
<h2>{{ item.name }}</h2>
{% if version_control.vcs_enabled %}
<div class="version-info text-sm text-gray-600">
Branch: {{ item.get_version_info.current_branch.name }}
</div>
{% endif %}
</div>
{% endfor %}
```
## Integration Steps
1. Update base template to include necessary JavaScript
```html
<!-- In base.html -->
<script src="{% static 'js/version-control.js' %}"></script>
```
2. Add version control UI to detail views
- Include the version control UI component
- Add branch switching functionality
- Display version history
3. Add version indicators to list views
- Show current branch
- Indicate if changes are pending
- Show version status
4. Update view classes
- Ensure models inherit from HistoricalModel
- Add version control context
- Handle branch switching
5. Test integration
- Verify UI appears correctly
- Test branch switching
- Verify history tracking
- Test merge functionality
## Next Steps
1. Create park area detail template with version control
2. Update ride detail template
3. Add version control to review system
4. Integrate with company templates

View File

@@ -0,0 +1,110 @@
# Version Control System UI Improvements
## Recent Improvements
### 1. Template Structure Enhancement
- Moved map initialization to dedicated JavaScript file
- Implemented data attribute pattern for passing data to JavaScript
- Improved template organization and maintainability
### 2. JavaScript Organization
- Created separate `map-init.js` for map functionality
- Established pattern for external JavaScript files
- Improved error handling and script loading
### 3. Asset Management
```javascript
// Static Asset Organization
/static/
/js/
version-control.js // Core VCS functionality
map-init.js // Map initialization logic
/css/
version-control.css // VCS styles
```
## Best Practices Established
### 1. Data Passing Pattern
```html
<!-- Using data attributes for JavaScript configuration -->
<div id="map"
data-lat="{{ coordinates.lat }}"
data-lng="{{ coordinates.lng }}"
data-name="{{ name }}">
</div>
```
### 2. JavaScript Separation
```javascript
// Modular JavaScript organization
document.addEventListener('DOMContentLoaded', function() {
// Initialize components
const mapContainer = document.getElementById('map');
if (mapContainer) {
// Component-specific logic
}
});
```
### 3. Template Structure
```html
{% block content %}
<!-- Main content -->
{% endblock %}
{% block extra_js %}
{{ block.super }}
<!-- Component-specific scripts -->
<script src="{% static 'js/component-script.js' %}"></script>
{% endblock %}
```
## Integration Guidelines
### 1. Adding New Components
1. Create dedicated JavaScript file in `/static/js/`
2. Use data attributes for configuration
3. Follow established loading pattern
4. Update base template if needed
### 2. Version Control UI
1. Include version control UI component
2. Add necessary data attributes
3. Ensure proper script loading
4. Follow established patterns
### 3. Static Asset Management
1. Keep JavaScript files modular
2. Use proper static file organization
3. Follow naming conventions
4. Maintain clear dependencies
## Next Steps
1. Apply this pattern to other templates:
- Ride detail template
- Review detail template
- Company detail template
2. Implement consistent error handling:
```javascript
function handleError(error) {
console.error('Component error:', error);
// Handle error appropriately
}
```
3. Add performance monitoring:
```javascript
// Add timing measurements
const startTime = performance.now();
// Component initialization
const endTime = performance.now();
console.debug(`Component initialized in ${endTime - startTime}ms`);
```
4. Documentation updates:
- Add JavaScript patterns to technical guide
- Update template integration guide
- Document asset organization

View File

@@ -73,7 +73,50 @@ class Park(HistoricalModel):
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()
}
def get_absolute_url(self) -> str:
return reverse("parks:park_detail", kwargs={"slug": self.slug})
@@ -134,7 +177,51 @@ class ParkArea(HistoricalModel):
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']
}
def get_absolute_url(self) -> str:
return reverse(

View File

@@ -0,0 +1,200 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ park.name }} - ThrillWiki{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Main Content Column -->
<div class="lg:col-span-2">
<!-- Version Control UI -->
{% include "history_tracking/includes/version_control_ui.html" %}
<!-- Park Information -->
<div class="bg-white rounded-lg shadow-sm p-6">
<div class="flex justify-between items-start">
<h1 class="text-3xl font-bold text-gray-900">{{ park.name }}</h1>
<span class="px-3 py-1 rounded text-sm
{% if park.status == 'OPERATING' %}
bg-green-100 text-green-800
{% elif park.status == 'CLOSED_TEMP' %}
bg-yellow-100 text-yellow-800
{% elif park.status == 'UNDER_CONSTRUCTION' %}
bg-blue-100 text-blue-800
{% else %}
bg-red-100 text-red-800
{% endif %}">
{{ park.get_status_display }}
</span>
</div>
{% if park.description %}
<div class="mt-4 prose">
{{ park.description|linebreaks }}
</div>
{% endif %}
<!-- Park Details -->
<div class="mt-6 grid grid-cols-2 gap-4">
{% if park.opening_date %}
<div>
<h3 class="text-sm font-medium text-gray-500">Opening Date</h3>
<p class="mt-1">{{ park.opening_date }}</p>
</div>
{% endif %}
{% if park.size_acres %}
<div>
<h3 class="text-sm font-medium text-gray-500">Size</h3>
<p class="mt-1">{{ park.size_acres }} acres</p>
</div>
{% endif %}
{% if park.operating_season %}
<div>
<h3 class="text-sm font-medium text-gray-500">Operating Season</h3>
<p class="mt-1">{{ park.operating_season }}</p>
</div>
{% endif %}
{% if park.owner %}
<div>
<h3 class="text-sm font-medium text-gray-500">Owner</h3>
<p class="mt-1">
<a href="{{ park.owner.get_absolute_url }}" class="text-blue-600 hover:underline">
{{ park.owner.name }}
</a>
</p>
</div>
{% endif %}
</div>
</div>
<!-- Rides Section -->
<div class="mt-8">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Rides</h2>
{% if park.rides.all %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% for ride in park.rides.all %}
<div class="bg-white rounded-lg shadow-sm p-4">
<h3 class="text-lg font-semibold">
<a href="{{ ride.get_absolute_url }}" class="text-blue-600 hover:underline">
{{ ride.name }}
</a>
</h3>
<p class="text-sm text-gray-600 mt-1">{{ ride.type }}</p>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-gray-600">No rides listed yet.</p>
{% endif %}
</div>
<!-- Areas Section -->
{% if park.areas.exists %}
<div class="mt-8">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Areas</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% for area in park.areas.all %}
<div class="bg-white rounded-lg shadow-sm p-4">
<h3 class="text-lg font-semibold">
<a href="{{ area.get_absolute_url }}" class="text-blue-600 hover:underline">
{{ area.name }}
</a>
</h3>
{% if area.description %}
<p class="text-sm text-gray-600 mt-1">{{ area.description|truncatewords:20 }}</p>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- Sidebar -->
<div class="lg:col-span-1">
<!-- Location -->
{% if park.formatted_location %}
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<h2 class="text-lg font-semibold mb-3">Location</h2>
<p>{{ park.formatted_location }}</p>
{% if park.coordinates %}
<div id="map"
class="h-64 mt-4 rounded-lg"
data-lat="{{ park.coordinates.0|stringformat:'f' }}"
data-lng="{{ park.coordinates.1|stringformat:'f' }}"
data-name="{{ park.name|escapejs }}">
</div>
{% endif %}
</div>
{% endif %}
<!-- Statistics -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<h2 class="text-lg font-semibold mb-3">Statistics</h2>
<div class="space-y-3">
{% if park.average_rating %}
<div>
<span class="text-gray-600">Average Rating:</span>
<span class="font-medium">{{ park.average_rating }}/5</span>
</div>
{% endif %}
{% if park.ride_count %}
<div>
<span class="text-gray-600">Total Rides:</span>
<span class="font-medium">{{ park.ride_count }}</span>
</div>
{% endif %}
{% if park.coaster_count %}
<div>
<span class="text-gray-600">Roller Coasters:</span>
<span class="font-medium">{{ park.coaster_count }}</span>
</div>
{% endif %}
</div>
</div>
<!-- Photo Gallery -->
{% if park.photos.exists %}
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold mb-3" id="photo-gallery">Photo Gallery</h2>
<ul class="grid grid-cols-2 gap-2 list-none p-0"
aria-labelledby="photo-gallery">
{% for photo in park.photos.all|slice:":4" %}
<li class="aspect-w-1 aspect-h-1">
<img src="{{ photo.image.url }}"
alt="{% if photo.title %}{{ photo.title }} - {% endif %}{{ park.name }}"
class="object-cover rounded"
loading="lazy"
decoding="async"
fetchpriority="low"
width="300"
height="300">
</li>
{% endfor %}
</ul>
{% if park.photos.count > 4 %}
<a href="{% url 'photos:park-gallery' park.slug %}"
class="text-blue-600 hover:underline text-sm block mt-3"
aria-label="View full photo gallery of {{ park.name }}">
View all {{ park.photos.count }} photos
</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ block.super }}
{% if park.coordinates %}
<script src="{% static 'js/map-init.js' %}"></script>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,181 @@
/* 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;
}
/* 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%;
}
}

20
static/js/map-init.js Normal file
View File

@@ -0,0 +1,20 @@
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);
});

View File

@@ -0,0 +1,155 @@
// 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());
}
// 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 = `
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
</div>
<div class="ml-3">
<p>${message}</p>
</div>
</div>
`;
document.querySelector('.version-control-ui').prepend(errorDiv);
setTimeout(() => errorDiv.remove(), 5000);
}
}
// Initialize version control
document.addEventListener('DOMContentLoaded', () => {
window.versionControl = new VersionControl();
});

169
templates/base.html Normal file
View File

@@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}ThrillWiki{% endblock %}</title>
<!-- CSRF Token -->
{% csrf_token %}
<!-- Tailwind CSS -->
<link href="{% static 'css/tailwind.css' %}" rel="stylesheet">
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- Alpine.js -->
<script defer src="https://unpkg.com/alpinejs@3.13.5/dist/cdn.min.js"></script>
<!-- Leaflet for maps -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Version Control Assets -->
<link href="{% static 'css/version-control.css' %}" rel="stylesheet">
<script src="{% static 'js/version-control.js' %}" defer></script>
<!-- Custom Styles -->
{% block extra_css %}{% endblock %}
</head>
<body class="bg-gray-50 min-h-screen">
<!-- Header -->
<header class="bg-white shadow-sm">
<nav class="container mx-auto px-4 py-4">
<div class="flex justify-between items-center">
<a href="{% url 'home' %}" class="text-2xl font-bold text-blue-600">
ThrillWiki
</a>
<!-- Navigation -->
<div class="hidden md:flex space-x-6">
<a href="{% url 'parks:park_list' %}" class="text-gray-600 hover:text-gray-900">Parks</a>
<a href="{% url 'rides:ride_list' %}" class="text-gray-600 hover:text-gray-900">Rides</a>
<a href="{% url 'companies:company_list' %}" class="text-gray-600 hover:text-gray-900">Companies</a>
</div>
<!-- User Menu -->
<div class="flex items-center space-x-4">
{% if user.is_authenticated %}
<div x-data="{ open: false }" class="relative">
<button @click="open = !open" class="flex items-center space-x-2">
<img src="{{ user.get_avatar_url }}"
alt="{{ user.username }}"
class="h-8 w-8 rounded-full">
<span class="text-gray-700">{{ user.username }}</span>
</button>
<div x-show="open"
@click.away="open = false"
class="absolute right-0 mt-2 py-2 w-48 bg-white rounded-lg shadow-xl">
<a href="{% url 'profile' user.username %}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Profile
</a>
<a href="{% url 'settings' %}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Settings
</a>
<form method="post" action="{% url 'account_logout' %}">
{% csrf_token %}
<button type="submit"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Sign Out
</button>
</form>
</div>
</div>
{% else %}
<a href="{% url 'account_login' %}"
class="text-gray-600 hover:text-gray-900">
Sign In
</a>
<a href="{% url 'account_signup' %}"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
Sign Up
</a>
{% endif %}
</div>
</div>
</nav>
</header>
<!-- Main Content -->
<main class="py-8">
{% if messages %}
<div class="container mx-auto px-4 mb-8">
{% for message in messages %}
<div class="p-4 {% if message.tags == 'success' %}bg-green-100 text-green-700{% elif message.tags == 'error' %}bg-red-100 text-red-700{% else %}bg-blue-100 text-blue-700{% endif %} rounded-lg">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="bg-white border-t mt-auto">
<div class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<h3 class="text-lg font-semibold mb-4">About</h3>
<p class="text-gray-600">
ThrillWiki is your source for theme park and attraction information.
</p>
</div>
<div>
<h3 class="text-lg font-semibold mb-4">Explore</h3>
<ul class="space-y-2">
<li><a href="{% url 'parks:park_list' %}" class="text-gray-600 hover:text-gray-900">Parks</a></li>
<li><a href="{% url 'rides:ride_list' %}" class="text-gray-600 hover:text-gray-900">Rides</a></li>
<li><a href="{% url 'companies:company_list' %}" class="text-gray-600 hover:text-gray-900">Companies</a></li>
</ul>
</div>
<div>
<h3 class="text-lg font-semibold mb-4">Legal</h3>
<ul class="space-y-2">
<li><a href="{% url 'terms' %}" class="text-gray-600 hover:text-gray-900">Terms of Service</a></li>
<li><a href="{% url 'privacy' %}" class="text-gray-600 hover:text-gray-900">Privacy Policy</a></li>
</ul>
</div>
<div>
<h3 class="text-lg font-semibold mb-4">Connect</h3>
<div class="flex space-x-4">
<a href="#" class="text-gray-400 hover:text-gray-500">
<span class="sr-only">Twitter</span>
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"></path>
</svg>
</a>
</div>
</div>
</div>
<div class="mt-8 border-t pt-8 text-center text-gray-400">
<p>&copy; {% now "Y" %} ThrillWiki. All rights reserved.</p>
</div>
</div>
</footer>
<!-- Extra Scripts -->
{% block extra_js %}{% endblock %}
<!-- Version Control Status -->
{% if version_control.current_branch %}
<div class="fixed bottom-4 right-4 bg-white rounded-lg shadow-lg p-3 text-sm">
<div class="flex items-center space-x-2">
<span class="text-gray-600">Branch:</span>
<span class="font-medium">{{ version_control.branch_name }}</span>
{% if version_control.recent_changes %}
<span class="bg-blue-100 text-blue-800 px-2 py-1 rounded-full text-xs">
{{ version_control.recent_changes|length }} changes
</span>
{% endif %}
</div>
</div>
{% endif %}
</body>
</html>

View File

@@ -86,6 +86,7 @@ TEMPLATES = [
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"moderation.context_processors.moderation_access",
"history_tracking.context_processors.version_control",
],
},
},