Created
July 2, 2010 11:37
-
-
Save kgodey/461250 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/contrib/penfield/models.py b/contrib/penfield/models.py | |
index 371ea57..7dee823 100644 | |
--- a/contrib/penfield/models.py | |
+++ b/contrib/penfield/models.py | |
@@ -4,6 +4,8 @@ from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_mo | |
from django.conf.urls.defaults import url, patterns | |
from django.http import Http404, HttpResponse | |
from datetime import datetime | |
+from philo.contrib.penfield.utils import paginate | |
+from philo.contrib.penfield.validators import validate_pagination_count | |
class Blog(Entity, Titled): | |
@@ -39,12 +41,13 @@ class BlogView(MultiView): | |
) | |
blog = models.ForeignKey(Blog, related_name='blogviews') | |
- | |
+ | |
index_page = models.ForeignKey(Page, related_name='blog_index_related') | |
entry_page = models.ForeignKey(Page, related_name='blog_entry_related') | |
entry_archive_page = models.ForeignKey(Page, related_name='blog_entry_archive_related', null=True, blank=True) | |
tag_page = models.ForeignKey(Page, related_name='blog_tag_related') | |
tag_archive_page = models.ForeignKey(Page, related_name='blog_tag_archive_related', null=True, blank=True) | |
+ entries_per_page = models.IntegerField(blank=True, validators=[validate_pagination_count]) | |
entry_permalink_style = models.CharField(max_length=1, choices=ENTRY_PERMALINK_STYLE_CHOICES) | |
entry_permalink_base = models.CharField(max_length=255, blank=False, default='entries') | |
@@ -87,9 +90,15 @@ class BlogView(MultiView): | |
return base_patterns + entry_patterns | |
def index_view(self, request, node=None, extra_context=None): | |
+ entries = self.blog.entries.all() | |
+ if self.entries_per_page: | |
+ paginated_page = paginate(request, entries, self.entries_per_page) | |
+ entries = paginated_page.object_list | |
+ else: | |
+ paginated_page = None | |
context = {} | |
context.update(extra_context or {}) | |
- context.update({'blog': self.blog}) | |
+ context.update({'blog': self.blog, 'entries': entries, 'paginated_page': paginated_page}) | |
return self.index_page.render_to_response(node, request, extra_context=context) | |
def entry_view(self, request, slug, year=None, month=None, day=None, node=None, extra_context=None): | |
@@ -119,9 +128,14 @@ class BlogView(MultiView): | |
entries = entries.filter(date__month=month) | |
if day: | |
entries = entries.filter(date__day=day) | |
+ if self.entries_per_page: | |
+ paginated_page = paginate(request, entries, self.entries_per_page) | |
+ entries = paginated_page.object_list | |
+ else: | |
+ paginated_page = None | |
context = {} | |
context.update(extra_context or {}) | |
- context.update({'blog': self.blog, 'year': year, 'month': month, 'day': day, 'entries': entries}) | |
+ context.update({'blog': self.blog, 'year': year, 'month': month, 'day': day, 'entries': entries, 'paginated_page': paginated_page}) | |
return self.entry_archive_page.render_to_response(node, request, extra_context=context) | |
def tag_view(self, request, tag, node=None, extra_context=None): | |
@@ -130,9 +144,14 @@ class BlogView(MultiView): | |
except: | |
raise Http404 | |
entries = self.blog.entries.filter(tags=tag) | |
+ if self.entries_per_page: | |
+ paginated_page = paginate(request, entries, self.entries_per_page) | |
+ entries = paginated_page.object_list | |
+ else: | |
+ paginated_page = None | |
context = {} | |
context.update(extra_context or {}) | |
- context.update({'blog': self.blog, 'tag': tag, 'entries': entries}) | |
+ context.update({'blog': self.blog, 'tag': tag, 'entries': entries, 'paginated_page': paginated_page}) | |
return self.tag_page.render_to_response(node, request, extra_context=context) | |
def tag_archive_view(self, request, node=None, extra_context=None): | |
@@ -284,4 +303,4 @@ class NewsletterView(MultiView): | |
context = {} | |
context.update(extra_context or {}) | |
context.update({'newsletter': self.newsletter}) | |
- return self.issue_archive_page.render_to_response(node, request, extra_context=context) | |
\ No newline at end of file | |
+ return self.issue_archive_page.render_to_response(node, request, extra_context=context) | |
diff --git a/contrib/penfield/utils.py b/contrib/penfield/utils.py | |
new file mode 100644 | |
index 0000000..5df800c | |
--- /dev/null | |
+++ b/contrib/penfield/utils.py | |
@@ -0,0 +1,20 @@ | |
+from django.core.paginator import Paginator, InvalidPage, EmptyPage | |
+ | |
+ | |
+def paginate(request, entries, entries_per_page): | |
+ paginator = Paginator(entries, entries_per_page) | |
+ try: | |
+ page_number = int(request.GET.get('page', '1')) | |
+ entries = paginator.page(page_number).object_list | |
+ page = paginator.page(page_number) | |
+ except ValueError: | |
+ page_number = 1 | |
+ entries = paginator.page(page_number).object_list | |
+ page = paginator.page(page_number) | |
+ try: | |
+ entries = paginator.page(page_number).object_list | |
+ page = paginator.page(page_number) | |
+ except (EmptyPage, InvalidPage): | |
+ entries = paginator.page(paginator.num_pages).object_list | |
+ page = paginator.page(page_number) | |
+ return page | |
\ No newline at end of file | |
diff --git a/contrib/penfield/validators.py b/contrib/penfield/validators.py | |
new file mode 100644 | |
index 0000000..48eae06 | |
--- /dev/null | |
+++ b/contrib/penfield/validators.py | |
@@ -0,0 +1,6 @@ | |
+from django.core.exceptions import ValidationError | |
+ | |
+ | |
+def validate_pagination_count(x): | |
+ if x not in range(1, 10000): | |
+ raise ValidationError('Please enter an integer between 1 and 9999.') | |
\ No newline at end of file | |
diff --git a/models.py b/models.py | |
new file mode 100644 | |
index 0000000..92f713f | |
--- /dev/null | |
+++ b/models.py | |
@@ -0,0 +1,417 @@ | |
+# encoding: utf-8 | |
+from django.utils.translation import ugettext_lazy as _ | |
+from django.contrib.auth.models import User, Group | |
+from django.contrib.contenttypes import generic | |
+from django.contrib.contenttypes.models import ContentType | |
+from django.db import models | |
+from django.contrib.sites.models import Site | |
+from philo.utils import fattr | |
+from django.template import add_to_builtins as register_templatetags | |
+from django.template import Template as DjangoTemplate | |
+from django.template import TemplateDoesNotExist | |
+from django.template import Context, RequestContext | |
+from django.core.exceptions import ObjectDoesNotExist | |
+from django.utils import simplejson as json | |
+from UserDict import DictMixin | |
+from philo.templatetags.containers import ContainerNode | |
+from django.template.loader_tags import ExtendsNode, ConstantIncludeNode, IncludeNode | |
+from django.template.loader import get_template | |
+from django.http import Http404, HttpResponse, HttpResponseServerError, HttpResponseRedirect | |
+from django.core.servers.basehttp import FileWrapper | |
+from django.conf import settings | |
+ | |
+ | |
+def register_value_model(model): | |
+ pass | |
+ | |
+ | |
+def unregister_value_model(model): | |
+ pass | |
+ | |
+ | |
+class Attribute(models.Model): | |
+ entity_content_type = models.ForeignKey(ContentType, verbose_name='Entity type') | |
+ entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID') | |
+ entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id') | |
+ key = models.CharField(max_length=255) | |
+ json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.') | |
+ | |
+ def get_value(self): | |
+ return json.loads(self.json_value) | |
+ | |
+ def set_value(self, value): | |
+ self.json_value = json.dumps(value) | |
+ | |
+ def delete_value(self): | |
+ self.json_value = json.dumps(None) | |
+ | |
+ value = property(get_value, set_value, delete_value) | |
+ | |
+ def __unicode__(self): | |
+ return u'"%s": %s' % (self.key, self.value) | |
+ | |
+ | |
+class Relationship(models.Model): | |
+ entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type') | |
+ entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID') | |
+ entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id') | |
+ key = models.CharField(max_length=255) | |
+ value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', verbose_name='Value type') | |
+ value_object_id = models.PositiveIntegerField(verbose_name='Value ID') | |
+ value = generic.GenericForeignKey('value_content_type', 'value_object_id') | |
+ | |
+ def __unicode__(self): | |
+ return u'"%s": %s' % (self.key, self.value) | |
+ | |
+ | |
+class QuerySetMapper(object, DictMixin): | |
+ def __init__(self, queryset, passthrough=None): | |
+ self.queryset = queryset | |
+ self.passthrough = passthrough | |
+ def __getitem__(self, key): | |
+ try: | |
+ return self.queryset.get(key__exact=key).value | |
+ except ObjectDoesNotExist: | |
+ if self.passthrough: | |
+ return self.passthrough.__getitem__(key) | |
+ raise KeyError | |
+ def keys(self): | |
+ keys = set(self.queryset.values_list('key', flat=True).distinct()) | |
+ if self.passthrough: | |
+ keys += set(self.passthrough.keys()) | |
+ return list(keys) | |
+ | |
+ | |
+class Entity(models.Model): | |
+ attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id') | |
+ relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id') | |
+ | |
+ @property | |
+ def attributes(self): | |
+ return QuerySetMapper(self.attribute_set) | |
+ | |
+ @property | |
+ def relationships(self): | |
+ return QuerySetMapper(self.relationship_set) | |
+ | |
+ class Meta: | |
+ abstract = True | |
+ | |
+ | |
+class Collection(models.Model): | |
+ name = models.CharField(max_length=255) | |
+ description = models.TextField(blank=True, null=True) | |
+ | |
+ @fattr(short_description='Members') | |
+ def get_count(self): | |
+ return self.members.count() | |
+ | |
+ def __unicode__(self): | |
+ return self.name | |
+ | |
+ | |
+class CollectionMemberManager(models.Manager): | |
+ use_for_related_fields = True | |
+ | |
+ def with_model(self, model): | |
+ return model._default_manager.filter(pk__in=self.filter(member_content_type=ContentType.objects.get_for_model(model)).values_list('member_object_id', flat=True)) | |
+ | |
+ | |
+class CollectionMember(models.Model): | |
+ objects = CollectionMemberManager() | |
+ collection = models.ForeignKey(Collection, related_name='members') | |
+ index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True) | |
+ member_content_type = models.ForeignKey(ContentType, verbose_name='Member type') | |
+ member_object_id = models.PositiveIntegerField(verbose_name='Member ID') | |
+ member = generic.GenericForeignKey('member_content_type', 'member_object_id') | |
+ | |
+ def __unicode__(self): | |
+ return u'%s - %s' % (self.collection, self.member) | |
+ | |
+ | |
+class TreeManager(models.Manager): | |
+ use_for_related_fields = True | |
+ | |
+ def roots(self): | |
+ return self.filter(parent__isnull=True) | |
+ | |
+ def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'): | |
+ """ | |
+ Returns the object with the path, or None if there is no object with that path, | |
+ unless absolute_result is set to False, in which case it returns a tuple containing | |
+ the deepest object found along the path, and the remainder of the path after that | |
+ object as a string (or None in the case that there is no remaining path). | |
+ """ | |
+ slugs = path.split(pathsep) | |
+ obj = root | |
+ remaining_slugs = list(slugs) | |
+ remainder = None | |
+ for slug in slugs: | |
+ remaining_slugs.remove(slug) | |
+ if slug: # ignore blank slugs, handles for multiple consecutive pathseps | |
+ try: | |
+ obj = self.get(slug__exact=slug, parent__exact=obj) | |
+ except self.model.DoesNotExist: | |
+ if absolute_result: | |
+ obj = None | |
+ remaining_slugs.insert(0, slug) | |
+ remainder = pathsep.join(remaining_slugs) | |
+ break | |
+ if obj: | |
+ if absolute_result: | |
+ return obj | |
+ else: | |
+ return (obj, remainder) | |
+ raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name) | |
+ | |
+ | |
+class TreeModel(models.Model): | |
+ objects = TreeManager() | |
+ parent = models.ForeignKey('self', related_name='children', null=True, blank=True) | |
+ slug = models.SlugField() | |
+ | |
+ def get_path(self, pathsep='/', field='slug'): | |
+ path = getattr(self, field, '?') | |
+ parent = self.parent | |
+ while parent: | |
+ path = getattr(parent, field, '?') + pathsep + path | |
+ parent = parent.parent | |
+ return path | |
+ path = property(get_path) | |
+ | |
+ def __unicode__(self): | |
+ return self.path | |
+ | |
+ class Meta: | |
+ abstract = True | |
+ | |
+ | |
+class TreeEntity(TreeModel, Entity): | |
+ @property | |
+ def attributes(self): | |
+ if self.parent: | |
+ return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes) | |
+ return super(TreeEntity, self).attributes | |
+ | |
+ @property | |
+ def relationships(self): | |
+ if self.parent: | |
+ return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships) | |
+ return super(TreeEntity, self).relationships | |
+ | |
+ class Meta: | |
+ abstract = True | |
+ | |
+ | |
+class InheritableTreeEntity(TreeEntity): | |
+ instance_type = models.ForeignKey(ContentType, editable=False) | |
+ | |
+ def save(self, force_insert=False, force_update=False): | |
+ if not hasattr(self, 'instance_type_ptr'): | |
+ self.instance_type = ContentType.objects.get_for_model(self.__class__) | |
+ super(InheritableTreeEntity, self).save(force_insert, force_update) | |
+ | |
+ @property | |
+ def instance(self): | |
+ return self.instance_type.get_object_for_this_type(id=self.id) | |
+ | |
+ def get_path(self, pathsep='/', field='slug'): | |
+ path = getattr(self.instance, field, '?') | |
+ parent = self.parent | |
+ while parent: | |
+ path = getattr(parent.instance, field, '?') + pathsep + path | |
+ parent = parent.parent | |
+ return path | |
+ path = property(get_path) | |
+ | |
+ @property | |
+ def attributes(self): | |
+ if self.parent: | |
+ return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes) | |
+ return QuerySetMapper(self.instance.attribute_set) | |
+ | |
+ @property | |
+ def relationships(self): | |
+ if self.parent: | |
+ return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships) | |
+ return QuerySetMapper(self.instance.relationship_set) | |
+ | |
+ class Meta: | |
+ abstract = True | |
+ | |
+ | |
+class Node(InheritableTreeEntity): | |
+ accepts_subpath = False | |
+ | |
+ def render_to_response(self, request, path=None, subpath=None): | |
+ return HttpResponseServerError() | |
+ | |
+ class Meta: | |
+ unique_together = (('parent', 'slug'),) | |
+ | |
+ | |
+class MultiNode(Node): | |
+ accepts_subpath = True | |
+ | |
+ urlpatterns = [] | |
+ | |
+ def render_to_response(self, request, path=None, subpath=None): | |
+ if not subpath: | |
+ subpath = "" | |
+ subpath = "/" + subpath | |
+ from django.core.urlresolvers import resolve | |
+ view, args, kwargs = resolve(subpath, urlconf=self) | |
+ return view(request, *args, **kwargs) | |
+ | |
+ class Meta: | |
+ abstract = True | |
+ | |
+ | |
+class Redirect(Node): | |
+ STATUS_CODES = ( | |
+ (302, 'Temporary'), | |
+ (301, 'Permanent'), | |
+ ) | |
+ target = models.URLField(help_text='Must be a valid, absolute URL (i.e. http://)') | |
+ status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type') | |
+ | |
+ def render_to_response(self, request, path=None, subpath=None): | |
+ response = HttpResponseRedirect(self.target) | |
+ response.status_code = self.status_code | |
+ return response | |
+ | |
+ | |
+class File(Node): | |
+ """ For storing arbitrary files """ | |
+ mimetype = models.CharField(max_length=255) | |
+ file = models.FileField(upload_to='philo/files/%Y/%m/%d') | |
+ | |
+ def render_to_response(self, request, path=None, subpath=None): | |
+ wrapper = FileWrapper(self.file) | |
+ response = HttpResponse(wrapper, content_type=self.mimetype) | |
+ response['Content-Length'] = self.file.size | |
+ return response | |
+ | |
+# def __unicode__(self): | |
+# return self.file | |
+ | |
+ | |
+class Template(TreeModel): | |
+ name = models.CharField(max_length=255) | |
+ documentation = models.TextField(null=True, blank=True) | |
+ mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE) | |
+ code = models.TextField(verbose_name='django template code') | |
+ | |
+ @property | |
+ def origin(self): | |
+ return 'philo.models.Template: ' + self.path | |
+ | |
+ @property | |
+ def django_template(self): | |
+ return DjangoTemplate(self.code) | |
+ | |
+ @property | |
+ def containers(self): | |
+ """ | |
+ Returns a tuple where the first item is a list of names of contentlets referenced by containers, | |
+ and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers. | |
+ This will break if there is a recursive extends or includes in the template code. | |
+ Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work. | |
+ """ | |
+ def container_nodes(template): | |
+ def nodelist_container_nodes(nodelist): | |
+ nodes = [] | |
+ for node in nodelist: | |
+ try: | |
+ for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'): | |
+ if hasattr(node, nodelist_name): | |
+ nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name))) | |
+ if isinstance(node, ContainerNode): | |
+ nodes.append(node) | |
+ elif isinstance(node, ExtendsNode): | |
+ extended_template = node.get_parent(Context()) | |
+ if extended_template: | |
+ nodes.extend(container_nodes(extended_template)) | |
+ elif isinstance(node, ConstantIncludeNode): | |
+ included_template = node.template | |
+ if included_template: | |
+ nodes.extend(container_nodes(included_template)) | |
+ elif isinstance(node, IncludeNode): | |
+ included_template = get_template(node.template_name.resolve(Context())) | |
+ if included_template: | |
+ nodes.extend(container_nodes(included_template)) | |
+ except: | |
+ pass # fail for this node | |
+ return nodes | |
+ return nodelist_container_nodes(template.nodelist) | |
+ all_nodes = container_nodes(self.django_template) | |
+ contentlet_node_names = set([node.name for node in all_nodes if not node.references]) | |
+ contentreference_node_names = [] | |
+ contentreference_node_specs = [] | |
+ for node in all_nodes: | |
+ if node.references and node.name not in contentreference_node_names: | |
+ contentreference_node_specs.append((node.name, node.references)) | |
+ contentreference_node_names.append(node.name) | |
+ return contentlet_node_names, contentreference_node_specs | |
+ | |
+ def __unicode__(self): | |
+ return self.get_path(u' › ', 'name') | |
+ | |
+ @staticmethod | |
+ @fattr(is_usable=True) | |
+ def loader(template_name, template_dirs=None): # load_template_source | |
+ try: | |
+ template = Template.objects.get_with_path(template_name) | |
+ except Template.DoesNotExist: | |
+ raise TemplateDoesNotExist(template_name) | |
+ return (template.code, template.origin) | |
+ | |
+ | |
+class Page(Node): | |
+ """ | |
+ Represents a page - something which is rendered according to a template. The page will have a number of related Contentlets depending on the template selected - but these will appear only after the page has been saved with that template. | |
+ """ | |
+ template = models.ForeignKey(Template, related_name='pages') | |
+ title = models.CharField(max_length=255) | |
+ | |
+ def render_to_response(self, request, path=None, subpath=None): | |
+ return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype) | |
+ | |
+ def __unicode__(self): | |
+ return self.get_path(u' › ', 'title') | |
+ | |
+ | |
+# the following line enables the selection of a node as the root for a given django.contrib.sites Site object | |
+models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node') | |
+ | |
+ | |
+class Contentlet(models.Model): | |
+ page = models.ForeignKey(Page, related_name='contentlets') | |
+ name = models.CharField(max_length=255) | |
+ content = models.TextField() | |
+ dynamic = models.BooleanField(default=False) | |
+ | |
+ def __unicode__(self): | |
+ return self.name | |
+ | |
+ | |
+class ContentReference(models.Model): | |
+ page = models.ForeignKey(Page, related_name='contentreferences') | |
+ name = models.CharField(max_length=255) | |
+ content_type = models.ForeignKey(ContentType, verbose_name='Content type') | |
+ content_id = models.PositiveIntegerField(verbose_name='Content ID') | |
+ content = generic.GenericForeignKey('content_type', 'content_id') | |
+ | |
+ def __unicode__(self): | |
+ return self.name | |
+ | |
+ | |
+register_templatetags('philo.templatetags.containers') | |
+ | |
+ | |
+register_value_model(User) | |
+register_value_model(Group) | |
+register_value_model(Site) | |
+register_value_model(Collection) | |
+register_value_model(Template) | |
+register_value_model(Page) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment