plugin distribution system
This commit is contained in:
384
backend/igny8_core/plugins/models.py
Normal file
384
backend/igny8_core/plugins/models.py
Normal file
@@ -0,0 +1,384 @@
|
||||
"""
|
||||
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__in=['released', 'update_ready']
|
||||
).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
|
||||
('testing', 'Testing'), # Internal testing
|
||||
('staged', 'Staged'), # Ready, not yet pushed
|
||||
('released', 'Released'), # Available for download
|
||||
('update_ready', 'Update Ready'), # Push to installed sites
|
||||
('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(
|
||||
help_text="Numeric version for comparison (1.0.1 -> 10001)"
|
||||
)
|
||||
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,
|
||||
help_text="Relative path to ZIP file in dist/ directory"
|
||||
)
|
||||
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}"
|
||||
Reference in New Issue
Block a user