diff --git a/django/contrib/databrowse/__init__.py b/django/contrib/databrowse/__init__.py new file mode 100644 index 0000000000..e2f48ac822 --- /dev/null +++ b/django/contrib/databrowse/__init__.py @@ -0,0 +1 @@ +from django.contrib.databrowse.sites import DatabrowsePlugin, ModelDatabrowse, DatabrowseSite, site diff --git a/django/contrib/databrowse/datastructures.py b/django/contrib/databrowse/datastructures.py new file mode 100644 index 0000000000..3c509a2830 --- /dev/null +++ b/django/contrib/databrowse/datastructures.py @@ -0,0 +1,188 @@ +""" +These classes are light wrappers around Django's database API that provide +convenience functionality and permalink functions for the databrowse app. +""" + +from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE +from django.db import models +from django.utils import dateformat +from django.utils.text import capfirst +from django.utils.translation import get_date_formats + +class EasyModel(object): + def __init__(self, site, model): + self.site = site + self.model = model + self.model_list = site.registry.keys() + self.verbose_name = model._meta.verbose_name + self.verbose_name_plural = model._meta.verbose_name_plural + + def __repr__(self): + return '' % self.model._meta.object_name + + def model_databrowse(self): + "Returns the ModelDatabrowse class for this model." + return self.site.registry[self.model] + + def url(self): + return '%s%s/%s/' % (self.site.root_url, self.model._meta.app_label, self.model._meta.module_name) + + def objects(self, **kwargs): + for obj in self.model._default_manager.filter(**kwargs): + yield EasyInstance(self, obj) + + def object_by_pk(self, pk): + return EasyInstance(self, self.model._default_manager.get(pk=pk)) + + def sample_objects(self): + for obj in self.model._default_manager.all()[:3]: + yield EasyInstance(self, obj) + + def field(self, name): + try: + f = self.model._meta.get_field(name) + except models.FieldDoesNotExist: + return None + return EasyField(self, f) + + def fields(self): + return [EasyField(self, f) for f in (self.model._meta.fields + self.model._meta.many_to_many)] + +class EasyField(object): + def __init__(self, easy_model, field): + self.model, self.field = easy_model, field + + def __repr__(self): + return '' % (self.model.model._meta.object_name, self.field.name) + + def choices(self): + for value, label in self.field.choices: + yield EasyChoice(self.model, self, value, label) + + def url(self): + if self.field.choices: + return '%s%s/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.name) + elif self.field.rel: + return '%s%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name) + +class EasyChoice(object): + def __init__(self, easy_model, field, value, label): + self.model, self.field = easy_model, field + self.value, self.label = value, label + + def __repr__(self): + return '' % (self.model.model._meta.object_name, self.field.name) + + def url(self): + return '%s%s/%s/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.field.name, self.value) + +class EasyInstance(object): + def __init__(self, easy_model, instance): + self.model, self.instance = easy_model, instance + + def __repr__(self): + return '' % (self.model.model._meta.object_name, self.instance._get_pk_val()) + + def __str__(self): + val = str(self.instance) + if len(val) > 30: + return val[:30] + '...' + return val + + def pk(self): + return self.instance._get_pk_val() + + def url(self): + return '%s%s/%s/objects/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.pk()) + + def fields(self): + """ + Generator that yields EasyInstanceFields for each field in this + EasyInstance's model. + """ + for f in self.model.model._meta.fields + self.model.model._meta.many_to_many: + yield EasyInstanceField(self.model, self, f) + + def related_objects(self): + """ + Generator that yields dictionaries of all models that have this + EasyInstance's model as a ForeignKey or ManyToManyField, along with + lists of related objects. + """ + for rel_object in self.model.model._meta.get_all_related_objects() + self.model.model._meta.get_all_related_many_to_many_objects(): + if rel_object.model not in self.model.model_list: + continue # Skip models that aren't in the model_list + em = EasyModel(self.model.site, rel_object.model) + yield { + 'model': em, + 'related_field': rel_object.field.verbose_name, + 'object_list': [EasyInstance(em, i) for i in getattr(self.instance, rel_object.get_accessor_name()).all()], + } + +class EasyInstanceField(object): + def __init__(self, easy_model, instance, field): + self.model, self.field, self.instance = easy_model, field, instance + self.raw_value = getattr(instance.instance, field.name) + + def __repr__(self): + return '' % (self.model.model._meta.object_name, self.field.name) + + def values(self): + """ + Returns a list of values for this field for this instance. It's a list + so we can accomodate many-to-many fields. + """ + if self.field.rel: + if isinstance(self.field.rel, models.ManyToOneRel): + objs = getattr(self.instance.instance, self.field.name) + elif isinstance(self.field.rel, models.ManyToManyRel): # ManyToManyRel + return list(getattr(self.instance.instance, self.field.name).all()) + elif self.field.choices: + objs = dict(self.field.choices).get(self.raw_value, EMPTY_CHANGELIST_VALUE) + elif isinstance(self.field, models.DateField) or isinstance(self.field, models.TimeField): + if self.raw_value: + date_format, datetime_format, time_format = get_date_formats() + if isinstance(self.field, models.DateTimeField): + objs = capfirst(dateformat.format(self.raw_value, datetime_format)) + elif isinstance(self.field, models.TimeField): + objs = capfirst(dateformat.time_format(self.raw_value, time_format)) + else: + objs = capfirst(dateformat.format(self.raw_value, date_format)) + else: + objs = EMPTY_CHANGELIST_VALUE + elif isinstance(self.field, models.BooleanField) or isinstance(self.field, models.NullBooleanField): + objs = {True: 'Yes', False: 'No', None: 'Unknown'}[self.raw_value] + else: + objs = self.raw_value + return [objs] + + def urls(self): + "Returns a list of (value, URL) tuples." + # First, check the urls() method for each plugin. + plugin_urls = [] + for plugin_name, plugin in self.model.model_databrowse().plugins.items(): + urls = plugin.urls(plugin_name, self) + if urls is not None: + #plugin_urls.append(urls) + values = self.values() + return zip(self.values(), urls) + if self.field.rel: + m = EasyModel(self.model.site, self.field.rel.to) + if self.field.rel.to in self.model.model_list: + lst = [] + for value in self.values(): + url = '%s%s/%s/objects/%s/' % (self.model.site.root_url, m.model._meta.app_label, m.model._meta.module_name, value._get_pk_val()) + lst.append((str(value), url)) + else: + lst = [(value, None) for value in self.values()] + elif self.field.choices: + lst = [] + for value in self.values(): + url = '%s%s/%s/fields/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.name, self.raw_value) + lst.append((value, url)) + elif isinstance(self.field, models.URLField): + val = self.values()[0] + lst = [(val, val)] + else: + lst = [(self.values()[0], None)] + return lst diff --git a/django/contrib/databrowse/plugins/__init__.py b/django/contrib/databrowse/plugins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/databrowse/plugins/calendars.py b/django/contrib/databrowse/plugins/calendars.py new file mode 100644 index 0000000000..7977524cdb --- /dev/null +++ b/django/contrib/databrowse/plugins/calendars.py @@ -0,0 +1,84 @@ +from django import http +from django.db import models +from django.contrib.databrowse.datastructures import EasyModel +from django.contrib.databrowse.sites import DatabrowsePlugin +from django.shortcuts import render_to_response +from django.utils.text import capfirst +from django.utils.translation import get_date_formats +from django.views.generic import date_based +import datetime +import time + +class CalendarPlugin(DatabrowsePlugin): + def __init__(self, field_names=None): + self.field_names = field_names + + def field_dict(self, model): + """ + Helper function that returns a dictionary of all DateFields or + DateTimeFields in the given model. If self.field_names is set, it takes + take that into account when building the dictionary. + """ + if self.field_names is None: + return dict([(f.name, f) for f in model._meta.fields if isinstance(f, models.DateField)]) + else: + return dict([(f.name, f) for f in model._meta.fields if isinstance(f, models.DateField) and f.name in self.field_names]) + + def model_index_html(self, request, model, site): + fields = self.field_dict(model) + if not fields: + return '' + return '

View calendar by: %s

' % \ + ', '.join(['%s' % (f.name, capfirst(f.verbose_name)) for f in fields.values()]) + + def urls(self, plugin_name, easy_instance_field): + if isinstance(easy_instance_field.field, models.DateField): + return ['%s%s/%s/%s/%s/%s/' % (easy_instance_field.model.url(), + plugin_name, easy_instance_field.field.name, + easy_instance_field.raw_value.year, + easy_instance_field.raw_value.strftime('%b').lower(), + easy_instance_field.raw_value.day)] + + def model_view(self, request, model_databrowse, url): + self.model, self.site = model_databrowse.model, model_databrowse.site + self.fields = self.field_dict(self.model) + + # If the model has no DateFields, there's no point in going further. + if not self.fields: + raise http.Http404('The requested model has no calendars.') + + if url is None: + return self.homepage_view(request) + url_bits = url.split('/') + if self.fields.has_key(url_bits[0]): + return self.calendar_view(request, self.fields[url_bits[0]], *url_bits[1:]) + + raise http.Http404('The requested page does not exist.') + + def homepage_view(self, request): + easy_model = EasyModel(self.site, self.model) + field_list = self.fields.values() + field_list.sort(lambda x, y: cmp(x.verbose_name, y.verbose_name)) + return render_to_response('databrowse/calendar_homepage.html', {'root_url': self.site.root_url, 'model': easy_model, 'field_list': field_list}) + + def calendar_view(self, request, field, year=None, month=None, day=None): + easy_model = EasyModel(self.site, self.model) + extra_context = {'root_url': self.site.root_url, 'model': easy_model, 'field': field} + if day is not None: + # TODO: The objects in this template should be EasyInstances + return date_based.archive_day(request, year, month, day, self.model.objects.all(), field.name, + template_name='databrowse/calendar_day.html', allow_empty=False, allow_future=True, + extra_context=extra_context) + elif month is not None: + return date_based.archive_month(request, year, month, self.model.objects.all(), field.name, + template_name='databrowse/calendar_month.html', allow_empty=False, allow_future=True, + extra_context=extra_context) + elif year is not None: + return date_based.archive_year(request, year, self.model.objects.all(), field.name, + template_name='databrowse/calendar_year.html', allow_empty=False, allow_future=True, + extra_context=extra_context) + else: + return date_based.archive_index(request, self.model.objects.all(), field.name, + template_name='databrowse/calendar_main.html', allow_empty=True, allow_future=True, + extra_context=extra_context) + assert False, ('%s, %s, %s, %s' % (field, year, month, day)) diff --git a/django/contrib/databrowse/plugins/fieldchoices.py b/django/contrib/databrowse/plugins/fieldchoices.py new file mode 100644 index 0000000000..49d17ff148 --- /dev/null +++ b/django/contrib/databrowse/plugins/fieldchoices.py @@ -0,0 +1,72 @@ +from django import http +from django.db import models +from django.contrib.databrowse.datastructures import EasyModel +from django.contrib.databrowse.sites import DatabrowsePlugin +from django.shortcuts import render_to_response +from django.utils.text import capfirst +from django.views.generic import date_based +import datetime +import time + +class FieldChoicePlugin(DatabrowsePlugin): + def __init__(self, field_filter=None): + # If field_filter is given, it should be a callable that takes a + # Django database Field instance and returns True if that field should + # be included. If field_filter is None, that all fields will be used. + self.field_filter = field_filter + + def field_dict(self, model): + """ + Helper function that returns a dictionary of all fields in the given + model. If self.field_filter is set, it only includes the fields that + match the filter. + """ + if self.field_filter: + return dict([(f.name, f) for f in model._meta.fields if self.field_filter(f)]) + else: + return dict([(f.name, f) for f in model._meta.fields if not f.rel and not f.primary_key and not f.unique and not isinstance(f, (models.AutoField, models.TextField))]) + + def model_index_html(self, request, model, site): + fields = self.field_dict(model) + if not fields: + return '' + return '

View by: %s

' % \ + ', '.join(['%s' % (f.name, capfirst(f.verbose_name)) for f in fields.values()]) + + def urls(self, plugin_name, easy_instance_field): + if easy_instance_field.field in self.field_dict(easy_instance_field.model.model).values(): + return ['%s%s/%s/%s/' % (easy_instance_field.model.url(), + plugin_name, easy_instance_field.field.name, + easy_instance_field.raw_value)] + + def model_view(self, request, model_databrowse, url): + self.model, self.site = model_databrowse.model, model_databrowse.site + self.fields = self.field_dict(self.model) + + # If the model has no fields with choices, there's no point in going + # further. + if not self.fields: + raise http.Http404('The requested model has no fields.') + + if url is None: + return self.homepage_view(request) + url_bits = url.split('/', 1) + if self.fields.has_key(url_bits[0]): + return self.field_view(request, self.fields[url_bits[0]], *url_bits[1:]) + + raise http.Http404('The requested page does not exist.') + + def homepage_view(self, request): + easy_model = EasyModel(self.site, self.model) + field_list = self.fields.values() + field_list.sort(lambda x, y: cmp(x.verbose_name, y.verbose_name)) + return render_to_response('databrowse/fieldchoice_homepage.html', {'root_url': self.site.root_url, 'model': easy_model, 'field_list': field_list}) + + def field_view(self, request, field, value=None): + easy_model = EasyModel(self.site, self.model) + easy_field = easy_model.field(field.name) + if value is not None: + obj_list = easy_model.objects(**{field.name: value}) + return render_to_response('databrowse/fieldchoice_detail.html', {'root_url': self.site.root_url, 'model': easy_model, 'field': easy_field, 'value': value, 'object_list': obj_list}) + obj_list = [v[field.name] for v in self.model._default_manager.distinct().order_by(field.name).values(field.name)] + return render_to_response('databrowse/fieldchoice_list.html', {'root_url': self.site.root_url, 'model': easy_model, 'field': easy_field, 'object_list': obj_list}) diff --git a/django/contrib/databrowse/plugins/objects.py b/django/contrib/databrowse/plugins/objects.py new file mode 100644 index 0000000000..7326566655 --- /dev/null +++ b/django/contrib/databrowse/plugins/objects.py @@ -0,0 +1,14 @@ +from django import http +from django.contrib.databrowse.datastructures import EasyModel +from django.contrib.databrowse.sites import DatabrowsePlugin +from django.shortcuts import render_to_response +import urlparse + +class ObjectDetailPlugin(DatabrowsePlugin): + def model_view(self, request, model_databrowse, url): + # If the object ID wasn't provided, redirect to the model page, which is one level up. + if url is None: + return http.HttpResponseRedirect(urlparse.urljoin(request.path, '../')) + easy_model = EasyModel(model_databrowse.site, model_databrowse.model) + obj = easy_model.object_by_pk(url) + return render_to_response('databrowse/object_detail.html', {'object': obj, 'root_url': model_databrowse.site.root_url}) diff --git a/django/contrib/databrowse/sites.py b/django/contrib/databrowse/sites.py new file mode 100644 index 0000000000..8521343e50 --- /dev/null +++ b/django/contrib/databrowse/sites.py @@ -0,0 +1,148 @@ +from django import http +from django.db import models +from django.contrib.databrowse.datastructures import EasyModel, EasyChoice +from django.shortcuts import render_to_response + +class AlreadyRegistered(Exception): + pass + +class NotRegistered(Exception): + pass + +class DatabrowsePlugin(object): + def urls(self, plugin_name, easy_instance_field): + """ + Given an EasyInstanceField object, returns a list of URLs for this + plugin's views of this object. These URLs should be absolute. + + Returns None if the EasyInstanceField object doesn't get a + list of plugin-specific URLs. + """ + return None + + def model_index_html(self, request, model, site): + """ + Returns a snippet of HTML to include on the model index page. + """ + return '' + + def model_view(self, request, model_databrowse, url): + """ + Handles main URL routing for a plugin's model-specific pages. + """ + raise NotImplementedError + +class ModelDatabrowse(object): + plugins = {} + + def __init__(self, model, site): + self.model = model + self.site = site + + def root(self, request, url): + """ + Handles main URL routing for the databrowse app. + + `url` is the remainder of the URL -- e.g. 'objects/3'. + """ + # Delegate to the appropriate method, based on the URL. + if url is None: + return self.main_view(request) + try: + plugin_name, rest_of_url = url.split('/', 1) + except ValueError: # need more than 1 value to unpack + plugin_name, rest_of_url = url, None + try: + plugin = self.plugins[plugin_name] + except KeyError: + raise http.Http404('A plugin with the requested name does not exist.') + return plugin.model_view(request, self, rest_of_url) + + def main_view(self, request): + easy_model = EasyModel(self.site, self.model) + html_snippets = '\n'.join([p.model_index_html(request, self.model, self.site) for p in self.plugins.values()]) + return render_to_response('databrowse/model_detail.html', { + 'model': easy_model, + 'root_url': self.site.root_url, + 'plugin_html': html_snippets, + }) + +class DatabrowseSite(object): + def __init__(self): + self.registry = {} # model_class -> databrowse_class + self.root_url = None + + def register(self, model_or_iterable, databrowse_class=None, **options): + """ + Registers the given model(s) with the given databrowse site. + + The model(s) should be Model classes, not instances. + + If a databrowse class isn't given, it will use DefaultModelDatabrowse + (the default databrowse options). + + If a model is already registered, this will raise AlreadyRegistered. + """ + databrowse_class = databrowse_class or DefaultModelDatabrowse + if issubclass(model_or_iterable, models.Model): + model_or_iterable = [model_or_iterable] + for model in model_or_iterable: + if model in self.registry: + raise AlreadyRegistered('The model %s is already registered' % model.__class__.__name__) + self.registry[model] = databrowse_class + + def unregister(self, model_or_iterable): + """ + Unregisters the given model(s). + + If a model isn't already registered, this will raise NotRegistered. + """ + if issubclass(model_or_iterable, models.Model): + model_or_iterable = [model_or_iterable] + for model in model_or_iterable: + if model not in self.registry: + raise NotRegistered('The model %s is not registered' % model.__class__.__name__) + del self.registry[model] + + def root(self, request, url): + """ + Handles main URL routing for the databrowse app. + + `url` is the remainder of the URL -- e.g. 'comments/comment/'. + """ + self.root_url = request.path[:len(request.path) - len(url)] + url = url.rstrip('/') # Trim trailing slash, if it exists. + + if url == '': + return self.index(request) + elif '/' in url: + return self.model_page(request, *url.split('/', 2)) + + raise http.Http404('The requested databrowse page does not exist.') + + def index(self, request): + m_list = [EasyModel(self, m) for m in self.registry.keys()] + return render_to_response('databrowse/homepage.html', {'model_list': m_list, 'root_url': self.root_url}) + + def model_page(self, request, app_label, model_name, rest_of_url=None): + """ + Handles the model-specific functionality of the databrowse site, delegating + to the appropriate ModelDatabrowse class. + """ + model = models.get_model(app_label, model_name) + if model is None: + raise http.Http404("App %r, model %r, not found." % (app_label, model_name)) + try: + databrowse_class = self.registry[model] + except KeyError: + raise http.Http404("This model exists but has not been registered with databrowse.") + return databrowse_class(model, self).root(request, rest_of_url) + +site = DatabrowseSite() + +from django.contrib.databrowse.plugins.calendars import CalendarPlugin +from django.contrib.databrowse.plugins.objects import ObjectDetailPlugin +from django.contrib.databrowse.plugins.fieldchoices import FieldChoicePlugin + +class DefaultModelDatabrowse(ModelDatabrowse): + plugins = {'objects': ObjectDetailPlugin(), 'calendars': CalendarPlugin(), 'fields': FieldChoicePlugin()} diff --git a/django/contrib/databrowse/templates/databrowse/base.html b/django/contrib/databrowse/templates/databrowse/base.html new file mode 100644 index 0000000000..30ba5bb2fc --- /dev/null +++ b/django/contrib/databrowse/templates/databrowse/base.html @@ -0,0 +1,58 @@ + + + +{% block title %}{% endblock %} + + + + +
+{% block content %}{% endblock %} +
+ + diff --git a/django/contrib/databrowse/templates/databrowse/calendar_day.html b/django/contrib/databrowse/templates/databrowse/calendar_day.html new file mode 100644 index 0000000000..d45fb3e768 --- /dev/null +++ b/django/contrib/databrowse/templates/databrowse/calendar_day.html @@ -0,0 +1,17 @@ +{% extends "databrowse/base.html" %} + +{% block title %}{{ model.verbose_name_plural|capfirst }} with {{ field.verbose_name }} {{ day|date:"F j, Y" }}{% endblock %} + +{% block content %} + + + +

{{ model.verbose_name_plural|capfirst }} with {{ field.verbose_name }} on {{ day|date:"F j, Y" }}

+ +
    +{% for object in object_list %} +
  • {{ object }}
  • +{% endfor %} +
+ +{% endblock %} diff --git a/django/contrib/databrowse/templates/databrowse/calendar_homepage.html b/django/contrib/databrowse/templates/databrowse/calendar_homepage.html new file mode 100644 index 0000000000..5bbc42e353 --- /dev/null +++ b/django/contrib/databrowse/templates/databrowse/calendar_homepage.html @@ -0,0 +1,17 @@ +{% extends "databrowse/base.html" %} + +{% block title %}Calendars{% endblock %} + +{% block content %} + + + +

Calendars

+ + + +{% endblock %} diff --git a/django/contrib/databrowse/templates/databrowse/calendar_main.html b/django/contrib/databrowse/templates/databrowse/calendar_main.html new file mode 100644 index 0000000000..7f9ba03bbe --- /dev/null +++ b/django/contrib/databrowse/templates/databrowse/calendar_main.html @@ -0,0 +1,17 @@ +{% extends "databrowse/base.html" %} + +{% block title %}{{ field.verbose_name|capfirst }} calendar{% endblock %} + +{% block content %} + + + +

{{ model.verbose_name_plural|capfirst }} by {{ field.verbose_name }}

+ + + +{% endblock %} diff --git a/django/contrib/databrowse/templates/databrowse/calendar_month.html b/django/contrib/databrowse/templates/databrowse/calendar_month.html new file mode 100644 index 0000000000..51a25967f8 --- /dev/null +++ b/django/contrib/databrowse/templates/databrowse/calendar_month.html @@ -0,0 +1,17 @@ +{% extends "databrowse/base.html" %} + +{% block title %}{{ model.verbose_name_plural|capfirst }} with {{ field.verbose_name }} in {{ month|date:"F Y" }}{% endblock %} + +{% block content %} + + + +

{{ model.verbose_name_plural|capfirst }} with {{ field.verbose_name }} in {{ month|date:"F Y" }}

+ +
    +{% for object in object_list %} +
  • {{ object }}
  • +{% endfor %} +
+ +{% endblock %} diff --git a/django/contrib/databrowse/templates/databrowse/calendar_year.html b/django/contrib/databrowse/templates/databrowse/calendar_year.html new file mode 100644 index 0000000000..676ae88e27 --- /dev/null +++ b/django/contrib/databrowse/templates/databrowse/calendar_year.html @@ -0,0 +1,17 @@ +{% extends "databrowse/base.html" %} + +{% block title %}{{ model.verbose_name_plural|capfirst }} with {{ field.verbose_name }} in {{ year }}{% endblock %} + +{% block content %} + + + +

{{ model.verbose_name_plural|capfirst }} with {{ field.verbose_name }} in {{ year }}

+ + + +{% endblock %} diff --git a/django/contrib/databrowse/templates/databrowse/choice_detail.html b/django/contrib/databrowse/templates/databrowse/choice_detail.html new file mode 100644 index 0000000000..977a4a482f --- /dev/null +++ b/django/contrib/databrowse/templates/databrowse/choice_detail.html @@ -0,0 +1,17 @@ +{% extends "databrowse/base.html" %} + +{% block title %}{{ model.verbose_name_plural|capfirst }} by {{ field.field.verbose_name }}: {{ value|escape }}{% endblock %} + +{% block content %} + + + +

{{ model.verbose_name_plural|capfirst }} by {{ field.field.verbose_name }}: {{ value|escape }}

+ +
    +{% for object in object_list %} +
  • {{ object }}
  • +{% endfor %} +
+ +{% endblock %} diff --git a/django/contrib/databrowse/templates/databrowse/choice_list.html b/django/contrib/databrowse/templates/databrowse/choice_list.html new file mode 100644 index 0000000000..b15531a0fd --- /dev/null +++ b/django/contrib/databrowse/templates/databrowse/choice_list.html @@ -0,0 +1,17 @@ +{% extends "databrowse/base.html" %} + +{% block title %}{{ model.verbose_name_plural|capfirst }} by {{ field.field.verbose_name }}{% endblock %} + +{% block content %} + + + +

{{ model.verbose_name_plural|capfirst }} by {{ field.field.verbose_name }}

+ + + +{% endblock %} diff --git a/django/contrib/databrowse/templates/databrowse/fieldchoice_detail.html b/django/contrib/databrowse/templates/databrowse/fieldchoice_detail.html new file mode 100644 index 0000000000..a620ec931c --- /dev/null +++ b/django/contrib/databrowse/templates/databrowse/fieldchoice_detail.html @@ -0,0 +1,17 @@ +{% extends "databrowse/base.html" %} + +{% block title %}{{ model.verbose_name_plural|capfirst|escape }} with {{ field.field.verbose_name|escape }} {{ value|escape }}{% endblock %} + +{% block content %} + + + +

{{ model.verbose_name_plural|capfirst|escape }} with {{ field.field.verbose_name|escape }} {{ value|escape }}

+ + + +{% endblock %} diff --git a/django/contrib/databrowse/templates/databrowse/fieldchoice_homepage.html b/django/contrib/databrowse/templates/databrowse/fieldchoice_homepage.html new file mode 100644 index 0000000000..ad842c1e6d --- /dev/null +++ b/django/contrib/databrowse/templates/databrowse/fieldchoice_homepage.html @@ -0,0 +1,17 @@ +{% extends "databrowse/base.html" %} + +{% block title %}Browsable fields in {{ model.verbose_name_plural|escape }}{% endblock %} + +{% block content %} + + + +

Browsable fields in {{ model.verbose_name_plural }}

+ + + +{% endblock %} diff --git a/django/contrib/databrowse/templates/databrowse/fieldchoice_list.html b/django/contrib/databrowse/templates/databrowse/fieldchoice_list.html new file mode 100644 index 0000000000..686e6bc533 --- /dev/null +++ b/django/contrib/databrowse/templates/databrowse/fieldchoice_list.html @@ -0,0 +1,17 @@ +{% extends "databrowse/base.html" %} + +{% block title %}{{ model.verbose_name_plural|capfirst|escape }} by {{ field.field.verbose_name|escape }}{% endblock %} + +{% block content %} + + + +

{{ model.verbose_name_plural|capfirst|escape }} by {{ field.field.verbose_name|escape }}

+ + + +{% endblock %} diff --git a/django/contrib/databrowse/templates/databrowse/homepage.html b/django/contrib/databrowse/templates/databrowse/homepage.html new file mode 100644 index 0000000000..ebf3ceca80 --- /dev/null +++ b/django/contrib/databrowse/templates/databrowse/homepage.html @@ -0,0 +1,21 @@ +{% extends "databrowse/base.html" %} + +{% block title %}Databrowse{% endblock %} + +{% block bodyid %}homepage{% endblock %} + +{% block content %} + +{% for model in model_list %} +
+

{{ model.verbose_name_plural|capfirst }}

+

+ {% for object in model.sample_objects %} + {{ object }}, + {% endfor %} + More → +

+
+{% endfor %} + +{% endblock %} diff --git a/django/contrib/databrowse/templates/databrowse/model_detail.html b/django/contrib/databrowse/templates/databrowse/model_detail.html new file mode 100644 index 0000000000..24cd766a3d --- /dev/null +++ b/django/contrib/databrowse/templates/databrowse/model_detail.html @@ -0,0 +1,19 @@ +{% extends "databrowse/base.html" %} + +{% block title %}{{ model.verbose_name_plural|capfirst }}{% endblock %} + +{% block content %} + + + +

{{ model.verbose_name_plural|capfirst }}

+ +{{ plugin_html }} + +
    +{% for object in model.objects %} +
  • {{ object }}
  • +{% endfor %} +
+ +{% endblock %} diff --git a/django/contrib/databrowse/templates/databrowse/object_detail.html b/django/contrib/databrowse/templates/databrowse/object_detail.html new file mode 100644 index 0000000000..0096178cd2 --- /dev/null +++ b/django/contrib/databrowse/templates/databrowse/object_detail.html @@ -0,0 +1,41 @@ +{% extends "databrowse/base.html" %} + +{% block title %}{{ object.model.verbose_name|capfirst }}: {{ object }}{% endblock %} + +{% block content %} + + + +

{{ object.model.verbose_name|capfirst }}: {{ object }}

+ + +{% for field in object.fields %} + + + + +{% endfor %} +
{{ field.field.verbose_name|capfirst }} +{% if field.urls %} +{% for urlvalue in field.urls %} +{% if urlvalue.1 %}{% endif %}{{ urlvalue.0 }}{% if urlvalue.1 %}{% endif %}{% if not forloop.last %}, {% endif %} +{% endfor %} +{% else %}None{% endif %} +
+ +{% for related_object in object.related_objects %} + + {% else %} +

(None)

+ {% endif %} +{% endfor %} + +{% endblock %} diff --git a/django/contrib/databrowse/urls.py b/django/contrib/databrowse/urls.py new file mode 100644 index 0000000000..9b85d142a2 --- /dev/null +++ b/django/contrib/databrowse/urls.py @@ -0,0 +1,20 @@ +from django.conf.urls.defaults import * +from django.contrib.databrowse import views + +# Note: The views in this URLconf all require a 'models' argument, +# which is a list of model classes (*not* instances). + +urlpatterns = patterns('', + #(r'^$', views.homepage), + #(r'^([^/]+)/([^/]+)/$', views.model_detail), + + (r'^([^/]+)/([^/]+)/fields/(\w+)/$', views.choice_list), + (r'^([^/]+)/([^/]+)/fields/(\w+)/(.*)/$', views.choice_detail), + + #(r'^([^/]+)/([^/]+)/calendars/(\w+)/$', views.calendar_main), + #(r'^([^/]+)/([^/]+)/calendars/(\w+)/(\d{4})/$', views.calendar_year), + #(r'^([^/]+)/([^/]+)/calendars/(\w+)/(\d{4})/(\w{3})/$', views.calendar_month), + #(r'^([^/]+)/([^/]+)/calendars/(\w+)/(\d{4})/(\w{3})/(\d{1,2})/$', views.calendar_day), + + #(r'^([^/]+)/([^/]+)/objects/(.*)/$', views.object_detail), +) diff --git a/django/contrib/databrowse/views.py b/django/contrib/databrowse/views.py new file mode 100644 index 0000000000..d493f9dad3 --- /dev/null +++ b/django/contrib/databrowse/views.py @@ -0,0 +1,23 @@ +from django.db.models import FieldDoesNotExist, DateTimeField +from django.http import Http404 +from django.shortcuts import render_to_response +from django.contrib.databrowse.datastructures import EasyModel, EasyChoice +import datetime +import time + +########### +# CHOICES # +########### + +def choice_list(request, app_label, module_name, field_name, models): + m, f = lookup_field(app_label, module_name, field_name, models) + return render_to_response('databrowse/choice_list.html', {'model': m, 'field': f}) + +def choice_detail(request, app_label, module_name, field_name, field_val, models): + m, f = lookup_field(app_label, module_name, field_name, models) + try: + label = dict(f.field.choices)[field_val] + except KeyError: + raise Http404('Invalid choice value given') + obj_list = m.objects(**{f.field.name: field_val}) + return render_to_response('databrowse/choice_detail.html', {'model': m, 'field': f, 'value': label, 'object_list': obj_list}) diff --git a/docs/databrowse.txt b/docs/databrowse.txt new file mode 100644 index 0000000000..e9691cc879 --- /dev/null +++ b/docs/databrowse.txt @@ -0,0 +1,56 @@ +========== +Databrowse +========== + +Databrowse is a Django application that lets you browse your data. + +As the Django admin dynamically creates an admin interface by introspecting +your models, Databrowse dynamically creates a rich, browsable Web site by +introspecting your models. + +.. admonition:: Note + + Databrowse is **very** new and is currently under active development. It + may change substantially before the next Django release. + + With that said, it's easy to use, and it doesn't require writing any + code. So you can play around with it today, with very little investment in + time or coding. + +How to use Databrowse +===================== + + 1. Point Django at the default Databrowse templates. There are two ways to + do this: + + * Add ``'django.contrib.databrowse'`` to your ``INSTALLED_APPS`` + setting. This will work if your ``TEMPLATE_LOADERS`` setting includes + the ``app_directories`` template loader (which is the case by + default). See the `template loader docs`_ for more. + + * Otherwise, determine the full filesystem path to the + ``django/contrib/databrowse/templates`` directory, and add that + directory to your ``TEMPLATE_DIRS`` setting. + + 2. Register a number of models with the Databrowse site:: + + from django.contrib import databrowse + + databrowse.site.register(SomeModel) + databrowse.site.register(SomeOtherModel) + + Note that you should register the model *classes*, not instances. + + It doesn't matter where you put this, as long as it gets executed at + some point. A good place for it is in your URLconf file (``urls.py``). + + 3. Add the following line to your URLconf:: + + (r'^databrowse/(.*)', databrowse.site.root), + + The prefix doesn't matter -- you can use ``databrowse/`` or ``db/`` or + whatever you'd like. + + 4. Run the Django server and visit ``/databrowse/`` in your browser. + +.. _template loader docs: ../templates_python/#loader-types diff --git a/docs/django-admin.txt b/docs/django-admin.txt index 52ee823cc3..917569e154 100644 --- a/docs/django-admin.txt +++ b/docs/django-admin.txt @@ -332,7 +332,7 @@ sqlall [appname appname ...] Prints the CREATE TABLE and initial-data SQL statements for the given appnames. -Refer to the description of ``sqlinitialdata`` for an explanation of how to +Refer to the description of ``sqlcustom`` for an explanation of how to specify initial data. sqlclear [appname appname ...] diff --git a/docs/install.txt b/docs/install.txt index 35f593477b..4650fd746b 100644 --- a/docs/install.txt +++ b/docs/install.txt @@ -58,6 +58,7 @@ installed. If you're on Windows, check out the unofficial `compiled Windows version`_. * If you're using MySQL, you'll need MySQLdb_, version 1.2.1p2 or higher. + You will also want to read the database-specific notes for the `MySQL backend`_. * If you're using SQLite, you'll need pysqlite_. Use version 2.0.3 or higher. @@ -69,6 +70,7 @@ installed. .. _MySQLdb: http://sourceforge.net/projects/mysql-python .. _SQLite: http://www.sqlite.org/ .. _pysqlite: http://initd.org/tracker/pysqlite +.. _MySQL backend: ../databases/ Remove any old versions of Django ================================= diff --git a/docs/model-api.txt b/docs/model-api.txt index 400617a012..7fe099e022 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -21,7 +21,7 @@ A companion to this document is the `official repository of model examples`_. (In the Django source distribution, these examples are in the ``tests/modeltests`` directory.) -.. _Database API reference: http://www.djangoproject.com/documentation/db_api/ +.. _Database API reference: ../db-api/ .. _official repository of model examples: http://www.djangoproject.com/documentation/models/ Quick example @@ -57,7 +57,7 @@ Some technical notes: syntax, but it's worth noting Django uses SQL tailored to the database backend specified in your `settings file`_. -.. _settings file: http://www.djangoproject.com/documentation/settings/ +.. _settings file: ../settings/ Fields ====== @@ -501,7 +501,7 @@ For each model field that has ``choices`` set, Django will add a method to retrieve the human-readable name for the field's current value. See `get_FOO_display`_ in the database API documentation. -.. _get_FOO_display: ../db_api/#get-foo-display +.. _get_FOO_display: ../db-api/#get-foo-display Finally, note that choices can be any iterable object -- not necessarily a list or tuple. This lets you construct choices dynamically. But if you find @@ -626,7 +626,7 @@ that takes the parameters ``field_data, all_data`` and raises Django comes with quite a few validators. They're in ``django.core.validators``. -.. _validator docs: http://www.djangoproject.com/documentation/forms/#validators +.. _validator docs: ../forms/#validators Verbose field names ------------------- @@ -792,8 +792,8 @@ relationship should work. All are optional: the related object. ======================= ============================================================ -.. _`Database API reference`: http://www.djangoproject.com/documentation/db_api/ -.. _related objects documentation: http://www.djangoproject.com/documentation/db_api/#related-objects +.. _`Database API reference`: ../db-api/ +.. _related objects documentation: ../db-api/#related-objects Many-to-many relationships ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -963,7 +963,7 @@ Example:: See the `docs for latest()`_ for more. -.. _docs for latest(): http://www.djangoproject.com/documentation/db_api/#latest-field-name-none +.. _docs for latest(): ../db-api/#latest-field-name-none ``order_with_respect_to`` ------------------------- @@ -1397,7 +1397,7 @@ if one of the ``list_display`` fields is a ``ForeignKey``. For more on ``select_related()``, see `the select_related() docs`_. -.. _the select_related() docs: http://www.djangoproject.com/documentation/db_api/#select-related +.. _the select_related() docs: ../db-api/#select-related ``ordering`` ------------ @@ -1502,7 +1502,7 @@ The way ``Manager`` classes work is documented in the `Retrieving objects`_ section of the database API docs, but this section specifically touches on model options that customize ``Manager`` behavior. -.. _Retrieving objects: http://www.djangoproject.com/documentation/db_api/#retrieving-objects +.. _Retrieving objects: ../db-api/#retrieving-objects Manager names ------------- @@ -1825,7 +1825,7 @@ just the ``where``, ``tables`` and ``params`` arguments to the standard lookup API. See `Other lookup options`_. .. _Python DB-API: http://www.python.org/peps/pep-0249.html -.. _Other lookup options: http://www.djangoproject.com/documentation/db_api/#extra-params-select-where-tables +.. _Other lookup options: ../db-api/#extra-params-select-where-tables Overriding default model methods -------------------------------- @@ -1858,7 +1858,7 @@ You can also prevent saving:: else: super(Blog, self).save() # Call the "real" save() method. -.. _database API docs: http://www.djangoproject.com/documentation/db_api/ +.. _database API docs: ../db-api/ Models across files =================== @@ -1915,7 +1915,7 @@ Each SQL file, if given, is expected to contain valid SQL. The SQL files are piped directly into the database after all of the models' table-creation statements have been executed. -The SQL files are read by the ``sqlinitialdata``, ``sqlreset``, ``sqlall`` and +The SQL files are read by the ``sqlcustom``, ``sqlreset``, ``sqlall`` and ``reset`` commands in ``manage.py``. Refer to the `manage.py documentation`_ for more information. @@ -1924,7 +1924,7 @@ order in which they're executed. The only thing you can assume is that, by the time your custom data files are executed, all the database tables already will have been created. -.. _`manage.py documentation`: http://www.djangoproject.com/documentation/django_admin/#sqlinitialdata-appname-appname +.. _`manage.py documentation`: ../django_admin/#sqlcustom-appname-appname Database-backend-specific SQL data ---------------------------------- diff --git a/docs/templates.txt b/docs/templates.txt index 82250c1f16..2f9f769b96 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -1293,3 +1293,11 @@ A collection of template filters that implement these common markup languages: * Textile * Markdown * ReST (ReStructured Text) + +django.contrib.webdesign +------------------------ + +A collection of template tags that can be useful while designing a website, +such as a generator of Lorem Ipsum text. See the `webdesign documentation`_. + +.. _webdesign documentation: ../webdesign/ diff --git a/docs/tutorial01.txt b/docs/tutorial01.txt index 0e857d09ca..b29d7b813f 100644 --- a/docs/tutorial01.txt +++ b/docs/tutorial01.txt @@ -382,7 +382,7 @@ If you're interested, also run the following commands: statements for this app. * ``python manage.py sqlall polls`` -- A combination of all the SQL from - the 'sql', 'sqlinitialdata', and 'sqlindexes' commands. + the 'sql', 'sqlcustom', and 'sqlindexes' commands. Looking at the output of those commands can help you understand what's actually happening under the hood.