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}")
# 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
resolved_content.update(_handle_manual_resolution(
conflict_id, source_change, manual_resolutions, user
))
# 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)