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 def validate_image_size(value): filesize = value.size if filesize > 1 * 1024 * 1024: # 1MB raise ValidationError("Maximum file size is 1MB") class Category(models.Model): name = models.CharField(max_length=100) slug = models.SlugField(unique=True) parent = models.ForeignKey( "self", on_delete=models.CASCADE, null=True, blank=True, related_name="children" ) description = models.TextField(blank=True) order = models.IntegerField(default=0) class Meta: verbose_name_plural = "Categories" ordering = ["order", "name"] def __str__(self): if self.parent: return f"{self.parent} > {self.name}" return self.name def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) super().save(*args, **kwargs) @property def full_path(self): path = [self.name] parent = self.parent while parent: path.append(parent.name) parent = parent.parent return " > ".join(reversed(path)) class CloudProvider(models.Model): name = models.CharField(max_length=100) slug = models.SlugField(unique=True) description = ProseEditorField() website = models.URLField() logo = models.ImageField( upload_to="cloud_provider_logos/", validators=[validate_image_size], null=True, blank=True, ) is_featured = models.BooleanField(default=False) def __str__(self): return self.name def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) super().save(*args, **kwargs) def get_absolute_url(self): 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() is_featured = models.BooleanField(default=False) is_coming_soon = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.name def clean(self): if self.is_featured and self.is_coming_soon: raise ValidationError( "A service cannot be both featured and coming soon simultaneously." ) super().clean() def save(self, *args, **kwargs): self.clean() # Ensure validation runs on save 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() logo = models.ImageField( upload_to="partner_logos/", validators=[validate_image_size], null=True, 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 ) is_featured = models.BooleanField(default=False) 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) super().save(*args, **kwargs) def get_absolute_url(self): return reverse("services:partner_detail", kwargs={"slug": self.slug}) 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(help_text="Provider-specific details and features") 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"] def __str__(self): return f"{self.service.name} on {self.cloud_provider.name}" def save(self, *args, **kwargs): if not self.slug: base_slug = f"{self.service.name}-{self.cloud_provider.name}" self.slug = slugify(base_slug) counter = 1 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:offering_detail", kwargs={"slug": self.slug}) 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) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["order", "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 Meta: unique_together = [["plan", "currency", "term"]] def __str__(self): return f"{self.plan.name} - {self.currency.code} {self.price}" class ExternalLink(models.Model): service = models.ForeignKey( Service, 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: ordering = ["order", "description"] verbose_name = "External Link" verbose_name_plural = "External Links" def __str__(self): 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 Lead(models.Model): name = models.CharField(max_length=200) email = models.EmailField() company = models.CharField(max_length=200, null=True, blank=True) phone = models.CharField(max_length=50, null=True, blank=True) message = models.TextField(blank=True, null=True, max_length=1000) odoo_lead_id = models.IntegerField(null=True, blank=True) service = models.ForeignKey( Service, on_delete=models.SET_NULL, null=True, blank=True ) 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) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.name} - {self.company} ({self.service})"