Prerequisites:
* Python 2.5.4+ <http://www.python.org/>
* Django 1.2+ <http://www.djangoproject.com/>
- * django-mptt 0.4+ <https://github.com/django-mptt/django-mptt/>
+ * django-mptt e734079+ <https://github.com/django-mptt/django-mptt/>
* (Optional) django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>
* (Optional) recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>
* (Optional) south 0.7.2+ <http://south.aeracode.org/>
3. include 'philo.urls' somewhere in your urls.py file.
4. Optionally add a root node to your current Site.
-Philo should be ready to go!
\ No newline at end of file
+Philo should be ready to go!
* [Python 2.5.4+ <http://www.python.org>](http://www.python.org/)
* [Django 1.2+ <http://www.djangoproject.com/>](http://www.djangoproject.com/)
- * [django-mptt 0.4+ <https://github.com/django-mptt/django-mptt/>](https://github.com/django-mptt/django-mptt/)
+ * [django-mptt e734079+ <https://github.com/django-mptt/django-mptt/>](https://github.com/django-mptt/django-mptt/)
* (Optional) [django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>](http://code.google.com/p/django-grappelli/)
* (Optional) [south 0.7.2+ <http://south.aeracode.org/)](http://south.aeracode.org/)
* (Optional) [recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>](http://code.google.com/p/recaptcha-django/)
3. include 'philo.urls' somewhere in your urls.py file.
4. Optionally add a root node to your current Site.
-Philo should be ready to go!
\ No newline at end of file
+Philo should be ready to go!
from django.utils import simplejson as json
from django.utils.html import escape
from philo.models import Tag, Attribute
-from philo.forms import AttributeForm, AttributeInlineFormSet
+from philo.models.fields.entities import ForeignKeyAttribute, ManyToManyAttribute
+from philo.admin.forms.attributes import AttributeForm, AttributeInlineFormSet
from philo.admin.widgets import TagFilteredSelectMultiple
+from philo.forms.entities import EntityForm, proxy_fields_for_entity_model
from mptt.admin import MPTTModelAdmin
template = 'admin/philo/edit_inline/tabular_attribute.html'
+def hide_proxy_fields(cls, attname, proxy_field_set):
+ val_set = set(getattr(cls, attname))
+ if proxy_field_set & val_set:
+ cls._hidden_attributes[attname] = list(val_set)
+ setattr(cls, attname, list(val_set - proxy_field_set))
+
+
+class EntityAdminMetaclass(admin.ModelAdmin.__metaclass__):
+ def __new__(cls, name, bases, attrs):
+ # HACK to bypass model validation for proxy fields by masking them as readonly fields
+ new_class = super(EntityAdminMetaclass, cls).__new__(cls, name, bases, attrs)
+ form = getattr(new_class, 'form', None)
+ if form:
+ opts = form._meta
+ if issubclass(form, EntityForm) and opts.model:
+ proxy_fields = proxy_fields_for_entity_model(opts.model).keys()
+ readonly_fields = new_class.readonly_fields
+ new_class._real_readonly_fields = readonly_fields
+ new_class.readonly_fields = list(readonly_fields) + proxy_fields
+
+ # Additional HACKS to handle raw_id_fields and other attributes that the admin
+ # uses model._meta.get_field to validate.
+ new_class._hidden_attributes = {}
+ proxy_fields = set(proxy_fields)
+ hide_proxy_fields(new_class, 'raw_id_fields', proxy_fields)
+ #END HACK
+ return new_class
+
+
class EntityAdmin(admin.ModelAdmin):
+ __metaclass__ = EntityAdminMetaclass
+ form = EntityForm
inlines = [AttributeInline]
save_on_top = True
+
+ def __init__(self, *args, **kwargs):
+ # HACK PART 2 restores the actual readonly fields etc. on __init__.
+ if hasattr(self, '_real_readonly_fields'):
+ self.readonly_fields = self.__class__._real_readonly_fields
+ if hasattr(self, '_hidden_attributes'):
+ for name, value in self._hidden_attributes.items():
+ setattr(self, name, value)
+ # END HACK
+ super(EntityAdmin, self).__init__(*args, **kwargs)
+
+ def formfield_for_dbfield(self, db_field, **kwargs):
+ """
+ Override the default behavior to provide special formfields for EntityEntitys.
+ Essentially clones the ForeignKey/ManyToManyField special behavior for the Attribute versions.
+ """
+ if not db_field.choices and isinstance(db_field, (ForeignKeyAttribute, ManyToManyAttribute)):
+ request = kwargs.pop("request", None)
+ # Combine the field kwargs with any options for formfield_overrides.
+ # Make sure the passed in **kwargs override anything in
+ # formfield_overrides because **kwargs is more specific, and should
+ # always win.
+ if db_field.__class__ in self.formfield_overrides:
+ kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs)
+
+ # Get the correct formfield.
+ if isinstance(db_field, ManyToManyAttribute):
+ formfield = self.formfield_for_manytomanyattribute(db_field, request, **kwargs)
+ elif isinstance(db_field, ForeignKeyAttribute):
+ formfield = self.formfield_for_foreignkeyattribute(db_field, request, **kwargs)
+
+ # For non-raw_id fields, wrap the widget with a wrapper that adds
+ # extra HTML -- the "add other" interface -- to the end of the
+ # rendered output. formfield can be None if it came from a
+ # OneToOneField with parent_link=True or a M2M intermediary.
+ # TODO: Implement this.
+ #if formfield and db_field.name not in self.raw_id_fields:
+ # formfield.widget = admin.widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field, self.admin_site)
+
+ return formfield
+ return super(EntityAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+
+ def formfield_for_foreignkeyattribute(self, db_field, request=None, **kwargs):
+ """Get a form field for a ForeignKeyAttribute field."""
+ db = kwargs.get('using')
+ if db_field.name in self.raw_id_fields:
+ kwargs['widget'] = admin.widgets.ForeignKeyRawIdWidget(db_field, db)
+ #TODO: Add support for radio fields
+ #elif db_field.name in self.radio_fields:
+ # kwargs['widget'] = widgets.AdminRadioSelect(attrs={
+ # 'class': get_ul_class(self.radio_fields[db_field.name]),
+ # })
+ # kwargs['empty_label'] = db_field.blank and _('None') or None
+
+ return db_field.formfield(**kwargs)
+
+ def formfield_for_manytomanyattribute(self, db_field, request=None, **kwargs):
+ """Get a form field for a ManyToManyAttribute field."""
+ db = kwargs.get('using')
+
+ if db_field.name in self.raw_id_fields:
+ kwargs['widget'] = admin.widgets.ManyToManyRawIdWidget(db_field, using=db)
+ kwargs['help_text'] = ''
+ #TODO: Add support for filtered fields.
+ #elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
+ # kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
+
+ return db_field.formfield(**kwargs)
class TreeAdmin(MPTTModelAdmin):
pass
-class TreeEntityAdmin(TreeAdmin, EntityAdmin):
+class TreeEntityAdmin(EntityAdmin, TreeAdmin):
pass
--- /dev/null
+from philo.admin.forms.attributes import *
+from philo.admin.forms.containers import *
\ No newline at end of file
--- /dev/null
+from django.contrib.contenttypes.generic import BaseGenericInlineFormSet
+from django.contrib.contenttypes.models import ContentType
+from django.forms.models import ModelForm
+from philo.models import Attribute
+
+
+__all__ = ('AttributeForm', 'AttributeInlineFormSet')
+
+
+class AttributeForm(ModelForm):
+ """
+ This class handles an attribute's fields as well as the fields for its value (if there is one.)
+ The fields defined will vary depending on the value type, but the fields for defining the value
+ (i.e. value_content_type and value_object_id) will always be defined. Except that value_object_id
+ will never be defined. BLARGH!
+ """
+ def __init__(self, *args, **kwargs):
+ super(AttributeForm, self).__init__(*args, **kwargs)
+
+ # This is necessary because model forms store changes to self.instance in their clean method.
+ # Mutter mutter.
+ value = self.instance.value
+ self._cached_value_ct = self.instance.value_content_type
+ self._cached_value = value
+
+ # If there is a value, pull in its fields.
+ if value is not None:
+ self.value_fields = value.value_formfields()
+ self.fields.update(self.value_fields)
+
+ def save(self, *args, **kwargs):
+ # At this point, the cleaned_data has already been stored on self.instance.
+
+ if self.instance.value_content_type != self._cached_value_ct:
+ # The value content type has changed. Clear the old value, if there was one.
+ if self._cached_value:
+ self._cached_value.delete()
+
+ # Clear the submitted value, if any.
+ self.cleaned_data.pop('value', None)
+
+ # Now create a new value instance so that on next instantiation, the form will
+ # know what fields to add.
+ if self.instance.value_content_type is not None:
+ self.instance.value = self.instance.value_content_type.model_class().objects.create()
+ elif self.instance.value is not None:
+ # The value content type is the same, but one of the value fields has changed.
+
+ # Use construct_instance to apply the changes from the cleaned_data to the value instance.
+ fields = self.value_fields.keys()
+ if set(fields) & set(self.changed_data):
+ self.instance.value.construct_instance(**dict([(key, self.cleaned_data[key]) for key in fields]))
+ self.instance.value.save()
+
+ return super(AttributeForm, self).save(*args, **kwargs)
+
+ class Meta:
+ model = Attribute
+
+
+class AttributeInlineFormSet(BaseGenericInlineFormSet):
+ "Necessary to force the GenericInlineFormset to use the form's save method for new objects."
+ def save_new(self, form, commit):
+ setattr(form.instance, self.ct_field.get_attname(), ContentType.objects.get_for_model(self.instance).pk)
+ setattr(form.instance, self.ct_fk_field.get_attname(), self.instance.pk)
+ return form.save()
\ No newline at end of file
--- /dev/null
+from django import forms
+from django.contrib.admin.widgets import AdminTextareaWidget
+from django.core.exceptions import ObjectDoesNotExist
+from django.db.models import Q
+from django.forms.models import ModelForm, BaseInlineFormSet
+from django.forms.formsets import TOTAL_FORM_COUNT
+from philo.admin.widgets import ModelLookupWidget
+from philo.models import Contentlet, ContentReference
+
+
+__all__ = (
+ 'ContentletForm',
+ 'ContentletInlineFormSet',
+ 'ContentReferenceForm',
+ 'ContentReferenceInlineFormSet'
+)
+
+
+class ContainerForm(ModelForm):
+ def __init__(self, *args, **kwargs):
+ super(ContainerForm, self).__init__(*args, **kwargs)
+ self.verbose_name = self.instance.name.replace('_', ' ')
+
+
+class ContentletForm(ContainerForm):
+ content = forms.CharField(required=False, widget=AdminTextareaWidget, label='Content')
+
+ def should_delete(self):
+ return not bool(self.cleaned_data['content'])
+
+ class Meta:
+ model = Contentlet
+ fields = ['name', 'content']
+
+
+class ContentReferenceForm(ContainerForm):
+ def __init__(self, *args, **kwargs):
+ super(ContentReferenceForm, self).__init__(*args, **kwargs)
+ try:
+ self.fields['content_id'].widget = ModelLookupWidget(self.instance.content_type)
+ except ObjectDoesNotExist:
+ # This will happen when an empty form (which we will never use) gets instantiated.
+ pass
+
+ def should_delete(self):
+ return (self.cleaned_data['content_id'] is None)
+
+ class Meta:
+ model = ContentReference
+ fields = ['name', 'content_id']
+
+
+class ContainerInlineFormSet(BaseInlineFormSet):
+ def __init__(self, containers, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
+ # Unfortunately, I need to add some things to BaseInline between its __init__ and its
+ # super call, so a lot of this is repetition.
+
+ # Start cribbed from BaseInline
+ from django.db.models.fields.related import RelatedObject
+ self.save_as_new = save_as_new
+ # is there a better way to get the object descriptor?
+ self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
+ if self.fk.rel.field_name == self.fk.rel.to._meta.pk.name:
+ backlink_value = self.instance
+ else:
+ backlink_value = getattr(self.instance, self.fk.rel.field_name)
+ if queryset is None:
+ queryset = self.model._default_manager
+ qs = queryset.filter(**{self.fk.name: backlink_value})
+ # End cribbed from BaseInline
+
+ self.container_instances, qs = self.get_container_instances(containers, qs)
+ self.extra_containers = containers
+ self.extra = len(self.extra_containers)
+ super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix, queryset=qs)
+
+ def get_container_instances(self, containers, qs):
+ raise NotImplementedError
+
+ def total_form_count(self):
+ if self.data or self.files:
+ return self.management_form.cleaned_data[TOTAL_FORM_COUNT]
+ else:
+ return self.initial_form_count() + self.extra
+
+ def save_existing_objects(self, commit=True):
+ self.changed_objects = []
+ self.deleted_objects = []
+ if not self.get_queryset():
+ return []
+
+ saved_instances = []
+ for form in self.initial_forms:
+ pk_name = self._pk_field.name
+ raw_pk_value = form._raw_value(pk_name)
+
+ # clean() for different types of PK fields can sometimes return
+ # the model instance, and sometimes the PK. Handle either.
+ pk_value = form.fields[pk_name].clean(raw_pk_value)
+ pk_value = getattr(pk_value, 'pk', pk_value)
+
+ obj = self._existing_object(pk_value)
+ if form.should_delete():
+ self.deleted_objects.append(obj)
+ obj.delete()
+ continue
+ if form.has_changed():
+ self.changed_objects.append((obj, form.changed_data))
+ saved_instances.append(self.save_existing(form, obj, commit=commit))
+ if not commit:
+ self.saved_forms.append(form)
+ return saved_instances
+
+ def save_new_objects(self, commit=True):
+ self.new_objects = []
+ for form in self.extra_forms:
+ if not form.has_changed():
+ continue
+ # If someone has marked an add form for deletion, don't save the
+ # object.
+ if form.should_delete():
+ continue
+ self.new_objects.append(self.save_new(form, commit=commit))
+ if not commit:
+ self.saved_forms.append(form)
+ return self.new_objects
+
+
+class ContentletInlineFormSet(ContainerInlineFormSet):
+ def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
+ if instance is None:
+ self.instance = self.fk.rel.to()
+ else:
+ self.instance = instance
+
+ try:
+ containers = list(self.instance.containers[0])
+ except ObjectDoesNotExist:
+ containers = []
+
+ super(ContentletInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset)
+
+ def get_container_instances(self, containers, qs):
+ qs = qs.filter(name__in=containers)
+ container_instances = []
+ for container in qs:
+ container_instances.append(container)
+ containers.remove(container.name)
+ return container_instances, qs
+
+ def _construct_form(self, i, **kwargs):
+ if i >= self.initial_form_count(): # and not kwargs.get('instance'):
+ kwargs['instance'] = self.model(name=self.extra_containers[i - self.initial_form_count() - 1])
+
+ return super(ContentletInlineFormSet, self)._construct_form(i, **kwargs)
+
+
+class ContentReferenceInlineFormSet(ContainerInlineFormSet):
+ def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
+ if instance is None:
+ self.instance = self.fk.rel.to()
+ else:
+ self.instance = instance
+
+ try:
+ containers = list(self.instance.containers[1])
+ except ObjectDoesNotExist:
+ containers = []
+
+ super(ContentReferenceInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset)
+
+ def get_container_instances(self, containers, qs):
+ filter = Q()
+
+ for name, ct in containers:
+ filter |= Q(name=name, content_type=ct)
+
+ qs = qs.filter(filter)
+ container_instances = []
+ for container in qs:
+ container_instances.append(container)
+ containers.remove((container.name, container.content_type))
+ return container_instances, qs
+
+ def _construct_form(self, i, **kwargs):
+ if i >= self.initial_form_count(): # and not kwargs.get('instance'):
+ name, content_type = self.extra_containers[i - self.initial_form_count() - 1]
+ kwargs['instance'] = self.model(name=name, content_type=content_type)
+
+ return super(ContentReferenceInlineFormSet, self)._construct_form(i, **kwargs)
\ No newline at end of file
from philo.admin.base import COLLAPSE_CLASSES, TreeAdmin
from philo.admin.nodes import ViewAdmin
from philo.models.pages import Page, Template, Contentlet, ContentReference
-from philo.forms import ContentletInlineFormSet, ContentReferenceInlineFormSet, ContentletForm, ContentReferenceForm
+from philo.admin.forms.containers import *
class ContentletInline(admin.StackedInline):
from django.contrib import admin
-from philo.admin import EntityAdmin, AddTagAdmin
+from django import forms
+from philo.admin import EntityAdmin, AddTagAdmin, COLLAPSE_CLASSES
from philo.contrib.penfield.models import BlogEntry, Blog, BlogView, Newsletter, NewsletterArticle, NewsletterIssue, NewsletterView
+class DelayedDateForm(forms.ModelForm):
+ date_field = 'date'
+
+ def __init__(self, *args, **kwargs):
+ super(DelayedDateForm, self).__init__(*args, **kwargs)
+ self.fields[self.date_field].required = False
+
+
class TitledAdmin(EntityAdmin):
prepopulated_fields = {'slug': ('title',)}
list_display = ('title', 'slug')
class BlogEntryAdmin(TitledAdmin, AddTagAdmin):
+ form = DelayedDateForm
filter_horizontal = ['tags']
+ list_filter = ['author', 'blog']
+ date_hierarchy = 'date'
+ search_fields = ('content',)
+ list_display = ['title', 'date', 'author']
+ raw_id_fields = ('author',)
+ fieldsets = (
+ (None, {
+ 'fields': ('title', 'author', 'blog')
+ }),
+ ('Content', {
+ 'fields': ('content', 'excerpt', 'tags'),
+ }),
+ ('Advanced', {
+ 'fields': ('slug', 'date'),
+ 'classes': COLLAPSE_CLASSES
+ })
+ )
class BlogViewAdmin(EntityAdmin):
- pass
+ fieldsets = (
+ (None, {
+ 'fields': ('blog',)
+ }),
+ ('Pages', {
+ 'fields': ('index_page', 'entry_page', 'tag_page')
+ }),
+ ('Archive Pages', {
+ 'fields': ('entry_archive_page', 'tag_archive_page')
+ }),
+ ('Permalinks', {
+ 'fields': ('entry_permalink_style', 'entry_permalink_base', 'tag_permalink_base'),
+ 'classes': COLLAPSE_CLASSES
+ }),
+ ('Feeds', {
+ 'fields': ('feed_suffix', 'feeds_enabled'),
+ 'classes': COLLAPSE_CLASSES
+ })
+ )
+ raw_id_fields = ('index_page', 'entry_page', 'tag_page', 'entry_archive_page', 'tag_archive_page',)
class NewsletterAdmin(TitledAdmin):
class NewsletterArticleAdmin(TitledAdmin, AddTagAdmin):
- filter_horizontal = TitledAdmin.filter_horizontal + ('tags', 'authors')
+ form = DelayedDateForm
+ filter_horizontal = ('tags', 'authors')
+ list_filter = ('newsletter',)
+ date_hierarchy = 'date'
+ search_fields = ('title', 'authors__name',)
+ list_display = ['title', 'date', 'author_names']
+ fieldsets = (
+ (None, {
+ 'fields': ('title', 'authors', 'newsletter')
+ }),
+ ('Content', {
+ 'fields': ('full_text', 'lede', 'tags')
+ }),
+ ('Advanced', {
+ 'fields': ('slug', 'date'),
+ 'classes': COLLAPSE_CLASSES
+ })
+ )
+
+ def author_names(self, obj):
+ return ', '.join([author.get_full_name() for author in obj.authors.all()])
+ author_names.short_description = "Authors"
class NewsletterIssueAdmin(TitledAdmin):
class NewsletterViewAdmin(EntityAdmin):
- pass
+ fieldsets = (
+ (None, {
+ 'fields': ('newsletter',)
+ }),
+ ('Pages', {
+ 'fields': ('index_page', 'article_page', 'issue_page')
+ }),
+ ('Archive Pages', {
+ 'fields': ('article_archive_page', 'issue_archive_page')
+ }),
+ ('Permalinks', {
+ 'fields': ('article_permalink_style', 'article_permalink_base', 'issue_permalink_base'),
+ 'classes': COLLAPSE_CLASSES
+ }),
+ ('Feeds', {
+ 'fields': ('feed_suffix', 'feeds_enabled'),
+ 'classes': COLLAPSE_CLASSES
+ })
+ )
+ raw_id_fields = ('index_page', 'article_page', 'issue_page', 'article_archive_page', 'issue_archive_page',)
admin.site.register(Blog, BlogAdmin)
-from django.db import models
from django.conf import settings
-from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model, TemplateField
-from philo.exceptions import ViewCanNotProvideSubpath
from django.conf.urls.defaults import url, patterns, include
+from django.db import models
from django.http import Http404
-from datetime import date, datetime
-from philo.utils import paginate
-from philo.contrib.penfield.validators import validate_pagination_count
+from django.template import loader, Context
from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
+from datetime import date, datetime
from philo.contrib.penfield.utils import FeedMultiViewMixin
+from philo.contrib.penfield.validators import validate_pagination_count
+from philo.exceptions import ViewCanNotProvideSubpath
+from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model, TemplateField
+from philo.utils import paginate
class Blog(Entity, Titled):
class BlogEntry(Entity, Titled):
blog = models.ForeignKey(Blog, related_name='entries', blank=True, null=True)
author = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='blogentries')
- date = models.DateTimeField(default=datetime.now)
+ date = models.DateTimeField(default=None)
content = models.TextField()
excerpt = models.TextField(blank=True, null=True)
tags = models.ManyToManyField(Tag, related_name='blogentries', blank=True, null=True)
+ def save(self, *args, **kwargs):
+ if self.date is None:
+ self.date = datetime.now()
+ super(BlogEntry, self).save(*args, **kwargs)
+
class Meta:
ordering = ['-date']
verbose_name_plural = "blog entries"
kwargs.update({'day': str(obj.date.day).zfill(2)})
return self.entry_view, [], kwargs
elif isinstance(obj, Tag):
- if obj in self.blog.entry_tags:
+ if obj in self.get_tag_queryset():
return 'entries_by_tag', [], {'tag_slugs': obj.slug}
elif isinstance(obj, (date, datetime)):
kwargs = {
return 'entries_by_day', [], kwargs
raise ViewCanNotProvideSubpath
- def get_context(self):
- return {'blog': self.blog}
-
@property
def urlpatterns(self):
urlpatterns = patterns('',
)
if self.tag_archive_page:
urlpatterns += patterns('',
- url((r'^(?:%s)/?$' % self.tag_permalink_base), self.tag_archive_view)
+ url((r'^(?:%s)/?$' % self.tag_permalink_base), self.tag_archive_view, 'tag_archive')
)
if self.entry_archive_page:
)
return urlpatterns
+ def get_context(self):
+ return {'blog': self.blog}
+
+ def get_entry_queryset(self):
+ return self.blog.entries.all()
+
+ def get_tag_queryset(self):
+ return self.blog.entry_tags
+
def get_all_entries(self, request, extra_context=None):
- return self.blog.entries.all(), extra_context
+ return self.get_entry_queryset(), extra_context
def get_entries_by_ymd(self, request, year=None, month=None, day=None, extra_context=None):
if not self.entry_archive_page:
raise Http404
- entries = self.blog.entries.all()
+ entries = self.get_entry_queryset()
if year:
entries = entries.filter(date__year=year)
if month:
return entries, context
def get_entries_by_tag(self, request, tag_slugs, extra_context=None):
- tags = []
- for tag_slug in tag_slugs.replace('+', '/').split('/'):
- if tag_slug: # ignore blank slugs, handles for multiple consecutive separators (+ or /)
- try:
- tag = self.blog.entry_tags.get(slug=tag_slug)
- except:
- raise Http404
- tags.append(tag)
- if len(tags) <= 0:
+ tag_slugs = tag_slugs.replace('+', '/').split('/')
+ tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
+
+ if not tags:
raise Http404
+
+ # Raise a 404 on an incorrect slug.
+ found_slugs = [tag.slug for tag in tags]
+ for slug in tag_slugs:
+ if slug and slug not in found_slugs:
+ raise Http404
- entries = self.blog.entries.all()
+ entries = self.get_entry_queryset()
for tag in tags:
entries = entries.filter(tags=tag)
- context = self.get_context()
- context.update(extra_context or {})
+ context = extra_context or {}
context.update({'tags': tags})
return entries, context
- def add_item(self, feed, obj, kwargs=None):
- defaults = {
- 'title': obj.title,
- 'description': obj.content,
- 'author_name': obj.author.get_full_name(),
- 'pubdate': obj.date
- }
- defaults.update(kwargs or {})
- super(BlogView, self).add_item(feed, obj, defaults)
-
- def get_feed(self, feed_type, extra_context, kwargs=None):
- tags = (extra_context or {}).get('tags', None)
- title = self.blog.title
-
- if tags is not None:
- title += " - %s" % ', '.join([tag.name for tag in tags])
-
- defaults = {
- 'title': title
- }
- defaults.update(kwargs or {})
- return super(BlogView, self).get_feed(feed_type, extra_context, defaults)
-
def entry_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
- entries = self.blog.entries.all()
+ entries = self.get_entry_queryset()
if year:
entries = entries.filter(date__year=year)
if month:
def tag_archive_view(self, request, extra_context=None):
if not self.tag_archive_page:
raise Http404
- context = {}
+ context = self.get_context()
context.update(extra_context or {})
- context.update({'blog': self.blog})
+ context.update({
+ 'tags': self.get_tag_queryset()
+ })
return self.tag_archive_page.render_to_response(request, extra_context=context)
+
+ def add_item(self, feed, obj, kwargs=None):
+ title = loader.get_template("penfield/feeds/blog_entry/title.html")
+ description = loader.get_template("penfield/feeds/blog_entry/description.html")
+ defaults = {
+ 'title': title.render(Context({'entry': obj})),
+ 'description': description.render(Context({'entry': obj})),
+ 'author_name': obj.author.get_full_name(),
+ 'pubdate': obj.date
+ }
+ defaults.update(kwargs or {})
+ super(BlogView, self).add_item(feed, obj, defaults)
+
+ def get_feed(self, feed_type, extra_context, kwargs=None):
+ tags = (extra_context or {}).get('tags', None)
+ title = self.blog.title
+
+ if tags is not None:
+ title += " - %s" % ', '.join([tag.name for tag in tags])
+
+ defaults = {
+ 'title': title
+ }
+ defaults.update(kwargs or {})
+ return super(BlogView, self).get_feed(feed_type, extra_context, defaults)
class Newsletter(Entity, Titled):
class NewsletterArticle(Entity, Titled):
newsletter = models.ForeignKey(Newsletter, related_name='articles')
authors = models.ManyToManyField(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='newsletterarticles')
- date = models.DateTimeField(default=datetime.now)
+ date = models.DateTimeField(default=None)
lede = TemplateField(null=True, blank=True, verbose_name='Summary')
full_text = TemplateField(db_index=True)
tags = models.ManyToManyField(Tag, related_name='newsletterarticles', blank=True, null=True)
+ def save(self, *args, **kwargs):
+ if self.date is None:
+ self.date = datetime.now()
+ super(NewsletterArticle, self).save(*args, **kwargs)
+
class Meta:
get_latest_by = 'date'
ordering = ['-date']
)
if self.issue_archive_page:
urlpatterns += patterns('',
- url(r'^(?:%s)/$' % self.issue_permalink_base, self.issue_archive_view)
+ url(r'^(?:%s)/$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive')
)
if self.article_archive_page:
urlpatterns += patterns('',
def get_context(self):
return {'newsletter': self.newsletter}
+ def get_article_queryset(self):
+ return self.newsletter.articles.all()
+
+ def get_issue_queryset(self):
+ return self.newsletter.issues.all()
+
def get_all_articles(self, request, extra_context=None):
- return self.newsletter.articles.all(), extra_context
+ return self.get_article_queryset(), extra_context
def get_articles_by_ymd(self, request, year, month=None, day=None, extra_context=None):
- articles = self.newsletter.articles.filter(dat__year=year)
+ articles = self.get_article_queryset().filter(date__year=year)
if month:
articles = articles.filter(date__month=month)
if day:
articles = articles.filter(date__day=day)
- return articles
+ return articles, extra_context
def get_articles_by_issue(self, request, numbering, extra_context=None):
try:
- issue = self.newsletter.issues.get(numbering=numbering)
+ issue = self.get_issue_queryset().get(numbering=numbering)
except NewsletterIssue.DoesNotExist:
raise Http404
context = extra_context or {}
context.update({'issue': issue})
- return issue.articles.all(), context
+ return self.get_article_queryset().filter(issues=issue), context
def article_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
- articles = self.newsletter.articles.all()
+ articles = self.get_article_queryset()
if year:
articles = articles.filter(date__year=year)
if month:
context.update({'article': article})
return self.article_page.render_to_response(request, extra_context=context)
- def issue_archive_view(self, request, extra_context=None):
+ def issue_archive_view(self, request, extra_context):
if not self.issue_archive_page:
raise Http404
- context = {}
+ context = self.get_context()
context.update(extra_context or {})
- context.update({'newsletter': self.newsletter})
+ context.update({
+ 'issues': self.get_issue_queryset()
+ })
return self.issue_archive_page.render_to_response(request, extra_context=context)
def add_item(self, feed, obj, kwargs=None):
+ title = loader.get_template("penfield/feeds/newsletter_article/title.html")
+ description = loader.get_template("penfield/feeds/newsletter_article/description.html")
defaults = {
- 'title': obj.title,
+ 'title': title.render(Context({'article': obj})),
'author_name': ', '.join([author.get_full_name() for author in obj.authors.all()]),
'pubdate': obj.date,
- 'description': obj.full_text,
+ 'description': description.render(Context({'article': obj})),
'categories': [tag.name for tag in obj.tags.all()]
}
defaults.update(kwargs or {})
--- /dev/null
+{{ entry.content }}
\ No newline at end of file
--- /dev/null
+{{ entry.title }}
\ No newline at end of file
--- /dev/null
+{{ article.full_text }}
\ No newline at end of file
--- /dev/null
+{{ article.title }}
\ No newline at end of file
--- /dev/null
+from django.contrib import admin
+from philo.admin import TreeEntityAdmin, COLLAPSE_CLASSES, NodeAdmin, EntityAdmin
+from philo.models import Node
+from philo.contrib.shipherd.models import NavigationItem, Navigation
+
+
+NAVIGATION_RAW_ID_FIELDS = ('navigation', 'parent', 'target_node')
+
+
+class NavigationItemInline(admin.StackedInline):
+ raw_id_fields = NAVIGATION_RAW_ID_FIELDS
+ model = NavigationItem
+ extra = 1
+ sortable_field_name = 'order'
+
+
+class NavigationItemChildInline(NavigationItemInline):
+ verbose_name = "child"
+ verbose_name_plural = "children"
+ fieldsets = (
+ (None, {
+ 'fields': ('text', 'parent')
+ }),
+ ('Target', {
+ 'fields': ('target_node', 'url_or_subpath',)
+ }),
+ ('Advanced', {
+ 'fields': ('reversing_parameters', 'order'),
+ 'classes': COLLAPSE_CLASSES
+ })
+ )
+
+
+class NavigationNavigationItemInline(NavigationItemInline):
+ fieldsets = (
+ (None, {
+ 'fields': ('text', 'navigation')
+ }),
+ ('Target', {
+ 'fields': ('target_node', 'url_or_subpath',)
+ }),
+ ('Advanced', {
+ 'fields': ('reversing_parameters', 'order'),
+ 'classes': COLLAPSE_CLASSES
+ })
+ )
+
+
+class NodeNavigationItemInline(NavigationItemInline):
+ verbose_name_plural = 'targeting navigation'
+ fieldsets = (
+ (None, {
+ 'fields': ('text',)
+ }),
+ ('Target', {
+ 'fields': ('target_node', 'url_or_subpath',)
+ }),
+ ('Advanced', {
+ 'fields': ('reversing_parameters', 'order'),
+ 'classes': COLLAPSE_CLASSES
+ }),
+ ('Expert', {
+ 'fields': ('parent', 'navigation')
+ }),
+ )
+
+
+class NodeNavigationInline(admin.TabularInline):
+ model = Navigation
+ extra = 1
+
+
+NodeAdmin.inlines = [NodeNavigationInline, NodeNavigationItemInline] + NodeAdmin.inlines
+
+
+class NavigationItemAdmin(TreeEntityAdmin):
+ list_display = ('__unicode__', 'target_node', 'url_or_subpath', 'reversing_parameters')
+ fieldsets = (
+ (None, {
+ 'fields': ('text', 'navigation',)
+ }),
+ ('Target', {
+ 'fields': ('target_node', 'url_or_subpath',)
+ }),
+ ('Advanced', {
+ 'fields': ('reversing_parameters',),
+ 'classes': COLLAPSE_CLASSES
+ }),
+ ('Expert', {
+ 'fields': ('parent', 'order'),
+ 'classes': COLLAPSE_CLASSES
+ })
+ )
+ raw_id_fields = NAVIGATION_RAW_ID_FIELDS
+ inlines = [NavigationItemChildInline] + TreeEntityAdmin.inlines
+
+
+class NavigationAdmin(EntityAdmin):
+ inlines = [NavigationNavigationItemInline]
+ raw_id_fields = ['node']
+
+
+admin.site.unregister(Node)
+admin.site.register(Node, NodeAdmin)
+admin.site.register(Navigation, NavigationAdmin)
+admin.site.register(NavigationItem, NavigationItemAdmin)
\ No newline at end of file
--- /dev/null
+#encoding: utf-8
+from django.core.exceptions import ValidationError
+from django.core.urlresolvers import NoReverseMatch
+from django.core.validators import RegexValidator, MinValueValidator
+from django.db import models
+from django.forms.models import model_to_dict
+from philo.models import TreeEntity, JSONField, Node, TreeManager, Entity
+from philo.validators import RedirectValidator
+from UserDict import DictMixin
+
+
+DEFAULT_NAVIGATION_DEPTH = 3
+
+
+class NavigationQuerySetMapper(object, DictMixin):
+ """This class exists to prevent setting of items in the navigation cache through node.navigation."""
+ def __init__(self, node):
+ self.node = node
+
+ def __getitem__(self, key):
+ return Navigation.objects.get_cache_for(self.node)[key]['root_items']
+
+ def keys(self):
+ return Navigation.objects.get_cache_for(self.node).keys()
+
+
+def navigation(self):
+ if not hasattr(self, '_navigation'):
+ self._navigation = NavigationQuerySetMapper(self)
+ return self._navigation
+
+
+Node.navigation = property(navigation)
+
+
+class NavigationCacheQuerySet(models.query.QuerySet):
+ """
+ This subclass will trigger general cache clearing for Navigation.objects when a mass
+ update or deletion is performed. As there is no convenient way to iterate over the
+ changed or deleted instances, there's no way to be more precise about what gets cleared.
+ """
+ def update(self, *args, **kwargs):
+ super(NavigationCacheQuerySet, self).update(*args, **kwargs)
+ Navigation.objects.clear_cache()
+
+ def delete(self, *args, **kwargs):
+ super(NavigationCacheQuerySet, self).delete(*args, **kwargs)
+ Navigation.objects.clear_cache()
+
+
+class NavigationManager(models.Manager):
+ # Since navigation is going to be hit frequently and changed
+ # relatively infrequently, cache it. Analogous to contenttypes.
+ use_for_related = True
+ _cache = {}
+
+ def get_queryset(self):
+ return NavigationCacheQuerySet(self.model, using=self._db)
+
+ def get_cache_for(self, node, update_targets=True):
+ created = False
+ if not self.has_cache_for(node):
+ self.create_cache_for(node)
+ created = True
+
+ if update_targets and not created:
+ self.update_targets_for(node)
+
+ return self.__class__._cache[self.db][node]
+
+ def has_cache_for(self, node):
+ return self.db in self.__class__._cache and node in self.__class__._cache[self.db]
+
+ def create_cache_for(self, node):
+ "This method loops through the nodes ancestors and caches all unique navigation keys."
+ ancestors = node.get_ancestors(ascending=True, include_self=True)
+
+ nodes_to_cache = []
+
+ for node in ancestors:
+ if self.has_cache_for(node):
+ cache = self.get_cache_for(node).copy()
+ break
+ else:
+ nodes_to_cache.insert(0, node)
+ else:
+ cache = {}
+
+ for node in nodes_to_cache:
+ cache = cache.copy()
+ cache.update(self._build_cache_for(node))
+ self.__class__._cache.setdefault(self.db, {})[node] = cache
+
+ def _build_cache_for(self, node):
+ cache = {}
+ tree_id_attr = NavigationItem._mptt_meta.tree_id_attr
+ level_attr = NavigationItem._mptt_meta.level_attr
+
+ for navigation in node.navigation_set.all():
+ tree_ids = navigation.roots.values_list(tree_id_attr)
+ items = list(NavigationItem.objects.filter(**{'%s__in' % tree_id_attr: tree_ids, '%s__lt' % level_attr: navigation.depth}).order_by('order', 'lft'))
+
+ root_items = []
+
+ for item in items:
+ item._is_cached = True
+
+ if not hasattr(item, '_cached_children'):
+ item._cached_children = []
+
+ if item.parent:
+ # alternatively, if I don't want to force it to a list, I could keep track of
+ # instances where the parent hasn't yet been met and do this step later for them.
+ # delayed action.
+ item.parent = items[items.index(item.parent)]
+ if not hasattr(item.parent, '_cached_children'):
+ item.parent._cached_children = []
+ item.parent._cached_children.append(item)
+ else:
+ root_items.append(item)
+
+ cache[navigation.key] = {
+ 'navigation': navigation,
+ 'root_items': root_items,
+ 'items': items
+ }
+
+ return cache
+
+ def clear_cache_for(self, node):
+ # Clear the cache for this node and all its descendants. The
+ # navigation for this node has probably changed, and for now,
+ # it isn't worth it to only clear the descendants actually
+ # affected by this.
+ if not self.has_cache_for(node):
+ # Already cleared.
+ return
+
+ descendants = node.get_descendants(include_self=True)
+ cache = self.__class__._cache[self.db]
+ for node in descendants:
+ cache.pop(node, None)
+
+ def update_targets_for(self, node):
+ # Manually update a cache's target nodes in case something's changed there.
+ # This should be a less complex operation than reloading the models each
+ # time. Not as good as selective updates... but not much to be done
+ # about that. TODO: Benchmark it.
+ caches = self.__class__._cache[self.db][node].values()
+
+ items = []
+
+ for cache in caches:
+ items += cache['items']
+
+ # A distinct query is not strictly necessary. TODO: benchmark the efficiency
+ # with/without distinct.
+ targets = list(Node.objects.filter(navigation_items__in=items).distinct())
+
+ for cache in caches:
+ for item in cache['items']:
+ item.target_node = targets[targets.index(item.target_node)]
+
+ def clear_cache(self):
+ self.__class__._cache.pop(self.db, None)
+
+
+class Navigation(Entity):
+ objects = NavigationManager()
+
+ node = models.ForeignKey(Node, related_name='navigation_set', help_text="Be available as navigation for this node.")
+ key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.")
+ depth = models.PositiveSmallIntegerField(default=DEFAULT_NAVIGATION_DEPTH, validators=[MinValueValidator(1)], help_text="Defines the maximum display depth of this navigation.")
+
+ def __init__(self, *args, **kwargs):
+ super(Navigation, self).__init__(*args, **kwargs)
+ self._initial_data = model_to_dict(self)
+
+ def __unicode__(self):
+ return "%s[%s]" % (self.node, self.key)
+
+ def _has_changed(self):
+ return self._initial_data != model_to_dict(self)
+
+ def save(self, *args, **kwargs):
+ super(Navigation, self).save(*args, **kwargs)
+
+ if self._has_changed():
+ Navigation.objects.clear_cache_for(self.node)
+ self._initial_data = model_to_dict(self)
+
+ def delete(self, *args, **kwargs):
+ super(Navigation, self).delete(*args, **kwargs)
+ Navigation.objects.clear_cache_for(self.node)
+
+ class Meta:
+ unique_together = ('node', 'key')
+
+
+class NavigationItemManager(TreeManager):
+ use_for_related = True
+
+ def get_queryset(self):
+ return NavigationCacheQuerySet(self.model, using=self._db)
+
+
+class NavigationItem(TreeEntity):
+ objects = NavigationItemManager()
+
+ navigation = models.ForeignKey(Navigation, blank=True, null=True, related_name='roots', help_text="Be a root in this navigation tree.")
+ text = models.CharField(max_length=50)
+
+ target_node = models.ForeignKey(Node, blank=True, null=True, related_name='navigation_items', help_text="Point to this node's url.")
+ url_or_subpath = models.CharField(max_length=200, validators=[RedirectValidator()], blank=True, help_text="Point to this url or, if a node is defined and accepts subpaths, this subpath of the node.")
+ reversing_parameters = JSONField(blank=True, help_text="If reversing parameters are defined, url_or_subpath will instead be interpreted as the view name to be reversed.")
+
+ order = models.PositiveSmallIntegerField(default=0)
+
+ def __init__(self, *args, **kwargs):
+ super(NavigationItem, self).__init__(*args, **kwargs)
+ self._initial_data = model_to_dict(self)
+ self._is_cached = False
+
+ def __unicode__(self):
+ return self.get_path(field='text', pathsep=u' › ')
+
+ def clean(self):
+ # Should this be enforced? Not enforcing it would allow creation of "headers" in the navbar.
+ if not self.target_node and not self.url_or_subpath:
+ raise ValidationError("Either a target node or a url must be defined.")
+
+ if self.reversing_parameters and (not self.url_or_subpath or not self.target_node):
+ raise ValidationError("Reversing parameters require a view name and a target node.")
+
+ try:
+ self.get_target_url()
+ except NoReverseMatch, e:
+ raise ValidationError(e.message)
+
+ if bool(self.parent) == bool(self.navigation):
+ raise ValidationError("Exactly one of `parent` and `navigation` must be defined.")
+
+ def get_target_url(self):
+ node = self.target_node
+ if node is not None and node.accepts_subpath and self.url_or_subpath:
+ if self.reversing_parameters is not None:
+ view_name = self.url_or_subpath
+ params = self.reversing_parameters
+ args = isinstance(params, list) and params or None
+ kwargs = isinstance(params, dict) and params or None
+ return node.view.reverse(view_name, args=args, kwargs=kwargs, node=node)
+ else:
+ subpath = self.url_or_subpath
+ while subpath and subpath[0] == '/':
+ subpath = subpath[1:]
+ return '%s%s' % (node.get_absolute_url(), subpath)
+ elif node is not None:
+ return node.get_absolute_url()
+ else:
+ return self.url_or_subpath
+ target_url = property(get_target_url)
+
+ def is_active(self, request):
+ if self.target_url == request.path:
+ # Handle the `default` case where the target_url and requested path
+ # are identical.
+ return True
+
+ if self.target_node is None and self.url_or_subpath == "http%s://%s%s" % (request.is_secure() and 's' or '', request.get_host(), request.path):
+ # If there's no target_node, double-check whether it's a full-url
+ # match.
+ return True
+
+ if self.target_node and not self.url_or_subpath:
+ # If there is a target node and it's targeted simply, but the target URL is not
+ # the same as the request path, check whether the target node is an ancestor
+ # of the requested node. If so, this is active unless the target node
+ # is the same as the ``host node`` for this navigation structure.
+ try:
+ host_node = self.get_root().navigation.node
+ except AttributeError:
+ pass
+ else:
+ if self.target_node != host_node and self.target_node.is_ancestor_of(request.node):
+ return True
+
+ return False
+
+ def has_active_descendants(self, request):
+ for child in self.get_children():
+ if child.is_active(request) or child.has_active_descendants(request):
+ return True
+ return False
+
+ def _has_changed(self):
+ if model_to_dict(self) == self._initial_data:
+ return False
+ return True
+
+ def _clear_cache(self):
+ try:
+ root = self.get_root()
+ if self.get_level() < root.navigation.depth:
+ Navigation.objects.clear_cache_for(self.get_root().navigation.node)
+ except AttributeError:
+ pass
+
+ def save(self, *args, **kwargs):
+ super(NavigationItem, self).save(*args, **kwargs)
+
+ if self._has_changed():
+ self._clear_cache()
+
+ def delete(self, *args, **kwargs):
+ super(NavigationItem, self).delete(*args, **kwargs)
+ self._clear_cache()
\ No newline at end of file
--- /dev/null
+from django import template
+from django.conf import settings
+from django.utils.safestring import mark_safe
+from philo.contrib.shipherd.models import Navigation
+from philo.models import Node
+from mptt.templatetags.mptt_tags import RecurseTreeNode, cache_tree_children
+from django.utils.translation import ugettext as _
+
+
+register = template.Library()
+
+
+class RecurseNavigationNode(RecurseTreeNode):
+ def __init__(self, template_nodes, instance_var, key):
+ self.template_nodes = template_nodes
+ self.instance_var = instance_var
+ self.key = key
+
+ def _render_node(self, context, item, request):
+ bits = []
+ context.push()
+ for child in item.get_children():
+ context['item'] = child
+ bits.append(self._render_node(context, child, request))
+ context['item'] = item
+ context['children'] = mark_safe(u''.join(bits))
+ context['active'] = item.is_active(request)
+ context['active_descendants'] = item.has_active_descendants(request)
+ rendered = self.template_nodes.render(context)
+ context.pop()
+ return rendered
+
+ def render(self, context):
+ try:
+ request = context['request']
+ except KeyError:
+ return ''
+
+ instance = self.instance_var.resolve(context)
+
+ try:
+ navigation = instance.navigation[self.key]
+ except:
+ return settings.TEMPLATE_STRING_IF_INVALID
+
+ bits = [self._render_node(context, item, request) for item in navigation]
+ return ''.join(bits)
+
+
+@register.tag
+def recursenavigation(parser, token):
+ """
+ Based on django-mptt's recursetree templatetag. In addition to {{ item }} and {{ children }},
+ sets {{ active }} and {{ active_descendants }} in the context.
+
+ Note that the tag takes one variable, which is a Node instance.
+
+ Usage:
+ <ul>
+ {% recursenavigation node main %}
+ <li{% if active %} class='active'{% endif %}>
+ {{ navigation.text }}
+ {% if navigation.get_children %}
+ <ul>
+ {{ children }}
+ </ul>
+ {% endif %}
+ </li>
+ {% endrecursenavigation %}
+ </ul>
+ """
+ bits = token.contents.split()
+ if len(bits) != 3:
+ raise template.TemplateSyntaxError(_('%s tag requires two arguments: a node and a navigation section name') % bits[0])
+
+ instance_var = parser.compile_filter(bits[1])
+ key = bits[2]
+
+ template_nodes = parser.parse(('endrecursenavigation',))
+ parser.delete_first_token()
+
+ return RecurseNavigationNode(template_nodes, instance_var, key)
+
+
+@register.filter
+def has_navigation(node): # optional arg for a key?
+ return bool(node.navigation)
+
+
+@register.filter
+def navigation_host(node, key):
+ try:
+ return Navigation.objects.filter(node__in=node.get_ancestors(include_self=True), key=key).order_by('-node__level')[0].node
+ except:
+ if settings.TEMPLATE_DEBUG:
+ raise
+ return node
\ No newline at end of file
}
kwargs.update(reverse_kwargs or {})
return 'http://%s%s' % (current_site.domain, self.reverse(confirmation_view, kwargs=kwargs, node=node))
-
- def get_context(self):
- """Hook for providing instance-specific context - such as the value of a Field - to all views."""
- return {}
def display_login_page(self, request, message, extra_context=None):
request.session.set_test_cookie()
return self.manage_account_page.render_to_response(request, extra_context=context)
def has_valid_account(self, user):
- user_form, profile_form = self.get_account_forms()
- forms = []
- forms.append(user_form(data=get_field_data(user, self.user_fields)))
-
- if profile_form is not None:
- profile = self.account_profile._default_manager.get_or_create(user=user)[0]
- forms.append(profile_form(data=get_field_data(profile, self.account_profile_fields)))
-
- for form in forms:
- if not form.is_valid():
- return False
- return True
+ form = self.account_form(user, {})
+ form.data = form.initial
+ return form.is_valid()
def account_required(self, view):
def inner(request, *args, **kwargs):
+++ /dev/null
-from django import forms
-from django.contrib.admin.widgets import AdminTextareaWidget
-from django.contrib.contenttypes.generic import BaseGenericInlineFormSet
-from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ValidationError, ObjectDoesNotExist
-from django.db.models import Q
-from django.forms.models import model_to_dict, fields_for_model, ModelFormMetaclass, ModelForm, BaseInlineFormSet
-from django.forms.formsets import TOTAL_FORM_COUNT
-from django.template import loader, loader_tags, TemplateDoesNotExist, Context, Template as DjangoTemplate
-from django.utils.datastructures import SortedDict
-from philo.admin.widgets import ModelLookupWidget
-from philo.models import Entity, Template, Contentlet, ContentReference, Attribute
-from philo.utils import fattr
-
-
-__all__ = ('EntityForm', )
-
-
-def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)):
- field_list = []
- ignored = []
- opts = entity_model._entity_meta
- for f in opts.proxy_fields:
- if not f.editable:
- continue
- if fields and not f.name in fields:
- continue
- if exclude and f.name in exclude:
- continue
- if widgets and f.name in widgets:
- kwargs = {'widget': widgets[f.name]}
- else:
- kwargs = {}
- formfield = formfield_callback(f, **kwargs)
- if formfield:
- field_list.append((f.name, formfield))
- else:
- ignored.append(f.name)
- field_dict = SortedDict(field_list)
- if fields:
- field_dict = SortedDict(
- [(f, field_dict.get(f)) for f in fields
- if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored) and (f in field_dict)]
- )
- return field_dict
-
-
-# BEGIN HACK - This will not be required after http://code.djangoproject.com/ticket/14082 has been resolved
-
-class EntityFormBase(ModelForm):
- pass
-
-_old_metaclass_new = ModelFormMetaclass.__new__
-
-def _new_metaclass_new(cls, name, bases, attrs):
- new_class = _old_metaclass_new(cls, name, bases, attrs)
- if issubclass(new_class, EntityFormBase) and new_class._meta.model:
- new_class.base_fields.update(proxy_fields_for_entity_model(new_class._meta.model, new_class._meta.fields, new_class._meta.exclude, new_class._meta.widgets)) # don't pass in formfield_callback
- return new_class
-
-ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new)
-
-# END HACK
-
-
-class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it weren't for the above HACK
- def __init__(self, *args, **kwargs):
- initial = kwargs.pop('initial', None)
- instance = kwargs.get('instance', None)
- if instance is not None:
- new_initial = {}
- for f in instance._entity_meta.proxy_fields:
- if self._meta.fields and not f.name in self._meta.fields:
- continue
- if self._meta.exclude and f.name in self._meta.exclude:
- continue
- new_initial[f.name] = f.value_from_object(instance)
- else:
- new_initial = {}
- if initial is not None:
- new_initial.update(initial)
- kwargs['initial'] = new_initial
- super(EntityForm, self).__init__(*args, **kwargs)
-
- @fattr(alters_data=True)
- def save(self, commit=True):
- cleaned_data = self.cleaned_data
- instance = super(EntityForm, self).save(commit=False)
-
- for f in instance._entity_meta.proxy_fields:
- if not f.editable or not f.name in cleaned_data:
- continue
- if self._meta.fields and f.name not in self._meta.fields:
- continue
- if self._meta.exclude and f.name in self._meta.exclude:
- continue
- setattr(instance, f.attname, cleaned_data[f.name])
-
- if commit:
- instance.save()
- self.save_m2m()
-
- return instance
-
-
-class AttributeForm(ModelForm):
- def __init__(self, *args, **kwargs):
- super(AttributeForm, self).__init__(*args, **kwargs)
-
- # This is necessary because model forms store changes to self.instance in their clean method.
- # Mutter mutter.
- self._cached_value_ct = self.instance.value_content_type
- self._cached_value = self.instance.value
-
- if self.instance.value is not None:
- value_field = self.instance.value.value_formfield()
- if value_field:
- self.fields['value'] = value_field
- if hasattr(self.instance.value, 'content_type'):
- self.fields['content_type'] = self.instance.value._meta.get_field('content_type').formfield(initial=getattr(self.instance.value.content_type, 'pk', None))
-
- def save(self, *args, **kwargs):
- # At this point, the cleaned_data has already been stored on self.instance.
- if self.instance.value_content_type != self._cached_value_ct:
- if self.instance.value is not None:
- self._cached_value.delete()
- if 'value' in self.cleaned_data:
- del(self.cleaned_data['value'])
-
- if self.instance.value_content_type is not None:
- # Make a blank value of the new type! Run special code for content_type attributes.
- if hasattr(self.instance.value_content_type.model_class(), 'content_type'):
- if self._cached_value and hasattr(self._cached_value, 'content_type'):
- new_ct = self._cached_value.content_type
- else:
- new_ct = None
- new_value = self.instance.value_content_type.model_class().objects.create(content_type=new_ct)
- else:
- new_value = self.instance.value_content_type.model_class().objects.create()
-
- new_value.apply_data(self.cleaned_data)
- new_value.save()
- self.instance.value = new_value
- else:
- # The value type is the same, but one of the fields has changed.
- # Check to see if the changed value was the content type. We have to check the
- # cleaned_data because self.instance.value.content_type was overridden.
- if hasattr(self.instance.value, 'content_type') and 'content_type' in self.cleaned_data and 'value' in self.cleaned_data and (not hasattr(self._cached_value, 'content_type') or self._cached_value.content_type != self.cleaned_data['content_type']):
- self.cleaned_data['value'] = None
-
- self.instance.value.apply_data(self.cleaned_data)
- self.instance.value.save()
-
- super(AttributeForm, self).save(*args, **kwargs)
- return self.instance
-
- class Meta:
- model = Attribute
-
-
-class AttributeInlineFormSet(BaseGenericInlineFormSet):
- "Necessary to force the GenericInlineFormset to use the form's save method for new objects."
- def save_new(self, form, commit):
- setattr(form.instance, self.ct_field.get_attname(), ContentType.objects.get_for_model(self.instance).pk)
- setattr(form.instance, self.ct_fk_field.get_attname(), self.instance.pk)
- return form.save()
-
-
-class ContainerForm(ModelForm):
- def __init__(self, *args, **kwargs):
- super(ContainerForm, self).__init__(*args, **kwargs)
- self.verbose_name = self.instance.name.replace('_', ' ')
-
-
-class ContentletForm(ContainerForm):
- content = forms.CharField(required=False, widget=AdminTextareaWidget, label='Content')
-
- def should_delete(self):
- return not bool(self.cleaned_data['content'])
-
- class Meta:
- model = Contentlet
- fields = ['name', 'content']
-
-
-class ContentReferenceForm(ContainerForm):
- def __init__(self, *args, **kwargs):
- super(ContentReferenceForm, self).__init__(*args, **kwargs)
- try:
- self.fields['content_id'].widget = ModelLookupWidget(self.instance.content_type)
- except ObjectDoesNotExist:
- # This will happen when an empty form (which we will never use) gets instantiated.
- pass
-
- def should_delete(self):
- return (self.cleaned_data['content_id'] is None)
-
- class Meta:
- model = ContentReference
- fields = ['name', 'content_id']
-
-
-class ContainerInlineFormSet(BaseInlineFormSet):
- def __init__(self, containers, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
- # Unfortunately, I need to add some things to BaseInline between its __init__ and its
- # super call, so a lot of this is repetition.
-
- # Start cribbed from BaseInline
- from django.db.models.fields.related import RelatedObject
- self.save_as_new = save_as_new
- # is there a better way to get the object descriptor?
- self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
- if self.fk.rel.field_name == self.fk.rel.to._meta.pk.name:
- backlink_value = self.instance
- else:
- backlink_value = getattr(self.instance, self.fk.rel.field_name)
- if queryset is None:
- queryset = self.model._default_manager
- qs = queryset.filter(**{self.fk.name: backlink_value})
- # End cribbed from BaseInline
-
- self.container_instances, qs = self.get_container_instances(containers, qs)
- self.extra_containers = containers
- self.extra = len(self.extra_containers)
- super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix, queryset=qs)
-
- def get_container_instances(self, containers, qs):
- raise NotImplementedError
-
- def total_form_count(self):
- if self.data or self.files:
- return self.management_form.cleaned_data[TOTAL_FORM_COUNT]
- else:
- return self.initial_form_count() + self.extra
-
- def save_existing_objects(self, commit=True):
- self.changed_objects = []
- self.deleted_objects = []
- if not self.get_queryset():
- return []
-
- saved_instances = []
- for form in self.initial_forms:
- pk_name = self._pk_field.name
- raw_pk_value = form._raw_value(pk_name)
-
- # clean() for different types of PK fields can sometimes return
- # the model instance, and sometimes the PK. Handle either.
- pk_value = form.fields[pk_name].clean(raw_pk_value)
- pk_value = getattr(pk_value, 'pk', pk_value)
-
- obj = self._existing_object(pk_value)
- if form.should_delete():
- self.deleted_objects.append(obj)
- obj.delete()
- continue
- if form.has_changed():
- self.changed_objects.append((obj, form.changed_data))
- saved_instances.append(self.save_existing(form, obj, commit=commit))
- if not commit:
- self.saved_forms.append(form)
- return saved_instances
-
- def save_new_objects(self, commit=True):
- self.new_objects = []
- for form in self.extra_forms:
- if not form.has_changed():
- continue
- # If someone has marked an add form for deletion, don't save the
- # object.
- if form.should_delete():
- continue
- self.new_objects.append(self.save_new(form, commit=commit))
- if not commit:
- self.saved_forms.append(form)
- return self.new_objects
-
-
-class ContentletInlineFormSet(ContainerInlineFormSet):
- def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
- if instance is None:
- self.instance = self.fk.rel.to()
- else:
- self.instance = instance
-
- try:
- containers = list(self.instance.containers[0])
- except ObjectDoesNotExist:
- containers = []
-
- super(ContentletInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset)
-
- def get_container_instances(self, containers, qs):
- qs = qs.filter(name__in=containers)
- container_instances = []
- for container in qs:
- container_instances.append(container)
- containers.remove(container.name)
- return container_instances, qs
-
- def _construct_form(self, i, **kwargs):
- if i >= self.initial_form_count(): # and not kwargs.get('instance'):
- kwargs['instance'] = self.model(name=self.extra_containers[i - self.initial_form_count() - 1])
-
- return super(ContentletInlineFormSet, self)._construct_form(i, **kwargs)
-
-
-class ContentReferenceInlineFormSet(ContainerInlineFormSet):
- def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
- if instance is None:
- self.instance = self.fk.rel.to()
- else:
- self.instance = instance
-
- try:
- containers = list(self.instance.containers[1])
- except ObjectDoesNotExist:
- containers = []
-
- super(ContentReferenceInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset)
-
- def get_container_instances(self, containers, qs):
- filter = Q()
-
- for name, ct in containers:
- filter |= Q(name=name, content_type=ct)
-
- qs = qs.filter(filter)
- container_instances = []
- for container in qs:
- container_instances.append(container)
- containers.remove((container.name, container.content_type))
- return container_instances, qs
-
- def _construct_form(self, i, **kwargs):
- if i >= self.initial_form_count(): # and not kwargs.get('instance'):
- name, content_type = self.extra_containers[i - self.initial_form_count() - 1]
- kwargs['instance'] = self.model(name=name, content_type=content_type)
-
- return super(ContentReferenceInlineFormSet, self)._construct_form(i, **kwargs)
\ No newline at end of file
--- /dev/null
+from philo.forms.fields import *
+from philo.forms.entities import *
\ No newline at end of file
--- /dev/null
+from django.forms.models import ModelFormMetaclass, ModelForm
+from django.utils.datastructures import SortedDict
+from philo.utils import fattr
+
+
+__all__ = ('EntityForm',)
+
+
+def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)):
+ field_list = []
+ ignored = []
+ opts = entity_model._entity_meta
+ for f in opts.proxy_fields:
+ if not f.editable:
+ continue
+ if fields and not f.name in fields:
+ continue
+ if exclude and f.name in exclude:
+ continue
+ if widgets and f.name in widgets:
+ kwargs = {'widget': widgets[f.name]}
+ else:
+ kwargs = {}
+ formfield = formfield_callback(f, **kwargs)
+ if formfield:
+ field_list.append((f.name, formfield))
+ else:
+ ignored.append(f.name)
+ field_dict = SortedDict(field_list)
+ if fields:
+ field_dict = SortedDict(
+ [(f, field_dict.get(f)) for f in fields
+ if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored) and (f in field_dict)]
+ )
+ return field_dict
+
+
+# BEGIN HACK - This will not be required after http://code.djangoproject.com/ticket/14082 has been resolved
+
+class EntityFormBase(ModelForm):
+ pass
+
+_old_metaclass_new = ModelFormMetaclass.__new__
+
+def _new_metaclass_new(cls, name, bases, attrs):
+ formfield_callback = attrs.get('formfield_callback', lambda f, **kwargs: f.formfield(**kwargs))
+ new_class = _old_metaclass_new(cls, name, bases, attrs)
+ opts = new_class._meta
+ if issubclass(new_class, EntityFormBase) and opts.model:
+ # "override" proxy fields with declared fields by excluding them if there's a name conflict.
+ exclude = (list(opts.exclude or []) + new_class.declared_fields.keys()) or None
+ proxy_fields = proxy_fields_for_entity_model(opts.model, opts.fields, exclude, opts.widgets, formfield_callback) # don't pass in formfield_callback
+ new_class.proxy_fields = proxy_fields
+ new_class.base_fields.update(proxy_fields)
+ return new_class
+
+ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new)
+
+# END HACK
+
+
+class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it weren't for the above HACK
+ def __init__(self, *args, **kwargs):
+ initial = kwargs.pop('initial', None)
+ instance = kwargs.get('instance', None)
+ if instance is not None:
+ new_initial = {}
+ for f in instance._entity_meta.proxy_fields:
+ if self._meta.fields and not f.name in self._meta.fields:
+ continue
+ if self._meta.exclude and f.name in self._meta.exclude:
+ continue
+ new_initial[f.name] = f.value_from_object(instance)
+ else:
+ new_initial = {}
+ if initial is not None:
+ new_initial.update(initial)
+ kwargs['initial'] = new_initial
+ super(EntityForm, self).__init__(*args, **kwargs)
+
+ @fattr(alters_data=True)
+ def save(self, commit=True):
+ cleaned_data = self.cleaned_data
+ instance = super(EntityForm, self).save(commit=False)
+
+ for f in instance._entity_meta.proxy_fields:
+ if not f.editable or not f.name in cleaned_data:
+ continue
+ if self._meta.fields and f.name not in self._meta.fields:
+ continue
+ if self._meta.exclude and f.name in self._meta.exclude:
+ continue
+ setattr(instance, f.attname, f.get_storage_value(cleaned_data[f.name]))
+
+ if commit:
+ instance.save()
+ self.save_m2m()
+
+ return instance
\ No newline at end of file
--- /dev/null
+from django import forms
+from django.core.exceptions import ValidationError
+from django.utils import simplejson as json
+from philo.validators import json_validator
+
+
+__all__ = ('JSONFormField',)
+
+
+class JSONFormField(forms.Field):
+ default_validators = [json_validator]
+
+ def clean(self, value):
+ if value == '' and not self.required:
+ return None
+ try:
+ return json.loads(value)
+ except Exception, e:
+ raise ValidationError(u'JSON decode error: %s' % e)
\ No newline at end of file
from philo.models.collections import *
from philo.models.nodes import *
from philo.models.pages import *
-from philo.models.fields import *
from django.contrib.auth.models import User, Group
from django.contrib.sites.models import Site
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
-from django.utils import simplejson as json
from django.core.exceptions import ObjectDoesNotExist
+from django.core.validators import RegexValidator
+from django.utils import simplejson as json
+from django.utils.encoding import smart_str
from philo.exceptions import AncestorDoesNotExist
from philo.models.fields import JSONField
from philo.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter
class Meta:
app_label = 'philo'
+ ordering = ('name',)
class Titled(models.Model):
def attribute(self):
return self.attribute_set.all()[0]
- def apply_data(self, data):
+ def set_value(self, value):
+ raise NotImplementedError
+
+ def value_formfields(self, **kwargs):
+ """Define any formfields that would be used to construct an instance of this value."""
raise NotImplementedError
- def value_formfield(self, **kwargs):
+ def construct_instance(self, **kwargs):
+ """Apply cleaned data from the formfields generated by valid_formfields to oneself."""
raise NotImplementedError
def __unicode__(self):
class JSONValue(AttributeValue):
- value = JSONField() #verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
+ value = JSONField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.', default='null')
def __unicode__(self):
- return self.value_json
+ return smart_str(self.value)
- def value_formfield(self, **kwargs):
- kwargs['initial'] = self.value_json
- return self._meta.get_field('value').formfield(**kwargs)
+ def value_formfields(self):
+ kwargs = {'initial': self.value_json}
+ field = self._meta.get_field('value')
+ return {field.name: field.formfield(**kwargs)}
- def apply_data(self, cleaned_data):
- self.value = cleaned_data.get('value', None)
+ def construct_instance(self, **kwargs):
+ field_name = self._meta.get_field('value').name
+ self.set_value(kwargs.pop(field_name, None))
+
+ def set_value(self, value):
+ self.value = value
class Meta:
app_label = 'philo'
object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
value = generic.GenericForeignKey()
- def value_formfield(self, form_class=forms.ModelChoiceField, **kwargs):
- if self.content_type is None:
- return None
- kwargs.update({'initial': self.object_id, 'required': False})
- return form_class(self.content_type.model_class()._default_manager.all(), **kwargs)
-
- def apply_data(self, cleaned_data):
- if 'value' in cleaned_data and cleaned_data['value'] is not None:
- self.value = cleaned_data['value']
- else:
- self.content_type = cleaned_data.get('content_type', None)
- # If there is no value set in the cleaned data, clear the stored value.
+ def value_formfields(self):
+ field = self._meta.get_field('content_type')
+ fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))}
+
+ if self.content_type:
+ kwargs = {
+ 'initial': self.object_id,
+ 'required': False,
+ 'queryset': self.content_type.model_class()._default_manager.all()
+ }
+ fields['value'] = forms.ModelChoiceField(**kwargs)
+ return fields
+
+ def construct_instance(self, **kwargs):
+ field_name = self._meta.get_field('content_type').name
+ ct = kwargs.pop(field_name, None)
+ if ct is None or ct != self.content_type:
self.object_id = None
+ self.content_type = ct
+ else:
+ value = kwargs.pop('value', None)
+ self.set_value(value)
+ if value is None:
+ self.content_type = ct
+
+ def set_value(self, value):
+ self.value = value
class Meta:
app_label = 'philo'
content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
values = models.ManyToManyField(ForeignKeyValue, blank=True, null=True)
- def get_object_id_list(self):
- if not self.values.count():
- return []
- else:
- return self.values.values_list('object_id', flat=True)
-
- def get_value(self):
- if self.content_type is None:
- return None
-
- return self.content_type.model_class()._default_manager.filter(id__in=self.get_object_id_list())
+ def get_object_ids(self):
+ return self.values.values_list('object_id', flat=True)
+ object_ids = property(get_object_ids)
def set_value(self, value):
- # Value is probably a queryset - but allow any iterable.
+ # Value must be a queryset. Watch out for ModelMultipleChoiceField;
+ # it returns its value as a list if empty.
- # These lines shouldn't be necessary; however, if value is an EmptyQuerySet,
- # the code (specifically the object_id__in query) won't work without them. Unclear why...
- if not value:
- value = []
+ self.content_type = ContentType.objects.get_for_model(value.model)
# Before we can fiddle with the many-to-many to foreignkeyvalues, we need
# a pk.
if self.pk is None:
self.save()
- if isinstance(value, models.query.QuerySet):
- value = value.values_list('id', flat=True)
+ object_ids = value.values_list('id', flat=True)
- self.values.filter(~models.Q(object_id__in=value)).delete()
- current = self.get_object_id_list()
-
- for v in value:
- if v in current:
- continue
- self.values.create(content_type=self.content_type, object_id=v)
-
- value = property(get_value, set_value)
+ # These lines shouldn't be necessary; however, if object_ids is an EmptyQuerySet,
+ # the code (specifically the object_id__in query) won't work without them. Unclear why...
+ # TODO: is this still the case?
+ if not object_ids:
+ self.values.all().delete()
+ else:
+ self.values.exclude(object_id__in=object_ids, content_type=self.content_type).delete()
+
+ current_ids = self.object_ids
+
+ for object_id in object_ids:
+ if object_id in current_ids:
+ continue
+ self.values.create(content_type=self.content_type, object_id=object_id)
- def value_formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
+ def get_value(self):
if self.content_type is None:
return None
- kwargs.update({'initial': self.get_object_id_list(), 'required': False})
- return form_class(self.content_type.model_class()._default_manager.all(), **kwargs)
+
+ # HACK to be safely explicit until http://code.djangoproject.com/ticket/15145 is resolved
+ object_ids = self.object_ids
+ manager = self.content_type.model_class()._default_manager
+ if not object_ids:
+ return manager.none()
+ return manager.filter(id__in=self.object_ids)
+
+ value = property(get_value, set_value)
- def apply_data(self, cleaned_data):
- if 'value' in cleaned_data and cleaned_data['value'] is not None:
- self.value = cleaned_data['value']
+ def value_formfields(self):
+ field = self._meta.get_field('content_type')
+ fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))}
+
+ if self.content_type:
+ kwargs = {
+ 'initial': self.object_ids,
+ 'required': False,
+ 'queryset': self.content_type.model_class()._default_manager.all()
+ }
+ fields['value'] = forms.ModelMultipleChoiceField(**kwargs)
+ return fields
+
+ def construct_instance(self, **kwargs):
+ field_name = self._meta.get_field('content_type').name
+ ct = kwargs.pop(field_name, None)
+ if ct is None or ct != self.content_type:
+ self.values.clear()
+ self.content_type = ct
else:
- self.content_type = cleaned_data.get('content_type', None)
- # If there is no value set in the cleaned data, clear the stored value.
- self.value = []
+ value = kwargs.get('value', None)
+ if not value:
+ value = self.content_type.model_class()._default_manager.none()
+ self.set_value(value)
+ construct_instance.alters_data = True
class Meta:
app_label = 'philo'
value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
value = generic.GenericForeignKey('value_content_type', 'value_object_id')
- key = models.CharField(max_length=255)
+ key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.")
def __unicode__(self):
return u'"%s": %s' % (self.key, self.value)
def attributes(self):
return QuerySetMapper(self.attribute_set.all())
- @property
- def _added_attribute_registry(self):
- if not hasattr(self, '_real_added_attribute_registry'):
- self._real_added_attribute_registry = {}
- return self._real_added_attribute_registry
-
- @property
- def _removed_attribute_registry(self):
- if not hasattr(self, '_real_removed_attribute_registry'):
- self._real_removed_attribute_registry = []
- return self._real_removed_attribute_registry
-
- def save(self, *args, **kwargs):
- super(Entity, self).save(*args, **kwargs)
-
- for key in self._removed_attribute_registry:
- self.attribute_set.filter(key__exact=key).delete()
- del self._removed_attribute_registry[:]
-
- for field, value in self._added_attribute_registry.items():
- try:
- attribute = self.attribute_set.get(key__exact=field.key)
- except Attribute.DoesNotExist:
- attribute = Attribute()
- attribute.entity = self
- attribute.key = field.key
-
- field.set_attribute_value(attribute, value)
- attribute.save()
- self._added_attribute_registry.clear()
-
class Meta:
abstract = True
if root is not None and not self.is_descendant_of(root):
raise AncestorDoesNotExist(root)
- qs = self.get_ancestors()
+ qs = self.get_ancestors(include_self=True)
if root is not None:
qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
- return pathsep.join([getattr(parent, field, '?') for parent in list(qs) + [self]])
+ return pathsep.join([getattr(parent, field, '?') for parent in qs])
path = property(get_path)
def __unicode__(self):
+++ /dev/null
-from django import forms
-from django.core.exceptions import FieldError, ValidationError
-from django.db import models
-from django.db.models.fields import NOT_PROVIDED
-from django.utils import simplejson as json
-from django.utils.text import capfirst
-from philo.signals import entity_class_prepared
-from philo.validators import TemplateValidator, json_validator
-
-
-__all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute')
-
-
-class EntityProxyField(object):
- descriptor_class = None
-
- def __init__(self, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, *args, **kwargs):
- if self.descriptor_class is None:
- raise NotImplementedError('EntityProxyField subclasses must specify a descriptor_class.')
- self.verbose_name = verbose_name
- self.help_text = help_text
- self.default = default
- self.editable = editable
-
- def actually_contribute_to_class(self, sender, **kwargs):
- sender._entity_meta.add_proxy_field(self)
- setattr(sender, self.attname, self.descriptor_class(self))
-
- def contribute_to_class(self, cls, name):
- from philo.models.base import Entity
- if issubclass(cls, Entity):
- self.name = name
- self.attname = name
- if self.verbose_name is None and name:
- self.verbose_name = name.replace('_', ' ')
- entity_class_prepared.connect(self.actually_contribute_to_class, sender=cls)
- else:
- raise FieldError('%s instances can only be declared on Entity subclasses.' % self.__class__.__name__)
-
- def formfield(self, *args, **kwargs):
- raise NotImplementedError('EntityProxyField subclasses must implement a formfield method.')
-
- def value_from_object(self, obj):
- return getattr(obj, self.attname)
-
- def has_default(self):
- return self.default is not NOT_PROVIDED
-
-
-class AttributeFieldDescriptor(object):
- def __init__(self, field):
- self.field = field
-
- def __get__(self, instance, owner):
- if instance:
- if self.field in instance._added_attribute_registry:
- return instance._added_attribute_registry[self.field]
- if self.field in instance._removed_attribute_registry:
- return None
- try:
- return instance.attributes[self.field.key]
- except KeyError:
- return None
- else:
- return None
-
- def __set__(self, instance, value):
- raise NotImplementedError('AttributeFieldDescriptor subclasses must implement a __set__ method.')
-
- def __delete__(self, instance):
- if self.field in instance._added_attribute_registry:
- del instance._added_attribute_registry[self.field]
- instance._removed_attribute_registry.append(self.field)
-
-
-class JSONAttributeDescriptor(AttributeFieldDescriptor):
- def __set__(self, instance, value):
- if self.field in instance._removed_attribute_registry:
- instance._removed_attribute_registry.remove(self.field)
- instance._added_attribute_registry[self.field] = value
-
-
-class ForeignKeyAttributeDescriptor(AttributeFieldDescriptor):
- def __set__(self, instance, value):
- if isinstance(value, (models.Model, type(None))):
- if self.field in instance._removed_attribute_registry:
- instance._removed_attribute_registry.remove(self.field)
- instance._added_attribute_registry[self.field] = value
- else:
- raise AttributeError('The \'%s\' attribute can only be set using existing Model objects.' % self.field.name)
-
-
-class ManyToManyAttributeDescriptor(AttributeFieldDescriptor):
- def __set__(self, instance, value):
- if isinstance(value, models.query.QuerySet):
- if self.field in instance._removed_attribute_registry:
- instance._removed_attribute_registry.remove(self.field)
- instance._added_attribute_registry[self.field] = value
- else:
- raise AttributeError('The \'%s\' attribute can only be set to a QuerySet.' % self.field.name)
-
-
-class AttributeField(EntityProxyField):
- def contribute_to_class(self, cls, name):
- super(AttributeField, self).contribute_to_class(cls, name)
- if self.key is None:
- self.key = name
-
- def set_attribute_value(self, attribute, value, value_class):
- if not isinstance(attribute.value, value_class):
- if isinstance(attribute.value, models.Model):
- attribute.value.delete()
- new_value = value_class()
- else:
- new_value = attribute.value
- new_value.value = value
- new_value.save()
- attribute.value = new_value
-
-
-class JSONAttribute(AttributeField):
- descriptor_class = JSONAttributeDescriptor
-
- def __init__(self, field_template=None, key=None, **kwargs):
- super(AttributeField, self).__init__(**kwargs)
- self.key = key
- if field_template is None:
- field_template = models.CharField(max_length=255)
- self.field_template = field_template
-
- def formfield(self, **kwargs):
- defaults = {'required': False, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
- if self.has_default():
- defaults['initial'] = self.default
- defaults.update(kwargs)
- return self.field_template.formfield(**defaults)
-
- def value_from_object(self, obj):
- try:
- return getattr(obj, self.attname)
- except AttributeError:
- return None
-
- def set_attribute_value(self, attribute, value, value_class=None):
- if value_class is None:
- from philo.models.base import JSONValue
- value_class = JSONValue
- super(JSONAttribute, self).set_attribute_value(attribute, value, value_class)
-
-
-class ForeignKeyAttribute(AttributeField):
- descriptor_class = ForeignKeyAttributeDescriptor
-
- def __init__(self, model, limit_choices_to=None, key=None, **kwargs):
- super(ForeignKeyAttribute, self).__init__(**kwargs)
- self.key = key
- self.model = model
- if limit_choices_to is None:
- limit_choices_to = {}
- self.limit_choices_to = limit_choices_to
-
- def formfield(self, form_class=forms.ModelChoiceField, **kwargs):
- defaults = {'required': False, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
- if self.has_default():
- defaults['initial'] = self.default
- defaults.update(kwargs)
- return form_class(self.model._default_manager.complex_filter(self.limit_choices_to), **defaults)
-
- def value_from_object(self, obj):
- try:
- relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
- except AttributeError:
- return None
- return getattr(relobj, 'pk', None)
-
- def set_attribute_value(self, attribute, value, value_class=None):
- if value_class is None:
- from philo.models.base import ForeignKeyValue
- value_class = ForeignKeyValue
- super(ForeignKeyAttribute, self).set_attribute_value(attribute, value, value_class)
-
-
-class ManyToManyAttribute(ForeignKeyAttribute):
- descriptor_class = ManyToManyAttributeDescriptor
-
- def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
- return super(ManyToManyAttribute, self).formfield(form_class, **kwargs)
-
- def set_attribute_value(self, attribute, value, value_class=None):
- if value_class is None:
- from philo.models.base import ManyToManyValue
- value_class = ManyToManyValue
- super(ManyToManyAttribute, self).set_attribute_value(attribute, value, value_class)
-
-
-class TemplateField(models.TextField):
- def __init__(self, allow=None, disallow=None, secure=True, *args, **kwargs):
- super(TemplateField, self).__init__(*args, **kwargs)
- self.validators.append(TemplateValidator(allow, disallow, secure))
-
-
-class JSONFormField(forms.Field):
- default_validators = [json_validator]
-
- def clean(self, value):
- if value == '' and not self.required:
- return None
- try:
- return json.loads(value)
- except Exception, e:
- raise ValidationError(u'JSON decode error: %s' % e)
-
-
-class JSONDescriptor(object):
- def __init__(self, field):
- self.field = field
-
- def __get__(self, instance, owner):
- if instance is None:
- raise AttributeError # ?
-
- if self.field.name not in instance.__dict__:
- json_string = getattr(instance, self.field.attname)
- instance.__dict__[self.field.name] = json.loads(json_string)
-
- return instance.__dict__[self.field.name]
-
- def __set__(self, instance, value):
- instance.__dict__[self.field.name] = value
- setattr(instance, self.field.attname, json.dumps(value))
-
- def __delete__(self, instance):
- del(instance.__dict__[self.field.name])
- setattr(instance, self.field.attname, json.dumps(None))
-
-
-class JSONField(models.TextField):
- default_validators = [json_validator]
-
- def get_attname(self):
- return "%s_json" % self.name
-
- def contribute_to_class(self, cls, name):
- super(JSONField, self).contribute_to_class(cls, name)
- setattr(cls, name, JSONDescriptor(self))
-
- def formfield(self, *args, **kwargs):
- kwargs["form_class"] = JSONFormField
- return super(JSONField, self).formfield(*args, **kwargs)
-
-
-try:
- from south.modelsinspector import add_introspection_rules
-except ImportError:
- pass
-else:
- add_introspection_rules([], ["^philo\.models\.fields\.TemplateField"])
- add_introspection_rules([], ["^philo\.models\.fields\.JSONField"])
\ No newline at end of file
--- /dev/null
+from django import forms
+from django.db import models
+from django.utils import simplejson as json
+from philo.forms.fields import JSONFormField
+from philo.validators import TemplateValidator, json_validator
+#from philo.models.fields.entities import *
+
+
+class TemplateField(models.TextField):
+ def __init__(self, allow=None, disallow=None, secure=True, *args, **kwargs):
+ super(TemplateField, self).__init__(*args, **kwargs)
+ self.validators.append(TemplateValidator(allow, disallow, secure))
+
+
+class JSONDescriptor(object):
+ def __init__(self, field):
+ self.field = field
+
+ def __get__(self, instance, owner):
+ if instance is None:
+ raise AttributeError # ?
+
+ if self.field.name not in instance.__dict__:
+ json_string = getattr(instance, self.field.attname)
+ instance.__dict__[self.field.name] = json.loads(json_string)
+
+ return instance.__dict__[self.field.name]
+
+ def __set__(self, instance, value):
+ instance.__dict__[self.field.name] = value
+ setattr(instance, self.field.attname, json.dumps(value))
+
+ def __delete__(self, instance):
+ del(instance.__dict__[self.field.name])
+ setattr(instance, self.field.attname, json.dumps(None))
+
+
+class JSONField(models.TextField):
+ default_validators = [json_validator]
+
+ def get_attname(self):
+ return "%s_json" % self.name
+
+ def contribute_to_class(self, cls, name):
+ super(JSONField, self).contribute_to_class(cls, name)
+ setattr(cls, name, JSONDescriptor(self))
+ models.signals.pre_init.connect(self.fix_init_kwarg, sender=cls)
+
+ def fix_init_kwarg(self, sender, args, kwargs, **signal_kwargs):
+ if self.name in kwargs:
+ kwargs[self.attname] = json.dumps(kwargs.pop(self.name))
+
+ def formfield(self, *args, **kwargs):
+ kwargs["form_class"] = JSONFormField
+ return super(JSONField, self).formfield(*args, **kwargs)
+
+
+try:
+ from south.modelsinspector import add_introspection_rules
+except ImportError:
+ pass
+else:
+ add_introspection_rules([], ["^philo\.models\.fields\.TemplateField"])
+ add_introspection_rules([], ["^philo\.models\.fields\.JSONField"])
\ No newline at end of file
--- /dev/null
+"""
+The EntityProxyFields defined in this file can be assigned as fields on
+a subclass of philo.models.Entity. They act like any other model
+fields, but instead of saving their data to the database, they save it
+to attributes related to a model instance. Additionally, a new
+attribute will be created for an instance if and only if the field's
+value has been set. This is relevant i.e. for passthroughs, where the
+value of the field may be defined by some other instance's attributes.
+
+Example::
+
+ class Thing(Entity):
+ numbers = models.PositiveIntegerField()
+
+ class ThingProxy(Thing):
+ improvised = JSONAttribute(models.BooleanField)
+"""
+from itertools import tee
+from django import forms
+from django.core.exceptions import FieldError
+from django.db import models
+from django.db.models.fields import NOT_PROVIDED
+from django.utils.text import capfirst
+from philo.signals import entity_class_prepared
+from philo.models import ManyToManyValue, JSONValue, ForeignKeyValue, Attribute, Entity
+import datetime
+
+
+__all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute')
+
+
+ATTRIBUTE_REGISTRY = '_attribute_registry'
+
+
+class EntityProxyField(object):
+ def __init__(self, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, choices=None, *args, **kwargs):
+ self.verbose_name = verbose_name
+ self.help_text = help_text
+ self.default = default
+ self.editable = editable
+ self._choices = choices or []
+
+ def actually_contribute_to_class(self, sender, **kwargs):
+ sender._entity_meta.add_proxy_field(self)
+
+ def contribute_to_class(self, cls, name):
+ if issubclass(cls, Entity):
+ self.name = self.attname = name
+ self.model = cls
+ if self.verbose_name is None and name:
+ self.verbose_name = name.replace('_', ' ')
+ entity_class_prepared.connect(self.actually_contribute_to_class, sender=cls)
+ else:
+ raise FieldError('%s instances can only be declared on Entity subclasses.' % self.__class__.__name__)
+
+ def formfield(self, form_class=forms.CharField, **kwargs):
+ defaults = {
+ 'required': False,
+ 'label': capfirst(self.verbose_name),
+ 'help_text': self.help_text
+ }
+ if self.has_default():
+ defaults['initial'] = self.default
+ defaults.update(kwargs)
+ return form_class(**defaults)
+
+ def value_from_object(self, obj):
+ """The return value of this method will be used by the EntityForm as
+ this field's initial value."""
+ return getattr(obj, self.name)
+
+ def get_storage_value(self, value):
+ """Final conversion of `value` before it gets stored on an Entity instance.
+ This step is performed by the ProxyFieldForm."""
+ return value
+
+ def has_default(self):
+ return self.default is not NOT_PROVIDED
+
+ def _get_choices(self):
+ if hasattr(self._choices, 'next'):
+ choices, self._choices = tee(self._choices)
+ return choices
+ else:
+ return self._choices
+ choices = property(_get_choices)
+
+
+class AttributeFieldDescriptor(object):
+ def __init__(self, field):
+ self.field = field
+
+ def get_registry(self, instance):
+ if ATTRIBUTE_REGISTRY not in instance.__dict__:
+ instance.__dict__[ATTRIBUTE_REGISTRY] = {'added': set(), 'removed': set()}
+ return instance.__dict__[ATTRIBUTE_REGISTRY]
+
+ def __get__(self, instance, owner):
+ if instance is None:
+ return self
+
+ if self.field.name not in instance.__dict__:
+ instance.__dict__[self.field.name] = instance.attributes.get(self.field.attribute_key, None)
+
+ return instance.__dict__[self.field.name]
+
+ def __set__(self, instance, value):
+ if instance is None:
+ raise AttributeError("%s must be accessed via instance" % self.field.name)
+
+ self.field.validate_value(value)
+ instance.__dict__[self.field.name] = value
+
+ registry = self.get_registry(instance)
+ registry['added'].add(self.field)
+ registry['removed'].discard(self.field)
+
+ def __delete__(self, instance):
+ del instance.__dict__[self.field.name]
+
+ registry = self.get_registry(instance)
+ registry['added'].discard(self.field)
+ registry['removed'].add(self.field)
+
+
+def process_attribute_fields(sender, instance, created, **kwargs):
+ if ATTRIBUTE_REGISTRY in instance.__dict__:
+ registry = instance.__dict__[ATTRIBUTE_REGISTRY]
+ instance.attribute_set.filter(key__in=[field.attribute_key for field in registry['removed']]).delete()
+
+ for field in registry['added']:
+ try:
+ attribute = instance.attribute_set.get(key=field.attribute_key)
+ except Attribute.DoesNotExist:
+ attribute = Attribute()
+ attribute.entity = instance
+ attribute.key = field.attribute_key
+
+ value_class = field.value_class
+ if isinstance(attribute.value, value_class):
+ value = attribute.value
+ else:
+ if isinstance(attribute.value, models.Model):
+ attribute.value.delete()
+ value = value_class()
+
+ value.set_value(getattr(instance, field.name, None))
+ value.save()
+
+ attribute.value = value
+ attribute.save()
+ del instance.__dict__[ATTRIBUTE_REGISTRY]
+
+
+class AttributeField(EntityProxyField):
+ def __init__(self, attribute_key=None, **kwargs):
+ self.attribute_key = attribute_key
+ super(AttributeField, self).__init__(**kwargs)
+
+ def actually_contribute_to_class(self, sender, **kwargs):
+ super(AttributeField, self).actually_contribute_to_class(sender, **kwargs)
+ setattr(sender, self.name, AttributeFieldDescriptor(self))
+ opts = sender._entity_meta
+ if not hasattr(opts, '_has_attribute_fields'):
+ opts._has_attribute_fields = True
+ models.signals.post_save.connect(process_attribute_fields, sender=sender)
+
+ def contribute_to_class(self, cls, name):
+ if self.attribute_key is None:
+ self.attribute_key = name
+ super(AttributeField, self).contribute_to_class(cls, name)
+
+ def validate_value(self, value):
+ "Confirm that the value is valid or raise an appropriate error."
+ pass
+
+ @property
+ def value_class(self):
+ raise AttributeError("value_class must be defined on AttributeField subclasses.")
+
+
+class JSONAttribute(AttributeField):
+ value_class = JSONValue
+
+ def __init__(self, field_template=None, **kwargs):
+ super(JSONAttribute, self).__init__(**kwargs)
+ if field_template is None:
+ field_template = models.CharField(max_length=255)
+ self.field_template = field_template
+
+ def formfield(self, **kwargs):
+ defaults = {
+ 'required': False,
+ 'label': capfirst(self.verbose_name),
+ 'help_text': self.help_text
+ }
+ if self.has_default():
+ defaults['initial'] = self.default
+ defaults.update(kwargs)
+ return self.field_template.formfield(**defaults)
+
+ def value_from_object(self, obj):
+ value = super(JSONAttribute, self).value_from_object(obj)
+ if isinstance(self.field_template, (models.DateField, models.DateTimeField)):
+ value = self.field_template.to_python(value)
+ return value
+
+ def get_storage_value(self, value):
+ if isinstance(value, datetime.datetime):
+ return value.strftime("%Y-%m-%d %H:%M:%S")
+ if isinstance(value, datetime.date):
+ return value.strftime("%Y-%m-%d")
+ return value
+
+
+class ForeignKeyAttribute(AttributeField):
+ value_class = ForeignKeyValue
+
+ def __init__(self, model, limit_choices_to=None, **kwargs):
+ super(ForeignKeyAttribute, self).__init__(**kwargs)
+ self.to = model
+ if limit_choices_to is None:
+ limit_choices_to = {}
+ self.limit_choices_to = limit_choices_to
+
+ def validate_value(self, value):
+ if value is not None and not isinstance(value, self.to) :
+ raise TypeError("The '%s' attribute can only be set to an instance of %s or None." % (self.name, self.to.__name__))
+
+ def formfield(self, form_class=forms.ModelChoiceField, **kwargs):
+ defaults = {
+ 'queryset': self.to._default_manager.complex_filter(self.limit_choices_to)
+ }
+ defaults.update(kwargs)
+ return super(ForeignKeyAttribute, self).formfield(form_class=form_class, **defaults)
+
+ def value_from_object(self, obj):
+ relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
+ return getattr(relobj, 'pk', None)
+
+ def get_related_field(self):
+ """Spoof being a rel from a ForeignKey."""
+ return self.to._meta.pk
+
+
+class ManyToManyAttribute(ForeignKeyAttribute):
+ value_class = ManyToManyValue
+
+ def validate_value(self, value):
+ if not isinstance(value, models.query.QuerySet) or value.model != self.to:
+ raise TypeError("The '%s' attribute can only be set to a %s QuerySet." % (self.name, self.to.__name__))
+
+ def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
+ return super(ManyToManyAttribute, self).formfield(form_class=form_class, **kwargs)
+
+ def value_from_object(self, obj):
+ qs = super(ForeignKeyAttribute, self).value_from_object(obj)
+ try:
+ return qs.values_list('pk', flat=True)
+ except:
+ return []
\ No newline at end of file
return '/%s/%s/' % (node.get_absolute_url().strip('/'), subpath.strip('/'))
return subpath
+ def get_context(self):
+ """Hook for providing instance-specific context - such as the value of a Field - to all views."""
+ return {}
+
+ def basic_view(self, view_name):
+ """
+ Wraps a field name and returns a simple view function that will render that view
+ with a basic context. This assumes that the field name is a ForeignKey to a
+ model with a render_to_response method.
+ """
+ field = self._meta.get_field(view_name)
+ view = getattr(self, field.name, None)
+
+ def inner(request, extra_context=None, **kwargs):
+ if not view:
+ raise Http404
+ context = self.get_context()
+ context.update(extra_context or {})
+ return view.render_to_response(request, extra_context=context)
+
+ return inner
+
class Meta:
abstract = True
<!-- group -->
<div class="group tabular{% if inline_admin_formset.opts.classes %} {{ inline_admin_formset.opts.classes|join:" " }}{% endif %}"
id="{{ inline_admin_formset.formset.prefix }}-group" >
- <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
+ <h2 class="collapse-handler">{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
<ul class="tools">
<li class="add-handler-container"><a href="javascript://" class="icon add-handler" title="{% trans 'Add Another' %}"> </a></li>
</ul>
<script type="text/javascript">
(function($) {
- $(document).ready(function($) {
-
- $("#{{ inline_admin_formset.formset.prefix }}-group").inline({
- prefix: "{{ inline_admin_formset.formset.prefix }}",
- deleteCssClass: "delete-handler",
- emptyCssClass: "empty-form",
- onAdded: tabular_onAdded
- });
-
-{% if inline_admin_formset.opts.sortable_field_name %}
- /**
- * sortable inlines
- * uses onAdded() and onRemoved() of inline() call above
- * uses sortable_updateFormIndex() and is_form_filled() from change_from.html
- */
-
- // hide sortable_field(_name) from form
- // hide div.td.{{ field.name }}
- var position_nodes = $("#{{ inline_admin_formset.formset.prefix }}-group").find("div.td.{{ inline_admin_formset.opts.sortable_field_name }}");
- position_nodes.hide();
-
- // hide its header (div.th) too (hard)
- // "div.th.{{ inline_admin_formset.opts.sortable_field_name }}" is not correct because
- // its div.th.<field.label> (and not name, see line#18).
-
- // so let's get the "position/idx" the first position div
- var tabular_row = position_nodes.first().parent().children("div.td");
- // get the "position" (== i) in the "table"
- for (var i = 0; i < tabular_row.length; i++) {
- if ($(tabular_row[i]).hasClass("{{ inline_admin_formset.opts.sortable_field_name }}")) break;
- }
- // we have the same order in the header of the "table"
- // so delete the div at the "position" (== i)
- var position_header = $("#{{ inline_admin_formset.formset.prefix }}-group").find("div.th")[i];
- // and hide
- $(position_header).hide()
-
- {% if errors %}
- // sort inline
- var container = $("#{{ inline_admin_formset.formset.prefix }}-group > div.table"),
- dynamic_forms = container.find("div.dynamic-form"),
- updated = false,
- curr_form,
- real_pos;
-
- // loop thru all inline forms
- for (var i = 0; i < dynamic_forms.length; i++) {
- curr_form = $(dynamic_forms[i]);
- // the real position according to the sort_field(_name)
- real_pos = curr_form.find("div.{{ inline_admin_formset.opts.sortable_field_name }}").find("input").val();
- // if there is none it's an empty inline (=> we are at the end)
- // TODO: klemens: maybe buggy. try continue?
- if (!real_pos) continue;
-
- real_pos = parseInt(real_pos, 10);
-
- // check if real position is not equal to the CURRENT position in the dom
- if (real_pos != container.find("div.dynamic-form").index(curr_form)) {
- // move to correct postition
- curr_form.insertBefore(container.find("div.dynamic-form")[real_pos]);
- // to update the inline lables
- updated = true;
- }
- }
-
- {% endif %}
-
- $("#{{ inline_admin_formset.formset.prefix }}-group > div.table").sortable({
- // drag&drop the inlines with the drag-handler only
- handle: "a.drag-handler",
- // very scary magic after drap&drop operations
- // pretty similar to inline() widget's removeHandler()
- // but removeHandler() can remove the current form and just reorder the rest
- // if we would do the same after drag&drop we would loose some form values
- // because while looping inputs would have the same names and maybe overwrite each other.
- placeholder: 'ui-sortable-placeholder',
- forcePlaceholderSize: true,
- items: "div.dynamic-form",
- axis: "y",
- start: function(evt, ui) {
- ui.item.hide()
- ui.placeholder.height(ui.placeholder.height()-4);
- //sadly we have to do this every time we start dragging
- var template = "<div class='tr'>",
- // minus 1 because we don't need the "sortable_field_name row"
- len = ui.item.find("div.tr").children("div.td").length - 1;
-
- for (var i = 0; i < len; i++) {
- template += "<div class='td'></div>"
- }
-
- template += "</div>"
- ui.placeholder.addClass("tbody module").append(template);
- },
- update: function(evt, ui) {
- ui.item.show();
- },
- appendTo: 'body',
- forceHelperSize: true,
- containment: '#{{ inline_admin_formset.formset.prefix }}-group > div.table',
- tolerance: 'pointer',
- helper: function(evt, elem) {
- var helper = $("<div class='module table' />");
- helper.html(elem.clone());
- return helper;
- },
- });
-
- // sets the new positions onSubmit (0 - n)
- $("#{{ opts.module_name }}_form").bind("submit", function(){
- var forms = $("#{{ inline_admin_formset.formset.prefix }}-group").find("div.dynamic-form"),
- form,
- idx = 0;
- for (var i = 0; i < forms.length; i++) {
- form = $(forms[i]);
-
- if (is_form_filled(form)) {
- form.find("div.{{ inline_admin_formset.opts.sortable_field_name }}").find("input").val(idx);
- idx++;
- }
- }
- });
-
-{% endif %}
-
- });
+ $(document).ready(function($) {
+
+ $("#{{ inline_admin_formset.formset.prefix }}-group").grp_inline({
+ prefix: "{{ inline_admin_formset.formset.prefix }}",
+ onBeforeAdded: function(inline) {},
+ onAfterAdded: function(form) {
+ grappelli.reinitDateTimeFields(form);
+ grappelli.updateSelectFilter(form);
+ form.find("input.vForeignKeyRawIdAdminField").grp_related_fk({lookup_url:"{% url grp_related_lookup %}"});
+ form.find("input.vManyToManyRawIdAdminField").grp_related_m2m({lookup_url:"{% url grp_m2m_lookup %}"});
+ form.find("input[name*='object_id'][name$='id']").grp_related_generic({lookup_url:"{% url grp_related_lookup %}"});
+ },
+ });
+
+ {% if inline_admin_formset.opts.sortable_field_name %}
+ $("#{{ inline_admin_formset.formset.prefix }}-group > div.table").sortable({
+ handle: "a.drag-handler",
+ items: "div.dynamic-form",
+ axis: "y",
+ appendTo: 'body',
+ forceHelperSize: true,
+ containment: '#{{ inline_admin_formset.formset.prefix }}-group > div.table',
+ tolerance: 'pointer',
+ });
+ $("#{{ opts.module_name }}_form").bind("submit", function(){
+ var sortable_field_name = "{{ inline_admin_formset.opts.sortable_field_name }}";
+ var i = 0;
+ $("#{{ inline_admin_formset.formset.prefix }}-group").find("div.dynamic-form").each(function(){
+ var fields = $(this).find("div.td :input[value]");
+ if (fields.serialize()) {
+ $(this).find("input[name$='"+sortable_field_name+"']").val(i);
+ i++;
+ }
+ });
+ });
+ {% endif %}
+
+ });
})(django.jQuery);
</script>
-
self.template = None
def compile_instance(self, object_pk):
- self.object_pk = object_pk
model = self.content_type.model_class()
try:
return model.objects.get(pk=object_pk)
setattr(ConstantEmbedNode, LOADED_TEMPLATE_ATTR, property(get_embedded))
-def get_content_type(bit):
+def parse_content_type(bit, tagname):
try:
app_label, model = bit.split('.')
except ValueError:
- raise template.TemplateSyntaxError('"%s" template tag expects the first argument to be of the form app_label.model' % tag)
+ raise template.TemplateSyntaxError('"%s" template tag expects the first argument to be of the form app_label.model' % tagname)
try:
ct = ContentType.objects.get(app_label=app_label, model=model)
except ContentType.DoesNotExist:
- raise template.TemplateSyntaxError('"%s" template tag requires an argument of the form app_label.model which refers to an installed content type (see django.contrib.contenttypes)' % tag)
+ raise template.TemplateSyntaxError('"%s" template tag requires an argument of the form app_label.model which refers to an installed content type (see django.contrib.contenttypes)' % tagname)
return ct
raise template.TemplateSyntaxError('"%s" template tag must have at least two arguments.' % tag)
if len(bits) == 3 and bits[-2] == 'with':
- ct = get_content_type(bits[0])
+ ct = parse_content_type(bits[0], tag)
if bits[2][0] in ['"', "'"] and bits[2][0] == bits[2][-1]:
return ConstantEmbedNode(ct, template_name=bits[2])
return InstanceEmbedNode(instance, kwargs)
elif len(bits) > 2:
raise template.TemplateSyntaxError('"%s" template tag expects at most 2 non-keyword arguments when embedding instances.')
- ct = get_content_type(bits[0])
+ ct = parse_content_type(bits[0], tag)
pk = bits[1]
try: