#!/usr/bin/env python # -*- coding: UTF-8 -*- """ Title: Content manager models Author: Will Hardy Project: Content manager Date: September 2007 Test Suite: python ./tests.py $Revision: 253 $ Copyright: Will Hardy 2007, released under GPL version 3 (see licence.txt) Description: A very simple content management system, that uses `Pages' for everything imaginable. The main focus is simplicity, keeping the codebase as small as possible. Everything is centered around Pages, the user only needs to make pages and link to them using the wiki syntax. The only exception is the introduction of "special pages", so that core pages can be found by a key instead of relying on the name of the page (which can change depending on language etc). Improvements: """ from django.utils.translation import ugettext as _ from django.db import models from django.utils.encoding import smart_unicode from django.utils.translation import ugettext as _ from django.db.models import Q from django.conf import settings from hardytools.content.templatetags import wiki_engine __all__ = ('Page', 'SpecialPage', 'Translation', 'Image') # If the MULTILINGUAL_CONTENT setting is on, then put ourselves into MONOLINGUAL mode. # There should be no difference between the two modes on the database level, only in the interface. if hasattr(settings, "MULTILINGUAL_CONTENT") and settings.MULTILINGUAL_CONTENT: MONOLINGUAL = False else: MONOLINGUAL = True class Page(models.Model): """ A page for the site. As simple as possible: just a title and some text. Improvements: * Description for META tags in the HTML and organisation * Larger sites may require better organisation features: labels etc """ if MONOLINGUAL: title_editable = True title_blank = False else: title_editable = False title_blank = True title = models.CharField(_("title"), max_length=40, editable=title_editable, default="", blank=title_blank) description = models.CharField(_("description"), max_length=200, blank=True, default="") def getName(self): " Chooses a meaningful title, possibly using a translation. " if MONOLINGUAL: return self.title else: translation = self.getTranslation() if translation: return translation.title # Fallback to description? return '(%s)' % _('no title') name = property(getName) def __unicode__(self): return self.name class Admin: list_display = ('title', 'description') class Meta: verbose_name = _("page") verbose_name_plural = _("pages") ordering = ['title'] def get_absolute_url(self): return '/%s/' % wiki_engine.urlify(self.title) def getTranslation(self, language=None): " Gets the translation for the given language, or just any translation. " if not MONOLINGUAL and language: #if language: # Try the selected language try: return self.translation_set.get(language=language) except Exception: pass # Try the default language, if given if not MONOLINGUAL and hasattr(settings, "LANGUAGE_CODE") and (not language or settings.LANGUAGE_CODE != language): try: return self.translation_set.get(language__startswith=settings.LANGUAGE_CODE) except Exception: pass # Try any random language try: return self.translation_set.all()[0] except IndexError: raise Translation.DoesNotExist('No matching translation for this page.') def save(self): " Sets extra fields on save, if in mono-lingual mode. " if not MONOLINGUAL: # Title is then under complete control of the system self.title = self.name super(Page, self).save() def getIntroText(self, length=50): # Not necessary with description, perhaps as a fallback if len(self.text) > length: return self.text[:length-3] + "..." else: return self.text[:length] sample = property(getIntroText) def is_redirection(self): " Determines if this page is a redirect page or not. " redirection = wiki_engine.isRedirect(self.text) if redirection: (self.redirect_mode, self.redirect) = redirection return True return False is_redirection.boolean = True is_redirection.short_description = _('is redirection?') def is_redirection_of(self, page): " Checks if this page is a redirection for the given page. " if self.is_redirect(): if self.redirect_mode == 'internal' and self.redirect == page.name: return True return False def avoid_name_collision(self): """ The last line of defence against corrupt data. This isn't too pretty, but the user will know what to do and no title will be duplicate. """ # 1. title must be unique for the given site for new objects if not self.id and Page.objects.filter(title=self.title).count() == 0: return # 2. title must be unique for the given site for existing objects if self.id: previous_object = Page.objects.get(id=self.id) if previous_object.title == self.title or Page.objects.filter(title=self.title).count() == 0: return try: existing_object = Page.objects.get(title=self.title) if existing_object.is_redirection() and existing_object.redirect == previous_object.title: existing_object.delete() return except Exception: pass # Create a list of unavailable title from the database query_set = Page.objects.filter(title__startswith=self.title) # If this object is already in the database, exclude it from the results if self.id: query_set = query_set.exclude(id=self.id) # Generate the list of title title_list = [] for item in query_set.values("title"): title_list.append(item["title"]) # If any items are found, we'll need to check them if title_list: base_title = self.title.strip("0123456789").strip("_") index = 2 # Make a unique new version if necessary while self.title in title_list: self.title = '%s_%d' % (base_title, index) index += 1 def old_save(self): """ I doubt this is useful anymore """ # Remember title in case it changes (updates only) if self.id: original_title = Page.objects.get(id=self.id).title else: original_title = None # Only save if this is valid self.avoid_name_collision() super(Page, self).save() # If title has changed, create a redirect page # this should have a unique title, because we have just renamed the old one. if original_title and self.title != original_title: # I would love to avoid doing this when not necessary redirect_page = Page() redirect_page.title = original_title redirect_page.description = '#REDIRECT [[%s]]' % self.title redirect_page.save() def __getUrlName(self): " This means, names with underscores might be masked! " return smart_unicode(self.name).replace(' ','_') url_name = property(__getUrlName) @staticmethod def getPage(title, language=None, spaces=False): """ We're actually returning a translation of a page, but the caller doesn't need to know that. It'll quack like the duck they're looking for. """ return Translation.getTranslation(title, language, spaces) # The Translation model is customised for the multi-lingual interface if MONOLINGUAL: translations_in_admin = 1 max_translations_in_admin = 1 language_editable = False show_title = False text_label = " " # This is in the class name for mono-lingual sites ;-) else: translations_in_admin = 3 max_translations_in_admin = None language_editable = True show_title = True text_label = _("text") if hasattr(settings, "LANGUAGES"): language_choices = settings.LANGUAGES else: language_choices = None class Translation(models.Model): """ Allows multi-lingual versions of a page, without too much complication. """ page = models.ForeignKey(Page, edit_inline=models.TABULAR, num_in_admin=translations_in_admin, max_num_in_admin=max_translations_in_admin, verbose_name=_("page")) language = models.CharField(_("language"), max_length=5, choices=language_choices, null=True, editable=language_editable) title = models.CharField(_("title"), max_length=50, default="", core=show_title, blank=(not show_title), editable=show_title) text = models.TextField(text_label, default="", core=(not show_title), blank=show_title) # Store the rendered version for quick access html = models.TextField(_("html"), editable=False, default="", blank=True) def __unicode__(self): return u'%s (%s)' % (self.page, self.language) class Admin: pass class Meta: # Usability: removing the idea of "translation" when this is a monolingual interface. if MONOLINGUAL: verbose_name = _('text') verbose_name_plural = _('text') # There is only one for monolingual sites else: verbose_name = _('translation') def get_absolute_url(self): if MONOLINGUAL: return u'/%s/' % (wiki_engine.urlify(self.title)) else: return u'/%s/%s/' % (self.language, wiki_engine.urlify(self.title)) def save(self): " Sets extra fields on save, if in mono-lingual mode. " if MONOLINGUAL and self.page_id: if not self.title: self.title = self.page.title if hasattr(settings, "LANGUAGE_CODE") and not self.language: self.language = settings.LANGUAGE_CODE # Update the rendered version self.html = wiki_engine.dewiki(self.text) super(Translation, self).save() @staticmethod def getTranslation(title, language=None, spaces=False): """ Try really hard to return a matching or even just useful translation. """ # Sub in spaces, if required if not spaces: title = smart_unicode(title).replace('-',' ').replace('_',' ') # Try looking for our page using the given language if language: try: # Currently does not support mixing parent languages with variants (i.e. "en" and "en-AU") return Translation.objects.get(title__iexact=title, language__startswith=language) except Exception: pass # Try getting our page in the default language if hasattr(settings, "LANGUAGE_CODE") and (not language or settings.LANGUAGE_CODE != language): try: # Currently does not support mixing parent languages with variants (i.e. "en" and "en-AU") return Translation.objects.get(title__iexact=title, language__startswith=settings.LANGUAGE_CODE) except Exception: pass # Otherwise, try any old page try: translations = Translation.objects.filter(title__iexact=title) return translations[0] except Exception: # Give up return None def getAlternatives(self): if self.page_id: return self.page.translation_set.all().exclude(id=self.id) else: return None def is_redirection(self): " Determines if this page is a redirect page or not. " redirection = wiki_engine.isRedirect(self.text) if redirection: (self.redirect_mode, self.redirect) = redirection return True return False is_redirection.boolean = True is_redirection.short_description = _('is redirection?') class SpecialPage(models.Model): """ Links particular pages to a key, which can be relied on to automatically retrieve specific pages. We can use Pages to store everying, even parts of a page: navigation, contact info, home_page, stylesheet Using this framework, we can find the right page without relying on its name. This makes i18n possible, and lets users choose the names they want. """ key = models.CharField(_("special key"), max_length=40) page = models.ForeignKey(Page, verbose_name=_("page"), null=True, blank=True) description = models.CharField(_("description"), max_length=200, default="", blank=True) def __unicode__(self): return u'%s (%s)' % (self.key, self.page) class Admin: list_display = ('key', 'description', 'adminEditPageLink') class Meta: " Verbose names for translation " verbose_name = _("special page") verbose_name_plural = _("special pages") ordering = ['key'] def adminEditPageLink(self): link = '/admin/content/page/%d/' % (self.page.id) return '%s (edit page)' % (self.page, link) adminEditPageLink.short_description = _('page') adminEditPageLink.allow_tags = True @staticmethod def getPage(key, language=None): " Get a page using the given key, or return None (no exceptions raised). " try: special_page = SpecialPage.objects.get(key=key) return special_page.page except Exception: return None @staticmethod def getPageText(key, language=None): " Get the text from a page using the given key, or return None (no exceptions raised). " page = SpecialPage.getPage(key) if page: translation = page.getTranslation(language=language) if translation: return smart_unicode(translation.html) return "" class Image(models.Model): " Image databank for uploading images. " name = models.CharField(max_length=50, verbose_name=_('name'), help_text=_("A name to help organise your images.")) image = models.FileField(upload_to='images/') description = models.CharField(max_length=200, help_text=_("This description helps blind people and search engines understand what is in the image."), blank=True, default="") def __unicode__(self): return self.name class Admin: list_display = ('name', 'image', 'description') class Meta: verbose_name = _("image") verbose_name_plural = _("images")