зеркало из https://github.com/nextcloud/appstore.git
Implement initial integration apps support
Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Родитель
695e5c8f71
Коммит
8c7c413212
|
@ -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'
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче