# 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 . import hashlib import json import os from django.conf import settings from django.contrib import messages from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.core.urlresolvers import reverse, reverse_lazy from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.shortcuts import redirect from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import force_text, force_bytes from django.utils.formats import date_format from django.utils.timezone import localtime from django.views.decorators.csrf import requires_csrf_token from django.views.generic import (RedirectView, DetailView, CreateView, UpdateView, ListView, DeleteView, FormView) from combo.data.models import Page, CellBase, ParentContentCell, PageSnapshot from combo.data.library import get_cell_class from combo.data.utils import export_site, import_site, MissingGroups from combo import plugins from .forms import (PageEditTitleForm, PageVisibilityForm, SiteImportForm, PageEditRedirectionForm, PageSelectTemplateForm, PageEditSlugForm, PageEditPictureForm, PageEditExcludeFromNavigationForm, PageEditDescriptionForm, CellVisibilityForm) 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) context = super(HomepageView, self).get_context_data(**kwargs) context['extra_actions'] = plugins.get_extra_manager_actions() return context homepage = HomepageView.as_view() class SiteExportView(ListView): model = Page def render_to_response(self, context, **response_kwargs): response = HttpResponse(content_type='application/json') json.dump(export_site(), 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): try: json_site = json.loads(force_text(self.request.FILES['site_json'].read())) except ValueError: form.add_error('site_json', _('File is not in the expected JSON format.')) return self.form_invalid(form) try: import_site(json_site) except MissingGroups as e: form.add_error('site_json', unicode(e)) return self.form_invalid(form) return super(SiteImportView, self).form_valid(form) site_import = SiteImportView.as_view() class PageAddView(CreateView): model = Page template_name = 'combo/page_add.html' form_class = PageEditTitleForm def get_initial(self): initial = super(PageAddView, self).get_initial() if Page.objects.count() == 0: # first page initial['title'] = _('Home') return initial def form_valid(self, form): response = super(PageAddView, self).form_valid(form) if self.object.slug != 'index' or self.object.parent_id: for placeholder in self.object.get_placeholders(request=self.request): if placeholder.acquired is True: ParentContentCell(page=self.object, placeholder=placeholder.key, order=0).save() return response def get_success_url(self): return reverse('combo-manager-page-view', kwargs={'pk': self.object.id}) page_add = PageAddView.as_view() class PageEditView(UpdateView): model = Page template_name = 'combo/page_add.html' comment = None def form_valid(self, form): result = super(PageEditView, self).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(PageSelectTemplateView, self).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 PageEditExcludeFromNavigationView(PageEditView): form_class = PageEditExcludeFromNavigationForm comment = _('changed navigation exclusion') page_edit_exclude_from_navigation = PageEditExcludeFromNavigationView.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(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 PageView(DetailView): model = Page template_name = 'combo/page_view.html' def get_context_data(self, **kwargs): context = super(PageView, self).get_context_data(**kwargs) 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']] = [] 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.keys(): 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]) cells = CellBase.get_cells(page_id=self.object.id) template = self.object.template_name placeholders = [] combo_template = settings.COMBO_PUBLIC_TEMPLATES.get(template) 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] placeholders.append(placeholder_dict) context['placeholders'] = placeholders return context page_view = requires_csrf_token(PageView.as_view()) class PageDeleteView(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(PageDeleteView, self).get_context_data() context['display_choice'] = True if subpages else False return context def get_success_url(self): return reverse('combo-manager-homepage') page_delete = PageDeleteView.as_view() class PageExportView(DetailView): model = Page def render_to_response(self, context, **response_kwargs): response = HttpResponse(content_type='application/json') json.dump({'pages': [self.object.get_serialized_page()]}, response, indent=2) return response page_export = PageExportView.as_view() class PageHistoryView(ListView): model = PageSnapshot template_name = 'combo/page_history.html' paginate_by = 20 def get_queryset(self): self.page = Page.objects.get(id=self.kwargs['pk']) return self.page.pagesnapshot_set.all() page_history = PageHistoryView.as_view() class SnapshotRestoreView(DetailView): http_method_names = ['get', 'post'] model = PageSnapshot template_name = 'combo/snapshot_restore.html' def get_context_data(self, **kwargs): context = super(SnapshotRestoreView, self).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() class PageAddCellView(RedirectView): permanent = False def get_redirect_url(self, page_pk, cell_type, variant, ph_key): cell_class = get_cell_class(cell_type) cell = cell_class(page_id=page_pk, placeholder=ph_key) cell.set_variant(variant) orders = [x.order for x in CellBase.get_cells(page_id=page_pk)] 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(UpdateView): def get_template_names(self): return [self.template_name or self.object.manager_form_template] def get_context_data(self, **kwargs): context = super(PageEditCellView, self).get_context_data(**kwargs) context['cell'] = self.get_object() return context 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 get_form_class(self): return self.object.get_default_form_class() def get_success_url(self): return reverse('combo-manager-page-view', kwargs={'pk': self.kwargs.get('page_pk')} ) + '#cell-' + self.object.get_reference() def form_valid(self, form): if self.request.is_ajax(): self.object = form.save() response = self.form_invalid(form) # avoid redirection else: response = super(PageEditCellView, self).form_valid(form) PageSnapshot.take(self.object.page, request=self.request, comment=_('changed cell "%s"') % self.object) return response page_edit_cell = PageEditCellView.as_view() class PageDeleteCellView(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(PageDeleteCellView, self).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 PageCellVisibilityView(PageEditCellView): template_name = 'combo/cell_visibility.html' def get_form_class(self): return CellVisibilityForm page_cell_visibility = PageCellVisibilityView.as_view() class PageCellOptionsView(PageEditCellView): template_name = 'combo/cell_options.html' def get_form_class(self): return self.object.get_options_form_class() 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() if has_changes: page = Page.objects.get(id=page_pk) PageSnapshot.take(page, request=request, comment=_('reordered cells')) return HttpResponse(status=204) 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']) current_parent_id = moved_page.parent_id if request.GET['moved-page-new-parent']: moved_page.parent_id = request.GET['moved-page-new-parent'] else: moved_page.parent_id = None moved_page.save() slug_conflict = False 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 if slug_conflict: # slug conflict after a page got moved, reload and rename moved_page = Page.objects.get(id=request.GET['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 page_get_additional_label(request, page_pk, cell_reference): cell = CellBase.get_cell(cell_reference, page_id=page_pk) response = HttpResponse(content_type='application/json') json.dump({'label': force_text(cell.get_additional_label()) or ''}, response) return response def menu_json(request): if settings.TEMPLATE_VARS.get('site_title'): label = _('Editing "%(site_title)s"') % settings.TEMPLATE_VARS else: label = _('Content Management') slug = 'portal' if getattr(settings, 'KNOWN_SERVICES') and 'combo' in settings.KNOWN_SERVICES: # switch to custom slug if the site is the portal agent. for service_slug, service in settings.KNOWN_SERVICES['combo'].items(): if service.get('is-portal-agent') and not service.get('secret'): slug = 'portal-agent' break 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