general: add initial support for custom user dashboards (#15043)
This commit is contained in:
parent
bd2df6fb70
commit
cc84f3c5be
|
@ -5,12 +5,14 @@ recursive-include combo/locale *.po *.mo
|
|||
recursive-include combo/apps/usersearch/static *.css *.js *.ico *.gif *.png *.jpg
|
||||
recursive-include combo/apps/lingo/static *.css *.js *.ico *.gif *.png *.jpg
|
||||
recursive-include combo/apps/dataviz/static *.css *.js *.ico *.gif *.png *.jpg
|
||||
recursive-include combo/apps/dashboard/static *.js
|
||||
recursive-include combo/manager/static *.css *.js *.ico *.gif *.png *.jpg
|
||||
recursive-include combo/public/static *.css *.js *.ico *.gif *.png *.jpg
|
||||
recursive-include data/themes *.css *.js *.gif *.png *.jpg *.jpeg *.html
|
||||
|
||||
# templates
|
||||
recursive-include combo/data/templates *.html *.txt
|
||||
recursive-include combo/apps/dashboard/templates *.html
|
||||
recursive-include combo/apps/search/templates *.html
|
||||
recursive-include combo/apps/usersearch/templates *.html
|
||||
recursive-include combo/apps/dataviz/templates *.html
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2014-2017 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
|
||||
import django.apps
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
class AppConfig(django.apps.AppConfig):
|
||||
name = 'combo.apps.dashboard'
|
||||
verbose_name = _('Dashboard')
|
||||
|
||||
def get_before_urls(self):
|
||||
from . import urls
|
||||
return urls.urlpatterns
|
||||
|
||||
default_app_config = 'combo.apps.dashboard.AppConfig'
|
|
@ -0,0 +1,50 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('auth', '0006_require_contenttypes_0002'),
|
||||
('data', '0025_jsoncell_varnames_str'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DashboardCell',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('placeholder', models.CharField(max_length=20)),
|
||||
('order', models.PositiveIntegerField()),
|
||||
('slug', models.SlugField(verbose_name='Slug', blank=True)),
|
||||
('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
|
||||
('public', models.BooleanField(default=True, verbose_name='Public')),
|
||||
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
|
||||
('last_update_timestamp', models.DateTimeField(auto_now=True)),
|
||||
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
|
||||
('page', models.ForeignKey(to='data.Page')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Dashboard',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tile',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('cell_pk', models.PositiveIntegerField()),
|
||||
('order', models.PositiveIntegerField()),
|
||||
('cell_type', models.ForeignKey(to='contenttypes.ContentType')),
|
||||
('dashboard', models.ForeignKey(to='dashboard.DashboardCell')),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('order',),
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,66 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2014-2017 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes import fields
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from combo.data.models import CellBase
|
||||
from combo.data.library import register_cell_class
|
||||
|
||||
|
||||
@register_cell_class
|
||||
class DashboardCell(CellBase):
|
||||
# container for tiles
|
||||
user_dependant = True
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Dashboard')
|
||||
|
||||
class Media:
|
||||
js = ('js/dashboard.js',)
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls):
|
||||
return settings.COMBO_DASHBOARD_ENABLED
|
||||
|
||||
def is_relevant(self, context):
|
||||
if not (getattr(context['request'], 'user', None) and context['request'].user.is_authenticated()):
|
||||
return False
|
||||
return True
|
||||
|
||||
def render(self, context):
|
||||
context['tiles'] = Tile.objects.filter(dashboard=self, user=context['user'])
|
||||
return super(DashboardCell, self).render(context)
|
||||
|
||||
|
||||
class Tile(models.Model):
|
||||
dashboard = models.ForeignKey(DashboardCell)
|
||||
cell_type = models.ForeignKey(ContentType)
|
||||
cell_pk = models.PositiveIntegerField()
|
||||
cell = fields.GenericForeignKey('cell_type', 'cell_pk')
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL)
|
||||
order = models.PositiveIntegerField()
|
||||
|
||||
class Meta:
|
||||
ordering = ('order',)
|
||||
|
||||
@classmethod
|
||||
def get_by_cell(cls, cell):
|
||||
cell_type = ContentType.objects.get_for_model(cell)
|
||||
return cls.objects.get(cell_type__pk=cell_type.id, cell_pk=cell.id)
|
|
@ -0,0 +1,5 @@
|
|||
$(function() {
|
||||
$('.dashboardcell').delegate('a.dashboard-cell-menu', 'click', function() {
|
||||
$(this).next().toggleClass('closed');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
{% load i18n %}
|
||||
{% if user.is_authenticated %}
|
||||
<span class="dashboard-cell-icons">
|
||||
{% if not in_dashboard %}
|
||||
<a class="add-to-dashboard" href="{% url 'combo-dashboard-add-tile' cell_reference=cell.get_reference %}"></a>
|
||||
{% else %}
|
||||
<a class="dashboard-cell-menu"></a>
|
||||
<ul class="menu closed">
|
||||
<li><a href="{% url 'combo-dashboard-remove-tile' cell_reference=cell.get_reference %}">{% trans 'Remove from favorites' %}</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</span>
|
|
@ -0,0 +1,11 @@
|
|||
{% load combo i18n %}
|
||||
{% for tile in tiles %}
|
||||
{% with cell=tile.cell %}
|
||||
<div class="cell {{ cell.css_class_names }} {% if cell.slug %}{{cell.slug}}{% endif %}"
|
||||
data-ajax-cell-url="{{ site_base }}{% url 'combo-public-ajax-page-cell' page_pk=cell.page.id cell_reference=cell.get_reference %}"
|
||||
data-ajax-cell-loading-message="{% trans "Loading..." %}"
|
||||
{% if cell.ajax_refresh %}
|
||||
data-ajax-cell-refresh="{{ cell.ajax_refresh }}"
|
||||
{% endif %}><div>{% render_cell cell %}</div></div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
|
@ -0,0 +1,28 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2014-2017 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^api/dashboard/add/(?P<cell_reference>[\w_-]+)/$',
|
||||
views.dashboard_add_tile,
|
||||
name='combo-dashboard-add-tile'),
|
||||
url(r'^api/dashboard/remove/(?P<cell_reference>[\w_-]+)/$',
|
||||
views.dashboard_remove_tile,
|
||||
name='combo-dashboard-remove-tile'),
|
||||
]
|
|
@ -0,0 +1,72 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2014-2017 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from combo.data.models import CellBase
|
||||
from combo.data.library import get_cell_class
|
||||
|
||||
from .models import DashboardCell, Tile
|
||||
|
||||
|
||||
class DashboardAddTileView(RedirectView):
|
||||
permanent = False
|
||||
|
||||
def get_redirect_url(self, cell_reference):
|
||||
dashboard = DashboardCell.objects.all()[0]
|
||||
|
||||
cell = CellBase.get_cell(cell_reference)
|
||||
if not cell.page.is_visible(self.request.user):
|
||||
raise PermissionDenied()
|
||||
if not cell.is_visible(self.request.user):
|
||||
raise PermissionDenied()
|
||||
cell.pk = None
|
||||
cell.page = dashboard.page
|
||||
cell.placeholder = '_dashboard'
|
||||
cell.save()
|
||||
|
||||
tile = Tile(dashboard=dashboard,
|
||||
cell=cell,
|
||||
user=self.request.user)
|
||||
tile.order = 0
|
||||
tile.save()
|
||||
|
||||
return dashboard.page.get_online_url()
|
||||
|
||||
dashboard_add_tile = DashboardAddTileView.as_view()
|
||||
|
||||
|
||||
class DashboardRemoveTileView(RedirectView):
|
||||
permanent = False
|
||||
|
||||
def get_redirect_url(self, cell_reference):
|
||||
cell = CellBase.get_cell(cell_reference)
|
||||
try:
|
||||
tile = Tile.get_by_cell(cell)
|
||||
except Tile.DoesNotExist:
|
||||
raise Http404()
|
||||
if tile.user != self.request.user:
|
||||
raise PermissionDenied()
|
||||
dashboard = tile.dashboard
|
||||
tile.delete()
|
||||
cell.delete()
|
||||
return dashboard.page.get_online_url()
|
||||
|
||||
dashboard_remove_tile = DashboardRemoveTileView.as_view()
|
|
@ -19,12 +19,14 @@ from __future__ import absolute_import
|
|||
import datetime
|
||||
|
||||
from django import template
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.template import RequestContext
|
||||
from django.template.base import TOKEN_BLOCK, TOKEN_VAR
|
||||
from django.utils import dateparse
|
||||
|
||||
from combo.public.menu import get_menu_context
|
||||
from combo.utils import NothingInCacheException
|
||||
from combo.apps.dashboard.models import DashboardCell, Tile
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
@ -49,6 +51,20 @@ def placeholder(context, placeholder_name):
|
|||
def render_cell(context, cell):
|
||||
if context.get('render_skeleton') and cell.is_user_dependant(context):
|
||||
return template.loader.get_template('combo/deferred-cell.html').render(context)
|
||||
|
||||
in_dashboard = False
|
||||
if DashboardCell.is_enabled():
|
||||
# check if cell is actually a dashboard tile
|
||||
try:
|
||||
tile = Tile.get_by_cell(cell)
|
||||
except Tile.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
if context['request'].user != tile.user:
|
||||
raise PermissionDenied()
|
||||
in_dashboard = True
|
||||
|
||||
context['in_dashboard'] = in_dashboard
|
||||
try:
|
||||
return cell.render(context)
|
||||
except NothingInCacheException:
|
||||
|
|
|
@ -65,6 +65,7 @@ INSTALLED_APPS = (
|
|||
'combo.profile',
|
||||
'combo.manager',
|
||||
'combo.public',
|
||||
'combo.apps.dashboard',
|
||||
'combo.apps.wcs',
|
||||
'combo.apps.publik',
|
||||
'combo.apps.family',
|
||||
|
@ -278,6 +279,9 @@ COMBO_INITIAL_LOGIN_PAGE_PATH = None
|
|||
# page to redirect on the first visit, to suggest user to log in.
|
||||
COMBO_WELCOME_PAGE_PATH = None
|
||||
|
||||
# dashboard support
|
||||
COMBO_DASHBOARD_ENABLED = False
|
||||
|
||||
local_settings_file = os.environ.get('COMBO_SETTINGS_FILE',
|
||||
os.path.join(os.path.dirname(__file__), 'local_settings.py'))
|
||||
if os.path.exists(local_settings_file):
|
||||
|
|
|
@ -21,6 +21,8 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
|
|||
LINGO_API_SIGN_KEY = '12345'
|
||||
LINGO_SIGNATURE_KEY = '54321'
|
||||
|
||||
COMBO_DASHBOARD_ENABLED = True
|
||||
|
||||
import tempfile
|
||||
MEDIA_ROOT = tempfile.mkdtemp('combo-test')
|
||||
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
from webtest import TestApp
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from combo.wsgi import application
|
||||
from combo.data.models import Page, CellBase, TextCell
|
||||
from combo.apps.dashboard.models import DashboardCell, Tile
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
from test_manager import admin_user, login
|
||||
|
||||
@pytest.fixture
|
||||
def site(admin_user):
|
||||
page = Page(title='One', slug='index')
|
||||
page.save()
|
||||
# order=100 will be useful to get to the cell later on
|
||||
cell = TextCell(page=page, order=100, placeholder='content', text='hello world')
|
||||
cell.save()
|
||||
|
||||
page = Page(title='Two', slug='two')
|
||||
page.save()
|
||||
dashboard_cell = DashboardCell(page=page, order=0, placeholder='content')
|
||||
dashboard_cell.save()
|
||||
|
||||
|
||||
def test_empty_dashboard(app, site):
|
||||
resp = app.get('/', status=200)
|
||||
assert 'hello world' in resp.body
|
||||
resp = app.get('/two/', status=200)
|
||||
assert not 'dashboardcell' in resp.body
|
||||
app = login(app)
|
||||
resp = app.get('/two/', status=200)
|
||||
assert 'dashboardcell' in resp.body
|
||||
|
||||
def test_add_to_dashboard(app, site):
|
||||
app = login(app)
|
||||
cell = TextCell.objects.get(order=100)
|
||||
dashboard = DashboardCell.objects.all()[0]
|
||||
user = User.objects.all()[0]
|
||||
resp = app.get(reverse('combo-dashboard-add-tile',
|
||||
kwargs={'cell_reference': cell.get_reference()}))
|
||||
assert Tile.objects.count() == 1
|
||||
assert Tile.objects.all()[0].cell.id != cell.id
|
||||
assert Tile.objects.all()[0].cell.text == cell.text
|
||||
assert Tile.objects.all()[0].dashboard_id == dashboard.id
|
||||
assert Tile.objects.all()[0].user_id == user.id
|
||||
|
||||
app = login(app)
|
||||
resp = app.get('/two/', status=200)
|
||||
assert 'hello world' in resp.body
|
||||
|
||||
def test_ajax_render(app, site):
|
||||
test_add_to_dashboard(app, site)
|
||||
app.reset() # logout
|
||||
tile = Tile.objects.all()[0]
|
||||
page = Page.objects.get(slug='two')
|
||||
resp = app.get(reverse('combo-public-ajax-page-cell',
|
||||
kwargs={'page_pk': page.id, 'cell_reference': tile.cell.get_reference()}),
|
||||
status=403)
|
||||
|
||||
app = login(app)
|
||||
resp = app.get(reverse('combo-public-ajax-page-cell',
|
||||
kwargs={'page_pk': page.id, 'cell_reference': tile.cell.get_reference()}),
|
||||
status=200)
|
||||
|
||||
user = User.objects.create_user('plop', email=None, password='plop')
|
||||
app = login(app, username='plop', password='plop')
|
||||
resp = app.get(reverse('combo-public-ajax-page-cell',
|
||||
kwargs={'page_pk': page.id, 'cell_reference': tile.cell.get_reference()}),
|
||||
status=403)
|
||||
|
||||
def test_remove_from_dashboard(app, site):
|
||||
test_add_to_dashboard(app, site)
|
||||
app.reset() # logout
|
||||
tile = Tile.objects.all()[0]
|
||||
resp = app.get(reverse('combo-dashboard-remove-tile',
|
||||
kwargs={'cell_reference': tile.cell.get_reference()}), status=403)
|
||||
|
||||
app = login(app)
|
||||
resp = app.get(reverse('combo-dashboard-remove-tile',
|
||||
kwargs={'cell_reference': tile.cell.get_reference()}), status=302)
|
||||
assert Tile.objects.count() == 0
|
||||
assert TextCell.objects.count() == 1
|
Loading…
Reference in New Issue