fix commas

This commit is contained in:
pacnpal
2024-11-03 20:21:39 +00:00
parent 1b0fe4588e
commit ed585e6a56
21 changed files with 390 additions and 98 deletions

View File

@@ -0,0 +1,2 @@
# history_tracking/__init__.py
default_app_config = "history_tracking.apps.HistoryTrackingConfig"

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

20
history_tracking/apps.py Normal file
View File

@@ -0,0 +1,20 @@
# history_tracking/apps.py
from django.apps import AppConfig
class HistoryTrackingConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "history_tracking"
def ready(self):
from .mixins import HistoricalChangeMixin
from .models import Park
models_with_history = [Park]
for model in models_with_history:
# Check if mixin is already applied
if HistoricalChangeMixin not in model.history.model.__bases__:
model.history.model.__bases__ = (
HistoricalChangeMixin,
) + model.history.model.__bases__

View File

View File

@@ -0,0 +1,99 @@
# history_tracking/management/commands/initialize_history.py
from django.core.management.base import BaseCommand
from django.utils import timezone
from django.apps import apps
from django.db.models import Model
from simple_history.models import HistoricalRecords
class Command(BaseCommand):
help = "Initialize history records for existing objects with historical records"
def add_arguments(self, parser):
parser.add_argument(
"--model",
type=str,
help="Specify model in format app_name.ModelName (e.g., history_tracking.Park)",
)
parser.add_argument(
"--all",
action="store_true",
help="Initialize history for all models with historical records",
)
parser.add_argument(
"--force",
action="store_true",
help="Create history even if records already exist",
)
def initialize_model(self, model, force=False):
total = model.objects.count()
initialized = 0
model_name = f"{model._meta.app_label}.{model._meta.model_name}"
self.stdout.write(f"Processing {model_name}: Found {total} records")
for obj in model.objects.all():
try:
if force or not obj.history.exists():
obj.history.create(
history_date=timezone.now(),
history_type="+",
history_change_reason="Initial history record",
**{
field.name: getattr(obj, field.name)
for field in obj._meta.fields
if not isinstance(field, HistoricalRecords)
},
)
initialized += 1
self.stdout.write(f"Created history for {model_name} id={obj.pk}")
except Exception as e:
self.stdout.write(
self.style.ERROR(
f"Error creating history for {model_name} id={obj.pk}: {str(e)}"
)
)
return initialized, total
def handle(self, *args, **options):
if not options["model"] and not options["all"]:
self.stdout.write(
self.style.ERROR("Please specify either --model or --all")
)
return
force = options["force"]
total_initialized = 0
total_records = 0
if options["model"]:
try:
app_label, model_name = options["model"].split(".")
model = apps.get_model(app_label, model_name)
if hasattr(model, "history"):
initialized, total = self.initialize_model(model, force)
total_initialized += initialized
total_records += total
else:
self.stdout.write(
self.style.ERROR(
f'Model {options["model"]} does not have historical records'
)
)
except Exception as e:
self.stdout.write(self.style.ERROR(str(e)))
else:
# Process all models with historical records
for model in apps.get_models():
if hasattr(model, "history"):
initialized, total = self.initialize_model(model, force)
total_initialized += initialized
total_records += total
self.stdout.write(
self.style.SUCCESS(
f"Successfully initialized {total_initialized} of {total_records} total records"
)
)

View File

@@ -0,0 +1,76 @@
# Generated by Django 5.1.2 on 2024-11-03 19:59
import django.db.models.deletion
import history_tracking.mixins
import simple_history.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Park",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=200)),
],
),
migrations.CreateModel(
name="HistoricalPark",
fields=[
(
"id",
models.BigIntegerField(
auto_created=True, blank=True, db_index=True, verbose_name="ID"
),
),
("name", models.CharField(max_length=200)),
("history_id", models.AutoField(primary_key=True, serialize=False)),
("history_date", models.DateTimeField(db_index=True)),
("history_change_reason", models.CharField(max_length=100, null=True)),
(
"history_type",
models.CharField(
choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
max_length=1,
),
),
(
"history_user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "historical park",
"verbose_name_plural": "historical parks",
"ordering": ("-history_date", "-history_id"),
"get_latest_by": ("history_date", "history_id"),
},
bases=(
history_tracking.mixins.HistoricalChangeMixin,
simple_history.models.HistoricalChanges,
models.Model,
),
),
]

View File

View File

@@ -0,0 +1,23 @@
# history_tracking/mixins.py
class HistoricalChangeMixin:
@property
def diff_against_previous(self):
prev_record = self.prev_record
if not prev_record:
return {}
changes = {}
for field in self.__dict__:
if field not in [
"history_date",
"history_id",
"history_type",
"history_user_id",
] and not field.startswith("_"):
old_value = getattr(prev_record, field)
new_value = getattr(self, field)
if old_value != new_value:
changes[field] = {"old": old_value, "new": new_value}
return changes

View File

@@ -0,0 +1,17 @@
# history_tracking/models.py
from django.db import models
from simple_history.models import HistoricalRecords
from .mixins import HistoricalChangeMixin
class Park(models.Model):
name = models.CharField(max_length=200)
# ... other fields ...
history = HistoricalRecords()
@property
def _history_model(self):
return self.history.model
# Apply the mixin

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -0,0 +1,80 @@
# Generated by Django 5.1.2 on 2024-11-03 19:59
import django.core.validators
import parks.models
from decimal import Decimal
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("parks", "0005_normalize_coordinates"),
]
operations = [
migrations.AlterField(
model_name="historicalpark",
name="latitude",
field=models.DecimalField(
blank=True,
decimal_places=6,
help_text="Latitude coordinate (-90 to 90)",
max_digits=9,
null=True,
validators=[
django.core.validators.MinValueValidator(Decimal("-90")),
django.core.validators.MaxValueValidator(Decimal("90")),
parks.models.validate_latitude_digits,
],
),
),
migrations.AlterField(
model_name="historicalpark",
name="longitude",
field=models.DecimalField(
blank=True,
decimal_places=6,
help_text="Longitude coordinate (-180 to 180)",
max_digits=10,
null=True,
validators=[
django.core.validators.MinValueValidator(Decimal("-180")),
django.core.validators.MaxValueValidator(Decimal("180")),
parks.models.validate_longitude_digits,
],
),
),
migrations.AlterField(
model_name="park",
name="latitude",
field=models.DecimalField(
blank=True,
decimal_places=6,
help_text="Latitude coordinate (-90 to 90)",
max_digits=9,
null=True,
validators=[
django.core.validators.MinValueValidator(Decimal("-90")),
django.core.validators.MaxValueValidator(Decimal("90")),
parks.models.validate_latitude_digits,
],
),
),
migrations.AlterField(
model_name="park",
name="longitude",
field=models.DecimalField(
blank=True,
decimal_places=6,
help_text="Longitude coordinate (-180 to 180)",
max_digits=10,
null=True,
validators=[
django.core.validators.MinValueValidator(Decimal("-180")),
django.core.validators.MaxValueValidator(Decimal("180")),
parks.models.validate_longitude_digits,
],
),
),
]

View File

@@ -20,7 +20,9 @@ def normalize_coordinate(value, max_digits, decimal_places):
# Convert to Decimal for precise handling # Convert to Decimal for precise handling
value = Decimal(str(value)) value = Decimal(str(value))
# Round to specified decimal places # Round to specified decimal places
value = Decimal(value.quantize(Decimal('0.' + '0' * decimal_places), rounding=ROUND_DOWN)) value = Decimal(
value.quantize(Decimal("0." + "0" * decimal_places), rounding=ROUND_DOWN)
)
return value return value
except (TypeError, ValueError, InvalidOperation): except (TypeError, ValueError, InvalidOperation):
@@ -34,10 +36,10 @@ def validate_coordinate_digits(value, max_digits, decimal_places):
# Convert to Decimal for precise handling # Convert to Decimal for precise handling
value = Decimal(str(value)) value = Decimal(str(value))
# Round to exactly 6 decimal places # Round to exactly 6 decimal places
value = value.quantize(Decimal('0.000001'), rounding=ROUND_DOWN) value = value.quantize(Decimal("0.000001"), rounding=ROUND_DOWN)
return value return value
except (InvalidOperation, TypeError): except (InvalidOperation, TypeError):
raise ValidationError('Invalid coordinate value.') raise ValidationError("Invalid coordinate value.")
return value return value
@@ -53,21 +55,19 @@ def validate_longitude_digits(value):
class Park(models.Model): class Park(models.Model):
STATUS_CHOICES = [ STATUS_CHOICES = [
('OPERATING', 'Operating'), ("OPERATING", "Operating"),
('CLOSED_TEMP', 'Temporarily Closed'), ("CLOSED_TEMP", "Temporarily Closed"),
('CLOSED_PERM', 'Permanently Closed'), ("CLOSED_PERM", "Permanently Closed"),
('UNDER_CONSTRUCTION', 'Under Construction'), ("UNDER_CONSTRUCTION", "Under Construction"),
('DEMOLISHED', 'Demolished'), ("DEMOLISHED", "Demolished"),
('RELOCATED', 'Relocated'), ("RELOCATED", "Relocated"),
] ]
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True) slug = models.SlugField(max_length=255, unique=True)
description = models.TextField(blank=True) description = models.TextField(blank=True)
status = models.CharField( status = models.CharField(
max_length=20, max_length=20, choices=STATUS_CHOICES, default="OPERATING"
choices=STATUS_CHOICES,
default='OPERATING'
) )
# Location fields # Location fields
@@ -76,24 +76,24 @@ class Park(models.Model):
decimal_places=6, decimal_places=6,
null=True, null=True,
blank=True, blank=True,
help_text='Latitude coordinate (-90 to 90)', help_text="Latitude coordinate (-90 to 90)",
validators=[ validators=[
MinValueValidator(Decimal('-90')), MinValueValidator(Decimal("-90")),
MaxValueValidator(Decimal('90')), MaxValueValidator(Decimal("90")),
validate_latitude_digits, validate_latitude_digits,
] ],
) )
longitude = models.DecimalField( longitude = models.DecimalField(
max_digits=10, max_digits=10,
decimal_places=6, decimal_places=6,
null=True, null=True,
blank=True, blank=True,
help_text='Longitude coordinate (-180 to 180)', help_text="Longitude coordinate (-180 to 180)",
validators=[ validators=[
MinValueValidator(Decimal('-180')), MinValueValidator(Decimal("-180")),
MaxValueValidator(Decimal('180')), MaxValueValidator(Decimal("180")),
validate_longitude_digits, validate_longitude_digits,
] ],
) )
street_address = models.CharField(max_length=255, blank=True) street_address = models.CharField(max_length=255, blank=True)
city = models.CharField(max_length=255, blank=True) city = models.CharField(max_length=255, blank=True)
@@ -106,32 +106,22 @@ class Park(models.Model):
closing_date = models.DateField(null=True, blank=True) closing_date = models.DateField(null=True, blank=True)
operating_season = models.CharField(max_length=255, blank=True) operating_season = models.CharField(max_length=255, blank=True)
size_acres = models.DecimalField( size_acres = models.DecimalField(
max_digits=10, max_digits=10, decimal_places=2, null=True, blank=True
decimal_places=2,
null=True,
blank=True
) )
website = models.URLField(blank=True) website = models.URLField(blank=True)
# Statistics # Statistics
average_rating = models.DecimalField( average_rating = models.DecimalField(
max_digits=3, max_digits=3, decimal_places=2, null=True, blank=True
decimal_places=2,
null=True,
blank=True
) )
total_rides = models.IntegerField(null=True, blank=True) total_rides = models.IntegerField(null=True, blank=True)
total_roller_coasters = models.IntegerField(null=True, blank=True) total_roller_coasters = models.IntegerField(null=True, blank=True)
# Relationships # Relationships
owner = models.ForeignKey( owner = models.ForeignKey(
Company, Company, on_delete=models.SET_NULL, null=True, blank=True, related_name="parks"
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='parks'
) )
photos = GenericRelation(Photo, related_query_name='park') photos = GenericRelation(Photo, related_query_name="park")
# Metadata # Metadata
created_at = models.DateTimeField(auto_now_add=True, null=True) created_at = models.DateTimeField(auto_now_add=True, null=True)
@@ -139,7 +129,7 @@ class Park(models.Model):
history = HistoricalRecords() history = HistoricalRecords()
class Meta: class Meta:
ordering = ['name'] ordering = ["name"]
def __str__(self): def __str__(self):
return self.name return self.name
@@ -157,7 +147,18 @@ class Park(models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('parks:park_detail', kwargs={'slug': self.slug}) return reverse("parks:park_detail", kwargs={"slug": self.slug})
@property
def formatted_location(self):
parts = []
if self.city:
parts.append(self.city)
if self.state:
parts.append(self.state)
if self.country:
parts.append(self.country)
return ", ".join(parts)
@classmethod @classmethod
def get_by_slug(cls, slug): def get_by_slug(cls, slug):
@@ -166,7 +167,7 @@ class Park(models.Model):
return cls.objects.get(slug=slug), False return cls.objects.get(slug=slug), False
except cls.DoesNotExist: except cls.DoesNotExist:
# Check historical slugs # Check historical slugs
history = cls.history.filter(slug=slug).order_by('-history_date').first() history = cls.history.filter(slug=slug).order_by("-history_date").first()
if history: if history:
try: try:
return cls.objects.get(id=history.id), True return cls.objects.get(id=history.id), True
@@ -176,11 +177,7 @@ class Park(models.Model):
class ParkArea(models.Model): class ParkArea(models.Model):
park = models.ForeignKey( park = models.ForeignKey(Park, on_delete=models.CASCADE, related_name="areas")
Park,
on_delete=models.CASCADE,
related_name='areas'
)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255) slug = models.SlugField(max_length=255)
description = models.TextField(blank=True) description = models.TextField(blank=True)
@@ -193,8 +190,8 @@ class ParkArea(models.Model):
history = HistoricalRecords() history = HistoricalRecords()
class Meta: class Meta:
ordering = ['name'] ordering = ["name"]
unique_together = ['park', 'slug'] unique_together = ["park", "slug"]
def __str__(self): def __str__(self):
return f"{self.name} at {self.park.name}" return f"{self.name} at {self.park.name}"
@@ -205,10 +202,10 @@ class ParkArea(models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('parks:area_detail', kwargs={ return reverse(
'park_slug': self.park.slug, "parks:area_detail",
'area_slug': self.slug kwargs={"park_slug": self.park.slug, "area_slug": self.slug},
}) )
@classmethod @classmethod
def get_by_slug(cls, slug): def get_by_slug(cls, slug):
@@ -217,7 +214,7 @@ class ParkArea(models.Model):
return cls.objects.get(slug=slug), False return cls.objects.get(slug=slug), False
except cls.DoesNotExist: except cls.DoesNotExist:
# Check historical slugs # Check historical slugs
history = cls.history.filter(slug=slug).order_by('-history_date').first() history = cls.history.filter(slug=slug).order_by("-history_date").first()
if history: if history:
try: try:
return cls.objects.get(id=history.id), True return cls.objects.get(id=history.id), True

View File

@@ -2401,10 +2401,6 @@ select {
height: 300px; height: 300px;
} }
.h-\[400px\] {
height: 400px;
}
.h-full { .h-full {
height: 100%; height: 100%;
} }
@@ -2860,11 +2856,6 @@ select {
background-color: rgb(202 138 4 / var(--tw-bg-opacity)); background-color: rgb(202 138 4 / var(--tw-bg-opacity));
} }
.bg-purple-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 232 255 / var(--tw-bg-opacity));
}
.bg-opacity-50 { .bg-opacity-50 {
--tw-bg-opacity: 0.5; --tw-bg-opacity: 0.5;
} }
@@ -3165,11 +3156,6 @@ select {
color: rgb(133 77 14 / var(--tw-text-opacity)); color: rgb(133 77 14 / var(--tw-text-opacity));
} }
.text-purple-800 {
--tw-text-opacity: 1;
color: rgb(107 33 168 / var(--tw-text-opacity));
}
.opacity-0 { .opacity-0 {
opacity: 0; opacity: 0;
} }
@@ -3569,11 +3555,6 @@ select {
background-color: rgb(113 63 18 / var(--tw-bg-opacity)); background-color: rgb(113 63 18 / var(--tw-bg-opacity));
} }
.dark\:bg-purple-700:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(126 34 206 / var(--tw-bg-opacity));
}
.dark\:from-gray-950:is(.dark *) { .dark\:from-gray-950:is(.dark *) {
--tw-gradient-from: #030712 var(--tw-gradient-from-position); --tw-gradient-from: #030712 var(--tw-gradient-from-position);
--tw-gradient-to: rgb(3 7 18 / 0) var(--tw-gradient-to-position); --tw-gradient-to: rgb(3 7 18 / 0) var(--tw-gradient-to-position);
@@ -3689,11 +3670,6 @@ select {
color: rgb(254 252 232 / var(--tw-text-opacity)); color: rgb(254 252 232 / var(--tw-text-opacity));
} }
.dark\:text-purple-50:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(250 245 255 / var(--tw-text-opacity));
}
.dark\:ring-1:is(.dark *) { .dark\:ring-1:is(.dark *) {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
@@ -3838,10 +3814,6 @@ select {
margin-right: 1.5rem; margin-right: 1.5rem;
} }
.lg\:mt-0 {
margin-top: 0px;
}
.lg\:flex { .lg\:flex {
display: flex; display: flex;
} }

View File

@@ -13,14 +13,11 @@
<div> <div>
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">{{ park.name }}</h1> <h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">{{ park.name }}</h1>
{% if park.city or park.state or park.country %} {% if park.city or park.state or park.country %}
{% spaceless %}
<p class="mb-2 text-gray-600 dark:text-gray-400"> <p class="mb-2 text-gray-600 dark:text-gray-400">
<i class="mr-1 fas fa-map-marker-alt"></i> <i class="mr-1 fas fa-map-marker-alt"></i> {% if park.city %}{{ park.city }}{% endif %}{% if park.city and park.state %}, {% endif %}{% if park.state %}{{ park.state }}{% endif %}{% if park.country and park.state or park.city %}, {% endif %}{% if park.country %}{{ park.country }}{% endif %}
{% if park.city %}{{ park.city }}{% endif %}
{% if park.city and park.state %}, {% endif %}
{% if park.state %}{{ park.state }}{% endif %}
{% if park.country and park.state or park.city %}, {% endif %}
{% if park.country %}{{ park.country }}{% endif %}
</p> </p>
{% endspaceless %}
{% endif %} {% endif %}
</div> </div>
{% if user.is_authenticated %} {% if user.is_authenticated %}
@@ -191,10 +188,10 @@
{% endif %} {% endif %}
</div> </div>
<div class="mt-2"> <div class="mt-2">
{% for field, change in record.diff_against_previous %} {% for field, changes in record.diff_against_previous.items %}
<div class="text-sm"> <div class="text-sm">
<span class="font-medium">{{ field }}:</span> <span class="font-medium">{{ field }}:</span>
{{ change.old }} → {{ change.new }} {{ changes.old }} → {{ changes.new }}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -19,11 +19,10 @@
{% if park.city or park.state or park.country %} {% if park.city or park.state or park.country %}
<p class="mb-3 text-gray-600 dark:text-gray-400"> <p class="mb-3 text-gray-600 dark:text-gray-400">
<i class="mr-1 fas fa-map-marker-alt"></i> <i class="mr-1 fas fa-map-marker-alt"></i>
{% if park.city %}{{ park.city }}{% endif %} {% spaceless %}
{% if park.city and park.state %}, {% endif %} {% if park.city %}{{ park.city }}{% endif %}{% if park.city and park.state %}, {% endif %}{% if park.state %}{{ park.state }}{% endif %}{% if park.country and park.state or park.city %}, {% endif %}{% if park.country %}{{ park.country }}{% endif %}
{% if park.state %}{{ park.state }}{% endif %} </p>
{% if park.country and park.state or park.city %}, {% endif %} {% endspaceless %}
{% if park.country %}{{ park.country }}{% endif %}
</p> </p>
{% endif %} {% endif %}
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">

View File

@@ -205,10 +205,10 @@
{% endif %} {% endif %}
</div> </div>
<div class="mt-2"> <div class="mt-2">
{% for field, change in record.diff_against_previous %} {% for field, changes in record.diff_against_previous.items %}
<div class="text-sm"> <div class="text-sm">
<span class="font-medium">{{ field }}:</span> <span class="font-medium">{{ field }}:</span>
{{ change.old }} → {{ change.new }} {{ changes.old }} → {{ changes.new }}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -44,6 +44,7 @@ INSTALLED_APPS = [
"email_service", "email_service",
"media.apps.MediaConfig", "media.apps.MediaConfig",
"moderation", "moderation",
"history_tracking",
] ]
MIDDLEWARE = [ MIDDLEWARE = [