diff --git a/dockerize/docker/REQUIREMENTS.txt b/dockerize/docker/REQUIREMENTS.txt index 01dedf14..c7634b79 100644 --- a/dockerize/docker/REQUIREMENTS.txt +++ b/dockerize/docker/REQUIREMENTS.txt @@ -41,4 +41,6 @@ djangorestframework==3.11.2 sorl-thumbnail-serializer-field==0.2.1 django-rest-auth==0.9.5 drf-yasg==1.17.1 -django-rest-multiple-models==2.1.3 \ No newline at end of file +django-rest-multiple-models==2.1.3 + +factory_boy==3.2.0 \ No newline at end of file diff --git a/qgis-app/plugins/management/commands/validate_existing_plugins.py b/qgis-app/plugins/management/commands/validate_existing_plugins.py new file mode 100644 index 00000000..fa516389 --- /dev/null +++ b/qgis-app/plugins/management/commands/validate_existing_plugins.py @@ -0,0 +1,125 @@ +"""A command to validate the existing zipfile Plugin Packages. + +We are using the same validator that used in uploading plugins. +Re-run this command when modify the validator to validate the existing plugins. +""" + +import os +from django.conf import settings +from django.core.files.uploadedfile import InMemoryUploadedFile +from django.core.mail import send_mail +from django.core.management.base import BaseCommand +from django.contrib.sites.models import Site +from django.utils.translation import ugettext_lazy as _ + +from plugins.models import PluginVersion, PluginInvalid +from plugins.validator import validator + + +DOMAIN = Site.objects.get_current().domain + + +def send_email_notification(plugin, version, message, url_version, recipients): + + message = ('\r\nPlease update ' + 'Plugin: %s ' + '- Version: %s\r\n' + '\r\nIt failed to pass validation with message:' + '\r\n%s\r\n' + '\r\nLink: %s') % (plugin, version, message, url_version) + send_mail( + subject='Invalid Plugin Metadata Notification', + message=_(message), + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=recipients, + fail_silently=True + ) + + +def get_recipients_email(plugin): + receipt_email = [] + if plugin.created_by.email: + receipt_email.append(plugin.created_by.email) + if plugin.email: + receipt_email.append(plugin.email) + return receipt_email + + +def validate_zipfile_version(version): + + if not os.path.exists(version.package.url): + return { + 'plugin': f'{version.plugin.name}', + 'created_by': f'{version.plugin.created_by}', + 'version': f'{version.version}', + 'version_id': version.id, + 'msg': [f'File does not exist. Please re-upload.'], + 'url': f'http://{DOMAIN}{version.get_absolute_url()}', + 'recipients_email': get_recipients_email(version.plugin) + } + + with open(version.package.url, 'rb') as buf: + package = InMemoryUploadedFile( + buf, + 'tempfile', + 'filename.zip', + 'application/zip', + 1000000, # ignore the filesize and assume it's 1MB + 'utf8') + try: + validator(package) + except Exception as e: + return { + 'plugin': f'{version.plugin.name}', + 'created_by': f'{version.plugin.created_by}', + 'version': f'{version.version}', + 'version_id': version.id, + 'msg': e.messages, + 'url': f'http://{DOMAIN}{version.get_absolute_url()}', + 'recipients_email': get_recipients_email(version.plugin) + } + return None + + +class Command(BaseCommand): + + help = ('Validate existing Plugins zipfile and send a notification email ' + 'for invalid Plugin') + + def handle(self, *args, **options): + self.stdout.write('Validating existing plugins...') + # get the latest version + versions = PluginVersion.approved_objects.\ + order_by('plugin_id', '-created_on').distinct('plugin_id').all()[:50] + num_count = 0 + for version in versions: + error_msg = validate_zipfile_version(version) + if error_msg: + send_email_notification( + plugin=error_msg['plugin'], + version=error_msg['version'], + message='\r\n'.join(error_msg['msg']), + url_version=error_msg['url'], + recipients=error_msg['recipients_email'] + ) + self.stdout.write( + _('Sent email to %s for Plugin %s - Version %s.') % ( + error_msg['recipients_email'], + error_msg['plugin'], + error_msg['version'] + ) + ) + num_count += 1 + plugin_version = PluginVersion.objects\ + .select_related('plugin').get(id=error_msg['version_id']) + PluginInvalid.objects.create( + plugin=plugin_version.plugin, + validated_version=plugin_version.version, + message=(error_msg['msg'] + if not isinstance(error_msg['msg'], list) + else ', '.join(error_msg['msg'])) + ) + self.stdout.write( + _('Successfully sent email notification for %s invalid plugins') + % (num_count) + ) diff --git a/qgis-app/plugins/migrations/0002_plugininvalid.py b/qgis-app/plugins/migrations/0002_plugininvalid.py new file mode 100644 index 00000000..eca9956e --- /dev/null +++ b/qgis-app/plugins/migrations/0002_plugininvalid.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.18 on 2021-06-25 22:31 + +from django.db import migrations, models +import django.db.models.deletion +import plugins.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plugins', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='PluginInvalid', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('validated_version', plugins.models.VersionField(db_index=True, max_length=32, verbose_name='Version')), + ('validated_at', models.DateTimeField(auto_now_add=True, verbose_name='Validated at')), + ('message', models.CharField(editable=False, help_text='Invalid error message', max_length=256, verbose_name='Message')), + ('plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plugins.Plugin', unique=True)), + ], + ), + ] diff --git a/qgis-app/plugins/models.py b/qgis-app/plugins/models.py index f0e13322..6ebd26f9 100644 --- a/qgis-app/plugins/models.py +++ b/qgis-app/plugins/models.py @@ -628,6 +628,31 @@ def delete_plugin_icon(sender, instance, **kw): pass +class PluginInvalid(models.Model): + """Invalid plugins model. + + There were existing plugins on the server before the validation + mechanism updated. + We run the management command to validate them, and track the invalid one + of the latest version. + """ + + plugin = models.ForeignKey(Plugin, on_delete=models.CASCADE, unique=True) + # We track the version number, not the version instance. + # So that when the version has been deleted, we keep the plugin + # in the tracking list + validated_version = VersionField( + _('Version'), max_length=32, db_index=True) + validated_at = models.DateTimeField( + _('Validated at'), auto_now_add=True, editable=False) + message = models.CharField( + _('Message'), + help_text=_('Invalid error message'), + max_length=256, + editable=False + ) + + models.signals.post_delete.connect( delete_version_package, sender=PluginVersion) models.signals.post_delete.connect(delete_plugin_icon, sender=Plugin) diff --git a/qgis-app/plugins/templates/plugins/plugin_base.html b/qgis-app/plugins/templates/plugins/plugin_base.html index 4bb7002b..45f9c727 100644 --- a/qgis-app/plugins/templates/plugins/plugin_base.html +++ b/qgis-app/plugins/templates/plugins/plugin_base.html @@ -31,6 +31,7 @@