File: models.py

package info (click to toggle)
python-django-feincms 1.6.2-2
  • links: PTS, VCS
  • area: main
  • in suites: wheezy
  • size: 2,716 kB
  • sloc: python: 6,585; makefile: 85; sh: 18
file content (817 lines) | stat: -rw-r--r-- 32,436 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
"""
This is the core of FeinCMS

All models defined here are abstract, which means no tables are created in
the feincms\_ namespace.
"""

import operator
import warnings

from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured
from django.db import connection, models
from django.db.models import Q
from django.db.models.fields import FieldDoesNotExist
from django.db.models.loading import get_model
from django.forms.widgets import Media
from django.template.loader import render_to_string
from django.utils.datastructures import SortedDict
from django.utils.encoding import force_unicode
from django.utils.translation import ugettext_lazy as _

from feincms import ensure_completely_loaded
from feincms.utils import get_object, copy_model_instance


class Region(object):
    """
    This class represents a region inside a template. Example regions might be
    'main' and 'sidebar'.
    """

    def __init__(self, key, title, *args):
        self.key = key
        self.title = title
        self.inherited = args and args[0] == 'inherited' or False
        self._content_types = []

    def __unicode__(self):
        return force_unicode(self.title)

    @property
    def content_types(self):
        """
        Returns a list of content types registered for this region as a list
        of (content type key, beautified content type name) tuples
        """

        return [(ct.__name__.lower(), ct._meta.verbose_name) for ct in self._content_types]


class Template(object):
    """
    A template is a standard Django template which is used to render a
    CMS object, most commonly a page.
    """

    def __init__(self, title, path, regions, key=None, preview_image=None):
        # The key is what will be stored in the database. If key is undefined
        # use the template path as fallback.
        if not key:
            key = path

        self.key = key
        self.title = title
        self.path = path
        self.preview_image = preview_image

        def _make_region(data):
            if isinstance(data, Region):
                return data
            return Region(*data)

        self.regions = [_make_region(row) for row in regions]
        self.regions_dict = dict((r.key, r) for r in self.regions)

    def __unicode__(self):
        return force_unicode(self.title)


class ContentProxy(object):
    """
    The ``ContentProxy`` is responsible for loading the content blocks for all
    regions (including content blocks in inherited regions) and assembling media
    definitions.

    The content inside a region can be fetched using attribute access with
    the region key. This is achieved through a custom ``__getattr__``
    implementation.
    """

    def __init__(self, item):
        item._needs_content_types()
        self.item = item
        self._cache = {
            'cts': {},
            }

    def _inherit_from(self):
        """
        Returns a list of item primary keys to try inheriting content from if
        a certain region is empty.

        Returns an ascending list of ancestors (using MPTT) by default. This
        is good enough (tm) for pages.
        """

        return self.item.get_ancestors(ascending=True).values_list('pk', flat=True)

    def _fetch_content_type_counts(self):
        """
        Returns a structure describing which content types exist for the object
        with the given primary key.

        The structure is as follows, where pk is the passed primary key and
        ct_idx are indices into the item._feincms_content_types list:

            {
                'region_key': [
                    (pk, ct_idx1),
                    (pk, ct_idx2),
                    ],
                ...
            }
        """

        if 'counts' not in self._cache:
            counts = self._fetch_content_type_count_helper(self.item.pk)

            empty_inherited_regions = set()
            for region in self.item.template.regions:
                if region.inherited and not counts.get(region.key):
                    empty_inherited_regions.add(region.key)

            if empty_inherited_regions:
                for parent in self._inherit_from():
                    parent_counts = self._fetch_content_type_count_helper(
                        parent, regions=tuple(empty_inherited_regions))

                    counts.update(parent_counts)
                    for key in parent_counts.keys():
                        empty_inherited_regions.discard(key)

                    if not empty_inherited_regions:
                        break

            self._cache['counts'] = counts
        return self._cache['counts']

    def _fetch_content_type_count_helper(self, pk, regions=None):
        tmpl = ['SELECT %d AS ct_idx, region, COUNT(id) FROM %s WHERE parent_id=%s']
        args = []

        if regions:
            tmpl.append('AND region IN (' + ','.join(['%%s'] * len(regions)) + ')')
            args.extend(regions * len(self.item._feincms_content_types))

        tmpl.append('GROUP BY region')
        tmpl = u' '.join(tmpl)

        sql = ' UNION '.join([tmpl % (idx, cls._meta.db_table, pk)\
            for idx, cls in enumerate(self.item._feincms_content_types)])
        sql = 'SELECT * FROM ( ' + sql + ' ) AS ct ORDER BY ct_idx'

        cursor = connection.cursor()
        cursor.execute(sql, args)

        _c = {}
        for ct_idx, region, count in cursor.fetchall():
            if count:
                _c.setdefault(region, []).append((pk, ct_idx))

        return _c

    def _popuplate_content_type_caches(self, types):
        """
        Populate internal caches for all content types passed
        """

        counts_by_type = {}
        for region, counts in self._fetch_content_type_counts().items():
            for pk, ct_idx in counts:
                counts_by_type.setdefault(self.item._feincms_content_types[ct_idx], []).append((region, pk))

        # Resolve abstract to concrete content types
        types = tuple(types)

        for type in (type for type in self.item._feincms_content_types if issubclass(type, types)):
            counts = counts_by_type.get(type)
            if type not in self._cache['cts']:
                if counts:
                    self._cache['cts'][type] = list(type.get_queryset(
                        reduce(operator.or_, (Q(region=r[0], parent=r[1]) for r in counts))))
                else:
                    self._cache['cts'][type] = []

    def _fetch_regions(self):
        """
        """

        if 'regions' not in self._cache:
            self._popuplate_content_type_caches(self.item._feincms_content_types)
            contents = {}
            for type, content_list in self._cache['cts'].items():
                for instance in content_list:
                    contents.setdefault(instance.region, []).append(instance)

            self._cache['regions'] = dict((region, sorted(instances, key=lambda c: c.ordering))\
                for region, instances in contents.iteritems())
        return self._cache['regions']

    def all_of_type(self, type_or_tuple):
        """
        Return all content type instances belonging to the type or types passed.
        If you want to filter for several types at the same time, type must be
        a tuple.
        """

        content_list = []
        if not hasattr(type_or_tuple, '__iter__'):
            type_or_tuple = (type_or_tuple,)
        self._popuplate_content_type_caches(type_or_tuple)

        for type, contents in self._cache['cts'].items():
            if any(issubclass(type, t) for t in type_or_tuple):
                content_list.extend(contents)

        # TODO: Sort content types by region?
        return sorted(content_list, key=lambda c: c.ordering)

    def _get_media(self):
        """
        Collect the media files of all content types of the current object
        """

        if 'media' not in self._cache:
            media = Media()

            for contents in self._fetch_regions().values():
                for content in contents:
                    if hasattr(content, 'media'):
                        media = media + content.media

            self._cache['media'] = media
        return self._cache['media']
    media = property(_get_media)

    def __getattr__(self, attr):
        """
        Get all item content instances for the specified item and region

        If no item contents could be found for the current item and the region
        has the inherited flag set, this method will go up the ancestor chain
        until either some item contents have found or no ancestors are left.
        """
        if (attr.startswith('__')):
            raise AttributeError

        # Do not trigger loading of real content type models if not necessary
        if not self._fetch_content_type_counts().get(attr):
            return []

        return self._fetch_regions().get(attr, [])


class ExtensionsMixin(object):
    @classmethod
    def register_extension(cls, register_fn):
        """
        Call the register function of an extension. You must override this
        if you provide a custom ModelAdmin class and want your extensions to
        be able to patch stuff in.
        """
        register_fn(cls, None)

    @classmethod
    def register_extensions(cls, *extensions):
        """
        Register all extensions passed as arguments.

        Extensions should be specified as a string to the python module
        containing the extension. If it is a bundled extension of FeinCMS,
        you do not need to specify the full python module path -- only
        specifying the last part (f.e. ``'seo'`` or ``'translations'``) is
        sufficient.
        """

        if not hasattr(cls, '_feincms_extensions'):
            cls._feincms_extensions = set()

        here = cls.__module__.split('.')[:-1]

        paths = [
            '.'.join(here + ['extensions']),
            '.'.join(here[:-1] + ['extensions']),
            'feincms.module.extensions',
            ]

        for ext in extensions:
            if ext in cls._feincms_extensions:
                continue

            fn = None
            if isinstance(ext, basestring):
                try:
                    fn = get_object(ext + '.register')
                except ImportError:
                    for path in paths:
                        try:
                            fn = get_object('%s.%s.register' % (path, ext))
                            if fn:
                                warnings.warn(
                                    'Using short names for extensions has been deprecated'
                                    ' and will be removed in FeinCMS v1.8.'
                                    ' Please provide the full python path to the extension'
                                    ' %s instead (%s.%s).' % (ext, path, ext),
                                    DeprecationWarning, stacklevel=2)

                                break
                        except ImportError:
                            pass

                if not fn:
                    raise ImproperlyConfigured, '%s is not a valid extension for %s' % (
                        ext, cls.__name__)

            # Not a string, maybe a callable?
            elif hasattr(ext, '__call__'):
                fn = ext

            # Take our chances and just try to access "register"
            else:
                fn = ext.register

            cls.register_extension(fn)
            cls._feincms_extensions.add(ext)


def create_base_model(inherit_from=models.Model):
    """
    This method can  be used to create a FeinCMS base model inheriting from your
    own custom subclass (f.e. extend ``MPTTModel``). The default is to extend
    :class:`django.db.models.Model`.
    """

    class Base(inherit_from, ExtensionsMixin):
        """
        This is the base class for your CMS models. It knows how to create and
        manage content types.
        """

        class Meta:
            abstract = True

        _cached_django_content_type = None

        @classmethod
        def register_regions(cls, *regions):
            """
            Register a list of regions. Only use this if you do not want to use
            multiple templates with this model (read: not use ``register_templates``)::

                BlogEntry.register_regions(
                    ('main', _('Main content area')),
                    )
            """

            if hasattr(cls, 'template'):
                warnings.warn(
                    'Ignoring second call to register_regions.',
                    RuntimeWarning)
                return

            # implicitly creates a dummy template object -- the item editor
            # depends on the presence of a template.
            cls.template = Template('', '', regions)
            cls._feincms_all_regions = cls.template.regions

        @classmethod
        def register_templates(cls, *templates):
            """
            Register templates and add a ``template_key`` field to the model for
            saving the selected template::

                Page.register_templates({
                    'key': 'base',
                    'title': _('Standard template'),
                    'path': 'feincms_base.html',
                    'regions': (
                        ('main', _('Main content area')),
                        ('sidebar', _('Sidebar'), 'inherited'),
                        ),
                    }, {
                    'key': '2col',
                    'title': _('Template with two columns'),
                    'path': 'feincms_2col.html',
                    'regions': (
                        ('col1', _('Column one')),
                        ('col2', _('Column two')),
                        ('sidebar', _('Sidebar'), 'inherited'),
                        ),
                    })
            """

            if not hasattr(cls, '_feincms_templates'):
                cls._feincms_templates = SortedDict()
                cls.TEMPLATES_CHOICES = []

            instances = cls._feincms_templates

            for template in templates:
                if not isinstance(template, Template):
                    template = Template(**template)

                instances[template.key] = template

            try:
                field = cls._meta.get_field_by_name('template_key')[0]
            except (FieldDoesNotExist, IndexError):
                cls.add_to_class('template_key', models.CharField(_('template'), max_length=255, choices=()))
                field = cls._meta.get_field_by_name('template_key')[0]

                def _template(self):
                    ensure_completely_loaded()

                    try:
                        return self._feincms_templates[self.template_key]
                    except KeyError:
                        # return first template as a fallback if the template
                        # has changed in-between
                        return self._feincms_templates[
                            self._feincms_templates.keys()[0]]

                cls.template = property(_template)

            cls.TEMPLATE_CHOICES = field._choices = [(template.key, template.title)
                for template in cls._feincms_templates.values()]
            field.default = field.choices[0][0]

            # Build a set of all regions used anywhere
            cls._feincms_all_regions = set()
            for template in cls._feincms_templates.values():
                cls._feincms_all_regions.update(template.regions)

        #: ``ContentProxy`` class this object uses to collect content blocks
        content_proxy_class = ContentProxy

        @property
        def content(self):
            """
            Instantiate and return a ``ContentProxy``. You can use your own custom
            ``ContentProxy`` by assigning a different class to the
            ``content_proxy_class`` member variable.
            """

            if not hasattr(self, '_content_proxy'):
                self._content_proxy = self.content_proxy_class(self)

            return self._content_proxy

        @classmethod
        def _create_content_base(cls):
            """
            This is purely an internal method. Here, we create a base class for the
            concrete content types, which are built in ``create_content_type``.

            The three fields added to build a concrete content type class/model are
            ``parent``, ``region`` and ``ordering``.
            """

            # We need a template, because of the possibility of restricting content types
            # to a subset of all available regions. Each region object carries a list of
            # all allowed content types around. Content types created before a region is
            # initialized would not be available in the respective region; we avoid this
            # problem by raising an ImproperlyConfigured exception early.
            cls._needs_templates()

            class Meta:
                abstract = True
                app_label = cls._meta.app_label
                ordering = ['ordering']

            def __unicode__(self):
                return u'%s on %s, ordering %s' % (self.region, self.parent, self.ordering)

            def render(self, **kwargs):
                """
                Default render implementation, tries to call a method named after the
                region key before giving up.

                You'll probably override the render method itself most of the time
                instead of adding region-specific render methods.
                """

                render_fn = getattr(self, 'render_%s' % self.region, None)

                if render_fn:
                    return render_fn(**kwargs)

                raise NotImplementedError

            def fe_render(self, **kwargs):
                """
                Frontend Editing enabled renderer
                """

                if 'request' in kwargs:
                    request = kwargs['request']

                    if request.session and request.session.get('frontend_editing'):
                        return render_to_string('admin/feincms/fe_box.html', {
                            'content': self.render(**kwargs),
                            'identifier': self.fe_identifier(),
                            })

                return self.render(**kwargs)

            def fe_identifier(self):
                """
                Returns an identifier which is understood by the frontend editing
                javascript code. (It is used to find the URL which should be used
                to load the form for every given block of content.)
                """

                return u'%s-%s-%s-%s-%s' % (
                    cls._meta.app_label,
                    cls._meta.module_name,
                    self.__class__.__name__.lower(),
                    self.parent_id,
                    self.id,
                    )

            def get_queryset(cls, filter_args):
                return cls.objects.select_related().filter(filter_args)

            attrs = {
                '__module__': cls.__module__, # The basic content type is put into
                                              # the same module as the CMS base type.
                                              # If an app_label is not given, Django
                                              # needs to know where a model comes
                                              # from, therefore we ensure that the
                                              # module is always known.
                '__unicode__': __unicode__,
                'render': render,
                'fe_render': fe_render,
                'fe_identifier': fe_identifier,
                'get_queryset': classmethod(get_queryset),
                'Meta': Meta,
                'parent': models.ForeignKey(cls, related_name='%(class)s_set'),
                'region': models.CharField(max_length=255),
                'ordering': models.IntegerField(_('ordering'), default=0),
                }

            # create content base type and save reference on CMS class
            cls._feincms_content_model = type('%sContent' % cls.__name__,
                (models.Model,), attrs)

            # list of concrete content types
            cls._feincms_content_types = []

            # list of concrete content types having methods which may be called
            # before or after rendering the content:
            #
            # def process(self, request, **kwargs):
            #     May return a response early to short-circuit the request-response cycle
            #
            # def finalize(self, request, response)
            #     May modify the response or replace it entirely by returning a new one
            #
            cls._feincms_content_types_with_process = []
            cls._feincms_content_types_with_finalize = []

            # list of item editor context processors, will be extended by content types
            if hasattr(cls, 'feincms_item_editor_context_processors'):
                cls.feincms_item_editor_context_processors = list(cls.feincms_item_editor_context_processors)
            else:
                cls.feincms_item_editor_context_processors = []

            # list of templates which should be included in the item editor, will be extended
            # by content types
            if hasattr(cls, 'feincms_item_editor_includes'):
                cls.feincms_item_editor_includes = dict(cls.feincms_item_editor_includes)
            else:
                cls.feincms_item_editor_includes = {}

        @classmethod
        def create_content_type(cls, model, regions=None, class_name=None, **kwargs):
            """
            This is the method you'll use to create concrete content types.

            If the CMS base class is ``page.models.Page``, its database table will be
            ``page_page``. A concrete content type which is created from ``ImageContent``
            will use ``page_page_imagecontent`` as its table.

            If you want a content type only available in a subset of regions, you can
            pass a list/tuple of region keys as ``regions``. The content type will only
            appear in the corresponding tabs in the item editor.

            If you use two content types with the same name in the same module,
            name clashes will happen and the content type created first will
            shadow all subsequent content types. You can work around it by
            specifying the content type class name using the ``class_name``
            argument. Please note that this will have an effect on the entries in
            ``django_content_type``, on ``related_name`` and on the table name
            used and should therefore not be changed after running ``syncdb`` for
            the first time.

            Name clashes will also happen if a content type has defined a
            relationship and you try to register that content type to more than one
            Base model (in different modules).  Django will raise an error when it
            tries to create the backward relationship. The solution to that problem
            is, as shown above, to specify the content type class name with the
            ``class_name`` argument.

            If you register a content type to more than one Base class, it is
            recommended to always specify a ``class_name`` when registering it
            a second time.

            You can pass additional keyword arguments to this factory function. These
            keyword arguments will be passed on to the concrete content type, provided
            that it has a ``initialize_type`` classmethod. This is used f.e. in
            ``MediaFileContent`` to pass a set of possible media positions (f.e. left,
            right, centered) through to the content type.
            """

            if not class_name:
                class_name = model.__name__

            # prevent double registration and registration of two different content types
            # with the same class name because of related_name clashes
            try:
                getattr(cls, '%s_set' % class_name.lower())
                warnings.warn(
                    'Cannot create content type using %s.%s for %s.%s, because %s_set is already taken.' % (
                        model.__module__, class_name,
                        cls.__module__, cls.__name__,
                        class_name.lower()),
                    RuntimeWarning)
                return
            except AttributeError:
                # everything ok
                pass

            # Next name clash test. Happens when the same content type is created
            # for two Base subclasses living in the same Django application
            # (github issues #73 and #150)
            other_model = get_model(cls._meta.app_label, class_name)
            if other_model:
                warnings.warn(
                    'It seems that the content type %s exists twice in %s. Use the class_name argument to create_content_type to avoid this error.' % (
                        model.__name__,
                        cls._meta.app_label),
                    RuntimeWarning)

            if not model._meta.abstract:
                raise ImproperlyConfigured, 'Cannot create content type from non-abstract model (yet).'

            if not hasattr(cls, '_feincms_content_model'):
                cls._create_content_base()

            feincms_content_base = cls._feincms_content_model

            class Meta(feincms_content_base.Meta):
                db_table = '%s_%s' % (cls._meta.db_table, class_name.lower())
                verbose_name = model._meta.verbose_name
                verbose_name_plural = model._meta.verbose_name_plural

            attrs = {
                '__module__': cls.__module__, # put the concrete content type into the
                                              # same module as the CMS base type; this is
                                              # necessary because 1. Django needs to know
                                              # the module where a model lives and 2. a
                                              # content type may be used by several CMS
                                              # base models at the same time (f.e. in
                                              # the blog and the page module).
                'Meta': Meta,
                }

            new_type = type(
                class_name,
                (model, feincms_content_base,),
                attrs)
            cls._feincms_content_types.append(new_type)

            if hasattr(getattr(new_type, 'process', None), '__call__'):
                cls._feincms_content_types_with_process.append(new_type)
            if hasattr(getattr(new_type, 'finalize', None), '__call__'):
                cls._feincms_content_types_with_finalize.append(new_type)

            # content types can be limited to a subset of regions
            if not regions:
                regions = set([region.key for region in cls._feincms_all_regions])

            for region in cls._feincms_all_regions:
                if region.key in regions:
                    region._content_types.append(new_type)

            # Add a list of CMS base types for which a concrete content type has
            # been created to the abstract content type. This is needed f.e. for the
            # update_rsscontent management command, which needs to find all concrete
            # RSSContent types, so that the RSS feeds can be fetched
            if not hasattr(model, '_feincms_content_models'):
                model._feincms_content_models = []

            model._feincms_content_models.append(new_type)

            # Add a backlink from content-type to content holder class
            new_type._feincms_content_class = cls

            # Handle optgroup argument for grouping content types in the item editor
            optgroup = kwargs.pop('optgroup', None)
            if optgroup:
                new_type.optgroup = optgroup

            # customization hook.
            if hasattr(new_type, 'initialize_type'):
                new_type.initialize_type(**kwargs)
            else:
                for k, v in kwargs.items():
                    setattr(new_type, k, v)

            # collect item editor context processors from the content type
            if hasattr(model, 'feincms_item_editor_context_processors'):
                cls.feincms_item_editor_context_processors.extend(
                    model.feincms_item_editor_context_processors)

            # collect item editor includes from the content type
            if hasattr(model, 'feincms_item_editor_includes'):
                for key, includes in model.feincms_item_editor_includes.items():
                    cls.feincms_item_editor_includes.setdefault(key, set()).update(includes)

            return new_type

        @property
        def _django_content_type(self):
            if getattr(self.__class__, '_cached_django_content_type', None) is None:
                self.__class__._cached_django_content_type = ContentType.objects.get_for_model(self)
            return self.__class__._cached_django_content_type

        @classmethod
        def content_type_for(cls, model):
            """
            Return the concrete content type for an abstract content type::

                from feincms.content.video.models import VideoContent
                concrete_type = Page.content_type_for(VideoContent)
            """

            if not hasattr(cls, '_feincms_content_types') or not cls._feincms_content_types:
                return None

            for type in cls._feincms_content_types:
                if issubclass(type, model):
                    return type
            return None

        @classmethod
        def _needs_templates(cls):
            ensure_completely_loaded()

            # helper which can be used to ensure that either register_regions or
            # register_templates has been executed before proceeding
            if not hasattr(cls, 'template'):
                raise ImproperlyConfigured, 'You need to register at least one template or one region on %s.' % (
                    cls.__name__,
                    )

        @classmethod
        def _needs_content_types(cls):
            ensure_completely_loaded()

            # Check whether any content types have been created for this base class
            if not hasattr(cls, '_feincms_content_types') or not cls._feincms_content_types:
                raise ImproperlyConfigured, 'You need to create at least one content type for the %s model.' % (cls.__name__)

        def copy_content_from(self, obj):
            """
            Copy all content blocks over to another CMS base object. (Must be of the
            same type, but this is not enforced. It will crash if you try to copy content
            from another CMS base type.)
            """

            for cls in self._feincms_content_types:
                for content in cls.objects.filter(parent=obj):
                    new = copy_model_instance(content, exclude=('id', 'parent'))
                    new.parent = self
                    new.save()

        def replace_content_with(self, obj):
            """
            Replace the content of the current object with content of another.

            Deletes all content blocks and calls ``copy_content_from`` afterwards.
            """

            for cls in self._feincms_content_types:
                cls.objects.filter(parent=self).delete()
            self.copy_content_from(obj)

        @classmethod
        def register_with_reversion(cls):
            try:
                import reversion
            except ImportError:
                raise EnvironmentError("django-reversion is not installed")

            follow = []
            for content_type_model in cls._feincms_content_types:
                related_manager = "%s_set" % content_type_model.__name__.lower()
                follow.append(related_manager)
                reversion.register(content_type_model)
            reversion.register(cls, follow=follow)

    return Base

# Legacy support
Base = create_base_model()