From: Stephen Burrows Date: Wed, 11 May 2011 21:23:41 +0000 (-0400) Subject: Merge branch 'attribute_access' into release X-Git-Tag: philo-0.9~12^2~18 X-Git-Url: http://git.ithinksw.org/philo.git/commitdiff_plain/7f6fa6595b4c558d7a97ed00cdb19469db4919df?hp=9cbb247dfb4faa47617438770bb6135155fade16 Merge branch 'attribute_access' into release Conflicts: philo/models/nodes.py --- diff --git a/docs/dummy-settings.py b/docs/dummy-settings.py index e69de29..7e424ab 100644 --- a/docs/dummy-settings.py +++ b/docs/dummy-settings.py @@ -0,0 +1,6 @@ +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'db.sl3' + } +} \ No newline at end of file diff --git a/docs/exceptions.rst b/docs/exceptions.rst new file mode 100644 index 0000000..679ac77 --- /dev/null +++ b/docs/exceptions.rst @@ -0,0 +1,5 @@ +Exceptions +========== + +.. automodule:: philo.exceptions + :members: MIDDLEWARE_NOT_CONFIGURED, AncestorDoesNotExist, ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index cfc7136..36470fb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,10 @@ Contents: intro models/intro + exceptions + middleware + signals + validators Indices and tables ================== diff --git a/docs/middleware.rst b/docs/middleware.rst new file mode 100644 index 0000000..4a5c05f --- /dev/null +++ b/docs/middleware.rst @@ -0,0 +1,5 @@ +Middleware +========== + +.. automodule:: philo.middleware + :members: diff --git a/docs/models/fields.rst b/docs/models/fields.rst index 91164a3..0b3d0f9 100644 --- a/docs/models/fields.rst +++ b/docs/models/fields.rst @@ -5,7 +5,7 @@ Custom Fields :members: EntityProxyFields -================= +----------------- .. automodule:: philo.models.fields.entities :members: \ No newline at end of file diff --git a/docs/signals.rst b/docs/signals.rst new file mode 100644 index 0000000..8b3da3c --- /dev/null +++ b/docs/signals.rst @@ -0,0 +1,5 @@ +Signals +======= + +.. automodule:: philo.signals + :members: diff --git a/docs/validators.rst b/docs/validators.rst new file mode 100644 index 0000000..f91818b --- /dev/null +++ b/docs/validators.rst @@ -0,0 +1,5 @@ +Validators +========== + +.. automodule:: philo.validators + :members: diff --git a/philo/contrib/shipherd/models.py b/philo/contrib/shipherd/models.py index 3ff5c44..a09f385 100644 --- a/philo/contrib/shipherd/models.py +++ b/philo/contrib/shipherd/models.py @@ -8,7 +8,6 @@ from django.db import models from django.forms.models import model_to_dict from philo.models import TreeEntity, Node, TreeManager, Entity, TargetURLModel -from philo.validators import RedirectValidator DEFAULT_NAVIGATION_DEPTH = 3 diff --git a/philo/contrib/sobol/models.py b/philo/contrib/sobol/models.py index 2cb1651..dbf37ef 100644 --- a/philo/contrib/sobol/models.py +++ b/philo/contrib/sobol/models.py @@ -4,6 +4,7 @@ from django.conf import settings from django.conf.urls.defaults import patterns, url from django.contrib import messages from django.core.exceptions import ValidationError +from django.core.validators import URLValidator from django.db import models from django.http import HttpResponseRedirect, Http404, HttpResponse from django.utils import simplejson as json @@ -15,7 +16,6 @@ from philo.contrib.sobol.utils import HASH_REDIRECT_GET_KEY, URL_REDIRECT_GET_KE from philo.exceptions import ViewCanNotProvideSubpath from philo.models import MultiView, Page from philo.models.fields import SlugMultipleChoiceField -from philo.validators import RedirectValidator eventlet = None if getattr(settings, 'SOBOL_USE_EVENTLET', False): @@ -83,7 +83,7 @@ class Search(models.Model): class ResultURL(models.Model): search = models.ForeignKey(Search, related_name='result_urls') - url = models.TextField(validators=[RedirectValidator()]) + url = models.TextField(validators=[URLValidator()]) def __unicode__(self): return self.url diff --git a/philo/contrib/sobol/templates/admin/sobol/search/grappelli_results.html b/philo/contrib/sobol/templates/admin/sobol/search/grappelli_results.html index 45135ff..f01eb88 100644 --- a/philo/contrib/sobol/templates/admin/sobol/search/grappelli_results.html +++ b/philo/contrib/sobol/templates/admin/sobol/search/grappelli_results.html @@ -28,9 +28,8 @@ {% block content %}
{% for search in queryset %} - {% if not forloop.first and not forloop.last %}

{{ search.string }}

{% endif %}
-

{% blocktrans %}Results{% endblocktrans %}

{% comment %}For the favored results, add a class?{% endcomment %} +

{{ search_string }}

diff --git a/philo/contrib/sobol/templates/admin/sobol/search/results.html b/philo/contrib/sobol/templates/admin/sobol/search/results.html index 44d4e7c..24442c7 100644 --- a/philo/contrib/sobol/templates/admin/sobol/search/results.html +++ b/philo/contrib/sobol/templates/admin/sobol/search/results.html @@ -23,9 +23,8 @@ {% block content %} {% for search in queryset %} - {% if not forloop.first and not forloop.last %}

{{ search.string }}

{% endif %}
-

{% blocktrans %}Results{% endblocktrans %}

{% comment %}For the favored results, add a class?{% endcomment %} +

{{ search.string }}

diff --git a/philo/exceptions.py b/philo/exceptions.py index f53083d..9a8908e 100644 --- a/philo/exceptions.py +++ b/philo/exceptions.py @@ -1,19 +1,20 @@ from django.core.exceptions import ImproperlyConfigured +#: Raised if ``request.node`` is required but not present. For example, this can be raised by :func:`philo.views.node_view`. :data:`MIDDLEWARE_NOT_CONFIGURED` is an instance of :exc:`django.core.exceptions.ImproperlyConfigured`. MIDDLEWARE_NOT_CONFIGURED = ImproperlyConfigured("""Philo requires the RequestNode middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'philo.middleware.RequestNodeMiddleware'.""") class ViewDoesNotProvideSubpaths(Exception): - """ Raised by View.reverse when the View does not provide subpaths (the default). """ + """Raised by :meth:`View.reverse` when the View does not provide subpaths (the default).""" silent_variable_failure = True class ViewCanNotProvideSubpath(Exception): - """ Raised by View.reverse when the View can not provide a subpath for the supplied arguments. """ + """Raised by :meth:`View.reverse` when the :class:`View` can not provide a subpath for the supplied arguments.""" silent_variable_failure = True class AncestorDoesNotExist(Exception): - """ Raised by get_path if the root model is not an ancestor of the current model """ + """Raised by :meth:`TreeModel.get_path` if the root instance is not an ancestor of the current instance.""" pass \ No newline at end of file diff --git a/philo/middleware.py b/philo/middleware.py index 4082d4b..b90067a 100644 --- a/philo/middleware.py +++ b/philo/middleware.py @@ -44,7 +44,7 @@ class LazyNode(object): class RequestNodeMiddleware(object): - """Middleware to process the request's path and attach the closest ancestor node.""" + """Adds a ``node`` attribute, representing the currently-viewed node, to every incoming :class:`HttpRequest` object. This is required by :func:`philo.views.node_view`.""" def process_request(self, request): request.__class__.node = LazyNode() diff --git a/philo/models/base.py b/philo/models/base.py index 0ab9b70..7d78383 100644 --- a/philo/models/base.py +++ b/philo/models/base.py @@ -332,7 +332,7 @@ class TreeManager(models.Manager): def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'): """ - If ``absolute_result`` is ``True``, returns the object at ``path`` (starting at ``root``) or raises a :class:`DoesNotExist` exception. Otherwise, returns a tuple containing the deepest object found along ``path`` (or ``root`` if no deeper object is found) and the remainder of the path after that object as a string (or None if there is no remaining path). + If ``absolute_result`` is ``True``, returns the object at ``path`` (starting at ``root``) or raises an :class:`~django.core.exceptions.ObjectDoesNotExist` exception. Otherwise, returns a tuple containing the deepest object found along ``path`` (or ``root`` if no deeper object is found) and the remainder of the path after that object as a string (or None if there is no remaining path). .. note:: If you are looking for something with an exact path, it is faster to use absolute_result=True, unless the path depth is over ~40, in which case the high cost of the absolute query may make a binary search (i.e. non-absolute) faster. @@ -343,7 +343,8 @@ class TreeManager(models.Manager): :param absolute_result: Whether to return an absolute result or do a binary search :param pathsep: The path separator used in ``path`` :param field: The field on the model which should be queried for ``path`` segment matching. - :returns: An instance if absolute_result is True or (instance, remaining_path) otherwise. + :returns: An instance if ``absolute_result`` is ``True`` or an (instance, remaining_path) tuple otherwise. + :raises django.core.exceptions.ObjectDoesNotExist: if no object can be found matching the input parameters. """ diff --git a/philo/models/nodes.py b/philo/models/nodes.py index 44a4d31..a225416 100644 --- a/philo/models/nodes.py +++ b/philo/models/nodes.py @@ -15,7 +15,6 @@ from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED, ViewCanNotProvideSubpath from philo.models.base import TreeEntity, Entity, register_value_model from philo.models.fields import JSONField from philo.utils import ContentTypeSubclassLimiter, LazyPassthroughAttributeMapper -from philo.validators import RedirectValidator from philo.signals import view_about_to_render, view_finished_rendering @@ -133,17 +132,14 @@ class View(Entity): If ``obj`` is provided, :meth:`get_reverse_params` will be called and the results will be combined with any ``view_name``, ``args``, and ``kwargs`` that may have been passed in. - This method will raise the following exceptions: - - - :class:`~philo.exceptions.ViewDoesNotProvideSubpaths` if :attr:`accepts_subpath` is False. - - :class:`~philo.exceptions.ViewCanNotProvideSubpath` if a reversal is not possible. - :param view_name: The name of the view to be reversed. :param args: Extra args for reversing the view. :param kwargs: A dictionary of arguments for reversing the view. :param node: The node whose subpath this is. :param obj: An object to be passed to :meth:`get_reverse_params` to generate a view_name, args, and kwargs for reversal. :returns: A subpath beyond the node that reverses the view, or an absolute url that reverses the view if a node was passed in. + :except philo.exceptions.ViewDoesNotProvideSubpaths: if :attr:`accepts_subpath` is False + :except philo.exceptions.ViewCanNotProvideSubpath: if a reversal is not possible. """ if not self.accepts_subpath: @@ -294,8 +290,8 @@ class TargetURLModel(models.Model): """An abstract parent class for models which deal in targeting a url.""" #: An optional :class:`ForeignKey` to a :class:`Node`. If provided, that node will be used as the basis for the redirect. target_node = models.ForeignKey(Node, blank=True, null=True, related_name="%(app_label)s_%(class)s_related") - #: A :class:`CharField` which may contain an absolute or relative URL. This will be validated with :class:`philo.validators.RedirectValidator`. - 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.") + #: A :class:`CharField` which may contain an absolute or relative URL, or the name of a node's subpath. + url_or_subpath = models.CharField(max_length=200, blank=True, help_text="Point to this url or, if a node is defined and accepts subpaths, this subpath of the node.") #: A :class:`~philo.models.fields.JSONField` instance. If the value of :attr:`reversing_parameters` is not None, the :attr:`url_or_subpath` will be treated as the name of a view to be reversed. The value of :attr:`reversing_parameters` will be passed into the reversal as args if it is a list or as kwargs if it is a dictionary. Otherwise it will be ignored. 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.") diff --git a/philo/signals.py b/philo/signals.py index 3653c54..558c6fe 100644 --- a/philo/signals.py +++ b/philo/signals.py @@ -1,8 +1,60 @@ from django.dispatch import Signal +#: Sent whenever an Entity subclass has been "prepared" -- that is, after the processing necessary to make :mod:`EntityProxyFields ` work has been completed. This will fire after :obj:`django.db.models.signals.class_prepared`. +#: +#: Arguments that are sent with this signal: +#: +#: ``sender`` +#: The model class. entity_class_prepared = Signal(providing_args=['class']) + +#: Sent when a :class:`~philo.models.nodes.View` instance is about to render. This allows you, for example, to modify the ``extra_context`` dictionary used in rendering. +#: +#: Arguments that are sent with this signal: +#: +#: ``sender`` +#: The :class:`~philo.models.nodes.View` instance +#: +#: ``request`` +#: The :class:`HttpRequest` instance which the :class:`~philo.models.nodes.View` is rendering in response to. +#: +#: ``extra_context`` +#: A dictionary which will be passed into :meth:`~philo.models.nodes.View.actually_render_to_response`. view_about_to_render = Signal(providing_args=['request', 'extra_context']) + +#: Sent when a view instance has finished rendering. +#: +#: Arguments that are sent with this signal: +#: +#: ``sender`` +#: The :class:`~philo.models.nodes.View` instance +#: +#: ``response`` +#: The :class:`HttpResponse` instance which :class:`~philo.models.nodes.View` view has rendered to. view_finished_rendering = Signal(providing_args=['response']) + +#: Sent when a :class:`~philo.models.pages.Page` instance is about to render as a string. If the :class:`~philo.models.pages.Page` is rendering as a response, this signal is sent after :obj:`view_about_to_render` and serves a similar function. However, there are situations where a :class:`~philo.models.pages.Page` may be rendered as a string without being rendered as a response afterwards. +#: +#: Arguments that are sent with this signal: +#: +#: ``sender`` +#: The :class:`~philo.models.pages.Page` instance +#: +#: ``request`` +#: The :class:`HttpRequest` instance which the :class:`~philo.models.pages.Page` is rendering in response to (if any). +#: +#: ``extra_context`` +#: A dictionary which will be passed into the :class:`Template` context. page_about_to_render_to_string = Signal(providing_args=['request', 'extra_context']) + +#: Sent when a :class:`~philo.models.pages.Page` instance has just finished rendering as a string. If the :class:`~philo.models.pages.Page` is rendering as a response, this signal is sent before :obj:`view_finished_rendering` and serves a similar function. However, there are situations where a :class:`~philo.models.pages.Page` may be rendered as a string without being rendered as a response afterwards. +#: +#: Arguments that are sent with this signal: +#: +#: ``sender`` +#: The :class:`~philo.models.pages.Page` instance +#: +#: ``string`` +#: The string which the :class:`~philo.models.pages.Page` has rendered to. page_finished_rendering_to_string = Signal(providing_args=['string']) \ No newline at end of file diff --git a/philo/validators.py b/philo/validators.py index 144699e..349dd56 100644 --- a/philo/validators.py +++ b/philo/validators.py @@ -1,7 +1,6 @@ import re from django.core.exceptions import ValidationError -from django.core.validators import RegexValidator from django.template import Template, Parser, Lexer, TOKEN_BLOCK, TOKEN_VAR, TemplateSyntaxError from django.utils import simplejson as json from django.utils.html import escape, mark_safe @@ -10,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _ from philo.utils import LOADED_TEMPLATE_ATTR +#: Tags which are considered insecure and are therefore always disallowed by secure :class:`TemplateValidator` instances. INSECURE_TAGS = ( 'load', 'extends', @@ -18,34 +18,8 @@ INSECURE_TAGS = ( ) -class RedirectValidator(RegexValidator): - """Based loosely on the URLValidator, but no option to verify_exists""" - regex = re.compile( - r'^(?:https?://' # http:// or https:// - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain... - r'localhost|' #localhost... - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip - r'(?::\d+)?' # optional port - r'(?:/?|[/?#]?\S+)|' - r'[^?#\s]\S*)$', - re.IGNORECASE) - message = _(u'Enter a valid absolute or relative redirect target') - - -class URLLinkValidator(RegexValidator): - """Based loosely on the URLValidator, but no option to verify_exists""" - regex = re.compile( - r'^(?:https?://' # http:// or https:// - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain... - r'localhost|' #localhost... - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip - r'(?::\d+)?' # optional port - r'|)' # also allow internal links - r'(?:/?|[/?#]?\S+)$', re.IGNORECASE) - message = _(u'Enter a valid absolute or relative redirect target') - - def json_validator(value): + """Validates whether ``value`` is a valid json string.""" try: json.loads(value) except Exception, e: @@ -130,6 +104,14 @@ def linebreak_iter(template_source): class TemplateValidator(object): + """ + Validates whether a string represents valid Django template code. + + :param allow: ``None`` or an iterable of tag names which are explicitly allowed. If provided, tags whose names are not in the iterable will cause a ValidationError to be raised if they are used in the template code. + :param disallow: ``None`` or an iterable of tag names which are explicitly allowed. If provided, tags whose names are in the iterable will cause a ValidationError to be raised if they are used in the template code. If a tag's name is in ``allow`` and ``disallow``, it will be disallowed. + :param secure: If the validator is set to secure, it will automatically disallow the tag names listed in :const:`INSECURE_TAGS`. Defaults to ``True``. + + """ def __init__(self, allow=None, disallow=None, secure=True): self.allow = allow self.disallow = disallow