change release section put video and more
This commit is contained in:
+124
-2
@@ -6,6 +6,7 @@ from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
|
||||
from apps.core.utils import CONTENT_FORMAT_CHOICES, FORMAT_PLAIN, render_content
|
||||
from apps.core.video import VideoBlockMixin
|
||||
|
||||
from .release_assets import (
|
||||
FILE_FIELD_BY_ASSET_KEY,
|
||||
@@ -36,6 +37,35 @@ DISTRIBUTION_CHOICES = [
|
||||
(DISTRIBUTION_PACKAGE, "Package (external / non-installable)"),
|
||||
]
|
||||
|
||||
RELEASE_CHANNEL_STABLE = "stable"
|
||||
RELEASE_CHANNEL_BETA = "beta"
|
||||
RELEASE_CHANNEL_RC = "rc"
|
||||
RELEASE_CHANNEL_PREVIEW = "preview"
|
||||
RELEASE_CHANNEL_NIGHTLY = "nightly"
|
||||
RELEASE_CHANNEL_CURRENT = "current"
|
||||
RELEASE_CHANNEL_PREVIOUS = "previous"
|
||||
|
||||
RELEASE_CHANNEL_CHOICES = [
|
||||
("", "None"),
|
||||
(RELEASE_CHANNEL_STABLE, "Stable"),
|
||||
(RELEASE_CHANNEL_BETA, "Beta"),
|
||||
(RELEASE_CHANNEL_RC, "Release candidate"),
|
||||
(RELEASE_CHANNEL_PREVIEW, "Preview"),
|
||||
(RELEASE_CHANNEL_NIGHTLY, "Nightly"),
|
||||
(RELEASE_CHANNEL_CURRENT, "Current"),
|
||||
(RELEASE_CHANNEL_PREVIOUS, "Previous"),
|
||||
]
|
||||
|
||||
RELEASE_CHANNEL_LABELS = {
|
||||
RELEASE_CHANNEL_STABLE: "Stable",
|
||||
RELEASE_CHANNEL_BETA: "Beta",
|
||||
RELEASE_CHANNEL_RC: "Release candidate",
|
||||
RELEASE_CHANNEL_PREVIEW: "Preview",
|
||||
RELEASE_CHANNEL_NIGHTLY: "Nightly",
|
||||
RELEASE_CHANNEL_CURRENT: "Current",
|
||||
RELEASE_CHANNEL_PREVIOUS: "Previous",
|
||||
}
|
||||
|
||||
|
||||
class MainProduct(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
@@ -280,9 +310,25 @@ class SubProductVersion(models.Model):
|
||||
blank=True,
|
||||
)
|
||||
version = models.CharField(max_length=80)
|
||||
is_featured_stable = models.BooleanField(
|
||||
release_channel = models.CharField(
|
||||
max_length=20,
|
||||
choices=RELEASE_CHANNEL_CHOICES,
|
||||
blank=True,
|
||||
default="",
|
||||
help_text="Preset badge for this release (Stable, Beta, Previous, etc.).",
|
||||
)
|
||||
channel_label = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
help_text="Optional custom badge text. Overrides the preset channel label when set.",
|
||||
)
|
||||
is_featured = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Highlighted as the main release on the product page.",
|
||||
help_text="Primary release shown at the top of the downloads section.",
|
||||
)
|
||||
show_on_product_page = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Also show this release on the product page (e.g. beta or previous version).",
|
||||
)
|
||||
windows_download_url = models.URLField(blank=True)
|
||||
macos_download_url = models.URLField(blank=True)
|
||||
@@ -361,6 +407,31 @@ class SubProductVersion(models.Model):
|
||||
parent = self.sub_product or self.main_product
|
||||
return f"{parent.name} v{self.version}"
|
||||
|
||||
@property
|
||||
def display_channel_label(self):
|
||||
custom = (self.channel_label or "").strip()
|
||||
if custom:
|
||||
return custom
|
||||
if self.release_channel:
|
||||
return RELEASE_CHANNEL_LABELS.get(
|
||||
self.release_channel,
|
||||
self.release_channel.replace("_", " ").title(),
|
||||
)
|
||||
return ""
|
||||
|
||||
@property
|
||||
def display_channel_css_modifier(self):
|
||||
if (self.channel_label or "").strip():
|
||||
return "custom"
|
||||
return self.release_channel or "none"
|
||||
|
||||
@property
|
||||
def is_previous_channel(self):
|
||||
if self.release_channel == RELEASE_CHANNEL_PREVIOUS:
|
||||
return True
|
||||
label = (self.channel_label or "").strip().lower()
|
||||
return label == "previous"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
for file_field, name_field in RELEASE_ORIGINAL_FILENAME_FIELDS.items():
|
||||
field_file = getattr(self, file_field)
|
||||
@@ -476,3 +547,54 @@ class ArticleCitation(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.article.title} — citation {self.order or self.pk}"
|
||||
|
||||
|
||||
class ProductVideo(VideoBlockMixin, models.Model):
|
||||
main_product = models.ForeignKey(
|
||||
MainProduct,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="videos",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
sub_product = models.ForeignKey(
|
||||
SubProduct,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="videos",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
badge = models.CharField(max_length=100, blank=True)
|
||||
title = models.CharField(max_length=300, blank=True)
|
||||
description = models.TextField(blank=True)
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["order", "pk"]
|
||||
verbose_name = "Product Video"
|
||||
verbose_name_plural = "Product Videos"
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
check=(
|
||||
models.Q(sub_product__isnull=False, main_product__isnull=True)
|
||||
| models.Q(sub_product__isnull=True, main_product__isnull=False)
|
||||
),
|
||||
name="product_video_exactly_one_parent",
|
||||
),
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
has_main = self.main_product_id is not None
|
||||
has_sub = self.sub_product_id is not None
|
||||
if has_main == has_sub:
|
||||
raise ValidationError(
|
||||
"A product video must belong to exactly one main product or sub-product."
|
||||
)
|
||||
self.clean_video_fields(require=True)
|
||||
|
||||
def __str__(self):
|
||||
parent = self.sub_product or self.main_product
|
||||
label = self.title or self.badge or "Video"
|
||||
return f"{parent} › {label}"
|
||||
|
||||
Reference in New Issue
Block a user