Здравствуйте! Эта статья скорее подходит для ознакомления. Я не сторонник всех этих тяжелых CMS, так довольно часто их приходится допиливать для конкретного проекта. Наша CMS будет прекрасно подходить для простых сайтов, типа визиток, каталогов и т.п. Теперь от слов к делу.
Что будет уметь наша CMS:
Начнем с настроек. Нам нужно добавить следующие батарейки: 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() спросит внимательный читатель? Об этом и многом другом я расскажу в следующей статье. Спасибо за внимание!