From b6da0569f5020de7f398424694ac853163360a57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Wed, 8 Aug 2018 22:18:36 +0200 Subject: [PATCH] general: add "sub slug" to create variable pages (#23535) --- combo/data/migrations/0036_page_sub_slug.py | 20 +++++ combo/data/models.py | 1 + combo/manager/forms.py | 2 +- combo/public/static/js/combo.public.js | 12 ++- combo/public/templates/combo/placeholder.html | 6 +- combo/public/templatetags/combo.py | 5 ++ combo/public/views.py | 80 ++++++++++++++----- tests/test_public.py | 55 ++++++++++++- 8 files changed, 153 insertions(+), 28 deletions(-) create mode 100644 combo/data/migrations/0036_page_sub_slug.py diff --git a/combo/data/migrations/0036_page_sub_slug.py b/combo/data/migrations/0036_page_sub_slug.py new file mode 100644 index 0000000..3a017a1 --- /dev/null +++ b/combo/data/migrations/0036_page_sub_slug.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-08-08 18:30 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('data', '0035_page_related_cells'), + ] + + operations = [ + migrations.AddField( + model_name='page', + name='sub_slug', + field=models.CharField(blank=True, max_length=150, verbose_name='Sub Slug'), + ), + ] diff --git a/combo/data/models.py b/combo/data/models.py index e551dad..0d570ef 100644 --- a/combo/data/models.py +++ b/combo/data/models.py @@ -124,6 +124,7 @@ class Page(models.Model): title = models.CharField(_('Title'), max_length=150) slug = models.SlugField(_('Slug')) + sub_slug = models.CharField(_('Sub Slug'), max_length=150, blank=True) description = models.TextField(_('Description'), blank=True) template_name = models.CharField(_('Template'), max_length=50) parent = models.ForeignKey('self', null=True, blank=True) diff --git a/combo/manager/forms.py b/combo/manager/forms.py index dc110f7..bc5e024 100644 --- a/combo/manager/forms.py +++ b/combo/manager/forms.py @@ -32,7 +32,7 @@ class PageEditTitleForm(forms.ModelForm): class PageEditSlugForm(forms.ModelForm): class Meta: model = Page - fields = ('slug',) + fields = ('slug', 'sub_slug') def clean_slug(self): value = self.cleaned_data.get('slug') diff --git a/combo/public/static/js/combo.public.js b/combo/public/static/js/combo.public.js index 25d0191..7dad6cf 100644 --- a/combo/public/static/js/combo.public.js +++ b/combo/public/static/js/combo.public.js @@ -1,8 +1,18 @@ function combo_load_cell(elem) { var $elem = $(elem); var url = $elem.data('ajax-cell-url'); + var extra_context = $elem.data('extra-context'); $.support.cors = true; /* IE9 */ - $.ajax({url: url + window.location.search, + var qs; + if (window.location.search) { + qs = window.location.search + '&'; + } else { + qs = '?'; + } + if (extra_context) { + qs += 'ctx=' + extra_context; + } + $.ajax({url: url + qs, xhrFields: { withCredentials: true }, async: true, dataType: 'html', diff --git a/combo/public/templates/combo/placeholder.html b/combo/public/templates/combo/placeholder.html index 503e729..1c6ddf2 100644 --- a/combo/public/templates/combo/placeholder.html +++ b/combo/public/templates/combo/placeholder.html @@ -5,9 +5,9 @@ {% if cell.slug %}id="{{ 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="{{ cell.loading_message }}" - {% if cell.ajax_refresh %} - data-ajax-cell-refresh="{{ cell.ajax_refresh }}" - {% endif %}>
{% render_cell cell %}
+ {% if cell.ajax_refresh %}data-ajax-cell-refresh="{{ cell.ajax_refresh }}"{% endif %} + {% if request.extra_context_data %}data-extra-context="{{ request.extra_context_data|signed|urlencode }}"{% endif %} + >
{% render_cell cell %}
{% endfor %} {% if render_skeleton %} {{ skeleton }} diff --git a/combo/public/templatetags/combo.py b/combo/public/templatetags/combo.py index 55d9ad2..72e2ac9 100644 --- a/combo/public/templatetags/combo.py +++ b/combo/public/templatetags/combo.py @@ -19,6 +19,7 @@ from __future__ import absolute_import import datetime from django import template +from django.core import signing from django.core.exceptions import PermissionDenied from django.template.base import TOKEN_BLOCK, TOKEN_VAR from django.template.defaultfilters import stringfilter @@ -224,3 +225,7 @@ def is_empty_placeholder(page, placeholder_name): @register.filter(name='list') def as_list(obj): return list(obj) + +@register.filter +def signed(obj): + return signing.dumps(obj) diff --git a/combo/public/views.py b/combo/public/views.py index 6d3bdb8..c5ce9c5 100644 --- a/combo/public/views.py +++ b/combo/public/views.py @@ -15,12 +15,15 @@ # along with this program. If not, see . import json +import re import django from django.conf import settings from django.contrib import messages from django.contrib.auth import logout as auth_logout from django.contrib.auth import views as auth_views +from django.contrib.auth.models import User +from django.core import signing from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction from django.http import (Http404, HttpResponse, HttpResponseRedirect, @@ -42,8 +45,10 @@ from haystack.query import SearchQuerySet, SQ if 'mellon' in settings.INSTALLED_APPS: from mellon.utils import get_idps + from mellon.models import UserSAMLIdentifier else: get_idps = lambda: [] + UserSAMLIdentifier = None from combo.data.models import (CellBase, PostException, Page, Redirect, ParentContentCell, TextCell, PageSnapshot) @@ -70,6 +75,12 @@ def logout(request, next_page=None): next_page = '/' return HttpResponseRedirect(next_page) +def modify_global_context(request, ctx): + if 'user_id' in ctx: + ctx['selected_user'] = User.objects.get(id=ctx['user_id']) + if 'name_id' in ctx and UserSAMLIdentifier: + ctx['selected_user'] = UserSAMLIdentifier.objects.get(name_id=ctx['name_id']).user + @csrf_exempt def ajax_page_cell(request, page_pk, cell_reference): try: @@ -121,6 +132,9 @@ def render_cell(request, cell): 'synchronous': True, 'site_base': request.build_absolute_uri('/')[:-1], } + if request.GET.get('ctx'): + context.update(signing.loads(request.GET['ctx'])) + modify_global_context(request, context) if cell.page_id: other_cells = [] @@ -327,6 +341,7 @@ def empty_site(request): def page(request): + request.extra_context_data = {} url = request.path_info parts = [x for x in request.path_info.strip('/').split('/') if x] if len(parts) == 1 and parts[0] == 'index': @@ -350,30 +365,49 @@ def page(request): request.session['visited'] = True return HttpResponseRedirect(settings.COMBO_WELCOME_PAGE_PATH) - slugs = {'parent__'*len(parts) + 'isnull': True} - for i, part in enumerate(reversed(parts)): - slugs['parent__'*i + 'slug'] = part - try: - page = Page.objects.get(**slugs) - except Page.DoesNotExist: - if Page.objects.count() == 0 and parts == ['index']: - return empty_site(request) - # maybe the page is a children of /index/, as /index/ is silent the - # page would appear directly under /; this is not a suggested practice. - parts = ['index'] + parts - slugs = {'parent__'*len(parts) + 'isnull': True} - for i, part in enumerate(reversed(parts)): - slugs['parent__'*i + 'slug'] = part + pages = {x.slug: x for x in Page.objects.filter(slug__in=parts)} + if pages == {} and parts == ['index'] and Page.objects.count() == 0: + return empty_site(request) + + i = 0 + hierarchy_ids = [None] + while i < len(parts): try: - page = Page.objects.get(**slugs) - except Page.DoesNotExist: + page = pages[parts[i]] + except KeyError: page = None - - if page is None or page.get_online_url() != url: - if not url.endswith('/') and settings.APPEND_SLASH: - # this is useful to allow /login, /manage, and other non-page - # URLs to work. - return HttpResponsePermanentRedirect(url + '/') + break + if page.parent_id != hierarchy_ids[-1]: + if i == 0: + # root page should be at root but maybe the page is a child of + # /index/, and as /index/ is silent the page would appear + # directly under /; this is not a suggested practice. + if page.parent.slug != 'index' and page.parent.parent_id is not None: + page = None + break + else: + page = None + break + if page.sub_slug: + if parts[i+1:] == []: + # a sub slug is expected but was not found + page = None + break + match = re.match(page.sub_slug, parts[i+1]) + if match is None or match.groups() != (parts[i+1],): + page = None + break + request.extra_context_data.update(match.groupdict()) + parts = parts[:i+1] + parts[i+2:] # skip variable component + i += 1 + hierarchy_ids.append(page.id) + + if not url.endswith('/') and settings.APPEND_SLASH: + # this is useful to allow /login, /manage, and other non-page + # URLs to work. + return HttpResponsePermanentRedirect(url + '/') + + if page is None: redirect = Redirect.objects.filter(old_url=url).last() if redirect: return HttpResponseRedirect(redirect.page.get_online_url()) @@ -405,6 +439,8 @@ def publish_page(request, page, status=200, template_name=None): 'request': request, 'media': sum((cell.media for cell in cells), Media()) } + ctx.update(getattr(request, 'extra_context_data', {})) + modify_global_context(request, ctx) for cell in cells: if cell.modify_global_context: diff --git a/tests/test_public.py b/tests/test_public.py index cdff58c..f0b3fb6 100644 --- a/tests/test_public.py +++ b/tests/test_public.py @@ -4,9 +4,11 @@ from webtest import TestApp import datetime import json import pytest +import re import os from django.conf import settings +from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.db import connection from django.utils.http import quote @@ -16,7 +18,7 @@ from django.test.utils import CaptureQueriesContext from combo.wsgi import application from combo.data.models import (Page, CellBase, TextCell, ParentContentCell, - FeedCell, LinkCell, ConfigJsonCell, Redirect) + FeedCell, LinkCell, ConfigJsonCell, Redirect, JsonCell) from combo.apps.family.models import FamilyInfosCell pytestmark = pytest.mark.django_db @@ -628,3 +630,54 @@ def test_redirects(app): page3.save() assert urlparse.urlparse(app.get('/second/third/', status=302).location).path == '/third2/' assert urlparse.urlparse(app.get('/second2/third2/', status=302).location).path == '/third2/' + +def test_sub_slug(app, john_doe, jane_doe): + Page.objects.all().delete() + page = Page(title='Home', slug='index', template_name='standard') + page.save() + + page2 = Page(title='User', slug='users', sub_slug='(?P[a-z]+)', template_name='standard') + page2.save() + + page3 = Page(title='Blah', slug='blah', parent=page2, template_name='standard') + page3.save() + + # without passing sub slug + app.get('/users/', status=404) + + # json cell so we can display the parameter value + cell = JsonCell(page=page2, url='http://example.net', order=0, placeholder='content') + cell.template_string = 'XX{{ blah }}YY' + cell.save() + + cell2 = JsonCell(page=page3, url='http://example.net', order=0, placeholder='content') + cell2.template_string = 'AA{{ blah }}BB' + cell2.save() + + with mock.patch('combo.utils.requests.get') as requests_get: + data = {'data': [{'url': 'http://a.b', 'text': 'xxx'}]} + requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=200) + resp = app.get('/users/whatever/', status=200) + assert 'XXwhateverYY' in resp.text + cell_url = re.findall(r'data-ajax-cell-url="(.*)"', resp.text)[0] + extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)[0] + resp = app.get(cell_url + '?ctx=' + extra_ctx) + assert resp.text == 'XXwhateverYY' + + # 404 on value that doesn't match the regex + resp = app.get('/users/WHATEVER/', status=404) + + # check sub page + resp = app.get('/users/whatever/plop/', status=404) + resp = app.get('/users/whatever/blah/', status=200) + assert 'AAwhateverBB' in resp.text + + # custom behaviour for , it will add the user to context + page2.sub_slug = '(?P[0-9]+)' + page2.save() + + cell.template_string = 'XX{{ selected_user.username }}YY' + cell.save() + + resp = app.get('/users/%s/' % john_doe.id, status=200) + assert 'XXjohn.doeYY' in resp.text -- 2.18.0