refactor all the things
This commit is contained in:
parent
8ed39690f1
commit
bb5cb708bd
36 changed files with 1563 additions and 931 deletions
|
@ -2,7 +2,6 @@ from django.db import models
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
|
||||
from django_prose_editor.fields import ProseEditorField
|
||||
|
||||
|
||||
|
@ -12,63 +11,6 @@ def validate_image_size(value):
|
|||
raise ValidationError("Maximum file size is 1MB")
|
||||
|
||||
|
||||
class Currency(models.Model):
|
||||
code = models.CharField(max_length=3, unique=True) # ISO 4217 currency code
|
||||
name = models.CharField(max_length=50)
|
||||
symbol = models.CharField(max_length=5)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Currencies"
|
||||
ordering = ["code"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code} ({self.name})"
|
||||
|
||||
|
||||
class Plan(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
description = ProseEditorField()
|
||||
service = models.ForeignKey(
|
||||
"Service", on_delete=models.CASCADE, related_name="plans"
|
||||
)
|
||||
is_default = models.BooleanField(default=False)
|
||||
features = ProseEditorField(blank=True)
|
||||
order = models.IntegerField(default=0)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["order", "name"]
|
||||
unique_together = [["service", "name"]]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.service.name} - {self.name}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.is_default:
|
||||
# Ensure only one default plan per service
|
||||
Plan.objects.filter(service=self.service).update(is_default=False)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
# If this is the only plan, make it default
|
||||
if self.pk and self.service and self.service.plans.count() == 1:
|
||||
self.is_default = True
|
||||
|
||||
|
||||
class PlanPrice(models.Model):
|
||||
plan = models.ForeignKey(Plan, on_delete=models.CASCADE, related_name="prices")
|
||||
currency = models.ForeignKey(Currency, on_delete=models.PROTECT)
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
|
||||
class Meta:
|
||||
unique_together = [["plan", "currency"]]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.plan.name} - {self.currency.code} {self.price}"
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
@ -105,7 +47,8 @@ class Category(models.Model):
|
|||
class CloudProvider(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
slug = models.SlugField(unique=True)
|
||||
description = ProseEditorField(blank=True)
|
||||
description = ProseEditorField()
|
||||
website = models.URLField()
|
||||
logo = models.ImageField(
|
||||
upload_to="cloud_provider_logos/",
|
||||
validators=[validate_image_size],
|
||||
|
@ -125,10 +68,66 @@ class CloudProvider(models.Model):
|
|||
return reverse("services:provider_detail", kwargs={"slug": self.slug})
|
||||
|
||||
|
||||
class Currency(models.Model):
|
||||
code = models.CharField(max_length=3, unique=True) # ISO 4217 currency code
|
||||
name = models.CharField(max_length=50)
|
||||
symbol = models.CharField(max_length=5)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Currencies"
|
||||
ordering = ["code"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code} ({self.name})"
|
||||
|
||||
|
||||
class Term(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.TextField(blank=True)
|
||||
order = models.IntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
ordering = ["order", "name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Service(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
slug = models.SlugField(max_length=250, unique=True)
|
||||
description = ProseEditorField()
|
||||
logo = models.ImageField(
|
||||
upload_to="service_logos/",
|
||||
validators=[validate_image_size],
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
categories = models.ManyToManyField(Category, related_name="services")
|
||||
features = ProseEditorField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
counter = 1
|
||||
while Service.objects.filter(slug=self.slug).exists():
|
||||
self.slug = f"{slugify(self.name)}-{counter}"
|
||||
counter += 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("services:service_detail", kwargs={"slug": self.slug})
|
||||
|
||||
|
||||
class ConsultingPartner(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
slug = models.SlugField(unique=True)
|
||||
description = ProseEditorField(blank=True)
|
||||
description = ProseEditorField()
|
||||
logo = models.ImageField(
|
||||
upload_to="partner_logos/",
|
||||
validators=[validate_image_size],
|
||||
|
@ -136,6 +135,10 @@ class ConsultingPartner(models.Model):
|
|||
blank=True,
|
||||
)
|
||||
website = models.URLField(blank=True)
|
||||
services = models.ManyToManyField(Service, related_name="consulting_partners")
|
||||
cloud_providers = models.ManyToManyField(
|
||||
CloudProvider, related_name="consulting_partners", blank=True
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
@ -151,60 +154,76 @@ class ConsultingPartner(models.Model):
|
|||
return reverse("services:partner_detail", kwargs={"slug": self.slug})
|
||||
|
||||
|
||||
class Service(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
class ServiceOffering(models.Model):
|
||||
service = models.ForeignKey(
|
||||
Service, on_delete=models.CASCADE, related_name="offerings"
|
||||
)
|
||||
cloud_provider = models.ForeignKey(
|
||||
CloudProvider, on_delete=models.CASCADE, related_name="offerings"
|
||||
)
|
||||
slug = models.SlugField(max_length=250, unique=True)
|
||||
description = ProseEditorField()
|
||||
cloud_provider = models.ForeignKey(CloudProvider, on_delete=models.CASCADE)
|
||||
consulting_partners = models.ManyToManyField(
|
||||
ConsultingPartner, related_name="services", blank=True
|
||||
)
|
||||
categories = models.ManyToManyField(Category, related_name="services")
|
||||
features = ProseEditorField()
|
||||
logo = models.ImageField(
|
||||
upload_to="service_logos/",
|
||||
validators=[validate_image_size],
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
description = ProseEditorField(help_text="Provider-specific details and features")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["service", "cloud_provider"]
|
||||
ordering = ["service__name", "cloud_provider__name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return f"{self.service.name} on {self.cloud_provider.name}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
base_slug = f"{self.name}-{self.cloud_provider.name}"
|
||||
base_slug = f"{self.service.name}-{self.cloud_provider.name}"
|
||||
self.slug = slugify(base_slug)
|
||||
|
||||
# If slug exists, append number
|
||||
counter = 1
|
||||
while Service.objects.filter(slug=self.slug).exists():
|
||||
while ServiceOffering.objects.filter(slug=self.slug).exists():
|
||||
self.slug = f"{slugify(base_slug)}-{counter}"
|
||||
counter += 1
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("services:service_detail", kwargs={"slug": self.slug})
|
||||
|
||||
def get_default_plan(self):
|
||||
return self.plans.filter(is_default=True).first() or self.plans.first()
|
||||
return reverse("services:offering_detail", kwargs={"slug": self.slug})
|
||||
|
||||
|
||||
class Lead(models.Model):
|
||||
service = models.ForeignKey(Service, on_delete=models.CASCADE)
|
||||
plan = models.ForeignKey(Plan, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
name = models.CharField(max_length=200)
|
||||
company = models.CharField(max_length=200)
|
||||
email = models.EmailField()
|
||||
phone = models.CharField(max_length=50)
|
||||
class Plan(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
description = ProseEditorField()
|
||||
offering = models.ForeignKey(
|
||||
ServiceOffering, on_delete=models.CASCADE, related_name="plans"
|
||||
)
|
||||
is_default = models.BooleanField(default=False)
|
||||
features = ProseEditorField(blank=True)
|
||||
order = models.IntegerField(default=0)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
odoo_lead_id = models.IntegerField(null=True, blank=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["order", "name"]
|
||||
unique_together = [["offering", "name"]]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} - {self.company} ({self.service})"
|
||||
return f"{self.offering} - {self.name}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.is_default:
|
||||
# Ensure only one default plan per offering
|
||||
Plan.objects.filter(offering=self.offering).update(is_default=False)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class PlanPrice(models.Model):
|
||||
plan = models.ForeignKey(Plan, on_delete=models.CASCADE, related_name="prices")
|
||||
currency = models.ForeignKey(Currency, on_delete=models.PROTECT)
|
||||
term = models.ForeignKey(Term, on_delete=models.PROTECT)
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
|
||||
class Meta:
|
||||
unique_together = [["plan", "currency", "term"]]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.plan.name} - {self.currency.code} {self.price}"
|
||||
|
||||
|
||||
class ExternalLink(models.Model):
|
||||
|
@ -227,11 +246,26 @@ class ExternalLink(models.Model):
|
|||
|
||||
def clean(self):
|
||||
from django.core.validators import URLValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
# Validate URL
|
||||
validate = URLValidator()
|
||||
try:
|
||||
validate(self.url)
|
||||
except ValidationError:
|
||||
raise ValidationError({"url": "Enter a valid URL."})
|
||||
|
||||
|
||||
class Lead(models.Model):
|
||||
service = models.ForeignKey(Service, on_delete=models.CASCADE)
|
||||
offering = models.ForeignKey(
|
||||
ServiceOffering, on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
plan = models.ForeignKey(Plan, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
name = models.CharField(max_length=200)
|
||||
company = models.CharField(max_length=200)
|
||||
email = models.EmailField()
|
||||
phone = models.CharField(max_length=50)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
odoo_lead_id = models.IntegerField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} - {self.company} ({self.service})"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue