Project

General

Profile

0001-add-section-to-create-deploy-applications-60699.patch

Frédéric Péters, 15 February 2022 10:13 AM

Download (70.8 KB)

View differences:

Subject: [PATCH 1/3] add section to create/deploy applications (#60699)

 README                                        |  38 +++
 hobo/applications/__init__.py                 |   0
 hobo/applications/forms.py                    |  22 ++
 hobo/applications/migrations/0001_initial.py  |  95 ++++++
 hobo/applications/migrations/__init__.py      |   0
 hobo/applications/models.py                   | 102 ++++++
 .../hobo/applications/add-element.html        |  45 +++
 .../templates/hobo/applications/create.html   |  14 +
 .../applications/element_confirm_delete.html  |  20 ++
 .../templates/hobo/applications/home.html     |  42 +++
 .../templates/hobo/applications/install.html  |  17 +
 .../templates/hobo/applications/manifest.html |  62 ++++
 hobo/applications/urls.py                     |  40 +++
 hobo/applications/utils.py                    |  66 ++++
 hobo/applications/views.py                    | 297 ++++++++++++++++++
 hobo/environment/utils.py                     |   4 +-
 hobo/settings.py                              |   1 +
 hobo/static/css/applications.svg              |   1 +
 hobo/static/css/build-application.svg         |   1 +
 hobo/static/css/style.scss                    |  46 +++
 hobo/urls.py                                  |   2 +
 tests/test_application.py                     | 222 +++++++++++++
 22 files changed, 1136 insertions(+), 1 deletion(-)
 create mode 100644 hobo/applications/__init__.py
 create mode 100644 hobo/applications/forms.py
 create mode 100644 hobo/applications/migrations/0001_initial.py
 create mode 100644 hobo/applications/migrations/__init__.py
 create mode 100644 hobo/applications/models.py
 create mode 100644 hobo/applications/templates/hobo/applications/add-element.html
 create mode 100644 hobo/applications/templates/hobo/applications/create.html
 create mode 100644 hobo/applications/templates/hobo/applications/element_confirm_delete.html
 create mode 100644 hobo/applications/templates/hobo/applications/home.html
 create mode 100644 hobo/applications/templates/hobo/applications/install.html
 create mode 100644 hobo/applications/templates/hobo/applications/manifest.html
 create mode 100644 hobo/applications/urls.py
 create mode 100644 hobo/applications/utils.py
 create mode 100644 hobo/applications/views.py
 create mode 100644 hobo/static/css/applications.svg
 create mode 100644 hobo/static/css/build-application.svg
 create mode 100644 tests/test_application.py
README
186 186

  
187 187
There is .pre-commit-config.yaml to use pre-commit to automatically run black and isort
188 188
before commits. (execute `pre-commit install` to install the git hook.)
189

  
190

  
191
License
192
-------
193

  
194
This program is free software: you can redistribute it and/or modify it under
195
the terms of the GNU Affero General Public License as published by the Free
196
Software Foundation, either version 3 of the License, or (at your option) any
197
later version.
198

  
199
This program is distributed in the hope that it will be useful, but WITHOUT ANY
200
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
201
PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
202
details.
203

  
204
You should have received a copy of the GNU Affero General Public License along
205
with this program.  If not, see <http://www.gnu.org/licenses/>.
206

  
207

  
208
Combo embeds some other pieces of code or art, with their own authors and
209
copyright notices:
210

  
211
Application images (hobo/static/css/*.svg) from the unDraw project:
212
 # https://undraw.co/
213
 #
214
 # All images, assets and vectors published on unDraw can be used for free. You
215
 # can use them for noncommercial and commercial purposes. You do not need to ask
216
 # permission from or provide credit to the creator or unDraw.
217
 #
218
 # More precisely, unDraw grants you an nonexclusive, worldwide copyright
219
 # license to download, copy, modify, distribute, perform, and use the assets
220
 # provided from unDraw for free, including for commercial purposes, without
221
 # permission from or attributing the creator or unDraw. This license does not
222
 # include the right to compile assets, vectors or images from unDraw to
223
 # replicate a similar or competing service, in any form or distribute the assets
224
 # in packs. This extends to automated and non-automated ways to link, embed,
225
 # scrape, search or download the assets included on the website without our
226
 # consent.
hobo/applications/forms.py
1
# hobo - portal to configure and deploy applications
2
# Copyright (C) 2015-2022 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django import forms
18
from django.utils.translation import ugettext_lazy as _
19

  
20

  
21
class InstallForm(forms.Form):
22
    bundle = forms.FileField(label=_('Application'))
hobo/applications/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.29 on 2022-01-09 13:16
3
from __future__ import unicode_literals
4

  
5
import django.contrib.postgres.fields.jsonb
6
import django.db.models.deletion
7
import django.utils.timezone
8
from django.db import migrations, models
9

  
10

  
11
class Migration(migrations.Migration):
12

  
13
    initial = True
14

  
15
    dependencies = []
16

  
17
    operations = [
18
        migrations.CreateModel(
19
            name='Application',
20
            fields=[
21
                (
22
                    'id',
23
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
24
                ),
25
                ('name', models.CharField(max_length=100, verbose_name='Name')),
26
                ('slug', models.SlugField(max_length=100)),
27
                ('description', models.TextField(blank=True, verbose_name='Description')),
28
                ('editable', models.BooleanField(default=True)),
29
                ('creation_timestamp', models.DateTimeField(default=django.utils.timezone.now)),
30
                ('last_update_timestamp', models.DateTimeField(auto_now=True)),
31
            ],
32
        ),
33
        migrations.CreateModel(
34
            name='Element',
35
            fields=[
36
                (
37
                    'id',
38
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
39
                ),
40
                ('type', models.CharField(max_length=25, verbose_name='Type')),
41
                ('slug', models.SlugField(max_length=500, verbose_name='Slug')),
42
                ('name', models.CharField(max_length=500, verbose_name='Name')),
43
                ('cache', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)),
44
            ],
45
        ),
46
        migrations.CreateModel(
47
            name='Relation',
48
            fields=[
49
                (
50
                    'id',
51
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
52
                ),
53
                ('auto_dependency', models.BooleanField(default=False)),
54
                (
55
                    'application',
56
                    models.ForeignKey(
57
                        on_delete=django.db.models.deletion.CASCADE, to='applications.Application'
58
                    ),
59
                ),
60
                (
61
                    'element',
62
                    models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='applications.Element'),
63
                ),
64
            ],
65
        ),
66
        migrations.CreateModel(
67
            name='Version',
68
            fields=[
69
                (
70
                    'id',
71
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
72
                ),
73
                ('bundle', models.FileField(blank=True, null=True, upload_to='applications')),
74
                ('creation_timestamp', models.DateTimeField(default=django.utils.timezone.now)),
75
                ('last_update_timestamp', models.DateTimeField(auto_now=True)),
76
                (
77
                    'deployment_status',
78
                    django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
79
                ),
80
                (
81
                    'application',
82
                    models.ForeignKey(
83
                        on_delete=django.db.models.deletion.CASCADE, to='applications.Application'
84
                    ),
85
                ),
86
            ],
87
        ),
88
        migrations.AddField(
89
            model_name='application',
90
            name='elements',
91
            field=models.ManyToManyField(
92
                blank=True, through='applications.Relation', to='applications.Element'
93
            ),
94
        ),
95
    ]
hobo/applications/models.py
1
# hobo - portal to configure and deploy applications
2
# Copyright (C) 2015-2022 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import urllib.parse
18

  
19
from django.conf import settings
20
from django.contrib.postgres.fields import JSONField
21
from django.db import models
22
from django.utils.text import slugify
23
from django.utils.timezone import now
24
from django.utils.translation import ugettext_lazy as _
25

  
26
from hobo.environment.utils import get_installed_services
27
from hobo.signature import sign_url
28

  
29
from .utils import Requests
30

  
31
requests = Requests()
32

  
33

  
34
class Application(models.Model):
35
    name = models.CharField(max_length=100, verbose_name=_('Name'))
36
    slug = models.SlugField(max_length=100)
37
    description = models.TextField(verbose_name=_('Description'), blank=True)
38
    editable = models.BooleanField(default=True)
39
    elements = models.ManyToManyField('Element', blank=True, through='Relation')
40
    creation_timestamp = models.DateTimeField(default=now)
41
    last_update_timestamp = models.DateTimeField(auto_now=True)
42

  
43
    def __repr__(self):
44
        return '<Application %s>' % self.slug
45

  
46
    def save(self, *args, **kwargs):
47
        if not self.slug:
48
            base_slug = slugify(self.name)[:95]
49
            slug = base_slug
50
            i = 1
51
            while Application.objects.filter(slug=slug).exists():
52
                slug = '%s-%s' % (base_slug, i)
53
                i += 1
54
            self.slug = slug
55
        super().save(*args, **kwargs)
56

  
57

  
58
class Element(models.Model):
59
    type = models.CharField(max_length=25, verbose_name=_('Type'))
60
    slug = models.SlugField(max_length=500, verbose_name=_('Slug'))
61
    name = models.CharField(max_length=500, verbose_name=_('Name'))
62
    cache = JSONField(blank=True, default=dict)
63

  
64
    def __repr__(self):
65
        return '<Element %s/%s>' % (self.type, self.slug)
66

  
67

  
68
class Relation(models.Model):
69
    application = models.ForeignKey(Application, on_delete=models.CASCADE)
70
    element = models.ForeignKey(Element, on_delete=models.CASCADE)
71
    auto_dependency = models.BooleanField(default=False)
72

  
73
    def __repr__(self):
74
        return '<Relation %s - %s/%s>' % (self.application.slug, self.element.type, self.element.slug)
75

  
76

  
77
class Version(models.Model):
78
    application = models.ForeignKey(Application, on_delete=models.CASCADE)
79
    bundle = models.FileField(upload_to='applications', blank=True, null=True)
80
    creation_timestamp = models.DateTimeField(default=now)
81
    last_update_timestamp = models.DateTimeField(auto_now=True)
82
    deployment_status = JSONField(blank=True, default=dict)
83

  
84
    def __repr__(self):
85
        return '<Version %s>' % self.application.slug
86

  
87
    def deploy(self):
88
        bundle_content = self.bundle.read()
89
        for service_id, services in getattr(settings, 'KNOWN_SERVICES', {}).items():
90
            if service_id != 'wcs':
91
                continue
92
            service_objects = {x.get_base_url_path(): x for x in get_installed_services(types=[service_id])}
93
            for service in services.values():
94
                if service_objects[service['url']].secondary:
95
                    continue
96
                url = urllib.parse.urljoin(service['url'], 'api/export-import/bundle-import/')
97
                response = requests.put(sign_url(url, service['secret']), data=bundle_content)
98
                if not response.ok:
99
                    # TODO: report failures
100
                    continue
101
                # TODO: look at response content for afterjob URLs to display a progress bar
102
                pass
hobo/applications/templates/hobo/applications/add-element.html
1
{% extends "hobo/applications/home.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
<h2>{{ type.text }}</h2>
6
{% endblock %}
7

  
8
{% block content %}
9
<form method="post">
10
  {% csrf_token %}
11
  <div class="application-elements">
12
    {% for element in elements %}
13
    <label data-slugged-text="{{ element.text|slugify }}"><input type="checkbox" name="elements" value="{{ element.id }}">{{ element.text }}</label>
14
    {% endfor %}
15
  </div>
16
  <div style="text-align: right">
17
  <label>{% trans "Filter:" %} <input type="search" id="element-filter"></label>
18
  </div>
19
<div class="buttons">
20
<button class="submit-button">{% trans "Add" %}</button>
21
<a class="cancel" href="{% url 'application-manifest' app_slug=app.slug %}">{% trans "Cancel" %}</a>
22
</div>
23
<script>
24
$('#element-filter').on('change blur keyup', function() {
25
  const val = $(this).val().toLowerCase();
26
  // force dimensions so a reduced number of elements do not affect size
27
  $('.application-elements').css('height', $('.application-elements').height());
28
  $('.application-elements').css('width', $('.application-elements').width());
29
  if (!val) {
30
    $('.application-elements label').show();
31
  } else {
32
    $('.application-elements label').each(function(idx, elem) {
33
      var slugged_text = $(elem).attr('data-slugged-text');
34
      if (slugged_text.indexOf(val) > -1) {
35
        $(elem).show();
36
      } else {
37
        $(elem).hide();
38
      }
39
    });
40
  }
41
});
42
</script>
43

  
44
</form>
45
{% endblock %}
hobo/applications/templates/hobo/applications/create.html
1
{% extends "hobo/base.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}<h2>{% trans "Create Application" %}</h2>{% endblock %}
5

  
6
{% block content %}
7
<form method="post">
8
{% csrf_token %}
9
{{ form.as_p }}
10
<div class="buttons">
11
<button class="submit-button">{% trans "Create" %}</button>
12
<a class="cancel" href=".">{% trans "Cancel" %}</a>
13
</div>
14
{% endblock %}
hobo/applications/templates/hobo/applications/element_confirm_delete.html
1
{% extends "hobo/base.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
<h2>{% blocktrans with title=object.element.name %}Removal of "{{ title }}"{% endblocktrans %}</h2>
6
{% endblock %}
7

  
8
{% block content %}
9
<form method="post">
10
  {% csrf_token %}
11
  <p>
12
  {% trans 'Are you sure you want to remove this element ?' %}
13
  </p>
14
  <div class="buttons">
15
    <button class="delete-button">{% trans 'Delete' %}</button>
16
    <a class="cancel" href="{% url 'application-manifest' app_slug=view.kwargs.app_slug %}">{% trans 'Cancel' %}</a>
17
  </div>
18
</form>
19
{% endblock %}
20

  
hobo/applications/templates/hobo/applications/home.html
1
{% extends "hobo/base.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{% url 'applications-home' %}">{% trans 'Applications' %}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
  <h2>{% trans 'Applications' %}</h2>
11
  <span class="actions">
12
    <a rel="popup" href="{% url 'application-install' %}">{% trans 'Install' %}</a>
13
    <a rel="popup" href="{% url 'application-init' %}">{% trans 'Create' %}</a>
14
  </span>
15
{% endblock %}
16

  
17
{% block content %}
18

  
19
{% if object_list %}
20
<div id="applications">
21
{% for application in object_list %}
22
 <div class="application application--{{ application.slug }}">
23
   <h3>{{ application.name }}</h3>
24
   <p>{{ application.description|default:"" }}</p>
25
   {% if application.editable %}
26
   <div class="buttons">
27
    <a class="button" href="{% url 'application-manifest' app_slug=application.slug %}"
28
                      >{% trans "Edit" %}</a>
29
   </div>
30
   {% endif %}
31
 </div>
32
{% endfor %}
33
</div>
34
{% else %}
35
  <div id="no-applications">
36
    <div class="infonotice">
37
    <p>{% trans "You should find, install or build some applications." %}</p>
38
    </div>
39
  </div>
40
{% endif %}
41

  
42
{% endblock %}
hobo/applications/templates/hobo/applications/install.html
1
{% extends "hobo/base.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
<h2>{% trans "Install" %}</h2>
6
{% endblock %}
7

  
8
{% block content %}
9
<form method="post" enctype="multipart/form-data">
10
  {% csrf_token %}
11
  {{ form.as_p }}
12
  <div class="buttons">
13
    <button class="submit-button">{% trans 'Install' %}</button>
14
    <a class="cancel" href="{% url 'applications-home' %}">{% trans 'Cancel' %}</a>
15
  </div>
16
</form>
17
{% endblock %}
hobo/applications/templates/hobo/applications/manifest.html
1
{% extends "hobo/applications/home.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{% url 'application-manifest' app_slug=app.slug %}">{{ app.name }}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>{{ app.name }}</h2>
11
  <span class="actions">
12
    <a rel="popup" href="{% url 'application-metadata' app_slug=app.slug %}">{% trans 'Metadata' %}</a>
13
  </span>
14
{% endblock %}
15

  
16
{% block content %}
17

  
18
  <ul class="objects-list single-links">
19
    {% for relation in relations %}
20
    {% if not relation.auto_dependency %}
21
    <li><a>{{ relation.element.name }} <span class="extra-info">- {{ relation.element.type_label }}</span></a>
22
            <a rel="popup" class="delete" href="{% url 'application-delete-element' app_slug=app.slug pk=relation.id %}">{% trans "remove" %}</a></li>
23
    {% endif %}
24
    {% endfor %}
25
    {% for relation in relations %}
26
    {% if relation.auto_dependency %}
27
    <li class="auto-dependency"><a>{{ relation.element.name }} <span class="extra-info">- {{ relation.element.type_label }}</span></a></li>
28
    {% endif %}
29
    {% endfor %}
30
  </ul>
31

  
32
  {% if relations %}
33
  <div class="buttons">
34
    <a class="pk-button" href="{% url 'application-scandeps' app_slug=app.slug %}">{% trans "Scan dependencies" %}</a>
35
    &nbsp; &nbsp;
36
    <a class="pk-button" href="{% url 'application-generate' app_slug=app.slug %}">{% trans "Generate application bundle" %}</a>
37
    {% if versions %}
38
    &nbsp; &nbsp;
39
    <a class="pk-button" download href="{% url 'application-download' app_slug=app.slug %}">{% trans "Download" %}</a>
40
    {% endif %}
41
  </div>
42
  {% else %}
43
  <div id="application-empty">
44
    <div class="infonotice">
45
    <p>{% trans "You should now assemble the different parts of your application." %}</p>
46
    </div>
47
  </div>
48
  {% endif %}
49

  
50
{% endblock %}
51

  
52
{% block sidebar %}
53
<aside id="sidebar">
54
<h3>{% trans "Add" %}</h3>
55
{% for service, types in types_by_service.items %}
56
<h4>{{ service }}</h4>
57
{% for type in types %}
58
<a class="button button-paragraph" rel="popup" href="{% url 'application-add-element' app_slug=app.slug type=type.id %}">{{ type.text }}</a>
59
{% endfor %}
60
{% endfor %}
61
</aside>
62
{% endblock %}
hobo/applications/urls.py
1
# hobo - portal to configure and deploy applications
2
# Copyright (C) 2015-2022 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.conf.urls import url
18

  
19
from . import views
20

  
21
urlpatterns = [
22
    url(r'^$', views.home, name='applications-home'),
23
    url(r'^create/$', views.init, name='application-init'),
24
    url(r'^install/$', views.install, name='application-install'),
25
    url(r'^manifest/(?P<app_slug>[\w-]+)/$', views.manifest, name='application-manifest'),
26
    url(r'^manifest/(?P<app_slug>[\w-]+)/metadata/$', views.metadata, name='application-metadata'),
27
    url(r'^manifest/(?P<app_slug>[\w-]+)/scandeps/$', views.scandeps, name='application-scandeps'),
28
    url(r'^manifest/(?P<app_slug>[\w-]+)/generate/$', views.generate, name='application-generate'),
29
    url(r'^manifest/(?P<app_slug>[\w-]+)/download/$', views.download, name='application-download'),
30
    url(
31
        r'^manifest/(?P<app_slug>[\w-]+)/add/(?P<type>[\w-]+)/$',
32
        views.add_element,
33
        name='application-add-element',
34
    ),
35
    url(
36
        r'^manifest/(?P<app_slug>[\w-]+)/delete/(?P<pk>\d+)/$',
37
        views.delete_element,
38
        name='application-delete-element',
39
    ),
40
]
hobo/applications/utils.py
1
# hobo - portal to configure and deploy applications
2
# Copyright (C) 2015-2022 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import urllib
18

  
19
import requests
20
from django.conf import settings
21
from django.utils.http import urlencode
22
from requests import Response
23
from requests import Session as RequestsSession
24
from requests.auth import AuthBase
25

  
26
from hobo.signature import sign_url
27

  
28

  
29
class PublikSignature(AuthBase):
30
    def __init__(self, secret):
31
        self.secret = secret
32

  
33
    def __call__(self, request):
34
        request.url = sign_url(request.url, self.secret)
35
        return request
36

  
37

  
38
def get_known_service_for_url(url):
39
    netloc = urllib.parse.urlparse(url).netloc
40
    for services in settings.KNOWN_SERVICES.values():
41
        for service in services.values():
42
            remote_url = service.get('url')
43
            if urllib.parse.urlparse(remote_url).netloc == netloc:
44
                return service
45
    return None
46

  
47

  
48
class Requests(RequestsSession):
49
    def request(self, method, url, **kwargs):
50
        remote_service = get_known_service_for_url(url)
51
        kwargs['auth'] = PublikSignature(remote_service.get('secret'))
52

  
53
        # only keeps the path (URI) in url parameter, scheme and netloc are
54
        # in remote_service
55
        scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(url)
56
        url = urllib.parse.urlunparse(('', '', path, params, query, fragment))
57

  
58
        query_params = {'orig': remote_service.get('orig')}
59

  
60
        remote_service_base_url = remote_service.get('url')
61
        scheme, netloc, dummy, params, old_query, fragment = urllib.parse.urlparse(remote_service_base_url)
62

  
63
        query = urlencode(query_params)
64
        url = urllib.parse.urlunparse((scheme, netloc, path, params, query, fragment))
65

  
66
        return super().request(method, url, **kwargs)
hobo/applications/views.py
1
# hobo - portal to configure and deploy applications
2
# Copyright (C) 2015-2022 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import io
18
import json
19
import tarfile
20
import urllib.parse
21

  
22
from django.conf import settings
23
from django.core.files.base import ContentFile
24
from django.http import HttpResponse, HttpResponseRedirect
25
from django.urls import reverse, reverse_lazy
26
from django.views.generic import FormView, ListView, TemplateView
27
from django.views.generic.edit import CreateView, DeleteView, UpdateView
28

  
29
from hobo.environment.utils import get_installed_services
30

  
31
from .forms import InstallForm
32
from .models import Application, Element, Relation, Version
33
from .utils import Requests
34

  
35
requests = Requests()
36

  
37

  
38
class HomeView(ListView):
39
    template_name = 'hobo/applications/home.html'
40
    model = Application
41

  
42
    def get_queryset(self):
43
        return super().get_queryset().order_by('name')
44

  
45

  
46
home = HomeView.as_view()
47

  
48

  
49
class InitView(CreateView):
50
    template_name = 'hobo/applications/create.html'
51
    model = Application
52
    fields = ['name']
53

  
54
    def get_success_url(self):
55
        return reverse('application-manifest', kwargs={'app_slug': self.object.slug})
56

  
57

  
58
init = InitView.as_view()
59

  
60

  
61
def get_object_types():
62
    object_types = []
63
    for service_id, services in getattr(settings, 'KNOWN_SERVICES', {}).items():
64
        if service_id not in ('combo', 'wcs'):
65
            continue
66
        service_objects = {x.get_base_url_path(): x for x in get_installed_services(types=[service_id])}
67
        for service in services.values():
68
            if service_objects[service['url']].secondary:
69
                continue
70
            url = urllib.parse.urljoin(service['url'], 'api/export-import/')
71
            response = requests.get(url)
72
            if not response.ok:
73
                continue
74
            for object_type in response.json()['data']:
75
                object_type['service'] = service
76
                object_types.append(object_type)
77
    return object_types
78

  
79

  
80
class ManifestView(TemplateView):
81
    template_name = 'hobo/applications/manifest.html'
82

  
83
    def get_context_data(self, **kwargs):
84
        context = super().get_context_data(**kwargs)
85

  
86
        context['app'] = Application.objects.get(slug=self.kwargs['app_slug'])
87

  
88
        context['relations'] = context['app'].relation_set.all().select_related('element')
89
        context['versions'] = context['app'].version_set.exists()
90
        context['types_by_service'] = {}
91

  
92
        type_labels = {}
93
        for object_type in get_object_types():
94
            type_labels[object_type['id']] = object_type['singular']
95
            if object_type.get('minor'):
96
                continue
97
            service = object_type['service']['title']
98
            if service not in context['types_by_service']:
99
                context['types_by_service'][service] = []
100
            context['types_by_service'][service].append(object_type)
101

  
102
        for relation in context['relations']:
103
            relation.element.type_label = type_labels.get(relation.element.type)
104

  
105
        return context
106

  
107

  
108
manifest = ManifestView.as_view()
109

  
110

  
111
class MetadataView(UpdateView):
112
    template_name = 'hobo/applications/edit-metadata.html'
113
    model = Application
114
    slug_url_kwarg = 'app_slug'
115
    fields = ['name', 'slug', 'description']
116

  
117
    def get_success_url(self):
118
        return reverse('application-manifest', kwargs={'app_slug': self.object.slug})
119

  
120

  
121
metadata = MetadataView.as_view()
122

  
123

  
124
class AppAddElementView(TemplateView):
125
    template_name = 'hobo/applications/add-element.html'
126

  
127
    def get_context_data(self, **kwargs):
128
        context = super().get_context_data(**kwargs)
129
        context['app'] = Application.objects.get(slug=self.kwargs['app_slug'])
130
        for object_type in get_object_types():
131
            if object_type.get('id') == self.kwargs['type']:
132
                context['type'] = object_type
133
                url = object_type['urls']['list']
134
                response = requests.get(url)
135
                context['elements'] = response.json()['data']
136
                break
137
        return context
138

  
139
    def post(self, request, app_slug, type):
140
        context = self.get_context_data()
141
        app = context['app']
142
        element_infos = {x['id']: x for x in context['elements']}
143
        for element_slug in request.POST.getlist('elements'):
144
            element, created = Element.objects.get_or_create(
145
                type=type, slug=element_slug, defaults={'name': element_infos[element_slug]['text']}
146
            )
147
            element.name = element_infos[element_slug]['text']
148
            element.cache = element_infos[element_slug]
149
            element.save()
150
            relation, created = Relation.objects.get_or_create(application=app, element=element)
151
            relation.auto_dependency = False
152
            relation.save()
153
        return HttpResponseRedirect(reverse('application-manifest', kwargs={'app_slug': app_slug}))
154

  
155

  
156
add_element = AppAddElementView.as_view()
157

  
158

  
159
class AppDeleteElementView(DeleteView):
160
    model = Relation
161
    template_name = 'hobo/applications/element_confirm_delete.html'
162

  
163
    def get_success_url(self):
164
        return reverse('application-manifest', kwargs={'app_slug': self.kwargs['app_slug']})
165

  
166

  
167
delete_element = AppDeleteElementView.as_view()
168

  
169

  
170
def scandeps(request, app_slug):
171
    app = Application.objects.get(slug=app_slug)
172
    app.relation_set.filter(auto_dependency=True).delete()
173
    relations = app.relation_set.select_related('element')
174
    elements = {(x.element.type, x.element.slug): x.element for x in relations}
175
    finished = False
176
    while not finished:
177
        finished = True
178
        for (type, slug), element in list(elements.items()):
179
            dependencies_url = element.cache['urls'].get('dependencies')
180
            element.done = True
181
            if not dependencies_url:
182
                continue
183
            response = requests.get(dependencies_url)
184
            for dependency in response.json()['data']:
185
                if (dependency['type'], dependency['id']) in elements:
186
                    continue
187
                finished = False
188
                element, created = Element.objects.get_or_create(
189
                    type=dependency['type'], slug=dependency['id'], defaults={'name': dependency['text']}
190
                )
191
                element.name = dependency['text']
192
                element.cache = dependency
193
                element.save()
194
                relation, created = Relation.objects.get_or_create(application=app, element=element)
195
                if created:
196
                    relation.auto_dependency = True
197
                    relation.save()
198
                elements[(element.type, element.slug)] = element
199

  
200
    return HttpResponseRedirect(reverse('application-manifest', kwargs={'app_slug': app_slug}))
201

  
202

  
203
def generate(request, app_slug):
204
    app = Application.objects.get(slug=app_slug)
205

  
206
    version = Version(application=app)
207
    version.save()
208

  
209
    tar_io = io.BytesIO()
210
    with tarfile.open(mode='w', fileobj=tar_io) as tar:
211
        manifest_json = {
212
            'application': app.name,
213
            'slug': app.slug,
214
            'description': app.description,
215
            'elements': [],
216
        }
217

  
218
        for relation in app.relation_set.all().select_related('element'):
219
            element = relation.element
220
            manifest_json['elements'].append(
221
                {
222
                    'type': element.type,
223
                    'slug': element.slug,
224
                    'name': element.name,
225
                    'auto-dependency': relation.auto_dependency,
226
                }
227
            )
228

  
229
            response = requests.get(element.cache['urls']['export'])
230
            tarinfo = tarfile.TarInfo('%s/%s' % (element.type, element.slug))
231
            tarinfo.mtime = version.last_update_timestamp.timestamp()
232
            tarinfo.size = int(response.headers['content-length'])
233
            tar.addfile(tarinfo, fileobj=io.BytesIO(response.content))
234

  
235
        manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode())
236
        tarinfo = tarfile.TarInfo('manifest.json')
237
        tarinfo.size = len(manifest_fd.getvalue())
238
        tarinfo.mtime = version.last_update_timestamp.timestamp()
239
        tar.addfile(tarinfo, fileobj=manifest_fd)
240

  
241
    version.bundle.save('%s.tar' % app_slug, content=ContentFile(tar_io.getvalue()))
242
    version.save()
243

  
244
    return HttpResponseRedirect(reverse('application-manifest', kwargs={'app_slug': app_slug}))
245

  
246

  
247
def download(request, app_slug):
248
    app = Application.objects.get(slug=app_slug)
249
    version = app.version_set.order_by('-last_update_timestamp')[0]
250
    response = HttpResponse(version.bundle, content_type='application/x-tar')
251
    response['Content-Disposition'] = 'attachment; filename="%s"' % '%s.tar' % app_slug
252
    return response
253

  
254

  
255
class Install(FormView):
256
    form_class = InstallForm
257
    template_name = 'hobo/applications/install.html'
258
    success_url = reverse_lazy('applications-home')
259

  
260
    def form_valid(self, form):
261
        tar_io = io.BytesIO(self.request.FILES['bundle'].read())
262
        tar = tarfile.open(fileobj=tar_io)
263
        manifest = json.loads(tar.extractfile('manifest.json').read().decode())
264
        app, created = Application.objects.get_or_create(
265
            slug=manifest.get('slug'), defaults={'name': manifest.get('application')}
266
        )
267
        app.name = manifest.get('application')
268
        app.description = manifest.get('description')
269
        app.editable = False
270
        if not created:
271
            app.relation_set.all().delete()
272
        app.save()
273

  
274
        for element_dict in manifest.get('elements'):
275
            element, created = Element.objects.get_or_create(
276
                type=element_dict['type'], slug=element_dict['slug'], defaults={'name': element_dict['name']}
277
            )
278
            element.name = element_dict['name']
279
            element.save()
280

  
281
            relation = Relation(
282
                application=app,
283
                element=element,
284
                auto_dependency=element_dict['auto-dependency'],
285
            )
286
            relation.save()
287

  
288
        version = Version(application=app)
289
        version.bundle.save('%s.tar' % app.slug, content=ContentFile(tar_io.getvalue()))
290
        version.save()
291

  
292
        version.deploy()
293

  
294
        return super().form_valid(form)
295

  
296

  
297
install = Install.as_view()
hobo/environment/utils.py
27 27
from hobo.profile.utils import get_profile_dict
28 28

  
29 29

  
30
def get_installed_services():
30
def get_installed_services(types=None):
31 31
    from .models import AVAILABLE_SERVICES
32 32

  
33 33
    installed_services = []
34 34
    for available_service in AVAILABLE_SERVICES:
35
        if types and available_service.Extra.service_id not in types:
36
            continue
35 37
        installed_services.extend(available_service.objects.all())
36 38
    return installed_services
37 39

  
hobo/settings.py
42 42
    'rest_framework',
43 43
    'mellon',
44 44
    'gadjo',
45
    'hobo.applications',
45 46
    'hobo.debug',
46 47
    'hobo.environment',
47 48
    'hobo.franceconnect',
hobo/static/css/applications.svg
1
<svg id="ee10700a-6e76-4a82-b03e-a9751bc157fe" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="803.05238" height="591.37509" viewBox="0 0 803.05238 591.37509"><path d="M424.91688,674.27641c2.92224-23.4981,17.48511-46.65063,39.89523-54.29754a109.09788,109.09788,0,0,0,.0054,74.90374c3.44358,9.31476,8.24353,19.31641,5.0044,28.70429-2.01535,5.84137-6.94565,10.31055-12.4567,13.105-5.51141,2.79443-11.6279,4.12634-17.66788,5.43158l-1.18869.98315C428.9203,721.45561,421.99464,697.77458,424.91688,674.27641Z" transform="translate(-198.47381 -154.31245)" fill="#f0f0f0"/><path d="M465.03449,620.4232a93.25009,93.25009,0,0,0-23.18185,52.4812,40.15631,40.15631,0,0,0,.45679,12.57361,23.03162,23.03162,0,0,0,5.72827,10.68256c2.58185,2.83667,5.5513,5.43933,7.39865,8.85425a14.10232,14.10232,0,0,1,.689,11.51263c-1.631,4.678-4.84564,8.491-8.11886,12.11054-3.63428,4.01879-7.47284,8.13549-9.0177,13.46337-.1872.64557-1.17789.31739-.991-.32721,2.68777-9.26959,11.68607-14.53485,15.97736-22.88385,2.00238-3.89581,2.84286-8.41864.96566-12.531-1.64154-3.59613-4.70135-6.28259-7.34039-9.1322a24.57672,24.57672,0,0,1-5.9902-10.23658,37.12347,37.12347,0,0,1-.93851-12.50964,90.469,90.469,0,0,1,6.60736-27.49286A94.90381,94.90381,0,0,1,464.344,619.64061c.44623-.4997,1.13385.28632.69055.78271Z" transform="translate(-198.47381 -154.31245)" fill="#fff"/><path d="M442.13355,666.58873A13.99051,13.99051,0,0,1,431.484,651.93315a.52227.52227,0,0,1,1.0426.05206,12.95492,12.95492,0,0,0,9.93414,13.61249C443.11408,665.753,442.78315,666.74315,442.13355,666.58873Z" transform="translate(-198.47381 -154.31245)" fill="#fff"/><path d="M446.53916,694.89464a26.96549,26.96549,0,0,0,12.04175-15.53015c.18945-.64484,1.18024-.3169.991.32721a28.04826,28.04826,0,0,1-12.56339,16.13507C446.42982,696.17,445.9636,695.23607,446.53916,694.89464Z" transform="translate(-198.47381 -154.31245)" fill="#fff"/><path d="M452.33448,637.95433a7.91916,7.91916,0,0,0,7.50482-.381c.57407-.35046,1.03971.584.46936.93213a8.87421,8.87421,0,0,1-8.30139.43988.53934.53934,0,0,1-.33191-.65912A.52444.52444,0,0,1,452.33448,637.95433Z" transform="translate(-198.47381 -154.31245)" fill="#fff"/><path d="M533.12128,661.94939c-.35223.229-.70446.458-1.05712.69586a104.2919,104.2919,0,0,0-13.335,10.411c-.326.29071-.652.59015-.969.88965a109.94776,109.94776,0,0,0-23.87842,32.68658,106.77244,106.77244,0,0,0-5.84863,15.13214c-2.15814,7.16095-3.92835,15.09687-8.20032,20.95423a18.31835,18.31835,0,0,1-1.427,1.76159H439.80021c-.08774-.044-.1759-.07928-.26407-.12335l-1.54141.0705c.06192-.273.132-.55493.194-.82794.03528-.15857.07913-.31708.11441-.47565.02624-.10571.05289-.21143.07052-.30829.00861-.03522.01764-.07043.02625-.09686.01764-.09687.04431-.185.06192-.27307q.58122-2.365,1.19778-4.72992c0-.00885,0-.00885.00861-.01764a156.9946,156.9946,0,0,1,13.212-34.686c.17633-.32586.35223-.66058.5462-.98651a101.88533,101.88533,0,0,1,9.15164-13.88147,90.0712,90.0712,0,0,1,5.99832-6.86145,74.90082,74.90082,0,0,1,18.74329-14.04c13.84637-7.31067,29.87671-10.11157,44.67444-5.64593C532.37254,661.71165,532.74284,661.8261,533.12128,661.94939Z" transform="translate(-198.47381 -154.31245)" fill="#f0f0f0"/><path d="M533.03452,662.43938A93.24986,93.24986,0,0,0,482.9278,690.3856a40.1569,40.1569,0,0,0-7.20544,10.31434,23.03161,23.03161,0,0,0-1.858,11.97827c.35357,3.81939,1.15756,7.68524.57657,11.52411a14.10252,14.10252,0,0,1-6.38129,9.607c-4.11878,2.75317-8.98114,3.86218-13.77384,4.7815-5.32138,1.02069-10.86477,1.99658-15.306,5.32049-.53812.40271-1.13157-.45581-.59427-.85791,7.727-5.783,18.08166-4.56946,26.53466-8.652,3.94434-1.905,7.33847-5.01019,8.31559-9.424.85443-3.85962.02877-7.84686-.36267-11.711a24.57678,24.57678,0,0,1,1.38028-11.77984,37.12469,37.12469,0,0,1,6.78231-10.55329,90.47,90.47,0,0,1,21.82822-17.97345,94.90373,94.90373,0,0,1,30.09042-11.56115c.65714-.13031.73294.91125.08011,1.04071Z" transform="translate(-198.47381 -154.31245)" fill="#fff"/><path d="M486.95457,685.512a13.99047,13.99047,0,0,1,.32061-18.1134c.44547-.50245,1.24717.16619.80109.66931a12.95487,12.95487,0,0,0-.26379,16.84985C488.24061,685.43517,487.38026,686.02647,486.95457,685.512Z" transform="translate(-198.47381 -154.31245)" fill="#fff"/><path d="M473.43006,710.76518a26.96573,26.96573,0,0,0,18.96488-5.15c.53952-.40082,1.13311.45758.59426.85791a28.04838,28.04838,0,0,1-19.74557,5.319C472.5749,711.7177,472.765,710.69127,473.43006,710.76518Z" transform="translate(-198.47381 -154.31245)" fill="#fff"/><path d="M512.3393,668.7907a7.91928,7.91928,0,0,0,6.22156,4.21423c.66934.06579.47855,1.09222-.18643,1.02685a8.87419,8.87419,0,0,1-6.893-4.64679.53933.53933,0,0,1,.13181-.72607.52444.52444,0,0,1,.7261.13184Z" transform="translate(-198.47381 -154.31245)" fill="#fff"/><path d="M458.76688,336.75688H331.0413a14.90431,14.90431,0,0,1-14.88721-14.88769V252.30374A14.90346,14.90346,0,0,1,331.0413,237.417H458.76688a14.90347,14.90347,0,0,1,14.88721,14.88672v69.56543A14.90431,14.90431,0,0,1,458.76688,336.75688Z" transform="translate(-198.47381 -154.31245)" fill="#e6e6e6"/><path d="M653.76688,336.75688H526.0413a14.90431,14.90431,0,0,1-14.88721-14.88769V252.30374A14.90346,14.90346,0,0,1,526.0413,237.417H653.76688a14.90345,14.90345,0,0,1,14.88721,14.88672v69.56543A14.90433,14.90433,0,0,1,653.76688,336.75688Z" transform="translate(-198.47381 -154.31245)" fill="#386ede"/><path d="M848.76688,336.75688H721.0413a14.90431,14.90431,0,0,1-14.88721-14.88769V252.30374A14.90346,14.90346,0,0,1,721.0413,237.417H848.76688a14.90345,14.90345,0,0,1,14.88721,14.88672v69.56543A14.90433,14.90433,0,0,1,848.76688,336.75688Z" transform="translate(-198.47381 -154.31245)" fill="#e6e6e6"/><path d="M458.76688,491.75688H331.0413a14.90431,14.90431,0,0,1-14.88721-14.88769V407.30376A14.90349,14.90349,0,0,1,331.0413,392.417H458.76688a14.90348,14.90348,0,0,1,14.88721,14.88672v69.56543A14.90431,14.90431,0,0,1,458.76688,491.75688Z" transform="translate(-198.47381 -154.31245)" fill="#f2f2f2"/><path d="M653.76688,491.75688H526.0413a14.90431,14.90431,0,0,1-14.88721-14.88769V407.30376A14.90349,14.90349,0,0,1,526.0413,392.417H653.76688a14.90346,14.90346,0,0,1,14.88721,14.88672v69.56543A14.90433,14.90433,0,0,1,653.76688,491.75688Z" transform="translate(-198.47381 -154.31245)" fill="#e6e6e6"/><g id="e3e4f7c8-e232-44d3-85d4-7e764218aef0" data-name="aa405d94-515b-444f-88dd-2c52df39214d"><circle id="fcc64bf3-a6e1-406f-af2a-c5c9529d985b" data-name="ede3a9f0-5e02-4455-ac7e-751a5f8c3692" cx="464.31155" cy="327.43306" r="22.81223" fill="#386ede"/><path id="a515bf60-7ad9-4961-925b-281b4504c434" data-name="ba039ca8-d148-45ed-9781-bacb7304e881" d="M672.25828,479.82985h-7.75611v-7.75613a1.82507,1.82507,0,0,0-1.82507-1.82511h0a1.82508,1.82508,0,0,0-1.82507,1.82511v7.75613h-7.75611a1.82507,1.82507,0,0,0-1.82507,1.8251h0a1.82508,1.82508,0,0,0,1.82507,1.82511h7.75617v7.75613a1.82507,1.82507,0,0,0,1.82507,1.8251h0a1.82508,1.82508,0,0,0,1.82508-1.82507V483.48h7.756a1.82508,1.82508,0,0,0,1.82507-1.82511h0a1.82507,1.82507,0,0,0-1.82507-1.8251h0Z" transform="translate(-198.47381 -154.31245)" fill="#fff"/></g><g id="e103fff6-fe82-4519-942e-bf1bf1b0aca3" data-name="bbf41f8a-587e-4352-8304-6d86282586e3"><circle id="add300b1-303e-47da-8a54-54886e1d1e76" data-name="e4c91425-bc08-43ed-9b2d-9a6452dd6ab0" cx="662.31155" cy="176.43306" r="22.81224" fill="#386ede"/><path id="ac71a829-1528-4702-ba68-9cb7b8b12af7" data-name="b08e8960-896e-4763-a2dc-6c3da3e5d175" d="M870.25828,328.82985h-7.75611v-7.75613a1.82507,1.82507,0,0,0-1.82507-1.82511h0a1.82508,1.82508,0,0,0-1.82507,1.82511v7.75613h-7.75611a1.82507,1.82507,0,0,0-1.82507,1.8251h0a1.82508,1.82508,0,0,0,1.82507,1.82511h7.75617v7.75613a1.82507,1.82507,0,0,0,1.82507,1.8251h0a1.82508,1.82508,0,0,0,1.82508-1.82507V332.48h7.756a1.82508,1.82508,0,0,0,1.82507-1.82511h0a1.82507,1.82507,0,0,0-1.82507-1.8251h0Z" transform="translate(-198.47381 -154.31245)" fill="#fff"/></g><g id="eec1f109-4866-4d4c-80db-d160015753dd" data-name="b77ad267-181d-433f-b31f-af61e58bf4a6"><circle id="ad2802dd-75a3-400f-a980-dd7e255328e3" data-name="e8aef769-5476-47e9-8295-ae809767f9aa" cx="269.31155" cy="174.43306" r="22.81223" fill="#386ede"/><path id="a152d324-d2c4-475a-92fe-001d0469b5e0" data-name="efbebd4e-dbd2-4103-8d2c-f0ca8346a3bf" d="M477.25828,326.82985h-7.75614v-7.75613a1.82509,1.82509,0,0,0-1.82507-1.82511h0a1.8251,1.8251,0,0,0-1.8251,1.82511h0v7.75613h-7.75614a1.8251,1.8251,0,0,0-1.8251,1.8251h0a1.8251,1.8251,0,0,0,1.82507,1.82511h7.75614v7.75613a1.82509,1.82509,0,0,0,1.8251,1.8251h0a1.82508,1.82508,0,0,0,1.8251-1.82507V330.48h7.75614a1.8251,1.8251,0,0,0,1.8251-1.82511h0a1.8251,1.8251,0,0,0-1.8251-1.8251h0Z" transform="translate(-198.47381 -154.31245)" fill="#fff"/></g><path d="M638.57566,297.23225a9.06978,9.06978,0,0,1-7.3208,11.82447l-10.54828,72.00079-17.303-8.451,17.40372-71.8703a9.11882,9.11882,0,0,1,17.76831-3.504Z" transform="translate(-198.47381 -154.31245)" fill="#9e616a"/><path d="M512.5893,527.83214a9.06961,9.06961,0,0,0-3.492-13.46173l7.11621-76.58634-14.90473,7.63986-4.654,73.80091a9.11882,9.11882,0,0,0,15.93442,8.6073Z" transform="translate(-198.47381 -154.31245)" fill="#9e616a"/><polygon points="324.298 334.784 388.259 332.324 391.949 265.903 318.148 265.903 324.298 334.784" fill="#9e616a"/><polygon points="312.427 569.355 323.357 569.354 328.556 527.197 312.425 527.197 312.427 569.355" fill="#9e616a"/><path d="M508.18961,718.27562l17.27774-1.03125v7.40253l16.42643,11.34466a4.6239,4.6239,0,0,1-2.62748,8.429H518.69651l-3.5455-7.32221-1.38434,7.32221h-7.75561Z" transform="translate(-198.47381 -154.31245)" fill="#2f2e41"/><polygon points="361.041 569.355 371.971 569.354 377.17 527.197 361.039 527.197 361.041 569.355" fill="#9e616a"/><path d="M521.88435,473.5041s-19.13755,15.45038-8.76307,70.62845c-1.5798.1883.20908,3.30231.20908,3.30231l1.497,44.95953,1.15683,5.1529-3.84607,8.31463,1.58551,9.034-4.888,81.9917,18.05719,2.33508L544.173,614.07915l3.2909-9.57257,3.3009-8.39075-1.78055-8.94769,1.46033-13.88788,9.96188-46.829,2.43869,81.21808-3.403,7.75378,1.89376,8.5116-4.219,4.51819-1.66748,75.98993,19.25439.90063,15.9311-76.60315-1.23529-9.77545,2.1195-7.22791V594.85387l3.63025-7.96808,7.914-43.44751s2.38742-33.95831-15.03183-73.91895Z" transform="translate(-198.47381 -154.31245)" fill="#2f2e41"/><path d="M529.68385,365.01326s-24.17157,4.78046-28.33655,25.39468-6.84455,67.26288-6.84455,67.26288l.91214,6.49622,23.38169,5.42178,14.92358-65.36273Z" transform="translate(-198.47381 -154.31245)" fill="#3f3d56"/><path d="M584.84711,371.97145s2.45014-3.43347,16.17112-3.963c.56491-.02179-.13456-1.14252-.13456-1.14252l6.59879-32.25272,21.9906,12.17527-3.7121,33.813L589.6254,421.70213Z" transform="translate(-198.47381 -154.31245)" fill="#3f3d56"/><path d="M571.13981,468.12233c-10.3526.00079-25.936-1.14172-49.39554-4.62451a7.165,7.165,0,0,1-6.01373-5.99893,5.42418,5.42418,0,0,1-.91135-6.16007l.02179-.04278-.53155-.6265a5.80077,5.80077,0,0,1-.38681-7.01147l-7.2652-51.97477a21.89569,21.89569,0,0,1,15.22772-24.02145l2.682-.83508,10.14248-13.26648.2.00232,32.5607.4335,10.56662,13.41824,17.08081,4.02756.04438.26,3.96609,22.914c2.06241,11.91773-2.66794,16.606-8.70578,27.0863l1.77213,29.43436.804,1.46082a3.95918,3.95918,0,0,1-.94793,4.96774,7.71178,7.71178,0,0,1,.10507,4.7724l-.41794,1.38065c.00079.0397-.11054.60083-1.01254,1.2857C589.09928,466.23891,584.4304,468.12233,571.13981,468.12233Z" transform="translate(-198.47381 -154.31245)" fill="#3f3d56"/><path d="M556.80378,718.27562l17.27774-1.03125v7.40253l16.42642,11.34466a4.6239,4.6239,0,0,1-2.62747,8.429H567.31068l-3.54551-7.32221-1.38433,7.32221h-7.75565Z" transform="translate(-198.47381 -154.31245)" fill="#2f2e41"/><circle cx="549.75371" cy="320.56323" r="25.52654" transform="matrix(0.16018, -0.98709, 0.98709, 0.16018, -53.20484, 657.55723)" fill="#9e616a"/><path d="M564.67988,295.61836c-9.35593-6.43451-17.59488-7.41464-24.36267-5.58966a8.89677,8.89677,0,0,0-3.55887-3.16012c-5.27838-13.30008-21.34168-21.28745-35.127-17.40467-13.87448,3.9079-23.44757,19.27674-20.83691,33.45273,1.61789,8.787,7.14792,16.39591,9.37222,25.04932,4.30573,16.75143-6.19781,35.94018-22.62927,41.34,15.55756,3.12384,32.68268-6.37912,38.264-21.23346,2.71084-7.215,2.87433-15.09237,3.219-22.79211a75.56441,75.56441,0,0,1,1.9426-15.783,33.72225,33.72225,0,0,1,2.51172-6.72034A22.99686,22.99686,0,0,1,524.4806,292.0992a8.778,8.778,0,0,0,1.28183,7.90252c-3.6485,4.57053-5.63455,9.51328-5.63455,12.47333-7.57935,13.8526,3.25924,25.75144,17.11218,33.3306a28.38088,28.38088,0,0,0,18.13275,3.16122l.57956-2.474.78522,2.22855,2.62121,7.47122q4.5358,2.19425,8.993,4.81055c-1.12893-5.30145-2.04214-10.59311-2.67059-15.767-.87006-7.04193,2.16293-17.52557,4.3508-23.90094a37.632,37.632,0,0,0,2.06033-12.2128v-.18173l2.13034,6.8428,7.75577-3.3085C580.53559,304.1989,581.97852,294.80348,564.67988,295.61836Z" transform="translate(-198.47381 -154.31245)" fill="#2f2e41"/><path d="M883.647,745.68755H316.353a1.19069,1.19069,0,0,1,0-2.38135H883.647a1.19069,1.19069,0,0,1,0,2.38135Z" transform="translate(-198.47381 -154.31245)" fill="#ccc"/><circle id="aeef577c-289f-4c95-850d-f6939249b5af" data-name="e4c91425-bc08-43ed-9b2d-9a6452dd6ab0" cx="266.70383" cy="330.43084" r="22.81224" fill="#e6e6e6"/><path d="M808.41264,471.63265H728.39982a9.33671,9.33671,0,0,1-9.326-9.3263V418.72756a9.33618,9.33618,0,0,1,9.326-9.3257h80.01282a9.33618,9.33618,0,0,1,9.326,9.32568v43.57879A9.33671,9.33671,0,0,1,808.41264,471.63265Z" transform="translate(-198.47381 -154.31245)" fill="#f2f2f2"/><g id="a3c861bb-19cc-462b-bc3a-1f29faec5e60" data-name="bc9af32e-f7cd-4c85-a4cc-5543d455b8de"><circle id="a24c8b8a-c30a-477e-8412-dbddb638fe0a" data-name="b56288f5-6382-4006-aaf1-aded98dde780" cx="611.11771" cy="287.50408" r="12.11453" fill="#386ede"/><path id="ac585bde-7a48-4e1f-a3d4-d671a5b024fe" data-name="b9aaef6b-2cfc-46ac-8dc0-30fdc28925de" d="M814.62215,440.79921h-4.119v-4.11893a.96921.96921,0,0,0-.96923-.96923h0a.96924.96924,0,0,0-.96923.96921v4.119H804.4458a.96923.96923,0,0,0-.96923.96921v0h0a.96921.96921,0,0,0,.96923.96923h4.11894v4.11892a.96921.96921,0,0,0,.96923.96923h0a.96923.96923,0,0,0,.96923-.96921v-4.11894h4.119a.96924.96924,0,0,0,.96923-.96921v0h0a.96921.96921,0,0,0-.96923-.96923Z" transform="translate(-198.47381 -154.31245)" fill="#fff"/></g></svg>
hobo/static/css/build-application.svg
1
<svg id="bb8d8cc3-2630-43b3-871f-91b3ed5692fc" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="904.88583" height="742" viewBox="0 0 904.88583 742"><polygon points="686.055 724.756 670.765 724.755 663.491 665.781 686.057 665.782 686.055 724.756" fill="#ffb6b6"/><path d="M837.51088,818.57692l-49.30025-.00183v-.62357a19.19007,19.19007,0,0,1,19.189-19.18872h.00122l30.11091.00122Z" transform="translate(-147.55709 -79)" fill="#2f2e41"/><polygon points="801.699 711.547 787.146 716.236 762.135 662.335 783.614 655.414 801.699 711.547" fill="#ffb6b6"/><path d="M957.51324,803.4578l-46.92469,15.11912-.19125-.59352a19.19005,19.19005,0,0,1,12.37881-24.14934l.00116-.00038,28.66-9.23414Z" transform="translate(-147.55709 -79)" fill="#2f2e41"/><circle cx="809.34809" cy="504.31001" r="19.09176" fill="#386ede"/><path d="M981.90481,618.14334c-.34382.18518-.684.35878-1.02442.532l-40.31-74.84561c.332-.18889.66413-.37741,1.008-.56259a42.52255,42.52255,0,0,1,40.32646,74.8762Z" transform="translate(-147.55709 -79)" fill="#3f3d56"/><path d="M944.68387,580.11939,939.0498,501.9506l1.07159-26.08994-21.95992,4.88623,8.08884,102.42635a11.44566,11.44566,0,1,0,18.43356-3.05385Z" transform="translate(-147.55709 -79)" fill="#ffb7b7"/><path d="M845.31021,511.217l-1.39631-26.19047-14.09344-11.45092L827.874,498.748l-51.88329,59.9845a11.44505,11.44505,0,1,0,13.25561,11.29506c0-.15408-.017-.30381-.023-.45639Z" transform="translate(-147.55709 -79)" fill="#ffb7b7"/><path d="M847.83725,502.83344,803.55709,559l-15-16,31-45,19.42316-85.3219a9.00378,9.00378,0,0,1,5.25966-4.68893,8.86575,8.86575,0,0,1,6.92027.4972l.25118.12731Z" transform="translate(-147.55709 -79)" fill="#386ede"/><path d="M923.94463,556l-14.066-97.7742.02322-.09032L924.34756,401.348l1.35352-.15742A15.77033,15.77033,0,0,1,942.874,413.67543L943.94463,559Z" transform="translate(-147.55709 -79)" fill="#386ede"/><path d="M928.38283,386.015H855.27312v-33.0315a36.55486,36.55486,0,1,1,73.10971,0Z" transform="translate(-147.55709 -79)" fill="#2f2e41"/><circle cx="890.98556" cy="357.12825" r="29.87879" transform="translate(2.69469 888.62749) rotate(-61.33681)" fill="#ffb7b7"/><circle cx="757.85329" cy="232.88951" r="19.88951" fill="#2f2e41"/><path d="M936.55709,358.709H858.79648v-.44042c0-20.15652,17.44151-36.55486,38.8803-36.55486s38.88031,16.39834,38.88031,36.55486Z" transform="translate(-147.55709 -79)" fill="#2f2e41"/><path d="M837.30468,774.28026l-28.88989-2.06348-.03858-.42285c-12.66089-138.21338-5.4768-229.499,20.22388-257.064l7.25195-13.56592-3.9707-22.83057c-10.60352-27.92969,7.20605-48.35986,8.55225-49.84473l5.74072-17.22168a6.51128,6.51128,0,0,1,5.50488-4.41113l78.01514-7.97851.05078.49755-.05078-.49755a6.49986,6.49986,0,0,1,7.14306,5.97607l.00684.0918-.02588.08838-28.43847,95.76806,2.49267,32.40772L944.436,765.13866,915.52978,771.333l-.13379-.43945L862.05761,596.06248Z" transform="translate(-147.55709 -79)" fill="#2f2e41"/><path d="M764.55709,636.5h-610a6.50745,6.50745,0,0,1-6.5-6.5V86a6.50745,6.50745,0,0,1,6.5-6.5h610a6.50753,6.50753,0,0,1,6.5,6.5V630A6.50753,6.50753,0,0,1,764.55709,636.5Z" transform="translate(-147.55709 -79)" fill="#fff"/><path d="M356.24944,103.43079V244.01555H355.963a70.29238,70.29238,0,0,1,0-140.58476Z" transform="translate(-147.55709 -79)" fill="#386ede"/><path d="M340.34247,135.11562v77.2151h-.15733a38.60755,38.60755,0,0,1,0-77.2151Z" transform="translate(-147.55709 -79)" fill="#fff"/><polygon points="312.543 281 100.146 281 100.146 93.723 138.114 93.723 138.114 95.723 102.146 95.723 102.146 279 312.543 279 312.543 281" fill="#3f3d56"/><circle cx="341.1804" cy="309.07154" r="19.09176" fill="#386ede"/><circle cx="519.08087" cy="493.91446" r="19.09176" fill="#3f3d56"/><circle cx="100.79782" cy="278.69829" r="19.09176" fill="#386ede"/><polygon points="312.543 280.434 360.272 280.434 360.272 232.705 264.813 232.705 264.813 328.163 312.543 328.163 312.543 280.434" fill="#3f3d56"/><rect x="414.94401" y="40.05132" width="209.14152" height="26.03421" fill="#e4e4e4"/><rect x="414.94401" y="96.45879" width="209.14152" height="26.03421" fill="#e4e4e4"/><path d="M709.16051,264.84292c0,.39053-.00848.77236-.01738,1.1542H624.13278c-.0089-.38184-.01737-.76367-.01737-1.1542a42.52255,42.52255,0,0,1,85.0451,0Z" transform="translate(-147.55709 -79)" fill="#e4e4e4"/><path d="M709.16051,567.42123c0,.39052-.00848.77236-.01738,1.1542H624.13278c-.0089-.38184-.01737-.76368-.01737-1.1542a42.52255,42.52255,0,0,1,85.0451,0Z" transform="translate(-147.55709 -79)" fill="#e4e4e4"/><rect x="518.08081" y="228.36523" width="2" height="217.42334" fill="#3f3d56"/><path d="M421.04853,546.88025a65.08773,65.08773,0,0,1-64.79909,65.08554V481.79471A65.08773,65.08773,0,0,1,421.04853,546.88025Z" transform="translate(-147.55709 -79)" fill="#386ede"/><path d="M766.43575,639H153.76387a6.21411,6.21411,0,0,1-6.20678-6.207V85.207A6.21411,6.21411,0,0,1,153.76387,79H766.43575a6.21412,6.21412,0,0,1,6.20679,6.207V632.793A6.21412,6.21412,0,0,1,766.43575,639ZM153.76387,81a4.21159,4.21159,0,0,0-4.20678,4.207V632.793A4.21159,4.21159,0,0,0,153.76387,637H766.43575a4.21167,4.21167,0,0,0,4.20679-4.207V85.207A4.21167,4.21167,0,0,0,766.43575,81Z" transform="translate(-147.55709 -79)" fill="#3f3d56"/><path d="M355.24947,245.01953V102.42676l1.004.0039a71.29307,71.29307,0,0,1,0,142.585Zm2-140.57764v138.5625a69.29321,69.29321,0,0,0,0-138.5625Z" transform="translate(-147.55709 -79)" fill="#3f3d56"/><path d="M357.24947,612.96582H355.963a66.08545,66.08545,0,1,1,0-132.1709h1.2865Zm-2-130.167a64.08551,64.08551,0,0,0,0,128.16308Z" transform="translate(-147.55709 -79)" fill="#3f3d56"/><path d="M666.6379,308.36523a43.28328,43.28328,0,0,1-43.50489-42.3413l-.02734-1.02686h87.06445l-.02734,1.02686A43.283,43.283,0,0,1,666.6379,308.36523Zm-41.46582-41.36816a41.5217,41.5217,0,0,0,82.93164,0Z" transform="translate(-147.55709 -79)" fill="#3f3d56"/><rect x="1" y="372" width="623.08545" height="2" fill="#3f3d56"/><rect x="1" y="185.99707" width="623.08545" height="2" fill="#3f3d56"/><rect x="415.39038" y="1" width="2" height="558" fill="#3f3d56"/><rect x="207.69226" y="1" width="2" height="558" fill="#3f3d56"/><circle cx="145.92379" cy="466.14463" r="19.09176" fill="#386ede"/><path d="M1051.44291,821h-381a1,1,0,0,1,0-2h381a1,1,0,0,1,0,2Z" transform="translate(-147.55709 -79)" fill="#cacaca"/><path d="M721.97734,818.98441c5.62838-18.13749.16075-38.76475-12.67716-52.54826a50.03693,50.03693,0,0,0-12.496-9.75441,1.50667,1.50667,0,0,0-1.81774,2.35586l2.5707,2.78174V759.698c-4.97606,4.64241-14.04638,1.101-14.95058-5.49862a11.31659,11.31659,0,0,1,3.42285-9.36476,10.74267,10.74267,0,0,1,10.20109-2.53135,13.348,13.348,0,0,1,8.74935,6.92766,14.767,14.767,0,0,1,.41642,11.19372c-2.74369,8.01876-11.49776,11.78914-19.48606,9.89977-8.15254-1.92822-13.99436-9.614-14.26081-17.91466a19.75346,19.75346,0,0,1,12.6194-18.62539c8.55783-3.16183,17.8492-.00063,25.07752,4.88676a56.44761,56.44761,0,0,1,17.57858,20.01556,57.753,57.753,0,0,1,7.07158,25.17383c.23295,7.25524-1.3078,15.497-6.65822,20.79705a16.4303,16.4303,0,0,1-4.38134,3.07615l1.51416,2.59041a56.674,56.674,0,0,0,27.98132-54.95041,57.41367,57.41367,0,0,0-5.47031-18.85381c-.84769-1.73431-3.43578-.2154-2.59041,1.51416a53.87588,53.87588,0,0,1,4.69032,32.384,53.6037,53.6037,0,0,1-26.12508,37.31569c-1.69095.97135-.20395,3.43316,1.51416,2.59041,8.07832-3.96249,11.6019-12.92989,12.37857-21.42363.88675-9.69762-1.63541-19.76923-5.71991-28.51533A60.92244,60.92244,0,0,0,712.198,737.01962c-8.04352-5.86637-18.93911-9.74427-28.72673-6.12806-8.75369,3.23419-15.117,12.09946-14.82189,21.51822.2962,9.45307,6.82361,18.34609,16.10063,20.71841,9.25367,2.36635,19.37327-1.71312,23.1035-10.76088a17.98912,17.98912,0,0,0,.35682-13.1791,16.20638,16.20638,0,0,0-9.20892-9.1364,14.34294,14.34294,0,0,0-12.76667.8315,14.52122,14.52122,0,0,0-6.55349,10.146,11.6174,11.6174,0,0,0,4.01712,10.80743,13.24166,13.24166,0,0,0,10.97121,2.64384,10.67206,10.67206,0,0,0,5.00886-2.66126,1.52536,1.52536,0,0,0,0-2.12132l-2.5707-2.78174L695.29,759.27215c15.72577,8.63312,25.56863,26.19162,25.9,43.9961a47.262,47.262,0,0,1-2.10547,14.91864c-.5735,1.84811,2.32153,2.63855,2.89284.79752Z" transform="translate(-147.55709 -79)" fill="#f0f0f0"/></svg>
hobo/static/css/style.scss
287 287
a.button.button-paragraph:hover p {
288 288
	color: white;
289 289
}
290

  
291
.application-elements {
292
	max-height: 40vh;
293
	overflow-y: scroll;
294
	label {
295
		display: block;
296
		max-width: 40em;
297
	}
298
}
299

  
300
#application-empty {
301
	background: url(build-application.svg) bottom right no-repeat;
302
	background-size: auto 90%;
303
	width: 100%;
304
	height: 80vh;
305
}
306

  
307
#no-applications {
308
	background: url(applications.svg) bottom center no-repeat;
309
	background-size: auto 90%;
310
	width: 100%;
311
	height: 80vh;
312
}
313

  
314
#applications {
315
	display: flex;
316
	flex-wrap: wrap;
317
	justify-content: space-between;
318
	.application {
319
		background: white;
320
		flex: 1;
321
		min-width: calc(50% - 0.5em);
322
		max-width: calc(50% - 0.5em);
323
		color: #3c3c33;
324
		box-sizing: border-box;
325
		border: 1px solid #ccc;
326
		margin-bottom: 1rem;
327
		padding: 0 1em;
328
		h3 {
329
			margin-top: 1em;
330
		}
331
		.buttons {
332
			justify-content: flex-end;
333
		}
334
	}
335
}
hobo/urls.py
18 18
from django.conf.urls import include, url
19 19
from django.contrib import admin
20 20

  
21
from .applications.urls import urlpatterns as applications_urls
21 22
from .debug.urls import urlpatterns as debug_urls
22 23
from .emails.urls import urlpatterns as emails_urls
23 24
from .environment.urls import urlpatterns as environment_urls
......
43 44
    url(r'^seo/', decorated_includes(admin_required, include(seo_urls))),
44 45
    url(r'^sms/', decorated_includes(admin_required, include(sms_urls))),
45 46
    url(r'^debug/', decorated_includes(admin_required, include(debug_urls))),
47
    url(r'^applications/', decorated_includes(admin_required, include(applications_urls))),
46 48
    url(r'^api/health/$', health_json, name='health-json'),
47 49
    url(r'^menu.json$', menu_json, name='menu_json'),
48 50
    url(r'^hobos.json$', hobo),
tests/test_application.py
1
import io
2
import json
3
import tarfile
4

  
5
import pytest
6
from httmock import HTTMock
7
from test_manager import login
8
from webtest import Upload
9

  
10
from hobo.applications.models import Application
11
from hobo.environment.models import Wcs
12

  
13
pytestmark = pytest.mark.django_db
14

  
15
WCS_AVAILABLE_OBJECTS = {
16
    "data": [
17
        {
18
            "id": "forms",
19
            "text": "Forms",
20
            "singular": "Form",
21
            "urls": {"list": "https://wcs.example.invalid/api/export-import/forms/"},
22
        },
23
        {
24
            "id": "cards",
25
            "text": "Card Models",
26
            "singular": "Card Model",
27
            "urls": {"list": "https://wcs.example.invalid/api/export-import/cards/"},
28
        },
29
        {
30
            "id": "workflows",
31
            "text": "Workflows",
32
            "singular": "Workflow",
33
            "urls": {"list": "https://wcs.example.invalid/api/export-import/workflows/"},
34
        },
35
        {
36
            "id": "blocks",
37
            "text": "Blocks",
38
            "singular": "Block of fields",
39
            "minor": True,
40
            "urls": {"list": "https://wcs.example.invalid/api/export-import/blocks/"},
41
        },
42
        {
43
            "id": "data-sources",
44
            "text": "Data Sources",
45
            "singular": "Data Source",
46
            "minor": True,
47
            "urls": {"list": "https://wcs.example.invalid/api/export-import/data-sources/"},
48
        },
49
        {
50
            "id": "mail-templates",
51
            "text": "Mail Templates",
52
            "singular": "Mail Template",
53
            "minor": True,
54
            "urls": {"list": "https://wcs.example.invalid/api/export-import/mail-templates/"},
55
        },
56
        {
57
            "id": "wscalls",
58
            "text": "Webservice Calls",
59
            "singular": "Webservice Call",
60
            "minor": True,
61
            "urls": {"list": "https://wcs.example.invalid/api/export-import/wscalls/"},
62
        },
63
    ]
64
}
65

  
66
WCS_AVAILABLE_FORMS = {
67
    "data": [
68
        {
69
            "id": "test-form",
70
            "text": "Test Form",
71
            "type": "forms",
72
            "urls": {
73
                "export": "https://wcs.example.invalid/api/export-import/forms/test-form/",
74
                "dependencies": "https://wcs.example.invalid/api/export-import/forms/test-form/dependencies/",
75
            },
76
        },
77
        {
78
            "id": "test2-form",
79
            "text": "Second Test Form",
80
            "type": "forms",
81
            "urls": {
82
                "export": "https://wcs.example.invalid/api/export-import/forms/test2-form/",
83
                "dependencies": "https://wcs.example.invalid/api/export-import/forms/test2-form/dependencies/",
84
            },
85
        },
86
    ]
87
}
88

  
89
WCS_FORM_DEPENDENCIES = {
90
    "data": [
91
        {
92
            "id": "test-card",
93
            "text": "Test Card",
94
            "type": "cards",
95
            "urls": {
96
                "export": "https://wcs.example.invalid/api/export-import/cards/test-card/",
97
                "dependencies": "https://wcs.example.invalid/api/export-import/cards/test-card/dependencies/",
98
            },
99
        }
100
    ]
101
}
102

  
103

  
104
def mocked_http(url, request):
105
    assert '&signature=' in url.query
106
    if url.netloc == 'wcs.example.invalid' and url.path == '/api/export-import/':
107
        return {'content': json.dumps(WCS_AVAILABLE_OBJECTS), 'status_code': 200}
108

  
109
    if url.path == '/api/export-import/forms/':
110
        return {'content': json.dumps(WCS_AVAILABLE_FORMS), 'status_code': 200}
111

  
112
    if url.path == '/api/export-import/forms/test-form/dependencies/':
113
        return {'content': json.dumps(WCS_FORM_DEPENDENCIES), 'status_code': 200}
114

  
115
    if url.path.endswith('/dependencies/'):
116
        return {'content': json.dumps({'data': []}), 'status_code': 200}
117

  
118
    if url.path == '/api/export-import/forms/test-form/':
119
        return {'content': '<formdef/>', 'status_code': 200, 'headers': {'content-length': '10'}}
120

  
121
    if url.path == '/api/export-import/cards/test-card/':
122
        return {'content': '<carddef/>', 'status_code': 200, 'headers': {'content-length': '10'}}
123

  
124
    if url.path == '/api/export-import/bundle-import/':
125
        return {'content': '{}', 'status_code': 200}
126

  
127

  
128
def test_create_application(app, admin_user, settings):
129
    Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar')
130

  
131
    settings.KNOWN_SERVICES = {
132
        'wcs': {
133
            'foobar': {
134
                'title': 'Foobar',
135
                'url': 'https://wcs.example.invalid/',
136
                'orig': 'example.org',
137
                'secret': 'xxx',
138
            }
139
        }
140
    }
141

  
142
    login(app)
143

  
144
    resp = app.get('/applications/')
145
    resp = resp.click('Create')
146
    resp.form['name'] = 'Test'
147
    resp = resp.form.submit()
148

  
149
    with HTTMock(mocked_http):
150
        resp = resp.follow()
151
        assert 'You should now assemble the different parts of your application.' in resp.text
152
        assert '/add/forms/' in resp
153
        resp = resp.click('Forms')
154
        assert resp.form.fields['elements'][0]._value == 'test-form'
155
        assert resp.form.fields['elements'][1]._value == 'test2-form'
156
        resp.form.fields['elements'][0].checked = True
157
        resp = resp.form.submit().follow()
158
        assert Application.objects.get(slug='test').elements.count() == 1
159
        element = Application.objects.get(slug='test').elements.all()[0]
160
        assert element.slug == 'test-form'
161

  
162
        resp = resp.click('Scan dependencies').follow()
163
        assert Application.objects.get(slug='test').elements.count() == 2
164
        assert 'Test Card' in resp.text
165

  
166
        resp = resp.click('Generate application bundle').follow()
167
        resp = resp.click('Download')
168
        assert resp.content_type == 'application/x-tar'
169
        # uncompressed tar, primitive check of contents
170
        assert b'<formdef/>' in resp.content
171
        assert b'<carddef/>' in resp.content
172

  
173

  
174
@pytest.fixture
175
def app_bundle():
176
    tar_io = io.BytesIO()
177
    with tarfile.open(mode='w', fileobj=tar_io) as tar:
178
        manifest_json = {
179
            'application': 'Test',
180
            'slug': 'test',
181
            'description': '',
182
            'elements': [
183
                {'type': 'forms', 'slug': 'test', 'name': 'test', 'auto-dependency': False},
184
                {'type': 'blocks', 'slug': 'test', 'name': 'test', 'auto-dependency': True},
185
                {'type': 'workflows', 'slug': 'test', 'name': 'test', 'auto-dependency': True},
186
            ],
187
        }
188
        manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode())
189
        tarinfo = tarfile.TarInfo('manifest.json')
190
        tarinfo.size = len(manifest_fd.getvalue())
191
        tar.addfile(tarinfo, fileobj=manifest_fd)
192

  
193
    return tar_io.getvalue()
194

  
195

  
196
def test_deploy_application(app, admin_user, settings, app_bundle):
197
    Application.objects.all().delete()
198
    Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar')
199

  
200
    settings.KNOWN_SERVICES = {
201
        'wcs': {
202
            'foobar': {
203
                'title': 'Foobar',
204
                'url': 'https://wcs.example.invalid/',
205
                'orig': 'example.org',
206
                'secret': 'xxx',
207
            }
208
        }
209
    }
210

  
211
    login(app)
212

  
213
    resp = app.get('/applications/')
214
    for i in range(2):
215
        resp = resp.click('Install')
216
        resp.form['bundle'] = Upload('app.tar', app_bundle, 'application/x-tar')
217
        with HTTMock(mocked_http):
218
            resp = resp.form.submit().follow()
219

  
220
        assert Application.objects.count() == 1
221
        assert Application.objects.get(slug='test').name == 'Test'
222
        assert Application.objects.get(slug='test').elements.count() == 3
0
-