* (Optional) south 0.7.2+ <http://south.aeracode.org/>
* (Optional) recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>
-To contribute, please visit the project website <http://project.philocms.org/> and/or make a fork of the git repository on GitHub <http://github.com/ithinksw/philo> or Gitorious <http://gitorious
-.org/ithinksw/philo>. Feel free to join us on IRC at irc://irc.oftc.net/#philo.
-
-
====
Using philo
====
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!
+Philo should be ready to go! All that's left is to learn more <http://philo.readthedocs.org> and contribute <http://philo.readthedocs.org/en/latest/contribute.html>.
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!
+Philo should be ready to go! All that's left is to [learn more](http://philo.readthedocs.org) and [contribute](http://philo.readthedocs.org/en/latest/contribute.html).
--- /dev/null
+Contributing to Philo
+=====================
+
+So you want to contribute to Philo? That's great! Here's some ways you can get started:
+
+* **Report bugs and request features.** :mod:`philo` uses a Redmine installation located at `http://ithinksw.org/projects/philo/issues <http://ithinksw.org/projects/philo/issues>`_ for issue tracking. In order to report an issue, you will need to register for an account with the tracker.
+* **Contribute code.** Philo uses git to manage its code. You can fork philo's repository either on `GitHub <http://github.com/ithinksw/philo>`_ or `Gitorious <http://gitorious.org/ithinksw/philo>`_. If you are contributing to Philo, you may need to submit a `Contributor License Agreement <http://en.wikipedia.org/wiki/Contributor_License_Agreement>`_.
+* **Join the discussion** on IRC at `irc://irc.oftc.net/#philo <irc://irc.oftc.net/#philo>`_ if you have any questions or suggestions or just want to chat about the project. You can also keep in touch via :mod:`philo`'s mailing lists: `philo@ithinksw.org <mailto:philo@ithinksw.org>`_ and `philo-devel@ithinksw.org <mailto:philo-devel@ithinksw.org>`_.
* (Optional) `south 0.7.2+ <http://south.aeracode.org/>`_
* (Optional) `recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>`_
-To contribute, please visit the `project website <http://project.philocms.org/>`_ and/or make a fork of the git repository on `GitHub <http://github.com/ithinksw/philo>`_ or `Gitorious <http://gitorious.org/ithinksw/philo>`_. Feel free to join us on IRC at `irc://irc.oftc.net/#philo <irc://irc.oftc.net/#philo>`_.
-
Contents
++++++++
forms
loaders
contrib/intro
+ contributing
Indices and tables
++++++++++++++++++
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, BaseModelFormSet
from django.forms.formsets import TOTAL_FORM_COUNT
from django.utils.datastructures import SortedDict
-from philo.admin.widgets import ModelLookupWidget
+from philo.admin.widgets import ModelLookupWidget, EmbedWidget
from philo.models import Contentlet, ContentReference
class ContentletForm(ContainerForm):
- content = forms.CharField(required=False, widget=AdminTextareaWidget, label='Content')
+ content = forms.CharField(required=False, widget=EmbedWidget, label='Content')
def should_delete(self):
# Delete iff: the data has changed and is now empty.
from philo.admin.base import COLLAPSE_CLASSES, TreeEntityAdmin
from philo.admin.forms.containers import *
from philo.admin.nodes import ViewAdmin
+from philo.admin.widgets import EmbedWidget
+from philo.models.fields import TemplateField
from philo.models.pages import Page, Template, Contentlet, ContentReference
-class ContentletInline(admin.StackedInline):
- model = Contentlet
+class ContainerInline(admin.StackedInline):
extra = 0
max_num = 0
- formset = ContentletInlineFormSet
- form = ContentletForm
can_delete = False
classes = ('collapse-open', 'collapse','open')
if 'grappelli' in settings.INSTALLED_APPS:
template = 'admin/philo/edit_inline/tabular_container.html'
-class ContentReferenceInline(admin.StackedInline):
+class ContentletInline(ContainerInline):
+ model = Contentlet
+ formset = ContentletInlineFormSet
+ form = ContentletForm
+
+
+class ContentReferenceInline(ContainerInline):
model = ContentReference
- extra = 0
- max_num = 0
formset = ContentReferenceInlineFormSet
form = ContentReferenceForm
- can_delete = False
- classes = ('collapse-open', 'collapse','open')
- if 'grappelli' in settings.INSTALLED_APPS:
- template = 'admin/philo/edit_inline/grappelli_tabular_container.html'
- else:
- template = 'admin/philo/edit_inline/tabular_container.html'
class PageAdmin(ViewAdmin):
'fields': ('mimetype',)
}),
)
+ formfield_overrides = {
+ TemplateField: {'widget': EmbedWidget}
+ }
save_on_top = True
save_as = True
list_display = ('__unicode__', 'slug', 'get_path',)
from django import forms
from django.conf import settings
from django.contrib.admin.widgets import url_params_from_lookup_dict
+from django.utils import simplejson as json
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.text import truncate_words
output.append(' <strong>%s</strong>' % escape(truncate_words(value_object, 14)))
except value_class.DoesNotExist:
pass
- return mark_safe(u''.join(output))
\ No newline at end of file
+ return mark_safe(u''.join(output))
+
+
+class EmbedWidget(forms.Textarea):
+ """A form widget with the HTML class embedding and an embedded list of content-types."""
+ def __init__(self, attrs=None):
+ from philo.models import value_content_type_limiter
+
+ content_types = value_content_type_limiter.classes
+ data = []
+
+ for content_type in content_types:
+ data.append({'app_label': content_type._meta.app_label, 'object_name': content_type._meta.object_name.lower(), 'verbose_name': unicode(content_type._meta.verbose_name)})
+
+ json_ = json.dumps(data)
+
+ default_attrs = {'class': 'embedding vLargeTextField', 'data-content-types': json_ }
+
+ if attrs:
+ default_attrs.update(attrs)
+
+ super(EmbedWidget, self).__init__(default_attrs)
+
+ class Media:
+ css = {
+ 'all': ('philo/css/EmbedWidget.css',),
+ }
+ js = ('philo/js/EmbedWidget.js',)
from django.http import HttpResponseRedirect, QueryDict
from philo.admin import EntityAdmin, COLLAPSE_CLASSES
+from philo.admin.widgets import EmbedWidget
from philo.contrib.penfield.models import BlogEntry, Blog, BlogView, Newsletter, NewsletterArticle, NewsletterIssue, NewsletterView
+from philo.models.fields import TemplateField
class DelayedDateForm(forms.ModelForm):
)
related_lookup_fields = {'fk': raw_id_fields}
prepopulated_fields = {'slug': ('title',)}
+ formfield_overrides = {
+ TemplateField: {'widget': EmbedWidget}
+ }
class BlogViewAdmin(EntityAdmin):
)
actions = ['make_issue']
prepopulated_fields = {'slug': ('title',)}
+ formfield_overrides = {
+ TemplateField: {'widget': EmbedWidget}
+ }
def author_names(self, obj):
return ', '.join([author.get_full_name() for author in obj.authors.all()])
+# encoding: utf-8
from datetime import date, datetime
from django.conf import settings
date = models.DateTimeField(default=None)
#: The content of the :class:`BlogEntry`.
- content = models.TextField()
+ content = TemplateField()
#: An optional brief excerpt from the :class:`BlogEntry`.
- excerpt = models.TextField(blank=True, null=True)
+ excerpt = TemplateField(blank=True, null=True)
#: A ``django-taggit`` :class:`TaggableManager`.
tags = TaggableManager()
tag_permalink_base = models.CharField(max_length=255, blank=False, default='tags')
item_context_var = 'entries'
- object_attr = 'blog'
def __unicode__(self):
return u'BlogView for %s' % self.blog.title
@property
def urlpatterns(self):
- urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index') +\
- self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_entries_by_tag', 'tag_page', 'entries_by_tag')
+ urlpatterns = self.feed_patterns(r'^', 'get_entries', 'index_page', 'index') +\
+ self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_entries', 'tag_page', 'entries_by_tag')
if self.tag_archive_page_id:
urlpatterns += patterns('',
if self.entry_archive_page_id:
if self.entry_permalink_style in 'DMY':
- urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_year')
+ urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})', 'get_entries', 'entry_archive_page', 'entries_by_year')
if self.entry_permalink_style in 'DM':
- urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_month')
+ urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'get_entries', 'entry_archive_page', 'entries_by_month')
if self.entry_permalink_style == 'D':
- urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_day')
+ urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'get_entries', 'entry_archive_page', 'entries_by_day')
if self.entry_permalink_style == 'D':
urlpatterns += patterns('',
)
return urlpatterns
- def get_context(self):
- return {'blog': self.blog}
-
def get_entry_queryset(self, obj):
"""Returns the default :class:`QuerySet` of :class:`BlogEntry` instances for the :class:`BlogView` - all entries that are considered posted in the past. This allows for scheduled posting of entries."""
return obj.entries.filter(date__lte=datetime.now())
"""Returns the default :class:`QuerySet` of :class:`.Tag`\ s for the :class:`BlogView`'s :meth:`get_entries_by_tag` and :meth:`tag_archive_view`."""
return obj.entry_tags
- def get_all_entries(self, obj, request, extra_context=None):
- """Used to generate :meth:`~.FeedView.feed_patterns` for all entries."""
- return self.get_entry_queryset(obj), extra_context
-
- def get_entries_by_ymd(self, obj, request, year=None, month=None, day=None, extra_context=None):
- """Used to generate :meth:`~.FeedView.feed_patterns` for entries with a specific year, month, and day."""
- if not self.entry_archive_page:
- raise Http404
- entries = self.get_entry_queryset(obj)
- if year:
- entries = entries.filter(date__year=year)
- if month:
- entries = entries.filter(date__month=month)
- if day:
- entries = entries.filter(date__day=day)
+ def get_object(self, request, year=None, month=None, day=None, tag_slugs=None):
+ """Returns a dictionary representing the parameters for a feed which will be exposed."""
+ if tag_slugs is None:
+ tags = None
+ else:
+ tag_slugs = tag_slugs.replace('+', '/').split('/')
+ tags = self.get_tag_queryset(self.blog).filter(slug__in=tag_slugs)
+ if not tags:
+ raise Http404
+
+ # Raise a 404 on an incorrect slug.
+ found_slugs = set([tag.slug for tag in tags])
+ for slug in tag_slugs:
+ if slug and slug not in found_slugs:
+ raise Http404
- context = extra_context or {}
- context.update({'year': year, 'month': month, 'day': day})
- return entries, context
+ try:
+ if year and month and day:
+ context_date = date(int(year), int(month), int(day))
+ elif year and month:
+ context_date = date(int(year), int(month), 1)
+ elif year:
+ context_date = date(int(year), 1, 1)
+ else:
+ context_date = None
+ except TypeError, ValueError:
+ context_date = None
+
+ return {
+ 'blog': self.blog,
+ 'tags': tags,
+ 'year': year,
+ 'month': month,
+ 'day': day,
+ 'date': context_date
+ }
- def get_entries_by_tag(self, obj, request, tag_slugs, extra_context=None):
- """Used to generate :meth:`~.FeedView.feed_patterns` for entries with all of the given tags."""
- tag_slugs = tag_slugs.replace('+', '/').split('/')
- tags = self.get_tag_queryset(obj).filter(slug__in=tag_slugs)
+ def get_entries(self, obj, request, year=None, month=None, day=None, tag_slugs=None, extra_context=None):
+ """Returns the :class:`BlogEntry` objects which will be exposed for the given object, as returned from :meth:`get_object`."""
+ entries = self.get_entry_queryset(obj['blog'])
- if not tags:
- raise Http404
+ if obj['tags'] is not None:
+ tags = obj['tags']
+ for tag in tags:
+ entries = entries.filter(tags=tag)
- # 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.get_entry_queryset(obj)
- for tag in tags:
- entries = entries.filter(tags=tag)
+ if obj['date'] is not None:
+ if year:
+ entries = entries.filter(date__year=year)
+ if month:
+ entries = entries.filter(date__month=month)
+ if day:
+ entries = entries.filter(date__day=day)
context = extra_context or {}
- context.update({'tags': tags})
+ context.update(obj)
return entries, context
})
return self.tag_archive_page.render_to_response(request, extra_context=context)
- def feed_view(self, get_items_attr, reverse_name, feed_type=None):
- """Overrides :meth:`.FeedView.feed_view` to add :class:`.Tag`\ s to the feed as categories."""
- get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
-
- def inner(request, extra_context=None, *args, **kwargs):
- obj = self.get_object(request, *args, **kwargs)
- feed = self.get_feed(obj, request, reverse_name, feed_type, *args, **kwargs)
- items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
- self.populate_feed(feed, items, request)
-
- if 'tags' in extra_context:
- tags = extra_context['tags']
- feed.feed['link'] = request.node.construct_url(self.reverse(obj=tags), with_domain=True, request=request, secure=request.is_secure())
- else:
- tags = obj.entry_tags
-
- feed.feed['categories'] = [tag.name for tag in tags]
-
- response = HttpResponse(mimetype=feed.mime_type)
- feed.write(response, 'utf-8')
- return response
-
- return inner
-
def process_page_items(self, request, items):
"""Overrides :meth:`.FeedView.process_page_items` to add pagination."""
if self.entries_per_page:
return items, item_context
def title(self, obj):
- return obj.title
+ title = obj['blog'].title
+ if obj['tags']:
+ title += u" – %s" % u", ".join((tag.name for tag in obj['tags']))
+ date = obj['date']
+ if date:
+ if obj['day']:
+ datestr = date.strftime("%F %j, %Y")
+ elif obj['month']:
+ datestr = date.strftime("%F, %Y")
+ elif obj['year']:
+ datestr = date.strftime("%Y")
+ title += u" – %s" % datestr
+ return title
+
+ def categories(self, obj):
+ tags = obj['tags']
+ if tags:
+ return (tag.name for tag in tags)
+ return None
def item_title(self, item):
return item.title
"""
get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr)
- page = page_attr if isinstance(page_attr, Page) else getattr(self, page_attr)
def inner(request, extra_context=None, *args, **kwargs):
obj = self.get_object(request, *args, **kwargs)
context.update(extra_context or {})
context.update(item_context or {})
+ page = page_attr if isinstance(page_attr, Page) else getattr(self, page_attr)
return page.render_to_response(request, extra_context=context)
return inner
+++ /dev/null
-from django.forms.widgets import Textarea
-from django.utils import simplejson as json
-
-__all__ = ('EmbedWidget',)
-
-class EmbedWidget(Textarea):
- """A form widget with the HTML class embedding and an embedded list of content-types."""
- def __init__(self, attrs=None):
- from philo.models import value_content_type_limiter
-
- content_types = value_content_type_limiter.classes
- data = []
-
- for content_type in content_types:
- data.append({'app_label': content_type._meta.app_label, 'object_name': content_type._meta.object_name.lower(), 'verbose_name': unicode(content_type._meta.verbose_name)})
-
- json_ = json.dumps(data)
-
- default_attrs = {'class': 'embedding vLargeTextField', 'data-content-types': json_ }
-
- if attrs:
- default_attrs.update(attrs)
-
- super(EmbedWidget, self).__init__(default_attrs)
-
- class Media:
- css = {
- 'all': ('philo/css/EmbedWidget.css',),
- }
- js = ('philo/js/EmbedWidget.js',)
\ No newline at end of file
from philo.forms.fields import JSONFormField
from philo.utils.registry import RegistryIterator
from philo.validators import TemplateValidator, json_validator
-from philo.forms.widgets import EmbedWidget
#from philo.models.fields.entities import *
-class TemplateField(models.Field):
+class TemplateField(models.TextField):
"""A :class:`TextField` which is validated with a :class:`.TemplateValidator`. ``allow``, ``disallow``, and ``secure`` will be passed into the validator's construction."""
def __init__(self, allow=None, disallow=None, secure=True, *args, **kwargs):
super(TemplateField, self).__init__(*args, **kwargs)
self.validators.append(TemplateValidator(allow, disallow, secure))
-
- def formfield(self, **kwargs):
- defaults = {'widget': EmbedWidget}
- defaults.update(kwargs)
- return super(TemplateField, self).formfield(**defaults)
class JSONDescriptor(object):
"""This is a shortcut method for :meth:`View.render_to_response`"""
if self.view_object_id and self.view_content_type_id:
view_model = ContentType.objects.get_for_id(self.view_content_type_id).model_class()
- self.view = view_model._default_manager.select_related(depth=1).get(pk=self.view_object_id)
+ self.view = view_model._default_manager.get(pk=self.view_object_id)
return self.view.render_to_response(request, extra_context)
raise Http404
oldDismissRelatedLookupPopup = window.dismissRelatedLookupPopup;
window.dismissRelatedLookupPopup = function (win, chosenId) {
var name = windowname_to_id(win.name),
- elem = $('#'+win.name), val;
+ elem = $('#'+name), val;
// if the original element was an embed widget, run our script
if (elem.parent().hasClass('embed-widget')) {
contenttype = $('select',elem.parent()).val();
+from functools import partial
from UserDict import DictMixin
from django.db import models
from django.contrib.contenttypes.models import ContentType
+from philo.utils.lazycompat import SimpleLazyObject
+
### AttributeMappers
value_lookups.setdefault(a.value_content_type_id, []).append(a.value_object_id)
self._attributes_cache[a.key] = a
- values_bulk = {}
+ values_bulk = dict(((ct_pk, SimpleLazyObject(partial(ContentType.objects.get_for_id(ct_pk).model_class().objects.in_bulk, pks))) for ct_pk, pks in value_lookups.items()))
+
+ cache = {}
- for ct_pk, pks in value_lookups.items():
- values_bulk[ct_pk] = ContentType.objects.get_for_id(ct_pk).model_class().objects.in_bulk(pks)
+ for a in attributes:
+ cache[a.key] = SimpleLazyObject(partial(self._lazy_value_from_bulk, values_bulk, a))
+ a._value_cache = cache[a.key]
- self._cache.update(dict([(a.key, getattr(values_bulk[a.value_content_type_id].get(a.value_object_id), 'value', None)) for a in attributes]))
+ self._cache.update(cache)
self._cache_filled = True
+ def _lazy_value_from_bulk(self, bulk, attribute):
+ v = bulk[attribute.value_content_type_id].get(attribute.value_object_id)
+ return getattr(v, 'value', None)
+
def clear_cache(self):
"""Clears the cache."""
self._cache = {}