combo/combo/apps/momo/utils.py

261 lines
9.5 KiB
Python

# 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 <http://www.gnu.org/licenses/>.
import datetime
import json
import os
import shutil
import zipfile
from django.core.files.storage import default_storage
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_names]
if cell.slug:
classnames.append(cell.slug)
return '<div class="%s">%s</div>' % (' '.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 = {
'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.get_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 page.slug == 'index' and page.parent_id is None: # home
children = page.get_siblings()[1:]
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
try:
homepage = Page.objects.get(slug='index', parent_id=None)
except Page.DoesNotExist:
raise GenerationError(_('The homepage needs to be created first.'))
manifest.update(get_page_dict(request, homepage, manifest))
# footer
footer_cells = CellBase.get_cells(page_id=homepage.id, placeholder='footer')
if footer_cells:
context = {
'synchronous': True,
'page': homepage,
'page_cells': footer_cells,
'request': request,
'site_base': request.build_absolute_uri('/')[:-1],
}
manifest['footer'] = '\n'.join([
'<div id="footer-%s">%s</div>' % (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.'))