hobo/hobo/applications/views.py

312 lines
11 KiB
Python

# hobo - portal to configure and deploy applications
# Copyright (C) 2015-2022 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import io
import json
import tarfile
import urllib.parse
from django.conf import settings
from django.core.files.base import ContentFile
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse, reverse_lazy
from django.views.generic import FormView, ListView, TemplateView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from hobo.environment.utils import get_installed_services
from .forms import InstallForm
from .models import Application, Element, Relation, Version
from .utils import Requests
requests = Requests()
class HomeView(ListView):
template_name = 'hobo/applications/home.html'
model = Application
def get_queryset(self):
return super().get_queryset().order_by('name')
home = HomeView.as_view()
class InitView(CreateView):
template_name = 'hobo/applications/create.html'
model = Application
fields = ['name']
def get_success_url(self):
return reverse('application-manifest', kwargs={'app_slug': self.object.slug})
init = InitView.as_view()
def get_object_types():
object_types = []
for service_id, services in getattr(settings, 'KNOWN_SERVICES', {}).items():
if service_id not in Application.SUPPORTED_MODULES:
continue
service_objects = {x.get_base_url_path(): x for x in get_installed_services(types=[service_id])}
for service in services.values():
if service_objects[service['url']].secondary:
continue
url = urllib.parse.urljoin(service['url'], 'api/export-import/')
response = requests.get(url)
if not response.ok:
continue
for object_type in response.json()['data']:
object_type['service'] = service
object_types.append(object_type)
return object_types
class ManifestView(TemplateView):
template_name = 'hobo/applications/manifest.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['app'] = Application.objects.get(slug=self.kwargs['app_slug'])
context['relations'] = context['app'].relation_set.all().select_related('element')
context['versions'] = context['app'].version_set.exists()
context['types_by_service'] = {}
type_labels = {}
for object_type in get_object_types():
type_labels[object_type['id']] = object_type['singular']
if object_type.get('minor'):
continue
service = object_type['service']['title']
if service not in context['types_by_service']:
context['types_by_service'][service] = []
context['types_by_service'][service].append(object_type)
for relation in context['relations']:
relation.element.type_label = type_labels.get(relation.element.type)
return context
manifest = ManifestView.as_view()
class MetadataView(UpdateView):
template_name = 'hobo/applications/edit-metadata.html'
model = Application
slug_url_kwarg = 'app_slug'
fields = ['name', 'slug', 'description']
def get_success_url(self):
return reverse('application-manifest', kwargs={'app_slug': self.object.slug})
metadata = MetadataView.as_view()
class AppAddElementView(TemplateView):
template_name = 'hobo/applications/add-element.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['app'] = Application.objects.get(slug=self.kwargs['app_slug'])
for object_type in get_object_types():
if object_type.get('id') == self.kwargs['type']:
context['type'] = object_type
url = object_type['urls']['list']
response = requests.get(url)
context['elements'] = response.json()['data']
break
return context
def post(self, request, app_slug, type):
context = self.get_context_data()
app = context['app']
element_infos = {x['id']: x for x in context['elements']}
for element_slug in request.POST.getlist('elements'):
element, created = Element.objects.get_or_create(
type=type, slug=element_slug, defaults={'name': element_infos[element_slug]['text']}
)
element.name = element_infos[element_slug]['text']
element.cache = element_infos[element_slug]
element.save()
relation, created = Relation.objects.get_or_create(application=app, element=element)
relation.auto_dependency = False
relation.save()
return HttpResponseRedirect(reverse('application-manifest', kwargs={'app_slug': app_slug}))
add_element = AppAddElementView.as_view()
class AppDeleteElementView(DeleteView):
model = Relation
template_name = 'hobo/applications/element_confirm_delete.html'
def get_success_url(self):
return reverse('application-manifest', kwargs={'app_slug': self.kwargs['app_slug']})
delete_element = AppDeleteElementView.as_view()
def scandeps(request, app_slug):
app = Application.objects.get(slug=app_slug)
app.relation_set.filter(auto_dependency=True).delete()
relations = app.relation_set.select_related('element')
elements = {(x.element.type, x.element.slug): x.element for x in relations}
finished = False
while not finished:
finished = True
for (type, slug), element in list(elements.items()):
dependencies_url = element.cache['urls'].get('dependencies')
element.done = True
if not dependencies_url:
continue
response = requests.get(dependencies_url)
for dependency in response.json()['data']:
if (dependency['type'], dependency['id']) in elements:
continue
finished = False
element, created = Element.objects.get_or_create(
type=dependency['type'], slug=dependency['id'], defaults={'name': dependency['text']}
)
element.name = dependency['text']
element.cache = dependency
element.save()
relation, created = Relation.objects.get_or_create(application=app, element=element)
if created:
relation.auto_dependency = True
relation.save()
elements[(element.type, element.slug)] = element
return HttpResponseRedirect(reverse('application-manifest', kwargs={'app_slug': app_slug}))
def generate(request, app_slug):
app = Application.objects.get(slug=app_slug)
version = Version(application=app)
version.save()
tar_io = io.BytesIO()
with tarfile.open(mode='w', fileobj=tar_io) as tar:
manifest_json = {
'application': app.name,
'slug': app.slug,
'description': app.description,
'elements': [],
}
for relation in app.relation_set.all().select_related('element'):
element = relation.element
manifest_json['elements'].append(
{
'type': element.type,
'slug': element.slug,
'name': element.name,
'auto-dependency': relation.auto_dependency,
}
)
response = requests.get(element.cache['urls']['export'])
tarinfo = tarfile.TarInfo('%s/%s' % (element.type, element.slug))
tarinfo.mtime = version.last_update_timestamp.timestamp()
tarinfo.size = int(response.headers['content-length'])
tar.addfile(tarinfo, fileobj=io.BytesIO(response.content))
manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode())
tarinfo = tarfile.TarInfo('manifest.json')
tarinfo.size = len(manifest_fd.getvalue())
tarinfo.mtime = version.last_update_timestamp.timestamp()
tar.addfile(tarinfo, fileobj=manifest_fd)
version.bundle.save('%s.tar' % app_slug, content=ContentFile(tar_io.getvalue()))
version.save()
return HttpResponseRedirect(reverse('application-manifest', kwargs={'app_slug': app_slug}))
def download(request, app_slug):
app = Application.objects.get(slug=app_slug)
version = app.version_set.order_by('-last_update_timestamp')[0]
response = HttpResponse(version.bundle, content_type='application/x-tar')
response['Content-Disposition'] = 'attachment; filename="%s"' % '%s.tar' % app_slug
return response
class Install(FormView):
form_class = InstallForm
template_name = 'hobo/applications/install.html'
success_url = reverse_lazy('applications-home')
def form_valid(self, form):
tar_io = io.BytesIO(self.request.FILES['bundle'].read())
tar = tarfile.open(fileobj=tar_io)
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
app, created = Application.objects.get_or_create(
slug=manifest.get('slug'), defaults={'name': manifest.get('application')}
)
app.name = manifest.get('application')
app.description = manifest.get('description')
if created:
# mark as non-editable only newly deployed applications, this allows
# overwriting a local application and keep on developing it.
app.editable = False
else:
app.relation_set.all().delete()
app.save()
for element_dict in manifest.get('elements'):
element, created = Element.objects.get_or_create(
type=element_dict['type'], slug=element_dict['slug'], defaults={'name': element_dict['name']}
)
element.name = element_dict['name']
element.save()
relation = Relation(
application=app,
element=element,
auto_dependency=element_dict['auto-dependency'],
)
relation.save()
version = Version(application=app)
version.bundle.save('%s.tar' % app.slug, content=ContentFile(tar_io.getvalue()))
version.save()
version.deploy()
return super().form_valid(form)
install = Install.as_view()
class AppDeleteView(DeleteView):
model = Application
template_name = 'hobo/applications/app_confirm_delete.html'
def get_success_url(self):
return reverse('applications-home')
delete = AppDeleteView.as_view()