Skip to content

Instantly share code, notes, and snippets.

@xen0n
Created August 28, 2012 15:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xen0n/3499023 to your computer and use it in GitHub Desktop.
Save xen0n/3499023 to your computer and use it in GitHub Desktop.
rudimentary tag support for django-cms
diff -Nur a//admin/forms.py b//admin/forms.py
--- a//admin/forms.py 2012-07-17 09:56:36.060026507 +0800
+++ b//admin/forms.py 2012-08-28 23:14:08.606910058 +0800
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
from cms.apphook_pool import apphook_pool
from cms.forms.widgets import UserSelectAdminWidget
+from cms.forms.textmodelfield import TextModelMultipleChoiceField
from cms.models import (Page, PagePermission, PageUser, ACCESS_PAGE,
- PageUserGroup)
+ PageUserGroup, Tag)
from cms.utils.mail import mail_page_user_change
from cms.utils.page import is_valid_page_slug
from cms.utils.page_resolver import get_page_from_path
@@ -152,6 +153,9 @@
help_text=_('A description of the page sometimes used by search engines.'))
meta_keywords = forms.CharField(label='Keywords meta tag', max_length=255, required=False,
help_text=_('A list of comma seperated keywords sometimes used by search engines.'))
+
+ tags = TextModelMultipleChoiceField(Tag.objects.all(), label=_('Page tags'), required=False,
+ help_text=_('Tags for the page, separated by commas (,)'))
def __init__(self, *args, **kwargs):
super(PageForm, self).__init__(*args, **kwargs)
diff -Nur a//forms/textmodelfield.py b//forms/textmodelfield.py
--- a//forms/textmodelfield.py 1970-01-01 08:00:00.000000000 +0800
+++ b//forms/textmodelfield.py 2012-08-28 22:50:14.726826638 +0800
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+#
+# written mainly for the tags -- text-based ModelMultipleChoiceField
+
+from django.core.exceptions import ValidationError, FieldError
+from django.utils.encoding import smart_unicode, force_unicode
+
+from django.forms.models import ModelChoiceField, ModelMultipleChoiceField
+from django.forms.widgets import HiddenInput, MultipleHiddenInput
+from django.utils.translation import ugettext_lazy as _
+
+# homebrew Select(Multiple)-TextInput hybrid
+from cms.forms.widgets import TextSelect
+
+# Default separator used
+DEFAULT_SEPARATOR = u','
+
+class TextModelMultipleChoiceField(ModelChoiceField):
+ """
+ A text-based ModelMultipleChoiceField.
+ """
+
+ widget = TextSelect
+ hidden_widget = MultipleHiddenInput
+ default_error_messages = {
+ 'list': _(u'Enter a list of values.'),
+ 'invalid_choice': _(u'Select a valid choice. %s is not one of the'
+ u' available choices.'),
+ 'invalid_pk_value': _(u'"%s" is not a valid value for a primary key.')
+ }
+
+ def __init__(self, queryset, cache_choices=False, required=True,
+ widget=None, label=None, initial=None,
+ help_text=None, separator=DEFAULT_SEPARATOR, *args, **kwargs):
+ super(TextModelMultipleChoiceField, self).__init__(queryset, None,
+ cache_choices, required, widget, label, initial, help_text,
+ *args, **kwargs)
+
+ self.separator = separator
+
+ # the prop is for appropriate syncing with widget
+ def _get_separator(self):
+ return self._separator
+
+ def _set_separator(self, new_separator):
+ self._separator = self.widget.separator = new_separator
+
+ separator = property(_get_separator, _set_separator)
+
+ def clean(self, value):
+ # This field's reason for existing is just enabling quick tag edit, so
+ # no matching against some "choices" is done.
+ # NOTE: Saving happens in PageAdmin.save_model()
+
+ # XXX eh... why is value a one-item list?
+ value = value[0]
+
+ # Some sanity checking is still required...
+ # print u'Text.M.M.C.Field: clean: value "%s"' % value
+ if self.required and not value:
+ raise ValidationError(self.error_messages['required'])
+ if not isinstance(value, unicode):
+ # FIXME: i18n
+ raise ValidationError(self.error_messages['list'])
+
+ # Just return the "raw" Unicode choice string.
+ return value
+
+ def prepare_value(self, value):
+ if hasattr(value, '__iter__'):
+ return [super(TextModelMultipleChoiceField, self).prepare_value(v) for v in value]
+ return super(TextModelMultipleChoiceField, self).prepare_value(value)
+
diff -Nur a//forms/widgets.py b//forms/widgets.py
--- a//forms/widgets.py 2012-07-17 09:56:36.076693174 +0800
+++ b//forms/widgets.py 2012-08-28 23:04:54.690211166 +0800
@@ -6,12 +6,16 @@
from django.conf import settings
from django.contrib.sites.models import Site
from django.forms.widgets import Select, MultiWidget, Widget
+from django.forms.util import flatatt
from django.template.context import RequestContext
from django.template.loader import render_to_string
+from django.utils.datastructures import MultiValueDict, MergeDict
from django.utils.encoding import force_unicode
+from django.utils.html import escape, conditional_escape
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
import copy
+from itertools import chain
from cms.templatetags.cms_admin import admin_static_url
class PageSelectWidget(MultiWidget):
@@ -218,3 +222,117 @@
# 'admin/cms/page/widgets/plugin_editor.html', context))
return mark_safe(render_to_string(
'admin/cms/page/widgets/placeholder_editor.html', context, RequestContext(self.request)))
+
+
+class TextSelect(Widget):
+ '''\
+ TextInput-like widget providing a Select(Multiple) interface.
+
+ This is actually a COPY of Django's Select widget with some minor
+ modifications to make it happy with text, mainly borrowing from
+ the TextInput and SelectMultiple class.
+ '''
+
+ def __init__(self, attrs=None, choices=(), separator=u','):
+ super(TextSelect, self).__init__(attrs)
+ # choices can be any iterable, but we may need to render this widget
+ # multiple times. Thus, collapse it into a list so it can be consumed
+ # more than once.
+ # print u'TextSelect ctor: attrs %s, choices %s' % (
+ # str(attrs), str(choices))
+ self.choices = list(choices)
+ self.separator = separator
+
+ def render(self, name, value, attrs=None, choices=()):
+ # print u'TextSelect.render: value %s, choices %s' % (
+ # repr(value), repr(choices))
+ # this part is hinted by TextInput...
+ if value is None:
+ value = ''
+
+ final_attrs = self.build_attrs(attrs, type=u'text', name=name)
+ # print u'TextSelect.render: finalattr %s' % (repr(final_attrs), )
+
+ # output = [u'<select%s>' % flatatt(final_attrs)]
+ # options = self.render_options(choices, [value])
+ # if options:
+ # output.append(options)
+ # output.append(u'</select>')
+ # return mark_safe(u'\n'.join(output))
+
+ if value != '':
+ # Only add the 'value' attribute if a value is non-empty.
+ final_attrs['value'] = self.render_options(choices, value)
+
+ # DEBUG
+ # print u'TextSelect.render: <input%s />' % flatatt(final_attrs)
+ return mark_safe(u'<input%s />' % flatatt(final_attrs))
+
+
+ def render_option(self, selected_choices, option_value, option_label):
+ # print u'TextSelect option: selected %s, val %s, lbl %s' % (
+ # repr(selected_choices),
+ # repr(option_value),
+ # repr(option_label), )
+
+ option_value = force_unicode(option_value)
+ selected = option_value in selected_choices
+ if not selected:
+ # not selected, don't render this entry
+ return None
+
+ # return u'<option value="%s"%s>%s</option>' % (
+ # escape(option_value), selected_html,
+ # conditional_escape(force_unicode(option_label)))
+ return conditional_escape(force_unicode(option_label))
+
+ def render_options(self, choices, selected_choices):
+ # print u'TextSelect.render_options: self.choices %s' % (
+ # repr(self.choices), )
+ # Only render the selected values...
+
+ # Normalize to strings.
+ selected_choices = set([force_unicode(v) for v in selected_choices])
+ # print u'TextSelect.render_options: selected %s' % (
+ # repr(selected_choices), )
+
+ output = []
+ for option_value, option_label in chain(self.choices, choices):
+ # print u'TextSelect.render_options: optval:', option_value
+ # print u'TextSelect.render_options: optlbl:', option_label
+
+ if isinstance(option_label, (list, tuple)):
+ # output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value)))
+ for option in option_label:
+ entry = self.render_option(selected_choices, *option)
+
+ if entry is not None:
+ output.append(entry)
+ # output.append(u'</optgroup>')
+ else:
+ entry = self.render_option(selected_choices, option_value, option_label)
+
+ if entry is not None:
+ output.append(entry)
+
+
+ # print u'TextSelect.render_options: output: %s' % (
+ # self.separator.join(output))
+ return self.separator.join(output)
+
+ def value_from_datadict(self, data, files, name):
+ if isinstance(data, (MultiValueDict, MergeDict)):
+ return data.getlist(name)
+ return data.get(name, None)
+
+ def _has_changed(self, initial, data):
+ if initial is None:
+ initial = []
+ if data is None:
+ data = []
+ if len(initial) != len(data):
+ return True
+ initial_set = set([force_unicode(value) for value in initial])
+ data_set = set([force_unicode(value) for value in data])
+ return data_set != initial_set
+
diff -Nur a//models/__init__.py b//models/__init__.py
--- a//models/__init__.py 2012-07-17 09:55:13.610021710 +0800
+++ b//models/__init__.py 2012-08-28 22:19:05.210051209 +0800
@@ -10,6 +10,7 @@
from placeholdermodel import *
from pluginmodel import *
from titlemodels import *
+from tagmodel import *
import django.core.urlresolvers
# must be last
from cms import signals as s_import
diff -Nur a//models/pagemodel.py b//models/pagemodel.py
--- a//models/pagemodel.py 2012-07-17 09:56:36.113359844 +0800
+++ b//models/pagemodel.py 2012-08-28 22:19:35.870052992 +0800
@@ -4,6 +4,7 @@
from cms.models.metaclasses import PageMetaClass
from cms.models.placeholdermodel import Placeholder
from cms.models.pluginmodel import CMSPlugin
+from cms.models.tagmodel import Tag
from cms.publisher.errors import MpttPublisherCantPublish
from cms.utils import i18n, urlutils, page as page_utils
from cms.utils.copy_plugins import copy_plugins_to
@@ -92,6 +93,9 @@
publisher_public = models.OneToOneField('self', related_name='publisher_draft', null=True, editable=False)
publisher_state = models.SmallIntegerField(default=0, editable=False, db_index=True)
+ # Tagging support
+ tags = models.ManyToManyField(Tag, blank=True)
+
# Managers
objects = PageManager()
permissions = PagePermissionsPermissionManager()
@@ -1072,6 +1076,85 @@
self.placeholders.add(placeholder)
found[placeholder_name] = placeholder
+ def get_tags(self):
+ return self.tags.all()
+
+ def get_tag_string(self):
+ return u', '.join(tag.name for tag in self.get_tags())
+
+ def set_tags(self, new_tagstr, separator=u','):
+ '''\
+ Updates tag setting of current page, returning a "canonical" form
+ of ManyToManyField representation for overwriting the custom form
+ field.
+ '''
+ # XXX FIXME: manual transaction here!!!
+ # raise NotImplementedError
+
+ # get the lists and make out some differences...
+ # hit the db once
+ old_tagobj = [(tag.pk, tag.name, ) for tag in self.tags.all()]
+
+ # when creating new page, this incoming var happens to be an empty
+ # list...
+ # that situation is handled specially...
+ if issubclass(type(new_tagstr), list):
+ new_taglst = [i.strip() for i in new_tagstr]
+ else:
+ new_taglst = [i.strip() for i in new_tagstr.split(separator)]
+ old_taglst = [name for pk, name in old_tagobj]
+
+ appended_tags = [i for i in new_taglst if i not in old_taglst]
+ removed_tags = [i for i in old_taglst if i not in new_taglst]
+
+ # scratch area...
+ new_pk = [pk for pk, name in old_tagobj]
+ # sanity check not needed, because any comma is discarded in the
+ # splitting process.
+
+ # Tag's (default) manager...
+ # this will save some run-time bindings
+ tagmanager = Tag.objects
+
+ if len(new_taglst) == 1 and new_taglst[0] == u'':
+ # FIX: DON'T CREATE A TAG WITH EMPTY NAME HERE!!
+ # Just delete all tags, and return.
+ pass
+ else:
+ # process added tags
+ for tag in appended_tags:
+ # get a Tag with the name specified by request,
+ # creating a new one if there isn't one...
+ obj, created = tagmanager.get_or_create(name=tag)
+ # print (u'Page: created tag %s, id %d' if created
+ # else u'Page: found tag %s, id %d') % (tag, obj.pk, )
+
+ self.tags.add(obj)
+ new_pk.append(obj.pk)
+
+ # process removed tags
+ for tag in removed_tags:
+ obj = tagmanager.get(name=tag)
+ # print u'Page: removing tag %s, id %d' % (tag, obj.pk, )
+
+ self.tags.remove(obj)
+ # since this association is unique, using remove() should be OK
+ new_pk.remove(obj.pk)
+
+ # if the "reference count" drops to zero, the tag
+ # should be removed...
+ # FIXME: is count() OK here?
+ if obj.page_set.all().count() == 0:
+ # print u'Page: deleting unused tag %s' % tag
+ obj.delete()
+
+ # print u'Page: tag update finished, new_pk=%s' % repr(new_pk)
+ # return True
+ # XXX: the admin form expects a "vanilla" form of ManyToManyField
+ # representation which is a list of pk's, so we return the new_pk...
+ return new_pk
+
+
def _reversion():
exclude_fields = ['publisher_is_draft', 'publisher_public', 'publisher_state']
diff -Nur a//models/tagmodel.py b//models/tagmodel.py
--- a//models/tagmodel.py 1970-01-01 08:00:00.000000000 +0800
+++ b//models/tagmodel.py 2012-08-28 21:44:01.466057816 +0800
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+#
+# tagging support for django-cms
+
+from django.db import models
+
+class Tag(models.Model):
+ name = models.CharField(unique=True, max_length=32)
+
+ class Meta:
+ app_label = 'cms'
+
+ def __unicode__(self):
+ return u'%s' % self.name
diff -Nur a//admin/pageadmin.py b//admin/pageadmin.py
--- a//admin/pageadmin.py 2012-07-17 09:56:36.076693174 +0800
+++ b//admin/pageadmin.py 2012-08-28 22:19:57.363387576 +0800
@@ -9,7 +9,7 @@
from cms.exceptions import NoPermissionsException
from cms.forms.widgets import PluginEditor
from cms.models import (Page, Title, CMSPlugin, PagePermission,
- PageModeratorState, EmptyTitle, GlobalPagePermission)
+ PageModeratorState, EmptyTitle, GlobalPagePermission, Tag)
from cms.models.managers import PagePermissionsPermissionManager
from cms.models.placeholdermodel import Placeholder
from cms.plugin_pool import plugin_pool
@@ -96,6 +96,8 @@
advanced_fields.append("navigation_extenders")
if apphook_pool.get_apphooks():
advanced_fields.append("application_urls")
+ if settings.CMS_TAGS:
+ general_fields.append('tags')
fieldsets = [
(None, {
@@ -283,6 +285,12 @@
language,
)
+ # Tag hook
+ new_tags = form.cleaned_data['tags']
+ if obj is not None:
+ if obj.has_change_permission(request):
+ form.cleaned_data['tags'] = obj.set_tags(new_tags)
+
# is there any moderation message? save/update state
if settings.CMS_MODERATOR and 'moderator_message' in form.cleaned_data and \
form.cleaned_data['moderator_message']:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment