general: handle /manage/ access to users with page edit roles (#56188)
gitea-wip/combo/pipeline/head There was a failure building this commit Details
gitea/combo/pipeline/head Build started... Details

This commit is contained in:
Frédéric Péters 2021-08-09 15:55:37 +02:00
parent af5caa199a
commit 69425c6200
14 changed files with 534 additions and 224 deletions

View File

@ -16,7 +16,7 @@
from django.conf.urls import include, url
from combo.urls_utils import decorated_includes, manager_required
from combo.urls_utils import decorated_includes, staff_required
from . import api_views, views
@ -34,7 +34,7 @@ assets_manager_urls = [
urlpatterns = [
url(r'^assets/(?P<key>[\w_:-]+)$', views.serve_asset),
url(r'^manage/assets/', decorated_includes(manager_required, include(assets_manager_urls))),
url(r'^manage/assets/', decorated_includes(staff_required, include(assets_manager_urls))),
url(r'^api/assets/set/(?P<key>[\w_:-]+)/$', api_views.view_set, name='api-assets-set'),
url(r'^ajax/assets-export-size/$', views.assets_export_size, name='combo-manager-assets-export-size'),
]

View File

@ -16,7 +16,7 @@
from django.conf.urls import include, url
from combo.urls_utils import decorated_includes, manager_required
from combo.urls_utils import decorated_includes, staff_required
from .manager_views import (
BasketItemErrorListView,
@ -110,7 +110,7 @@ urlpatterns = [
ReturnView.as_view(),
name='lingo-return-payment-backend',
),
url(r'^manage/lingo/', decorated_includes(manager_required, include(lingo_manager_urls))),
url(r'^manage/lingo/', decorated_includes(staff_required, include(lingo_manager_urls))),
url(
r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_crypto_id>[\w,-]+)/pdf$',
ItemDownloadView.as_view(),

View File

@ -16,7 +16,7 @@
from django.conf.urls import include, url
from combo.urls_utils import decorated_includes, manager_required
from combo.urls_utils import decorated_includes, staff_required
from . import manager_views
from .views import GeojsonView
@ -56,7 +56,7 @@ maps_manager_urls = [
]
urlpatterns = [
url(r'^manage/maps/', decorated_includes(manager_required, include(maps_manager_urls))),
url(r'^manage/maps/', decorated_includes(staff_required, include(maps_manager_urls))),
url(
r'^ajax/mapcell/geojson/(?P<cell_id>\d+)/(?P<layer_slug>[\w-]+)/$',
GeojsonView.as_view(),

View File

@ -16,7 +16,7 @@
from django.conf.urls import include, url
from combo.urls_utils import decorated_includes, manager_required
from combo.urls_utils import decorated_includes, staff_required
from .manager_views import (
ManagerAddNavigationEntry,
@ -55,5 +55,5 @@ urlpatterns = [
url('^service-worker-registration.js$', service_worker_registration_js),
url('^api/pwa/push/subscribe$', subscribe_push, name='pwa-subscribe-push'),
url('^__pwa__/offline/$', offline_page),
url(r'^manage/pwa/', decorated_includes(manager_required, include(pwa_manager_urls))),
url(r'^manage/pwa/', decorated_includes(staff_required, include(pwa_manager_urls))),
]

View File

@ -18,6 +18,7 @@ from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from django.views.generic import View
from combo.apps.search.forms import (
CardsEngineSettingsForm,
@ -29,138 +30,180 @@ from combo.apps.search.forms import (
)
from combo.apps.search.models import SearchCell
from combo.data.models import PageSnapshot
from combo.manager.views import ManagedPageMixin
from combo.profile import default_description_template
def page_search_cell_add_engine(request, page_pk, cell_reference, engine_slug):
cell = get_object_or_404(SearchCell, pk=cell_reference.split('-')[1], page=page_pk)
class PageSearchCellAddEngine(ManagedPageMixin, View):
def get(self, *args, **kwargs):
return self.post(*args, **kwargs)
def add_slug(slug, **options):
if slug in cell.available_engines or slug.startswith('_text_page') or slug.startswith('cards:'):
if not cell._search_services or not cell._search_services.get('data'):
cell._search_services = {'data': []}
if not cell._search_services.get('options'):
cell._search_services['options'] = {}
cell._search_services['data'].append(slug)
cell._search_services['options'][slug] = options
def post(self, *args, **kwargs):
request = self.request
page_pk = self.kwargs['page_pk']
cell_reference = self.kwargs['cell_reference']
engine_slug = self.kwargs['engine_slug']
cell = get_object_or_404(SearchCell, pk=cell_reference.split('-')[1], page=page_pk)
def add_slug(slug, **options):
if slug in cell.available_engines or slug.startswith('_text_page') or slug.startswith('cards:'):
if not cell._search_services or not cell._search_services.get('data'):
cell._search_services = {'data': []}
if not cell._search_services.get('options'):
cell._search_services['options'] = {}
cell._search_services['data'].append(slug)
cell._search_services['options'][slug] = options
cell.save()
PageSnapshot.take(cell.page, request=request, comment=_('changed cell "%s"') % cell)
return HttpResponseRedirect(
'%s#cell-%s' % (reverse('combo-manager-page-view', kwargs={'pk': page_pk}), cell_reference)
)
if engine_slug not in ['_text', 'users'] and not engine_slug.startswith('cards:'):
# add engine without intermediary form and popup
return add_slug(engine_slug)
form_class = TextEngineSettingsForm
if engine_slug == 'users':
form_class = UsersEngineSettingsForm
if engine_slug.startswith('cards:'):
form_class = CardsEngineSettingsForm
if request.method == 'POST':
form = form_class(instance=cell, data=request.POST, engine_slug=engine_slug)
if form.is_valid():
kwargs = {
'title': form.cleaned_data['title'],
}
if form.cleaned_data.get('description_template'):
kwargs['description_template'] = form.cleaned_data['description_template']
for key in ['without_user', 'with_description']:
if form.cleaned_data.get(key):
kwargs[key] = True
return add_slug(form.get_slug(), **kwargs)
else:
form = form_class(instance=cell, engine_slug=engine_slug)
context = {
'form': form,
'cell': cell,
}
return render(request, 'combo/manager/engine-form.html', context)
page_search_cell_add_engine = PageSearchCellAddEngine.as_view()
class PageSearchCellUpdateEngine(ManagedPageMixin, View):
def get(self, *args, **kwargs):
return self.post(*args, **kwargs)
def post(self, *args, **kwargs):
request = self.request
page_pk = self.kwargs['page_pk']
cell_reference = self.kwargs['cell_reference']
engine_slug = self.kwargs['engine_slug']
cell = get_object_or_404(SearchCell, pk=cell_reference.split('-')[1], page=page_pk)
form_class = TextEngineSettingsUpdateForm
if engine_slug == 'users':
form_class = UsersEngineSettingsUpdateForm
if engine_slug.startswith('cards:'):
form_class = CardsEngineSettingsUpdateForm
if request.method == 'POST':
form = form_class(instance=cell, data=request.POST, engine_slug=engine_slug)
if form.is_valid():
kwargs = {
'title': form.cleaned_data['title'],
}
if form.cleaned_data.get('description_template'):
kwargs['description_template'] = form.cleaned_data['description_template']
for key in ['without_user', 'with_description']:
if form.cleaned_data.get(key):
kwargs[key] = True
if not cell._search_services.get('options'):
cell._search_services['options'] = {}
cell._search_services['options'][engine_slug] = kwargs
cell.save()
PageSnapshot.take(cell.page, request=request, comment=_('changed cell "%s"') % cell)
return HttpResponseRedirect(
'%s#cell-%s'
% (reverse('combo-manager-page-view', kwargs={'pk': page_pk}), cell_reference)
)
else:
initial = {
'title': cell._search_services.get('options', {}).get(engine_slug, {}).get('title'),
'without_user': cell._search_services.get('options', {})
.get(engine_slug, {})
.get('without_user'),
'with_description': cell._search_services.get('options', {})
.get(engine_slug, {})
.get('with_description'),
'description_template': cell._search_services.get('options', {})
.get(engine_slug, {})
.get('description_template')
or default_description_template,
}
form = form_class(instance=cell, engine_slug=engine_slug, initial=initial)
context = {
'form': form,
'cell': cell,
}
return render(request, 'combo/manager/engine-form.html', context)
page_search_cell_update_engine = PageSearchCellUpdateEngine.as_view()
class PageSearchCellDeleteEngine(ManagedPageMixin, View):
def get(self, *args, **kwargs):
request = self.request
page_pk = self.kwargs['page_pk']
cell_reference = self.kwargs['cell_reference']
engine_slug = self.kwargs['engine_slug']
cell = get_object_or_404(SearchCell, pk=cell_reference.split('-')[1], page=page_pk)
if engine_slug in cell._search_services.get('data'):
cell._search_services['data'].remove(engine_slug)
cell.save()
PageSnapshot.take(cell.page, request=request, comment=_('changed cell "%s"') % cell)
return HttpResponseRedirect(
'%s#cell-%s' % (reverse('combo-manager-page-view', kwargs={'pk': page_pk}), cell_reference)
)
if engine_slug not in ['_text', 'users'] and not engine_slug.startswith('cards:'):
# add engine without intermediary form and popup
return add_slug(engine_slug)
form_class = TextEngineSettingsForm
if engine_slug == 'users':
form_class = UsersEngineSettingsForm
if engine_slug.startswith('cards:'):
form_class = CardsEngineSettingsForm
if request.method == 'POST':
form = form_class(instance=cell, data=request.POST, engine_slug=engine_slug)
if form.is_valid():
kwargs = {
'title': form.cleaned_data['title'],
}
if form.cleaned_data.get('description_template'):
kwargs['description_template'] = form.cleaned_data['description_template']
for key in ['without_user', 'with_description']:
if form.cleaned_data.get(key):
kwargs[key] = True
return add_slug(form.get_slug(), **kwargs)
else:
form = form_class(instance=cell, engine_slug=engine_slug)
context = {
'form': form,
'cell': cell,
}
return render(request, 'combo/manager/engine-form.html', context)
page_search_cell_delete_engine = PageSearchCellDeleteEngine.as_view()
def page_search_cell_update_engine(request, page_pk, cell_reference, engine_slug):
cell = get_object_or_404(SearchCell, pk=cell_reference.split('-')[1], page=page_pk)
class PageSearchEnginesOrder(ManagedPageMixin, View):
def get(self, *args, **kwargs):
request = self.request
page_pk = self.kwargs['page_pk']
cell_reference = self.kwargs['cell_reference']
cell = get_object_or_404(SearchCell, pk=cell_reference.split('-')[1], page=page_pk)
form_class = TextEngineSettingsUpdateForm
if engine_slug == 'users':
form_class = UsersEngineSettingsUpdateForm
if engine_slug.startswith('cards:'):
form_class = CardsEngineSettingsUpdateForm
if not cell._search_services.get('data'):
return HttpResponse(status=204)
if request.method == 'POST':
form = form_class(instance=cell, data=request.POST, engine_slug=engine_slug)
if form.is_valid():
kwargs = {
'title': form.cleaned_data['title'],
}
if form.cleaned_data.get('description_template'):
kwargs['description_template'] = form.cleaned_data['description_template']
for key in ['without_user', 'with_description']:
if form.cleaned_data.get(key):
kwargs[key] = True
engines = []
for engine_slug in cell._search_services['data']:
try:
new_order = int(request.GET.get('pos_' + str(engine_slug)))
except TypeError:
new_order = 0
engines.append((new_order, engine_slug))
if not cell._search_services.get('options'):
cell._search_services['options'] = {}
cell._search_services['options'][engine_slug] = kwargs
ordered_engines = [a[1] for a in sorted(engines, key=lambda a: a[0])]
if ordered_engines != cell._search_services['data']:
cell._search_services['data'] = ordered_engines
cell.save()
PageSnapshot.take(cell.page, request=request, comment=_('changed cell "%s"') % cell)
return HttpResponseRedirect(
'%s#cell-%s' % (reverse('combo-manager-page-view', kwargs={'pk': page_pk}), cell_reference)
)
else:
initial = {
'title': cell._search_services.get('options', {}).get(engine_slug, {}).get('title'),
'without_user': cell._search_services.get('options', {}).get(engine_slug, {}).get('without_user'),
'with_description': cell._search_services.get('options', {})
.get(engine_slug, {})
.get('with_description'),
'description_template': cell._search_services.get('options', {})
.get(engine_slug, {})
.get('description_template')
or default_description_template,
}
form = form_class(instance=cell, engine_slug=engine_slug, initial=initial)
context = {
'form': form,
'cell': cell,
}
return render(request, 'combo/manager/engine-form.html', context)
def page_search_cell_delete_engine(request, page_pk, cell_reference, engine_slug):
cell = get_object_or_404(SearchCell, pk=cell_reference.split('-')[1], page=page_pk)
if engine_slug in cell._search_services.get('data'):
cell._search_services['data'].remove(engine_slug)
cell.save()
PageSnapshot.take(cell.page, request=request, comment=_('changed cell "%s"') % cell)
return HttpResponseRedirect(
'%s#cell-%s' % (reverse('combo-manager-page-view', kwargs={'pk': page_pk}), cell_reference)
)
def search_engines_order(request, page_pk, cell_reference):
cell = get_object_or_404(SearchCell, pk=cell_reference.split('-')[1], page=page_pk)
if not cell._search_services.get('data'):
return HttpResponse(status=204)
engines = []
for engine_slug in cell._search_services['data']:
try:
new_order = int(request.GET.get('pos_' + str(engine_slug)))
except TypeError:
new_order = 0
engines.append((new_order, engine_slug))
ordered_engines = [a[1] for a in sorted(engines, key=lambda a: a[0])]
if ordered_engines != cell._search_services['data']:
cell._search_services['data'] = ordered_engines
cell.save()
PageSnapshot.take(cell.page, request=request, comment=_('changed cell "%s"') % cell)
return HttpResponse(status=204)
search_engines_order = PageSearchEnginesOrder.as_view()

View File

@ -14,7 +14,6 @@
# 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/>.
import collections
import copy
import datetime
import hashlib
@ -354,6 +353,18 @@ class Page(models.Model):
return True
return False
def is_editable(self, user):
if user.is_staff:
return True
group_ids = [x.id for x in user.groups.all()]
if self.edit_role_id in group_ids:
return True
hierarchy = self.get_parents_and_self()
for page in hierarchy:
if page.subpages_edit_role_id in group_ids:
return True
return False
def get_placeholders(self, request, traverse_cells=False, template_name=None):
placeholders = []
@ -382,8 +393,8 @@ class Page(models.Model):
tmpl.render(context, request)
return placeholders
def get_next_page(self, user=None, check_visibility=True):
pages = Page.get_as_reordered_flat_hierarchy(Page.objects.all())
def get_next_page(self, user=None, check_visibility=True, **kwargs):
pages = Page.get_as_reordered_flat_hierarchy(Page.objects.all(), **kwargs)
this_page = [x for x in pages if x.id == self.id][0]
pages = pages[pages.index(this_page) + 1 :]
for page in pages:
@ -391,8 +402,8 @@ class Page(models.Model):
return page
return None
def get_previous_page(self, user=None, check_visibility=True):
pages = Page.get_as_reordered_flat_hierarchy(Page.objects.all())
def get_previous_page(self, user=None, check_visibility=True, **kwargs):
pages = Page.get_as_reordered_flat_hierarchy(Page.objects.all(), **kwargs)
pages.reverse()
this_page = [x for x in pages if x.id == self.id][0]
pages = pages[pages.index(this_page) + 1 :]
@ -402,23 +413,56 @@ class Page(models.Model):
return None
@classmethod
def get_as_reordered_flat_hierarchy(cls, object_list, root_page=None):
reordered = []
parenting = collections.defaultdict(list)
def get_as_reordered_flat_hierarchy(cls, object_list, root_page=None, follow_user_perms=None):
# create a list of [(page.order, page.id, page), (subpage.order, subpage.id, subpage),
# (subsubpage.order, subpage.id, subsubpage)] and sort it to get the page hierarchy
# as a flat list.
# follow_user_perms can be None or a User object, in that case only pages that are
# editable by user will be returned.
all_pages = {}
for page in object_list:
parenting[page.parent_id].append(page)
all_pages[page.id] = page
def fill_list(object_sublist, level=0, parent=None):
for page in object_sublist:
parent_id = parent.pk if parent else None
if page.parent_id == parent_id or page == root_page and parent is None:
page.level = level
reordered.append(page)
if page.id in parenting:
fill_list(object_sublist, level=level + 1, parent=page)
pages_hierarchy = []
for page in object_list:
page_hierarchy = [(page.order, page.id, page)]
parent_id = page.parent_id
while parent_id and parent_id in all_pages:
parent_page = all_pages[parent_id]
page_hierarchy.append((parent_page.order, parent_page.id, parent_page))
parent_id = parent_page.parent_id
page_hierarchy.reverse()
page.level = len(page_hierarchy) - 1
pages_hierarchy.append(page_hierarchy)
fill_list(object_list)
return reordered
group_ids = None # None = do not pay attention to groups
if follow_user_perms and not follow_user_perms.is_staff:
group_ids = [x.id for x in follow_user_perms.groups.all()]
pages_hierarchy.sort()
if group_ids is not None:
# remove pages the user cannot see/edit
pages_hierarchy = [
x
for x in pages_hierarchy
if x[-1][-1].edit_role_id in group_ids
or any(y[-1].subpages_edit_role_id in group_ids for y in x[:-1])
]
# adjust levels to have shallowest level at 0
seen_pages = {} # page_id -> page_level
for page_hierarchy in pages_hierarchy:
_, page_id, page = page_hierarchy[-1]
if page.parent_id in seen_pages:
# parent page is displayed, adjust level according to it
page.level = seen_pages[page.parent_id] + 1
else:
# page with no parent displayed, set it at root level
page.level = 0
seen_pages[page_id] = page.level
return [x[-1][-1] for x in pages_hierarchy]
@staticmethod
@utils.cache_during_request

View File

@ -106,6 +106,23 @@ class PageAddForm(forms.ModelForm):
return self.instance
class PageRestrictedAddForm(PageAddForm):
class Meta:
model = Page
fields = ('title', 'template_name', 'parent')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
group_ids = [x.id for x in self.request.user.groups.all()]
self.fields['parent'].queryset = Page.objects.filter(subpages_edit_role_id__in=group_ids)
self.fields['parent'].required = True
self.fields['parent'].empty_label = None
def save(self, *args, **kwargs):
self.parent = self.cleaned_data.get('parent')
return super().save(*args, **kwargs)
class PageEditTitleForm(forms.ModelForm):
class Meta:
model = Page

View File

@ -4,8 +4,13 @@
{% block appbar %}
<h2>{% trans 'Pages' %}</h2>
<span class="actions">
{% if user.is_staff %}
<a class="extra-actions-menu-opener"></a>
{% endif %}
{% if can_add_page %}
<a rel="popup" href="{% url 'combo-manager-page-add' %}">{% trans 'New' %}</a>
{% endif %}
{% if user.is_staff %}
<ul class="extra-actions-menu">
<li><a href="{% url 'combo-manager-site-export' %}" rel="popup" data-autoclose-dialog="true">{% trans 'Export Site' %}</a></li>
<li><a href="{% url 'combo-manager-site-import' %}">{% trans 'Import Site' %}</a></li>
@ -14,6 +19,7 @@
<li><a href="{{ extra_action.href }}">{{ extra_action.text }}</a></li>
{% endfor %}
</ul>
{% endif %}
</span>
{% endblock %}
@ -30,7 +36,7 @@ Use drag and drop with the ⣿ handles to reorder and change hierarchy of pages.
<div class="objects-list" id="pages-list" data-page-order-url="{% url 'combo-manager-page-order' %}">
{% for page in object_list %}
<div class="page level-{{page.level}}{% if collapse_pages %} untoggled{% endif %}" data-page-id="{{page.id}}" data-level="{{page.level}}">
<span class="handle"></span>
{% if user.is_staff %}<span class="handle"></span>{% endif %}
<span class="group1">
<a href="{% url 'combo-manager-page-view' pk=page.id %}">
{{ page.title }}

View File

@ -11,10 +11,12 @@
<ul class="extra-actions-menu">
<li><a class="action-history" href="{% url 'combo-manager-page-history' pk=object.id %}">{% trans 'History' %}</a></li>
<li><a {% if page_has_subpages %}rel="popup" data-autoclose-dialog="true" {% endif %}class="action-export" href="{% url 'combo-manager-page-export' pk=object.id %}">{% trans 'Export' %}</a></li>
{% if request.user.is_staff %}
<li><a class="action-add-child" rel="popup" href="{% url 'combo-manager-page-add-child' pk=object.id %}">{% trans 'Add a child page' %}</a></li>
<li><a class="action-edit-roles" rel="popup" href="{% url 'combo-manager-page-edit-roles' pk=object.id %}">{% trans 'Manage edit roles' %}</a></li>
<li><a rel="popup" class="action-duplicate" href="{% url 'combo-manager-page-duplicate' pk=object.id %}">{% trans 'Duplicate' %}</a></li>
<li><a class="action-delete" rel="popup" href="{% url 'combo-manager-page-delete' pk=object.id %}">{% trans 'Delete' %}</a></li>
{% endif %}
</ul>
</span>
{% endblock %}
@ -94,7 +96,7 @@
<div class="page-options navigation">
<h3>{% trans 'Navigation' %}</h3>
<ul>
{% if object.parent_id %}
{% if object.parent_id and request.user.is_staff %}
<li class="nav-up"><a href="{% url 'combo-manager-page-view' pk=object.parent_id %}">{{ object.parent.title }}</a></li>
{% endif %}
{% if previous_page %}

View File

@ -16,10 +16,10 @@
import ckeditor.views as ckeditor_views
from django.conf.urls import url
from django.contrib.admin.views.decorators import staff_member_required
from django.views.decorators.cache import never_cache
from combo.apps.assets import views as assets_views
from combo.urls_utils import staff_required
from .. import plugins
from . import views
@ -27,9 +27,13 @@ from . import views
urlpatterns = [
url(r'^$', views.homepage, name='combo-manager-homepage'),
url(r'^menu.json$', views.menu_json),
url(r'^site-export$', views.site_export, name='combo-manager-site-export'),
url(r'^site-import$', views.site_import, name='combo-manager-site-import'),
url(r'^cells/invalid-report/$', views.invalid_cell_report, name='combo-manager-invalid-cell-report'),
url(r'^site-export$', staff_required(views.site_export), name='combo-manager-site-export'),
url(r'^site-import$', staff_required(views.site_import), name='combo-manager-site-import'),
url(
r'^cells/invalid-report/$',
staff_required(views.invalid_cell_report),
name='combo-manager-invalid-cell-report',
),
url(r'^pages/add/$', views.page_add, name='combo-manager-page-add'),
url(r'^pages/(?P<pk>\d+)/$', views.page_view, name='combo-manager-page-view'),
url(
@ -59,15 +63,25 @@ urlpatterns = [
views.page_remove_picture,
name='combo-manager-page-remove-picture',
),
url(r'^pages/(?P<pk>\d+)/delete$', views.page_delete, name='combo-manager-page-delete'),
url(r'^pages/(?P<pk>\d+)/delete$', staff_required(views.page_delete), name='combo-manager-page-delete'),
url(r'^pages/(?P<pk>\d+)/export$', views.page_export, name='combo-manager-page-export'),
url(r'^pages/(?P<pk>\d+)/add/$', views.page_add_child, name='combo-manager-page-add-child'),
url(r'^pages/(?P<pk>\d+)/duplicate$', views.page_duplicate, name='combo-manager-page-duplicate'),
url(r'^pages/(?P<pk>\d+)/edit-roles/$', views.page_edit_roles, name='combo-manager-page-edit-roles'),
url(
r'^pages/(?P<pk>\d+)/add/$', staff_required(views.page_add_child), name='combo-manager-page-add-child'
),
url(
r'^pages/(?P<pk>\d+)/duplicate$',
staff_required(views.page_duplicate),
name='combo-manager-page-duplicate',
),
url(
r'^pages/(?P<pk>\d+)/edit-roles/$',
staff_required(views.page_edit_roles),
name='combo-manager-page-edit-roles',
),
url(r'^pages/(?P<pk>\d+)/history$', views.page_history, name='combo-manager-page-history'),
url(
r'^pages/(?P<page_pk>\d+)/history/(?P<pk>\d+)/$',
views.snapshot_restore,
staff_required(views.snapshot_restore),
name='combo-manager-snapshot-restore',
),
url(
@ -127,10 +141,8 @@ urlpatterns = [
),
url(r'^pages/(?P<page_pk>\d+)/order$', views.cell_order, name='combo-manager-cell-order'),
url(r'^pages/order$', views.page_order, name='combo-manager-page-order'),
url(r'^ckeditor/upload/', staff_member_required(ckeditor_views.upload), name='ckeditor_upload'),
url(
r'^ckeditor/browse/', never_cache(staff_member_required(assets_views.browse)), name='ckeditor_browse'
),
url(r'^ckeditor/upload/', ckeditor_views.upload, name='ckeditor_upload'),
url(r'^ckeditor/browse/', never_cache(assets_views.browse), name='ckeditor_browse'),
]
urlpatterns = plugins.register_plugins_manager_urls(urlpatterns)

View File

@ -22,7 +22,7 @@ from operator import attrgetter
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
@ -41,12 +41,14 @@ from django.views.generic import (
ListView,
RedirectView,
UpdateView,
View,
)
from combo import plugins
from combo.data.library import get_cell_class
from combo.data.models import CellBase, LinkListCell, Page, PageSnapshot, ParentContentCell
from combo.data.utils import ImportSiteError, export_site, export_site_tar, import_site, import_site_tar
from combo.urls_utils import staff_required
from .forms import (
CellVisibilityForm,
@ -60,6 +62,7 @@ from .forms import (
PageEditSlugForm,
PageEditTitleForm,
PageExportForm,
PageRestrictedAddForm,
PageSelectTemplateForm,
PageVisibilityForm,
SiteExportForm,
@ -67,15 +70,25 @@ from .forms import (
)
def can_add_page(user):
if user.is_staff:
return True
group_ids = [x.id for x in user.groups.all()]
return bool(Page.objects.filter(subpages_edit_role_id__in=group_ids).exists())
class HomepageView(ListView):
model = Page
template_name = 'combo/manager_home.html'
def get_context_data(self, **kwargs):
self.object_list = Page.get_as_reordered_flat_hierarchy(self.object_list)
self.object_list = Page.get_as_reordered_flat_hierarchy(
self.object_list, follow_user_perms=self.request.user
)
context = super().get_context_data(**kwargs)
context['extra_actions'] = plugins.get_extra_manager_actions()
context['collapse_pages'] = settings.COMBO_MANAGE_HOME_COLLAPSE_PAGES
context['can_add_page'] = can_add_page(self.request.user)
return context
@ -170,7 +183,13 @@ def invalid_cell_report(request):
class PageAddView(CreateView):
model = Page
template_name = 'combo/page_add.html'
form_class = PageAddForm
def get_form_class(self):
if self.request.user.is_staff:
return PageAddForm
elif can_add_page(self.request.user):
return PageRestrictedAddForm
raise PermissionDenied()
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
@ -190,7 +209,15 @@ class PageAddView(CreateView):
page_add = PageAddView.as_view()
class PageAddChildView(PageAddView):
class ManagedPageMixin:
def dispatch(self, request, *args, **kwargs):
self.page = get_object_or_404(Page, id=kwargs.get('page_pk') or kwargs.get('pk'))
if not self.page.is_editable(request.user):
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
class PageAddChildView(ManagedPageMixin, PageAddView):
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['parent'] = get_object_or_404(Page, pk=self.kwargs['pk'])
@ -204,7 +231,7 @@ class PageAddChildView(PageAddView):
page_add_child = PageAddChildView.as_view()
class PageEditView(UpdateView):
class PageEditView(ManagedPageMixin, UpdateView):
model = Page
template_name = 'combo/page_add.html'
comment = None
@ -316,7 +343,7 @@ class PageEditPictureView(PageEditView):
page_edit_picture = PageEditPictureView.as_view()
class PageRemovePictureView(DetailView):
class PageRemovePictureView(ManagedPageMixin, DetailView):
model = Page
def get(self, *args, **kwargs):
@ -330,7 +357,7 @@ class PageRemovePictureView(DetailView):
page_remove_picture = PageRemovePictureView.as_view()
class PageView(DetailView):
class PageView(ManagedPageMixin, DetailView):
model = Page
template_name = 'combo/page_view.html'
@ -397,8 +424,12 @@ class PageView(DetailView):
context.update(
{
'previous_page': self.object.get_previous_page(check_visibility=False),
'next_page': self.object.get_next_page(check_visibility=False),
'previous_page': self.object.get_previous_page(
check_visibility=False, follow_user_perms=self.request.user
),
'next_page': self.object.get_next_page(
check_visibility=False, follow_user_perms=self.request.user
),
}
)
@ -408,7 +439,7 @@ class PageView(DetailView):
page_view = requires_csrf_token(PageView.as_view())
class PageDeleteView(DeleteView):
class PageDeleteView(ManagedPageMixin, DeleteView):
model = Page
template_name = 'combo/delete_page.html'
@ -435,7 +466,7 @@ class PageDeleteView(DeleteView):
page_delete = PageDeleteView.as_view()
class PageExportView(FormView):
class PageExportView(ManagedPageMixin, FormView):
form_class = PageExportForm
template_name = 'combo/page_export.html'
@ -477,7 +508,7 @@ class PageExportView(FormView):
page_export = PageExportView.as_view()
class PageDuplicateView(FormView):
class PageDuplicateView(ManagedPageMixin, FormView):
form_class = PageDuplicateForm
template_name = 'combo/page_duplicate.html'
@ -506,7 +537,7 @@ class PageDuplicateView(FormView):
page_duplicate = PageDuplicateView.as_view()
class PageHistoryView(ListView):
class PageHistoryView(ManagedPageMixin, ListView):
model = PageSnapshot
template_name = 'combo/page_history.html'
paginate_by = 20
@ -519,7 +550,7 @@ class PageHistoryView(ListView):
page_history = PageHistoryView.as_view()
class SnapshotRestoreView(DetailView):
class SnapshotRestoreView(ManagedPageMixin, DetailView):
http_method_names = ['get', 'post']
model = PageSnapshot
template_name = 'combo/snapshot_restore.html'
@ -541,7 +572,7 @@ class SnapshotRestoreView(DetailView):
snapshot_restore = SnapshotRestoreView.as_view()
class PageAddCellView(RedirectView):
class PageAddCellView(ManagedPageMixin, RedirectView):
permanent = False
def get_redirect_url(self, page_pk, cell_type, variant, ph_key):
@ -561,7 +592,7 @@ class PageAddCellView(RedirectView):
page_add_cell = PageAddCellView.as_view()
class PageEditCellView(UpdateView):
class PageEditCellView(ManagedPageMixin, UpdateView):
def get_template_names(self):
return [self.template_name or self.object.manager_form_template]
@ -609,7 +640,7 @@ class PageEditCellView(UpdateView):
page_edit_cell = PageEditCellView.as_view()
class PageDeleteCellView(DeleteView):
class PageDeleteCellView(ManagedPageMixin, DeleteView):
template_name = 'combo/generic_confirm_delete.html'
def get_object(self, queryset=None):
@ -635,7 +666,7 @@ class PageDeleteCellView(DeleteView):
page_delete_cell = PageDeleteCellView.as_view()
class PageDuplicateCellView(RedirectView):
class PageDuplicateCellView(ManagedPageMixin, RedirectView):
permanent = False
def get_redirect_url(self, page_pk, cell_reference):
@ -672,30 +703,37 @@ class PageCellOptionsView(PageEditCellView):
page_cell_options = PageCellOptionsView.as_view()
def cell_order(request, page_pk):
has_changes = False
for cell in CellBase.get_cells(page_id=page_pk):
old_order = cell.order
old_placeholder = cell.placeholder
key_suffix = cell.get_reference()
try:
new_order = int(request.GET.get('pos_' + key_suffix))
except TypeError:
# the cell is not present in the query string, most probably
# because it's in a different placeholder
continue
new_placeholder = request.GET.get('ph_' + key_suffix)
if new_order != old_order or new_placeholder != old_placeholder:
cell.order = new_order
cell.placeholder = new_placeholder
has_changes = True
cell.save(update_fields=['order', 'placeholder'])
if has_changes:
page = Page.objects.get(id=page_pk)
PageSnapshot.take(page, request=request, comment=_('reordered cells'))
return HttpResponse(status=204)
class PageCellOrder(ManagedPageMixin, View):
def get(self, *args, **kwargs):
request = self.request
page_pk = self.kwargs['page_pk']
has_changes = False
for cell in CellBase.get_cells(page_id=page_pk):
old_order = cell.order
old_placeholder = cell.placeholder
key_suffix = cell.get_reference()
try:
new_order = int(request.GET.get('pos_' + key_suffix))
except TypeError:
# the cell is not present in the query string, most probably
# because it's in a different placeholder
continue
new_placeholder = request.GET.get('ph_' + key_suffix)
if new_order != old_order or new_placeholder != old_placeholder:
cell.order = new_order
cell.placeholder = new_placeholder
has_changes = True
cell.save(update_fields=['order', 'placeholder'])
if has_changes:
page = Page.objects.get(id=page_pk)
PageSnapshot.take(page, request=request, comment=_('reordered cells'))
return HttpResponse(status=204)
cell_order = PageCellOrder.as_view()
@staff_required
def page_order(request):
new_order = [int(x) for x in request.GET['new-order'].split(',')]
moved_page = Page.objects.get(id=request.GET['moved-page-id'])
@ -782,7 +820,7 @@ def menu_json(request):
return response
class PageListCellAddLinkView(CreateView):
class PageListCellAddLinkView(ManagedPageMixin, CreateView):
template_name = 'combo/link_cell_form.html'
def dispatch(self, request, *args, **kwargs):
@ -827,7 +865,7 @@ class PageListCellAddLinkView(CreateView):
page_list_cell_add_link = PageListCellAddLinkView.as_view()
class PageListCellEditLinkView(UpdateView):
class PageListCellEditLinkView(ManagedPageMixin, UpdateView):
template_name = 'combo/link_cell_form.html'
def dispatch(self, request, *args, **kwargs):
@ -870,7 +908,7 @@ class PageListCellEditLinkView(UpdateView):
page_list_cell_edit_link = PageListCellEditLinkView.as_view()
class PageListCellDeleteLinkView(DeleteView):
class PageListCellDeleteLinkView(ManagedPageMixin, DeleteView):
template_name = 'combo/generic_confirm_delete.html'
def dispatch(self, request, *args, **kwargs):
@ -906,25 +944,32 @@ class PageListCellDeleteLinkView(DeleteView):
page_list_cell_delete_link = PageListCellDeleteLinkView.as_view()
def link_list_order(request, page_pk, cell_reference):
try:
cell = CellBase.get_cell(cell_reference, page=page_pk)
except LinkListCell.DoesNotExist:
raise Http404
has_changes = False
for link in cell.get_items():
old_order = link.order
class LinkListOrder(ManagedPageMixin, View):
def get(self, *args, **kwargs):
request = self.request
page_pk = self.kwargs['page_pk']
cell_reference = self.kwargs['cell_reference']
try:
new_order = int(request.GET.get('pos_' + str(link.pk)))
except TypeError:
continue
if new_order != old_order:
link.order = new_order
has_changes = True
link.save(update_fields=['order'])
cell = CellBase.get_cell(cell_reference, page=page_pk)
except LinkListCell.DoesNotExist:
raise Http404
if has_changes:
PageSnapshot.take(cell.page, request=request, comment=_('reordered cells'))
has_changes = False
for link in cell.get_items():
old_order = link.order
try:
new_order = int(request.GET.get('pos_' + str(link.pk)))
except TypeError:
continue
if new_order != old_order:
link.order = new_order
has_changes = True
link.save(update_fields=['order'])
return HttpResponse(status=204)
if has_changes:
PageSnapshot.take(cell.page, request=request, comment=_('reordered cells'))
return HttpResponse(status=204)
link_list_order = LinkListOrder.as_view()

View File

@ -22,6 +22,8 @@ from django.conf.urls import include, url
from django.http import Http404
from django.views.debug import technical_404_response
from .urls_utils import decorated_includes, staff_required
logger = logging.getLogger(__name__)
PLUGIN_GROUP_NAME = 'combo.plugin'
@ -75,7 +77,11 @@ def register_plugins_manager_urls(urlpatterns):
urls = get_plugin_includes(plugin, 'get_after_manager_urls')
if urls:
post_urls.append(urls)
return pre_urls + urlpatterns + post_urls
return (
[url('', decorated_includes(staff_required, include(pre_urls)))]
+ urlpatterns
+ [url('', decorated_includes(staff_required, include(post_urls)))]
)
def get_extra_manager_actions():

View File

@ -19,6 +19,7 @@
import django
from django.contrib.auth.decorators import user_passes_test
from django.core.exceptions import PermissionDenied
from django.db.models import Q
if django.VERSION < (2, 0, 0):
from django.urls.resolvers import RegexURLPattern as URLPattern # pylint: disable=no-name-in-module
@ -61,6 +62,13 @@ def manager_required(function=None, login_url=None):
if user and user.is_staff:
return True
if user and not user.is_anonymous:
from combo.data.models import Page
group_ids = [x.id for x in user.groups.all()]
if Page.objects.filter(
Q(edit_role_id__in=group_ids) | Q(subpages_edit_role_id__in=group_ids)
).exists():
return True
raise PermissionDenied()
# As the last resort, show the login form
return False
@ -69,3 +77,18 @@ def manager_required(function=None, login_url=None):
if function:
return actual_decorator(function)
return actual_decorator
def staff_required(function=None, login_url=None):
def check_staff(user):
if user and user.is_staff:
return True
if user and not user.is_anonymous:
raise PermissionDenied()
# As the last resort, show the login form
return False
actual_decorator = user_passes_test(check_staff, login_url=login_url)
if function:
return actual_decorator(function)
return actual_decorator

View File

@ -23,6 +23,7 @@ from django.utils.encoding import force_bytes, force_str
from django.utils.http import urlencode
from django.utils.six import BytesIO
from django.utils.timezone import now
from pyquery import PyQuery
from webtest import Upload
from combo.apps.assets.models import Asset
@ -613,7 +614,7 @@ def test_edit_page_num_queries(settings, app, admin_user):
app.get('/manage/pages/%s/' % page.pk) # load once to populate caches
with CaptureQueriesContext(connection) as ctx:
app.get('/manage/pages/%s/' % page.pk)
assert len(ctx.captured_queries) == 32
assert len(ctx.captured_queries) == 33
def test_delete_page(app, admin_user):
@ -848,10 +849,7 @@ def test_export_page_order():
page4 = Page.objects.create(title='Four', slug='four', parent=page1, template_name='standard')
random_list = [page3, page4, page1, page2]
ordered_list = Page.get_as_reordered_flat_hierarchy(random_list)
assert ordered_list[0] == page1
assert ordered_list[1] == page4
assert ordered_list[2] == page2
assert ordered_list[3] == page3
assert ordered_list in ([page1, page4, page2, page3], [page1, page2, page3, page4])
def test_site_export_import_json(app, admin_user):
@ -2283,7 +2281,7 @@ def test_page_versionning(app, admin_user):
resp = resp.click('restore', index=6)
with CaptureQueriesContext(connection) as ctx:
resp = resp.form.submit().follow()
assert len(ctx.captured_queries) == 144
assert len(ctx.captured_queries) == 146
resp2 = resp.click('See online')
assert resp2.text.index('Foobar1') < resp2.text.index('Foobar2') < resp2.text.index('Foobar3')
@ -2486,3 +2484,117 @@ def test_edit_link_list_order(app, admin_user):
for i, item in enumerate(items):
item.refresh_from_db()
assert item.order == new_order[i]
def test_restricted_page_edit(app, admin_user, john_doe):
group = Group.objects.create(name='foobar')
john_doe.groups.set([group])
Page.objects.all().delete()
page1 = Page.objects.create(title='One', slug='one', parent=None, template_name='standard')
page2 = Page.objects.create(title='Two', slug='two', parent=page1, template_name='standard')
page3 = Page.objects.create(title='Three', slug='three', parent=page1, template_name='standard')
page4 = Page.objects.create(title='Four', slug='four', parent=None, template_name='standard')
app = login(app, username='john.doe', password='john.doe')
resp = app.get('/manage/', status=403)
page4.edit_role = group
page4.save()
resp = app.get('/manage/', status=200)
assert [x.attrib['href'] for x in PyQuery(resp.text).find('a[href^="/manage/pages/"]')] == [
'/manage/pages/%s/' % page4.id,
]
app.get('/manage/pages/%s/' % page1.id, status=403)
resp = app.get('/manage/pages/%s/' % page4.id, status=200)
# check all page links are ok
page_links = [x.attrib['href'] for x in PyQuery(resp.text).find('a[href^="/manage/"]')]
for target in page_links:
app.get(target, status=200)
# relogin as admin to get all links
app = login(app)
resp = app.get('/manage/pages/%s/' % page4.id, status=200)
admin_links = [x.attrib['href'] for x in PyQuery(resp.text).find('a[href^="/manage/"]')]
# back to normal user
app = login(app, username='john.doe', password='john.doe')
only_admin_links = [x for x in admin_links if x not in page_links]
for target in only_admin_links:
app.get(target, status=403)
# check some important pages are correctly limited to admins
assert '/manage/pages/%s/delete' % page4.id in only_admin_links
assert '/manage/pages/%s/edit-roles/' % page4.id in only_admin_links
# check combo.apps pages are forbidden
app.get('/manage/pwa/', status=403)
app.get('/manage/maps/', status=403)
app.get('/manage/lingo/', status=403)
# check page can be modified for real, add a cell
resp = app.get('/manage/pages/%s/' % page4.id)
resp = app.get(resp.html.find('option').get('data-add-url'))
cells = CellBase.get_cells(page_id=page4.id)
assert len(cells) == 1
# check it's not possible to add a page
app.get('/manage/pages/add/', status=403)
# give access to children of page1
page1.subpages_edit_role = group
page1.save()
resp = app.get('/manage/', status=200)
assert [x.attrib['href'] for x in PyQuery(resp.text).find('a[href^="/manage/pages/"]')] == [
'/manage/pages/add/',
'/manage/pages/%s/' % page2.id,
'/manage/pages/%s/' % page3.id,
'/manage/pages/%s/' % page4.id,
]
resp = app.get('/manage/pages/%s/' % page2.id)
resp = app.get(resp.html.find('option').get('data-add-url'))
cells = CellBase.get_cells(page_id=page2.id)
assert len(cells) == 1
# add a subpage, the "new page" dialog will have an extra "parent" field.
resp = app.get('/manage/', status=200)
resp = app.get('/manage/pages/add/')
resp.forms[0]['title'].value = 'Foobar'
assert resp.forms[0]['parent'].options == [(str(page1.id), False, 'One')]
resp = resp.forms[0].submit()
page5 = Page.objects.get(slug='foobar')
assert page5.parent_id == page1.id
# check levels are adjusted on index page
resp = app.get('/manage/', status=200)
assert [
(int(x.attrib['data-page-id']), int(x.attrib['data-level']))
for x in PyQuery(resp.text).find('div.page')
] == [
(page2.id, 0),
(page3.id, 0),
(page5.id, 0),
(page4.id, 0),
]
# make page1 editable (-> visible), this will push subpages a level down, but
# the independant page4 will stay at level 0.
page1.edit_role = group
page1.save()
resp = app.get('/manage/', status=200)
assert [
(int(x.attrib['data-page-id']), int(x.attrib['data-level']))
for x in PyQuery(resp.text).find('div.page')
] == [
(page1.id, 0),
(page2.id, 1),
(page3.id, 1),
(page5.id, 1),
(page4.id, 0),
]