ATTACHMENTS_WITH_SENDFILE = False class Page(PageModelBase):
    """
    Full page document
    """
    modified = models.DateTimeField(_('last edit'), auto_now=True)
    slug = models.SlugField(_('slug'), unique=True, max_length=75, help_text=_("Unique slug used in URL, should be automatically filled with sluggified title."))
    
    @models.permalink
    def get_absolute_url(self):
        return ('documents-page-details', [self.slug])
    
    def get_render_cache_key(self, **kwargs):
        """
        Get the cache key for the content render with according to the given settings
        """
        return settings.PAGE_RENDER_CACHE_KEY_NAME.format(id=self.id, **kwargs)
    
    def get_toc_cache_key(self, **kwargs):
        """
        Get the cache key for the content TOC with according to the given settings
        """
        return settings.PAGE_TOC_CACHE_KEY_NAME.format(id=self.id, **kwargs)
    
    def clear_cache(self):
        """
        Invalidate all possible cache keys
        """
        keys = _get_cache_keyset(settings.PAGE_RENDER_CACHE_KEY_NAME, **{
            'id': self.id,
            'setting': RSTVIEW_PARSER_FILTER_SETTINGS.keys(),
        })
        keys += _get_cache_keyset(settings.PAGE_TOC_CACHE_KEY_NAME, **{
            'id': self.id,
            'setting': RSTVIEW_PARSER_FILTER_SETTINGS.keys(),
        })
        # Drop cache for knowed pages slugs used in the ``page`` rest role
        cache.delete_many([settings.PAGE_SLUGS_CACHE_KEY_NAME]+keys)
        return keys
    
    def _get_current_revision(self):
        return (self.revision.all().aggregate(models.Max('revision')).get('revision__max') or 0)+1
    current_revision = property(_get_current_revision)
    
    def save(self, *args, **kwargs):
        # First create
        if not self.created:
            self.created = datetime.now()
        # Creating a new revision archive
        elif settings.DOCUMENTS_PAGE_ARCHIVED:
            old = Page.objects.get(pk=self.id)
            PageRevision(
                page=self,
                created=self.modified,
                parent=old.parent,
                author=old.author,
                title=old.title,
                published=old.published,
                slug=old.slug,
                template=old.template,
                order=old.order,
                visible=old.visible,
                content=old.content,
                revision=self.current_revision,
                comment=old.comment,
            ).save()
        
        # Fill in the published date with the created date if empty
        if not self.published:
            self.published = self.created
        # Invalidate all caches on edit
        if self.modified:
            self.clear_cache()
        
        super(Page, self).save(*args, **kwargs)
    
    def delete(self, using=None):
        self.clear_cache()
        super(Page, self).delete(using=using)
    
    class Meta:
        verbose_name = _("page")
        verbose_name_plural = _("pages") class Attachment(models.Model):
    """
    Attachment file for a Page document
    
    TODO: Remove attachment map cache on update/delete and also when Page is removed, and updated ?
    """
    page = models.ForeignKey(Page, verbose_name=_('page'), related_name='attachment')
    author = models.ForeignKey(User, verbose_name=_('author'))
    title = models.CharField(_('title'), max_length=75, blank=True)
    slug = models.CharField(_('slug'), max_length=75, blank=True, help_text=_("Used as the key to put the attachment in your documents. This does not really use the slug syntax, you can use more special characters. If empty, will be filled with the original file name."))
    created = models.DateTimeField(_('created'), auto_now_add=True)
    file = models.FileField(_('file'), upload_to=ATTACH_FILE_UPLOADTO, storage=ATTACHMENT_FS_STORAGE, max_length=255, blank=False)
    size = models.IntegerField(_('size'), blank=False, default=0, editable=False)
    content_type = models.CharField(_('content_type'), max_length=120, blank=False, null=True, editable=False)
    
    def __unicode__(self):
        return self.slug
    
    def is_image(self):
        return (self.content_type and self.content_type in IMAGE_MIMETYPES)
    
    def save(self, *args, **kwargs):
        if not self.slug:
            # TODO: slugify
            self.slug = self.file.name.strip()
        if not self.title:
            self.title = self.slug
        super(Attachment, self).save(*args, **kwargs)

    class Meta:
        verbose_name = _("attachment file")
        verbose_name_plural = _("attachment files") class PageForm(CrispyFormMixin, forms.ModelForm):
    """
    Page form
    """
    crispy_form_helper_path = 'sveedocuments.forms.crispies.page_helper'
    codemirror_config_name = 'sveetchies-documents-add-page'
    
    def __init__(self, author=None, parent=None, *args, **kwargs):
        self.author = author
        self.parent = parent

        self.crispy_form_helper_kwargs = {'parent': self.parent}
        
        super(PageForm, self).__init__(*args, **kwargs)
        super(forms.ModelForm, self).__init__(*args, **kwargs)
        
        # En cas d'édition, limite les choix de parenté à tout ceux qui ne sont pas 
        # descendant de l'instance pour empêcher une exception 
        # sur une erreur de déplacement
        extra_settings = {}
        if self.instance.id:
            parent_queryset = Page.objects.all()
            children = self.instance.get_descendants(include_self=True).values_list('id', flat=True)
            parent_queryset = parent_queryset.exclude(id__in=children)
            self.fields['parent'] = TreeNodeChoiceField(queryset=parent_queryset, empty_label=_(u"-- Root --"), required=False)
        # En cas de création, mode pour ajouter une page directement sous un "parent"
        else:
            if self.parent:
                del self.fields['parent']
            # Désactive l'option de sauvegarde rapide
            extra_settings = {'quicksave_url': None}
            
        # Widget d'édition du contenu
        self.fields['content'].widget = CodeMirrorWidget(attrs={'rows': 30}, config_name=self.codemirror_config_name)
    
    def clean_slug(self):
        slug = self.cleaned_data.get("slug")
        if slug:
            if slug in settings.DOCUMENTS_PAGE_RESERVED_SLUGS:
                raise forms.ValidationError(_('This is a reserved keyword'))
        return slug
    
    def clean_content(self):
        """
        Parse le contenu pour vérifier qu'il ne contient par d'erreurs de syntaxe
        """
        content = self.cleaned_data.get("content")
        if content:
            errors = SourceReporter(content)
            if errors:
                raise forms.ValidationError(map(map_parsing_errors, errors))
        return content
    
    def save(self, *args, **kwargs):
        edited = (self.instance and self.instance.id)

        instance = super(PageForm, self).save(commit=False, *args, **kwargs)
        instance.author = self.author
        if self.parent:
            instance.parent = self.parent
        instance.save()
        
        # Send a Django signal for page update
        documents_page_update_signal.send(sender=self, page_instance=instance, edited=edited)
        
        return instance
    
    class Meta:
        model = Page
        exclude = ('created', 'author')
        widgets = {
            'published': forms.SplitDateTimeWidget,
        } class PageEditForm(PageForm):
    crispy_form_helper_path = 'sveedocuments.forms.crispies.page_edit_helper'
    codemirror_config_name = 'sveetchies-documents-edit-page' class PageQuickForm(forms.ModelForm):
    """
    *Page* quicksave form
    
    This is only about the instance *content* field
    
    TODO: This should also accepts the associated 'comment' field, but in first the 
          editor must implement to send the "comment" content with the "content" 
          content.
    """
    def __init__(self, author=None, *args, **kwargs):
        self.author = author
        super(PageQuickForm, self).__init__(*args, **kwargs)
    
    def clean_content(self):
        """
        Parse content to check syntax alerts
        """
        content = self.cleaned_data.get("content")
        if content:
            errors = SourceReporter(content)
            if errors:
                raise forms.ValidationError(map(map_parsing_errors, errors))
        return content
    
    def save(self, *args, **kwargs):
        instance = super(PageQuickForm, self).save(commit=True, *args, **kwargs)
        return instance
    
    class Meta:
        model = Page
        fields = ('content',) class AttachmentForm(CrispyFormMixin, forms.ModelForm):
    """
    Attachment form
    """
    crispy_form_helper_path = 'sveedocuments.forms.crispies.attachment_helper'
    
    def __init__(self, *args, **kwargs):
        self.page = kwargs.pop('page_instance')
        self.author = kwargs.pop('author')
        
        super(AttachmentForm, self).__init__(*args, **kwargs)
        super(forms.ModelForm, self).__init__(*args, **kwargs)
    
    def clean_file(self):
        file = self.cleaned_data['file']
        if file:
            self.size = file._size
            self.content_type = file.content_type

        return file
    
    def save(self, *args, **kwargs):
        instance = super(AttachmentForm, self).save(commit=False, *args, **kwargs)
        instance.page = self.page
        instance.author = self.author
        instance.size = self.size
        instance.content_type = self.content_type
        instance.save()
        
        return instance
    
    class Meta:
        model = Attachment
        fields = ('title', 'slug', 'file') def get_instance_children(obj, depth=0, sig=0):
    """
    Récupèration récursive des relations enfants d'un objet
    
    @depth: integer limitant le niveau de recherche des enfants, 0=illimité
    """
    children = []
    # Pour toute les relations enfants de l'objet
    for child in obj._meta.get_all_related_objects():
        # Nom de l'attribut d'accès
        cname = child.get_accessor_name()
        verbose_name = child.model._meta.verbose_name
        # Récupère tout les objets des relations
        for elem in getattr(obj, cname).all():
            followed = []
            # Recherche récursive des enfants
            if depth == 0 or sig < depth:
                followed = get_instance_children(elem, depth=depth, sig=sig+1)
            children.append( (verbose_name, unicode(elem), followed) )
    return children class DetailListAppendView(ListAppendView):
    """
    A view to display a parent object details, list his "children" and display a form 
    to append a new child
    
    Have the same behaviours than "ListAppendView" but get the parent object before 
    doing anything.
    
    "model" and "form_class" attribute are for the children, "context_parent_object_name" 
    is used to name the parent variable in the template context.
    
    "get_parent_object" must be defined to return the parent instance and also "get_queryset" 
    to filter queryset only for the children of the parent.
    
    The parent object is also given to the append form, under the name defined with the 
    "context_parent_object_name" attribute. Your Form should be aware of this.
    """
    context_parent_object_name = 'parent_object'
    
    def get_parent_object(self):
        raise ImproperlyConfigured(u"%(cls)s's 'get_parent_object' method must be defined " % {"cls": self.__class__.__name__})

    def get_context_data(self, **kwargs):
        kwargs.update({
            self.context_parent_object_name: self.parent_object,
        })
        return super(DetailListAppendView, self).get_context_data(**kwargs)
        
    def get_form_kwargs(self):
        """
        Returns an instance of the form to be used in this view.
        """
        kwargs = super(DetailListAppendView, self).get_form_kwargs()
        kwargs.update({
            self.context_parent_object_name: self.parent_object,
        })
        return kwargs
        
    def get(self, request, *args, **kwargs):
        self.parent_object = self.get_parent_object()
        return super(DetailListAppendView, self).get(request, *args, **kwargs)
        
    def post(self, request, *args, **kwargs):
        self.parent_object = self.get_parent_object()
        return super(DetailListAppendView, self).post(request, *args, **kwargs) class DirectDeleteView(BaseDeleteView):
    """
    To directly delete an object without template rendering on GET or POST methods
    
    "get_success_url" or "success_url" should be correctly filled
    """
    memoize_old_object = False
    _memoized_attr = ['id', 'slug', 'title'] # memoized attributes if exists
    old_object = {} # where the attributes will be putted
    
    def get(self, *args, **kwargs):
        if self.memoize_old_object:
            self.memoize_object()
        return self.delete(*args, **kwargs)
    
    def memoize_object(self, *args, **kwargs):
        old = self.get_object()
        for item in self._memoized_attr:
            self.old_object[item] = getattr(old, item, None) class DownloadMixin(object):
    """
    Simple Mixin to send a downloadable content
    
    Inherits must have :
    
    * Filled the ``self.content_type`` attribute with the content content_type to send;
    * Implementation of ``get_filename()`` that return the filename to use in response 
      headers;
    * Implementation of ``get_content()`` that return the content to send as downloadable.
    
    If the content is a not a string, it is assumed to be a fileobject to send as 
    the content with its ``read()`` method.
    
    Optionnaly implement a ``close_content()`` to close specifics objects linked to 
    content fileobject, if it does not exists a try will be made on a close() method 
    on the content fileobject;
    
    A "get_filename_timestamp" method is implemented to return a timestamp to use in your 
    filename if needed, his date format is defined in "timestamp_format" attribute (in a 
    suitable way to use with strftime on a datetime object).
    """
    content_type = None
    timestamp_format = "%Y-%m-%d"
    
    def get_filename_timestamp(self):
        return datetime.datetime.now().strftime(self.timestamp_format)
    
    def get_filename(self, context):
        raise ImproperlyConfigured("DownloadMixin requires an implementation of 'get_filename()' to return the filename to use in headers")
    
    def get_content(self, context):
        raise ImproperlyConfigured("DownloadMixin requires an implementation of 'get_content()' to return the downloadable content")
    
    def render_to_response(self, context, **response_kwargs):
        if getattr(self, 'content_type', None) is None:
            raise ImproperlyConfigured("DownloadMixin requires a definition of 'content_type' attribute")
        # Needed headers
        response = HttpResponse(content_type=self.content_type, **response_kwargs)
        response['Content-Disposition'] = 'attachment; filename={0}'.format(self.get_filename(context))
        # Read the content file object or string, append it to response and close it
        content = self.get_content(context)
        if isinstance(content, basestring):
            response.write(content)
        else:
            response.write(content.read())
        # Conditionnal closing content object
        if hasattr(self, 'close_content'):
            self.close_content(context, content)
        elif hasattr(content, 'close'):
            content.close()
            
        return response

    def get_context_data(self, **kwargs):
        return {
            'params': kwargs
        }

    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)
from datetime import datetime
from django.conf import settings
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404
from django.template.loader import render_to_string
from django.views import generic
from django.views.generic.detail import SingleObjectMixin
from django.contrib import messages
from django.utils.translation import ugettext as _
from mptt.templatetags.mptt_tags import cache_tree_children
from braces.views import LoginRequiredMixin, PermissionRequiredMixin
from djangocodemirror.views import SampleQuicksaveMixin
from sveedocuments.models import ATTACHMENTS_WITH_SENDFILE, Page, Attachment
from sveedocuments.forms.page import PageForm, PageEditForm, PageQuickForm
from sveedocuments.forms.attachment import AttachmentForm
from sveedocuments.utils.objects import get_instance_children
from sveedocuments.utils.braces_addons import DetailListAppendView, DirectDeleteView, DownloadMixin
# -*- coding: utf-8 -*-
"""
Pages management views
"""







class PageTabsContentMixin(object):
    def get_context_data(self, **kwargs):
        context = super(PageTabsContentMixin, self).get_context_data(**kwargs)
        # Little trick to work with SingleObjectMixin and DetailListAppendView
        if hasattr(self, 'parent_object'):
            obj = self.parent_object
        else:
            obj = self.object
        
        context.update({
            'revisions_count': obj.revision.all().count()+1,
            'files_count': obj.attachment.all().count(),
        })
        return context


class PageCreateView(PermissionRequiredMixin, generic.CreateView):
    """
    Form view to create a *Page* document
    """
    model = Page
    context_object_name = "page_instance"
    template_name = "sveedocuments/board/page_form.html"

    form_class = PageForm