0001-general-remove-combo.apps.momo-32913.patch
MANIFEST.in | ||
---|---|---|
21 | 21 |
recursive-include combo/apps/fargo/templates *.html |
22 | 22 |
recursive-include combo/apps/lingo/templates *.html |
23 | 23 |
recursive-include combo/apps/maps/templates *.html |
24 |
recursive-include combo/apps/momo/templates *.html |
|
25 | 24 |
recursive-include combo/apps/newsletters/templates *.html |
26 | 25 |
recursive-include combo/apps/notifications/templates *.html |
27 | 26 |
recursive-include combo/apps/pwa/templates *.html *.js *.json |
combo/apps/momo/README | ||
---|---|---|
1 |
Combo/momo integration |
|
2 |
====================== |
|
3 | ||
4 |
Support is off by default, set ENABLE_MOMO = True to activate it. |
|
5 | ||
6 | ||
7 |
The application hierarchy is structured that way: |
|
8 | ||
9 |
- the home screen is the homepage |
|
10 |
- the application pages are created from the homepage siblings and their |
|
11 |
children |
|
12 |
- the application menu is created from direct children of the homepage |
|
13 | ||
14 | ||
15 |
Link cells are rendered as "see also" links. |
combo/apps/momo/__init__.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2015 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 django.apps |
|
18 |
from django.conf import settings |
|
19 |
from django.core.urlresolvers import reverse |
|
20 |
from django.db import connection |
|
21 |
from django.test.client import RequestFactory |
|
22 |
from django.utils import translation |
|
23 |
from django.utils.six.moves.urllib.parse import urlparse |
|
24 |
from django.utils.translation import ugettext_lazy as _ |
|
25 | ||
26 |
class AppConfig(django.apps.AppConfig): |
|
27 |
name = 'combo.apps.momo' |
|
28 |
verbose_name = _('Mobile Application') |
|
29 | ||
30 |
def is_enabled(self): |
|
31 |
return getattr(settings, 'ENABLE_MOMO', False) |
|
32 | ||
33 |
def get_before_urls(self): |
|
34 |
from . import urls |
|
35 |
return urls.urlpatterns |
|
36 | ||
37 |
def get_extra_manager_actions(self): |
|
38 |
return [{'href': reverse('momo-manager-homepage'), |
|
39 |
'text': _('Mobile Application')}] |
|
40 | ||
41 |
def hourly(self): |
|
42 |
from .utils import GenerationInfo |
|
43 |
try: |
|
44 |
self.update_momo_manifest() |
|
45 |
except GenerationInfo: |
|
46 |
pass |
|
47 | ||
48 |
def update_momo_manifest(self): |
|
49 |
from .utils import generate_manifest |
|
50 |
tenant = connection.get_tenant() |
|
51 |
parsed_base_url = urlparse(tenant.get_base_url()) |
|
52 |
if ':' in parsed_base_url.netloc: |
|
53 |
server_name, server_port = parsed_base_url.netloc.split(':') |
|
54 |
else: |
|
55 |
server_name = parsed_base_url.netloc |
|
56 |
server_port = '80' if parsed_base_url.scheme == 'http' else '443' |
|
57 |
request = RequestFactory().get('/', SERVER_NAME=server_name, |
|
58 |
SERVER_PORT=server_port) |
|
59 |
request._get_scheme = lambda: parsed_base_url.scheme |
|
60 | ||
61 |
with translation.override(settings.LANGUAGE_CODE): |
|
62 |
generate_manifest(request) |
|
63 | ||
64 |
default_app_config = 'combo.apps.momo.AppConfig' |
combo/apps/momo/migrations/0001_initial.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import models, migrations |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('auth', '0001_initial'), |
|
11 |
('data', '0010_feedcell'), |
|
12 |
] |
|
13 | ||
14 |
operations = [ |
|
15 |
migrations.CreateModel( |
|
16 |
name='MomoIconCell', |
|
17 |
fields=[ |
|
18 |
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
|
19 |
('placeholder', models.CharField(max_length=20)), |
|
20 |
('order', models.PositiveIntegerField()), |
|
21 |
('slug', models.SlugField(verbose_name='Slug', blank=True)), |
|
22 |
('public', models.BooleanField(default=True, verbose_name='Public')), |
|
23 |
('icon', models.CharField(default=b'', max_length=50, verbose_name='Icon', blank=True, choices=[(b'fa-home', 'Home'), (b'fa-globe', 'Globe'), (b'fa-mobile', 'Mobile'), (b'fa-comments', 'Comments'), (b'fa-map', 'Map'), (b'fa-users', 'Users'), (b'fa-institution', 'Institution'), (b'fa-bullhorn', 'Bull Horn'), (b'fa-calendar', 'Calendar'), (b'fa-map-marker', 'Map Marker'), (b'fa-book', 'Book')])), |
|
24 |
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)), |
|
25 |
('page', models.ForeignKey(to='data.Page')), |
|
26 |
], |
|
27 |
options={ |
|
28 |
'verbose_name': 'Icon for mobile', |
|
29 |
}, |
|
30 |
bases=(models.Model,), |
|
31 |
), |
|
32 |
] |
combo/apps/momo/migrations/0002_momooptions.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import models, migrations |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('momo', '0001_initial'), |
|
11 |
] |
|
12 | ||
13 |
operations = [ |
|
14 |
migrations.CreateModel( |
|
15 |
name='MomoOptions', |
|
16 |
fields=[ |
|
17 |
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
|
18 |
('title', models.CharField(max_length=100, null=True, verbose_name='Application Title')), |
|
19 |
('contact_email', models.EmailField(max_length=75, null=True, verbose_name='Contact Email')), |
|
20 |
('update_freq', models.PositiveIntegerField(default=86400, null=True, verbose_name='Update Frequency (in seconds)')), |
|
21 |
('icons_on_homepage', models.BooleanField(default=False, verbose_name='Use icons on the homepage')), |
|
22 |
], |
|
23 |
options={ |
|
24 |
}, |
|
25 |
bases=(models.Model,), |
|
26 |
), |
|
27 |
] |
combo/apps/momo/migrations/0003_auto_20151021_1616.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import models, migrations |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('momo', '0002_momooptions'), |
|
11 |
] |
|
12 | ||
13 |
operations = [ |
|
14 |
migrations.AlterField( |
|
15 |
model_name='momoiconcell', |
|
16 |
name='icon', |
|
17 |
field=models.CharField(default=b'', max_length=50, verbose_name='Icon', blank=True, choices=[(b'fa-home', 'Home'), (b'fa-globe', 'Globe'), (b'fa-mobile', 'Mobile'), (b'fa-comments', 'Comments'), (b'fa-map', 'Map'), (b'fa-users', 'Users'), (b'fa-institution', 'Institution'), (b'fa-bullhorn', 'Bull Horn'), (b'fa-calendar', 'Calendar'), (b'fa-map-marker', 'Map Marker'), (b'fa-book', 'Book'), (b'fa-envelope', 'Envelope'), (b'fa-car', 'Car'), (b'fa-road', 'Road')]), |
|
18 |
preserve_default=True, |
|
19 |
), |
|
20 |
] |
combo/apps/momo/migrations/0004_momoiconcell_description.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import models, migrations |
|
5 |
import ckeditor.fields |
|
6 | ||
7 | ||
8 |
class Migration(migrations.Migration): |
|
9 | ||
10 |
dependencies = [ |
|
11 |
('momo', '0003_auto_20151021_1616'), |
|
12 |
] |
|
13 | ||
14 |
operations = [ |
|
15 |
migrations.AlterModelOptions( |
|
16 |
name='momoiconcell', |
|
17 |
options={'verbose_name': 'Meta for mobile'}, |
|
18 |
), |
|
19 |
migrations.AddField( |
|
20 |
model_name='momoiconcell', |
|
21 |
name='description', |
|
22 |
field=ckeditor.fields.RichTextField(null=True, verbose_name='Description', blank=True), |
|
23 |
preserve_default=True, |
|
24 |
), |
|
25 |
] |
combo/apps/momo/migrations/0005_momoiconcell_embed_page.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import models, migrations |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('momo', '0004_momoiconcell_description'), |
|
11 |
] |
|
12 | ||
13 |
operations = [ |
|
14 |
migrations.AddField( |
|
15 |
model_name='momoiconcell', |
|
16 |
name='embed_page', |
|
17 |
field=models.BooleanField(default=False, verbose_name='Embed redirection URL'), |
|
18 |
preserve_default=True, |
|
19 |
), |
|
20 |
] |
combo/apps/momo/migrations/0006_momooptions_extra_css.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import models, migrations |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('momo', '0005_momoiconcell_embed_page'), |
|
11 |
] |
|
12 | ||
13 |
operations = [ |
|
14 |
migrations.AddField( |
|
15 |
model_name='momooptions', |
|
16 |
name='extra_css', |
|
17 |
field=models.CharField(max_length=100, null=True, verbose_name='Extra CSS', blank=True), |
|
18 |
preserve_default=True, |
|
19 |
), |
|
20 |
] |
combo/apps/momo/migrations/0007_momoiconcell_restricted_to_unlogged.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import models, migrations |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('momo', '0006_momooptions_extra_css'), |
|
11 |
] |
|
12 | ||
13 |
operations = [ |
|
14 |
migrations.AddField( |
|
15 |
model_name='momoiconcell', |
|
16 |
name='restricted_to_unlogged', |
|
17 |
field=models.BooleanField(default=False, verbose_name='Restrict to unlogged users'), |
|
18 |
preserve_default=True, |
|
19 |
), |
|
20 |
] |
combo/apps/momo/migrations/0008_momoiconcell_style.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import models, migrations |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('momo', '0007_momoiconcell_restricted_to_unlogged'), |
|
11 |
] |
|
12 | ||
13 |
operations = [ |
|
14 |
migrations.AddField( |
|
15 |
model_name='momoiconcell', |
|
16 |
name='style', |
|
17 |
field=models.CharField(default=b'', max_length=128, verbose_name='Style', blank=True), |
|
18 |
preserve_default=True, |
|
19 |
), |
|
20 |
] |
combo/apps/momo/migrations/0009_auto_20160504_1036.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import migrations, models |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('momo', '0008_momoiconcell_style'), |
|
11 |
] |
|
12 | ||
13 |
operations = [ |
|
14 |
migrations.AlterField( |
|
15 |
model_name='momoiconcell', |
|
16 |
name='icon', |
|
17 |
field=models.CharField(default=b'', max_length=50, verbose_name='Icon', blank=True, choices=[(b'fa-home', 'Home'), (b'fa-globe', 'Globe'), (b'fa-mobile', 'Mobile'), (b'fa-comments', 'Comments'), (b'fa-map', 'Map'), (b'fa-users', 'Users'), (b'fa-institution', 'Institution'), (b'fa-bullhorn', 'Bull Horn'), (b'fa-calendar', 'Calendar'), (b'fa-map-marker', 'Map Marker'), (b'fa-book', 'Book'), (b'fa-envelope', 'Envelope'), (b'fa-car', 'Car'), (b'fa-road', 'Road'), (b'fa-heart', 'Heart')]), |
|
18 |
), |
|
19 |
] |
combo/apps/momo/migrations/0010_auto_20160928_1152.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import migrations, models |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('momo', '0009_auto_20160504_1036'), |
|
11 |
] |
|
12 | ||
13 |
operations = [ |
|
14 |
migrations.AddField( |
|
15 |
model_name='momoiconcell', |
|
16 |
name='extra_css_class', |
|
17 |
field=models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True), |
|
18 |
), |
|
19 |
migrations.AlterField( |
|
20 |
model_name='momooptions', |
|
21 |
name='contact_email', |
|
22 |
field=models.EmailField(max_length=254, null=True, verbose_name='Contact Email'), |
|
23 |
), |
|
24 |
] |
combo/apps/momo/migrations/0011_momoiconcell_last_update_timestamp.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import migrations, models |
|
5 |
import datetime |
|
6 |
from django.utils.timezone import utc |
|
7 |
import combo.data.fields |
|
8 | ||
9 | ||
10 |
class Migration(migrations.Migration): |
|
11 | ||
12 |
dependencies = [ |
|
13 |
('momo', '0010_auto_20160928_1152'), |
|
14 |
] |
|
15 | ||
16 |
operations = [ |
|
17 |
migrations.AddField( |
|
18 |
model_name='momoiconcell', |
|
19 |
name='last_update_timestamp', |
|
20 |
field=models.DateTimeField(default=datetime.datetime.now(utc), auto_now=True), |
|
21 |
preserve_default=False, |
|
22 |
), |
|
23 |
migrations.AlterField( |
|
24 |
model_name='momoiconcell', |
|
25 |
name='description', |
|
26 |
field=combo.data.fields.RichTextField(null=True, verbose_name='Description', blank=True), |
|
27 |
), |
|
28 |
] |
combo/apps/momo/models.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2014-2015 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.apps import apps |
|
18 |
from django.db import models |
|
19 |
from django.forms import models as model_forms |
|
20 |
from django.forms import Select |
|
21 |
from django.utils.translation import ugettext_lazy as _ |
|
22 | ||
23 |
from combo.data.fields import RichTextField |
|
24 |
from combo.data.models import CellBase |
|
25 |
from combo.data.library import register_cell_class |
|
26 | ||
27 | ||
28 |
class MomoOptions(models.Model): |
|
29 |
title = models.CharField(_('Application Title'), max_length=100, null=True) |
|
30 |
contact_email = models.EmailField(_('Contact Email'), null=True) |
|
31 |
update_freq = models.PositiveIntegerField(_('Update Frequency (in seconds)'), |
|
32 |
default=86400, null=True) |
|
33 |
icons_on_homepage = models.BooleanField( |
|
34 |
_('Use icons on the homepage'), default=False) |
|
35 |
extra_css = models.CharField(_('Extra CSS'), max_length=100, blank=True, null=True) |
|
36 | ||
37 |
def save(self, *args, **kwargs): |
|
38 |
self.id = 1 |
|
39 |
return super(MomoOptions, self).save(*args, **kwargs) |
|
40 | ||
41 |
@classmethod |
|
42 |
def get_object(cls, *args, **kwargs): |
|
43 |
obj, created = cls.objects.get_or_create(pk=1) |
|
44 |
return obj |
|
45 | ||
46 | ||
47 |
@register_cell_class |
|
48 |
class MomoIconCell(CellBase): |
|
49 |
# initially for icons, now it holds additional page metadata such as |
|
50 |
# description. |
|
51 | ||
52 |
icon = models.CharField(_('Icon'), max_length=50, |
|
53 |
default='', blank=True, |
|
54 |
choices=[ |
|
55 |
('fa-home', _('Home')), |
|
56 |
('fa-globe', _('Globe')), |
|
57 |
('fa-mobile', _('Mobile')), |
|
58 |
('fa-comments', _('Comments')), |
|
59 |
('fa-map', _('Map')), |
|
60 |
('fa-users', _('Users')), |
|
61 |
('fa-institution', _('Institution')), |
|
62 |
('fa-bullhorn', _('Bull Horn')), |
|
63 |
('fa-calendar', _('Calendar')), |
|
64 |
('fa-map-marker', _('Map Marker')), |
|
65 |
('fa-book', _('Book')), |
|
66 |
('fa-envelope', _('Envelope')), |
|
67 |
('fa-car', _('Car')), |
|
68 |
('fa-road', _('Road')), |
|
69 |
('fa-heart', _('Heart')), |
|
70 |
]) |
|
71 |
description = RichTextField(_('Description'), blank=True, null=True) |
|
72 |
style = models.CharField(_('Style'), max_length=128, default='', blank=True) |
|
73 |
embed_page = models.BooleanField(_('Embed redirection URL'), default=False) |
|
74 | ||
75 |
class Meta: |
|
76 |
verbose_name = _('Meta for mobile') |
|
77 | ||
78 |
def render(self, context): |
|
79 |
return '' |
|
80 | ||
81 |
@classmethod |
|
82 |
def is_enabled(cls): |
|
83 |
return apps.get_app_config('momo').is_enabled() |
|
84 | ||
85 |
def get_default_form_class(self): |
|
86 |
sorted_icons = self._meta.get_field('icon').choices |
|
87 |
sorted_icons.sort(key=lambda x: x[1]) |
|
88 |
return model_forms.modelform_factory(self.__class__, |
|
89 |
fields=['icon', 'style', 'description', 'embed_page'], |
|
90 |
widgets={'icon': Select(choices=sorted_icons)}) |
combo/apps/momo/templates/momo/manager_base.html | ||
---|---|---|
1 |
{% extends "combo/manager_base.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block appbar %} |
|
5 |
<h2>{% trans 'Mobile Application' %}</h2> |
|
6 |
{% endblock %} |
|
7 | ||
8 |
{% block breadcrumb %} |
|
9 |
{{ block.super }} |
|
10 |
<a href="{% url 'momo-manager-homepage' %}">{% trans 'Mobile Application' %}</a> |
|
11 |
{% endblock %} |
combo/apps/momo/templates/momo/manager_home.html | ||
---|---|---|
1 |
{% extends "momo/manager_base.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block appbar %} |
|
5 |
<h2>{% trans 'Mobile Application' %}</h2> |
|
6 |
{% endblock %} |
|
7 | ||
8 |
{% block content %} |
|
9 | ||
10 |
<p> |
|
11 |
<a rel="popup" href="{% url 'momo-manager-options' %}">{% trans 'Options' %}</a> |
|
12 |
</p> |
|
13 | ||
14 |
<p> |
|
15 |
<a href="{% url 'momo-manager-generate' %}">{% trans 'Generate Content Update' %}</a> |
|
16 |
</p> |
|
17 | ||
18 |
{% endblock %} |
combo/apps/momo/templates/momo/momooptions_form.html | ||
---|---|---|
1 |
{% extends "momo/manager_base.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block appbar %} |
|
5 |
<h2>{% trans "Options" %}</h2> |
|
6 |
{% endblock %} |
|
7 | ||
8 |
{% block content %} |
|
9 | ||
10 |
<form method="post" enctype="multipart/form-data"> |
|
11 |
{% csrf_token %} |
|
12 |
{{ form.as_p }} |
|
13 |
<div class="buttons"> |
|
14 |
<button class="submit-button">{% trans "Save" %}</button> |
|
15 |
<a class="cancel" href="{% url 'momo-manager-homepage' %}">{% trans 'Cancel' %}</a> |
|
16 |
</div> |
|
17 |
</form> |
|
18 |
{% endblock %} |
|
19 |
combo/apps/momo/urls.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2015 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, include |
|
18 | ||
19 |
from combo.urls_utils import decorated_includes, manager_required |
|
20 | ||
21 |
from .views import MomoManagerView, OptionsUpdateView, generate |
|
22 | ||
23 |
momo_manager_urls = [ |
|
24 |
url('^$', MomoManagerView.as_view(), name='momo-manager-homepage'), |
|
25 |
url('^options/$', OptionsUpdateView.as_view(), name='momo-manager-options'), |
|
26 |
url('^generate/$', generate, name='momo-manager-generate'), |
|
27 |
] |
|
28 | ||
29 |
urlpatterns = [ |
|
30 |
url(r'^manage/momo/', decorated_includes(manager_required, |
|
31 |
include(momo_manager_urls))), |
|
32 |
] |
combo/apps/momo/utils.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2016 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 datetime |
|
18 |
import json |
|
19 |
import os |
|
20 |
import shutil |
|
21 |
import zipfile |
|
22 | ||
23 |
from django.core.files.storage import default_storage |
|
24 |
from django.utils.translation import ugettext as _ |
|
25 | ||
26 |
import ckeditor |
|
27 |
import ckeditor.views |
|
28 | ||
29 |
from combo.data.models import CellBase, LinkCell, FeedCell, Page |
|
30 |
from .models import MomoIconCell, MomoOptions |
|
31 | ||
32 | ||
33 |
class GenerationError(Exception): |
|
34 |
pass |
|
35 | ||
36 | ||
37 |
class GenerationInfo(Exception): |
|
38 |
pass |
|
39 | ||
40 | ||
41 |
def render_cell(cell, context): |
|
42 |
classnames = ['cell', cell.css_class_names] |
|
43 |
if cell.slug: |
|
44 |
classnames.append(cell.slug) |
|
45 |
return '<div class="%s">%s</div>' % (' '.join(classnames), cell.render(context)) |
|
46 | ||
47 | ||
48 |
def get_page_dict(request, page, manifest): |
|
49 |
cells = [x for x in CellBase.get_cells(page_id=page.id) if x.placeholder != 'footer'] |
|
50 | ||
51 |
page_dict = { |
|
52 |
'title': page.title, |
|
53 |
'id': 'page-%s-%s' % (page.slug, page.id), |
|
54 |
} |
|
55 | ||
56 |
link_cells = [x for x in cells if isinstance(x, LinkCell)] |
|
57 |
icon_cells = [x for x in cells if isinstance(x, MomoIconCell)] |
|
58 |
feed_cells = [x for x in cells if isinstance(x, FeedCell)] |
|
59 |
cells = [x for x in cells if not (isinstance(x, LinkCell) or isinstance(x, FeedCell))] |
|
60 | ||
61 |
if cells: |
|
62 |
context = { |
|
63 |
'synchronous': True, |
|
64 |
'page': page, |
|
65 |
'page_cells': cells, |
|
66 |
'request': request, |
|
67 |
'site_base': request.build_absolute_uri('/')[:-1], |
|
68 |
} |
|
69 |
page_dict['content'] = '\n'.join([render_cell(cell, context) for cell in cells]) |
|
70 | ||
71 |
if link_cells: |
|
72 |
page_dict['seealso'] = [] |
|
73 |
for cell in link_cells: |
|
74 |
if cell.link_page: |
|
75 |
# internal link |
|
76 |
page_dict['seealso'].append('page-%s-%s' % |
|
77 |
(cell.link_page.slug, cell.link_page.id)) |
|
78 |
else: |
|
79 |
# external link |
|
80 |
page_dict['seealso'].append('seealso-%s' % cell.id) |
|
81 |
manifest['_pages'].append({ |
|
82 |
'title': cell.title, |
|
83 |
'external': True, |
|
84 |
'url': cell.url, |
|
85 |
'id': 'seealso-%s' % cell.id}) |
|
86 | ||
87 |
if page.redirect_url: |
|
88 |
page_dict['external'] = True |
|
89 |
page_dict['url'] = page.get_redirect_url() |
|
90 | ||
91 |
if icon_cells: |
|
92 |
page_dict['icon'] = icon_cells[0].icon |
|
93 |
page_dict['style'] = icon_cells[0].style |
|
94 |
page_dict['description'] = icon_cells[0].description |
|
95 |
if page_dict.get('external') and icon_cells[0].embed_page: |
|
96 |
page_dict['external'] = False |
|
97 | ||
98 |
if page.slug == 'index' and page.parent_id is None: # home |
|
99 |
children = page.get_siblings()[1:] |
|
100 |
else: |
|
101 |
children = page.get_children() |
|
102 | ||
103 |
if children: |
|
104 |
page_dict['pages'] = [] |
|
105 |
for child in children: |
|
106 |
page_dict['pages'].append(get_page_dict(request, child, manifest)) |
|
107 | ||
108 |
if feed_cells: |
|
109 |
if not 'pages' in page_dict: |
|
110 |
page_dict['pages'] = [] |
|
111 |
# turn feed entries in external pages |
|
112 |
for feed_cell in feed_cells: |
|
113 |
feed_context = feed_cell.get_cell_extra_context({}) |
|
114 |
if feed_context.get('feed'): |
|
115 |
for entry in feed_context.get('feed').entries: |
|
116 |
feed_entry_page = { |
|
117 |
'title': entry.title, |
|
118 |
'id': 'feed-entry-%s-%s' % (feed_cell.id, entry.id), |
|
119 |
'url': entry.link, |
|
120 |
'external': True, |
|
121 |
} |
|
122 |
if entry.description: |
|
123 |
feed_entry_page['description'] = entry.description |
|
124 |
page_dict['pages'].append(feed_entry_page) |
|
125 | ||
126 |
return page_dict |
|
127 | ||
128 | ||
129 | ||
130 | ||
131 |
def generate_manifest(request): |
|
132 |
if not default_storage.exists('assets-base.zip'): |
|
133 |
raise GenerationError(_('Missing base assets file')) |
|
134 | ||
135 |
manifest = { |
|
136 |
'menu': [], |
|
137 |
'_pages': [] |
|
138 |
} |
|
139 |
level0_pages = Page.objects.filter(parent=None) |
|
140 | ||
141 |
# the application hierarchy is structured that way: |
|
142 |
# - the home screen is the homepage |
|
143 |
# - the application pages are created from the homepage siblings and their |
|
144 |
# children |
|
145 |
# - the application menu is created from direct children of the homepage |
|
146 |
try: |
|
147 |
homepage = Page.objects.get(slug='index', parent_id=None) |
|
148 |
except Page.DoesNotExist: |
|
149 |
raise GenerationError(_('The homepage needs to be created first.')) |
|
150 | ||
151 |
manifest.update(get_page_dict(request, homepage, manifest)) |
|
152 | ||
153 |
# footer |
|
154 |
footer_cells = CellBase.get_cells(page_id=homepage.id, placeholder='footer') |
|
155 |
if footer_cells: |
|
156 |
context = { |
|
157 |
'synchronous': True, |
|
158 |
'page': homepage, |
|
159 |
'page_cells': footer_cells, |
|
160 |
'request': request, |
|
161 |
'site_base': request.build_absolute_uri('/')[:-1], |
|
162 |
} |
|
163 |
manifest['footer'] = '\n'.join([ |
|
164 |
'<div id="footer-%s">%s</div>' % (cell.slug, cell.render(context)) for cell in footer_cells]) |
|
165 | ||
166 |
# construct the application menu |
|
167 |
manifest['menu'].append('home') # link to home screen |
|
168 | ||
169 |
# add real homepage children |
|
170 |
menu_children = homepage.get_children() |
|
171 |
for menu_child in menu_children: |
|
172 |
link_cells = LinkCell.objects.filter(page_id=menu_child.id) |
|
173 |
if link_cells: |
|
174 |
# use link info instead of redirect url |
|
175 |
link_cell = link_cells[0] |
|
176 |
if link_cell.link_page: # internal link |
|
177 |
menu_id = 'page-%s-%s' % (link_cell.link_page.slug, link_cell.link_page.id) |
|
178 |
else: |
|
179 |
menu_id = 'menu-%s-%s' % (menu_child.slug, menu_child.id) |
|
180 |
link_context = link_cell.get_cell_extra_context({}) |
|
181 |
manifest['_pages'].append({ |
|
182 |
'title': link_context['title'], |
|
183 |
'external': True, |
|
184 |
'url': link_context['url'], |
|
185 |
'id': menu_id, |
|
186 |
}) |
|
187 |
else: |
|
188 |
menu_id = 'menu-%s-%s' % (menu_child.slug, menu_child.id) |
|
189 |
manifest['_pages'].append({ |
|
190 |
'title': menu_child.title, |
|
191 |
'external': True, |
|
192 |
'url': menu_child.redirect_url, |
|
193 |
'id': menu_id, |
|
194 |
}) |
|
195 |
manifest['menu'].append(menu_id) |
|
196 | ||
197 |
# last item, application refresh |
|
198 |
manifest['menu'].append({ |
|
199 |
'icon': 'fa-refresh', |
|
200 |
'id': 'momo-update', |
|
201 |
'title': _('Update Application')}) |
|
202 | ||
203 |
options = MomoOptions.get_object() |
|
204 |
manifest['meta'] = { |
|
205 |
'title': options.title or homepage.title, |
|
206 |
'icon': 'icon.png', |
|
207 |
'contact': options.contact_email or 'info@entrouvert.com', |
|
208 |
'updateFreq': options.update_freq or 86400, |
|
209 |
'manifestUrl': request.build_absolute_uri(default_storage.url('index.json')), |
|
210 |
'assetsUrl': request.build_absolute_uri(default_storage.url('assets.zip')), |
|
211 |
'stylesheets': ["assets/index.css"], |
|
212 |
} |
|
213 | ||
214 |
if options.extra_css: |
|
215 |
manifest['meta']['stylesheets'].append('assets/%s' % options.extra_css) |
|
216 | ||
217 |
if options.icons_on_homepage: |
|
218 |
manifest['display'] = 'icons' |
|
219 | ||
220 |
current_manifest = None |
|
221 |
if default_storage.exists('index.json'): |
|
222 |
with default_storage.open('index.json', mode='r') as fp: |
|
223 |
current_manifest = fp.read() |
|
224 | ||
225 |
new_manifest = json.dumps(manifest, indent=2) |
|
226 |
if new_manifest != current_manifest: |
|
227 |
with default_storage.open('index.json', mode='w') as fp: |
|
228 |
fp.write(new_manifest) |
|
229 |
else: |
|
230 |
raise GenerationInfo(_('No changes were detected.')) |
|
231 | ||
232 |
# assets.zip |
|
233 |
if default_storage.exists('assets.zip'): |
|
234 |
zf = zipfile.ZipFile(default_storage.open('assets.zip')) |
|
235 |
existing_files = set([x for x in zf.namelist() if x[0] != '/' and x[-1] != '/']) |
|
236 |
zf.close() |
|
237 |
assets_mtime = default_storage.modified_time('assets.zip') |
|
238 |
else: |
|
239 |
existing_files = set([]) |
|
240 |
assets_mtime = datetime.datetime(2015, 1, 1) |
|
241 | ||
242 |
ckeditor_filenames = set(ckeditor.views.get_image_files()) |
|
243 |
media_ckeditor_filenames = set(['media/' + x for x in ckeditor_filenames]) |
|
244 | ||
245 |
if not media_ckeditor_filenames.issubset(existing_files) or default_storage.modified_time('assets-base.zip') > assets_mtime: |
|
246 |
# if there are new files, or if the base assets file changed, we |
|
247 |
# generate a new assets.zip |
|
248 |
shutil.copy(default_storage.path('assets-base.zip'), |
|
249 |
default_storage.path('assets.zip.tmp')) |
|
250 |
zf = zipfile.ZipFile(default_storage.path('assets.zip.tmp'), 'a') |
|
251 |
for filename in ckeditor_filenames: |
|
252 |
zf.write(default_storage.path(filename), 'media/' + filename) |
|
253 |
zf.close() |
|
254 |
if os.path.exists(default_storage.path('assets.zip')): |
|
255 |
os.unlink(default_storage.path('assets.zip')) |
|
256 |
os.rename(default_storage.path('assets.zip.tmp'), default_storage.path('assets.zip')) |
|
257 | ||
258 |
raise GenerationInfo(_('A new update (including new assets) has been generated.')) |
|
259 |
else: |
|
260 |
raise GenerationInfo(_('A new update has been generated.')) |
combo/apps/momo/views.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2015 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.contrib import messages |
|
18 |
from django.core.urlresolvers import reverse |
|
19 |
from django.http import HttpResponseRedirect |
|
20 |
from django.utils.encoding import force_text |
|
21 |
from django.views.generic import TemplateView, UpdateView |
|
22 | ||
23 |
from .models import MomoOptions |
|
24 |
from .utils import generate_manifest, GenerationError, GenerationInfo |
|
25 | ||
26 | ||
27 |
class MomoManagerView(TemplateView): |
|
28 |
template_name = 'momo/manager_home.html' |
|
29 | ||
30 |
def generate(request, **kwargs): |
|
31 |
try: |
|
32 |
generate_manifest(request) |
|
33 |
except GenerationError as e: |
|
34 |
messages.error(request, force_text(e)) |
|
35 |
except GenerationInfo as e: |
|
36 |
messages.info(request, force_text(e)) |
|
37 |
return HttpResponseRedirect(reverse('momo-manager-homepage')) |
|
38 | ||
39 | ||
40 |
class OptionsUpdateView(UpdateView): |
|
41 |
model = MomoOptions |
|
42 |
fields = '__all__' |
|
43 | ||
44 |
def get_object(self, *args, **kwargs): |
|
45 |
return MomoOptions.get_object() |
|
46 | ||
47 |
def get_success_url(self): |
|
48 |
return reverse('momo-manager-homepage') |
combo/settings.py | ||
---|---|---|
68 | 68 |
'combo.apps.family', |
69 | 69 |
'combo.apps.dataviz', |
70 | 70 |
'combo.apps.lingo', |
71 |
'combo.apps.momo', |
|
72 | 71 |
'combo.apps.newsletters', |
73 | 72 |
'combo.apps.fargo', |
74 | 73 |
'combo.apps.notifications', |
tests/test_momo.py | ||
---|---|---|
1 |
import json |
|
2 |
import os |
|
3 |
import zipfile |
|
4 | ||
5 |
import pytest |
|
6 |
from webtest import TestApp |
|
7 | ||
8 |
from django.conf import settings |
|
9 | ||
10 |
from combo.data.models import Page, CellBase, TextCell, LinkCell, FeedCell |
|
11 | ||
12 |
from .test_manager import login |
|
13 | ||
14 |
pytestmark = pytest.mark.django_db |
|
15 | ||
16 | ||
17 |
class MomoEnabled(object): |
|
18 |
def __enter__(self): |
|
19 |
settings.ENABLE_MOMO = True |
|
20 | ||
21 |
def __exit__(self, *args, **kwargs): |
|
22 |
settings.ENABLE_MOMO = False |
|
23 | ||
24 | ||
25 |
@pytest.fixture |
|
26 |
def assets_base(): |
|
27 |
assets_base_path = os.path.join(settings.MEDIA_ROOT, 'assets-base.zip') |
|
28 |
if not os.path.exists(assets_base_path): |
|
29 |
fd = open(assets_base_path, 'wb') |
|
30 |
z = zipfile.ZipFile(fd, 'w') |
|
31 |
z.close() |
|
32 | ||
33 | ||
34 |
def test_no_menu_if_not_enabled(app, admin_user): |
|
35 |
app = login(app) |
|
36 |
resp = app.get('/manage/', status=200) |
|
37 |
assert not 'Mobile Application' in resp.text |
|
38 | ||
39 | ||
40 |
def test_menu_if_enabled(app, admin_user): |
|
41 |
with MomoEnabled(): |
|
42 |
app = login(app) |
|
43 |
resp = app.get('/manage/', status=200) |
|
44 |
assert 'Mobile Application' in resp.text |
|
45 | ||
46 |
def test_options(app, admin_user): |
|
47 |
with MomoEnabled(): |
|
48 |
app = login(app) |
|
49 |
resp = app.get('/manage/', status=200) |
|
50 |
resp = resp.click('Mobile Application') |
|
51 |
resp = resp.click('Options') |
|
52 |
resp.form['title'] = 'Momo Test' |
|
53 |
resp.form['contact_email'] = 'foobar@localhost' |
|
54 |
resp = resp.form.submit() |
|
55 |
resp = resp.follow() |
|
56 |
resp = resp.click('Options') |
|
57 |
assert resp.form['title'].value == 'Momo Test' |
|
58 | ||
59 |
def test_generate_no_assets_base(app, admin_user): |
|
60 |
with MomoEnabled(): |
|
61 |
app = login(app) |
|
62 |
resp = app.get('/manage/', status=200) |
|
63 |
resp = resp.click('Mobile Application') |
|
64 |
resp = resp.click('Generate Content Update') |
|
65 |
assert not os.path.exists(os.path.join(settings.MEDIA_ROOT, 'index.json')) |
|
66 | ||
67 |
def test_generate_no_homepage(app, admin_user, assets_base): |
|
68 |
with MomoEnabled(): |
|
69 |
app = login(app) |
|
70 |
resp = app.get('/manage/', status=200) |
|
71 |
resp = resp.click('Mobile Application') |
|
72 |
resp = resp.click('Generate Content Update') |
|
73 |
assert not os.path.exists(os.path.join(settings.MEDIA_ROOT, 'index.json')) |
|
74 | ||
75 |
def test_generate_simple(app, admin_user, assets_base): |
|
76 |
Page.objects.all().delete() |
|
77 |
page1 = Page(title='My Mobile App', slug='index', template_name='standard') |
|
78 |
page1.save() |
|
79 |
page2 = Page(title='Two', slug='two', template_name='standard') |
|
80 |
page2.save() |
|
81 |
page3 = Page(title='Three', slug='three', parent=page1, template_name='standard') |
|
82 |
page3.save() |
|
83 | ||
84 |
cell = TextCell(page=page2, placeholder='content', text='Lorem ipsum', order=0) |
|
85 |
cell.save() |
|
86 | ||
87 |
cell = TextCell(page=page1, placeholder='footer', text='This is the footer', order=0) |
|
88 |
cell.save() |
|
89 | ||
90 |
with MomoEnabled(): |
|
91 |
app = login(app) |
|
92 |
resp = app.get('/manage/', status=200) |
|
93 |
resp = resp.click('Mobile Application') |
|
94 |
resp = resp.click('Generate Content Update') |
|
95 |
assert os.path.exists(os.path.join(settings.MEDIA_ROOT, 'index.json')) |
|
96 |
assert os.path.exists(os.path.join(settings.MEDIA_ROOT, 'assets.zip')) |
|
97 |
content = json.load(open(os.path.join(settings.MEDIA_ROOT, 'index.json'))) |
|
98 |
assert content['meta']['title'] == 'My Mobile App' |
|
99 |
assert 'This is the footer' in content['footer'] |
|
100 |
assert len(content['pages']) == 1 |
|
101 |
assert content['pages'][0]['title'] == 'Two' |
|
102 |
assert 'Lorem ipsum' in content['pages'][0]['content'] |
|
103 |
assert 'menu-three-%s' % page3.id in content['menu'] |
|
104 |
- |