Projet

Général

Profil

0001-general-add-initial-support-for-custom-user-dashboar.patch

Frédéric Péters, 12 mai 2017 14:07

Télécharger (18,8 ko)

Voir les différences:

Subject: [PATCH] general: add initial support for custom user dashboards
 (#15043)

 combo/apps/dashboard/__init__.py                   | 25 +++++++
 combo/apps/dashboard/migrations/0001_initial.py    | 50 +++++++++++++
 combo/apps/dashboard/migrations/__init__.py        |  0
 combo/apps/dashboard/models.py                     | 66 +++++++++++++++++
 .../templates/combo/dashboard_cell_icons.html      | 11 +++
 .../dashboard/templates/combo/dashboardcell.html   | 11 +++
 combo/apps/dashboard/urls.py                       | 28 +++++++
 combo/apps/dashboard/views.py                      | 72 ++++++++++++++++++
 combo/public/templatetags/combo.py                 | 16 ++++
 combo/settings.py                                  |  4 +
 tests/settings.py                                  |  2 +
 tests/test_dashboard.py                            | 86 ++++++++++++++++++++++
 12 files changed, 371 insertions(+)
 create mode 100644 combo/apps/dashboard/__init__.py
 create mode 100644 combo/apps/dashboard/migrations/0001_initial.py
 create mode 100644 combo/apps/dashboard/migrations/__init__.py
 create mode 100644 combo/apps/dashboard/models.py
 create mode 100644 combo/apps/dashboard/templates/combo/dashboard_cell_icons.html
 create mode 100644 combo/apps/dashboard/templates/combo/dashboardcell.html
 create mode 100644 combo/apps/dashboard/urls.py
 create mode 100644 combo/apps/dashboard/views.py
 create mode 100644 tests/test_dashboard.py
combo/apps/dashboard/__init__.py
1
# combo - content management system
2
# Copyright (C) 2014-2017  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
import django.apps
15
from django.utils.translation import ugettext_lazy as _
16

  
17
class AppConfig(django.apps.AppConfig):
18
    name = 'combo.apps.dashboard'
19
    verbose_name = _('Dashboard')
20

  
21
    def get_before_urls(self):
22
        from . import urls
23
        return urls.urlpatterns
24

  
25
default_app_config = 'combo.apps.dashboard.AppConfig'
combo/apps/dashboard/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import migrations, models
5
from django.conf import settings
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('contenttypes', '0002_remove_content_type_name'),
12
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13
        ('auth', '0006_require_contenttypes_0002'),
14
        ('data', '0025_jsoncell_varnames_str'),
15
    ]
16

  
17
    operations = [
18
        migrations.CreateModel(
19
            name='DashboardCell',
20
            fields=[
21
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
22
                ('placeholder', models.CharField(max_length=20)),
23
                ('order', models.PositiveIntegerField()),
24
                ('slug', models.SlugField(verbose_name='Slug', blank=True)),
25
                ('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
26
                ('public', models.BooleanField(default=True, verbose_name='Public')),
27
                ('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
28
                ('last_update_timestamp', models.DateTimeField(auto_now=True)),
29
                ('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
30
                ('page', models.ForeignKey(to='data.Page')),
31
            ],
32
            options={
33
                'verbose_name': 'Dashboard',
34
            },
35
        ),
36
        migrations.CreateModel(
37
            name='Tile',
38
            fields=[
39
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
40
                ('cell_pk', models.PositiveIntegerField()),
41
                ('order', models.PositiveIntegerField()),
42
                ('cell_type', models.ForeignKey(to='contenttypes.ContentType')),
43
                ('dashboard', models.ForeignKey(to='dashboard.DashboardCell')),
44
                ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
45
            ],
46
            options={
47
                'ordering': ('order',),
48
            },
49
        ),
50
    ]
combo/apps/dashboard/models.py
1
# combo - content management system
2
# Copyright (C) 2014-2017  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 import settings
18
from django.contrib.contenttypes.models import ContentType
19
from django.contrib.contenttypes import fields
20
from django.db import models
21
from django.utils.translation import ugettext_lazy as _
22

  
23
from combo.data.models import CellBase
24
from combo.data.library import register_cell_class
25

  
26

  
27
@register_cell_class
28
class DashboardCell(CellBase):
29
    # container for tiles
30
    user_dependant = True
31

  
32
    class Meta:
33
        verbose_name = _('Dashboard')
34

  
35
    class Media:
36
        js = ('js/dashboard.js',)
37

  
38
    @classmethod
39
    def is_enabled(cls):
40
        return settings.COMBO_DASHBOARD_ENABLED
41

  
42
    def is_relevant(self, context):
43
        if not (getattr(context['request'], 'user', None) and context['request'].user.is_authenticated()):
44
            return False
45
        return True
46

  
47
    def render(self, context):
48
        context['tiles'] = Tile.objects.filter(dashboard=self, user=context['user'])
49
        return super(DashboardCell, self).render(context)
50

  
51

  
52
class Tile(models.Model):
53
    dashboard = models.ForeignKey(DashboardCell)
54
    cell_type = models.ForeignKey(ContentType)
55
    cell_pk = models.PositiveIntegerField()
56
    cell = fields.GenericForeignKey('cell_type', 'cell_pk')
57
    user = models.ForeignKey(settings.AUTH_USER_MODEL)
58
    order = models.PositiveIntegerField()
59

  
60
    class Meta:
61
        ordering = ('order',)
62

  
63
    @classmethod
64
    def get_by_cell(cls, cell):
65
        cell_type = ContentType.objects.get_for_model(cell)
66
        return cls.objects.get(cell_type__pk=cell_type.id, cell_pk=cell.id)
combo/apps/dashboard/templates/combo/dashboard_cell_icons.html
1
{% load i18n %}
2
<span class="dashboard-cell-icons">
3
{% if not in_dashboard %}
4
<a class="add-to-dashboard" href="{% url 'combo-dashboard-add-tile' cell_reference=cell.get_reference %}"></a>
5
{% else %}
6
<a class="dashboard-cell-menu"></a>
7
<ul class="menu closed">
8
  <li><a href="{% url 'combo-dashboard-remove-tile' cell_reference=cell.get_reference %}">{% trans 'Remove from favorites' %}</a></li>
9
</ul>
10
{% endif %}
11
</span>
combo/apps/dashboard/templates/combo/dashboardcell.html
1
{% load combo i18n %}
2
{% for tile in tiles %}
3
{% with cell=tile.cell %}
4
<div class="cell {{ cell.css_class_names }} {% if cell.slug %}{{cell.slug}}{% endif %}"
5
     data-ajax-cell-url="{{ site_base }}{% url 'combo-public-ajax-page-cell' page_pk=cell.page.id cell_reference=cell.get_reference %}"
6
     data-ajax-cell-loading-message="{% trans "Loading..." %}"
7
     {% if cell.ajax_refresh %}
8
     data-ajax-cell-refresh="{{ cell.ajax_refresh }}"
9
     {% endif %}><div>{% render_cell cell %}</div></div>
10
{% endwith %}
11
{% endfor %}
combo/apps/dashboard/urls.py
1
# combo - content management system
2
# Copyright (C) 2014-2017  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
18

  
19
from . import views
20

  
21
urlpatterns = [
22
    url(r'^api/dashboard/add/(?P<cell_reference>[\w_-]+)/$',
23
        views.dashboard_add_tile,
24
        name='combo-dashboard-add-tile'),
25
    url(r'^api/dashboard/remove/(?P<cell_reference>[\w_-]+)/$',
26
        views.dashboard_remove_tile,
27
        name='combo-dashboard-remove-tile'),
28
]
combo/apps/dashboard/views.py
1
# combo - content management system
2
# Copyright (C) 2014-2017  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 import settings
18
from django.contrib.contenttypes.models import ContentType
19
from django.core.exceptions import PermissionDenied
20
from django.http import Http404
21
from django.views.generic import RedirectView
22

  
23
from combo.data.models import CellBase
24
from combo.data.library import get_cell_class
25

  
26
from .models import DashboardCell, Tile
27

  
28

  
29
class DashboardAddTileView(RedirectView):
30
    permanent = False
31

  
32
    def get_redirect_url(self, cell_reference):
33
        dashboard = DashboardCell.objects.all()[0]
34

  
35
        cell = CellBase.get_cell(cell_reference)
36
        if not cell.page.is_visible(self.request.user):
37
            raise PermissionDenied()
38
        if not cell.is_visible(self.request.user):
39
            raise PermissionDenied()
40
        cell.pk = None
41
        cell.page = dashboard.page
42
        cell.placeholder = '_dashboard'
43
        cell.save()
44

  
45
        tile = Tile(dashboard=dashboard,
46
                cell=cell,
47
                user=self.request.user)
48
        tile.order = 0
49
        tile.save()
50

  
51
        return dashboard.page.get_online_url()
52

  
53
dashboard_add_tile = DashboardAddTileView.as_view()
54

  
55

  
56
class DashboardRemoveTileView(RedirectView):
57
    permanent = False
58

  
59
    def get_redirect_url(self, cell_reference):
60
        cell = CellBase.get_cell(cell_reference)
61
        try:
62
            tile = Tile.get_by_cell(cell)
63
        except Tile.DoesNotExist:
64
            raise Http404()
65
        if tile.user != self.request.user:
66
            raise PermissionDenied()
67
        dashboard = tile.dashboard
68
        tile.delete()
69
        cell.delete()
70
        return dashboard.page.get_online_url()
71

  
72
dashboard_remove_tile = DashboardRemoveTileView.as_view()
combo/public/templatetags/combo.py
19 19
import datetime
20 20

  
21 21
from django import template
22
from django.core.exceptions import PermissionDenied
22 23
from django.template import RequestContext
23 24
from django.template.base import TOKEN_BLOCK, TOKEN_VAR
24 25
from django.utils import dateparse
25 26

  
26 27
from combo.public.menu import get_menu_context
27 28
from combo.utils import NothingInCacheException
29
from combo.apps.dashboard.models import DashboardCell, Tile
28 30

  
29 31
register = template.Library()
30 32

  
......
49 51
def render_cell(context, cell):
50 52
    if context.get('render_skeleton') and cell.is_user_dependant(context):
51 53
        return template.loader.get_template('combo/deferred-cell.html').render(context)
54

  
55
    in_dashboard = False
56
    if DashboardCell.is_enabled():
57
        # check if cell is actually a dashboard tile
58
        try:
59
            tile = Tile.get_by_cell(cell)
60
        except Tile.DoesNotExist:
61
            pass
62
        else:
63
            if context['request'].user != tile.user:
64
                raise PermissionDenied()
65
            in_dashboard = True
66

  
67
    context['in_dashboard'] = in_dashboard
52 68
    try:
53 69
        return cell.render(context)
54 70
    except NothingInCacheException:
combo/settings.py
64 64
    'combo.profile',
65 65
    'combo.manager',
66 66
    'combo.public',
67
    'combo.apps.dashboard',
67 68
    'combo.apps.wcs',
68 69
    'combo.apps.publik',
69 70
    'combo.apps.family',
......
277 278
# page to redirect on the first visit, to suggest user to log in.
278 279
COMBO_WELCOME_PAGE_PATH = None
279 280

  
281
# dashboard support
282
COMBO_DASHBOARD_ENABLED = False
283

  
280 284
local_settings_file = os.environ.get('COMBO_SETTINGS_FILE',
281 285
        os.path.join(os.path.dirname(__file__), 'local_settings.py'))
282 286
if os.path.exists(local_settings_file):
tests/settings.py
21 21
LINGO_API_SIGN_KEY = '12345'
22 22
LINGO_SIGNATURE_KEY = '54321'
23 23

  
24
COMBO_DASHBOARD_ENABLED = True
25

  
24 26
import tempfile
25 27
MEDIA_ROOT = tempfile.mkdtemp('combo-test')
26 28

  
tests/test_dashboard.py
1
from webtest import TestApp
2
import pytest
3

  
4
from django.contrib.auth.models import User
5
from django.core.urlresolvers import reverse
6

  
7
from combo.wsgi import application
8
from combo.data.models import Page, CellBase, TextCell
9
from combo.apps.dashboard.models import DashboardCell, Tile
10

  
11
pytestmark = pytest.mark.django_db
12

  
13
from test_manager import admin_user, login
14

  
15
@pytest.fixture
16
def site(admin_user):
17
    page = Page(title='One', slug='index')
18
    page.save()
19
    # order=100 will be useful to get to the cell later on
20
    cell = TextCell(page=page, order=100, placeholder='content', text='hello world')
21
    cell.save()
22

  
23
    page = Page(title='Two', slug='two')
24
    page.save()
25
    dashboard_cell = DashboardCell(page=page, order=0, placeholder='content')
26
    dashboard_cell.save()
27

  
28

  
29
def test_empty_dashboard(app, site):
30
    resp = app.get('/', status=200)
31
    assert 'hello world' in resp.body
32
    resp = app.get('/two/', status=200)
33
    assert not 'dashboardcell' in resp.body
34
    app = login(app)
35
    resp = app.get('/two/', status=200)
36
    assert 'dashboardcell' in resp.body
37

  
38
def test_add_to_dashboard(app, site):
39
    app = login(app)
40
    cell = TextCell.objects.get(order=100)
41
    dashboard = DashboardCell.objects.all()[0]
42
    user = User.objects.all()[0]
43
    resp = app.get(reverse('combo-dashboard-add-tile',
44
        kwargs={'cell_reference': cell.get_reference()}))
45
    assert Tile.objects.count() == 1
46
    assert Tile.objects.all()[0].cell.id != cell.id
47
    assert Tile.objects.all()[0].cell.text == cell.text
48
    assert Tile.objects.all()[0].dashboard_id == dashboard.id
49
    assert Tile.objects.all()[0].user_id == user.id
50

  
51
    app = login(app)
52
    resp = app.get('/two/', status=200)
53
    assert 'hello world' in resp.body
54

  
55
def test_ajax_render(app, site):
56
    test_add_to_dashboard(app, site)
57
    app.reset() # logout
58
    tile = Tile.objects.all()[0]
59
    page = Page.objects.get(slug='two')
60
    resp = app.get(reverse('combo-public-ajax-page-cell',
61
        kwargs={'page_pk': page.id, 'cell_reference': tile.cell.get_reference()}),
62
        status=403)
63

  
64
    app = login(app)
65
    resp = app.get(reverse('combo-public-ajax-page-cell',
66
        kwargs={'page_pk': page.id, 'cell_reference': tile.cell.get_reference()}),
67
        status=200)
68

  
69
    user = User.objects.create_user('plop', email=None, password='plop')
70
    app = login(app, username='plop', password='plop')
71
    resp = app.get(reverse('combo-public-ajax-page-cell',
72
        kwargs={'page_pk': page.id, 'cell_reference': tile.cell.get_reference()}),
73
        status=403)
74

  
75
def test_remove_from_dashboard(app, site):
76
    test_add_to_dashboard(app, site)
77
    app.reset() # logout
78
    tile = Tile.objects.all()[0]
79
    resp = app.get(reverse('combo-dashboard-remove-tile',
80
        kwargs={'cell_reference': tile.cell.get_reference()}), status=403)
81

  
82
    app = login(app)
83
    resp = app.get(reverse('combo-dashboard-remove-tile',
84
        kwargs={'cell_reference': tile.cell.get_reference()}), status=302)
85
    assert Tile.objects.count() == 0
86
    assert TextCell.objects.count() == 1
0
-