PLugin versioning fixes
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Django Admin Configuration for Plugin Distribution System
|
||||
"""
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
@@ -8,6 +9,68 @@ from unfold.admin import ModelAdmin, TabularInline
|
||||
from .models import Plugin, PluginVersion, PluginInstallation, PluginDownload
|
||||
|
||||
|
||||
class PluginVersionForm(forms.ModelForm):
|
||||
"""
|
||||
Simplified form for creating new plugin versions.
|
||||
|
||||
Auto-fills most fields from the latest version, only requires:
|
||||
- Plugin (select)
|
||||
- Version number
|
||||
- Changelog
|
||||
- Status (defaults to 'draft')
|
||||
|
||||
All other fields are either:
|
||||
- Auto-filled from previous version (min_api_version, min_php_version, etc.)
|
||||
- Auto-generated on release (file_path, file_size, checksum)
|
||||
- Auto-calculated (version_code)
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = PluginVersion
|
||||
fields = '__all__'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# If this is a new version (no instance), auto-fill from latest
|
||||
if not self.instance.pk:
|
||||
# Check if plugin is already selected (from initial data or POST data)
|
||||
plugin_id = None
|
||||
|
||||
# Try to get plugin from POST data (when form is submitted)
|
||||
if self.data:
|
||||
plugin_id = self.data.get('plugin')
|
||||
# Try to get plugin from initial data (when form is pre-filled)
|
||||
elif self.initial:
|
||||
plugin_id = self.initial.get('plugin')
|
||||
|
||||
if plugin_id:
|
||||
try:
|
||||
plugin = Plugin.objects.get(pk=plugin_id)
|
||||
latest = plugin.get_latest_version()
|
||||
|
||||
if latest:
|
||||
# Auto-fill from latest version (only if not POST)
|
||||
if not self.data:
|
||||
if 'min_api_version' in self.fields:
|
||||
self.fields['min_api_version'].initial = latest.min_api_version
|
||||
if 'min_platform_version' in self.fields:
|
||||
self.fields['min_platform_version'].initial = latest.min_platform_version
|
||||
if 'min_php_version' in self.fields:
|
||||
self.fields['min_php_version'].initial = latest.min_php_version
|
||||
if 'force_update' in self.fields:
|
||||
self.fields['force_update'].initial = False
|
||||
except (Plugin.DoesNotExist, ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Set helpful help texts for fields that exist in the form
|
||||
# (some fields may be readonly and not in self.fields)
|
||||
if 'version' in self.fields:
|
||||
self.fields['version'].help_text = "Semantic version (e.g., 1.2.0)"
|
||||
if 'changelog' in self.fields:
|
||||
self.fields['changelog'].help_text = "What's new in this version"
|
||||
|
||||
|
||||
class PluginVersionInline(TabularInline):
|
||||
"""Inline admin for plugin versions."""
|
||||
model = PluginVersion
|
||||
@@ -66,35 +129,37 @@ class PluginAdmin(ModelAdmin):
|
||||
class PluginVersionAdmin(ModelAdmin):
|
||||
"""Admin configuration for PluginVersion model."""
|
||||
|
||||
form = PluginVersionForm
|
||||
|
||||
list_display = ['plugin', 'version', 'status', 'file_size_display', 'download_count', 'released_at']
|
||||
list_filter = ['plugin', 'status', 'released_at']
|
||||
search_fields = ['plugin__name', 'version', 'changelog']
|
||||
readonly_fields = ['version_code', 'created_at', 'download_count']
|
||||
readonly_fields = ['version_code', 'file_path', 'file_size', 'checksum', 'created_at', 'released_at', 'download_count']
|
||||
|
||||
fieldsets = [
|
||||
('Version Info', {
|
||||
'fields': ['plugin', 'version', 'version_code', 'status']
|
||||
('Required Fields', {
|
||||
'fields': ['plugin', 'version', 'status', 'changelog'],
|
||||
'description': 'Only these fields are required. Others are auto-filled from previous version or auto-generated.'
|
||||
}),
|
||||
('File Info', {
|
||||
'fields': ['file_path', 'file_size', 'checksum']
|
||||
}),
|
||||
('Requirements', {
|
||||
'fields': ['min_api_version', 'min_platform_version', 'min_php_version']
|
||||
}),
|
||||
('Release', {
|
||||
'fields': ['changelog', 'force_update', 'released_at']
|
||||
}),
|
||||
('Statistics', {
|
||||
'fields': ['download_count'],
|
||||
('Requirements (Auto-filled from previous version)', {
|
||||
'fields': ['min_api_version', 'min_platform_version', 'min_php_version'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ['created_at'],
|
||||
('File Info (Auto-generated on release)', {
|
||||
'fields': ['file_path', 'file_size', 'checksum'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('Advanced Options', {
|
||||
'fields': ['version_code', 'force_update'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ['released_at', 'download_count', 'created_at'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
]
|
||||
|
||||
actions = ['release_versions', 'mark_as_update_ready']
|
||||
actions = ['release_versions', 'mark_as_update_ready', 'mark_as_deprecated']
|
||||
|
||||
def file_size_display(self, obj):
|
||||
if obj.file_size:
|
||||
@@ -109,19 +174,25 @@ class PluginVersionAdmin(ModelAdmin):
|
||||
return obj.get_download_count()
|
||||
download_count.short_description = 'Downloads'
|
||||
|
||||
@admin.action(description="Release selected versions")
|
||||
@admin.action(description="✅ Release selected versions (builds ZIP automatically)")
|
||||
def release_versions(self, request, queryset):
|
||||
from django.utils import timezone
|
||||
count = queryset.filter(status__in=['draft', 'testing', 'staged']).update(
|
||||
status='released',
|
||||
released_at=timezone.now()
|
||||
)
|
||||
self.message_user(request, f"Released {count} version(s)")
|
||||
count = 0
|
||||
for version in queryset.filter(status__in=['draft', 'testing', 'staged']):
|
||||
version.status = 'released'
|
||||
version.save() # Triggers signal to build ZIP
|
||||
count += 1
|
||||
self.message_user(request, f"Released {count} version(s). ZIP files are being built automatically.")
|
||||
|
||||
@admin.action(description="Mark as update ready (push to installations)")
|
||||
@admin.action(description="📢 Mark as update ready (notify WordPress sites)")
|
||||
def mark_as_update_ready(self, request, queryset):
|
||||
count = queryset.filter(status='released').update(status='update_ready')
|
||||
self.message_user(request, f"Marked {count} version(s) as update ready")
|
||||
self.message_user(request, f"Marked {count} version(s) as update ready. WordPress sites will be notified.")
|
||||
|
||||
@admin.action(description="🗑️ Mark as deprecated")
|
||||
def mark_as_deprecated(self, request, queryset):
|
||||
count = queryset.update(status='deprecated')
|
||||
self.message_user(request, f"Marked {count} version(s) as deprecated")
|
||||
|
||||
|
||||
@admin.register(PluginInstallation)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.10 on 2026-01-09 23:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('plugins', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='pluginversion',
|
||||
name='file_path',
|
||||
field=models.CharField(blank=True, default='', help_text='Relative path to ZIP file in dist/ directory. Auto-generated on release.', max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pluginversion',
|
||||
name='version_code',
|
||||
field=models.IntegerField(blank=True, help_text='Numeric version for comparison (1.0.1 -> 10001). Auto-calculated.', null=True),
|
||||
),
|
||||
]
|
||||
@@ -99,7 +99,9 @@ class PluginVersion(models.Model):
|
||||
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)"
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Numeric version for comparison (1.0.1 -> 10001). Auto-calculated."
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
@@ -112,7 +114,9 @@ class PluginVersion(models.Model):
|
||||
# File info
|
||||
file_path = models.CharField(
|
||||
max_length=500,
|
||||
help_text="Relative path to ZIP file in dist/ directory"
|
||||
blank=True,
|
||||
default='',
|
||||
help_text="Relative path to ZIP file in dist/ directory. Auto-generated on release."
|
||||
)
|
||||
file_size = models.IntegerField(
|
||||
default=0,
|
||||
|
||||
@@ -23,58 +23,68 @@ def auto_build_plugin_on_release(sender, instance, **kwargs):
|
||||
1. ZIP file is always up-to-date with source code
|
||||
2. File size and checksum are auto-calculated
|
||||
3. No manual intervention needed for releases
|
||||
|
||||
Triggers on:
|
||||
- New version created with status 'released' or 'update_ready'
|
||||
- Existing version status changed to 'released' or 'update_ready'
|
||||
"""
|
||||
# Skip if this is a new instance (no pk yet)
|
||||
if not instance.pk:
|
||||
return
|
||||
|
||||
try:
|
||||
# Get the old instance to check for status change
|
||||
old_instance = PluginVersion.objects.get(pk=instance.pk)
|
||||
except PluginVersion.DoesNotExist:
|
||||
return
|
||||
|
||||
# Check if status is changing TO 'released' or 'update_ready'
|
||||
release_statuses = ['released', 'update_ready']
|
||||
old_status = old_instance.status
|
||||
new_status = instance.status
|
||||
|
||||
# Only trigger build if:
|
||||
# 1. Status is changing
|
||||
# 2. New status is a release status
|
||||
# 3. Old status was not a release status (avoid rebuilding on every save)
|
||||
if old_status == new_status:
|
||||
# Check if this version should have a ZIP built
|
||||
should_build = False
|
||||
|
||||
if not instance.pk:
|
||||
# New instance - build if status is a release status
|
||||
if instance.status in release_statuses:
|
||||
should_build = True
|
||||
logger.info(f"New plugin version {instance.plugin.slug} v{instance.version} created with status '{instance.status}' - building ZIP")
|
||||
else:
|
||||
# Existing instance - check if status changed to a release status
|
||||
try:
|
||||
old_instance = PluginVersion.objects.get(pk=instance.pk)
|
||||
old_status = old_instance.status
|
||||
new_status = instance.status
|
||||
|
||||
# Build if moving to a release status from a non-release status
|
||||
if new_status in release_statuses and old_status not in release_statuses:
|
||||
should_build = True
|
||||
logger.info(f"Building plugin ZIP for {instance.plugin.slug} v{instance.version} (status: {old_status} -> {new_status})")
|
||||
elif old_status == new_status and new_status in release_statuses:
|
||||
# No status change, but already released - no rebuild
|
||||
return
|
||||
elif old_status in release_statuses and new_status in release_statuses:
|
||||
# Moving between release statuses - no rebuild
|
||||
logger.info(f"Plugin {instance.plugin.slug} v{instance.version}: Status changing from {old_status} to {new_status}, no rebuild needed")
|
||||
return
|
||||
except PluginVersion.DoesNotExist:
|
||||
return
|
||||
|
||||
if not should_build:
|
||||
return
|
||||
|
||||
if new_status not in release_statuses:
|
||||
return
|
||||
|
||||
# If moving from one release status to another, don't rebuild
|
||||
if old_status in release_statuses:
|
||||
logger.info(f"Plugin {instance.plugin.slug} v{instance.version}: Status changing from {old_status} to {new_status}, no rebuild needed")
|
||||
return
|
||||
|
||||
logger.info(f"Building plugin ZIP for {instance.plugin.slug} v{instance.version} (status: {old_status} -> {new_status})")
|
||||
|
||||
# Build the ZIP
|
||||
file_path, checksum, file_size = create_plugin_zip(
|
||||
platform=instance.plugin.platform,
|
||||
plugin_slug=instance.plugin.slug,
|
||||
version=instance.version,
|
||||
update_version=True
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
logger.error(f"Failed to build ZIP for {instance.plugin.slug} v{instance.version}")
|
||||
return
|
||||
|
||||
# Update the instance with new file info
|
||||
instance.file_path = file_path
|
||||
instance.checksum = checksum
|
||||
instance.file_size = file_size
|
||||
|
||||
# Set released_at if moving to released status and not already set
|
||||
if new_status in release_statuses and not instance.released_at:
|
||||
instance.released_at = timezone.now()
|
||||
|
||||
logger.info(f"Built plugin ZIP: {file_path} ({file_size} bytes, checksum: {checksum[:16]}...)")
|
||||
try:
|
||||
file_path, checksum, file_size = create_plugin_zip(
|
||||
platform=instance.plugin.platform,
|
||||
plugin_slug=instance.plugin.slug,
|
||||
version=instance.version,
|
||||
update_version=True
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
logger.error(f"Failed to build ZIP for {instance.plugin.slug} v{instance.version}")
|
||||
return
|
||||
|
||||
# Update the instance with new file info
|
||||
instance.file_path = file_path
|
||||
instance.checksum = checksum
|
||||
instance.file_size = file_size
|
||||
|
||||
# Set released_at if not already set
|
||||
if not instance.released_at:
|
||||
instance.released_at = timezone.now()
|
||||
|
||||
logger.info(f"Built plugin ZIP: {file_path} ({file_size} bytes, checksum: {checksum[:16]}...)")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error building ZIP for {instance.plugin.slug} v{instance.version}: {e}")
|
||||
|
||||
Reference in New Issue
Block a user