├── .gitignore
├── .travis.yml
├── AUTHORS
├── LICENSE
├── MANIFEST.in
├── README.md
├── books
├── __init__.py
├── datagrid
├── manage.py
├── mylibrary
│ ├── __init__.py
│ ├── admin.py
│ ├── grids.py
│ ├── models.py
│ ├── templates
│ │ └── mylibrary
│ │ │ ├── real.html
│ │ │ └── simple.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── requirements.txt
├── settings.py
└── urls.py
├── datagrid
├── __init__.py
├── doc.txt
├── grids.py
├── models.py
├── templates
│ └── datagrid
│ │ ├── as_csv.csv
│ │ ├── as_pdf.pdf
│ │ ├── cell.html
│ │ ├── column_header.html
│ │ ├── datagrid.html
│ │ ├── get_csv_link.html
│ │ ├── get_filter_form.html
│ │ ├── get_pdf_link.html
│ │ ├── get_search_form.html
│ │ ├── listview.html
│ │ ├── pagination_size_frag.html
│ │ └── paginator.html
├── templatetags
│ ├── __init__.py
│ └── datagrid.py
└── tests.py
├── docs
└── usage.txt
├── examples.txt
├── ez_setup.py
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | django_datagrid.egg-info/*
3 | build/*
4 | dist/*
5 | *.db
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | python:
4 | - "2.7"
5 |
6 | # command to install dependencies
7 | install:
8 | - pip install -r books/requirements.txt --use-mirrors
9 | - pip install coveralls --use-mirrors
10 |
11 | # command to run tests
12 | script:
13 | - coverage run --source="." books/manage.py test mylibrary
14 |
15 | after_success:
16 | - coveralls
17 |
--------------------------------------------------------------------------------
/AUTHORS:
--------------------------------------------------------------------------------
1 | Ashok Raavi, ashok@agiliq.com
2 | Shabda Raaj, shabda@agiliq.com
3 |
4 | Original authors from Djblets/Reviewboard.
5 |
6 | Lead Developers:
7 |
8 | * Christian Hammond
9 | * David Trowbridge
10 | * Micah Dowty
11 |
12 |
13 | Contributors:
14 |
15 | * Brad Taylor
16 | * Cory McWilliams
17 | * Hussain Bohra
18 | * Onkar Shinde
19 | * Paolo Borelli
20 | * Thilo-Alexander Ginkel
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README
2 | include LICENSE
3 | recursive-include datagrid/templates *
4 | recursive-include example *
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/agiliq/django-datagrid)
2 |
3 | [](https://coveralls.io/r/agiliq/django-datagrid)
4 |
5 | A Django based datagrid.
6 |
7 | Based on the datagrid from Review-board and [djblets](http://github.com/djblets/djblets)
8 |
--------------------------------------------------------------------------------
/books/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agiliq/django-datagrid/0ab533b93f014b8fba6e9c4daddbcd5b7f31b9c9/books/__init__.py
--------------------------------------------------------------------------------
/books/datagrid:
--------------------------------------------------------------------------------
1 | ../datagrid
--------------------------------------------------------------------------------
/books/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from django.core.management import execute_manager
3 | try:
4 | import settings # Assumed to be in the same directory.
5 | except ImportError:
6 | import sys
7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
8 | sys.exit(1)
9 |
10 | if __name__ == "__main__":
11 | execute_manager(settings)
12 |
--------------------------------------------------------------------------------
/books/mylibrary/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agiliq/django-datagrid/0ab533b93f014b8fba6e9c4daddbcd5b7f31b9c9/books/mylibrary/__init__.py
--------------------------------------------------------------------------------
/books/mylibrary/admin.py:
--------------------------------------------------------------------------------
1 | from mylibrary.models import Book
2 | from django.contrib import admin
3 |
4 | admin.site.register(Book)
5 |
6 |
--------------------------------------------------------------------------------
/books/mylibrary/grids.py:
--------------------------------------------------------------------------------
1 | from datagrid.grids import DataGrid, Column, NonDatabaseColumn
2 |
3 |
4 | class SimpleGrid(DataGrid):
5 | name = NonDatabaseColumn("Hello")
6 |
7 | class RealGrid(DataGrid):
8 | name = Column()
9 | publisher = Column()
10 | recommended_by = Column()
11 |
12 | class SortableGrid(DataGrid):
13 | name = Column(sortable = True)
14 | publisher = Column(sortable = True)
15 | recommended_by = Column(sortable = True)
--------------------------------------------------------------------------------
/books/mylibrary/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 |
4 |
5 | class Book(models.Model):
6 | name = models.CharField(max_length = 100)
7 | publisher = models.CharField(max_length = 100)
8 | published_in_year = models.IntegerField()
9 | total_in_stock = models.IntegerField()
10 | recommended_by = models.ForeignKey(User)
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/books/mylibrary/templates/mylibrary/real.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 | {{ datagrid.render_listview }}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/books/mylibrary/templates/mylibrary/simple.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 | {{ datagrid.render_listview }}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/books/mylibrary/tests.py:
--------------------------------------------------------------------------------
1 | """
2 | This file demonstrates two different styles of tests (one doctest and one
3 | unittest). These will both pass when you run "manage.py test".
4 |
5 | Replace these with more appropriate tests for your application.
6 | """
7 |
8 | from django.test import TestCase
9 |
10 | class SimpleTest(TestCase):
11 | def test_basic_addition(self):
12 | """
13 | Tests that 1 + 1 always equals 2.
14 | """
15 | self.failUnlessEqual(1 + 1, 2)
16 |
17 | __test__ = {"doctest": """
18 | Another way to test that 1 + 1 is equal to 2.
19 |
20 | >>> 1 + 1 == 2
21 | True
22 | """}
23 |
24 |
--------------------------------------------------------------------------------
/books/mylibrary/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import *
2 |
3 |
4 |
5 | urlpatterns = patterns('mylibrary.views',
6 | url("simple/$", "simple"),
7 | url("real/$", "real"),
8 | url("sortable/$", "sortable"),
9 |
10 | )
11 |
--------------------------------------------------------------------------------
/books/mylibrary/views.py:
--------------------------------------------------------------------------------
1 | from mylibrary.grids import SimpleGrid, SortableGrid, RealGrid
2 | from mylibrary.models import Book
3 |
4 | def simple(request):
5 | books = Book.objects.all()
6 | return SimpleGrid(request, books).render_to_response("mylibrary/simple.html")
7 |
8 |
9 | def real(request):
10 | books = Book.objects.all()
11 | return RealGrid(request, books).render_to_response("mylibrary/real.html")
12 |
13 | def sortable(request):
14 | books = Book.objects.all()
15 | return SortableGrid(request, books).render_to_response("mylibrary/real.html")
--------------------------------------------------------------------------------
/books/requirements.txt:
--------------------------------------------------------------------------------
1 | Django==1.5.1
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/books/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import *
2 | from django.contrib import admin
3 |
4 |
5 | admin.autodiscover()
6 |
7 | urlpatterns = patterns('',
8 | (r'^books/', include('mylibrary.urls')),
9 |
10 | (r'^admin/', include(admin.site.urls)),
11 | )
12 |
--------------------------------------------------------------------------------
/datagrid/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agiliq/django-datagrid/0ab533b93f014b8fba6e9c4daddbcd5b7f31b9c9/datagrid/__init__.py
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/datagrid/models.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agiliq/django-datagrid/0ab533b93f014b8fba6e9c4daddbcd5b7f31b9c9/datagrid/models.py
--------------------------------------------------------------------------------
/datagrid/templates/datagrid/as_csv.csv:
--------------------------------------------------------------------------------
1 | {% for column in datagrid.columns %}"{{ column.label }}",{% endfor %}
2 | {% for row in datagrid.rows %}{% for datum in row.data %}"{{ datum }}",{% endfor %}
3 | {% endfor %}
4 |
--------------------------------------------------------------------------------
/datagrid/templates/datagrid/as_pdf.pdf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% for column in datagrid.columns %}
7 |
8 | {{ column.label }}
9 |
10 | {% endfor %}
11 |
12 |
13 |
14 | {% for row in datagrid.rows %}
15 |