Skip to content

Commit

Permalink
Merge pull request #1452 from nextcloud/feature/donate-buttons
Browse files Browse the repository at this point in the history
feat: add donation attribute to `appinfo/info.xml`, donation buttons to app details page
  • Loading branch information
edward-ly authored Aug 16, 2024
2 parents 10cc03e + 7da2894 commit 533fda9
Show file tree
Hide file tree
Showing 19 changed files with 197 additions and 1 deletion.
8 changes: 8 additions & 0 deletions docs/developer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ A full blown example would look like this (needs to be utf-8 encoded):
<repository>https://github.com/nextcloud/news</repository>
<screenshot small-thumbnail="https://example.com/1-small.png">https://example.com/1.png</screenshot>
<screenshot>https://example.com/2.jpg</screenshot>
<donation type="paypal" title="Donate via PayPal">https://paypal.com/example-link</donation>
<donation>https://github.com/sponsors/example</donation>
<dependencies>
<php min-version="5.6" min-int-size="64"/>
<database min-version="9.4">pgsql</database>
Expand Down Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions nextcloudappstore/api/v1/release/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Category,
Database,
DatabaseDependency,
Donation,
License,
PhpExtension,
PhpExtensionDependency,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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():
Expand Down
19 changes: 19 additions & 0 deletions nextcloudappstore/api/v1/release/info.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
maxOccurs="1"/>
<xs:element name="screenshot" type="screenshot" minOccurs="0"
maxOccurs="10"/>
<xs:element name="donation" type="url" minOccurs="0"
maxOccurs="10"/>
<xs:element name="dependencies" type="dependencies"
minOccurs="1" maxOccurs="1"/>
<xs:element name="background-jobs" type="jobs"
Expand Down Expand Up @@ -404,6 +406,23 @@
</xs:sequence>
</xs:complexType>

<xs:simpleType name="donate-platform">
<xs:restriction base="xs:string">
<xs:enumeration value="paypal"/>
<xs:enumeration value="stripe"/>
<xs:enumeration value="other"/>
</xs:restriction>
</xs:simpleType>

<xs:complexType name="donation">
<xs:simpleContent>
<xs:extension base="secure-url">
<xs:attribute name="title" type="limited-string" use="optional"/>
<xs:attribute name="type" type="donate-platform" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>

<xs:complexType name="activity">
<xs:sequence>
<xs:element name="settings" type="activity-settings" minOccurs="0"
Expand Down
33 changes: 33 additions & 0 deletions nextcloudappstore/api/v1/release/info.xslt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,39 @@

<!-- optional elements need defaults -->

<donations type="list">
<xsl:for-each select="donation">
<donation>
<url>
<xsl:value-of select="."/>
</url>
<xsl:choose>
<xsl:when test="donation/@title">
<title>
<xsl:value-of select="donation/@title"/>
</title>
</xsl:when>
<xsl:otherwise>
<title>Donate to support this app</title>
</xsl:otherwise>
</xsl:choose>
<xsl:choose>
<xsl:when test="donation/@type">
<type>
<xsl:value-of select="donation/@type"/>
</type>
</xsl:when>
<xsl:otherwise>
<type>other</type>
</xsl:otherwise>
</xsl:choose>
<ordering type="int">
<xsl:value-of select="position()"/>
</ordering>
</donation>
</xsl:for-each>
</donations>

<xsl:if test="documentation/admin[starts-with(., 'https://')]">
<admin-docs>
<xsl:value-of select="documentation/admin"/>
Expand Down
1 change: 1 addition & 0 deletions nextcloudappstore/api/v1/release/pre-info.xslt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<xsl:copy-of select="bugs"/>
<xsl:copy-of select="repository"/>
<xsl:copy-of select="screenshot"/>
<xsl:copy-of select="donation"/>
<xsl:apply-templates select="dependencies"/>
<xsl:copy-of select="background-jobs"/>
<xsl:apply-templates select="repair-steps"/>
Expand Down
2 changes: 2 additions & 0 deletions nextcloudappstore/api/v1/tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}],
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions nextcloudappstore/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Category,
Database,
DatabaseDependency,
Donation,
License,
NextcloudRelease,
PhpExtension,
Expand Down Expand Up @@ -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")
Expand Down
30 changes: 30 additions & 0 deletions nextcloudappstore/core/migrations/0033_donation.py
Original file line number Diff line number Diff line change
@@ -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'],
},
),
]
16 changes: 16 additions & 0 deletions nextcloudappstore/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
36 changes: 36 additions & 0 deletions nextcloudappstore/core/static/assets/css/icons.css
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions nextcloudappstore/core/static/assets/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions nextcloudappstore/core/templates/app/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ <h5>{% trans "Interact" %}</h5>
{% trans 'Ask questions or discuss' %}
</a>
</section>
{% if object.donations.all %}
<section class="donate-section">
<h5>{% trans 'Donate' %}</h5>
{% for donation in object.donations.all %}
<a rel="noreferrer noopener"
href="{{ donation.url }}"
class="btn btn-default btn-light">
<span class="icon icon-donate-{{ donation.type }}"></span>
{{ donation.title }}
</a>
{% endfor %}
</section>
{% endif %}
{% if object.is_enterprise_supported %}
<section class="support-section">
<h5>{% trans 'Need Enterprise Support?' %}</h5>
Expand Down
5 changes: 4 additions & 1 deletion nextcloudappstore/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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


Expand Down

0 comments on commit 533fda9

Please sign in to comment.