diff --git a/docs/developer.rst b/docs/developer.rst
index a8e874c284a..6da4f22e3b9 100644
--- a/docs/developer.rst
+++ b/docs/developer.rst
@@ -211,6 +211,8 @@ A full blown example would look like this (needs to be utf-8 encoded):
https://github.com/nextcloud/news
https://example.com/1.png
https://example.com/2.jpg
+ https://paypal.com/example-link
+ https://github.com/sponsors/example
pgsql
@@ -399,6 +401,12 @@ screenshot
* must contain an HTTPS URL to an image
* can contain a **small-thumbnail** attribute which must contain an https url to an image. This image will be used as small preview (e.g. on the app list overview). Keep it small so it renders fast
* will be rendered on the app list and detail page in the given order
+donation
+ * optional
+ * can occur multiple times containing different donation URLs
+ * can contain a **title** attribute which must be a string, defaults to **Donate to support this app**
+ * can contain a **type** attribute, **paypal**, **stripe**, and **other** are allowed values, defaults to **other**
+ * will be rendered on the app detail page in the given order
dependencies/php
* optional
* can contain a **min-version** attribute (maximum 3 digits separated by dots)
diff --git a/nextcloudappstore/api/v1/release/importer.py b/nextcloudappstore/api/v1/release/importer.py
index c76a3b8bea0..d4d127a5cd4 100644
--- a/nextcloudappstore/api/v1/release/importer.py
+++ b/nextcloudappstore/api/v1/release/importer.py
@@ -15,6 +15,7 @@
Category,
Database,
DatabaseDependency,
+ Donation,
License,
PhpExtension,
PhpExtensionDependency,
@@ -144,6 +145,21 @@ def create_screenshot(img: dict[str, str]) -> Screenshot:
obj.screenshots.set(list(shots))
+class DonationsImporter(ScalarImporter):
+ def import_data(self, key: str, value: Any, obj: Any) -> None:
+ def create_donation(img: dict[str, str]) -> Donation:
+ return Donation.objects.create(
+ url=img["url"],
+ app=obj,
+ ordering=img["ordering"],
+ title=img["title"],
+ type=img["type"],
+ )
+
+ shots = map(lambda val: create_donation(val["donation"]), value)
+ obj.donations.set(list(shots))
+
+
class CategoryImporter(ScalarImporter):
def import_data(self, key: str, value: Any, obj: Any) -> None:
def map_categories(cat: dict) -> Category:
@@ -251,6 +267,7 @@ def __init__(
self,
release_importer: AppReleaseImporter,
screenshots_importer: ScreenshotsImporter,
+ donations_importer: DonationsImporter,
attribute_importer: StringAttributeImporter,
l10n_importer: L10NImporter,
category_importer: CategoryImporter,
@@ -261,6 +278,7 @@ def __init__(
{
"release": release_importer,
"screenshots": screenshots_importer,
+ "donations": donations_importer,
"user_docs": attribute_importer,
"admin_docs": attribute_importer,
"website": attribute_importer,
@@ -294,6 +312,7 @@ def _before_import(self, key: str, value: Any, obj: Any) -> tuple[Any, Any]:
if self._should_update_everything(value):
# clear all relations
obj.screenshots.all().delete()
+ obj.donations.all().delete()
obj.authors.all().delete()
obj.categories.clear()
for translation in obj.translations.all():
diff --git a/nextcloudappstore/api/v1/release/info.xsd b/nextcloudappstore/api/v1/release/info.xsd
index 7b0a79ccd97..dfa61327788 100644
--- a/nextcloudappstore/api/v1/release/info.xsd
+++ b/nextcloudappstore/api/v1/release/info.xsd
@@ -36,6 +36,8 @@
maxOccurs="1"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Donate to support this app
+
+
+
+
+
+
+
+
+
+ other
+
+
+
+
+
+
+
+
+
diff --git a/nextcloudappstore/api/v1/release/pre-info.xslt b/nextcloudappstore/api/v1/release/pre-info.xslt
index 090fc4589d9..a5b5f3e9b9a 100644
--- a/nextcloudappstore/api/v1/release/pre-info.xslt
+++ b/nextcloudappstore/api/v1/release/pre-info.xslt
@@ -32,6 +32,7 @@
+
diff --git a/nextcloudappstore/api/v1/tests/test_parser.py b/nextcloudappstore/api/v1/tests/test_parser.py
index d8c83d1a6c7..3197bf60cfa 100644
--- a/nextcloudappstore/api/v1/tests/test_parser.py
+++ b/nextcloudappstore/api/v1/tests/test_parser.py
@@ -43,6 +43,7 @@ def test_parse_minimal(self):
"issue_tracker": "https://github.com/nextcloud/news/issues",
"screenshots": [],
"categories": [{"category": {"id": "multimedia"}}],
+ "donations": [],
"release": {
"databases": [],
"licenses": [{"license": {"id": "agpl"}}],
@@ -504,6 +505,7 @@ def test_map_data(self):
{"screenshot": {"url": "https://example.com/1.png", "small_thumbnail": None, "ordering": 1}},
{"screenshot": {"url": "https://example.com/2.jpg", "small_thumbnail": None, "ordering": 2}},
],
+ "donations": [],
}
}
self.assertDictEqual(expected, result)
diff --git a/nextcloudappstore/core/admin.py b/nextcloudappstore/core/admin.py
index 165f0cbc59b..3522f64e7bf 100644
--- a/nextcloudappstore/core/admin.py
+++ b/nextcloudappstore/core/admin.py
@@ -12,6 +12,7 @@
Category,
Database,
DatabaseDependency,
+ Donation,
License,
NextcloudRelease,
PhpExtension,
@@ -136,6 +137,13 @@ class ScreenshotAdmin(admin.ModelAdmin):
list_filter = ("app__id",)
+@admin.register(Donation)
+class DonationAdmin(admin.ModelAdmin):
+ ordering = ("app", "ordering")
+ list_display = ("url", "type", "title", "app", "ordering")
+ list_filter = ("app__id",)
+
+
@admin.register(NextcloudRelease)
class NextcloudReleaseAdmin(admin.ModelAdmin):
list_display = ("version", "is_current", "has_release", "is_supported")
diff --git a/nextcloudappstore/core/migrations/0033_donation.py b/nextcloudappstore/core/migrations/0033_donation.py
new file mode 100644
index 00000000000..f69f92fb7b5
--- /dev/null
+++ b/nextcloudappstore/core/migrations/0033_donation.py
@@ -0,0 +1,30 @@
+# Generated by Django 4.2.14 on 2024-08-13 04:14
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0032_app_is_enterprise_supported'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Donation',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('url', models.URLField(max_length=256, verbose_name='Donation URL')),
+ ('type', models.CharField(default='other', max_length=256, verbose_name='Donation Type')),
+ ('title', models.CharField(default='Donate to support this app', max_length=256, verbose_name='Donation Title')),
+ ('ordering', models.IntegerField(verbose_name='Ordering')),
+ ('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='donations', to='core.app', verbose_name='App')),
+ ],
+ options={
+ 'verbose_name': 'Donation',
+ 'verbose_name_plural': 'Donations',
+ 'ordering': ['ordering'],
+ },
+ ),
+ ]
diff --git a/nextcloudappstore/core/models.py b/nextcloudappstore/core/models.py
index f376c125818..45ebe2c4834 100644
--- a/nextcloudappstore/core/models.py
+++ b/nextcloudappstore/core/models.py
@@ -557,6 +557,22 @@ def __str__(self) -> str:
return self.url
+class Donation(Model):
+ url = URLField(max_length=256, verbose_name=_("Donation URL"))
+ type = CharField(max_length=256, verbose_name=_("Donation Type"), default="other")
+ title = CharField(max_length=256, verbose_name=_("Donation Title"), default="Donate to support this app")
+ app = ForeignKey("App", on_delete=CASCADE, verbose_name=_("App"), related_name="donations")
+ ordering = IntegerField(verbose_name=_("Ordering"))
+
+ class Meta:
+ verbose_name = _("Donation")
+ verbose_name_plural = _("Donations")
+ ordering = ["ordering"]
+
+ def __str__(self) -> str:
+ return self.url
+
+
class ShellCommand(Model):
name = CharField(
max_length=256,
diff --git a/nextcloudappstore/core/static/assets/css/icons.css b/nextcloudappstore/core/static/assets/css/icons.css
index 46112e007e6..3521c591b8c 100644
--- a/nextcloudappstore/core/static/assets/css/icons.css
+++ b/nextcloudappstore/core/static/assets/css/icons.css
@@ -44,6 +44,12 @@
--original-icon-comment-question-white: url('../img/icons/detail/comment-question-white.svg');
--original-icon-feature-search-dark: url('../img/icons/detail/feature-search.svg');
--original-icon-feature-search-white: url('../img/icons/detail/feature-search-white.svg');
+ --original-icon-donate-paypal-dark: url('../img/icons/detail/donate-paypal.svg');
+ --original-icon-donate-paypal-white: url('../img/icons/detail/donate-paypal-white.svg');
+ --original-icon-donate-stripe-dark: url('../img/icons/detail/donate-stripe.svg');
+ --original-icon-donate-stripe-white: url('../img/icons/detail/donate-stripe-white.svg');
+ --original-icon-donate-other-dark: url('../img/icons/detail/donate-other.svg');
+ --original-icon-donate-other-white: url('../img/icons/detail/donate-other-white.svg');
--original-icon-send-dark: url('../img/icons/detail/send.svg');
--original-icon-send-white: url('../img/icons/detail/send-white.svg');
--original-icon-relevance-dark: url('../img/icons/list/relevance.svg');
@@ -107,6 +113,12 @@ body {
--icon-comment-question-white: var(--original-icon-comment-question-white);
--icon-feature-search-dark: var(--original-icon-feature-search-dark);
--icon-feature-search-white: var(--original-icon-feature-search-white);
+ --icon-donate-paypal-dark: var(--original-icon-donate-paypal-dark);
+ --icon-donate-paypal-white: var(--original-icon-donate-paypal-white);
+ --icon-donate-stripe-dark: var(--original-icon-donate-stripe-dark);
+ --icon-donate-stripe-white: var(--original-icon-donate-stripe-white);
+ --icon-donate-other-dark: var(--original-icon-donate-other-dark);
+ --icon-donate-other-white: var(--original-icon-donate-other-white);
--icon-send-dark: var(--original-icon-send-dark);
--icon-send-white: var(--original-icon-send-white);
--icon-relevance-dark: var(--original-icon-relevance-dark);
@@ -307,6 +319,30 @@ body .icon-feature-search-white,
body .icon-feature-search.icon-white {
background-image: var(--icon-feature-search-white);
}
+body .icon-donate-paypal,
+body .icon-donate-paypal-dark {
+ background-image: var(--icon-donate-paypal-dark);
+}
+body .icon-donate-paypal-white,
+body .icon-donate-paypal.icon-white {
+ background-image: var(--icon-donate-paypal-white);
+}
+body .icon-donate-stripe,
+body .icon-donate-stripe-dark {
+ background-image: var(--icon-donate-stripe-dark);
+}
+body .icon-donate-stripe-white,
+body .icon-donate-stripe.icon-white {
+ background-image: var(--icon-donate-stripe-white);
+}
+body .icon-donate-other,
+body .icon-donate-other-dark {
+ background-image: var(--icon-donate-other-dark);
+}
+body .icon-donate-other-white,
+body .icon-donate-other.icon-white {
+ background-image: var(--icon-donate-other-white);
+}
body .icon-send,
body .icon-send-dark {
background-image: var(--icon-send-dark);
diff --git a/nextcloudappstore/core/static/assets/css/style.css b/nextcloudappstore/core/static/assets/css/style.css
index d1e4f8c3e10..18d17e1e1aa 100644
--- a/nextcloudappstore/core/static/assets/css/style.css
+++ b/nextcloudappstore/core/static/assets/css/style.css
@@ -926,11 +926,13 @@ address {
}
.interact-section h5,
+.donate-section h5,
.support-section h5 {
margin-bottom: 20px;
}
.interact-section a, .interact-section button,
+.donate-section a, .donate-section button,
.support-section a, .support-section button {
margin-bottom: 10px;
}
diff --git a/nextcloudappstore/core/static/assets/img/icons/detail/donate-other-white.svg b/nextcloudappstore/core/static/assets/img/icons/detail/donate-other-white.svg
new file mode 100644
index 00000000000..73feff95f88
--- /dev/null
+++ b/nextcloudappstore/core/static/assets/img/icons/detail/donate-other-white.svg
@@ -0,0 +1 @@
+
diff --git a/nextcloudappstore/core/static/assets/img/icons/detail/donate-other.svg b/nextcloudappstore/core/static/assets/img/icons/detail/donate-other.svg
new file mode 100644
index 00000000000..dd562a32653
--- /dev/null
+++ b/nextcloudappstore/core/static/assets/img/icons/detail/donate-other.svg
@@ -0,0 +1 @@
+
diff --git a/nextcloudappstore/core/static/assets/img/icons/detail/donate-paypal-white.svg b/nextcloudappstore/core/static/assets/img/icons/detail/donate-paypal-white.svg
new file mode 100644
index 00000000000..f9ca79c4326
--- /dev/null
+++ b/nextcloudappstore/core/static/assets/img/icons/detail/donate-paypal-white.svg
@@ -0,0 +1 @@
+
diff --git a/nextcloudappstore/core/static/assets/img/icons/detail/donate-paypal.svg b/nextcloudappstore/core/static/assets/img/icons/detail/donate-paypal.svg
new file mode 100644
index 00000000000..481803f15db
--- /dev/null
+++ b/nextcloudappstore/core/static/assets/img/icons/detail/donate-paypal.svg
@@ -0,0 +1 @@
+
diff --git a/nextcloudappstore/core/static/assets/img/icons/detail/donate-stripe-white.svg b/nextcloudappstore/core/static/assets/img/icons/detail/donate-stripe-white.svg
new file mode 100644
index 00000000000..929dd28570c
--- /dev/null
+++ b/nextcloudappstore/core/static/assets/img/icons/detail/donate-stripe-white.svg
@@ -0,0 +1 @@
+
diff --git a/nextcloudappstore/core/static/assets/img/icons/detail/donate-stripe.svg b/nextcloudappstore/core/static/assets/img/icons/detail/donate-stripe.svg
new file mode 100644
index 00000000000..acd75988c35
--- /dev/null
+++ b/nextcloudappstore/core/static/assets/img/icons/detail/donate-stripe.svg
@@ -0,0 +1 @@
+
diff --git a/nextcloudappstore/core/templates/app/detail.html b/nextcloudappstore/core/templates/app/detail.html
index 521deece62d..a0c5cb3539c 100644
--- a/nextcloudappstore/core/templates/app/detail.html
+++ b/nextcloudappstore/core/templates/app/detail.html
@@ -135,6 +135,19 @@ {% trans "Interact" %}
{% trans 'Ask questions or discuss' %}
+ {% if object.donations.all %}
+
+ {% endif %}
{% if object.is_enterprise_supported %}
{% trans 'Need Enterprise Support?' %}
diff --git a/nextcloudappstore/core/views.py b/nextcloudappstore/core/views.py
index 7c002710b8e..fcc4f15cf57 100644
--- a/nextcloudappstore/core/views.py
+++ b/nextcloudappstore/core/views.py
@@ -24,7 +24,7 @@
AppRegisterForm,
AppReleaseUploadForm,
)
-from nextcloudappstore.core.models import App, AppRating, Category, Podcast
+from nextcloudappstore.core.models import App, AppRating, Category, Donation, Podcast
from nextcloudappstore.core.serializers import AppRatingSerializer
from nextcloudappstore.core.versioning import pad_min_version
@@ -139,10 +139,13 @@ def get_context_data(self, **kwargs):
context["user_has_rated_app"] = True
except AppRating.DoesNotExist:
pass
+
+ context["donations"] = Donation.objects.filter(app=context["app"])
context["categories"] = Category.objects.prefetch_related("translations").all()
context["latest_releases_by_platform_v"] = self.object.latest_releases_by_platform_v()
context["is_integration"] = self.object.is_integration
context["is_outdated"] = self.object.is_outdated()
+
return context