Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion qgis-app/plugins/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def plugin_upload(package, **kwargs):
try:
# JSON-RPC cannot deserialize base64 strings to bytes, do it here instead
if isinstance(package, str):
package = b64decode(package.encode('utf-8'))
package = b64decode(package.encode("utf-8"))

request = kwargs.get("request")
package = BytesIO(package)
Expand Down Expand Up @@ -130,10 +130,17 @@ def plugin_upload(package, **kwargs):
version_data["changelog"] = cleaned_data.get("changelog")
if cleaned_data.get("qgisMaximumVersion"):
version_data["max_qg_version"] = cleaned_data.get("qgisMaximumVersion")
if cleaned_data.get("screenshot_file"):
version_data["screenshot"] = cleaned_data.get("screenshot_file")

new_version = PluginVersion(**version_data)
new_version.clean()
new_version.save()

# Update plugin-level screenshot when version has one
if cleaned_data.get("screenshot_file"):
plugin.screenshot = cleaned_data.get("screenshot_file")
plugin.save(update_fields=["screenshot"])
except IntegrityError as e:
# Avoids error: current transaction is aborted, commands ignored until
# end of transaction block
Expand Down
6 changes: 6 additions & 0 deletions qgis-app/plugins/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class Meta:
fields = (
"description",
"about",
"screenshot",
"author",
"email",
"icon",
Expand Down Expand Up @@ -191,6 +192,11 @@ def clean(self):
self.instance.experimental = self.cleaned_data.get("experimental")
if "supportsQt6" in self.cleaned_data:
self.instance.supports_qt6 = self.cleaned_data.get("supportsQt6")

# Handle screenshot from package only
if self.cleaned_data.get("screenshot_file"):
self.instance.screenshot = self.cleaned_data.get("screenshot_file")

return super(PluginVersionForm, self).clean()


Expand Down
24 changes: 24 additions & 0 deletions qgis-app/plugins/migrations/0019_pluginversion_screenshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.27 on 2026-02-04 05:07

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("plugins", "0018_pluginversionsecurityscan"),
]

operations = [
migrations.AddField(
model_name="pluginversion",
name="screenshot",
field=models.ImageField(
blank=True,
help_text="Optional preview screenshot for the plugin. Maximum size: 2MB. Supported formats: PNG, JPG, JPEG, GIF.",
null=True,
upload_to="packages/%Y",
verbose_name="Screenshot",
),
),
]
24 changes: 24 additions & 0 deletions qgis-app/plugins/migrations/0020_plugin_screenshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.27 on 2026-02-04 05:44

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("plugins", "0019_pluginversion_screenshot"),
]

operations = [
migrations.AddField(
model_name="plugin",
name="screenshot",
field=models.ImageField(
blank=True,
help_text="Default preview screenshot for the plugin. This will be used if no version-specific screenshot is available. Maximum size: 2MB. Supported formats: PNG, JPG, JPEG, GIF.",
null=True,
upload_to="packages/%Y",
verbose_name="Screenshot",
),
),
]
39 changes: 38 additions & 1 deletion qgis-app/plugins/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,16 @@ class Plugin(models.Model):
_("Icon"), blank=True, null=True, upload_to=PLUGINS_STORAGE_PATH
)

screenshot = models.ImageField(
_("Screenshot"),
blank=True,
null=True,
upload_to=PLUGINS_STORAGE_PATH,
help_text=_(
"Default preview screenshot for the plugin. This will be used if no version-specific screenshot is available. Maximum size: 2MB. Supported formats: PNG, JPG, JPEG, GIF."
),
)

# downloads (soft trigger from versions)
downloads = models.IntegerField(_("Downloads"), default=0, editable=False)

Expand Down Expand Up @@ -918,6 +928,16 @@ class PluginVersion(models.Model):

# the file!
package = models.FileField(_("Plugin package"), upload_to=PLUGINS_STORAGE_PATH)
# Preview screenshot
screenshot = models.ImageField(
_("Screenshot"),
blank=True,
null=True,
upload_to=PLUGINS_STORAGE_PATH,
help_text=_(
"Optional preview screenshot for the plugin. Maximum size: 2MB. Supported formats: PNG, JPG, JPEG, GIF."
),
)
# Flags: checks on unique current/experimental are done in save() and possibly in the views
experimental = models.BooleanField(
_("Experimental flag"),
Expand Down Expand Up @@ -1142,14 +1162,30 @@ def delete_version_package(sender, instance, **kw):
pass


def delete_version_screenshot(sender, instance, **kw):
"""
Removes the version screenshot
"""
try:
if instance.screenshot:
instance.screenshot.delete(False)
except:
pass


def delete_plugin_icon(sender, instance, **kw):
"""
Removes the plugin icon
Removes the plugin icon and screenshot
"""
try:
instance.icon.delete(False)
except:
pass
try:
if instance.screenshot:
instance.screenshot.delete(False)
except:
pass


def delete_feedback_attachment(sender, instance, **kw):
Expand Down Expand Up @@ -1241,6 +1277,7 @@ def pass_rate(self):


models.signals.post_delete.connect(delete_version_package, sender=PluginVersion)
models.signals.post_delete.connect(delete_version_screenshot, sender=PluginVersion)
models.signals.post_delete.connect(delete_plugin_icon, sender=Plugin)
models.signals.post_delete.connect(
delete_feedback_attachment, sender=PluginVersionFeedbackAttachment
Expand Down
52 changes: 29 additions & 23 deletions qgis-app/plugins/templates/plugins/form_snippet.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
{% for error in field.errors %}
<p class="help is-danger has-text-weight-medium">{{ error }}</p>
{% endfor %}
{% endif %}
{% endif %}
</div>
{% elif field.field.widget|klass == 'ClearableFileInput' %}
<div class="file has-name">
Expand All @@ -37,35 +37,35 @@
</span>
<span class="file-label"> {{ field.label }} </span>
</span>
<span class="file-name" id="filename"> Choose a file… </span>
<span class="file-name" id="filename-{{ field.html_name }}"> Choose a file… </span>
</label>
</div>
{% if field.errors %}
{% for error in field.errors %}
<p class="help is-danger has-text-weight-medium">{{ error }}</p>
{% endfor %}
{% endif %}
{% endif %}
{% elif field.field.widget|klass == 'Textarea' %}
<div class="field">
<label class="label">{{ field.label_tag }}</label>
<div class="control">
<textarea
class="textarea"
placeholder="{{ field.help_text | safe }}"
<textarea
class="textarea"
placeholder="{{ field.help_text | safe }}"
id="{{ field.html_name }}"
name="{{ field.html_name }}"
>{{ field.value|default_if_none:'' }}</textarea>
</div>
{% if field.errors %}
<p class="help is-danger">{{ field.errors }}</p>
{% endif %}
{% endif %}
</div>
{% elif field.field.widget|klass == 'TextInput' or field.field.widget|klass == 'URLInput' %}
<div class="field">
<label class="label">{{ field.label_tag }}</label>
<div class="control">
<input class="input" type="text"
placeholder="{{ field.help_text | safe }}"
<input class="input" type="text"
placeholder="{{ field.help_text | safe }}"
id="{{ field.html_name }}"
name="{{ field.html_name }}"
value="{{ field.value|default_if_none:'' }}"
Expand All @@ -75,13 +75,13 @@
{% for error in field.errors %}
<p class="help is-danger has-text-weight-medium">{{ error }}</p>
{% endfor %}
{% endif %}
{% endif %}
</div>
{% elif field.field.widget|klass == 'EmailInput' %}
<div class="field">
<label class="label">{{ field.label_tag }}</label>
<div class="control has-icons-left">
<input class="input" type="email"
<input class="input" type="email"
id="{{ field.html_name }}"
name="{{ field.html_name }}"
value="{{ field.value|default_if_none:'' }}"
Expand All @@ -94,7 +94,7 @@
{% for error in field.errors %}
<p class="help is-danger has-text-weight-medium">{{ error }}</p>
{% endfor %}
{% endif %}
{% endif %}
</div>
{% elif field.field.widget|klass == 'Select' %}
<div class="field">
Expand All @@ -114,9 +114,9 @@
{% for error in field.errors %}
<p class="help is-danger has-text-weight-medium">{{ error }}</p>
{% endfor %}
{% endif %}
{% endif %}
</div>
{% else %}
{% else %}
<div class="field">
<label class="label">{{ field.label_tag }}</label>
<div class="control">
Expand All @@ -126,7 +126,7 @@
{% for error in field.errors %}
<p class="help is-danger has-text-weight-medium">{{ error }}</p>
{% endfor %}
{% endif %}
{% endif %}
</div>
{% endif %}
<div class="help has-text-grey">{{ field.help_text | safe }}</div>
Expand All @@ -136,15 +136,21 @@

<script>
document.addEventListener("DOMContentLoaded", function () {
const fileInput = document.querySelector(".file-input");
const fileName = document.querySelector("#filename");
const fileInputs = document.querySelectorAll(".file-input");

fileInputs.forEach(function (fileInput) {
fileInput.addEventListener("change", function () {
const fieldName = fileInput.getAttribute("name");
const fileName = document.querySelector("#filename-" + fieldName);

fileInput.addEventListener("change", function () {
if (fileInput.files.length > 0) {
fileName.textContent = fileInput.files[0].name;
} else {
fileName.textContent = "Choose a file…";
}
if (fileName) {
if (fileInput.files.length > 0) {
fileName.textContent = fileInput.files[0].name;
} else {
fileName.textContent = "Choose a file…";
}
}
});
});
});
</script>
9 changes: 9 additions & 0 deletions qgis-app/plugins/templates/plugins/plugin_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,15 @@ <h2>
{% if object.about %}
<div class="tab-pane is-active" id="plugin-about">
<p>{{ object.about|linebreaksbr }}</p>

{% comment %}Display plugin screenshot if available{% endcomment %}
{% if object.screenshot %}
<div class="mt-4">
<figure class="image">
<img src="{{ object.screenshot.url }}" alt="{% trans "Plugin screenshot" %}" style="max-width: 100%; height: auto;">
</figure>
</div>
{% endif %}
</div>
{% endif %}
<div class="tab-pane{% if not object.about %} is-active{% endif %}" id="plugin-details">
Expand Down
10 changes: 10 additions & 0 deletions qgis-app/plugins/templates/plugins/version_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,16 @@ <h2 class="title is-4">{% trans "Version" %}: {{ version }}</h2>
</div>
</div>
</div>

{% comment %}Display screenshot if available{% endcomment %}
{% if version.screenshot %}
<div class="mt-4">
<label class="label has-text-weight-bold">{% trans "Preview Screenshot" %}</label>
<figure class="image">
<img src="{{ version.screenshot.url }}" alt="{% trans "Plugin screenshot" %}" style="max-width: 100%; height: auto;">
</figure>
</div>
{% endif %}
</div>

{% if user.is_staff or user in version.plugin.editors %}
Expand Down
Loading