diff --git a/combo/apps/momo/management/__init__.py b/combo/apps/momo/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/combo/apps/momo/management/commands/__init__.py b/combo/apps/momo/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/combo/apps/momo/management/commands/update_momo_manifest.py b/combo/apps/momo/management/commands/update_momo_manifest.py new file mode 100644 index 00000000..8a2526f2 --- /dev/null +++ b/combo/apps/momo/management/commands/update_momo_manifest.py @@ -0,0 +1,50 @@ +# combo - content management system +# Copyright (C) 2016 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 . + +from urlparse import urlparse + +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from django.db import connection +from django.test.client import RequestFactory +from django.utils import translation + +from combo.apps.momo.utils import generate_manifest, GenerationError, GenerationInfo + +class Command(BaseCommand): + def handle(self, *args, **kwargs): + if not getattr(settings, 'ENABLE_MOMO', False): + return + tenant = connection.get_tenant() + parsed_base_url = urlparse(tenant.get_base_url()) + if ':' in parsed_base_url.netloc: + server_name, server_port = parsed_base_url.netloc.split(':') + else: + server_name = parsed_base_url.netloc + server_port = '80' if parsed_base_url.scheme == 'http' else '443' + request = RequestFactory().get('/', SERVER_NAME=server_name, + SERVER_PORT=server_port) + request._get_scheme = lambda: parsed_base_url.scheme + + translation.activate(settings.LANGUAGE_CODE) + try: + generate_manifest(request) + except GenerationError as e: + raise CommandError(e.message) + except GenerationInfo as e: + if kwargs.get('verbosity') > 0: + print e.message + translation.deactivate() diff --git a/combo/apps/momo/utils.py b/combo/apps/momo/utils.py new file mode 100644 index 00000000..5814c3f7 --- /dev/null +++ b/combo/apps/momo/utils.py @@ -0,0 +1,268 @@ +# combo - content management system +# Copyright (C) 2016 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 . + +import datetime +import json +import os +import shutil +import zipfile + +from django.core.files.storage import default_storage +from django.template import RequestContext +from django.utils.translation import ugettext as _ + +import ckeditor +import ckeditor.views + +from combo.data.models import CellBase, LinkCell, FeedCell, Page +from .models import MomoIconCell, MomoOptions + + +class GenerationError(Exception): + pass + + +class GenerationInfo(Exception): + pass + + +def render_cell(cell, context): + classnames = ['cell', cell.css_class_name] + if cell.slug: + classnames.append(cell.slug) + return '
%s
' % (' '.join(classnames), cell.render(context)) + + +def get_page_dict(request, page, manifest): + cells = [x for x in CellBase.get_cells(page_id=page.id) if x.placeholder != 'footer'] + + page_dict = { + 'title': page.title, + 'id': 'page-%s-%s' % (page.slug, page.id), + } + + link_cells = [x for x in cells if isinstance(x, LinkCell)] + icon_cells = [x for x in cells if isinstance(x, MomoIconCell)] + feed_cells = [x for x in cells if isinstance(x, FeedCell)] + cells = [x for x in cells if not (isinstance(x, LinkCell) or isinstance(x, FeedCell))] + + if cells: + context = RequestContext(request, { + 'synchronous': True, + 'page': page, + 'page_cells': cells, + 'request': request, + 'site_base': request.build_absolute_uri('/')[:-1], + }) + page_dict['content'] = '\n'.join([render_cell(cell, context) for cell in cells]) + + if link_cells: + page_dict['seealso'] = [] + for cell in link_cells: + if cell.link_page: + # internal link + page_dict['seealso'].append('page-%s-%s' % + (cell.link_page.slug, cell.link_page.id)) + else: + # external link + page_dict['seealso'].append('seealso-%s' % cell.id) + manifest['_pages'].append({ + 'title': cell.title, + 'external': True, + 'url': cell.url, + 'id': 'seealso-%s' % cell.id}) + + if page.redirect_url: + page_dict['external'] = True + page_dict['url'] = page.redirect_url + + if icon_cells: + page_dict['icon'] = icon_cells[0].icon + page_dict['style'] = icon_cells[0].style + page_dict['description'] = icon_cells[0].description + if page_dict.get('external') and icon_cells[0].embed_page: + page_dict['external'] = False + + if hasattr(page, '_children') and page._children: + children = page._children + else: + children = page.get_children() + + if children: + page_dict['pages'] = [] + for child in children: + page_dict['pages'].append(get_page_dict(request, child, manifest)) + + if feed_cells: + if not 'pages' in page_dict: + page_dict['pages'] = [] + # turn feed entries in external pages + for feed_cell in feed_cells: + feed_context = feed_cell.get_cell_extra_context({}) + if feed_context.get('feed'): + for entry in feed_context.get('feed').entries: + feed_entry_page = { + 'title': entry.title, + 'id': 'feed-entry-%s-%s' % (feed_cell.id, entry.id), + 'url': entry.link, + 'external': True, + } + if entry.description: + feed_entry_page['description'] = entry.description + page_dict['pages'].append(feed_entry_page) + + return page_dict + + + + +def generate_manifest(request): + if not default_storage.exists('assets-base.zip'): + raise GenerationError(_('Missing base assets file')) + + manifest = { + 'menu': [], + '_pages': [] + } + level0_pages = Page.objects.filter(parent=None) + + # the application hierarchy is structured that way: + # - the home screen is the homepage + # - the application pages are created from the homepage siblings and their + # children + # - the application menu is created from direct children of the homepage + children = [] + homepage = None + for page in level0_pages: + if page.slug == 'index': + homepage = page + else: + children.append(page) + + if not homepage: + raise GenerationError(_('The homepage needs to be created first.')) + + homepage._children = children + manifest.update(get_page_dict(request, homepage, manifest)) + + # footer + footer_cells = CellBase.get_cells(page_id=homepage.id, placeholder='footer') + if footer_cells: + context = RequestContext(request, { + 'synchronous': True, + 'page': homepage, + 'page_cells': footer_cells, + 'request': request, + 'site_base': request.build_absolute_uri('/')[:-1], + }) + manifest['footer'] = '\n'.join([ + '' % (cell.slug, cell.render(context)) for cell in footer_cells]) + + # construct the application menu + manifest['menu'].append('home') # link to home screen + + # add real homepage children + menu_children = homepage.get_children() + for menu_child in menu_children: + link_cells = LinkCell.objects.filter(page_id=menu_child.id) + if link_cells: + # use link info instead of redirect url + link_cell = link_cells[0] + if link_cell.link_page: # internal link + menu_id = 'page-%s-%s' % (link_cell.link_page.slug, link_cell.link_page.id) + else: + menu_id = 'menu-%s-%s' % (menu_child.slug, menu_child.id) + link_context = link_cell.get_cell_extra_context({}) + manifest['_pages'].append({ + 'title': link_context['title'], + 'external': True, + 'url': link_context['url'], + 'id': menu_id, + }) + else: + menu_id = 'menu-%s-%s' % (menu_child.slug, menu_child.id) + manifest['_pages'].append({ + 'title': menu_child.title, + 'external': True, + 'url': menu_child.redirect_url, + 'id': menu_id, + }) + manifest['menu'].append(menu_id) + + # last item, application refresh + manifest['menu'].append({ + 'icon': 'fa-refresh', + 'id': 'momo-update', + 'title': _('Update Application')}) + + options = MomoOptions.get_object() + manifest['meta'] = { + 'title': options.title or homepage.title, + 'icon': 'icon.png', + 'contact': options.contact_email or 'info@entrouvert.com', + 'updateFreq': options.update_freq or 86400, + 'manifestUrl': request.build_absolute_uri(default_storage.url('index.json')), + 'assetsUrl': request.build_absolute_uri(default_storage.url('assets.zip')), + 'stylesheets': ["assets/index.css"], + } + + if options.extra_css: + manifest['meta']['stylesheets'].append('assets/%s' % options.extra_css) + + if options.icons_on_homepage: + manifest['display'] = 'icons' + + current_manifest = None + if default_storage.exists('index.json'): + with default_storage.open('index.json', mode='r') as fp: + current_manifest = fp.read() + + new_manifest = json.dumps(manifest, indent=2) + if new_manifest != current_manifest: + with default_storage.open('index.json', mode='w') as fp: + fp.write(new_manifest) + else: + raise GenerationInfo(_('No changes were detected.')) + + # assets.zip + if default_storage.exists('assets.zip'): + zf = zipfile.ZipFile(default_storage.open('assets.zip')) + existing_files = set([x for x in zf.namelist() if x[0] != '/' and x[-1] != '/']) + zf.close() + assets_mtime = default_storage.modified_time('assets.zip') + else: + existing_files = set([]) + assets_mtime = datetime.datetime(2015, 1, 1) + + ckeditor_filenames = set(ckeditor.views.get_image_files()) + media_ckeditor_filenames = set(['media/' + x for x in ckeditor_filenames]) + + if not media_ckeditor_filenames.issubset(existing_files) or default_storage.modified_time('assets-base.zip') > assets_mtime: + # if there are new files, or if the base assets file changed, we + # generate a new assets.zip + shutil.copy(default_storage.path('assets-base.zip'), + default_storage.path('assets.zip.tmp')) + zf = zipfile.ZipFile(default_storage.path('assets.zip.tmp'), 'a') + for filename in ckeditor_filenames: + zf.write(default_storage.path(filename), 'media/' + filename) + zf.close() + if os.path.exists(default_storage.path('assets.zip')): + os.unlink(default_storage.path('assets.zip')) + os.rename(default_storage.path('assets.zip.tmp'), default_storage.path('assets.zip')) + + raise GenerationInfo(_('A new update (including new assets) has been generated.')) + else: + raise GenerationInfo(_('A new update has been generated.')) diff --git a/combo/apps/momo/views.py b/combo/apps/momo/views.py index 7fc4aa3b..b1f57ff6 100644 --- a/combo/apps/momo/views.py +++ b/combo/apps/momo/views.py @@ -14,259 +14,27 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import datetime -import json -import shutil -import os -import zipfile - from django.conf import settings from django.contrib import messages -from django.core.files.storage import default_storage from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect -from django.template import RequestContext from django.utils.translation import ugettext as _ from django.views.generic import TemplateView, UpdateView -import ckeditor - -from combo.data.models import CellBase, LinkCell, FeedCell, Page -from .models import MomoIconCell, MomoOptions +from .models import MomoOptions +from .utils import generate_manifest, GenerationError, GenerationInfo class MomoManagerView(TemplateView): template_name = 'momo/manager_home.html' - -def render_cell(cell, context): - classnames = ['cell', cell.css_class_name] - if cell.slug: - classnames.append(cell.slug) - return '
%s
' % (' '.join(classnames), cell.render(context)) - -def get_page_dict(request, page, manifest): - cells = [x for x in CellBase.get_cells(page_id=page.id) if x.placeholder != 'footer'] - - page_dict = { - 'title': page.title, - 'id': 'page-%s-%s' % (page.slug, page.id), - } - - link_cells = [x for x in cells if isinstance(x, LinkCell)] - icon_cells = [x for x in cells if isinstance(x, MomoIconCell)] - feed_cells = [x for x in cells if isinstance(x, FeedCell)] - cells = [x for x in cells if not (isinstance(x, LinkCell) or isinstance(x, FeedCell))] - - if cells: - context = RequestContext(request, { - 'synchronous': True, - 'page': page, - 'page_cells': cells, - 'request': request, - 'site_base': request.build_absolute_uri('/')[:-1], - }) - page_dict['content'] = '\n'.join([render_cell(cell, context) for cell in cells]) - - if link_cells: - page_dict['seealso'] = [] - for cell in link_cells: - if cell.link_page: - # internal link - page_dict['seealso'].append('page-%s-%s' % - (cell.link_page.slug, cell.link_page.id)) - else: - # external link - page_dict['seealso'].append('seealso-%s' % cell.id) - manifest['_pages'].append({ - 'title': cell.title, - 'external': True, - 'url': cell.url, - 'id': 'seealso-%s' % cell.id}) - - if page.redirect_url: - page_dict['external'] = True - page_dict['url'] = page.redirect_url - - if icon_cells: - page_dict['icon'] = icon_cells[0].icon - page_dict['style'] = icon_cells[0].style - page_dict['description'] = icon_cells[0].description - if page_dict.get('external') and icon_cells[0].embed_page: - page_dict['external'] = False - - if hasattr(page, '_children') and page._children: - children = page._children - else: - children = page.get_children() - - if children: - page_dict['pages'] = [] - for child in children: - page_dict['pages'].append(get_page_dict(request, child, manifest)) - - if feed_cells: - if not 'pages' in page_dict: - page_dict['pages'] = [] - # turn feed entries in external pages - for feed_cell in feed_cells: - feed_context = feed_cell.get_cell_extra_context({}) - if feed_context.get('feed'): - for entry in feed_context.get('feed').entries: - feed_entry_page = { - 'title': entry.title, - 'id': 'feed-entry-%s-%s' % (feed_cell.id, entry.id), - 'url': entry.link, - 'external': True, - } - if entry.description: - feed_entry_page['description'] = entry.description - page_dict['pages'].append(feed_entry_page) - - return page_dict - - def generate(request, **kwargs): - if not default_storage.exists('assets-base.zip'): - messages.error(request, _('Missing base assets file')) - return HttpResponseRedirect(reverse('momo-manager-homepage')) - - manifest = { - 'menu': [], - '_pages': [] - } - level0_pages = Page.objects.filter(parent=None) - - # the application hierarchy is structured that way: - # - the home screen is the homepage - # - the application pages are created from the homepage siblings and their - # children - # - the application menu is created from direct children of the homepage - children = [] - homepage = None - for page in level0_pages: - if page.slug == 'index': - homepage = page - else: - children.append(page) - - if not homepage: - messages.error(request, _('The homepage needs to be created first.')) - return HttpResponseRedirect(reverse('momo-manager-homepage')) - - homepage._children = children - manifest.update(get_page_dict(request, homepage, manifest)) - - # footer - footer_cells = CellBase.get_cells(page_id=homepage.id, placeholder='footer') - if footer_cells: - context = RequestContext(request, { - 'synchronous': True, - 'page': homepage, - 'page_cells': footer_cells, - 'request': request, - 'site_base': request.build_absolute_uri('/')[:-1], - }) - manifest['footer'] = '\n'.join([ - '' % (cell.slug, cell.render(context)) for cell in footer_cells]) - - # construct the application menu - manifest['menu'].append('home') # link to home screen - - # add real homepage children - menu_children = homepage.get_children() - for menu_child in menu_children: - link_cells = LinkCell.objects.filter(page_id=menu_child.id) - if link_cells: - # use link info instead of redirect url - link_cell = link_cells[0] - if link_cell.link_page: # internal link - menu_id = 'page-%s-%s' % (link_cell.link_page.slug, link_cell.link_page.id) - else: - menu_id = 'menu-%s-%s' % (menu_child.slug, menu_child.id) - link_context = link_cell.get_cell_extra_context({}) - manifest['_pages'].append({ - 'title': link_context['title'], - 'external': True, - 'url': link_context['url'], - 'id': menu_id, - }) - else: - menu_id = 'menu-%s-%s' % (menu_child.slug, menu_child.id) - manifest['_pages'].append({ - 'title': menu_child.title, - 'external': True, - 'url': menu_child.redirect_url, - 'id': menu_id, - }) - manifest['menu'].append(menu_id) - - # last item, application refresh - manifest['menu'].append({ - 'icon': 'fa-refresh', - 'id': 'momo-update', - 'title': _('Update Application')}) - - options = MomoOptions.get_object() - manifest['meta'] = { - 'title': options.title or homepage.title, - 'icon': 'icon.png', - 'contact': options.contact_email or 'info@entrouvert.com', - 'updateFreq': options.update_freq or 86400, - 'manifestUrl': request.build_absolute_uri(default_storage.url('index.json')), - 'assetsUrl': request.build_absolute_uri(default_storage.url('assets.zip')), - 'stylesheets': ["assets/index.css"], - } - - if options.extra_css: - manifest['meta']['stylesheets'].append('assets/%s' % options.extra_css) - - if options.icons_on_homepage: - manifest['display'] = 'icons' - - current_manifest = None - if default_storage.exists('index.json'): - with default_storage.open('index.json', mode='r') as fp: - current_manifest = fp.read() - - new_manifest = json.dumps(manifest, indent=2) - if new_manifest != current_manifest: - with default_storage.open('index.json', mode='w') as fp: - fp.write(new_manifest) - else: - messages.info(request, _('No changes were detected.')) - return HttpResponseRedirect(reverse('momo-manager-homepage')) - - # assets.zip - if default_storage.exists('assets.zip'): - zf = zipfile.ZipFile(default_storage.open('assets.zip')) - existing_files = set([x for x in zf.namelist() if x[0] != '/' and x[-1] != '/']) - zf.close() - assets_mtime = default_storage.modified_time('assets.zip') - else: - existing_files = set([]) - assets_mtime = datetime.datetime(2015, 1, 1) - - ckeditor_filenames = set(ckeditor.views.get_image_files()) - media_ckeditor_filenames = set(['media/' + x for x in ckeditor_filenames]) - - if not media_ckeditor_filenames.issubset(existing_files) or default_storage.modified_time('assets-base.zip') > assets_mtime: - # if there are new files, or if the base assets file changed, we - # generate a new assets.zip - shutil.copy(default_storage.path('assets-base.zip'), - default_storage.path('assets.zip.tmp')) - zf = zipfile.ZipFile(default_storage.path('assets.zip.tmp'), 'a') - for filename in ckeditor_filenames: - zf.write(default_storage.path(filename), 'media/' + filename) - zf.close() - if os.path.exists(default_storage.path('assets.zip')): - os.unlink(default_storage.path('assets.zip')) - os.rename(default_storage.path('assets.zip.tmp'), default_storage.path('assets.zip')) - - messages.info(request, _('A new update (including new assets) has been generated.')) - else: - messages.info(request, _('A new update has been generated.')) - + try: + generate_manifest(request) + except GenerationError as e: + message.error(request, e.message) + except GenerationInfo as e: + messages.info(request, e.message) return HttpResponseRedirect(reverse('momo-manager-homepage')) diff --git a/debian/cron.hourly b/debian/cron.hourly index 7a5784ba..d04e9c3a 100644 --- a/debian/cron.hourly +++ b/debian/cron.hourly @@ -1,2 +1,3 @@ #!/bin/sh -su combo -s /bin/sh -c "/usr/bin/combo-manage tenant_command update_transactions --all-tenants" \ No newline at end of file +su combo -s /bin/sh -c "/usr/bin/combo-manage tenant_command update_transactions --all-tenants" +su combo -s /bin/sh -c "/usr/bin/combo-manage tenant_command update_momo_manifest --all-tenants -v0"