Created
August 28, 2012 15:19
-
-
Save xen0n/3499023 to your computer and use it in GitHub Desktop.
rudimentary tag support for django-cms
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
+ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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