Projet

Général

Profil

0001-general-remove-combo.apps.momo-32913.patch

Frédéric Péters, 08 mai 2019 17:52

Télécharger (41 ko)

Voir les différences:

Subject: [PATCH] general: remove combo.apps.momo (#32913)

 MANIFEST.in                                   |   1 -
 combo/apps/momo/README                        |  15 -
 combo/apps/momo/__init__.py                   |  64 -----
 combo/apps/momo/migrations/0001_initial.py    |  32 ---
 .../apps/momo/migrations/0002_momooptions.py  |  27 --
 .../migrations/0003_auto_20151021_1616.py     |  20 --
 .../0004_momoiconcell_description.py          |  25 --
 .../0005_momoiconcell_embed_page.py           |  20 --
 .../migrations/0006_momooptions_extra_css.py  |  20 --
 ...007_momoiconcell_restricted_to_unlogged.py |  20 --
 .../migrations/0008_momoiconcell_style.py     |  20 --
 .../migrations/0009_auto_20160504_1036.py     |  19 --
 .../migrations/0010_auto_20160928_1152.py     |  24 --
 ...0011_momoiconcell_last_update_timestamp.py |  28 --
 combo/apps/momo/migrations/__init__.py        |   0
 combo/apps/momo/models.py                     |  90 ------
 .../momo/templates/momo/manager_base.html     |  11 -
 .../momo/templates/momo/manager_home.html     |  18 --
 .../momo/templates/momo/momooptions_form.html |  19 --
 combo/apps/momo/urls.py                       |  32 ---
 combo/apps/momo/utils.py                      | 260 ------------------
 combo/apps/momo/views.py                      |  48 ----
 combo/settings.py                             |   1 -
 tests/test_momo.py                            | 103 -------
 24 files changed, 917 deletions(-)
 delete mode 100644 combo/apps/momo/README
 delete mode 100644 combo/apps/momo/__init__.py
 delete mode 100644 combo/apps/momo/migrations/0001_initial.py
 delete mode 100644 combo/apps/momo/migrations/0002_momooptions.py
 delete mode 100644 combo/apps/momo/migrations/0003_auto_20151021_1616.py
 delete mode 100644 combo/apps/momo/migrations/0004_momoiconcell_description.py
 delete mode 100644 combo/apps/momo/migrations/0005_momoiconcell_embed_page.py
 delete mode 100644 combo/apps/momo/migrations/0006_momooptions_extra_css.py
 delete mode 100644 combo/apps/momo/migrations/0007_momoiconcell_restricted_to_unlogged.py
 delete mode 100644 combo/apps/momo/migrations/0008_momoiconcell_style.py
 delete mode 100644 combo/apps/momo/migrations/0009_auto_20160504_1036.py
 delete mode 100644 combo/apps/momo/migrations/0010_auto_20160928_1152.py
 delete mode 100644 combo/apps/momo/migrations/0011_momoiconcell_last_update_timestamp.py
 delete mode 100644 combo/apps/momo/migrations/__init__.py
 delete mode 100644 combo/apps/momo/models.py
 delete mode 100644 combo/apps/momo/templates/momo/manager_base.html
 delete mode 100644 combo/apps/momo/templates/momo/manager_home.html
 delete mode 100644 combo/apps/momo/templates/momo/momooptions_form.html
 delete mode 100644 combo/apps/momo/urls.py
 delete mode 100644 combo/apps/momo/utils.py
 delete mode 100644 combo/apps/momo/views.py
 delete mode 100644 tests/test_momo.py
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
-