Files
igny8/backend/igny8_core/plugins/models.py
2026-01-10 00:26:00 +00:00

386 lines
11 KiB
Python

"""
Plugin Distribution System Models
Tracks plugins, versions, and installations across sites.
"""
from django.db import models
from django.conf import settings
from django.utils import timezone
class Plugin(models.Model):
"""
Represents a plugin type (WordPress, Shopify, etc.)
Each plugin can have multiple versions and be installed on multiple sites.
"""
PLATFORM_CHOICES = [
('wordpress', 'WordPress'),
('shopify', 'Shopify'),
('custom', 'Custom Site'),
]
name = models.CharField(
max_length=100,
help_text="Human-readable plugin name (e.g., 'IGNY8 WP Bridge')"
)
slug = models.SlugField(
unique=True,
help_text="URL-safe identifier (e.g., 'igny8-wp-bridge')"
)
platform = models.CharField(
max_length=20,
choices=PLATFORM_CHOICES,
help_text="Target platform for this plugin"
)
description = models.TextField(
blank=True,
help_text="Plugin description for display in download pages"
)
homepage_url = models.URLField(
blank=True,
help_text="Plugin homepage or documentation URL"
)
is_active = models.BooleanField(
default=True,
db_index=True,
help_text="Whether this plugin is available for download"
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'plugins'
ordering = ['name']
verbose_name = 'Plugin'
verbose_name_plural = 'Plugins'
def __str__(self):
return f"{self.name} ({self.platform})"
def get_latest_version(self):
"""Get the latest released version of this plugin."""
return self.versions.filter(
status='released'
).first()
def get_download_count(self):
"""Get total download count across all versions."""
return self.downloads.count()
class PluginVersion(models.Model):
"""
Tracks each version of a plugin.
Versions follow semantic versioning (major.minor.patch).
"""
STATUS_CHOICES = [
('draft', 'Draft'), # In development - NOT available for download
('released', 'Released'), # Available for download and updates
('deprecated', 'Deprecated'), # Old version, not recommended
]
plugin = models.ForeignKey(
Plugin,
on_delete=models.CASCADE,
related_name='versions',
help_text="Plugin this version belongs to"
)
version = models.CharField(
max_length=20,
help_text="Semantic version string (e.g., '1.0.0', '1.0.1')"
)
version_code = models.IntegerField(
null=True,
blank=True,
help_text="Numeric version for comparison (1.0.1 -> 10001). Auto-calculated."
)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='draft',
db_index=True,
help_text="Release status of this version"
)
# File info
file_path = models.CharField(
max_length=500,
blank=True,
default='',
help_text="Relative path to ZIP file in dist/ directory. Auto-generated on release."
)
file_size = models.IntegerField(
default=0,
help_text="File size in bytes"
)
checksum = models.CharField(
max_length=64,
blank=True,
help_text="SHA256 checksum for integrity verification"
)
# Release info
changelog = models.TextField(
blank=True,
help_text="What's new in this version (supports Markdown)"
)
min_api_version = models.CharField(
max_length=20,
default='1.0',
help_text="Minimum IGNY8 API version required"
)
min_platform_version = models.CharField(
max_length=20,
blank=True,
help_text="Minimum platform version (e.g., WordPress 5.0)"
)
min_php_version = models.CharField(
max_length=10,
default='7.4',
help_text="Minimum PHP version required (for WordPress plugins)"
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
released_at = models.DateTimeField(
null=True,
blank=True,
help_text="When this version was released"
)
# Auto-update control
force_update = models.BooleanField(
default=False,
help_text="Force update for critical security fixes"
)
class Meta:
db_table = 'plugin_versions'
unique_together = ['plugin', 'version']
ordering = ['-version_code']
verbose_name = 'Plugin Version'
verbose_name_plural = 'Plugin Versions'
def __str__(self):
return f"{self.plugin.name} v{self.version}"
def save(self, *args, **kwargs):
"""Calculate version_code from version string if not set."""
if not self.version_code:
self.version_code = self.parse_version_code(self.version)
super().save(*args, **kwargs)
@staticmethod
def parse_version_code(version_string):
"""
Convert version string to numeric code for comparison.
'1.0.0' -> 10000
'1.0.1' -> 10001
'1.2.3' -> 10203
'2.0.0' -> 20000
"""
try:
parts = version_string.split('.')
major = int(parts[0]) if len(parts) > 0 else 0
minor = int(parts[1]) if len(parts) > 1 else 0
patch = int(parts[2]) if len(parts) > 2 else 0
return major * 10000 + minor * 100 + patch
except (ValueError, IndexError):
return 0
def release(self):
"""Mark this version as released."""
self.status = 'released'
self.released_at = timezone.now()
self.save(update_fields=['status', 'released_at'])
def get_download_count(self):
"""Get download count for this version."""
return self.downloads.count()
class PluginInstallation(models.Model):
"""
Tracks where plugins are installed (per site).
This allows us to:
- Notify sites about available updates
- Track version distribution
- Monitor plugin health
"""
site = models.ForeignKey(
'igny8_core_auth.Site',
on_delete=models.CASCADE,
related_name='plugin_installations',
help_text="Site where plugin is installed"
)
plugin = models.ForeignKey(
Plugin,
on_delete=models.CASCADE,
related_name='installations',
help_text="Installed plugin"
)
current_version = models.ForeignKey(
PluginVersion,
on_delete=models.SET_NULL,
null=True,
related_name='current_installations',
help_text="Currently installed version"
)
# Installation status
is_active = models.BooleanField(
default=True,
db_index=True,
help_text="Whether the plugin is currently active"
)
last_health_check = models.DateTimeField(
null=True,
blank=True,
help_text="Last successful health check timestamp"
)
health_status = models.CharField(
max_length=20,
default='unknown',
choices=[
('healthy', 'Healthy'),
('outdated', 'Outdated'),
('error', 'Error'),
('unknown', 'Unknown'),
],
help_text="Current health status"
)
# Update tracking
pending_update = models.ForeignKey(
PluginVersion,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='pending_installations',
help_text="Pending version update"
)
update_notified_at = models.DateTimeField(
null=True,
blank=True,
help_text="When site was notified about pending update"
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'plugin_installations'
unique_together = ['site', 'plugin']
ordering = ['-updated_at']
verbose_name = 'Plugin Installation'
verbose_name_plural = 'Plugin Installations'
def __str__(self):
version_str = f" v{self.current_version.version}" if self.current_version else ""
return f"{self.plugin.name}{version_str} on {self.site.name}"
def check_for_update(self):
"""Check if an update is available for this installation."""
latest = self.plugin.get_latest_version()
if not latest or not self.current_version:
return None
if latest.version_code > self.current_version.version_code:
return latest
return None
def update_health_status(self):
"""Update health status based on current state."""
latest = self.plugin.get_latest_version()
if not self.current_version:
self.health_status = 'unknown'
elif latest and latest.version_code > self.current_version.version_code:
self.health_status = 'outdated'
else:
self.health_status = 'healthy'
self.last_health_check = timezone.now()
self.save(update_fields=['health_status', 'last_health_check'])
class PluginDownload(models.Model):
"""
Tracks plugin download events for analytics.
"""
plugin = models.ForeignKey(
Plugin,
on_delete=models.CASCADE,
related_name='downloads'
)
version = models.ForeignKey(
PluginVersion,
on_delete=models.CASCADE,
related_name='downloads'
)
# Download context
site = models.ForeignKey(
'igny8_core_auth.Site',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='plugin_downloads',
help_text="Site that initiated the download (if authenticated)"
)
account = models.ForeignKey(
'igny8_core_auth.Account',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='plugin_downloads',
help_text="Account that initiated the download"
)
# Request info
ip_address = models.GenericIPAddressField(
null=True,
blank=True
)
user_agent = models.CharField(
max_length=500,
blank=True
)
# Download type
download_type = models.CharField(
max_length=20,
default='manual',
choices=[
('manual', 'Manual Download'),
('update', 'Auto Update'),
('api', 'API Download'),
]
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'plugin_downloads'
ordering = ['-created_at']
indexes = [
models.Index(fields=['plugin', 'created_at']),
models.Index(fields=['version', 'created_at']),
]
verbose_name = 'Plugin Download'
verbose_name_plural = 'Plugin Downloads'
def __str__(self):
return f"Download: {self.plugin.name} v{self.version.version}"