├── .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 | [![Build Status](https://travis-ci.org/agiliq/django-datagrid.png?branch=master)](https://travis-ci.org/agiliq/django-datagrid) 2 | 3 | [![Coverage Status](https://coveralls.io/repos/agiliq/django-datagrid/badge.png)](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 | <!-- Insert your title here --> 7 | 8 | 9 | {{ datagrid.render_listview }} 10 | 11 | 12 | -------------------------------------------------------------------------------- /books/mylibrary/templates/mylibrary/simple.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | <!-- Insert your title here --> 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 | 10 | {% endfor %} 11 | 12 | 13 | 14 | {% for row in datagrid.rows %} 15 | 16 | {% for datum in row.data %} 17 | 18 | {% endfor %} 19 | 20 | {% endfor %} 21 | 22 |
8 | {{ column.label }} 9 |
{{ datum }}
23 | 24 | 25 | -------------------------------------------------------------------------------- /datagrid/templates/datagrid/cell.html: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 | 3 | {% if url %} 4 | {{data}} 5 | {% else %} 6 | {{data}} 7 | {% endif %} 8 | 9 | {% endspaceless %} 10 | -------------------------------------------------------------------------------- /datagrid/templates/datagrid/column_header.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% if column.sortable %} 4 | 5 | 6 | {% if column.label %}{{column.label}}{% endif %} 7 | {% if column.image_url %} 8 | {{column.image_alt}} 9 | {% endif %} 10 | 11 | {% if in_sort %} 12 | ({% if sort_ascending %}{% trans 15 | 16 | 17 | 18 | {% trans 19 | {% endif %} 20 | 21 | 22 | {% else %} 23 | 24 | {% if column.label %}{{column.label}}{% endif %} 25 | {% if column.image_url %} 26 | {{column.image_alt}} 27 | {% endif %} 28 | 29 | {% endif %} 30 | -------------------------------------------------------------------------------- /datagrid/templates/datagrid/datagrid.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load djblets_deco %} 3 | {% block title %}{{datagrid.title}}{% endblock %} 4 | 5 | {% block extrahead %} 6 | 7 | 8 | 9 | 10 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 | {% box %} 15 | {{datagrid.render_listview}} 16 | {% endbox %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /datagrid/templates/datagrid/get_csv_link.html: -------------------------------------------------------------------------------- 1 | Get csv -------------------------------------------------------------------------------- /datagrid/templates/datagrid/get_filter_form.html: -------------------------------------------------------------------------------- 1 |
2 | Filter list by the 3 | {% for field, options in filtering_options %} 4 | 5 | {{ field }} 6 | 7 | 12 | {% endfor %} 13 | 14 | 15 |
-------------------------------------------------------------------------------- /datagrid/templates/datagrid/get_pdf_link.html: -------------------------------------------------------------------------------- 1 | Get pdf -------------------------------------------------------------------------------- /datagrid/templates/datagrid/get_search_form.html: -------------------------------------------------------------------------------- 1 |
2 | {% for name, value in getvars %} 3 | 4 | {% endfor %} 5 | 6 | 7 | 8 |
-------------------------------------------------------------------------------- /datagrid/templates/datagrid/listview.html: -------------------------------------------------------------------------------- 1 | {% load datagrid %} 2 | 3 |
4 | 5 |
6 | {% block datagrid_title %} 7 |

{{datagrid.grid_header}}

8 | {% endblock %} 9 |
10 | 11 |
12 | {% if filter_fields %} 13 | {% get_filter_form %} 14 | {% endif %} 15 | 16 | {% if search_fields %} 17 | {% get_search_form %} 18 | {% endif %} 19 | 20 | {% if pagination_control_widget %} 21 | {% render_pagination_size_widget %} 22 | {% endif %} 23 | 24 | {% if get_pdf_link %} 25 | {% get_pdf_link %} 26 | {% endif %} 27 | 28 | {% if get_csv_link %} 29 | {% get_csv_link %} 30 | {% endif %} 31 |
32 | 33 |
34 | 35 | 36 | {% for column in datagrid.columns %} 37 | 38 | {% endfor %} 39 | 40 | 41 | 42 | 43 | {% for column in datagrid.columns %} 44 | {{column.get_header}} 45 | {% endfor %} 46 | 47 | 48 | 49 | {% if datagrid.rows %} 50 | {% for row in datagrid.rows %} 51 | 52 | {% for cell in row.cells %} 53 | {{cell}} 54 | {% endfor %} 55 | {% endfor %} 56 | {% else %} 57 | 58 | {% endif %} 59 | 60 |

We do not have any data for your selection.

61 |
62 | 63 | 64 | {% for column in datagrid.all_columns %} 65 | {% with column.toggle_url as toggle_url %} 66 | 67 | 68 | 74 | 75 | {% endwith %} 76 | {% endfor %} 77 | 78 | 79 | {% if is_paginated %} 80 | {% paginator %} 81 | {% endif %} 82 | 83 |
84 | -------------------------------------------------------------------------------- /datagrid/templates/datagrid/pagination_size_frag.html: -------------------------------------------------------------------------------- 1 |
2 | 8 | {% endfor %} 9 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /datagrid/templates/datagrid/paginator.html: -------------------------------------------------------------------------------- 1 |
2 | {% if show_first %}«{% endif %} 3 | {% if has_previous %}<{% endif %} 4 | {% for pagenum in page_numbers %} 5 | {% ifequal pagenum page %} 6 | {{pagenum}} 7 | {% else %} 8 | {{pagenum}} 9 | {% endifequal %} 10 | {% endfor %} 11 | {% if has_next %}>{% endif %} 12 | {% if show_last %}»{% endif %} 13 | {{pages}} pages 14 |
15 | -------------------------------------------------------------------------------- /datagrid/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-datagrid/0ab533b93f014b8fba6e9c4daddbcd5b7f31b9c9/datagrid/templatetags/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------