combo/combo/manager/views.py

1243 lines
42 KiB
Python

# combo - content management system
# Copyright (C) 2014 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/>.
import datetime
import hashlib
import json
import tarfile
from io import BytesIO
from operator import attrgetter, itemgetter
from django import forms, template
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction
from django.http import (
Http404,
HttpResponse,
HttpResponseBadRequest,
HttpResponseNotAllowed,
HttpResponseRedirect,
JsonResponse,
)
from django.shortcuts import get_object_or_404, redirect, render
from django.template import engines
from django.urls import reverse, reverse_lazy
from django.utils.encoding import force_bytes, force_str
from django.utils.formats import date_format
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt, requires_csrf_token
from django.views.generic import (
CreateView,
DeleteView,
DetailView,
FormView,
ListView,
RedirectView,
UpdateView,
View,
)
from combo import plugins
from combo.apps.wcs.utils import is_wcs_enabled
from combo.data.library import get_cell_class
from combo.data.models import CellBase, LinkListCell, Page, PageSnapshot, ParentContentCell, SiteSettings
from combo.data.utils import (
ImportSiteError,
MissingSubSlug,
export_site,
export_site_tar,
get_page_from_url_parts,
import_site,
import_site_tar,
)
from combo.urls_utils import staff_required
from combo.utils.misc import is_ajax, is_portal_agent
from .forms import (
CellDuplicateForm,
PageAddForm,
PageDuplicateForm,
PageEditDescriptionForm,
PageEditExtraVariablesFormSet,
PageEditIncludeInNavigationForm,
PageEditLinkedCardForm,
PageEditPictureForm,
PageEditRedirectionForm,
PageEditRolesForm,
PageEditSlugForm,
PageEditTitleForm,
PageExportForm,
PageRestrictedAddForm,
PageSelectTemplateForm,
PageSnapshotSaveForm,
PageVisibilityForm,
PlaceholderOptionsForm,
SiteExportForm,
SiteImportForm,
SiteSettingsForm,
build_tab_is_not_default,
)
def can_add_page(user):
if user.has_perm('data.add_page'):
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, 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
homepage = HomepageView.as_view()
class SiteExportView(FormView):
form_class = SiteExportForm
template_name = 'combo/site_export.html'
def form_valid(self, form):
today = datetime.date.today()
filename = 'site_export_%s' % today.strftime('%Y%m%d')
asset_files = form.cleaned_data.pop('asset_files')
if asset_files:
fd = BytesIO()
export_site_tar(fd, export_kwargs=form.cleaned_data)
response = HttpResponse(content=fd.getvalue(), content_type='application/x-tar')
response['Content-Disposition'] = 'attachment; filename="%s.tar"' % filename
else:
response = HttpResponse(content_type='application/json')
response['Content-Disposition'] = 'attachment; filename="%s.json"' % filename
json.dump(export_site(**form.cleaned_data), response, indent=2)
return response
site_export = SiteExportView.as_view()
class SiteImportView(FormView):
form_class = SiteImportForm
template_name = 'combo/site_import.html'
success_url = reverse_lazy('combo-manager-homepage')
def form_valid(self, form):
fd = self.request.FILES['site_file'].file
json_site = None
try:
with tarfile.open(mode='r', fileobj=fd):
pass
except tarfile.TarError:
try:
fd.seek(0)
json_site = json.loads(force_str(fd.read()))
except ValueError:
form.add_error('site_file', _('File is not in the expected TAR or JSON format.'))
return self.form_invalid(form)
else:
format = 'json'
else:
format = 'tar'
fd.seek(0)
try:
if format == 'json':
pages = import_site(json_site, request=self.request)
else:
pages = import_site_tar(fd, request=self.request)
except ImportSiteError as e:
form.add_error('site_file', force_str(e))
return self.form_invalid(form)
else:
for page in pages:
PageSnapshot.take(page, request=self.request, comment=_('imported'))
return super().form_valid(form)
site_import = SiteImportView.as_view()
def invalid_cell_report(request):
invalid_cells = CellBase.get_cells(
page__snapshot__isnull=True,
validity_info__invalid_since__isnull=False,
load_contenttypes=True,
get_all_objects=True,
)
# manual prefetch of cell pages (for ordering and placeholders)
all_pages_by_pk = {p.pk: p for p in Page.objects.filter(snapshot__isnull=True)}
for cell in invalid_cells:
cell.page = all_pages_by_pk.get(cell.page_id)
# exclude some cells on placeholder naming
invalid_cells = [c for c in invalid_cells if c.placeholder and not c.placeholder.startswith('_')]
# exclude cells with an inactive placeholder
invalid_cells = [c for c in invalid_cells if c.is_placeholder_active()]
# sort cells
invalid_cells.sort(key=attrgetter('page.order', 'page.pk', 'order'))
context = {
'object_list': invalid_cells,
}
return render(request, 'combo/invalid_cell_report.html', context)
class PageAddView(CreateView):
model = Page
template_name = 'combo/page_add.html'
def get_form_class(self):
if self.request.user.has_perm('data.add_page'):
return PageAddForm
elif can_add_page(self.request.user):
return PageRestrictedAddForm
raise PermissionDenied()
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['request'] = self.request
return kwargs
def get_initial(self):
initial = super().get_initial()
if not Page.objects.exists(): # first page
initial['title'] = _('Home')
return initial
def get_success_url(self):
return reverse('combo-manager-page-view', kwargs={'pk': self.object.id})
page_add = PageAddView.as_view()
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'])
return kwargs
def get_initial(self):
# it can not be the first page, so don't try to initialize the title
return super(CreateView, self).get_initial()
page_add_child = PageAddChildView.as_view()
class PageEditView(ManagedPageMixin, UpdateView):
model = Page
template_name = 'combo/page_add.html'
comment = None
def form_valid(self, form):
result = super().form_valid(form)
PageSnapshot.take(self.get_object(), request=self.request, comment=self.comment)
return result
def get_success_url(self):
return reverse('combo-manager-page-view', kwargs={'pk': self.object.id})
class PageSelectTemplateView(PageEditView):
form_class = PageSelectTemplateForm
@property
def comment(self):
return (
_('switched template to %s') % settings.COMBO_PUBLIC_TEMPLATES[self.object.template_name]['name']
)
def form_valid(self, form):
old_template_name = self.get_object().template_name
new_template_name = self.object.template_name
response = super().form_valid(form)
if old_template_name != new_template_name:
old_placeholder_keys = [x.key for x in self.get_object().get_placeholders(request=self.request)]
page_cells = CellBase.get_cells(page_id=self.object.id)
for placeholder in self.object.get_placeholders(
request=self.request, template_name=new_template_name
):
if not placeholder.acquired:
continue
if placeholder.key in old_placeholder_keys:
continue
if [x for x in page_cells if x.placeholder == placeholder.key]:
# skip placeholders that already have cells
continue
ParentContentCell(page=self.object, placeholder=placeholder.key, order=0).save()
return response
page_select_template = PageSelectTemplateView.as_view()
class PageEditRedirectionView(PageEditView):
form_class = PageEditRedirectionForm
comment = _('changed redirection')
page_edit_redirection = PageEditRedirectionView.as_view()
class PageEditRolesView(PageEditView):
page_title = _('Edit roles management')
form_class = PageEditRolesForm
comment = _('changed edit roles')
page_edit_roles = PageEditRolesView.as_view()
class PageEditIncludeInNavigationView(PageEditView):
form_class = PageEditIncludeInNavigationForm
comment = _('changed navigation inclusion')
page_edit_include_in_navigation = PageEditIncludeInNavigationView.as_view()
class PageEditSlugView(PageEditView):
form_class = PageEditSlugForm
comment = _('changed slug')
page_edit_slug = PageEditSlugView.as_view()
class PageEditLinkedCardView(PageEditView):
form_class = PageEditLinkedCardForm
comment = _('changed linked card')
template_name = 'combo/page_linked_card.html'
page_edit_linked_card = PageEditLinkedCardView.as_view()
class PageEditDescriptionView(PageEditView):
form_class = PageEditDescriptionForm
comment = _('changed description')
page_edit_description = PageEditDescriptionView.as_view()
class PageEditTitleView(PageEditView):
form_class = PageEditTitleForm
comment = _('changed title')
page_edit_title = PageEditTitleView.as_view()
class PageVisibilityView(PageEditView):
form_class = PageVisibilityForm
comment = _('changed visibility')
page_visibility = PageVisibilityView.as_view()
class PageEditPictureView(PageEditView):
form_class = PageEditPictureForm
comment = _('changed picture')
page_edit_picture = PageEditPictureView.as_view()
class PageRemovePictureView(ManagedPageMixin, DetailView):
model = Page
def get(self, *args, **kwargs):
page = self.get_object()
page.picture = None
page.save()
PageSnapshot.take(page, request=self.request, comment=_('removed picture'))
return HttpResponseRedirect(reverse('combo-manager-page-view', kwargs={'pk': page.id}))
page_remove_picture = PageRemovePictureView.as_view()
class PageEditExtraVariablesView(PageEditView):
form_class = PageEditExtraVariablesFormSet
comment = _('changed extra variables')
template_name = 'combo/page_extra_variables.html'
page_title = _('Extra page variables')
def get_initial(self):
return sorted(
({'key': k, 'value': v} for k, v in self.get_object().extra_variables.items()),
key=itemgetter('key'),
)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.pop('instance')
return kwargs
def form_valid(self, form):
self.object = self.get_object()
self.object.extra_variables = {}
for sub_data in form.cleaned_data:
if not sub_data.get('key'):
continue
self.object.extra_variables[sub_data['key']] = sub_data['value']
self.object.save()
PageSnapshot.take(self.object, request=self.request, comment=self.comment)
return HttpResponseRedirect(self.get_success_url())
page_edit_extra_variables = PageEditExtraVariablesView.as_view()
class PageView(ManagedPageMixin, DetailView):
model = Page
template_name = 'combo/page_view.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
cells = CellBase.get_cells(
page=self.object,
prefetch_validity_info=True,
prefetch_groups=True,
select_related={'__all__': ['page']},
)
existing_cell_types = {cell.get_cell_type_str() for cell in cells}
cell_type_groups = {}
for cell_type in CellBase.get_all_cell_types():
if not cell_type['group'] in cell_type_groups:
cell_type_groups[cell_type['group']] = []
if cell_type.get('max_one_by_page') and cell_type['cell_type_str'] in existing_cell_types:
continue
cell_type_groups[cell_type['group']].append(cell_type)
for cell_group in cell_type_groups.values():
cell_group.sort(key=lambda x: (x.get('order'), x.get('name')))
if 'data' in cell_type_groups:
cell_type_groups[''] = cell_type_groups.get('data')
del cell_type_groups['data']
context['cell_type_groups'] = list(cell_type_groups.items())
context['cell_type_groups'].sort(key=lambda x: x[0])
context['page_has_subpages'] = self.object.get_children().exists()
context['with_wcs'] = is_wcs_enabled(None)
self.object.prefetched_cells = cells
placeholders = []
optional_placeholders = []
for placeholder in self.object.get_placeholders(request=self.request, traverse_cells=True):
placeholder_dict = {
'key': placeholder.key,
'name': placeholder.get_name(),
}
placeholder_dict['cells'] = [x for x in cells if x.placeholder == placeholder.key]
# not optional placeholder
if not placeholder.optional:
placeholders.append(placeholder_dict)
continue
# optional placeholder, but requested for edition
if self.request.GET.get('include-section') == placeholder.key:
placeholders.append(placeholder_dict)
continue
# empty optional placeholder: in optional sections
if not placeholder_dict['cells']:
placeholder_dict['is_empty'] = True
optional_placeholders.append(placeholder_dict)
continue
# optional placeholder with at least 2 cells: in page edition
if len(placeholder_dict['cells']) > 1:
placeholders.append(placeholder_dict)
continue
# optional placeholder with only one ParentContentCell: in optional sections
if isinstance(placeholder_dict['cells'][0], ParentContentCell):
placeholder_dict['is_empty'] = False
optional_placeholders.append(placeholder_dict)
continue
# other cases: in page edition
placeholders.append(placeholder_dict)
context['placeholders'] = placeholders
context['optional_placeholders'] = optional_placeholders
context.update(
{
'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
),
}
)
if self.object.redirect_url:
context['redirect_url_is_absolute'] = not self.object.redirect_url.startswith('/')
return context
page_view = requires_csrf_token(PageView.as_view())
class PageDeleteView(ManagedPageMixin, DeleteView):
model = Page
template_name = 'combo/delete_page.html'
def post(self, request, *args, **kwargs):
deleted_page = self.get_object()
if request.POST.get('choice') == 'delete-one':
new_parent = deleted_page.parent_id
Page.objects.filter(parent=deleted_page).update(parent=new_parent)
PageSnapshot.take(deleted_page, request=self.request, deletion=True)
return self.delete(request, *args, **kwargs)
def get_context_data(self, **kwargs):
subpages = Page.objects.filter(parent=self.get_object().id)
context = super().get_context_data()
context['display_choice'] = bool(subpages)
return context
def get_success_url(self):
return reverse('combo-manager-homepage')
page_delete = PageDeleteView.as_view()
class PageExportView(ManagedPageMixin, FormView):
form_class = PageExportForm
template_name = 'combo/page_export.html'
def get_object(self):
return get_object_or_404(Page, pk=self.kwargs['pk'])
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page'] = self.get_object()
return context
def export(self, with_children=False):
instance = self.get_object()
response = HttpResponse(content_type='application/json')
today = datetime.date.today()
if with_children:
pages = instance.get_descendants_and_me()
ordered_pages = Page.get_as_reordered_flat_hierarchy(pages, root_page=instance)
filename_part = f'{instance.slug}_and_subpages'
else:
ordered_pages = [instance]
filename_part = instance.slug
response['Content-Disposition'] = 'attachment; filename="export_page_{}_{}.json"'.format(
filename_part, today.strftime('%Y%m%d')
)
json.dump({'pages': [p.get_serialized_page() for p in ordered_pages]}, response, indent=2)
return response
def get(self, request, *args, **kwargs):
subpages = self.get_object().get_children()
if not subpages.exists():
return self.export()
return super().get(request, *args, **kwargs)
def form_valid(self, form):
return self.export(with_children=form.cleaned_data['with_subpages'])
page_export = PageExportView.as_view()
class PageDuplicateView(ManagedPageMixin, FormView):
form_class = PageDuplicateForm
template_name = 'combo/page_duplicate.html'
def get_success_url(self):
return reverse('combo-manager-page-view', kwargs={'pk': self.new_page.pk})
def form_valid(self, form):
page = Page.objects.get(pk=self.kwargs['pk'])
self.new_page = page.duplicate(title=form.cleaned_data['title'])
if not page.exclude_from_navigation:
messages.info(
self.request,
_('Page %s has been duplicated, it has been marked as excluded from navigation.')
% page.title,
)
else:
messages.info(self.request, _('Page %s has been duplicated.') % page.title)
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page'] = Page.objects.get(id=self.kwargs['pk'])
return context
page_duplicate = PageDuplicateView.as_view()
class PageHistoryView(ManagedPageMixin, ListView):
model = PageSnapshot
template_name = 'combo/page_history.html'
paginate_by = 20
def get_queryset(self):
self.page = get_object_or_404(Page, pk=self.kwargs['pk'])
return self.page.pagesnapshot_set.all()
page_history = PageHistoryView.as_view()
class SnapshotRestoreView(ManagedPageMixin, DetailView):
http_method_names = ['get', 'post']
model = PageSnapshot
template_name = 'combo/snapshot_restore.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page'] = Page.objects.get(id=self.kwargs['page_pk'])
return context
def post(self, request, *args, **kwargs):
snapshot = self.get_object()
datetime_str = date_format(localtime(snapshot.timestamp), format='DATETIME_FORMAT')
page = self.get_object().restore()
PageSnapshot.take(page, request=self.request, comment=_('restored snapshot from %s') % datetime_str)
messages.info(self.request, _('Snapshot from %s has been restored.') % datetime_str)
return HttpResponseRedirect(reverse('combo-manager-page-view', kwargs={'pk': self.kwargs['page_pk']}))
snapshot_restore = SnapshotRestoreView.as_view()
def snapshot_export(request, *args, **kwargs):
snapshot = PageSnapshot.objects.get(id=kwargs['pk'])
page = snapshot.get_page()
response = HttpResponse(content_type='application/json')
response['Content-Disposition'] = 'attachment; filename="export_page_{}_{}.json"'.format(
page.slug, snapshot.timestamp.strftime('%Y%m%d')
)
json.dump(page.get_serialized_page(), response, indent=2)
return response
class SnapshotSaveView(ManagedPageMixin, FormView):
form_class = PageSnapshotSaveForm
template_name = 'combo/snapshot_save.html'
def get_success_url(self):
return reverse('combo-manager-page-view', kwargs={'pk': self.page.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page'] = self.page
return context
def form_valid(self, form):
PageSnapshot.take(self.page, request=self.request, label=form.cleaned_data['label'])
messages.info(self.request, _('Snapshot "%s" has been saved.') % form.cleaned_data['label'])
return super().form_valid(form)
snapshot_save = SnapshotSaveView.as_view()
class PageRedirectToEditView(RedirectView):
permanent = False
def get_redirect_url(self, page_path):
try:
page = get_page_from_url_parts(page_path.split('/'))
except MissingSubSlug as e:
page = e.page
if not page:
raise Http404()
return reverse('combo-manager-page-view', kwargs={'pk': page.pk})
page_redirect_to_edit = PageRedirectToEditView.as_view()
class PageAddCellView(ManagedPageMixin, RedirectView):
permanent = False
def get_redirect_url(self, page_pk, cell_type, variant, ph_key):
page_cells = CellBase.get_cells(page_id=page_pk)
cell_class = get_cell_class(cell_type)
if cell_class.max_one_by_page and any(isinstance(x, cell_class) for x in page_cells):
raise PermissionDenied()
cell = cell_class(page_id=page_pk, placeholder=ph_key)
cell.set_variant(variant)
orders = [x.order for x in page_cells]
if orders:
cell.order = max(orders) + 1
else:
cell.order = 1
cell.save()
PageSnapshot.take(cell.page, request=self.request, comment=_('added cell "%s"') % cell)
return reverse('combo-manager-page-view', kwargs={'pk': page_pk}) + '#cell-' + cell.get_reference()
page_add_cell = PageAddCellView.as_view()
class PageEditCellView(ManagedPageMixin, UpdateView):
http_method_names = ['post']
def get_object(self, queryset=None):
page_pk = self.kwargs.get('page_pk')
cell_reference = self.kwargs.get('cell_reference')
try:
return CellBase.get_cell(cell_reference, page_id=page_pk)
except ObjectDoesNotExist:
raise Http404()
def get_prefix(self):
return 'c%s' % self.kwargs.get('cell_reference')
def post(self, request, *args, **kwargs):
self.object = self.get_object()
response = {'errorlist': {}, 'tabs': {}, 'prefix': self.get_prefix()}
context = {}
context['page'] = self.object.page
context['cell'] = self.object
context.update(self.object.get_extra_manager_context())
tab_error_forms = {}
class Rollback(Exception):
pass
try:
with transaction.atomic():
for tab in self.object.get_manager_tabs():
if tab.get('fields'):
tab['form'] = forms.models.modelform_factory(
self.object.__class__, fields=tab['fields']
)
form = tab['form'](**self.get_form_kwargs())
if form.is_valid():
self.object = form.save()
else:
tab_error_forms[tab['slug']] = form
response['errorlist'][tab['slug']] = form.errors
self.object.refresh_from_db()
if response['errorlist']:
raise Rollback()
except Rollback:
pass
for tab in self.object.get_manager_tabs():
if tab.get('fields'):
tab['form'] = forms.models.modelform_factory(self.object.__class__, fields=tab['fields'])
# if current form had no errors, create a new unbound form using current object
# so it can get new dynamic fields
form = tab_error_forms.get(tab['slug']) or tab['form'](
instance=self.object, prefix=self.get_prefix(), initial={}
)
is_not_default = build_tab_is_not_default(form)
if tab['slug'] == 'general':
form_name = 'form'
else:
form_name = '%s_form' % tab['slug']
context[form_name] = form
if tab.get('template'):
cell_form_template = template.loader.get_template(tab['template'])
else:
cell_form_template = engines['django'].from_string('{{ %s.as_p }}' % form_name)
response['tabs'][tab['slug']] = {
'form': cell_form_template.render(context),
'is_not_default': is_not_default,
}
response['visibility_css_class'] = self.object.get_manager_visibility_css_class()
response['visibility_content'] = self.object.get_manager_visibility_content()
response['extra_css_class'] = self.object.cleaned_extra_css_class
response['slug'] = self.object.slug
response['template_label'] = self.object.get_template_label()
response['additional_label'] = self.object.get_additional_label()
response['invalid_reason'] = self.object.get_invalid_reason()
if not response['errorlist']:
PageSnapshot.take(
self.object.page, request=self.request, comment=_('changed cell "%s"') % self.object
)
return JsonResponse(response)
page_edit_cell = PageEditCellView.as_view()
class PageDeleteCellView(ManagedPageMixin, DeleteView):
template_name = 'combo/generic_confirm_delete.html'
def get_object(self, queryset=None):
page_pk = self.kwargs.get('page_pk')
cell_reference = self.kwargs.get('cell_reference')
try:
return CellBase.get_cell(cell_reference, page_id=page_pk)
except ObjectDoesNotExist:
raise Http404()
def delete(self, request, *args, **kwargs):
cell = self.get_object()
comment = _('removed cell "%s"') % cell
page = cell.page
response = super().delete(request, *args, **kwargs)
PageSnapshot.take(page, request=self.request, comment=comment)
return response
def get_success_url(self):
return reverse('combo-manager-page-view', kwargs={'pk': self.kwargs.get('page_pk')})
page_delete_cell = PageDeleteCellView.as_view()
class PageDuplicateCellView(ManagedPageMixin, FormView):
form_class = CellDuplicateForm
template_name = 'combo/cell_duplicate.html'
def dispatch(self, request, *args, **kwargs):
try:
self.current_cell = CellBase.get_cell(kwargs['cell_reference'], page=kwargs['page_pk'])
except ObjectDoesNotExist:
raise Http404
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return (
reverse('combo-manager-page-view', kwargs={'pk': self.new_cell.page.pk})
+ '#cell-'
+ self.new_cell.get_reference()
)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['request'] = self.request
kwargs['current_page'] = self.page
return kwargs
def form_valid(self, form):
self.new_cell = self.current_cell.duplicate(
page_target=form.cleaned_data['target_page'], reset_slug=True, set_order=True
)
if self.new_cell.page == self.page:
PageSnapshot.take(
self.new_cell.page, request=self.request, comment=_('duplicated cell "%s"') % self.new_cell
)
else:
PageSnapshot.take(
self.new_cell.page,
request=self.request,
comment=_('added cell "%s", duplicated from "%s" page') % (self.new_cell, self.page),
)
messages.info(self.request, _('Cell %s has been duplicated.') % self.current_cell)
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['current_page'] = self.page
return context
page_duplicate_cell = PageDuplicateCellView.as_view()
class PageEditCellAttributeView(ManagedPageMixin, UpdateView):
http_method_names = ['post']
def get_object(self, queryset=None):
page_pk = self.kwargs.get('page_pk')
cell_reference = self.kwargs.get('cell_reference')
try:
return CellBase.get_cell(cell_reference, page_id=page_pk)
except ObjectDoesNotExist:
raise Http404()
def post(self, request, *args, **kwargs):
self.object = self.get_object()
attr = kwargs['attribute']
if attr not in self.object.get_default_form_fields():
return JsonResponse({'err': 1, 'err_desc': 'not-attribute-from-default-form'})
form_class = self.object.get_default_form_class(fields=[attr])
form = form_class(**self.get_form_kwargs())
response = {}
if form.is_valid():
response['err'] = 0
self.object = form.save()
PageSnapshot.take(
self.object.page,
request=self.request,
comment=_('changed cell "%(cell)s" (attribute: %(attr)s)')
% {
'cell': self.object,
'attr': attr,
},
)
else:
response['err'] = 1
response['err_desc'] = 'form-error-see-errorlist'
response['errorlist'] = form.errors
return JsonResponse(response)
page_edit_cell_attribute = PageEditCellAttributeView.as_view()
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()
@csrf_exempt
@staff_required
def page_order(request):
if request.method != 'POST':
return HttpResponseNotAllowed(['post'])
params = ['new-order', 'moved-page-id', 'moved-page-new-parent']
for param in params:
if param not in request.POST:
return HttpResponseBadRequest('missing %s parameter' % param)
new_order = [int(x) for x in request.POST['new-order'].split(',')]
moved_page = Page.objects.get(id=request.POST['moved-page-id'])
if request.POST['moved-page-new-parent']:
# recreate full hierarchy to avoid cycles
current_hierarchy = Page.objects.get(id=request.POST['moved-page-new-parent']).get_parents_and_self()
new_hierarchy = [x for x in current_hierarchy if not x.id == moved_page.id] + [moved_page]
for i, page in enumerate(new_hierarchy):
old_parent_id = page.parent_id
if i == 0:
page.parent_id = None
else:
page.parent_id = new_hierarchy[i - 1].id
if old_parent_id != page.parent_id:
page.save()
else:
moved_page.parent_id = None
moved_page.save()
slug_conflict = False
try:
with transaction.atomic():
for page in Page.objects.filter(parent_id=moved_page.parent_id):
page.order = new_order.index(page.id) + 1
page.save()
if moved_page.id != page.id and moved_page.slug == page.slug:
slug_conflict = True
except ValueError:
# missing child page in new_order, fail silently
return redirect(reverse('combo-manager-homepage'))
if slug_conflict:
# slug conflict after a page got moved, reload and rename
moved_page = Page.objects.get(id=request.POST['moved-page-id'])
moved_page.slug = moved_page.slug + '-' + hashlib.md5(force_bytes(moved_page.id)).hexdigest()[:4]
moved_page.save()
return redirect(reverse('combo-manager-homepage'))
def menu_json(request):
if settings.TEMPLATE_VARS.get('site_title'):
label = _('Editing "%(site_title)s"') % settings.TEMPLATE_VARS
else:
label = _('Content Management')
# use a custom slug if the site is the portal agent.
slug = 'portal-agent' if is_portal_agent() else 'portal'
json_str = json.dumps(
[
{
'label': force_str(label),
'slug': slug,
'url': request.build_absolute_uri(reverse('combo-manager-homepage')),
}
]
)
content_type = 'application/json'
for variable in ('jsonpCallback', 'callback'):
if variable in request.GET:
json_str = '%s(%s);' % (request.GET[variable], json_str)
content_type = 'application/javascript'
break
response = HttpResponse(content_type=content_type)
response.write(json_str)
return response
class PageListCellAddLinkView(ManagedPageMixin, CreateView):
template_name = 'combo/link_cell_form.html'
def dispatch(self, request, *args, **kwargs):
try:
self.cell = CellBase.get_cell(kwargs['cell_reference'], page=kwargs['page_pk'])
except LinkListCell.DoesNotExist:
raise Http404
for klass in self.cell.get_link_cell_classes():
if klass.add_as_link_code == kwargs['link_code']:
self.model = klass
break
if self.model is None:
raise Http404
return super().dispatch(request, *args, **kwargs)
def get_form_class(self):
return self.model().get_form_class_for_link_list_cell()
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['instance'] = self.model(page=self.cell.page, placeholder=self.cell.link_placeholder)
return kwargs
def form_valid(self, form):
orders = [x.order for x in self.cell.get_items()]
if orders:
form.instance.order = max(orders) + 1
else:
form.instance.order = 1
PageSnapshot.take(self.cell.page, request=self.request, comment=_('changed cell "%s"') % self.cell)
response = super().form_valid(form)
self.cell.check_validity()
return response
def get_success_url(self):
return '%s#cell-%s' % (
reverse('combo-manager-page-view', kwargs={'pk': self.kwargs.get('page_pk')}),
self.kwargs['cell_reference'],
)
page_list_cell_add_link = PageListCellAddLinkView.as_view()
class PageListCellEditLinkView(ManagedPageMixin, UpdateView):
template_name = 'combo/link_cell_form.html'
def dispatch(self, request, *args, **kwargs):
try:
self.cell = CellBase.get_cell(kwargs['cell_reference'], page=kwargs['page_pk'])
except LinkListCell.DoesNotExist:
raise Http404
try:
self.object = CellBase.get_cell(kwargs['link_cell_reference'], page=kwargs['page_pk'])
except ObjectDoesNotExist:
raise Http404
if self.object.placeholder != self.cell.link_placeholder:
raise Http404
self.model = self.object.__class__
return super().dispatch(request, *args, **kwargs)
def get_object(self, *args, **kwargs):
return self.object
def get_form_class(self):
return self.model().get_form_class_for_link_list_cell()
def form_valid(self, form):
if is_ajax(self.request):
self.object = form.save()
response = self.form_invalid(form) # avoid redirection
else:
response = super().form_valid(form)
PageSnapshot.take(self.cell.page, request=self.request, comment=_('changed cell "%s"') % self.cell)
self.cell.check_validity()
return response
def get_success_url(self):
return '%s#cell-%s' % (
reverse('combo-manager-page-view', kwargs={'pk': self.kwargs.get('page_pk')}),
self.kwargs['cell_reference'],
)
page_list_cell_edit_link = PageListCellEditLinkView.as_view()
class PageListCellDeleteLinkView(ManagedPageMixin, DeleteView):
template_name = 'combo/generic_confirm_delete.html'
def dispatch(self, request, *args, **kwargs):
try:
self.cell = CellBase.get_cell(kwargs['cell_reference'], page=kwargs['page_pk'])
except LinkListCell.DoesNotExist:
raise Http404
try:
self.object = CellBase.get_cell(kwargs['link_cell_reference'], page=kwargs['page_pk'])
except ObjectDoesNotExist:
raise Http404
if self.object.placeholder != self.cell.link_placeholder:
raise Http404
self.model = self.object.__class__
return super().dispatch(request, *args, **kwargs)
def get_object(self, *args, **kwargs):
return self.object
def delete(self, request, *args, **kwargs):
response = super().delete(request, *args, **kwargs)
PageSnapshot.take(self.cell.page, request=self.request, comment=_('changed cell "%s"') % self.cell)
self.cell.check_validity()
return response
def get_success_url(self):
return '%s#cell-%s' % (
reverse('combo-manager-page-view', kwargs={'pk': self.kwargs.get('page_pk')}),
self.kwargs['cell_reference'],
)
page_list_cell_delete_link = PageListCellDeleteLinkView.as_view()
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:
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
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'])
if has_changes:
PageSnapshot.take(cell.page, request=request, comment=_('reordered cells'))
return HttpResponse(status=204)
link_list_order = LinkListOrder.as_view()
class SiteSettingsView(UpdateView):
form_class = SiteSettingsForm
template_name = 'combo/site_settings.html'
success_url = reverse_lazy('combo-manager-homepage')
def get_object(self):
return SiteSettings.get_singleton()
site_settings = SiteSettingsView.as_view()
class PlaceholderOptionsView(ManagedPageMixin, FormView):
form_class = PlaceholderOptionsForm
template_name = 'combo/placeholder_options.html'
def get_object(self):
return get_object_or_404(Page, pk=self.kwargs['page_pk'])
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page'] = self.get_object()
return context
def get_initial(self):
return self.page.placeholder_options.get(self.kwargs['placeholder'])
def form_valid(self, form):
self.page.placeholder_options[self.kwargs['placeholder']] = form.cleaned_data
self.page.save()
placeholder_label = self.kwargs['placeholder']
for placeholder in self.page.get_placeholders(self.request):
if placeholder.key == self.kwargs['placeholder']:
placeholder_label = placeholder.get_name()
break
PageSnapshot.take(
self.page,
request=self.request,
comment=_('modified options for "%s" placeholder') % placeholder_label,
)
return super().form_valid(form)
def get_success_url(self):
return reverse('combo-manager-page-view', kwargs={'pk': self.page.id})
placeholder_options = PlaceholderOptionsView.as_view()