""" 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}"