Add OWASP compliance mapping and security test case templates, and document version control implementation phases

This commit is contained in:
pacnpal
2025-02-07 10:51:11 -05:00
parent 2c82489691
commit c083f54afb
38 changed files with 5313 additions and 94 deletions

View File

@@ -1,12 +1,15 @@
from django.db import models
from django.utils.text import slugify
from django.urls import reverse
from django.contrib.contenttypes.models import ContentType
from typing import Tuple, Optional, ClassVar, TYPE_CHECKING
from history_tracking.models import HistoricalModel, VersionBranch, ChangeSet
from history_tracking.signals import get_current_branch, ChangesetContextManager
if TYPE_CHECKING:
from history_tracking.models import HistoricalSlug
class Company(models.Model):
class Company(HistoricalModel):
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
website = models.URLField(blank=True)
@@ -29,7 +32,47 @@ class Company(models.Model):
def save(self, *args, **kwargs) -> None:
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
# Get the branch from context or use default
current_branch = get_current_branch()
if current_branch:
# Save in the context of the current branch
super().save(*args, **kwargs)
else:
# If no branch context, save in main branch
main_branch, _ = VersionBranch.objects.get_or_create(
name='main',
defaults={'metadata': {'type': 'default_branch'}}
)
with ChangesetContextManager(branch=main_branch):
super().save(*args, **kwargs)
def get_version_info(self) -> dict:
"""Get version control information for this company"""
content_type = ContentType.objects.get_for_model(self)
latest_changes = ChangeSet.objects.filter(
content_type=content_type,
object_id=self.pk,
status='applied'
).order_by('-created_at')[:5]
active_branches = VersionBranch.objects.filter(
changesets__content_type=content_type,
changesets__object_id=self.pk,
is_active=True
).distinct()
return {
'latest_changes': latest_changes,
'active_branches': active_branches,
'current_branch': get_current_branch(),
'total_changes': latest_changes.count()
}
def get_absolute_url(self) -> str:
return reverse("companies:company_detail", kwargs={"slug": self.slug})
@classmethod
def get_by_slug(cls, slug: str) -> Tuple['Company', bool]:
@@ -48,7 +91,7 @@ class Company(models.Model):
except (HistoricalSlug.DoesNotExist, cls.DoesNotExist):
raise cls.DoesNotExist()
class Manufacturer(models.Model):
class Manufacturer(HistoricalModel):
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
website = models.URLField(blank=True)
@@ -70,7 +113,47 @@ class Manufacturer(models.Model):
def save(self, *args, **kwargs) -> None:
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
# Get the branch from context or use default
current_branch = get_current_branch()
if current_branch:
# Save in the context of the current branch
super().save(*args, **kwargs)
else:
# If no branch context, save in main branch
main_branch, _ = VersionBranch.objects.get_or_create(
name='main',
defaults={'metadata': {'type': 'default_branch'}}
)
with ChangesetContextManager(branch=main_branch):
super().save(*args, **kwargs)
def get_version_info(self) -> dict:
"""Get version control information for this manufacturer"""
content_type = ContentType.objects.get_for_model(self)
latest_changes = ChangeSet.objects.filter(
content_type=content_type,
object_id=self.pk,
status='applied'
).order_by('-created_at')[:5]
active_branches = VersionBranch.objects.filter(
changesets__content_type=content_type,
changesets__object_id=self.pk,
is_active=True
).distinct()
return {
'latest_changes': latest_changes,
'active_branches': active_branches,
'current_branch': get_current_branch(),
'total_changes': latest_changes.count()
}
def get_absolute_url(self) -> str:
return reverse("companies:manufacturer_detail", kwargs={"slug": self.slug})
@classmethod
def get_by_slug(cls, slug: str) -> Tuple['Manufacturer', bool]:
@@ -89,7 +172,7 @@ class Manufacturer(models.Model):
except (HistoricalSlug.DoesNotExist, cls.DoesNotExist):
raise cls.DoesNotExist()
class Designer(models.Model):
class Designer(HistoricalModel):
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
website = models.URLField(blank=True)
@@ -110,7 +193,47 @@ class Designer(models.Model):
def save(self, *args, **kwargs) -> None:
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
# Get the branch from context or use default
current_branch = get_current_branch()
if current_branch:
# Save in the context of the current branch
super().save(*args, **kwargs)
else:
# If no branch context, save in main branch
main_branch, _ = VersionBranch.objects.get_or_create(
name='main',
defaults={'metadata': {'type': 'default_branch'}}
)
with ChangesetContextManager(branch=main_branch):
super().save(*args, **kwargs)
def get_version_info(self) -> dict:
"""Get version control information for this designer"""
content_type = ContentType.objects.get_for_model(self)
latest_changes = ChangeSet.objects.filter(
content_type=content_type,
object_id=self.pk,
status='applied'
).order_by('-created_at')[:5]
active_branches = VersionBranch.objects.filter(
changesets__content_type=content_type,
changesets__object_id=self.pk,
is_active=True
).distinct()
return {
'latest_changes': latest_changes,
'active_branches': active_branches,
'current_branch': get_current_branch(),
'total_changes': latest_changes.count()
}
def get_absolute_url(self) -> str:
return reverse("companies:designer_detail", kwargs={"slug": self.slug})
@classmethod
def get_by_slug(cls, slug: str) -> Tuple['Designer', bool]:

View File

@@ -0,0 +1,137 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ company.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" %}
<!-- Company Information -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h1 class="text-3xl font-bold text-gray-900 mb-4">{{ company.name }}</h1>
{% if company.description %}
<div class="prose max-w-none mb-6">
{{ company.description|linebreaks }}
</div>
{% endif %}
<!-- Company Details -->
<div class="grid grid-cols-2 gap-4">
{% if company.headquarters %}
<div>
<h3 class="text-sm font-medium text-gray-500">Headquarters</h3>
<p class="mt-1">{{ company.headquarters }}</p>
</div>
{% endif %}
{% if company.website %}
<div>
<h3 class="text-sm font-medium text-gray-500">Website</h3>
<p class="mt-1">
<a href="{{ company.website }}" class="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer">
{{ company.website }}
</a>
</p>
</div>
{% endif %}
</div>
</div>
<!-- Parks Section -->
{% if company.parks.exists %}
<div class="mt-8">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Theme Parks</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% for park in company.parks.all %}
<div class="bg-white rounded-lg shadow-sm p-4">
<h3 class="text-lg font-semibold">
<a href="{{ park.get_absolute_url }}" class="text-blue-600 hover:underline">
{{ park.name }}
</a>
</h3>
<p class="text-sm text-gray-600 mt-1">{{ park.get_status_display }}</p>
{% if park.formatted_location %}
<p class="text-sm text-gray-500 mt-1">{{ park.formatted_location }}</p>
{% endif %}
<!-- Version Control Status -->
{% with version_info=park.get_version_info %}
{% if version_info.active_branches.count > 1 %}
<div class="mt-2 text-sm">
<span class="text-yellow-600">
{{ version_info.active_branches.count }} active branches
</span>
</div>
{% endif %}
{% endwith %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- Sidebar -->
<div class="lg:col-span-1">
<!-- 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">
<div>
<span class="text-gray-600">Total Parks:</span>
<span class="font-medium">{{ company.total_parks }}</span>
</div>
<div>
<span class="text-gray-600">Total Rides:</span>
<span class="font-medium">{{ company.total_rides }}</span>
</div>
</div>
</div>
<!-- Version Control Info -->
{% with version_info=company.get_version_info %}
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<h2 class="text-lg font-semibold mb-3">Version Control</h2>
<div class="space-y-3 text-sm">
<div>
<span class="text-gray-600">Active Branches:</span>
<span class="font-medium">{{ version_info.active_branches.count }}</span>
</div>
<div>
<span class="text-gray-600">Total Changes:</span>
<span class="font-medium">{{ version_info.total_changes }}</span>
</div>
{% if version_info.latest_changes %}
<div>
<span class="text-gray-600 block mb-2">Recent Changes:</span>
<ul class="space-y-2">
{% for change in version_info.latest_changes|slice:":3" %}
<li class="text-gray-700">{{ change.description }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
{% endwith %}
<!-- Metadata -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold mb-3">Details</h2>
<div class="space-y-2 text-sm">
<p><span class="text-gray-600">Created:</span> {{ company.created_at|date:"F j, Y" }}</p>
{% if company.created_at != company.updated_at %}
<p><span class="text-gray-600">Last updated:</span> {{ company.updated_at|date:"F j, Y" }}</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,136 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Companies - ThrillWiki{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Version Control UI -->
{% include "history_tracking/includes/version_control_ui.html" %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Companies</h1>
<p class="text-gray-600 mt-2">Theme park owners and operators</p>
</div>
<!-- Filters -->
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
<form method="get" class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="min_parks" class="block text-sm font-medium text-gray-700">Minimum Parks</label>
<select name="min_parks" id="min_parks" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
<option value="">Any</option>
{% for i in "12345" %}
<option value="{{ i }}" {% if min_parks == i %}selected{% endif %}>{{ i }}+ parks</option>
{% endfor %}
</select>
</div>
<div>
<label for="order" class="block text-sm font-medium text-gray-700">Sort By</label>
<select name="order" id="order" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
<option value="name" {% if order == 'name' %}selected{% endif %}>Name (A-Z)</option>
<option value="-name" {% if order == '-name' %}selected{% endif %}>Name (Z-A)</option>
<option value="-total_parks" {% if order == '-total_parks' %}selected{% endif %}>Most Parks</option>
<option value="-total_rides" {% if order == '-total_rides' %}selected{% endif %}>Most Rides</option>
</select>
</div>
<div class="flex items-end">
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded">
Apply Filters
</button>
</div>
</form>
</div>
<!-- Companies Grid -->
{% if companies %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for company in companies %}
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
<div class="p-6">
<h2 class="text-xl font-semibold mb-2">
<a href="{{ company.get_absolute_url }}" class="text-blue-600 hover:underline">
{{ company.name }}
</a>
</h2>
{% if company.description %}
<p class="text-gray-600 text-sm mb-4">{{ company.description|truncatewords:30 }}</p>
{% endif %}
<div class="grid grid-cols-2 gap-4 mb-4 text-sm">
{% if company.headquarters %}
<div>
<span class="text-gray-500">Headquarters</span>
<p class="font-medium">{{ company.headquarters }}</p>
</div>
{% endif %}
{% if company.website %}
<div>
<span class="text-gray-500">Website</span>
<p>
<a href="{{ company.website }}"
class="text-blue-600 hover:underline"
target="_blank"
rel="noopener noreferrer">
Visit Site
</a>
</p>
</div>
{% endif %}
<div>
<span class="text-gray-500">Total Parks</span>
<p class="font-medium">{{ company.total_parks }}</p>
</div>
<div>
<span class="text-gray-500">Total Rides</span>
<p class="font-medium">{{ company.total_rides }}</p>
</div>
</div>
<!-- Version Control Status -->
{% with version_info=company.get_version_info %}
{% if version_info.active_branches.count > 1 %}
<div class="text-sm">
<span class="text-yellow-600">
{{ version_info.active_branches.count }} active branches
</span>
</div>
{% endif %}
{% endwith %}
</div>
</div>
{% endfor %}
</div>
<!-- Pagination -->
{% if is_paginated %}
<div class="mt-8 flex justify-center">
<nav class="inline-flex rounded-md shadow">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}{% if request.GET.min_parks %}&min_parks={{ request.GET.min_parks }}{% endif %}{% if request.GET.order %}&order={{ request.GET.order }}{% endif %}"
class="px-3 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
Previous
</a>
{% endif %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}{% if request.GET.min_parks %}&min_parks={{ request.GET.min_parks }}{% endif %}{% if request.GET.order %}&order={{ request.GET.order }}{% endif %}"
class="px-3 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
Next
</a>
{% endif %}
</nav>
</div>
{% endif %}
{% else %}
<div class="text-center py-12">
<p class="text-gray-600">No companies found matching your criteria.</p>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,154 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ designer.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" %}
<!-- Designer Information -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h1 class="text-3xl font-bold text-gray-900 mb-4">{{ designer.name }}</h1>
{% if designer.description %}
<div class="prose max-w-none mb-6">
{{ designer.description|linebreaks }}
</div>
{% endif %}
<!-- Designer Details -->
{% if designer.website %}
<div class="mb-4">
<h3 class="text-sm font-medium text-gray-500">Website</h3>
<p class="mt-1">
<a href="{{ designer.website }}" class="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer">
{{ designer.website }}
</a>
</p>
</div>
{% endif %}
</div>
<!-- Designed Rides Section -->
{% if designer.rides.exists %}
<div class="mt-8">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Designed Rides</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% for ride in designer.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">
at
<a href="{{ ride.park.get_absolute_url }}" class="hover:underline">
{{ ride.park.name }}
</a>
</p>
{% if ride.manufacturer %}
<p class="text-sm text-gray-600 mt-1">
Built by
<a href="{{ ride.manufacturer.get_absolute_url }}" class="hover:underline">
{{ ride.manufacturer.name }}
</a>
</p>
{% endif %}
<div class="mt-2">
<span class="px-2 py-1 text-xs rounded
{% if ride.status == 'OPERATING' %}
bg-green-100 text-green-800
{% elif ride.status == 'SBNO' %}
bg-yellow-100 text-yellow-800
{% elif ride.status == 'UNDER_CONSTRUCTION' %}
bg-blue-100 text-blue-800
{% else %}
bg-red-100 text-red-800
{% endif %}">
{{ ride.get_status_display }}
</span>
</div>
<!-- Version Control Status -->
{% with version_info=ride.get_version_info %}
{% if version_info.active_branches.count > 1 %}
<div class="mt-2 text-sm">
<span class="text-yellow-600">
{{ version_info.active_branches.count }} active branches
</span>
</div>
{% endif %}
{% endwith %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- Sidebar -->
<div class="lg:col-span-1">
<!-- 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">
<div>
<span class="text-gray-600">Total Rides:</span>
<span class="font-medium">{{ designer.total_rides }}</span>
</div>
<div>
<span class="text-gray-600">Roller Coasters:</span>
<span class="font-medium">{{ designer.total_roller_coasters }}</span>
</div>
</div>
</div>
<!-- Version Control Info -->
{% with version_info=designer.get_version_info %}
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<h2 class="text-lg font-semibold mb-3">Version Control</h2>
<div class="space-y-3 text-sm">
<div>
<span class="text-gray-600">Active Branches:</span>
<span class="font-medium">{{ version_info.active_branches.count }}</span>
</div>
<div>
<span class="text-gray-600">Total Changes:</span>
<span class="font-medium">{{ version_info.total_changes }}</span>
</div>
{% if version_info.latest_changes %}
<div>
<span class="text-gray-600 block mb-2">Recent Changes:</span>
<ul class="space-y-2">
{% for change in version_info.latest_changes|slice:":3" %}
<li class="text-gray-700">{{ change.description }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
{% endwith %}
<!-- Metadata -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold mb-3">Details</h2>
<div class="space-y-2 text-sm">
<p><span class="text-gray-600">Created:</span> {{ designer.created_at|date:"F j, Y" }}</p>
{% if designer.created_at != designer.updated_at %}
<p><span class="text-gray-600">Last updated:</span> {{ designer.updated_at|date:"F j, Y" }}</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,146 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Designers - ThrillWiki{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Version Control UI -->
{% include "history_tracking/includes/version_control_ui.html" %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Ride Designers</h1>
<p class="text-gray-600 mt-2">Ride and attraction designers and engineers</p>
</div>
<!-- Filters -->
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
<form method="get" class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="min_rides" class="block text-sm font-medium text-gray-700">Minimum Rides</label>
<select name="min_rides" id="min_rides" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
<option value="">Any</option>
{% for i in "12345" %}
<option value="{{ i }}" {% if min_rides == i %}selected{% endif %}>{{ i }}+ rides</option>
{% endfor %}
</select>
</div>
<div>
<label for="order" class="block text-sm font-medium text-gray-700">Sort By</label>
<select name="order" id="order" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
<option value="name" {% if order == 'name' %}selected{% endif %}>Name (A-Z)</option>
<option value="-name" {% if order == '-name' %}selected{% endif %}>Name (Z-A)</option>
<option value="-total_rides" {% if order == '-total_rides' %}selected{% endif %}>Most Rides</option>
<option value="-total_roller_coasters" {% if order == '-total_roller_coasters' %}selected{% endif %}>Most Roller Coasters</option>
</select>
</div>
<div class="flex items-end">
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded">
Apply Filters
</button>
</div>
</form>
</div>
<!-- Designers Grid -->
{% if designers %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for designer in designers %}
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
<div class="p-6">
<h2 class="text-xl font-semibold mb-2">
<a href="{{ designer.get_absolute_url }}" class="text-blue-600 hover:underline">
{{ designer.name }}
</a>
</h2>
{% if designer.description %}
<p class="text-gray-600 text-sm mb-4">{{ designer.description|truncatewords:30 }}</p>
{% endif %}
<div class="grid grid-cols-2 gap-4 mb-4 text-sm">
{% if designer.website %}
<div>
<span class="text-gray-500">Website</span>
<p>
<a href="{{ designer.website }}"
class="text-blue-600 hover:underline"
target="_blank"
rel="noopener noreferrer">
Visit Site
</a>
</p>
</div>
{% endif %}
<div>
<span class="text-gray-500">Total Rides</span>
<p class="font-medium">{{ designer.total_rides }}</p>
</div>
<div>
<span class="text-gray-500">Roller Coasters</span>
<p class="font-medium">{{ designer.total_roller_coasters }}</p>
</div>
</div>
<!-- Notable Rides Preview -->
{% if designer.rides.exists %}
<div class="text-sm text-gray-600 mb-4">
<span class="font-medium">Notable Works:</span>
<div class="mt-1">
{% for ride in designer.rides.all|slice:":3" %}
<span class="inline-block bg-gray-100 rounded-full px-3 py-1 text-xs font-medium text-gray-700 mr-2 mb-2">
{{ ride.name }}
</span>
{% endfor %}
{% if designer.rides.count > 3 %}
<span class="text-blue-600">+{{ designer.rides.count|add:"-3" }} more</span>
{% endif %}
</div>
</div>
{% endif %}
<!-- Version Control Status -->
{% with version_info=designer.get_version_info %}
{% if version_info.active_branches.count > 1 %}
<div class="text-sm">
<span class="text-yellow-600">
{{ version_info.active_branches.count }} active branches
</span>
</div>
{% endif %}
{% endwith %}
</div>
</div>
{% endfor %}
</div>
<!-- Pagination -->
{% if is_paginated %}
<div class="mt-8 flex justify-center">
<nav class="inline-flex rounded-md shadow">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}{% if request.GET.min_rides %}&min_rides={{ request.GET.min_rides }}{% endif %}{% if request.GET.order %}&order={{ request.GET.order }}{% endif %}"
class="px-3 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
Previous
</a>
{% endif %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}{% if request.GET.min_rides %}&min_rides={{ request.GET.min_rides }}{% endif %}{% if request.GET.order %}&order={{ request.GET.order }}{% endif %}"
class="px-3 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
Next
</a>
{% endif %}
</nav>
</div>
{% endif %}
{% else %}
<div class="text-center py-12">
<p class="text-gray-600">No designers found matching your criteria.</p>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,188 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ manufacturer.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" %}
<!-- Manufacturer Information -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h1 class="text-3xl font-bold text-gray-900 mb-4">{{ manufacturer.name }}</h1>
{% if manufacturer.description %}
<div class="prose max-w-none mb-6">
{{ manufacturer.description|linebreaks }}
</div>
{% endif %}
<!-- Manufacturer Details -->
<div class="grid grid-cols-2 gap-4">
{% if manufacturer.headquarters %}
<div>
<h3 class="text-sm font-medium text-gray-500">Headquarters</h3>
<p class="mt-1">{{ manufacturer.headquarters }}</p>
</div>
{% endif %}
{% if manufacturer.website %}
<div>
<h3 class="text-sm font-medium text-gray-500">Website</h3>
<p class="mt-1">
<a href="{{ manufacturer.website }}" class="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer">
{{ manufacturer.website }}
</a>
</p>
</div>
{% endif %}
</div>
</div>
<!-- Ride Models Section -->
{% if manufacturer.ride_models.exists %}
<div class="mt-8">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Ride Models</h2>
<div class="grid grid-cols-1 gap-4">
{% for model in manufacturer.ride_models.all %}
<div class="bg-white rounded-lg shadow-sm p-4">
<h3 class="text-lg font-semibold">
<a href="{{ model.get_absolute_url }}" class="text-blue-600 hover:underline">
{{ model.name }}
</a>
</h3>
{% if model.category %}
<p class="text-sm text-gray-600 mt-1">{{ model.get_category_display }}</p>
{% endif %}
{% if model.description %}
<p class="text-sm text-gray-600 mt-2">{{ model.description|truncatewords:50 }}</p>
{% endif %}
<!-- Version Control Status -->
{% with version_info=model.get_version_info %}
{% if version_info.active_branches.count > 1 %}
<div class="mt-2 text-sm">
<span class="text-yellow-600">
{{ version_info.active_branches.count }} active branches
</span>
</div>
{% endif %}
{% endwith %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Installed Rides Section -->
{% if manufacturer.rides.exists %}
<div class="mt-8">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Installed Rides</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% for ride in manufacturer.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">
at
<a href="{{ ride.park.get_absolute_url }}" class="hover:underline">
{{ ride.park.name }}
</a>
</p>
<div class="mt-2">
<span class="px-2 py-1 text-xs rounded
{% if ride.status == 'OPERATING' %}
bg-green-100 text-green-800
{% elif ride.status == 'SBNO' %}
bg-yellow-100 text-yellow-800
{% elif ride.status == 'UNDER_CONSTRUCTION' %}
bg-blue-100 text-blue-800
{% else %}
bg-red-100 text-red-800
{% endif %}">
{{ ride.get_status_display }}
</span>
</div>
<!-- Version Control Status -->
{% with version_info=ride.get_version_info %}
{% if version_info.active_branches.count > 1 %}
<div class="mt-2 text-sm">
<span class="text-yellow-600">
{{ version_info.active_branches.count }} active branches
</span>
</div>
{% endif %}
{% endwith %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- Sidebar -->
<div class="lg:col-span-1">
<!-- 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">
<div>
<span class="text-gray-600">Total Rides:</span>
<span class="font-medium">{{ manufacturer.total_rides }}</span>
</div>
<div>
<span class="text-gray-600">Roller Coasters:</span>
<span class="font-medium">{{ manufacturer.total_roller_coasters }}</span>
</div>
</div>
</div>
<!-- Version Control Info -->
{% with version_info=manufacturer.get_version_info %}
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<h2 class="text-lg font-semibold mb-3">Version Control</h2>
<div class="space-y-3 text-sm">
<div>
<span class="text-gray-600">Active Branches:</span>
<span class="font-medium">{{ version_info.active_branches.count }}</span>
</div>
<div>
<span class="text-gray-600">Total Changes:</span>
<span class="font-medium">{{ version_info.total_changes }}</span>
</div>
{% if version_info.latest_changes %}
<div>
<span class="text-gray-600 block mb-2">Recent Changes:</span>
<ul class="space-y-2">
{% for change in version_info.latest_changes|slice:":3" %}
<li class="text-gray-700">{{ change.description }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
{% endwith %}
<!-- Metadata -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold mb-3">Details</h2>
<div class="space-y-2 text-sm">
<p><span class="text-gray-600">Created:</span> {{ manufacturer.created_at|date:"F j, Y" }}</p>
{% if manufacturer.created_at != manufacturer.updated_at %}
<p><span class="text-gray-600">Last updated:</span> {{ manufacturer.updated_at|date:"F j, Y" }}</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,162 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Manufacturers - ThrillWiki{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Version Control UI -->
{% include "history_tracking/includes/version_control_ui.html" %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Manufacturers</h1>
<p class="text-gray-600 mt-2">Ride and attraction manufacturers</p>
</div>
<!-- Filters -->
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
<form method="get" class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label for="category" class="block text-sm font-medium text-gray-700">Ride Type</label>
<select name="category" id="category" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
<option value="">All Types</option>
{% for code, name in category_choices %}
<option value="{{ code }}" {% if category == code %}selected{% endif %}>{{ name }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="min_rides" class="block text-sm font-medium text-gray-700">Minimum Rides</label>
<select name="min_rides" id="min_rides" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
<option value="">Any</option>
{% for i in "12345" %}
<option value="{{ i }}" {% if min_rides == i %}selected{% endif %}>{{ i }}+ rides</option>
{% endfor %}
</select>
</div>
<div>
<label for="order" class="block text-sm font-medium text-gray-700">Sort By</label>
<select name="order" id="order" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
<option value="name" {% if order == 'name' %}selected{% endif %}>Name (A-Z)</option>
<option value="-name" {% if order == '-name' %}selected{% endif %}>Name (Z-A)</option>
<option value="-total_rides" {% if order == '-total_rides' %}selected{% endif %}>Most Rides</option>
<option value="-total_roller_coasters" {% if order == '-total_roller_coasters' %}selected{% endif %}>Most Roller Coasters</option>
</select>
</div>
<div class="flex items-end">
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded">
Apply Filters
</button>
</div>
</form>
</div>
<!-- Manufacturers Grid -->
{% if manufacturers %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for manufacturer in manufacturers %}
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
<div class="p-6">
<h2 class="text-xl font-semibold mb-2">
<a href="{{ manufacturer.get_absolute_url }}" class="text-blue-600 hover:underline">
{{ manufacturer.name }}
</a>
</h2>
{% if manufacturer.description %}
<p class="text-gray-600 text-sm mb-4">{{ manufacturer.description|truncatewords:30 }}</p>
{% endif %}
<div class="grid grid-cols-2 gap-4 mb-4 text-sm">
{% if manufacturer.headquarters %}
<div>
<span class="text-gray-500">Headquarters</span>
<p class="font-medium">{{ manufacturer.headquarters }}</p>
</div>
{% endif %}
{% if manufacturer.website %}
<div>
<span class="text-gray-500">Website</span>
<p>
<a href="{{ manufacturer.website }}"
class="text-blue-600 hover:underline"
target="_blank"
rel="noopener noreferrer">
Visit Site
</a>
</p>
</div>
{% endif %}
<div>
<span class="text-gray-500">Total Rides</span>
<p class="font-medium">{{ manufacturer.total_rides }}</p>
</div>
<div>
<span class="text-gray-500">Roller Coasters</span>
<p class="font-medium">{{ manufacturer.total_roller_coasters }}</p>
</div>
</div>
<!-- Ride Models Preview -->
{% if manufacturer.ride_models.exists %}
<div class="text-sm text-gray-600 mb-4">
<span class="font-medium">Popular Models:</span>
<div class="mt-1">
{% for model in manufacturer.ride_models.all|slice:":3" %}
<span class="inline-block bg-gray-100 rounded-full px-3 py-1 text-xs font-medium text-gray-700 mr-2 mb-2">
{{ model.name }}
</span>
{% endfor %}
{% if manufacturer.ride_models.count > 3 %}
<span class="text-blue-600">+{{ manufacturer.ride_models.count|add:"-3" }} more</span>
{% endif %}
</div>
</div>
{% endif %}
<!-- Version Control Status -->
{% with version_info=manufacturer.get_version_info %}
{% if version_info.active_branches.count > 1 %}
<div class="text-sm">
<span class="text-yellow-600">
{{ version_info.active_branches.count }} active branches
</span>
</div>
{% endif %}
{% endwith %}
</div>
</div>
{% endfor %}
</div>
<!-- Pagination -->
{% if is_paginated %}
<div class="mt-8 flex justify-center">
<nav class="inline-flex rounded-md shadow">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}{% if request.GET.category %}&category={{ request.GET.category }}{% endif %}{% if request.GET.min_rides %}&min_rides={{ request.GET.min_rides }}{% endif %}{% if request.GET.order %}&order={{ request.GET.order }}{% endif %}"
class="px-3 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
Previous
</a>
{% endif %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}{% if request.GET.category %}&category={{ request.GET.category }}{% endif %}{% if request.GET.min_rides %}&min_rides={{ request.GET.min_rides }}{% endif %}{% if request.GET.order %}&order={{ request.GET.order }}{% endif %}"
class="px-3 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
Next
</a>
{% endif %}
</nav>
</div>
{% endif %}
{% else %}
<div class="text-center py-12">
<p class="text-gray-600">No manufacturers found matching your criteria.</p>
</div>
{% endif %}
</div>
{% endblock %}