15 |
--------------------------------------------------------------------------------
/docs/usage.txt:
--------------------------------------------------------------------------------
1 | To create a Grid, you need to subclass DataGrid.
2 |
3 | `class BlogGrid(DataGrid)`
4 |
5 | This needs to add the columns which shown in datagrid.
6 |
7 | class BlogGrid(DataGrid):
8 | created_by = Column(sortable=True,
9 | link=True,
10 | cell_clickable=True,
11 | css_class='red')
12 | ....
13 |
14 | Using the grid
15 | ----------------
16 |
17 | To use the grid you need to pass a queryset. The number of rows shown in the grid is same as that in the queryset.
18 | Each element from the queryset is made avaialble to each `Column` for the grid, to let it decide how to render the cell.
19 |
20 |
21 | Types of columns
22 | ------------------------
23 |
24 | `Column`
25 | `DateTimeColumn`
26 | `DateTimeSinceColumn`
27 | `NonDatabaseColumn`
28 |
29 | Common arguments to each column.
30 | --------------------------------------
31 |
32 | `link`: Whether objects should be linked form the column.
33 | `Sortable`: Whether column header shouold be sortable.
34 |
35 |
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2007 Christian Hammond
2 | Copyright (c) 2007 David Trowbridge
3 | Copyright (c) 2010 Agiliq
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9 | of the Software, and to permit persons to whom the Software is furnished to do
10 | so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples.txt:
--------------------------------------------------------------------------------
1 | Examples
2 | -------------------
3 |
4 | 1. Simplest
5 |
6 | 1.1 Create a Grid:
7 |
8 | class SimpleGrid(DataGrid):
9 | name = NonDatabaseColumn("Hello")
10 |
11 | 1.2 In the views
12 |
13 | def simple(request):
14 | books = Book.objects.all()
15 | return SimpleGrid(request, books).render_to_response("mylibrary/simple.html")
16 |
17 | This creates a view with as many rows as the elemns in `books` queryset.
18 |
19 | 2. A little better.
20 |
21 | OK that grid is useless, lets createa one with real data.
22 |
23 | class RealGrid(DataGrid):
24 | name = Column()
25 | publisher = Column()
26 | recommended_by = Column()
27 |
28 | This will show the attributes named `name`, `publisher` and `recommended_by`.
29 | view remains about same.
30 |
31 |
32 | def real(request):
33 | books = Book.objects.all()
34 | return RealGrid(request, books).render_to_response("mylibrary/real.html")
35 |
36 | 3. Cool lets add sorting.
37 |
38 | class RealGrid(DataGrid):
39 | name = Column(sortable = True)
40 | publisher = Column(sortable = True)
41 | recommended_by = Column(sortable = True)
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name="django-datagrid",
5 | description="A django based Datagrid",
6 | keywords='django, datagrid',
7 | packages=find_packages(),
8 | include_package_data=True,
9 | zip_safe=False,
10 | version="1.1",
11 | author = "Agiliq and friends",
12 | author_email="hello@agiliq.com",
13 | classifiers = ['Development Status :: 4 - Beta',
14 | 'Environment :: Web Environment',
15 | 'Framework :: Django',
16 | 'Intended Audience :: Developers',
17 | 'License :: OSI Approved :: BSD License',
18 | 'Operating System :: OS Independent',
19 | 'Programming Language :: Python',
20 | 'Topic :: Internet :: WWW/HTTP',
21 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
22 | 'Topic :: Internet :: WWW/HTTP :: WSGI',
23 | 'Topic :: Software Development :: Libraries :: Application Frameworks',
24 | 'Topic :: Software Development :: Libraries :: Python Modules',
25 | ],
26 | url="http://www.agiliq.com/",
27 | license="BSD",
28 | platforms=["all"],
29 | )
30 |
--------------------------------------------------------------------------------
/datagrid/templates/datagrid/column_header.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 | {% if column.sortable %}
4 |
84 |
--------------------------------------------------------------------------------
/books/settings.py:
--------------------------------------------------------------------------------
1 | # Django settings for books project.
2 |
3 | DEBUG = True
4 | TEMPLATE_DEBUG = DEBUG
5 |
6 | ADMINS = (
7 | # ('Your Name', 'your_email@domain.com'),
8 | )
9 |
10 | MANAGERS = ADMINS
11 |
12 | DATABASES = {
13 | 'default': {
14 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
15 | 'NAME': 'books.db', # Or path to database file if using sqlite3.
16 | # The following settings are not used with sqlite3:
17 | 'USER': '',
18 | 'PASSWORD': '',
19 | 'HOST': '', # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP.
20 | 'PORT': '', # Set to empty string for default.
21 | }
22 | }
23 |
24 | # Local time zone for this installation. Choices can be found here:
25 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
26 | # although not all choices may be available on all operating systems.
27 | # If running in a Windows environment this must be set to the same as your
28 | # system time zone.
29 | TIME_ZONE = 'America/Chicago'
30 |
31 | # Language code for this installation. All choices can be found here:
32 | # http://www.i18nguy.com/unicode/language-identifiers.html
33 | LANGUAGE_CODE = 'en-us'
34 |
35 | SITE_ID = 1
36 |
37 | # If you set this to False, Django will make some optimizations so as not
38 | # to load the internationalization machinery.
39 | USE_I18N = True
40 |
41 | # Absolute path to the directory that holds media.
42 | # Example: "/home/media/media.lawrence.com/"
43 | MEDIA_ROOT = ''
44 |
45 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a
46 | # trailing slash if there is a path component (optional in other cases).
47 | # Examples: "http://media.lawrence.com", "http://example.com/media/"
48 | MEDIA_URL = ''
49 |
50 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
51 | # trailing slash.
52 | # Examples: "http://foo.com/media/", "/media/".
53 | ADMIN_MEDIA_PREFIX = '/media/'
54 |
55 | # Make this unique, and don't share it with anybody.
56 | SECRET_KEY = 'jzvthi6++)nfd*s#^)a=$&c$d5c90$bbq8%304d_16xxcerlpx'
57 |
58 | TEMPLATE_LOADERS = (
59 | 'django.template.loaders.filesystem.Loader',
60 | 'django.template.loaders.app_directories.Loader',
61 | # 'django.template.loaders.eggs.Loader',
62 | )
63 | MIDDLEWARE_CLASSES = (
64 | 'django.middleware.common.CommonMiddleware',
65 | 'django.contrib.sessions.middleware.SessionMiddleware',
66 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
67 | )
68 |
69 | ROOT_URLCONF = 'books.urls'
70 |
71 | TEMPLATE_DIRS = (
72 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
73 | # Always use forward slashes, even on Windows.
74 | # Don't forget to use absolute paths, not relative paths.
75 | )
76 |
77 | INSTALLED_APPS = (
78 | 'django.contrib.auth',
79 | 'django.contrib.contenttypes',
80 | 'django.contrib.sessions',
81 | 'django.contrib.sites',
82 | 'django.contrib.admin',
83 | 'datagrid',
84 | 'mylibrary',
85 | )
86 |
--------------------------------------------------------------------------------
/datagrid/templatetags/datagrid.py:
--------------------------------------------------------------------------------
1 | import urllib
2 | from django import template
3 | register = template.Library()
4 |
5 |
6 | PAGINATION_DEFAULT = 20
7 | @register.inclusion_tag('datagrid/pagination_size_frag.html', takes_context=True)
8 | def render_pagination_size_widget(context):
9 | "Usage {% render_pagination_size_widget %}"
10 | payload = {}
11 | payload['page_sizes'] = [1, 10, 20, 50, 100, 500]
12 | if 'request' in context:
13 |
14 | request = context['request']
15 | getvars = request.GET.copy()
16 | if 'page_size' in getvars:
17 | payload['current_page_size'] = int(getvars['page_size'])
18 | del getvars['page_size']
19 | else:
20 | from django.conf import settings
21 | payload['current_page_size'] = getattr(settings, 'PAGINATION_DEFAULT_PAGINATION', PAGINATION_DEFAULT)
22 | if 'page' in getvars:
23 | del getvars['page']
24 | if len(getvars.keys()) > 0:
25 | payload['getpagingvars'] = zip(getvars.keys(), getvars.values())
26 | else:
27 | payload['getpagingvars'] = []
28 | return payload
29 |
30 |
31 | @register.inclusion_tag('datagrid/get_pdf_link.html', takes_context=True)
32 | def get_pdf_link(context):
33 | getvars = {}
34 | if 'request' in context:
35 | request = context['request']
36 | getvars = request.GET.copy()
37 | getvars['is_pdf'] = 1
38 | getvars = urllib.urlencode(getvars)
39 | return {'getvars':getvars}
40 | else:
41 | getvars['is_pdf'] = 1
42 | getvars = urllib.urlencode(getvars)
43 | return {'getvars':getvars}
44 |
45 |
46 | @register.inclusion_tag('datagrid/get_csv_link.html', takes_context=True)
47 | def get_csv_link(context):
48 | getvars = {}
49 | if 'request' in context:
50 | request = context['request']
51 | getvars = request.GET.copy()
52 | getvars['is_csv'] = 1
53 | getvars = urllib.urlencode(getvars)
54 | else:
55 | getvars['is_csv'] = 1
56 | getvars = urllib.urlencode(getvars)
57 | return {'getvars':getvars}
58 |
59 | @register.inclusion_tag('datagrid/get_search_form.html', takes_context=True)
60 | def get_search_form(context):
61 | getvars = {}
62 | if 'request' in context:
63 | request = context['request']
64 | getvars = request.GET.copy()
65 | if 'q' in getvars:
66 | searchterm = getvars['q']
67 | del getvars['q']
68 | return {'getvars':getvars.items(), 'searchterm': searchterm}
69 | else:
70 | return {'getvars':[], 'searchterm': ''}
71 |
72 | @register.inclusion_tag('datagrid/get_filter_form.html', takes_context=True)
73 | def get_filter_form(context):
74 | #TODO
75 |
76 | return context
77 |
78 |
79 |
80 | @register.inclusion_tag('datagrid/paginator.html', takes_context=True)
81 | def paginator(context, adjacent_pages=3):
82 | """
83 | Renders a paginator used for jumping between pages of results.
84 | """
85 | page_nums = range(max(1, context['page'] - adjacent_pages),
86 | min(context['pages'], context['page'] + adjacent_pages)
87 | + 1)
88 | getvars = {}
89 | if 'request' in context:
90 | request = context['request']
91 | getvars = request.GET.copy()
92 | if 'page' in getvars:
93 | getvars['page']
94 | getvars = urllib.urlencode(getvars)
95 |
96 | return {
97 | 'hits': context['hits'],
98 | 'results_per_page': context['results_per_page'],
99 | 'page': context['page'],
100 | 'pages': context['pages'],
101 | 'page_numbers': page_nums,
102 | 'next': context['next'],
103 | 'previous': context['previous'],
104 | 'has_next': context['has_next'],
105 | 'has_previous': context['has_previous'],
106 | 'show_first': 1 not in page_nums,
107 | 'show_last': context['pages'] not in page_nums,
108 | 'extra_query': context.get('extra_query', None),
109 | 'getvars': getvars,
110 | }
111 |
--------------------------------------------------------------------------------
/datagrid/tests.py:
--------------------------------------------------------------------------------
1 | #
2 | # tests.py -- Unit tests for classes in djblets.datagrid
3 | #
4 | # Copyright (c) 2007-2008 Christian Hammond
5 | # Copyright (c) 2007-2008 David Trowbridge
6 | #
7 | # Permission is hereby granted, free of charge, to any person obtaining
8 | # a copy of this software and associated documentation files (the
9 | # "Software"), to deal in the Software without restriction, including
10 | # without limitation the rights to use, copy, modify, merge, publish,
11 | # distribute, sublicense, and/or sell copies of the Software, and to
12 | # permit persons to whom the Software is furnished to do so, subject to
13 | # the following conditions:
14 | #
15 | # The above copyright notice and this permission notice shall be included
16 | # in all copies or substantial portions of the Software.
17 | #
18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
21 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
23 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
24 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 |
26 |
27 | from datetime import datetime, timedelta
28 |
29 | from django.conf import settings
30 | from django.contrib.auth.models import Group, User
31 | from django.http import HttpRequest
32 |
33 | from datagrid.grids import Column, DataGrid, DateTimeSinceColumn
34 | from django.test import TestCase
35 |
36 |
37 | def populate_groups():
38 | for i in range(1, 100):
39 | group = Group(name="Group %02d" % i)
40 | group.save()
41 |
42 |
43 | class GroupDataGrid(DataGrid):
44 | objid = Column("ID", link=True, sortable=True, field_name="id")
45 | name = Column("Group Name", link=True, sortable=True, expand=True)
46 |
47 | def __init__(self, request):
48 | DataGrid.__init__(self, request, Group.objects.all(), "All Groups")
49 | self.default_sort = []
50 | self.default_columns = [
51 | "objid", "name"
52 | ]
53 |
54 |
55 | class ColumnsTest(TestCase):
56 | def testDateTimeSinceColumn(self):
57 | """Testing DateTimeSinceColumn"""
58 | class DummyObj:
59 | time = None
60 |
61 | column = DateTimeSinceColumn("Test", field_name='time')
62 | now = datetime.now()
63 |
64 | obj = DummyObj()
65 | obj.time = now
66 | self.assertEqual(column.render_data(obj), "0 minutes ago")
67 |
68 | obj.time = now - timedelta(days=5)
69 | self.assertEqual(column.render_data(obj), "5 days ago")
70 |
71 | obj.time = now - timedelta(days=7)
72 | self.assertEqual(column.render_data(obj), "1 week ago")
73 |
74 |
75 | class DataGridTest(TestCase):
76 | def setUp(self):
77 | self.old_auth_profile_module = getattr(settings, "AUTH_PROFILE_MODULE",
78 | None)
79 | settings.AUTH_PROFILE_MODULE = None
80 | populate_groups()
81 | self.user = User(username="testuser")
82 | self.request = HttpRequest()
83 | self.request.user = self.user
84 | self.datagrid = GroupDataGrid(self.request)
85 |
86 | def tearDown(self):
87 | settings.AUTH_PROFILE_MODULE = self.old_auth_profile_module
88 |
89 | def testRender(self):
90 | """Testing basic datagrid rendering"""
91 | self.datagrid.render_listview()
92 |
93 | def testRenderToResponse(self):
94 | """Testing rendering datagrid to HTTPResponse"""
95 | self.datagrid.render_listview_to_response()
96 |
97 | def testSortAscending(self):
98 | """Testing datagrids with ascending sort"""
99 | self.request.GET['sort'] = "name,objid"
100 | self.datagrid.load_state()
101 |
102 | self.assertEqual(self.datagrid.sort_list, ["name", "objid"])
103 | self.assertEqual(len(self.datagrid.rows), self.datagrid.paginate_by)
104 | self.assertEqual(self.datagrid.rows[0]['object'].name, "Group 01")
105 | self.assertEqual(self.datagrid.rows[1]['object'].name, "Group 02")
106 | self.assertEqual(self.datagrid.rows[2]['object'].name, "Group 03")
107 |
108 | # Exercise the code paths when rendering
109 | self.datagrid.render_listview()
110 |
111 | def testSortDescending(self):
112 | """Testing datagrids with descending sort"""
113 | self.request.GET['sort'] = "-name"
114 | self.datagrid.load_state()
115 |
116 | self.assertEqual(self.datagrid.sort_list, ["-name"])
117 | self.assertEqual(len(self.datagrid.rows), self.datagrid.paginate_by)
118 | self.assertEqual(self.datagrid.rows[0]['object'].name, "Group 99")
119 | self.assertEqual(self.datagrid.rows[1]['object'].name, "Group 98")
120 | self.assertEqual(self.datagrid.rows[2]['object'].name, "Group 97")
121 |
122 | # Exercise the code paths when rendering
123 | self.datagrid.render_listview()
124 |
125 |
126 | def testCustomColumns(self):
127 | """Testing datagrids with custom column orders"""
128 | self.request.GET['columns'] = "objid"
129 | self.datagrid.load_state()
130 |
131 | self.assertEqual(len(self.datagrid.rows), self.datagrid.paginate_by)
132 | self.assertEqual(len(self.datagrid.rows[0]['cells']), 1)
133 |
134 | # Exercise the code paths when rendering
135 | self.datagrid.render_listview()
136 |
--------------------------------------------------------------------------------
/datagrid/doc.txt:
--------------------------------------------------------------------------------
1 |
2 | Field Types
3 |
4 | Column
5 |
6 | options
7 |
8 | label
9 | label to be displayed in the table header
10 |
11 | db_field
12 | used to specify what the field is defined as in django-models, if the
13 | column name in datagrid is different from the field name defined in django-models
14 |
15 | image_url
16 | url of the image do be displayed next to the header
17 | ex: /site_media/blogango/images/date_icon.png
18 | or http://media.agiliq.com/images/terminal.png
19 |
20 | image_width
21 | if image_url is set,
22 | image_width defines the width of the image do be displayed next to the header
23 |
24 | image_height
25 | if image_url is set,
26 | image_height defines the height of the image do be displayed next to the header
27 |
28 | image_alt
29 | if image_url is set,
30 | image_alt defines the alt text to be shown if the image does not load
31 |
32 | shrink
33 | Boolean True or False, default False
34 | reduce the width of column
35 |
36 | expand
37 | Boolean True or False, default False
38 | expand the width of column
39 |
40 | sortable
41 | Boolean True or False, default False
42 | sort the table data based on this column value
43 |
44 | default_sort_dir
45 | default sort is descending order, for value 0
46 | default_sort_dir = 1, sort in ascending order
47 |
48 | link
49 | Boolean True or False, default False
50 | the obj should have get_absolute_url or can pass link_func which generates the url
51 |
52 | link_func
53 | link_func(obj, rendered_data)
54 | if link=True, given link_func is used to get the value of url
55 | return value can be absolute url or relative url
56 |
57 | cell_clickable
58 | Boolean True or False
59 | if the cell is link, clicking anywhere inside the cell will redirect to the linked url
60 |
61 | css_class
62 | does not apply to the header, applies only to the data cells
63 |
64 | data_func
65 | if the display text is
66 | passes the value of the field as argument to the given function
67 |
68 |
69 | DateTimeColumn
70 |
71 | options
72 |
73 | same as the Column field and one optional argument
74 |
75 | format
76 | formats the date same as django.template.filter.date
77 |
78 | DateTimeSinceColumn
79 |
80 | options
81 |
82 | same as the Column field
83 |
84 | uses django.template.defaultfilters.timesince to format the date
85 |
86 | NonDatabaseColumn
87 | Helpful for displaying a column not specified in the model
88 |
89 | options
90 | same as the Column field
91 |
92 | if data_func is specified uses the data_func to get the value of column
93 | otherwise label of the column is returned as the value
94 |
95 | object of the current row is passed as argument to the data_func function
96 |
97 |
98 | Grids
99 |
100 | DataGrid
101 | A representation of a list of objects, sorted and organized by
102 | columns. The sort order and column lists can be customized. allowing
103 | users to view this data however they prefer.
104 |
105 | This is meant to be subclassed for specific uses. The subclasses are
106 | responsible for defining one or more column types. It can also set
107 | one or more of the below specified optional arguments.
108 |
109 | required arguments
110 |
111 | request
112 | the request object
113 |
114 | queryset
115 | A QuerySet that represents the objects.
116 |
117 | optional arguments
118 |
119 | title="",
120 | title of the grid
121 |
122 | extra_context={}
123 | extra context to be passed to the template
124 | useful when DataGrid's render_to_response method is called.
125 |
126 | optimize_sorts
127 | Whether or not to optimize queries when
128 | using multiple sorts. This can offattr_metaer a
129 | speed improvement, but may need to be
130 | turned off for more advanced querysets
131 | (such as when using extra()).
132 | The default is True.
133 |
134 | listview_template
135 | template to be used to render the list view
136 | default is 'datagrid/listview.html',
137 |
138 | column_header_template
139 | template to be used to render the column headers
140 | default is 'datagrid/column_header.html',
141 |
142 | cell_template
143 | template to be used to render the data of each cell
144 | default is 'datagrid/cell.html'
145 |
146 | methods
147 |
148 | render_listview
149 | Renders the standard list view of the grid.
150 | This can be called from templates.
151 |
152 | render_listview_to_response
153 | Renders the listview to a response, preventing caching in the
154 | process.
155 |
156 | render_to_response
157 | Renders a template containing this datagrid as a context variable.
--------------------------------------------------------------------------------
/ez_setup.py:
--------------------------------------------------------------------------------
1 | #!python
2 | """Bootstrap setuptools installation
3 |
4 | If you want to use setuptools in your package's setup.py, just include this
5 | file in the same directory with it, and add this to the top of your setup.py::
6 |
7 | from ez_setup import use_setuptools
8 | use_setuptools()
9 |
10 | If you want to require a specific version of setuptools, set a download
11 | mirror, or use an alternate download directory, you can do so by supplying
12 | the appropriate options to ``use_setuptools()``.
13 |
14 | This file can also be run as a script to install or upgrade setuptools.
15 | """
16 | import sys
17 | DEFAULT_VERSION = "0.6c9"
18 | DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
19 |
20 | md5_data = {
21 | 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
22 | 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
23 | 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
24 | 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
25 | 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
26 | 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
27 | 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
28 | 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
29 | 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
30 | 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
31 | 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
32 | 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
33 | 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
34 | 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
35 | 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
36 | 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
37 | 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
38 | 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
39 | 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
40 | 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
41 | 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
42 | 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
43 | 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
44 | 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
45 | 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
46 | 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
47 | 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
48 | 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
49 | 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
50 | 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
51 | 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
52 | 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
53 | 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
54 | 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
55 | }
56 |
57 | import sys, os
58 | try: from hashlib import md5
59 | except ImportError: from md5 import md5
60 |
61 | def _validate_md5(egg_name, data):
62 | if egg_name in md5_data:
63 | digest = md5(data).hexdigest()
64 | if digest != md5_data[egg_name]:
65 | print >>sys.stderr, (
66 | "md5 validation of %s failed! (Possible download problem?)"
67 | % egg_name
68 | )
69 | sys.exit(2)
70 | return data
71 |
72 | def use_setuptools(
73 | version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
74 | download_delay=15
75 | ):
76 | """Automatically find/download setuptools and make it available on sys.path
77 |
78 | `version` should be a valid setuptools version number that is available
79 | as an egg for download under the `download_base` URL (which should end with
80 | a '/'). `to_dir` is the directory where setuptools will be downloaded, if
81 | it is not already available. If `download_delay` is specified, it should
82 | be the number of seconds that will be paused before initiating a download,
83 | should one be required. If an older version of setuptools is installed,
84 | this routine will print a message to ``sys.stderr`` and raise SystemExit in
85 | an attempt to abort the calling script.
86 | """
87 | was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
88 | def do_download():
89 | egg = download_setuptools(version, download_base, to_dir, download_delay)
90 | sys.path.insert(0, egg)
91 | import setuptools; setuptools.bootstrap_install_from = egg
92 | try:
93 | import pkg_resources
94 | except ImportError:
95 | return do_download()
96 | try:
97 | pkg_resources.require("setuptools>="+version); return
98 | except pkg_resources.VersionConflict, e:
99 | if was_imported:
100 | print >>sys.stderr, (
101 | "The required version of setuptools (>=%s) is not available, and\n"
102 | "can't be installed while this script is running. Please install\n"
103 | " a more recent version first, using 'easy_install -U setuptools'."
104 | "\n\n(Currently using %r)"
105 | ) % (version, e.args[0])
106 | sys.exit(2)
107 | else:
108 | del pkg_resources, sys.modules['pkg_resources'] # reload ok
109 | return do_download()
110 | except pkg_resources.DistributionNotFound:
111 | return do_download()
112 |
113 |
114 |
115 | def download_setuptools(
116 | version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
117 | delay = 15
118 | ):
119 | """Download setuptools from a specified location and return its filename
120 |
121 | `version` should be a valid setuptools version number that is available
122 | as an egg for download under the `download_base` URL (which should end
123 | with a '/'). `to_dir` is the directory where the egg will be downloaded.
124 | `delay` is the number of seconds to pause before an actual download attempt.
125 | """
126 | import urllib2, shutil
127 | egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
128 | url = download_base + egg_name
129 | saveto = os.path.join(to_dir, egg_name)
130 | src = dst = None
131 | if not os.path.exists(saveto): # Avoid repeated downloads
132 | try:
133 | from distutils import log
134 | if delay:
135 | log.warn("""
136 | ---------------------------------------------------------------------------
137 | This script requires setuptools version %s to run (even to display
138 | help). I will attempt to download it for you (from
139 | %s), but
140 | you may need to enable firewall access for this script first.
141 | I will start the download in %d seconds.
142 |
143 | (Note: if this machine does not have network access, please obtain the file
144 |
145 | %s
146 |
147 | and place it in this directory before rerunning this script.)
148 | ---------------------------------------------------------------------------""",
149 | version, download_base, delay, url
150 | ); from time import sleep; sleep(delay)
151 | log.warn("Downloading %s", url)
152 | src = urllib2.urlopen(url)
153 | # Read/write all in one block, so we don't create a corrupt file
154 | # if the download is interrupted.
155 | data = _validate_md5(egg_name, src.read())
156 | dst = open(saveto,"wb"); dst.write(data)
157 | finally:
158 | if src: src.close()
159 | if dst: dst.close()
160 | return os.path.realpath(saveto)
161 |
162 |
163 | def main(argv, version=DEFAULT_VERSION):
164 | """Install or upgrade setuptools and EasyInstall"""
165 | try:
166 | import setuptools
167 | except ImportError:
168 | egg = None
169 | try:
170 | egg = download_setuptools(version, delay=0)
171 | sys.path.insert(0,egg)
172 | from setuptools.command.easy_install import main
173 | return main(list(argv)+[egg]) # we're done here
174 | finally:
175 | if egg and os.path.exists(egg):
176 | os.unlink(egg)
177 | else:
178 | if setuptools.__version__ == '0.0.1':
179 | print >>sys.stderr, (
180 | "You have an obsolete version of setuptools installed. Please\n"
181 | "remove it from your system entirely before rerunning this script."
182 | )
183 | sys.exit(2)
184 |
185 | req = "setuptools>="+version
186 | import pkg_resources
187 | try:
188 | pkg_resources.require(req)
189 | except pkg_resources.VersionConflict:
190 | try:
191 | from setuptools.command.easy_install import main
192 | except ImportError:
193 | from easy_install import main
194 | main(list(argv)+[download_setuptools(delay=0)])
195 | sys.exit(0) # try to force an exit
196 | else:
197 | if argv:
198 | from setuptools.command.easy_install import main
199 | main(argv)
200 | else:
201 | print "Setuptools version",version,"or greater has been installed."
202 | print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
203 |
204 |
205 |
206 | def update_md5(filenames):
207 | """Update our built-in md5 registry"""
208 |
209 | import re
210 |
211 | for name in filenames:
212 | base = os.path.basename(name)
213 | f = open(name,'rb')
214 | md5_data[base] = md5(f.read()).hexdigest()
215 | f.close()
216 |
217 | data = [" %r: %r,\n" % it for it in md5_data.items()]
218 | data.sort()
219 | repl = "".join(data)
220 |
221 | import inspect
222 | srcfile = inspect.getsourcefile(sys.modules[__name__])
223 | f = open(srcfile, 'rb'); src = f.read(); f.close()
224 |
225 | match = re.search("\nmd5_data = {\n([^}]+)}", src)
226 | if not match:
227 | print >>sys.stderr, "Internal error!"
228 | sys.exit(2)
229 |
230 | src = src[:match.start(1)] + repl + src[match.end(1):]
231 | f = open(srcfile,'w')
232 | f.write(src)
233 | f.close()
234 |
235 |
236 | if __name__=='__main__':
237 | if len(sys.argv)>2 and sys.argv[1]=='--md5update':
238 | update_md5(sys.argv[2:])
239 | else:
240 | main(sys.argv[1:])
241 |
242 |
243 |
244 |
245 |
246 |
247 |
--------------------------------------------------------------------------------
/datagrid/grids.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.contrib.auth.models import SiteProfileNotAvailable
3 | from django.core.exceptions import ObjectDoesNotExist
4 | from django.core.paginator import InvalidPage, Paginator
5 | from django.http import Http404, HttpResponse
6 | from django.shortcuts import render_to_response
7 | from django.template.context import RequestContext
8 | from django.template.defaultfilters import date, timesince
9 | from django.template.loader import render_to_string
10 | from django.utils.safestring import mark_safe
11 | from django.utils.translation import ugettext as _
12 | from django.views.decorators.cache import cache_control
13 | from django.db.models import Q
14 |
15 | import StringIO
16 |
17 |
18 | class Column(object):
19 | """
20 | A column in a data grid.
21 |
22 | The column is the primary component of the data grid. It is used to
23 | display not only the column header but the HTML for the cell as well.
24 |
25 | Columns can be tied to database fields and can be used for sorting.
26 | Not all columns have to allow for this, though.
27 |
28 | Columns can have an image, text, or both in the column header. The
29 | contents of the cells can be instructed to link to the object on the
30 | row or the data in the cell.
31 | """
32 | SORT_DESCENDING = 0
33 | SORT_ASCENDING = 1
34 | creation_counter = 0
35 | def __init__(self, label=None, detailed_label=None,
36 | field_name=None, db_field=None,
37 | image_url=None, image_width=None, image_height=None,
38 | image_alt="", shrink=False, expand=False, sortable=False,
39 | default_sort_dir=SORT_DESCENDING, link=False,
40 | link_func=None, cell_clickable=False, css_class="", data_func=None):
41 | self.id = None
42 | self.datagrid = None
43 | self.field_name = field_name
44 | self.db_field = db_field or field_name
45 | self.label = label
46 | # self.detailed_label = detailed_label or self.label
47 | self.image_url = image_url
48 | self.image_width = image_width
49 | self.image_height = image_height
50 | self.image_alt = image_alt
51 | self.shrink = shrink
52 | self.expand = expand
53 | self.sortable = sortable
54 | self.default_sort_dir = default_sort_dir
55 | self.cell_clickable = cell_clickable
56 | self.link = link
57 | self.link_func = link_func or \
58 | (lambda x, y: self.datagrid.link_to_object(x, y))
59 | self.css_class = css_class
60 | self.data_func = data_func
61 | self.creation_counter = Column.creation_counter
62 | Column.creation_counter += 1
63 |
64 | # State
65 | self.active = False
66 | self.last = False
67 | self.width = 0
68 |
69 | def get_toggle_url(self):
70 | """
71 | Returns the URL of the current page with this column's visibility
72 | toggled.
73 | """
74 | columns = [column.id for column in self.datagrid.columns]
75 |
76 | if self.active:
77 | columns.remove(self.id)
78 | else:
79 | columns.append(self.id)
80 |
81 | return "?%scolumns=%s" % (self.get_url_params_except("columns"),
82 | ",".join(columns))
83 | toggle_url = property(get_toggle_url)
84 |
85 | def get_header(self):
86 | """
87 | Displays a sortable column header.
88 |
89 | The column header will include the current sort indicator, if it
90 | belongs in the sort list. It will also be made clickable in order
91 | to modify the sort order appropriately, if sortable.
92 | """
93 | in_sort = False
94 | sort_direction = self.SORT_DESCENDING
95 | sort_primary = False
96 | sort_url = ""
97 | unsort_url = ""
98 |
99 | if self.sortable:
100 | sort_list = self.datagrid.sort_list
101 |
102 | if sort_list:
103 | rev_column_id = "-%s" % self.id
104 | new_column_id = self.id
105 | cur_column_id = ""
106 |
107 | if self.id in sort_list:
108 | # This column is currently being sorted in
109 | # ascending order.
110 | sort_direction = self.SORT_ASCENDING
111 | cur_column_id = self.id
112 | new_column_id = rev_column_id
113 | elif rev_column_id in sort_list:
114 | # This column is currently being sorted in
115 | # descending order.
116 | sort_direction = self.SORT_DESCENDING
117 | cur_column_id = rev_column_id
118 | new_column_id = self.id
119 |
120 | if cur_column_id:
121 | in_sort = True
122 | sort_primary = (sort_list[0] == cur_column_id)
123 |
124 | if not sort_primary:
125 | # If this is not the primary column, we want to keep
126 | # the sort order intact.
127 | new_column_id = cur_column_id
128 |
129 | # Remove this column from the current location in the list
130 | # so we can move it to the front of the list.
131 | sort_list.remove(cur_column_id)
132 |
133 | # Insert the column name into the beginning of the sort list.
134 | sort_list.insert(0, new_column_id)
135 | else:
136 | # There's no sort list to begin with. Make this column
137 | # the only entry.
138 | sort_list = [self.id]
139 |
140 | # We can only support two entries in the sort list, so truncate
141 | # this.
142 | # del(sort_list[2:])
143 | del(sort_list[1:])
144 |
145 | url_prefix = "?%ssort=" % self.get_url_params_except("sort",
146 | "datagrid-id",
147 | "gridonly",
148 | "columns")
149 | unsort_url = url_prefix + ','.join(sort_list[1:])
150 | sort_url = url_prefix + ','.join(sort_list)
151 |
152 | return mark_safe(render_to_string(
153 | self.datagrid.column_header_template, {
154 | 'MEDIA_URL': settings.MEDIA_URL,
155 | 'column': self,
156 | 'in_sort': in_sort,
157 | 'sort_ascending': sort_direction == self.SORT_ASCENDING,
158 | 'sort_primary': sort_primary,
159 | 'sort_url': sort_url,
160 | 'unsort_url': unsort_url,
161 | }))
162 | header = property(get_header)
163 |
164 | def get_url_params_except(self, *params):
165 | """
166 | Utility function to return a string containing URL parameters to
167 | this page with the specified parameter filtered out.
168 | """
169 | s = ""
170 |
171 | for key in self.datagrid.request.GET:
172 | if key not in params:
173 | s += "%s=%s&" % (key, self.datagrid.request.GET[key])
174 |
175 | return s
176 |
177 | def render_cell(self, obj):
178 | """
179 | Renders the table cell containing column data.
180 | """
181 | rendered_data = self.render_data(obj)
182 | css_class = ""
183 | url = ""
184 |
185 | if self.css_class:
186 | if callable(self.css_class):
187 | css_class = self.css_class(obj)
188 | else:
189 | css_class = self.css_class
190 |
191 | if self.link:
192 | try:
193 | url = self.link_func(obj, rendered_data)
194 | except AttributeError:
195 | pass
196 |
197 | self.label = self.label or ' '.join(self.id.split('_')).title()
198 | return mark_safe(render_to_string(self.datagrid.cell_template, {
199 | 'MEDIA_URL': settings.MEDIA_URL,
200 | 'column': self,
201 | 'css_class': css_class,
202 | 'url': url,
203 | 'data': mark_safe(rendered_data)
204 | }))
205 |
206 | def render_data(self, obj):
207 | """
208 | Renders the column data to a string. This may contain HTML.
209 | """
210 | field_names = self.field_name.split('.')
211 | if len(field_names) > 1:
212 | field_name = field_names.pop(0)
213 | value = getattr(obj, field_name)
214 | if callable(value):
215 | value = value()
216 | if value is None:
217 | #NO further processing is possible, so bailout early.
218 | return value
219 | while field_names:
220 | field_name = field_names.pop(0)
221 | value = getattr(value, field_name)
222 | if callable(value):
223 | value = value()
224 | if value is None:
225 | #NO further processing is possible, so bailout early.
226 | return value
227 | else:
228 | # value = getattr(obj, self.field_name)
229 | value = getattr(obj, self.db_field)
230 | if self.data_func:
231 | value = self.data_func(value)
232 | if callable(value):
233 | return value()
234 | else:
235 | return value
236 |
237 | class NonDatabaseColumn(Column):
238 | def render_data(self, obj):
239 | if self.data_func:
240 | return self.data_func(obj)
241 | return self.label
242 |
243 | class DateTimeColumn(Column):
244 | """
245 | A column that renders a date or time.
246 | """
247 | def __init__(self, label, format=None, sortable=True, *args, **kwargs):
248 | Column.__init__(self, label, sortable=sortable, *args, **kwargs)
249 | self.format = format
250 |
251 | def render_data(self, obj):
252 | # return date(getattr(obj, self.field_name), self.format)
253 | return date(getattr(obj, self.db_field), self.format)
254 |
255 | class DateTimeSinceColumn(Column):
256 | """
257 | A column that renders a date or time relative to now.
258 | """
259 | def __init__(self, label, sortable=True, *args, **kwargs):
260 | Column.__init__(self, label, sortable=sortable, *args, **kwargs)
261 |
262 | def render_data(self, obj):
263 | # return _("%s ago") % timesince(getattr(obj, self.field_name))
264 | return _("%s ago") % timesince(getattr(obj, self.db_field))
265 |
266 |
267 | class DataGrid(object):
268 | """
269 | A representation of a list of objects, sorted and organized by
270 | columns. The sort order and column lists can be customized. allowing
271 | users to view this data however they prefer.
272 |
273 | This is meant to be subclassed for specific uses. The subclasses are
274 | responsible for defining one or more column types. It can also set
275 | one or more of the following optional variables:
276 |
277 | * 'title': The title of the grid.
278 | * 'profile_sort_field': The variable name in the user profile
279 | where the sort order can be loaded and
280 | saved.
281 | * 'profile_columns_field": The variable name in the user profile
282 | where the columns list can be loaded and
283 | saved.
284 | * 'paginate_by': The number of items to show on each page
285 | of the grid. The default is 50.
286 | * 'paginate_orphans': If this number of objects or fewer are
287 | on the last page, it will be rolled into
288 | the previous page. The default is 3.
289 | * 'page': The page to display. If this is not
290 | specified, the 'page' variable passed
291 | in the URL will be used, or 1 if that is
292 | not specified.
293 | * 'listview_template': The template used to render the list view.
294 | The default is 'datagrid/listview.html'
295 | * 'column_header_template': The template used to render each column
296 | header. The default is
297 | 'datagrid/column_header.html'
298 | * 'cell_template': The template used to render a cell of
299 | data. The default is 'datagrid/cell.html'
300 | * 'optimize_sorts': Whether or not to optimize queries when
301 | using multiple sorts. This can offattr_metaer a
302 | speed improvement, but may need to be
303 | turned off for more advanced querysets
304 | (such as when using extra()).
305 | The default is True.
306 | """
307 | def __init__(self, request, queryset, title="", extra_context={},
308 | optimize_sorts=True, listview_template='datagrid/listview.html',
309 | column_header_template='datagrid/column_header.html', cell_template='datagrid/cell.html'):
310 | self.request = request
311 | self.queryset = queryset
312 | self.rows = []
313 | self.columns = []
314 | self.all_columns = []
315 | self.db_field_map = {}
316 | self.paginator = None
317 | self.page = None
318 | self.sort_list = None
319 | self.state_loaded = False
320 | self.page_num = 0
321 | self.id = None
322 | self.extra_context = dict(extra_context)
323 | self.optimize_sorts = optimize_sorts
324 |
325 | if not hasattr(request, "datagrid_count"):
326 | request.datagrid_count = 0
327 |
328 | self.id = "datagrid-%s" % request.datagrid_count
329 | request.datagrid_count += 1
330 |
331 | # Customizable variables
332 | # self.title = title
333 | self.grid_header = title
334 | self.profile_sort_field = None
335 | self.profile_columns_field = None
336 | self.paginate_by = 10
337 | self.paginate_orphans = 3
338 | self.listview_template = listview_template
339 | self.column_header_template = column_header_template
340 | self.cell_template = cell_template
341 |
342 |
343 | for attr in dir(self):
344 | column = getattr(self, attr)
345 | if isinstance(column, Column):
346 | self.all_columns.append(column)
347 | column.datagrid = self
348 | column.id = attr
349 |
350 | # Reset the column.
351 | column.active = False
352 | column.last = False
353 | column.width = 0
354 |
355 | if not column.field_name:
356 | column.field_name = column.id
357 |
358 | if not column.db_field:
359 | column.db_field = column.field_name
360 |
361 | self.db_field_map[column.id] = column.db_field
362 |
363 | self.all_columns.sort(key=lambda x: x.creation_counter)
364 | self.columns = self.all_columns
365 |
366 | # self.default_columns = [el.label or ' '.join(el.id.split()).title() for el in self.all_columns]#TODO:FOR now
367 | # self.default_sort = self.default_columns[0]
368 | self.default_sort = self.all_columns[0].id
369 |
370 | #Get the meta fields
371 | meta = getattr(self, 'Meta', None)
372 | page_size = request.GET.get('page_size', None)
373 | if page_size:
374 | try:
375 | self.paginate_by = int(page_size)
376 | if self.paginate_by < self.paginate_orphans:
377 | #Special case, because in this case other values will result in weird result
378 | self.paginate_orphans = 0
379 | except ValueError:
380 | pass
381 |
382 | #Handle controls
383 | self.pagination_control_widget = getattr(meta, 'pagination_control_widget', False)
384 | self.get_pdf_link = getattr(meta, 'get_pdf_link', False)
385 | self.get_csv_link = getattr(meta, 'get_csv_link', False)
386 | self.filter_fields = getattr(meta, 'filter_fields', [])
387 | self.search_fields = getattr(meta, 'search_fields', [])
388 | self.filtering_options = {}
389 | if self.filter_fields:
390 | filtering_options = {}
391 | #TODO: This is very costly for large querysets so we may want to cache this, or do this in SQL
392 | for field in self.filter_fields:
393 | filtering_options[field] = set([getattr(el, field) for el in queryset])
394 | self.filtering_options = filtering_options
395 |
396 |
397 |
398 | def load_state(self):
399 | """
400 | Loads the state of the datagrid.
401 |
402 | This will retrieve the user-specified or previously stored
403 | sorting order and columns list, as well as any state a subclass
404 | may need.
405 | """
406 |
407 | if self.state_loaded:
408 | return
409 |
410 | profile_sort_list = None
411 | profile_columns_list = None
412 | profile = None
413 | profile_dirty = False
414 |
415 | # Get the saved settings for this grid in the profile. These will
416 | # work as defaults and allow us to determine if we need to save
417 | # the profile.
418 | if self.request.user.is_authenticated():
419 | try:
420 | profile = self.request.user.get_profile()
421 |
422 | if self.profile_sort_field:
423 | profile_sort_list = \
424 | getattr(profile, self.profile_sort_field, None)
425 |
426 | if self.profile_columns_field:
427 | profile_columns_list = \
428 | getattr(profile, self.profile_columns_field, None)
429 | except SiteProfileNotAvailable:
430 | pass
431 | except ObjectDoesNotExist:
432 | pass
433 |
434 |
435 | # Figure out the columns we're going to display
436 | # We're also going to calculate the column widths based on the
437 | # shrink and expand values.
438 | # colnames_str = self.request.GET.get('columns', profile_columns_list)
439 |
440 | # if colnames_str:
441 | # colnames = colnames_str.split(',')
442 | # else:
443 | colnames = self.all_columns
444 | # colnames_str = ",".join(self.default_columns)
445 |
446 | expand_columns = []
447 | normal_columns = []
448 |
449 | for colname in colnames:
450 | try:
451 | column = getattr(self, colname.id)
452 | except AttributeError:
453 | # The user specified a column that doesn't exist. Skip it.
454 | continue
455 |
456 | if column not in self.columns:
457 | self.columns.append(column)
458 | column.active = True
459 |
460 | if column.expand:
461 | # This column is requesting all remaining space. Save it for
462 | # later so we can tell how much to give it. Each expanded
463 | # column will count as two normal columns when calculating
464 | # the normal sized columns.
465 | expand_columns.append(column)
466 | elif column.shrink:
467 | # Make this as small as possible.
468 | column.width = 0
469 | else:
470 | # We'll divide the column widths equally after we've built
471 | # up the lists of expanded and normal sized columns.
472 | normal_columns.append(column)
473 |
474 | self.columns[-1].last = True
475 |
476 | # Try to figure out the column widths for each column.
477 | # We'll start with the normal sized columns.
478 | total_pct = 100
479 |
480 | # Each expanded column counts as two normal columns.
481 | normal_column_width = total_pct / (len(self.columns) +
482 | len(expand_columns))
483 |
484 | for column in normal_columns:
485 | column.width = normal_column_width
486 | total_pct -= normal_column_width
487 |
488 | if len(expand_columns) > 0:
489 | expanded_column_width = total_pct / len(expand_columns)
490 | else:
491 | expanded_column_width = 0
492 |
493 | for column in expand_columns:
494 | column.width = expanded_column_width
495 |
496 |
497 | # Now get the sorting order for the columns.
498 | sort_str = self.request.GET.get('sort', profile_sort_list)
499 |
500 | if sort_str:
501 | self.sort_list = sort_str.split(',')
502 | else:
503 | self.sort_list = [self.default_sort]
504 | sort_str = ",".join(self.sort_list)
505 |
506 |
507 | # A subclass might have some work to do for loading and saving
508 | # as well.
509 | if self.load_extra_state(profile):
510 | profile_dirty = True
511 |
512 |
513 | # Now that we have all that, figure out if we need to save new
514 | # settings back to the profile.
515 | # if profile:
516 | # if self.profile_columns_field and \
517 | # colnames_str != profile_columns_list:
518 | # setattr(profile, self.profile_columns_field, colnames_str)
519 | # profile_dirty = True
520 | #
521 | # if self.profile_sort_field and sort_str != profile_sort_list:
522 | # setattr(profile, self.profile_sort_field, sort_str)
523 | # profile_dirty = True
524 | #
525 | # if profile_dirty:
526 | # profile.save()
527 |
528 | self.state_loaded = True
529 |
530 | # Fetch the list of objects and have it ready.
531 | self.precompute_objects()
532 |
533 |
534 | def load_extra_state(self, profile):
535 | """
536 | Loads any extra state needed for this grid.
537 |
538 | This is used by subclasses that may have additional data to load
539 | and save. This should return True if any profile-stored state has
540 | changed, or False otherwise.
541 | """
542 | return False
543 |
544 | def precompute_objects(self):
545 | """
546 | Builds the queryset and stores the list of objects for use in
547 | rendering the datagrid.
548 | """
549 | query = self.queryset
550 | use_select_related = False
551 | # Generate the actual list of fields we'll be sorting by
552 | sort_list = []
553 | for sort_item in self.sort_list:
554 | if sort_item[0] == "-":
555 | base_sort_item = sort_item[1:]
556 | prefix = "-"
557 | else:
558 | base_sort_item = sort_item
559 | prefix = ""
560 |
561 | if sort_item and base_sort_item in self.db_field_map:
562 | db_field = self.db_field_map[base_sort_item]
563 | sort_list.append(prefix + db_field)
564 |
565 | # Lookups spanning tables require that we query from those
566 | # tables. In order to keep things simple, we'll just use
567 | # select_related so that we don't have to figure out the
568 | # table relationships. We only do this if we have a lookup
569 | # spanning tables.
570 | if '.' in db_field:
571 | use_select_related = True
572 |
573 | if sort_list:
574 | query = query.order_by(*sort_list)
575 | self.paginator = Paginator(query, self.paginate_by,
576 | self.paginate_orphans)
577 | page_num = self.request.GET.get('page', 1)
578 |
579 | # Accept either "last" or a valid page number.
580 | if page_num == "last":
581 | page_num = self.paginator.num_pages
582 |
583 | try:
584 | self.page = self.paginator.page(page_num)
585 | except InvalidPage:
586 | raise Http404
587 |
588 | self.rows = []
589 | self.rows_raw = []
590 | id_list = None
591 |
592 | if self.optimize_sorts and len(sort_list) > 0:
593 | # This can be slow when sorting by multiple columns. If we
594 | # have multiple items in the sort list, we'll request just the
595 | # IDs and then fetch the actual details from that.
596 | id_list = list(self.page.object_list.distinct().values_list(
597 | 'pk', flat=True))
598 |
599 | # Make sure to unset the order. We can't meaningfully order these
600 | # results in the query, as what we really want is to keep it in
601 | # the order specified in id_list, and we certainly don't want
602 | # the database to do any special ordering (possibly slowing things
603 | # down). We'll set the order properly in a minute.
604 | self.page.object_list = self.post_process_queryset(
605 | self.queryset.model.objects.filter(pk__in=id_list).order_by())
606 |
607 | if use_select_related:
608 | self.page.object_list = \
609 | self.page.object_list.select_related(depth=1)
610 |
611 | if id_list:
612 | # The database will give us the items in a more or less random
613 | # order, since it doesn't know to keep it in the order provided by
614 | # the ID list. This will place the results back in the order we
615 | # expect.
616 | index = dict([(id, pos) for (pos, id) in enumerate(id_list)])
617 | object_list = [None] * len(id_list)
618 |
619 | for obj in list(self.page.object_list):
620 | object_list[index[obj.id]] = obj
621 | else:
622 | # Grab the whole list at once. We know it won't be too large,
623 | # and it will prevent one query per row.
624 | object_list = list(self.page.object_list)
625 |
626 | for obj in object_list:
627 | self.rows.append({
628 | 'object': obj,
629 | 'cells': [column.render_cell(obj) for column in self.columns],
630 | 'data': [column.render_data(obj) for column in self.columns],
631 | })
632 |
633 | def post_process_queryset(self, queryset):
634 | """
635 | Processes a QuerySet after the initial query has been built and
636 | pagination applied. This is only used when optimizing a sort.
637 |
638 | By default, this just returns the existing queryset. Custom datagrid
639 | subclasses can override this to add additional queries (such as
640 | subqueries in an extra() call) for use in the cell renderers.
641 |
642 | When optimize_sorts is True, subqueries (using extra()) on the initial
643 | QuerySet passed to the datagrid will be stripped from the final
644 | result. This function can be used to re-add those subqueries.
645 | """
646 | return queryset
647 |
648 | def handle_search(self):
649 | if not self.search_fields:
650 | return
651 | query = self.request.GET.get('q', None)
652 | if not query:
653 | return
654 | query_criteria=Q(id=-1)
655 | for field in self.search_fields:
656 | field = field+"__icontains"
657 | query_criteria = query_criteria | Q(**{field:query})
658 | self.queryset = self.queryset.filter(query_criteria)
659 |
660 | def handle_filter(self):
661 | queryset = self.queryset
662 | if not self.filter_fields:
663 | return
664 | for field in self.filter_fields:
665 | query = self.request.GET.get(field, None)
666 | if query:
667 | self.queryset = queryset.filter(**{field: query})
668 |
669 |
670 |
671 | def render_listview(self):
672 | """
673 | Renders the standard list view of the grid.
674 |
675 | This can be called from templates.
676 | """
677 | self.handle_search()
678 | self.handle_filter()
679 | self.load_state()
680 | context = {
681 | 'datagrid': self,
682 | 'request': self.request,
683 | 'is_paginated': self.page.has_other_pages(),
684 | 'results_per_page': self.paginate_by,
685 | 'has_next': self.page.has_next(),
686 | 'has_previous': self.page.has_previous(),
687 | 'page': self.page.number,
688 | 'next': self.page.next_page_number(),
689 | 'previous': self.page.previous_page_number(),
690 | 'last_on_page': self.page.end_index(),
691 | 'first_on_page': self.page.start_index(),
692 | 'pages': self.paginator.num_pages,
693 | 'hits': self.paginator.count,
694 | 'page_range': self.paginator.page_range,
695 | 'pagination_control_widget': self.pagination_control_widget,
696 | 'get_pdf_link': self.get_pdf_link,
697 | 'get_csv_link': self.get_csv_link,
698 | 'filter_fields': self.filter_fields,
699 | 'search_fields': self.search_fields,
700 | 'filtering_options': self.filtering_options.items(),
701 | }
702 |
703 |
704 | context.update(self.extra_context)
705 |
706 | return mark_safe(render_to_string(self.listview_template,
707 | RequestContext(self.request, context)))
708 |
709 | @cache_control(no_cache=True, no_store=True, max_age=0,
710 | must_revalidate=True)
711 | def render_listview_to_response(self):
712 | """
713 | Renders the listview to a response, preventing caching in the
714 | process.
715 | """
716 | return HttpResponse(unicode(self.render_listview()))
717 |
718 | def render_to_response(self, template_name, extra_context={}):
719 | """
720 | Renders a template containing this datagrid as a context variable.
721 | """
722 | self.handle_search()
723 | self.handle_filter()
724 | self.load_state()
725 |
726 |
727 | # If the caller is requesting just this particular grid, return it.
728 | if self.request.GET.get('gridonly', False) and \
729 | self.request.GET.get('datagrid-id', None) == self.id:
730 | return self.render_listview_to_response()
731 |
732 | context = {
733 | 'datagrid': self
734 | }
735 | context.update(extra_context)
736 | context.update(self.extra_context)
737 | if self.request.GET.get('is_pdf', None):
738 | import ho.pisa as pisa
739 | file_data = render_to_string('datagrid/as_pdf.pdf', context)
740 | myfile = StringIO.StringIO()
741 | pisa.CreatePDF(file_data, myfile)
742 | myfile.seek(0)
743 | response = HttpResponse(myfile, mimetype='application/pdf')
744 | response['Content-Disposition'] = 'attachment; filename=data.pdf'
745 | return response
746 | elif self.request.GET.get('is_csv', None):
747 | file_data = render_to_string('datagrid/as_csv.csv', context)
748 | response = HttpResponse(file_data, mimetype='text.csv')
749 | response['Content-Disposition'] = 'attachment; filename=data.csv'
750 | return response
751 |
752 |
753 | return render_to_response(template_name, RequestContext(self.request,
754 | context))
755 |
756 | @staticmethod
757 | def link_to_object(obj, value):
758 | return obj.get_absolute_url()
759 |
760 | @staticmethod
761 | def link_to_value(obj, value):
762 | return value.get_absolute_url()
763 |
--------------------------------------------------------------------------------