public
Created

rudimentary tag support for django-cms

  • Download Gist
admin-forms.patch
Diff
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
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)
cms-forms.patch
Diff
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
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
+
cms-models.patch
Diff
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
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
pageadmin.patch
Diff
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
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']:

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.