feat: rebuild site content management
Replace legacy FAQ, downloads, and release flows with project and blog publishing. Add configurable branding and theme fields plus clean-start Compose seeding support.
This commit is contained in:
+12
-91
@@ -30,13 +30,6 @@ INSTALLABLE_PLATFORM_SPECS = (
|
||||
("source_code_url", "Source code", None),
|
||||
)
|
||||
|
||||
DISTRIBUTION_INSTALLABLE = "installable"
|
||||
DISTRIBUTION_PACKAGE = "package"
|
||||
DISTRIBUTION_CHOICES = [
|
||||
(DISTRIBUTION_INSTALLABLE, "Installable application"),
|
||||
(DISTRIBUTION_PACKAGE, "Package (external / non-installable)"),
|
||||
]
|
||||
|
||||
RELEASE_CHANNEL_STABLE = "stable"
|
||||
RELEASE_CHANNEL_BETA = "beta"
|
||||
RELEASE_CHANNEL_RC = "rc"
|
||||
@@ -66,7 +59,6 @@ RELEASE_CHANNEL_LABELS = {
|
||||
RELEASE_CHANNEL_PREVIOUS: "Previous",
|
||||
}
|
||||
|
||||
|
||||
class MainProduct(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
slug = models.SlugField(unique=True, blank=True)
|
||||
@@ -76,42 +68,17 @@ class MainProduct(models.Model):
|
||||
max_length=20, choices=CONTENT_FORMAT_CHOICES, default=FORMAT_PLAIN
|
||||
)
|
||||
image = models.ImageField(upload_to="products/main/", blank=True, null=True)
|
||||
distribution = models.CharField(
|
||||
max_length=20,
|
||||
choices=DISTRIBUTION_CHOICES,
|
||||
default=DISTRIBUTION_INSTALLABLE,
|
||||
help_text="Only affects how releases appear on the public site.",
|
||||
)
|
||||
package_resource_button_text = models.CharField(
|
||||
max_length=100,
|
||||
default="PyPI",
|
||||
help_text="Label for the package resource button on package releases.",
|
||||
)
|
||||
package_source_button_text = models.CharField(
|
||||
max_length=100,
|
||||
default="Source",
|
||||
help_text="Label for the source code button on package releases.",
|
||||
)
|
||||
downloads_section_link_url = models.URLField(
|
||||
blank=True,
|
||||
help_text="Optional link shown in the top-right corner of the downloads section.",
|
||||
)
|
||||
downloads_section_link_text = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text="Label for the optional downloads section link.",
|
||||
)
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
show_on_homepage = models.BooleanField(default=False, help_text="Display this product in the homepage Products section.")
|
||||
homepage_order = models.PositiveIntegerField(default=0, help_text="Order within the homepage Products section.")
|
||||
show_on_homepage = models.BooleanField(default=False, help_text="Display this project in the homepage Projects section.")
|
||||
homepage_order = models.PositiveIntegerField(default=0, help_text="Order within the homepage Projects section.")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["order", "name"]
|
||||
verbose_name = "Main Product"
|
||||
verbose_name_plural = "Main Products"
|
||||
verbose_name = "Main Project"
|
||||
verbose_name_plural = "Main Projects"
|
||||
|
||||
@property
|
||||
def rendered_description(self):
|
||||
@@ -128,18 +95,7 @@ class MainProduct(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return reverse("products:main_product_detail", kwargs={"main_slug": self.slug})
|
||||
|
||||
def get_versions_archive_url(self):
|
||||
return reverse(
|
||||
"products:main_product_versions",
|
||||
kwargs={"main_slug": self.slug},
|
||||
)
|
||||
|
||||
|
||||
class SubProduct(models.Model):
|
||||
DISTRIBUTION_INSTALLABLE = DISTRIBUTION_INSTALLABLE
|
||||
DISTRIBUTION_PACKAGE = DISTRIBUTION_PACKAGE
|
||||
DISTRIBUTION_CHOICES = DISTRIBUTION_CHOICES
|
||||
|
||||
main_product = models.ForeignKey(
|
||||
MainProduct,
|
||||
on_delete=models.CASCADE,
|
||||
@@ -156,41 +112,16 @@ class SubProduct(models.Model):
|
||||
logo = models.ImageField(upload_to="products/sub_logos/", blank=True, null=True, help_text="Small logo shown as a corner badge on homepage product cards.")
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
show_on_homepage = models.BooleanField(default=False, help_text="Display this sub-product in the homepage Products section.")
|
||||
homepage_order = models.PositiveIntegerField(default=0, help_text="Order within the homepage Products section.")
|
||||
distribution = models.CharField(
|
||||
max_length=20,
|
||||
choices=DISTRIBUTION_CHOICES,
|
||||
default=DISTRIBUTION_INSTALLABLE,
|
||||
help_text="Only affects how releases appear on the public site.",
|
||||
)
|
||||
package_resource_button_text = models.CharField(
|
||||
max_length=100,
|
||||
default="PyPI",
|
||||
help_text="Label for the package resource button on package releases.",
|
||||
)
|
||||
package_source_button_text = models.CharField(
|
||||
max_length=100,
|
||||
default="Source",
|
||||
help_text="Label for the source code button on package releases.",
|
||||
)
|
||||
downloads_section_link_url = models.URLField(
|
||||
blank=True,
|
||||
help_text="Optional link shown in the top-right corner of the downloads section.",
|
||||
)
|
||||
downloads_section_link_text = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text="Label for the optional downloads section link.",
|
||||
)
|
||||
show_on_homepage = models.BooleanField(default=False, help_text="Display this sub-project in the homepage Projects section.")
|
||||
homepage_order = models.PositiveIntegerField(default=0, help_text="Order within the homepage Projects section.")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["order", "name"]
|
||||
unique_together = [["main_product", "slug"]]
|
||||
verbose_name = "Sub Product"
|
||||
verbose_name_plural = "Sub Products"
|
||||
verbose_name = "Sub-project"
|
||||
verbose_name_plural = "Sub-projects"
|
||||
|
||||
@property
|
||||
def rendered_description(self):
|
||||
@@ -213,16 +144,6 @@ class SubProduct(models.Model):
|
||||
},
|
||||
)
|
||||
|
||||
def get_versions_archive_url(self):
|
||||
return reverse(
|
||||
"products:sub_product_versions",
|
||||
kwargs={
|
||||
"main_slug": self.main_product.slug,
|
||||
"sub_slug": self.slug,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class Article(models.Model):
|
||||
main_product = models.ForeignKey(
|
||||
MainProduct,
|
||||
@@ -287,7 +208,7 @@ class Article(models.Model):
|
||||
has_sub = self.sub_product_id is not None
|
||||
if has_main == has_sub:
|
||||
raise ValidationError(
|
||||
"An article must belong to exactly one main product or sub-product."
|
||||
"An article must belong to exactly one main project or sub-project."
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
@@ -572,8 +493,8 @@ class ProductVideo(VideoBlockMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ["order", "pk"]
|
||||
verbose_name = "Product Video"
|
||||
verbose_name_plural = "Product Videos"
|
||||
verbose_name = "Project Video"
|
||||
verbose_name_plural = "Project Videos"
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
check=(
|
||||
@@ -590,7 +511,7 @@ class ProductVideo(VideoBlockMixin, models.Model):
|
||||
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."
|
||||
"A project video must belong to exactly one main project or sub-project."
|
||||
)
|
||||
self.clean_video_fields(require=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user