1169 lines
40 KiB
Python
1169 lines
40 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_text
|
|
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.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_portal_agent
|
|
|
|
from .forms import (
|
|
CellDuplicateForm,
|
|
PageAddForm,
|
|
PageDuplicateForm,
|
|
PageEditDescriptionForm,
|
|
PageEditExtraVariablesFormSet,
|
|
PageEditIncludeInNavigationForm,
|
|
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_text(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_text(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
|
|
)
|
|
# 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 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)
|
|
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()
|
|
|
|
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(
|
|
snapshot.get_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 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_text(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 self.request.is_ajax():
|
|
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()
|
|
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()
|