Django. Своя CMS. Часть 1

Дата публикации:29 апреля 2013 г. 1:32:54

Здравствуйте! Эта статья скорее подходит для ознакомления. Я не сторонник всех этих тяжелых CMS, так довольно часто их приходится допиливать для конкретного проекта. Наша CMS будет прекрасно подходить для простых сайтов, типа визиток, каталогов и т.п. Теперь от слов к делу.

Что будет уметь наша CMS:

  • Страницы(куда же без них). Причем я не буду использовать родные джанговские FlatPage, а сделаю свои.
  • SEO
  • Меню
  • Вроде всё. Поправьте, если я что-то еще сделал, но не указал здесь :)

 

Начнем с настроек. Нам нужно добавить следующие батарейки: mptt, south, grappelli, filebrowser и tinyMCE. Добавим их и файл настроек примет примерно такой вид(файл представлен не весь, а лишь то, что нужно добавить). Также, не забудьте указать свои настройки для tinyMCE.

Файл settings.py

TEMPLATE_CONTEXT_PROCESSORS = (
    'django.core.context_processors.debug',
    'django.core.context_processors.i18n',
    'django.core.context_processors.media',
    'django.core.context_processors.static',
    'django.contrib.auth.context_processors.auth',
    'django.core.context_processors.request',
    'django.contrib.messages.context_processors.messages',
)

INSTALLED_APPS = (
    'grappelli.dashboard',
    'grappelli',
    'django.contrib.admin',
    'filebrowser',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.admin',
    'south',
    'tinymce',
    'mptt',
    'app',
    'app.base',
    'app.pages',
)

APP_TITLE = u'Название нашей CMS'

GRAPPELLI_ADMIN_TITLE = APP_TITLE

GRAPPELLI_INDEX_DASHBOARD = 'app.dashboard.CustomIndexDashboard'

MENU_CHOICES = (
    (0, u'Верхнее'),
    (1, u'Левое'),
)

 

MENU_CHOICES это наши предустановленные меню. Почему здесь? Все равно, шаблон правите вы, а следовательно и меню добавлять/удалять будете вы. Да и базу данных лишний раз напрягать тоже не нужно. Мое мнение.

Итак, сделали syncdb и migrate. Идем дальше.

Теперь страницы. Создаем наше приложение, назовем его app. Внутри создадим две папки base и pages. В base будем хранить абстрактные классы, которые нам могут понадобится не только для страниц, но и например для товаров(если потом будем делать магазин). Итак, в папке base создадим файлы: __init__.py, models.py, fields.py и папки: migrations, templatetags и locale. Внутри папок не забудьте создать файл __init__.py.

Файл models.py

#! /usr/bin/python
# -*- coding: utf-8 -*-

from django.db import models
from django.conf import settings
from django.contrib.sites.models import Site
from django.utils.translation import ugettext as _

from app.base.fields import *

if 'tinymce' in settings.INSTALLED_APPS:
    from tinymce.models import HTMLField

class Base(models.Model):
    title = models.CharField(
        max_length = 80,
        verbose_name = _('title'),
    )
    slug = models.SlugField(
        max_length = 80,
        unique = False,
        verbose_name = _('slug'),
    )
publish = models.BooleanField(
default = True,
verbose_name = _('publish'),
) try: content = HTMLField( blank = True, verbose_name = _('content'), ) except: content = models.TextField( blank = True, verbose_name = _('content'), ) pub_date = models.DateTimeField( auto_now_add = True, verbose_name = _('date of publish'), ) change_date = models.DateTimeField( auto_now = True, verbose_name = _('date of change'), ) menu = MultiSelectField( max_length = 254, choices = settings.MENU_CHOICES, blank = True, verbose_name = _('display in menu'), ) class Meta: abstract = True

Пояснения: блок с try except в content нужен для того, если вы все таки не захотите использовать tinymce в каком-нибудь проекте. Обратите внимание, этот класс абстрактный. Он не создает в базе никаких таблиц. MultiSelectField как видно из названия позволяет выбирать несколько значений для меню, что удобно, когда у вас например меню дублируется вверху и внизу страницы. Приведу его код.

Файл fields.py

#! /usr/bin/python
# -*- coding: utf-8 -*-

from django import forms
from django.db import models
from django.conf import settings
from django.core import exceptions
from django.utils.text import capfirst

class MultiSelectFormField(forms.MultipleChoiceField):
    widget = forms.CheckboxSelectMultiple
 
    def __init__(self, *args, **kwargs):
        self.max_choices = kwargs.pop('max_choices', 0)
        super(MultiSelectFormField, self).__init__(*args, **kwargs)
 
    def clean(self, value):
        if not value and self.required:
            raise forms.ValidationError(self.error_messages['required'])
        return value
 
class MultiSelectField(models.Field):
    __metaclass__ = models.SubfieldBase
 
    def get_internal_type(self):
        return 'CharField'
 
    def get_choices_default(self):
        return self.get_choices(include_blank=False)
 
    def _get_FIELD_display(self, field):
        value = getattr(self, field.attname)
        choicedict = dict(field.choices)
 
    def formfield(self, **kwargs):
        defaults = {
            'required': not self.blank,
            'label': capfirst(self.verbose_name),
            'help_text': self.help_text,
            'choices': self.choices
        }
        if self.has_default():
            defaults['initial'] = self.get_default()
        defaults.update(kwargs)
        return MultiSelectFormField(**defaults)

    def get_prep_value(self, value):
        return value

    def get_db_prep_value(self, value, connection = None, prepared = False):
        if isinstance(value, basestring):
            return value
        elif isinstance(value, list):
            return ",".join(value)
 
    def to_python(self, value):
        if value is not None:
            return value if isinstance(value, list) else value.split(',')
        return ''

    def contribute_to_class(self, cls, name):
        super(MultiSelectField, self).contribute_to_class(cls, name)
        if self.choices:
            func = lambda self, fieldname = name, choicedict = dict(self.choices): ','.join([choicedict.get(value, value) for value in getattr(self, fieldname)])
            setattr(cls, 'get_%s_display' % self.name, func)
 
    def validate(self, value, model_instance):
        arr_choices = self.get_choices_selected(self.get_choices_default())
        for opt_select in value:
            if (int(opt_select) not in arr_choices):
                raise exceptions.ValidationError(
                    self.error_messages['invalid_choice'] % value
                )  
        return
 
    def get_choices_selected(self, arr_choices=''):
        if not arr_choices:
            return False
        list = []
        for choice_selected in arr_choices:
            list.append(choice_selected[0])
        return list
 
    def value_to_string(self, obj):
        value = self._get_val_from_obj(obj)
        return self.get_db_prep_value(value)

# Здесь укажите свой путь до вашего приложения(нужно для south)
if 'south' in settings.INSTALLED_APPS:
    from south.modelsinspector import add_introspection_rules  
    add_introspection_rules([], ['^app\.base\.fields\.MultiSelectField'])

И да, домашнее задание: причешите код в соответствии с PEP8. :) Дальше. В файле models.py в папке base создадим еще один абстрактный класс для SEO.

Файл models.py

class MetaInf(models.Model):
    site = models.ForeignKey(
        Site,
        verbose_name = _('site')
    )
    meta_title = models.CharField(
        max_length = 254,
        blank = True,
        verbose_name = _('META title'),
    )
    meta_description = models.TextField(
        blank = True,
        verbose_name = _('META description'),
    )
    meta_keywords = models.CharField(
        max_length = 254,
        blank = True,
        verbose_name = _('META keywords'),
    )
    
    class Meta:
        abstract = True
        verbose_name = _('META information')
        verbose_name_plural = _('META information')

Пояснения: site нужен для того, чтобы, например для каждого поддомена показывать свою SEO информацию. Можете убрать это поведение, но мне кажется это удобным. Дальше. Идем в папку templatetags и создаем там файл app_tags.py

Файл app_tags.py

#! /usr/bin/python
# -*- coding: utf-8 -*-

from django import template
from django.conf import settings

register = template.Library()

@register.simple_tag()
def site_title():
    title = settings.APP_TITLE
    return title

Это нужно для того, чтобы показывать в шаблоне название сайта. Можете этого не делать, но это удобно. Также, можно это сделать через context процессор, но мне и так нравится. Дальше. Идем в папку pages и создаем там файл models.py, admin.py, __init__.py, utils.py, views.py и папки migrations, templatetags и locale.

Файл models.py

#! /usr/bin/python
# -*- coding: utf-8 -*-

from django.db import models
from django.forms import ValidationError
from django.utils.translation import ugettext as _
from django.core.exceptions import ObjectDoesNotExist

import mptt
from mptt.fields import TreeForeignKey

from app.pages.utils import *
from app.base.models import Base, MetaInf

class Page(Base):
    homepage = models.BooleanField(
        default = False,
        verbose_name = _('Homepage'),
    )
    parent = TreeForeignKey(
        'self',
        null = True,
        blank = True,
        related_name = 'children',
        verbose_name = _('parent'),
    )
    
    def save(self, *args, **kwargs):
        if self.homepage is True:
            try:
                page = Page.objects.get(homepage = True)
                page.homepage = False
                page.save()
            except ObjectDoesNotExist:
                self.homepage = True
        return super(Page, self).save(*args, **kwargs)
   
    def clean(self):
        if Page.objects.filter(slug = self.slug, parent = None).exclude(
            id = self.id).exists() and self.parent is None:
                raise ValidationError(
                    _('Record with this slug already exists')
                )

    class Meta:
        verbose_name = _('page')
        ordering = ('-homepage', '-pub_date', '-change_date',)
        verbose_name_plural = _('pages')
        unique_together = ('parent', 'slug')
    
    def get_absolute_url(self):
        if self.parent is None and self.homepage is False:
            return '/page/%s/' % self.slug
        if self.homepage:
            return '/'
        url = get_all_ancestors(self.parent, self.slug)
        return '/page%s' % url
    get_absolute_url.short_description = _('URL')
        
    def __unicode__(self):
        return self.title

mptt.register(Page)

class PageMetaInf(MetaInf):
    page = models.ForeignKey(
        Page,
        verbose_name = _('page'),
    )
    
    class Meta:
        unique_together = ('page', 'site')
        verbose_name = _('META information')
        verbose_name_plural = _('META information')

Здесь мы наследуемся от класса Base в классе Page и в классе PageMetaInf от класса MetaInf. Как видно, есть главная, двух главных быть не может, поэтому в методе save() мы это проверяем и исправляем. Также, не может быть двух и более страниц с одинаковыми родителями и урлами, это мы тоже проверяем. В классе PageMetaInf мы проверяем нет ли одинаковых site и page, чтобы исключить возможность повторения двух SEO данных для одной страницы в одном домене. Что за метод get_all_ancestors() спросит внимательный читатель? Об этом и многом другом я расскажу в следующей статье. Спасибо за внимание!

Метки:django, cms, система управления, python