0001-general-add-initial-support-for-custom-user-dashboar.patch
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/static/js/dashboard.js | ||
---|---|---|
1 |
$(function() { |
|
2 |
$('.dashboardcell').delegate('a.dashboard-cell-menu', 'click', function() { |
|
3 |
$(this).next().toggleClass('closed'); |
|
4 |
}); |
|
5 |
}); |
combo/apps/dashboard/templates/combo/dashboard_cell_icons.html | ||
---|---|---|
1 |
{% load i18n %} |
|
2 |
{% if user.is_authenticated %} |
|
3 |
<span class="dashboard-cell-icons"> |
|
4 |
{% if not in_dashboard %} |
|
5 |
<a class="add-to-dashboard" href="{% url 'combo-dashboard-add-tile' cell_reference=cell.get_reference %}"></a> |
|
6 |
{% else %} |
|
7 |
<a class="dashboard-cell-menu"></a> |
|
8 |
<ul class="menu closed"> |
|
9 |
<li><a href="{% url 'combo-dashboard-remove-tile' cell_reference=cell.get_reference %}">{% trans 'Remove from favorites' %}</a></li> |
|
10 |
</ul> |
|
11 |
{% endif %} |
|
12 |
{% endif %} |
|
13 |
</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 |
- |