Implement initial integration apps support

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2020-03-28 23:20:59 +01:00
Родитель 695e5c8f71
Коммит 8c7c413212
18 изменённых файлов: 278 добавлений и 13 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -36,3 +36,4 @@ newrelic.ini
/media
/logs
/config
.DS_Store

Просмотреть файл

@ -3,5 +3,8 @@
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_13" default="false" project-jdk-name="Python 3.7 (venv)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (appstore)" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>
</project>

Просмотреть файл

@ -14,7 +14,7 @@
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.8 (appstore)" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Python 3.7 (appstore)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PackageRequirementsSettings">

Просмотреть файл

@ -47,9 +47,10 @@ class CategoryAdmin(TranslatableAdmin):
class AppAdmin(TranslatableAdmin):
list_display = ('id', 'owner', 'name', 'last_release', 'rating_recent',
'rating_overall', 'summary', 'is_featured',
'ownership_transfer_enabled')
'ownership_transfer_enabled', 'is_integration')
list_filter = ('owner', 'co_maintainers', 'categories', 'created',
'is_featured', 'last_release', 'ownership_transfer_enabled')
'is_featured', 'last_release', 'ownership_transfer_enabled',
'is_integration')
ordering = ('id',)

Просмотреть файл

@ -0,0 +1,38 @@
from django.db import migrations, models
import django.db.models.deletion
import parler.fields
class Migration(migrations.Migration):
dependencies = [
('core', '0021_nextcloudrelease_is_supported'),
]
operations = [
migrations.AddField(
model_name='app',
name='is_integration',
field=models.BooleanField(default=False, verbose_name='Integration (i.e. Outlook plugin)'),
),
migrations.AlterField(
model_name='appratingtranslation',
name='master',
field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='core.AppRating'),
),
migrations.AlterField(
model_name='appreleasetranslation',
name='master',
field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='core.AppRelease'),
),
migrations.AlterField(
model_name='apptranslation',
name='master',
field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='core.App'),
),
migrations.AlterField(
model_name='categorytranslation',
name='master',
field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='core.Category'),
),
]

Просмотреть файл

@ -105,6 +105,8 @@ class App(TranslatableModel):
help_text=_('If enabled, a user can try to register the same app '
'again using the public certificate and signature. If he '
'does, the app will be transferred to him.'))
is_integration = BooleanField(verbose_name=_('Integration (i.e. Outlook '
'plugin)'), default=False)
class Meta:
verbose_name = _('App')

Просмотреть файл

@ -114,6 +114,7 @@
{% endif %}
</div>
<section class="app-description markdown loading"></section>
{% if not object.is_integration %}
<div class="row app-download">
<div class="col-md-12">
<section>
@ -133,6 +134,7 @@
</section>
</div>
</div>
{% endif %}
<div class="row app-comments">
<ul class="col-md-12">
<h4 class="section-heading">{% trans 'Discussion' %}</h4>
@ -156,7 +158,7 @@
<li>
<button id="toggle-comment-button" aria-expanded="true"
data-toggle="collapse" data-target="#app-ratings"
class="btn btn-primary">{% trans 'Rate app' %}</button>
class="btn btn-primary">{% if not object.is_integration %}{% trans 'Rate app' %}{% trans 'Register integration' %}{% else %}{% trans 'Rate integration' %}{% endif %}</button>
</li>
{% endif %}
</ul>

Просмотреть файл

@ -0,0 +1,16 @@
{% extends 'base.html' %}
{% load i18n staticfiles %}
{% block head-title %}{% if integration_page == 'developer-integration' %}{% trans 'Register integration' %}{% else %}{% trans 'Edit integration' %}{% endif %} - {% endblock %}
{% block content %}
<div class="app-form">
<h1>{% if integration_page == 'developer-integration' %}{% trans 'Register integration' %}{% else %}{% trans 'Edit integration' %}{% endif %}</h1>
<hr>
<form id="integration-register-form" method="post" action="{% if integration_page == 'developer-integration' %}{% url 'integration-scaffold' %}{% else %}{{ request.get_full_path }}{% endif %}">
{% csrf_token %}
{% include 'form-fields.html' %}
<button id="submit" class="btn btn-primary btn-block">{% if integration_page == 'developer-integration' %}{% trans 'Register' %}{% else %}{% trans 'Update' %}{% endif %}</button>
</form>
</div>
{% endblock %}

Просмотреть файл

@ -7,7 +7,7 @@
<p class="text-danger">{{ error }}</p>
{% endfor %}
{% endif %}
{% for field in form %}
{% for field in form.visible_fields %}
<div class="form-group {% if field.errors %}has-error has-feedback{% endif %}">
{% if field|field_type == 'CheckboxInput' %}

Просмотреть файл

@ -60,6 +60,18 @@
{% endif %}
</ul>
</li>
{% if request.user.is_authenticated %}
<li role="presentation" class="dropdown">
<a class="dropdown-toggle nav-heading" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">
{% trans 'Integration developer' %} <span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li class="nav-link">
<a href="{% url 'integration-scaffold' %}">{% trans 'Register integration' %}</a>
</li>
</ul>
</li>
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
{% if request.user.is_superuser %}

Просмотреть файл

@ -122,6 +122,7 @@ class AppDetailView(DetailView):
'translations').all()
context['latest_releases_by_platform_v'] = \
self.object.latest_releases_by_platform_v()
context['is_integration'] = self.object.is_integration
return context
@ -194,7 +195,7 @@ class CategoryAppListView(ListView):
lang = get_language_info(get_language())['code']
category_id = self.kwargs['id']
queryset = App.objects.search(self.search_terms, lang).order_by(
*sort_columns).filter(releases__gt=0)
*sort_columns).filter(Q(releases__gt=0) | Q(is_integration=True))
if maintainer:
try:
user = User.objects.get_by_natural_key(maintainer)

Просмотреть файл

@ -1,14 +1,17 @@
import re
from os import listdir
import uuid
from django.conf import settings
from django.core.exceptions import ValidationError
from django.forms import Textarea, Form, URLField, MultipleChoiceField, \
TextInput
from django.utils.translation import ugettext_lazy as _ # type: ignore
from django.forms.fields import EmailField, CharField, ChoiceField
from django.forms.fields import EmailField, CharField, ChoiceField,\
HiddenInput
from nextcloudappstore.core.facades import resolve_file_relative_path
from nextcloudappstore.core.models import Category
from nextcloudappstore.core.models import App, Category, Screenshot
from django.utils.functional import lazy
@ -50,3 +53,82 @@ class AppScaffoldingForm(Form):
description = CharField(widget=Textarea, label=_('Description'),
help_text=_('Full description of what your app '
'does. Can contain Markdown.'))
class IntegrationScaffoldingForm(Form):
name = CharField(max_length=80, label=_('Integration name'),
widget=TextInput())
author_name = CharField(max_length=80, label=_('Author\'s full name'))
author_email = EmailField(label=_('Author\'s e-mail'))
author_homepage = URLField(label=_('Author\'s homepage'), required=False)
issue_tracker = URLField(label=_('Issue tracker URL'), required=False,
help_text=_('Bug reports and feature requests'))
categories = MultipleChoiceField(widget=HiddenInput(), disabled=True,
required=True, label=_('Categories'),
choices=lazy(get_categories, list),
help_text=_('Hold down Ctrl and click to '
'select multiple entries'))
summary = CharField(max_length=256, label=_('Summary'), help_text=_(
'Short description of your app that will be rendered as short teaser'))
screenshot = URLField(max_length=256, label=_('Screenshot URL'),
required=False,
help_text=_('URL for integration screenshot'))
screenshot_thumbnail = URLField(max_length=256, label=_('Screenshot '
'thumbnail URL'),
required=False,
help_text=_('URL for integration '
'screenshot in '
'smaller dimensions. '
'Must be used in combination '
'with a larger screenshot.'))
description = CharField(widget=Textarea, label=_('Description'),
help_text=_('Full description of what your'
' integration '
'does. Can contain Markdown.'))
def save(self, user, app_id):
if app_id is None:
app_id = self.cleaned_data['name'].lower().replace(" ", "_")
try:
app = App.objects.get(id=app_id)
if app.can_update(user):
'''Not optimal but works'''
Screenshot.objects.filter(app=app).delete()
if self.data['screenshot']:
screenshot = Screenshot.objects.create(
url=self.cleaned_data['screenshot'],
small_thumbnail=self.cleaned_data[
'screenshot_thumbnail'],
ordering=1, app=app)
screenshot.save()
app.description = self.cleaned_data['description']
app.name = self.cleaned_data['name']
app.summary = self.cleaned_data['summary']
app.website = self.cleaned_data['author_homepage']
app.issue_tracker = self.cleaned_data['issue_tracker']
app.save()
return app_id
except App.DoesNotExist:
app = App.objects.create(id=app_id, owner=user,
certificate=uuid.uuid1().urn)
app.set_current_language('en')
app.categories.set(self.cleaned_data['categories'])
app.description = self.cleaned_data['description']
app.name = self.cleaned_data['name']
app.summary = self.cleaned_data['summary']
app.website = self.cleaned_data['author_homepage']
app.issue_tracker = self.cleaned_data['issue_tracker']
app.save()
p = App.objects.get(id=app_id)
p.is_integration = True
p.save()
if self.data['screenshot']:
screenshot = Screenshot.objects.create(
url=self.cleaned_data['screenshot'],
small_thumbnail=self.cleaned_data['screenshot_thumbnail'],
ordering=1, app=p)
screenshot.save()
if settings.DISCOURSE_TOKEN:
self._create_discourse_category(app_id)
return app_id

Просмотреть файл

@ -1,9 +1,11 @@
from django.http import HttpResponse
from django.views.generic import FormView
from django.contrib.auth.mixins import LoginRequiredMixin
from nextcloudappstore.core.models import NextcloudRelease
from nextcloudappstore.core.models import App, Screenshot, NextcloudRelease
from nextcloudappstore.scaffolding.archive import build_archive
from nextcloudappstore.scaffolding.forms import AppScaffoldingForm
from nextcloudappstore.scaffolding.forms import AppScaffoldingForm, \
IntegrationScaffoldingForm
class AppScaffoldingView(FormView):
@ -29,3 +31,52 @@ class AppScaffoldingView(FormView):
buffer.close()
response.write(value)
return response
class IntegrationScaffoldingView(LoginRequiredMixin, FormView):
template_name = 'app/integration_scaffold.html'
form_class = IntegrationScaffoldingForm
success_url = '/'
app_id = None
def get_initial(self):
self.app_id = self.kwargs.get('pk', None)
init = {
'categories': ('integration',)
}
if self.app_id is not None:
app = App.objects.get(id=self.app_id)
screenshots = Screenshot.objects.filter(app=app)
if len(screenshots) == 1:
screenshot = screenshots[0]
init['screenshot'] = screenshot.url
init['screenshot_thumbnail'] = screenshot.small_thumbnail
init['description'] = app.description
init['name'] = app.name
init['summary'] = app.summary
init['author_homepage'] = app.website
init['issue_tracker'] = app.issue_tracker
user = self.request.user
init['author_name'] = '%s %s' % (user.first_name, user.last_name)
init['author_email'] = user.email
return init
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.app_id is None:
context['integration_page'] = 'developer-integration'
else:
context['integration_page'] = 'account-integration'
return context
def form_valid(self, form):
self.success_url = form.save(self.request.user, self.app_id)
return super().form_valid(form)
def get_success_url(self):
return "/apps/{}".format(self.success_url)

Просмотреть файл

@ -13,7 +13,8 @@ from nextcloudappstore.core.feeds import AppReleaseAtomFeed, AppReleaseRssFeed
from nextcloudappstore.core.views import CategoryAppListView, AppDetailView, \
app_description, AppReleasesView, AppUploadView, AppRatingApi, \
AppRegisterView
from nextcloudappstore.scaffolding.views import AppScaffoldingView
from nextcloudappstore.scaffolding.views import AppScaffoldingView, \
IntegrationScaffoldingView
admin.site.login = login_required(admin.site.login)
@ -32,6 +33,8 @@ urlpatterns = [
name='category-app-list'),
url(r'^developer/apps/generate/?$', AppScaffoldingView.as_view(),
name='app-scaffold'),
url(r'^developer/integration/new/?$', IntegrationScaffoldingView.as_view(),
name='integration-scaffold'),
url(r'^developer/apps/releases/new/?$', AppUploadView.as_view(),
name='app-upload'),
url(r'^developer/apps/new/?$', AppRegisterView.as_view(),

Просмотреть файл

@ -11,6 +11,9 @@
<li class="{% if acc_page == 'account' %}active{% endif %}">
<a href="{% url 'user:account' %}">{% trans 'Account' %}</a>
</li>
<li class="{% if acc_page == 'account-integrations' %}active{% endif %}">
<a href="{% url 'user:account-integrations' %}">{% trans 'Integrations' %}</a>
</li>
<li class="{% if acc_page == 'account-transfer-apps' %}active{% endif %}">
<a href="{% url 'user:account-transfer-apps' %}">{% trans 'Transfer app ownership' %}</a>
</li>

Просмотреть файл

@ -0,0 +1,32 @@
{% extends "user/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% block head-title %}{% trans 'Integrations' %} - {% endblock %}
{% block account-content %}
<h1>{% trans "Integrations" %}</h1>
<section>
{% if apps %}
<table class="table table-striped">
<tr>
<th>{% trans 'Integration name' %}</th>
<th>{% trans 'Actions' %}</th>
</tr>
{% for app in apps %}
<tr>
<td>{{ app.name }}</td>
<td>
<form action="{% url 'user:account-integration' pk=app.id %}" method="get">
{% csrf_token %}
<button class="btn btn-primary btn-block" type="submit">{% trans 'Edit' %}</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="alert alert-info">{% trans 'You have not submitted any integration yet!' %}</p>
{% endif %}
</section>
{% endblock %}

Просмотреть файл

@ -1,12 +1,19 @@
from django.conf.urls import url
from nextcloudappstore.user.views import PasswordView, AccountView, \
APITokenView, DeleteAccountView, ChangeLanguageView, TransferAppsView
APITokenView, DeleteAccountView, ChangeLanguageView, TransferAppsView, \
IntegrationsView
from nextcloudappstore.scaffolding.views import IntegrationScaffoldingView
app_name = 'user'
urlpatterns = [
url(r'^$', AccountView.as_view(), name='account'),
url(r'^integrations/?$', IntegrationsView.as_view(),
name='account-integrations'),
url(r'^integrations/(?P<pk>[a-z0-9_]+)/?$',
IntegrationScaffoldingView.as_view(),
name='account-integration'),
url(r'^transfer-apps/?$', TransferAppsView.as_view(),
name='account-transfer-apps'),
url(r'^transfer-apps/(?P<pk>[a-z0-9_]+)/?$', TransferAppsView.as_view(),

Просмотреть файл

@ -12,6 +12,17 @@ from nextcloudappstore.core.models import App
from nextcloudappstore.user.forms import DeleteAccountForm, AccountForm
class IntegrationsView(LoginRequiredMixin, TemplateView):
template_name = 'user/integrations.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['apps'] = App.objects.filter(owner=self.request.user).filter(
is_integration=True)
context['acc_page'] = 'account-integrations'
return context
class TransferAppsView(LoginRequiredMixin, TemplateView):
template_name = 'user/transfer-apps.html'