From 30fb62d71af0186c98c7d5208a72609f74943092 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 16 Apr 2007 21:56:10 +0000 Subject: [PATCH] Added django.contrib.databrowse git-svn-id: http://code.djangoproject.com/svn/django/trunk@5011 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/databrowse/__init__.py | 1 + django/contrib/databrowse/datastructures.py | 188 ++++++++++++++++++ django/contrib/databrowse/plugins/__init__.py | 0 .../contrib/databrowse/plugins/calendars.py | 84 ++++++++ .../databrowse/plugins/fieldchoices.py | 72 +++++++ django/contrib/databrowse/plugins/objects.py | 14 ++ django/contrib/databrowse/sites.py | 148 ++++++++++++++ .../databrowse/templates/databrowse/base.html | 58 ++++++ .../templates/databrowse/calendar_day.html | 17 ++ .../databrowse/calendar_homepage.html | 17 ++ .../templates/databrowse/calendar_main.html | 17 ++ .../templates/databrowse/calendar_month.html | 17 ++ .../templates/databrowse/calendar_year.html | 17 ++ .../templates/databrowse/choice_detail.html | 17 ++ .../templates/databrowse/choice_list.html | 17 ++ .../databrowse/fieldchoice_detail.html | 17 ++ .../databrowse/fieldchoice_homepage.html | 17 ++ .../databrowse/fieldchoice_list.html | 17 ++ .../templates/databrowse/homepage.html | 21 ++ .../templates/databrowse/model_detail.html | 19 ++ .../templates/databrowse/object_detail.html | 41 ++++ django/contrib/databrowse/urls.py | 20 ++ django/contrib/databrowse/views.py | 23 +++ docs/databrowse.txt | 54 +++++ 24 files changed, 913 insertions(+) create mode 100644 django/contrib/databrowse/__init__.py create mode 100644 django/contrib/databrowse/datastructures.py create mode 100644 django/contrib/databrowse/plugins/__init__.py create mode 100644 django/contrib/databrowse/plugins/calendars.py create mode 100644 django/contrib/databrowse/plugins/fieldchoices.py create mode 100644 django/contrib/databrowse/plugins/objects.py create mode 100644 django/contrib/databrowse/sites.py create mode 100644 django/contrib/databrowse/templates/databrowse/base.html create mode 100644 django/contrib/databrowse/templates/databrowse/calendar_day.html create mode 100644 django/contrib/databrowse/templates/databrowse/calendar_homepage.html create mode 100644 django/contrib/databrowse/templates/databrowse/calendar_main.html create mode 100644 django/contrib/databrowse/templates/databrowse/calendar_month.html create mode 100644 django/contrib/databrowse/templates/databrowse/calendar_year.html create mode 100644 django/contrib/databrowse/templates/databrowse/choice_detail.html create mode 100644 django/contrib/databrowse/templates/databrowse/choice_list.html create mode 100644 django/contrib/databrowse/templates/databrowse/fieldchoice_detail.html create mode 100644 django/contrib/databrowse/templates/databrowse/fieldchoice_homepage.html create mode 100644 django/contrib/databrowse/templates/databrowse/fieldchoice_list.html create mode 100644 django/contrib/databrowse/templates/databrowse/homepage.html create mode 100644 django/contrib/databrowse/templates/databrowse/model_detail.html create mode 100644 django/contrib/databrowse/templates/databrowse/object_detail.html create mode 100644 django/contrib/databrowse/urls.py create mode 100644 django/contrib/databrowse/views.py create mode 100644 docs/databrowse.txt diff --git a/django/contrib/databrowse/__init__.py b/django/contrib/databrowse/__init__.py new file mode 100644 index 00000000000..e2f48ac822d --- /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 00000000000..3c509a28301 --- /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 00000000000..e69de29bb2d diff --git a/django/contrib/databrowse/plugins/calendars.py b/django/contrib/databrowse/plugins/calendars.py new file mode 100644 index 00000000000..7977524cdb3 --- /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 00000000000..49d17ff1484 --- /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 00000000000..73265666558 --- /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 00000000000..8521343e509 --- /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 00000000000..30ba5bb2fce --- /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 00000000000..d45fb3e7686 --- /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 00000000000..5bbc42e3539 --- /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 00000000000..7f9ba03bbed --- /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 00000000000..51a25967f86 --- /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 00000000000..676ae88e273 --- /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 00000000000..977a4a482fc --- /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 00000000000..b15531a0fd9 --- /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 00000000000..a620ec931c3 --- /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 00000000000..ad842c1e6d3 --- /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 00000000000..686e6bc5332 --- /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 00000000000..ebf3ceca802 --- /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 00000000000..24cd766a3d7 --- /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 00000000000..0096178cd23 --- /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 00000000000..9b85d142a2e --- /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 00000000000..d493f9dad33 --- /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 00000000000..81fd2e8cb84 --- /dev/null +++ b/docs/databrowse.txt @@ -0,0 +1,54 @@ +========== +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.