complete rework of offerings
This commit is contained in:
parent
84e25c82d1
commit
20f27bd6b5
16 changed files with 313 additions and 294 deletions
|
@ -11,6 +11,44 @@ def validate_image_size(value):
|
|||
raise ValidationError("Maximum file size is 1MB")
|
||||
|
||||
|
||||
class ReusableText(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
textsnippet = models.ForeignKey(
|
||||
"self",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="children",
|
||||
)
|
||||
text = ProseEditorField()
|
||||
|
||||
class Meta:
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_full_text(self):
|
||||
"""Returns the text with all nested textsnippet content recursively included in reverse order"""
|
||||
text_parts = []
|
||||
|
||||
# Recursively collect all text parts
|
||||
def collect_text(snippet):
|
||||
if snippet is None:
|
||||
return
|
||||
collect_text(snippet.textsnippet) # Collect deepest snippets first
|
||||
text_parts.append(snippet.text)
|
||||
|
||||
# Start collection with the deepest snippets
|
||||
collect_text(self.textsnippet)
|
||||
|
||||
# Add the main text at the end
|
||||
text_parts.append(self.text)
|
||||
|
||||
# Join all text parts
|
||||
return "".join(text_parts)
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
@ -69,31 +107,6 @@ 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)
|
||||
|
@ -182,19 +195,18 @@ class ServiceOffering(models.Model):
|
|||
CloudProvider, on_delete=models.CASCADE, related_name="offerings"
|
||||
)
|
||||
slug = models.SlugField(max_length=250, unique=True)
|
||||
description = ProseEditorField(help_text="Provider-specific details and features")
|
||||
description = ProseEditorField(blank=True, null=True)
|
||||
offer_description = models.ForeignKey(
|
||||
ReusableText,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="offer_descriptions",
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
STATUS_CHOICES = [
|
||||
("available", "Available"),
|
||||
("planned", "Planned"),
|
||||
("on_request", "On Request"),
|
||||
]
|
||||
status = models.CharField(
|
||||
max_length=20, choices=STATUS_CHOICES, default="available"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["service", "cloud_provider"]
|
||||
ordering = ["service__name", "cloud_provider__name"]
|
||||
|
@ -219,40 +231,54 @@ class ServiceOffering(models.Model):
|
|||
class Plan(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
description = ProseEditorField()
|
||||
plan_description = models.ForeignKey(
|
||||
ReusableText,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="plan_descriptions",
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
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)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["order", "name"]
|
||||
ordering = ["name"]
|
||||
unique_together = [["offering", "name"]]
|
||||
|
||||
def __str__(self):
|
||||
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 ExternalLinkOffering(models.Model):
|
||||
offering = models.ForeignKey(
|
||||
ServiceOffering, on_delete=models.CASCADE, related_name="external_links"
|
||||
)
|
||||
url = models.URLField()
|
||||
description = models.CharField(max_length=200)
|
||||
order = models.IntegerField(default=0)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = [["plan", "currency", "term"]]
|
||||
ordering = ["order", "description"]
|
||||
verbose_name = "External Link"
|
||||
verbose_name_plural = "External Links"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.plan.name} - {self.currency.code} {self.price}"
|
||||
return f"{self.description} ({self.url})"
|
||||
|
||||
def clean(self):
|
||||
from django.core.validators import URLValidator
|
||||
|
||||
validate = URLValidator()
|
||||
try:
|
||||
validate(self.url)
|
||||
except ValidationError:
|
||||
raise ValidationError({"url": "Enter a valid URL."})
|
||||
|
||||
|
||||
class ExternalLink(models.Model):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue