├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs └── index.html ├── example ├── __init__.py ├── app │ ├── __init__.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20150328_1648.py │ │ ├── 0003_person_married.py │ │ └── __init__.py │ ├── models.py │ ├── tables.py │ ├── tests.py │ └── views.py ├── db.sqlite3 ├── manage.py ├── settings.py ├── templates │ ├── buttons_table.html │ └── index.html ├── urls.py └── wsgi.py ├── setup.py └── table ├── __init__.py ├── columns ├── __init__.py ├── base.py ├── calendarcolumn.py ├── checkboxcolumn.py ├── datetimecolumn.py ├── imagecolumn.py ├── linkcolumn.py └── sequencecolumn.py ├── forms.py ├── models.py ├── static └── table │ ├── css │ └── datatable.bootstrap.css │ ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff │ ├── images │ ├── Sorting icons.psd │ ├── back_disabled.png │ ├── back_enabled.png │ ├── back_enabled_hover.png │ ├── favicon.ico │ ├── forward_disabled.png │ ├── forward_enabled.png │ ├── forward_enabled_hover.png │ ├── sort_asc.png │ ├── sort_asc_disabled.png │ ├── sort_both.png │ ├── sort_desc.png │ └── sort_desc_disabled.png │ └── js │ ├── bootstrap.dataTables.js │ ├── dataTables.fixedColumns.min.js │ ├── jquery.browser.min.js │ └── jquery.dataTables.min.js ├── tables.py ├── templates └── table │ ├── simple_table.html │ └── table.html ├── templatetags ├── __init__.py └── table_tags.py ├── tests ├── __init__.py ├── test_columns.py ├── test_tables.py ├── test_utils.py ├── test_views.py └── test_widgets.py ├── urls.py ├── utils.py ├── views.py └── widgets.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | .settings 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | env: 5 | - DJANGO_VERSION=1.5 6 | - DJANGO_VERSION=1.6 7 | - DJANGO_VERSION=1.7 8 | - DJANGO_VERSION=1.8 9 | - DJANGO_VERSION=1.9 10 | - DJANGO_VERSION=1.10 11 | - DJANGO_VERSION=1.11 12 | install: 13 | - pip install -q Django==$DJANGO_VERSION 14 | script: 15 | - cd example 16 | - python -Wall manage.py test table 17 | 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.2.1 2 | * Fix error about loading ajax data when serving requests with multiple processes in production env. 3 | 4 | ## v0.2.0 5 | * Added compatibility with Python 3 6 | * Make checkboxfield related to object field. 7 | * Added ImageColumn 8 | 9 | ## v0.1.5 10 | 11 | * PyPi support 12 | 13 | ## v0.1.4 14 | * Fix bug about escape issue for ajax data source. 15 | 16 | ## v0.1.3 17 | * Fix bug about sorting field across different relations. 18 | 19 | ## v0.1.2 20 | * New columns: 21 | 22 | * SequenceColumn 23 | * DaysColumn 24 | * WeeksColumn 25 | * MonthColumn 26 | * CalendarColumn 27 | * CheckboxColumn 28 | 29 | * New column options: 30 | 31 | * space 32 | 33 | * Refactoring table module 34 | * Support subclass table 35 | 36 | ## v0.1.1 37 | 38 | * New column: 39 | 40 | * DatetimeColumn 41 | 42 | * New options to disable widgets 43 | 44 | * search 45 | * info 46 | * pagination 47 | * length_menu 48 | * ext_button 49 | 50 | * Fix bug about filtering href columns, ignore text of HTML tag. 51 | 52 | ## v0.1.0 53 | 54 | First commit for codebase. 55 | 56 | ## v0.3.0 57 | 58 | * Deprecate table option `ext_button`. 59 | * Make table template inheritable. 60 | 61 | ## v.0.3.1 62 | * Fix bug about template context, campatible with Django 1.10 & 1.11. 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Qiushi Guo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do 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.md 2 | recursive-include table/templates * 3 | recursive-include table/static * 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-datatable 2 | ================ 3 | 4 | |Build Status| |PyPI| 5 | 6 | .. figure:: https://www.shymonk.com/django-datatable/static/django_datatable_example.png 7 | :alt: preview 8 | 9 | `online demo `__ 10 | 11 | Overview 12 | -------- 13 | 14 | django-datatable is a simple Django app to organize data in tabular 15 | form based on `datatable `__ and 16 | `bootstrap `__. 17 | 18 | It is worth mentioning that the design of this project makes reference 19 | to `django-table2 `__ 20 | and is mainly for the purpose of learning. I really appreciate anyone 21 | making a pull-request to improve it. 22 | 23 | Requirements 24 | ------------ 25 | 26 | - Python 2.x 27 | 28 | - jQuery 1.6+ 29 | 30 | - Django 1.5+ 31 | 32 | - Bootstrap 3.0 33 | 34 | Quick start 35 | ----------- 36 | 37 | - Setup Django-datatable application in Python environment: 38 | 39 | :: 40 | 41 | $ pip install django-datatable 42 | 43 | - Define a simple model named Person: 44 | 45 | :: 46 | 47 | # example/app/models.py 48 | class Person(models.Model): 49 | name = models.CharField(max_length=100) 50 | 51 | - Add "table" to your INSTALLED\_APPS setting like this: 52 | 53 | :: 54 | 55 | INSTALLED_APPS = ( 56 | ..., 57 | 'table', 58 | ) 59 | 60 | - Add some data so you have something to display in the table. Now 61 | define a PersonTable class without any options in the table file. 62 | 63 | :: 64 | 65 | # example/app/tables.py 66 | from models import Person 67 | from table import Table 68 | from table.columns import Column 69 | 70 | class PersonTable(Table): 71 | id = Column(field='id') 72 | name = Column(field='name') 73 | class Meta: 74 | model = Person 75 | 76 | And pass a table instance to the view. 77 | 78 | :: 79 | 80 | # example/app/views.py 81 | from django.shortcuts import render 82 | from app.tables import PersonTable 83 | 84 | def people(request): 85 | people = PersonTable() 86 | return render(request, "index.html", {'people': people}) 87 | 88 | - Finally, implement the template: 89 | 90 | :: 91 | 92 | {# example/templates/index.html} 93 | {% load static %} 94 | {% load table_tags %} 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | person 105 | 106 | 107 |
108 |

people

109 |
110 | {% render_table people %} 111 |
112 | 113 | 114 | 115 | Tag 116 | --- 117 | 118 | Render the whole table by simple tag ``{% render_table %}``, pass 119 | ``Table`` instance as single argument. 120 | 121 | :: 122 | 123 | {% render_table table %} 124 | 125 | DataSource 126 | ---------- 127 | 128 | Model 129 | ````` 130 | 131 | Uses a django MTV model as table data source, and queries all data in 132 | database by default. See **model** in table options for details. 133 | 134 | QuerySet 135 | ```````` 136 | 137 | Similiar to **Model**, but pass queryset when you initialize the table 138 | instance instead of defining model option. Basically, it is used to 139 | filter or sort data you want to display in table. 140 | 141 | :: 142 | 143 | Models: 144 | 145 | # models.py 146 | class Person(models.Model): 147 | name = models.CharField(max_length=100) 148 | 149 | Tables: 150 | 151 | # tables.py 152 | from models import Person 153 | from table import Table 154 | from table.columns import Column 155 | 156 | class PersonTable(Table): 157 | id = Column(field='id') 158 | name = Column(field='name') 159 | 160 | Views: 161 | 162 | # views.py 163 | from django.shortcuts import render 164 | from models import Person 165 | from app.tables import PersonTable 166 | 167 | def people(request): 168 | people = PersonTable(Person.objects.all()) 169 | return render(request, "index.html", {'people': people}) 170 | 171 | Dict-List 172 | ````````` 173 | 174 | Use a list of dictionaries as table data source. Fields declared in 175 | columns correspond to the dictionary keys. 176 | 177 | :: 178 | 179 | Tables: 180 | 181 | # tables.py 182 | from table import Table 183 | from table.columns import Column 184 | 185 | class PersonTable(Table): 186 | id = Column(field='id') 187 | name = Column(field='name') 188 | 189 | Views: 190 | 191 | # views.py 192 | from django.shortcuts import render 193 | from app.tables import PersonTable 194 | 195 | def people(request): 196 | data = [{'id': 1, 'name': 'John'}, {'id': 2, 'name': 'Tom'}] 197 | people = PersonTable(data) 198 | return render(request, "index.html", {'people': people}) 199 | 200 | Built-in Ajax 201 | ````````````` 202 | 203 | For large amounts of data, loading them on front-end entirely is 204 | impossible. So, django-table provides a simle option 'ajax' to load data 205 | from the server-side asynchronously. 206 | 207 | Note that once toggling ``ajax``, the ``model`` option is necessary. 208 | Django-table will do paging/searching/sorting based on 209 | ``ModelClass.objects.all()``. 210 | 211 | :: 212 | 213 | Urls: 214 | 215 | # urls.py 216 | urlpatterns = patterns('', 217 | url(r'^table/', include(table.urls')), 218 | ) 219 | 220 | Tables: 221 | 222 | # tables.py 223 | from table import Table 224 | from table.columns import Column 225 | 226 | class PersonTable(Table): 227 | id = Column(field='id') 228 | name = Column(field='name') 229 | 230 | class Meta: 231 | model = Person 232 | ajax = True 233 | 234 | Custom Ajax 235 | ``````````` 236 | 237 | If you want to customize base data, use ``ajax_source`` option and 238 | implement your own Class-based View by subclassing ``FeedDataView``. 239 | 240 | :: 241 | 242 | Tables: 243 | 244 | # tables.py 245 | class PersonTable(Table): 246 | id = Column(field='id') 247 | name = Column(field='name') 248 | 249 | class Meta: 250 | model = Person 251 | ajax = True 252 | ajax_source = reverse_lazy('table_data') 253 | 254 | Urls: 255 | 256 | # urls.py 257 | urlpatterns = patterns('', 258 | url(r'^table/data/$', MyDataView.as_view(), name='table_data'), 259 | ) 260 | 261 | Views: 262 | 263 | # views.py 264 | from table.views import FeedDataView 265 | from app.tables import PersonTable 266 | 267 | class MyDataView(FeedDataView): 268 | 269 | token = PersonTable.token 270 | 271 | def get_queryset(self): 272 | return super(MyDataView, self).get_queryset().filter(id__gt=5) 273 | 274 | Columns 275 | ------- 276 | 277 | - Column 278 | 279 | - Link Column 280 | 281 | - Datetime Column 282 | 283 | - Checkbox Column 284 | 285 | - Sequence Column 286 | 287 | - Calendar Column 288 | 289 | Widgets 290 | ------- 291 | 292 | - search-box 293 | 294 | - info-label 295 | 296 | - pagination 297 | 298 | - length-menu 299 | 300 | - exten-button(deprecated) 301 | 302 | API Reference 303 | ------------- 304 | 305 | - `wiki `__ 306 | 307 | .. |Build Status| image:: https://travis-ci.org/shymonk/django-datatable.svg?branch=master 308 | :target: https://travis-ci.org/shymonk/django-datatable 309 | .. |PyPI| image:: https://img.shields.io/pypi/v/django-datatable.png 310 | :target: https://pypi.python.org/pypi/django-datatable 311 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/docs/index.html -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/example/__init__.py -------------------------------------------------------------------------------- /example/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/example/app/__init__.py -------------------------------------------------------------------------------- /example/app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Person', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('name', models.CharField(max_length=100, verbose_name=b'full name')), 18 | ], 19 | options={ 20 | }, 21 | bases=(models.Model,), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /example/app/migrations/0002_auto_20150328_1648.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('app', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Organization', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 18 | ('name', models.CharField(max_length=100, verbose_name=b'NAME')), 19 | ], 20 | options={ 21 | }, 22 | bases=(models.Model,), 23 | ), 24 | migrations.AddField( 25 | model_name='person', 26 | name='organization', 27 | field=models.ForeignKey(blank=True, to='app.Organization', null=True, on_delete=models.CASCADE), 28 | preserve_default=True, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /example/app/migrations/0003_person_married.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-01-21 14:02 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('app', '0002_auto_20150328_1648'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='person', 17 | name='married', 18 | field=models.BooleanField(default=False, verbose_name=b'married'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /example/app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/example/app/migrations/__init__.py -------------------------------------------------------------------------------- /example/app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Organization(models.Model): 5 | name = models.CharField(verbose_name="NAME", max_length=100) 6 | 7 | 8 | class Person(models.Model): 9 | name = models.CharField(verbose_name="full name", max_length=100) 10 | organization = models.ForeignKey(Organization, null=True, blank=True, on_delete=models.CASCADE) 11 | married = models.BooleanField(verbose_name="married", default=False) 12 | -------------------------------------------------------------------------------- /example/app/tables.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from datetime import date 4 | import django 5 | 6 | if django.VERSION >= (1, 10): 7 | from django.urls import reverse_lazy 8 | else: 9 | from django.core.urlresolvers import reverse_lazy 10 | 11 | from table.columns import Column 12 | from table.columns.calendarcolumn import CalendarColumn 13 | from table.columns.sequencecolumn import SequenceColumn 14 | from table.columns.imagecolumn import ImageColumn 15 | from table.columns.linkcolumn import LinkColumn, Link, ImageLink 16 | from table.columns.checkboxcolumn import CheckboxColumn 17 | from table import Table 18 | from table.utils import A 19 | 20 | from app.models import Person 21 | 22 | 23 | class ModelTable(Table): 24 | id = Column(field='id', header='#') 25 | name = Column(field='name', header='NAME') 26 | 27 | class Meta: 28 | model = Person 29 | 30 | 31 | class AjaxTable(Table): 32 | id = Column(field='id', header='#') 33 | name = Column(field='name', header='NAME') 34 | organization = Column(field='organization.name', header='ORG') 35 | 36 | class Meta: 37 | model = Person 38 | ajax = True 39 | 40 | 41 | class AjaxSourceTable(Table): 42 | id = Column(field='id', header='#') 43 | name = Column(field='name', header='NAME') 44 | 45 | class Meta: 46 | model = Person 47 | ajax = True 48 | ajax_source = reverse_lazy('ajax_source_api') 49 | 50 | 51 | class SequenceColumnTable(Table): 52 | id = Column(field='id', header='#') 53 | seq = SequenceColumn(field='calendar', headers=["A", "B", "C", "D", "E"]) 54 | 55 | 56 | class CalendarColumnTable(Table): 57 | id = Column(field='id', header='#', header_attrs={'rowspan': '3'}) 58 | name = Column(field='name', header='NAME', header_attrs={'rowspan': '3'}) 59 | calendar = CalendarColumn(field='calendar', start_date=date(2014, 4, 27), end_date=date(2014, 5, 9)) 60 | 61 | 62 | image_url = 'https://cdn0.iconfinder.com/data/icons/users-android-l-lollipop-icon-pack/24/user-32.png' 63 | 64 | 65 | class LinkColumnTable(Table): 66 | id = Column(field='id', header='#') 67 | name = LinkColumn(header='NAME', links=[ 68 | Link(viewname='user_profile', args=(A('id'),), text=A('name'))]) 69 | avatar = LinkColumn(header='AVATAR', links=[ 70 | ImageLink(viewname='user_profile', args=(A('id'),), image=image_url, image_title='avatar')]) 71 | # logo = ImageColumn(field='logo.url', header='Logo Image', image_title='logo') 72 | 73 | class Meta: 74 | model = Person 75 | 76 | 77 | class CheckboxColumnTable(Table): 78 | id = Column(field='id', header='#') 79 | name = Column(field='name', header='NAME') 80 | married = CheckboxColumn(field='married', header='MARRIED') 81 | 82 | class Meta: 83 | model = Person 84 | 85 | 86 | class ButtonsExtensionTable(Table): 87 | id = Column(field='id', header='#') 88 | name = Column(field='name', header='NAME') 89 | organization = Column(field='organization.name', header='ORGANIZATION') 90 | 91 | class Meta: 92 | model = Person 93 | template_name = 'buttons_table.html' 94 | -------------------------------------------------------------------------------- /example/app/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /example/app/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from django.shortcuts import render 5 | 6 | from table.views import FeedDataView 7 | 8 | from app.tables import ( 9 | ModelTable, AjaxTable, AjaxSourceTable, 10 | CalendarColumnTable, SequenceColumnTable, 11 | LinkColumnTable, CheckboxColumnTable, 12 | ButtonsExtensionTable 13 | ) 14 | 15 | 16 | def base(request): 17 | table = ModelTable() 18 | return render(request, "index.html", {'people': table}) 19 | 20 | 21 | def ajax(request): 22 | table = AjaxTable() 23 | return render(request, "index.html", {'people': table}) 24 | 25 | 26 | def ajax_source(request): 27 | table = AjaxSourceTable() 28 | return render(request, "index.html", {'people': table}) 29 | 30 | 31 | class Foo(object): 32 | def __init__(self, id, name, calendar): 33 | self.id = id 34 | self.name = name 35 | self.calendar = calendar 36 | 37 | 38 | def sequence_column(request): 39 | data = [ 40 | Foo(1, 'A', [1, 2, 3, 4, 5]), 41 | Foo(2, 'B', [1, 2, 3, 4, 5]), 42 | Foo(3, 'C', [1, 2, 3, 4, 5]) 43 | ] 44 | table = SequenceColumnTable(data) 45 | return render(request, "index.html", {'people': table}) 46 | 47 | 48 | def calendar_column(request): 49 | data = [ 50 | Foo(1, 'A', range(1, 14)), 51 | Foo(2, 'B', range(1, 14)), 52 | Foo(3, 'C', range(1, 14)) 53 | ] 54 | table = CalendarColumnTable(data) 55 | return render(request, "index.html", {'people': table}) 56 | 57 | 58 | def link_column(request): 59 | table = LinkColumnTable() 60 | return render(request, "index.html", {'people': table}) 61 | 62 | 63 | def checkbox_column(request): 64 | table = CheckboxColumnTable() 65 | return render(request, "index.html", {'people': table}) 66 | 67 | 68 | def buttons_extension(request): 69 | table = ButtonsExtensionTable() 70 | return render(request, "index.html", {'people': table}) 71 | 72 | def user_profile(request, uid): 73 | from app.models import Person 74 | from django.http import HttpResponse 75 | from django.shortcuts import get_object_or_404 76 | person = get_object_or_404(Person, pk=uid) 77 | return HttpResponse("User %s" % person.name) 78 | 79 | 80 | class MyDataView(FeedDataView): 81 | 82 | token = AjaxSourceTable.token 83 | 84 | def get_queryset(self): 85 | return super(MyDataView, self).get_queryset().filter(id__gt=5) 86 | -------------------------------------------------------------------------------- /example/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/example/db.sqlite3 -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for table project. 2 | import os 3 | import sys 4 | 5 | 6 | # make table package importable 7 | sys.path.append('..') 8 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | DEBUG = True 11 | TEMPLATE_DEBUG = False 12 | 13 | ADMINS = ( 14 | # ('Your Name', 'your_email@example.com'), 15 | ) 16 | 17 | MANAGERS = ADMINS 18 | 19 | DATABASES = { 20 | 'default': { 21 | 'ENGINE': 'django.db.backends.sqlite3', 22 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 23 | } 24 | } 25 | 26 | # Local time zone for this installation. Choices can be found here: 27 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 28 | # although not all choices may be available on all operating systems. 29 | # In a Windows environment this must be set to your system time zone. 30 | TIME_ZONE = 'Asia/Shanghai' 31 | 32 | # Language code for this installation. All choices can be found here: 33 | # http://www.i18nguy.com/unicode/language-identifiers.html 34 | LANGUAGE_CODE = 'en-us' 35 | 36 | SITE_ID = 1 37 | 38 | # If you set this to False, Django will make some optimizations so as not 39 | # to load the internationalization machinery. 40 | USE_I18N = True 41 | 42 | # If you set this to False, Django will not format dates, numbers and 43 | # calendars according to the current locale. 44 | USE_L10N = True 45 | 46 | # If you set this to False, Django will not use timezone-aware datetimes. 47 | USE_TZ = True 48 | 49 | # Absolute filesystem path to the directory that will hold user-uploaded files. 50 | # Example: "/home/media/media.lawrence.com/media/" 51 | MEDIA_ROOT = '' 52 | 53 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 54 | # trailing slash. 55 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 56 | MEDIA_URL = '' 57 | 58 | # Absolute path to the directory static files should be collected to. 59 | # Don't put anything in this directory yourself; store your static files 60 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 61 | # Example: "/home/media/media.lawrence.com/static/" 62 | STATIC_ROOT = os.path.join(os.path.dirname(__file__), 'static').replace('\\', '/') 63 | 64 | # URL prefix for static files. 65 | # Example: "http://media.lawrence.com/static/" 66 | STATIC_URL = '/static/' 67 | 68 | # Additional locations of static files 69 | STATICFILES_DIRS = ( 70 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 71 | # Always use forward slashes, even on Windows. 72 | # Don't forget to use absolute paths, not relative paths. 73 | ) 74 | 75 | # List of finder classes that know how to find static files in 76 | # various locations. 77 | STATICFILES_FINDERS = ( 78 | 'django.contrib.staticfiles.finders.FileSystemFinder', 79 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 80 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 81 | ) 82 | 83 | # Make this unique, and don't share it with anybody. 84 | SECRET_KEY = 'sq9c#o&a0wyo%iv8gicf=3!eb=r9k9!lm!oz1150+=7g@-5i*^' 85 | 86 | MIDDLEWARE = [ 87 | 'django.middleware.common.CommonMiddleware', 88 | 'django.contrib.sessions.middleware.SessionMiddleware', 89 | 'django.middleware.csrf.CsrfViewMiddleware', 90 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 91 | 'django.contrib.messages.middleware.MessageMiddleware', 92 | # Uncomment the next line for simple clickjacking protection: 93 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 94 | ] 95 | 96 | ROOT_URLCONF = 'urls' 97 | 98 | # Python dotted path to the WSGI application used by Django's runserver. 99 | WSGI_APPLICATION = 'wsgi.application' 100 | 101 | TEMPLATES = [ 102 | { 103 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 104 | 'APP_DIRS': True, 105 | 'DIRS': [ 106 | os.path.join(os.path.dirname(__file__), 'templates').replace('\\','/'), 107 | ], 108 | 'OPTIONS': { 109 | 'context_processors': [ 110 | 'django.contrib.auth.context_processors.auth', 111 | 'django.template.context_processors.debug', 112 | 'django.template.context_processors.i18n', 113 | 'django.template.context_processors.media', 114 | 'django.template.context_processors.static', 115 | 'django.template.context_processors.tz', 116 | 'django.contrib.messages.context_processors.messages', 117 | ], 118 | }, 119 | }, 120 | ] 121 | 122 | INSTALLED_APPS = ( 123 | 'django.contrib.auth', 124 | 'django.contrib.contenttypes', 125 | 'django.contrib.sessions', 126 | 'django.contrib.sites', 127 | 'django.contrib.messages', 128 | 'django.contrib.staticfiles', 129 | 'app', 130 | 'table', 131 | # Uncomment the next line to enable the admin: 132 | # 'django.contrib.admin', 133 | # Uncomment the next line to enable admin documentation: 134 | # 'django.contrib.admindocs', 135 | ) 136 | 137 | # A sample logging configuration. The only tangible logging 138 | # performed by this configuration is to send an email to 139 | # the site admins on every HTTP 500 error when DEBUG=False. 140 | # See http://docs.djangoproject.com/en/dev/topics/logging for 141 | # more details on how to customize your logging configuration. 142 | LOGGING = { 143 | 'version': 1, 144 | 'disable_existing_loggers': False, 145 | 'filters': { 146 | 'require_debug_false': { 147 | '()': 'django.utils.log.RequireDebugFalse' 148 | } 149 | }, 150 | 'handlers': { 151 | 'mail_admins': { 152 | 'level': 'ERROR', 153 | 'filters': ['require_debug_false'], 154 | 'class': 'django.utils.log.AdminEmailHandler' 155 | } 156 | }, 157 | 'loggers': { 158 | 'django.request': { 159 | 'handlers': ['mail_admins'], 160 | 'level': 'ERROR', 161 | 'propagate': True, 162 | }, 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /example/templates/buttons_table.html: -------------------------------------------------------------------------------- 1 | {% extends "table/table.html" %} 2 | 3 | {% block extra_css %} 4 | 5 | {% endblock extra_css %} 6 | 7 | {% block extra_js %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% endblock extra_js %} 15 | 16 | {% block table_dom %} 17 | "dom": "<'row'<'col-sm-9 col-md-9 col-lg-9'B><'col-sm-3 col-md-3 col-lg-3'f>>rt<'row'<'col-sm-3 col-md-3 col-lg-3'i><'col-sm-6 col-md-6 col-lg-6 col-sm-offset-2 col-md-offset-2 col-lg-offset-2'p><'col-sm-1 col-md-1 col-lg-1'l>>", 18 | {% endblock %} 19 | {% block table_extra_options %} 20 | "buttons": ['copy', 'excel', 'pdf'], 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /example/templates/index.html: -------------------------------------------------------------------------------- 1 | {% load table_tags %} 2 | 3 | 4 | 5 | 6 | 7 | person 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 58 | 59 |
60 | {% render_table people %} 61 |
62 | 63 | 64 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from django.conf.urls import include, url 4 | 5 | import app.views 6 | 7 | 8 | urlpatterns = [ 9 | url(r'^$', app.views.base, name='base'), 10 | url(r'^datasource/ajax/$', app.views.ajax, name='ajax'), 11 | url(r'^datasource/ajaxsource/$', app.views.ajax_source, name='ajax_source'), 12 | url(r'^datasource/ajaxsource/api/$', app.views.MyDataView.as_view(), name='ajax_source_api'), 13 | 14 | url(r'^column/sequence/$', app.views.sequence_column, name='sequence_column'), 15 | url(r'^column/calendar/$', app.views.calendar_column, name='calendar_column'), 16 | url(r'^column/link/$', app.views.link_column, name='link_column'), 17 | url(r'^column/checkbox/$', app.views.checkbox_column, name='checkbox_column'), 18 | 19 | url(r'^extensions/buttons/$', app.views.buttons_extension, name='buttons_extension'), 20 | 21 | url(r'^user/(\d+)/$', app.views.user_profile, name='user_profile'), 22 | url(r'^table/', include('table.urls')), 23 | ] 24 | -------------------------------------------------------------------------------- /example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for table project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "table.settings") 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | from django.core.wsgi import get_wsgi_application 24 | application = get_wsgi_application() 25 | 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from setuptools import setup, find_packages 4 | 5 | 6 | setup( 7 | name='django-datatable', 8 | version='0.3.1', 9 | author='shymonk', 10 | author_email='hellojohn201@gmail.com', 11 | url='https://github.com/shymonk/django-datatable', 12 | description='A simple Django app to origanize data in tabular form.', 13 | long_description=open('README.rst').read(), 14 | packages=find_packages(exclude=['test*', 'example*']), 15 | include_package_data=True, 16 | zip_safe=False, 17 | install_requires=["django>=1.5"], 18 | license='MIT License', 19 | classifiers=[ 20 | 'Development Status :: 3 - Alpha', 21 | 'Environment :: Web Environment', 22 | 'Framework :: Django', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python', 27 | 'Programming Language :: Python :: 2', 28 | 'Programming Language :: Python :: 2.7', 29 | 'Topic :: Internet :: WWW/HTTP', 30 | 'Topic :: Software Development :: Libraries', 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /table/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | 5 | from .tables import Table, TableData # NOQA 6 | -------------------------------------------------------------------------------- /table/columns/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from .base import Column, BoundColumn # NOQA 4 | from .linkcolumn import LinkColumn, Link, ImageLink # NOQA 5 | from .datetimecolumn import DatetimeColumn # NOQA 6 | from .calendarcolumn import MonthsColumn, WeeksColumn, DaysColumn, CalendarColumn # NOQA 7 | from .sequencecolumn import SequenceColumn # NOQA 8 | from .checkboxcolumn import CheckboxColumn # NOQA 9 | -------------------------------------------------------------------------------- /table/columns/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from django.utils.html import escape 4 | 5 | from table.utils import Accessor, AttributesDict 6 | 7 | 8 | class Column(object): 9 | """ Represents a single column. 10 | """ 11 | 12 | instance_order = 0 13 | 14 | def __init__(self, field=None, header=None, attrs=None, header_attrs=None, 15 | header_row_order=0, sortable=True, searchable=True, safe=True, 16 | visible=True, space=True): 17 | self.field = field 18 | self.attrs = attrs or {} 19 | self.sortable = sortable 20 | self.searchable = searchable 21 | self.safe = safe 22 | self.visible = visible 23 | self.space = space 24 | self.header = ColumnHeader(header, header_attrs, header_row_order) 25 | 26 | self.instance_order = Column.instance_order 27 | Column.instance_order += 1 28 | 29 | def __str__(self): 30 | return self.header.text 31 | 32 | def render(self, obj): 33 | text = Accessor(self.field).resolve(obj) 34 | return escape(text) 35 | 36 | 37 | class BoundColumn(object): 38 | """ A run-time version of Column. The difference between 39 | BoundColumn and Column is that BoundColumn objects include the 40 | relationship between a Column and a object. In practice, this 41 | means that a BoundColumn knows the "field value" given to the 42 | Column when it was declared on the Table. 43 | """ 44 | def __init__(self, obj, column): 45 | self.obj = obj 46 | self.column = column 47 | self.base_attrs = column.attrs.copy() 48 | 49 | # copy non-object-related attributes to self directly 50 | self.field = column.field 51 | self.sortable = column.sortable 52 | self.searchable = column.searchable 53 | self.safe = column.safe 54 | self.visible = column.visible 55 | self.header = column.header 56 | 57 | @property 58 | def html(self): 59 | text = self.column.render(self.obj) 60 | if text is None: 61 | return '' 62 | else: 63 | return text 64 | 65 | @property 66 | def attrs(self): 67 | attrs = {} 68 | for attr_name, attr in self.base_attrs.items(): 69 | if callable(attr): 70 | attrs[attr_name] = attr(self.obj, self.field) 71 | elif isinstance(attr, Accessor): 72 | attrs[attr_name] = attr.resolve(self.obj) 73 | else: 74 | attrs[attr_name] = attr 75 | return AttributesDict(attrs).render() 76 | 77 | 78 | class ColumnHeader(object): 79 | def __init__(self, text=None, attrs=None, row_order=0): 80 | self.text = text 81 | self.base_attrs = attrs or {} 82 | self.row_order = row_order 83 | 84 | @property 85 | def attrs(self): 86 | return AttributesDict(self.base_attrs).render() 87 | -------------------------------------------------------------------------------- /table/columns/calendarcolumn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import calendar 4 | from datetime import timedelta 5 | 6 | from table.columns.base import Column 7 | from table.columns.sequencecolumn import SequenceColumn 8 | 9 | 10 | class DaysColumn(SequenceColumn): 11 | def __init__(self, field=None, start_date=None, end_date=None, **kwargs): 12 | total_days = (end_date - start_date).days + 1 13 | headers = [(start_date + timedelta(day)).strftime("%d") 14 | for day in range(total_days)] 15 | super(DaysColumn, self).__init__(field, headers, **kwargs) 16 | 17 | 18 | class WeeksColumn(SequenceColumn): 19 | WEEK_NAME = calendar.day_abbr 20 | 21 | def __init__(self, field=None, start_date=None, end_date=None, **kwargs): 22 | total_days = (end_date - start_date).days + 1 23 | headers = [self.WEEK_NAME[(start_date + timedelta(day)).weekday()] 24 | for day in range(total_days)] 25 | super(WeeksColumn, self).__init__(field, headers, **kwargs) 26 | 27 | 28 | class MonthsColumn(SequenceColumn): 29 | MONTH_NAME = calendar.month_name[1:] 30 | 31 | def __init__(self, field=None, start_date=None, end_date=None, **kwargs): 32 | delta_year = end_date.year - start_date.year 33 | delta_month = end_date.month - start_date.month 34 | total_months = delta_year * 12 + delta_month + 1 35 | headers = [self.MONTH_NAME[(start_date.month + month - 1) % 12] 36 | for month in range(total_months)] 37 | super(MonthsColumn, self).__init__(field, headers, **kwargs) 38 | 39 | 40 | class InlineDaysColumn(DaysColumn): 41 | def __init__(self, field=None, start_date=None, end_date=None, **kwargs): 42 | kwargs['sortable'] = False 43 | kwargs.setdefault('header_attrs', {}) 44 | kwargs['header_attrs'].update({'class': 'calendar'}) 45 | super(InlineDaysColumn, self).__init__(field, start_date, end_date, **kwargs) 46 | 47 | 48 | class InlineWeeksColumn(WeeksColumn): 49 | def __init__(self, start_date=None, end_date=None, **kwargs): 50 | kwargs['space'] = False 51 | kwargs['sortable'] = False 52 | kwargs.setdefault('header_attrs', {}) 53 | kwargs['header_attrs'].update({'class': 'calendar'}) 54 | super(InlineWeeksColumn, self).__init__(start_date=start_date, end_date=end_date, **kwargs) 55 | 56 | 57 | class InlineMonthsColumn(MonthsColumn): 58 | def __init__(self, start_date=None, end_date=None, **kwargs): 59 | self.start_date = start_date 60 | self.end_date = end_date 61 | kwargs['space'] = False 62 | kwargs['sortable'] = False 63 | super(InlineMonthsColumn, self).__init__(start_date=start_date, end_date=end_date, **kwargs) 64 | 65 | def get_column(self, key): 66 | return Column(field=self.get_field(key), 67 | header=self.get_header(key), 68 | header_attrs=self.get_column_header_attrs(key), 69 | **self.kwargs) 70 | 71 | def get_column_header_attrs(self, index): 72 | header_attrs = self.kwargs.pop("header_attrs", {}) 73 | header_attrs.update({"colspan": self.get_column_span(index)}) 74 | return header_attrs 75 | 76 | def get_column_span(self, index): 77 | """ 78 | Get `colspan` value for tag. 79 | It will render as 80 | """ 81 | return str(self.get_days_span(index)) 82 | 83 | def get_days_span(self, month_index): 84 | """ 85 | Calculate how many days the month spans. 86 | """ 87 | is_first_month = month_index == 0 88 | is_last_month = month_index == self.__len__() - 1 89 | 90 | y = int(self.start_date.year + (self.start_date.month + month_index) / 13) 91 | m = int((self.start_date.month + month_index) % 12 or 12) 92 | total = calendar.monthrange(y, m)[1] 93 | 94 | if is_first_month and is_last_month: 95 | return (self.end_date - self.start_date).days + 1 96 | else: 97 | if is_first_month: 98 | return total - self.start_date.day + 1 99 | elif is_last_month: 100 | return self.end_date.day 101 | else: 102 | return total 103 | 104 | 105 | class CalendarColumn(SequenceColumn): 106 | MonthsColumnClass = InlineMonthsColumn 107 | WeeksColumnClass = InlineWeeksColumn 108 | DaysColumnClass = InlineDaysColumn 109 | 110 | def __init__(self, field, start_date, end_date, **kwargs): 111 | self.months_column = self.MonthsColumnClass(start_date, end_date, **kwargs) 112 | self.weeks_column = self.WeeksColumnClass(start_date, end_date, header_row_order=1) 113 | self.days_column = self.DaysColumnClass(field, start_date, end_date, header_row_order=2) 114 | headers = self.months_column.headers + self.weeks_column.headers + self.days_column.headers 115 | super(CalendarColumn, self).__init__(field, headers, **kwargs) 116 | 117 | @property 118 | def columns(self): 119 | return self.months_column.columns + self.weeks_column.columns + self.days_column.columns 120 | -------------------------------------------------------------------------------- /table/columns/checkboxcolumn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from django.utils.safestring import mark_safe 4 | 5 | from table.columns import Column 6 | from table.utils import Accessor 7 | 8 | 9 | class CheckboxColumn(Column): 10 | def __init__(self, field=None, header=None, **kwargs): 11 | kwargs["safe"] = False 12 | kwargs["sortable"] = False 13 | kwargs["searchable"] = False 14 | super(CheckboxColumn, self).__init__(field=field, header=header, **kwargs) 15 | 16 | def render(self, obj): 17 | checked = bool(Accessor(self.field).resolve(obj)) if self.field else False 18 | if checked: 19 | return mark_safe('') 20 | else: 21 | return mark_safe('') 22 | -------------------------------------------------------------------------------- /table/columns/datetimecolumn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from django.utils.html import escape 4 | 5 | from table.utils import Accessor 6 | from .base import Column 7 | 8 | 9 | class DatetimeColumn(Column): 10 | 11 | DEFAULT_FORMAT = "%Y-%m-%d %H:%M:%S" 12 | 13 | def __init__(self, field, header=None, format=None, **kwargs): 14 | self.format = format or DatetimeColumn.DEFAULT_FORMAT 15 | super(DatetimeColumn, self).__init__(field, header, **kwargs) 16 | 17 | def render(self, obj): 18 | datetime = Accessor(self.field).resolve(obj) 19 | text = datetime.strftime(self.format) 20 | return escape(text) 21 | -------------------------------------------------------------------------------- /table/columns/imagecolumn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from django.template import Template, Context 4 | 5 | from table.columns import Column 6 | from table.utils import Accessor 7 | 8 | 9 | class ImageColumn(Column): 10 | def __init__(self, field=None, image_title=None, *args, **kwargs): 11 | kwargs["sortable"] = False 12 | kwargs["searchable"] = False 13 | self.image_title = image_title 14 | super(ImageColumn, self).__init__(field=field, *args, **kwargs) 15 | 16 | def render(self, obj): 17 | path = Accessor(self.field).resolve(obj) 18 | if isinstance(self.image_title, Accessor): 19 | title = self.image_title.resolve(self.obj) 20 | else: 21 | title = self.image_title 22 | template = Template('{%% load static %%}' % (path, title)) 24 | return template.render(Context()) 25 | -------------------------------------------------------------------------------- /table/columns/linkcolumn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import django 4 | 5 | if django.VERSION >= (1, 10): 6 | from django.urls import reverse 7 | else: 8 | from django.core.urlresolvers import reverse 9 | 10 | from django.utils.safestring import mark_safe 11 | from django.utils.html import escape 12 | from django.template import Template, Context 13 | 14 | from table.utils import Accessor 15 | from table.columns.base import Column 16 | 17 | 18 | class LinkColumn(Column): 19 | def __init__(self, header=None, links=None, delimiter=' ', field=None, **kwargs): 20 | self.links = links 21 | self.delimiter = delimiter 22 | kwargs['safe'] = False 23 | super(LinkColumn, self).__init__(field, header, **kwargs) 24 | 25 | def render(self, obj): 26 | return self.delimiter.join([link.render(obj) for link in self.links]) 27 | 28 | 29 | class Link(object): 30 | """ 31 | Represents a html tag. 32 | """ 33 | def __init__(self, text=None, viewname=None, args=None, kwargs=None, urlconf=None, 34 | current_app=None, attrs=None): 35 | self.basetext = text 36 | self.viewname = viewname 37 | self.args = args or [] 38 | self.kwargs = kwargs or {} 39 | self.urlconf = urlconf 40 | self.current_app = current_app 41 | self.base_attrs = attrs or {} 42 | 43 | @property 44 | def text(self): 45 | if isinstance(self.basetext, Accessor): 46 | basetext = self.basetext.resolve(self.obj) 47 | else: 48 | basetext = self.basetext 49 | return escape(basetext) 50 | 51 | @property 52 | def url(self): 53 | if self.viewname is None: 54 | return "" 55 | 56 | # The following params + if statements create optional arguments to 57 | # pass to Django's reverse() function. 58 | params = {} 59 | if self.args: 60 | params['args'] = [arg.resolve(self.obj) 61 | if isinstance(arg, Accessor) else arg 62 | for arg in self.args] 63 | if self.kwargs: 64 | params['kwargs'] = {} 65 | for key, value in self.kwargs.items(): 66 | params['kwargs'][key] = (value.resolve(self.obj) 67 | if isinstance(value, Accessor) else value) 68 | if self.urlconf: 69 | params['urlconf'] = (self.urlconf.resolve(self.obj) 70 | if isinstance(self.urlconf, Accessor) 71 | else self.urlconf) 72 | if self.current_app: 73 | params['current_app'] = (self.current_app.resolve(self.obj) 74 | if isinstance(self.current_app, Accessor) 75 | else self.current_app) 76 | 77 | return reverse(self.viewname, **params) 78 | 79 | @property 80 | def attrs(self): 81 | if self.url: 82 | self.base_attrs["href"] = self.url 83 | return self.base_attrs 84 | 85 | def render(self, obj): 86 | """ Render link as HTML output tag . 87 | """ 88 | self.obj = obj 89 | attrs = ' '.join([ 90 | '%s="%s"' % (attr_name, attr.resolve(obj)) 91 | if isinstance(attr, Accessor) 92 | else '%s="%s"' % (attr_name, attr) 93 | for attr_name, attr in self.attrs.items() 94 | ]) 95 | return mark_safe(u'%s' % (attrs, self.text)) 96 | 97 | 98 | class ImageLink(Link): 99 | """ 100 | Represents a html tag that contains . 101 | """ 102 | def __init__(self, image, image_title, *args, **kwargs): 103 | self.image_path = image 104 | self.image_title = image_title 105 | super(ImageLink, self).__init__(*args, **kwargs) 106 | 107 | @property 108 | def image(self): 109 | path = self.image_path 110 | if isinstance(self.image_title, Accessor): 111 | title = self.image_title.resolve(self.obj) 112 | else: 113 | title = self.image_title 114 | template = Template('{%% load static %%}' % (path, title)) 116 | return template.render(Context()) 117 | 118 | @property 119 | def text(self): 120 | return self.image 121 | -------------------------------------------------------------------------------- /table/columns/sequencecolumn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from table.columns.base import Column 4 | 5 | 6 | class SequenceColumn(Column): 7 | def __init__(self, field, headers, **kwargs): 8 | self.headers = headers 9 | self.kwargs = kwargs 10 | super(SequenceColumn, self).__init__(field, **kwargs) 11 | 12 | @property 13 | def columns(self): 14 | return [self.get_column(key) for key in range(self.__len__())] 15 | 16 | def __str__(self): 17 | return str(self.columns) 18 | 19 | def __len__(self): 20 | return len(self.headers) 21 | 22 | def __getitem__(self, key): 23 | return self.columns[key] 24 | 25 | def __setitem__(self, key, value): 26 | self.columns[key] = value 27 | 28 | def get_column(self, key): 29 | return Column(field=self.get_field(key), 30 | header=self.get_header(key), 31 | **self.kwargs) 32 | 33 | def get_field(self, key): 34 | if self.field: 35 | return ".".join([self.field, str(key)]) 36 | else: 37 | return None 38 | 39 | def get_header(self, key): 40 | return self.headers[key] 41 | -------------------------------------------------------------------------------- /table/forms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from django import forms 4 | 5 | 6 | class QueryDataForm(forms.Form): 7 | """ 8 | Non-interactive form that used to organize query parameters 9 | of DataTables. 10 | """ 11 | sEcho = forms.CharField() 12 | iDisplayStart = forms.IntegerField() 13 | iDisplayLength = forms.IntegerField() 14 | iColumns = forms.IntegerField() 15 | sSearch = forms.CharField(required=False) 16 | bRegex = forms.BooleanField(required=False) 17 | iSortingCols = forms.IntegerField(required=False) 18 | 19 | def __init__(self, data=None, *args, **kwargs): 20 | super(QueryDataForm, self).__init__(data, *args, **kwargs) 21 | for key in data.keys(): 22 | if key.startswith("iSortCol"): 23 | self.fields[key] = forms.IntegerField() 24 | if key.startswith("sSortDir"): 25 | self.fields[key] = forms.CharField() 26 | -------------------------------------------------------------------------------- /table/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | """ Model for test. 4 | """ 5 | from django.db import models 6 | 7 | 8 | class Person(models.Model): 9 | id = models.IntegerField(primary_key=True) 10 | name = models.CharField(max_length=100) 11 | email = models.CharField(max_length=100) 12 | -------------------------------------------------------------------------------- /table/static/table/css/datatable.bootstrap.css: -------------------------------------------------------------------------------- 1 | div.dataTables_length label { 2 | margin-top: 4px; 3 | font-weight: normal; 4 | text-align: left; 5 | float: right; 6 | } 7 | 8 | div.dataTables_length select { 9 | width: 75px; 10 | height: 34px; 11 | } 12 | 13 | div.dataTables_filter label { 14 | width: 100%; 15 | font-weight: normal; 16 | float: right; 17 | } 18 | 19 | div.dataTables_filter input { 20 | width: 16em; 21 | border-left: 0px; 22 | background-color: #FFFFFF; 23 | } 24 | 25 | div.dataTables_filter .glyphicon-search:before { 26 | content: "\e003"; 27 | color: #999999; 28 | } 29 | 30 | .filter_icon { 31 | border-right: 0px; 32 | background-color: #FFFFFF; 33 | padding-bottom: 5px; 34 | } 35 | 36 | div.dataTables_info { 37 | margin-top: 4px; 38 | width:auto; 39 | } 40 | 41 | div.dataTables_paginate { 42 | float: right; 43 | margin: 0; 44 | } 45 | 46 | div.dataTables_paginate ul.pagination { 47 | margin-top: 4px; 48 | } 49 | 50 | table.table { 51 | clear: both; 52 | margin-top: 6px !important; 53 | margin-bottom: 6px !important; 54 | max-width: none !important; 55 | } 56 | 57 | table.table thead .sorting, 58 | table.table thead .sorting_asc, 59 | table.table thead .sorting_desc, 60 | table.table thead .sorting_asc_disabled, 61 | table.table thead .sorting_desc_disabled { 62 | cursor: pointer; 63 | } 64 | 65 | table.table thead .sorting { background: url('../images/sort_both.png') no-repeat center right; } 66 | table.table thead .sorting_asc { background: url('../images/sort_asc.png') no-repeat center right; } 67 | table.table thead .sorting_desc { background: url('../images/sort_desc.png') no-repeat center right; } 68 | 69 | table.table thead .sorting_asc_disabled { background: url('../images/sort_asc_disabled.png') no-repeat center right; } 70 | table.table thead .sorting_desc_disabled { background: url('../images/sort_desc_disabled.png') no-repeat center right; } 71 | 72 | table.dataTable th:active { 73 | outline: none; 74 | } 75 | 76 | /* Scrolling */ 77 | div.dataTables_scrollHead table { 78 | margin-bottom: 0 !important; 79 | border-bottom-left-radius: 0; 80 | border-bottom-right-radius: 0; 81 | } 82 | 83 | div.dataTables_scrollHead table thead tr:last-child th:first-child, 84 | div.dataTables_scrollHead table thead tr:last-child td:first-child { 85 | border-bottom-left-radius: 0 !important; 86 | border-bottom-right-radius: 0 !important; 87 | } 88 | 89 | div.dataTables_scrollBody table { 90 | border-top: none; 91 | margin-top: 0 !important; 92 | margin-bottom: 0 !important; 93 | } 94 | 95 | div.dataTables_scrollBody tbody tr:first-child th, 96 | div.dataTables_scrollBody tbody tr:first-child td { 97 | border-top: none; 98 | } 99 | 100 | div.dataTables_scrollFoot table { 101 | border-top: none; 102 | } 103 | 104 | 105 | .dataTables_processing { 106 | position: absolute; 107 | top: 40%; 108 | left: 50%; 109 | width: 250px; 110 | height: 30px; 111 | margin-left: -125px; 112 | margin-top: -15px; 113 | /* padding: 14px 0 2px 0; */ 114 | border: 1px solid #ddd; 115 | text-align: center; 116 | color: #999; 117 | font-size: 14px; 118 | background-color: #f5f5f5; 119 | } 120 | 121 | /* 122 | * TableTools styles 123 | */ 124 | .table tbody tr.active td, 125 | .table tbody tr.active th { 126 | background-color: #08C; 127 | color: white; 128 | } 129 | 130 | .table tbody tr.active:hover td, 131 | .table tbody tr.active:hover th { 132 | background-color: #0075b0 !important; 133 | } 134 | 135 | .table-striped tbody tr.active:nth-child(odd) td, 136 | .table-striped tbody tr.active:nth-child(odd) th { 137 | background-color: #017ebc; 138 | } 139 | 140 | table.DTTT_selectable tbody tr { 141 | cursor: pointer; 142 | } 143 | 144 | div.DTTT .btn { 145 | color: #333 !important; 146 | font-size: 12px; 147 | } 148 | 149 | div.DTTT .btn:hover { 150 | text-decoration: none !important; 151 | } 152 | 153 | ul.DTTT_dropdown.dropdown-menu { 154 | z-index: 2003; 155 | } 156 | 157 | ul.DTTT_dropdown.dropdown-menu a { 158 | color: #333 !important; /* needed only when demo_page.css is included */ 159 | } 160 | 161 | ul.DTTT_dropdown.dropdown-menu li { 162 | position: relative; 163 | } 164 | 165 | ul.DTTT_dropdown.dropdown-menu li:hover a { 166 | background-color: #0088cc; 167 | color: white !important; 168 | } 169 | 170 | div.DTTT_collection_background { 171 | z-index: 2002; 172 | } 173 | 174 | /* TableTools information display */ 175 | div.DTTT_print_info.modal { 176 | height: 150px; 177 | margin-top: -75px; 178 | text-align: center; 179 | } 180 | 181 | div.DTTT_print_info h6 { 182 | font-weight: normal; 183 | font-size: 28px; 184 | line-height: 28px; 185 | margin: 1em; 186 | } 187 | 188 | div.DTTT_print_info p { 189 | font-size: 14px; 190 | line-height: 20px; 191 | } 192 | 193 | 194 | 195 | /* 196 | * FixedColumns styles 197 | */ 198 | div.DTFC_LeftHeadWrapper table, 199 | div.DTFC_LeftFootWrapper table, 200 | div.DTFC_RightHeadWrapper table, 201 | div.DTFC_RightFootWrapper table, 202 | table.DTFC_Cloned tr.even { 203 | background-color: white; 204 | } 205 | 206 | div.DTFC_RightHeadWrapper table , 207 | div.DTFC_LeftHeadWrapper table { 208 | margin-bottom: 0 !important; 209 | border-top-right-radius: 0 !important; 210 | border-bottom-left-radius: 0 !important; 211 | border-bottom-right-radius: 0 !important; 212 | } 213 | 214 | div.DTFC_RightHeadWrapper table thead tr:last-child th:first-child, 215 | div.DTFC_RightHeadWrapper table thead tr:last-child td:first-child, 216 | div.DTFC_LeftHeadWrapper table thead tr:last-child th:first-child, 217 | div.DTFC_LeftHeadWrapper table thead tr:last-child td:first-child { 218 | border-bottom-left-radius: 0 !important; 219 | border-bottom-right-radius: 0 !important; 220 | } 221 | 222 | div.DTFC_RightBodyWrapper table, 223 | div.DTFC_LeftBodyWrapper table { 224 | border-top: none; 225 | margin-top: 0 !important; 226 | margin-bottom: 0 !important; 227 | } 228 | 229 | div.DTFC_RightBodyWrapper tbody tr:first-child th, 230 | div.DTFC_RightBodyWrapper tbody tr:first-child td, 231 | div.DTFC_LeftBodyWrapper tbody tr:first-child th, 232 | div.DTFC_LeftBodyWrapper tbody tr:first-child td { 233 | border-top: none; 234 | } 235 | 236 | div.DTFC_RightFootWrapper table, 237 | div.DTFC_LeftFootWrapper table { 238 | border-top: none; 239 | } 240 | 241 | .table-bordered > thead > tr > th, 242 | .table-bordered > thead > tr > td { 243 | border-bottom-width: 0px; 244 | } 245 | -------------------------------------------------------------------------------- /table/static/table/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /table/static/table/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | -------------------------------------------------------------------------------- /table/static/table/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /table/static/table/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /table/static/table/images/Sorting icons.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/images/Sorting icons.psd -------------------------------------------------------------------------------- /table/static/table/images/back_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/images/back_disabled.png -------------------------------------------------------------------------------- /table/static/table/images/back_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/images/back_enabled.png -------------------------------------------------------------------------------- /table/static/table/images/back_enabled_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/images/back_enabled_hover.png -------------------------------------------------------------------------------- /table/static/table/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/images/favicon.ico -------------------------------------------------------------------------------- /table/static/table/images/forward_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/images/forward_disabled.png -------------------------------------------------------------------------------- /table/static/table/images/forward_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/images/forward_enabled.png -------------------------------------------------------------------------------- /table/static/table/images/forward_enabled_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/images/forward_enabled_hover.png -------------------------------------------------------------------------------- /table/static/table/images/sort_asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/images/sort_asc.png -------------------------------------------------------------------------------- /table/static/table/images/sort_asc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/images/sort_asc_disabled.png -------------------------------------------------------------------------------- /table/static/table/images/sort_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/images/sort_both.png -------------------------------------------------------------------------------- /table/static/table/images/sort_desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/images/sort_desc.png -------------------------------------------------------------------------------- /table/static/table/images/sort_desc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/static/table/images/sort_desc_disabled.png -------------------------------------------------------------------------------- /table/static/table/js/bootstrap.dataTables.js: -------------------------------------------------------------------------------- 1 | /* Set the defaults for DataTables initialisation */ 2 | $.extend( true, $.fn.dataTable.defaults, { 3 | "sDom": "<'row'<'col-xs-6'l><'col-xs-6'f>r>t<'row'<'col-xs-6'i><'col-xs-6'p>>", 4 | "sPaginationType": "bootstrap", 5 | "oLanguage": { 6 | "sLengthMenu": "_MENU_", 7 | "sSearch": "", 8 | "sInfoFiltered": "", 9 | "sProcessing": "Loading", 10 | }, 11 | "bAutoWidth": false, 12 | "fnPreDrawCallback": function(oSettings, json) { 13 | $('.dataTables_filter input').addClass('form-control input'); 14 | $('.dataTables_length select').addClass('form-control input'); 15 | $('.dataTables_info').addClass('form-control'); 16 | } 17 | }); 18 | 19 | 20 | /* Default class modification */ 21 | $.extend( $.fn.dataTableExt.oStdClasses, { 22 | "sWrapper": "dataTables_wrapper form-inline", 23 | "sFilterInput": "form-control input-sm", 24 | "sLengthSelect": "form-control input-sm" 25 | }); 26 | 27 | /* API method to get hidden rows */ 28 | $.fn.dataTableExt.oApi.fnGetHiddenNodes = function ( oSettings ) 29 | { 30 | 31 | /* Note the use of a DataTables 'private' function thought the 'oApi' object */ 32 | // DataTables 1.10 33 | var api = new jQuery.fn.dataTable.Api( oSettings ); 34 | var anNodes = api.rows().nodes().toArray(); 35 | // var anNodes = this.oApi._fnGetTrNodes( oSettings ); 36 | var anDisplay = $('tbody tr', oSettings.nTable); 37 | 38 | /* Remove nodes which are being displayed */ 39 | for ( var i=0 ; i'+ 83 | ''+ 84 | ''+ 85 | '' 86 | ); 87 | var els = $('a', nPaging); 88 | $(els[0]).bind( 'click.DT', { action: "previous" }, fnClickHandler ); 89 | $(els[1]).bind( 'click.DT', { action: "next" }, fnClickHandler ); 90 | }, 91 | 92 | "fnUpdate": function ( oSettings, fnDraw ) { 93 | var iListLength = 5; 94 | var oPaging = oSettings.oInstance.fnPagingInfo(); 95 | var an = oSettings.aanFeatures.p; 96 | var i, ien, j, sClass, iStart, iEnd, iHalf=Math.floor(iListLength/2); 97 | 98 | if ( oPaging.iTotalPages < iListLength) { 99 | iStart = 1; 100 | iEnd = oPaging.iTotalPages; 101 | } 102 | else if ( oPaging.iPage <= iHalf ) { 103 | iStart = 1; 104 | iEnd = iListLength; 105 | } else if ( oPaging.iPage >= (oPaging.iTotalPages-iHalf) ) { 106 | iStart = oPaging.iTotalPages - iListLength + 1; 107 | iEnd = oPaging.iTotalPages; 108 | } else { 109 | iStart = oPaging.iPage - iHalf + 1; 110 | iEnd = iStart + iListLength - 1; 111 | } 112 | 113 | for ( i=0, ien=an.length ; i'+j+'') 121 | .insertBefore( $('li:last', an[i])[0] ) 122 | .bind('click', function (e) { 123 | e.preventDefault(); 124 | oSettings._iDisplayStart = (parseInt($('a', this).text(),10)-1) * oPaging.iLength; 125 | fnDraw( oSettings ); 126 | } ); 127 | } 128 | 129 | // Add / remove disabled classes from the static elements 130 | if ( oPaging.iPage === 0 ) { 131 | $('li:first', an[i]).addClass('disabled'); 132 | } else { 133 | $('li:first', an[i]).removeClass('disabled'); 134 | } 135 | 136 | if ( oPaging.iPage === oPaging.iTotalPages-1 || oPaging.iTotalPages === 0 ) { 137 | $('li:last', an[i]).addClass('disabled'); 138 | } else { 139 | $('li:last', an[i]).removeClass('disabled'); 140 | } 141 | } 142 | } 143 | } 144 | } ); 145 | 146 | 147 | /* 148 | * TableTools Bootstrap compatibility 149 | * Required TableTools 2.1+ 150 | */ 151 | if ( $.fn.DataTable.TableTools ) { 152 | // Set the classes that TableTools uses to something suitable for Bootstrap 153 | $.extend( true, $.fn.DataTable.TableTools.classes, { 154 | "container": "DTTT btn-group", 155 | "buttons": { 156 | "normal": "btn btn-default", 157 | "disabled": "disabled" 158 | }, 159 | "collection": { 160 | "container": "DTTT_dropdown dropdown-menu", 161 | "buttons": { 162 | "normal": "", 163 | "disabled": "disabled" 164 | } 165 | }, 166 | "print": { 167 | "info": "DTTT_print_info modal" 168 | }, 169 | "select": { 170 | "row": "active" 171 | } 172 | } ); 173 | 174 | // Have the collection use a bootstrap compatible dropdown 175 | $.extend( true, $.fn.DataTable.TableTools.DEFAULTS.oTags, { 176 | "collection": { 177 | "container": "ul", 178 | "button": "li", 179 | "liner": "a" 180 | } 181 | } ); 182 | } 183 | -------------------------------------------------------------------------------- /table/static/table/js/dataTables.fixedColumns.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | FixedColumns 3.0.1 3 | ©2010-2014 SpryMedia Ltd - datatables.net/license 4 | */ 5 | (function(q){var r=function(d){var k=function(a,b){var c=this;if(!this instanceof k)alert("FixedColumns warning: FixedColumns must be initialised with the 'new' keyword.");else{"undefined"==typeof b&&(b={});d.fn.dataTable.camelToHungarian&&d.fn.dataTable.camelToHungarian(k.defaults,b);var g=d.fn.dataTable.Api?(new d.fn.dataTable.Api(a)).settings()[0]:a.fnSettings();this.s={dt:g,iTableColumns:g.aoColumns.length,aiOuterWidths:[],aiInnerWidths:[]};this.dom={scroller:null,header:null,body:null,footer:null, 6 | grid:{wrapper:null,dt:null,left:{wrapper:null,head:null,body:null,foot:null},right:{wrapper:null,head:null,body:null,foot:null}},clone:{left:{header:null,body:null,footer:null},right:{header:null,body:null,footer:null}}};g._oFixedColumns=this;g._bInitComplete?this._fnConstruct(b):g.oApi._fnCallbackReg(g,"aoInitComplete",function(){c._fnConstruct(b)},"FixedColumns")}};k.prototype={fnUpdate:function(){this._fnDraw(!0)},fnRedrawLayout:function(){this._fnColCalc();this._fnGridLayout();this.fnUpdate()}, 7 | fnRecalculateHeight:function(a){delete a._DTTC_iHeight;a.style.height="auto"},fnSetRowHeight:function(a,b){a.style.height=b+"px"},fnGetPosition:function(a){var b=this.s.dt.oInstance;if(d(a).parents(".DTFC_Cloned").length){if("tr"===a.nodeName.toLowerCase())return a=d(a).index(),b.fnGetPosition(d("tr",this.s.dt.nTBody)[a]);var c=d(a).index(),a=d(a.parentNode).index();return[b.fnGetPosition(d("tr",this.s.dt.nTBody)[a]),c,b.oApi._fnVisibleToColumnIndex(this.s.dt,c)]}return b.fnGetPosition(a)},_fnConstruct:function(a){var b= 8 | this;if("function"!=typeof this.s.dt.oInstance.fnVersionCheck||!0!==this.s.dt.oInstance.fnVersionCheck("1.8.0"))alert("FixedColumns "+k.VERSION+" required DataTables 1.8.0 or later. Please upgrade your DataTables installation");else if(""===this.s.dt.oScroll.sX)this.s.dt.oInstance.oApi._fnLog(this.s.dt,1,"FixedColumns is not needed (no x-scrolling in DataTables enabled), so no action will be taken. Use 'FixedHeader' for column fixing when scrolling is not enabled");else{this.s=d.extend(!0,this.s, 9 | k.defaults,a);a=this.s.dt.oClasses;this.dom.grid.dt=d(this.s.dt.nTable).parents("div."+a.sScrollWrapper)[0];this.dom.scroller=d("div."+a.sScrollBody,this.dom.grid.dt)[0];this._fnColCalc();this._fnGridSetup();var c;d(this.dom.scroller).on("mouseover.DTFC touchstart.DTFC",function(){c="main"}).on("scroll.DTFC",function(){if("main"===c&&(0
')[0], 15 | g=c.childNodes[0],f=c.childNodes[1];this.dom.grid.dt.parentNode.insertBefore(c,this.dom.grid.dt);c.appendChild(this.dom.grid.dt);this.dom.grid.wrapper=c;0b.clientWidth&&(c.x=!0);a.offsetHeight>b.clientHeight&&(c.y=!0);return c},_fnDraw:function(a){this._fnGridLayout();this._fnCloneLeft(a);this._fnCloneRight(a);null!==this.s.fnDrawCallback&&this.s.fnDrawCallback.call(this,this.dom.clone.left,this.dom.clone.right);d(this).trigger("draw.dtfc",{leftClone:this.dom.clone.left,rightClone:this.dom.clone.right})},_fnCloneRight:function(a){if(!(0>=this.s.iRightColumns)){var b,c=[];for(b=this.s.iTableColumns-this.s.iRightColumns;b=this.s.iLeftColumns)){var b,c=[];for(b=0;bthead",a.header);i.empty();e=0; 22 | for(h=m.length;ethead",a.header)[0]);e=0;for(h=m.length;etbody>tr",f.dom.body).css("height","auto");null!==a.body&&(a.body.parentNode.removeChild(a.body),a.body=null);a.body=d(this.dom.body).clone(!0)[0];a.body.className+=" DTFC_Cloned";a.body.style.paddingBottom=this.s.dt.oScroll.iBarWidth+"px";a.body.style.marginBottom=2*this.s.dt.oScroll.iBarWidth+"px";null!==a.body.getAttribute("id")&&a.body.removeAttribute("id");d(">thead>tr",a.body).empty();d(">tfoot",a.body).remove();var p=d("tbody",a.body)[0];d(p).empty();if(0thead>tr",a.body)[0];for(k=0;ktbody>tr",f.dom.body).each(function(a){var b=this.cloneNode(false);b.removeAttribute("id");a=f.s.dt.oFeatures.bServerSide===false?f.s.dt.aiDisplay[f.s.dt._iDisplayStart+a]:a;for(k=0;k0){l=d(e[n]).clone(true,true)[0];b.appendChild(l)}}p.appendChild(b)})}else d(">tbody>tr",f.dom.body).each(function(){l=this.cloneNode(true);l.className=l.className+" DTFC_NoData";d("td",l).html("");p.appendChild(l)});a.body.style.width="100%";a.body.style.margin="0";a.body.style.padding="0";g&&"undefined"!=typeof this.s.dt.oScroller&&b.liner.appendChild(this.s.dt.oScroller.dom.force.cloneNode(!0));b.liner.appendChild(a.body);this._fnEqualiseHeights("tbody", 26 | f.dom.body,a.body);if(null!==this.s.dt.nTFoot){if(g){null!==a.footer&&a.footer.parentNode.removeChild(a.footer);a.footer=d(this.dom.footer).clone(!0,!0)[0];a.footer.className+=" DTFC_Cloned";a.footer.style.width="100%";b.foot.appendChild(a.footer);m=this._fnCopyLayout(this.s.dt.aoFooter,c);b=d(">tfoot",a.footer);b.empty();e=0;for(h=m.length;etfoot",a.footer)[0]);e=0;for(h=m.length;ethead",a.header)[0]);d(b).each(function(a){n=c[a];this.style.width=f.s.aiInnerWidths[n]+"px"});null!==f.s.dt.nTFoot&&(b=this.s.dt.oApi._fnGetUniqueThs(this.s.dt,d(">tfoot",a.footer)[0]),d(b).each(function(a){n=c[a];this.style.width=f.s.aiInnerWidths[n]+"px"}))},_fnGetTrNodes:function(a){for(var b= 28 | [],c=0,d=a.childNodes.length;c"+a+">tr:eq(0)",b).children(":first");a.outerHeight();a.height();for(var e=this._fnGetTrNodes(e),b=this._fnGetTrNodes(c),h=[],c=0,a=b.length;cg?f:g,"semiauto"==this.s.sHeightMatch&& 29 | (e[c]._DTTC_iHeight=g),h.push(g);c=0;for(a=b.length;c" 121 | dom += "rt" 122 | if self.info_label.visible or self.pagination.visible or self.length_menu.visible: 123 | dom += "<'row'" + ''.join([self.info_label.dom, self.pagination.dom, self.length_menu.dom]) + ">" 124 | return mark_safe(dom) 125 | 126 | 127 | class TableOptions(object): 128 | def __init__(self, options=None): 129 | self.model = getattr(options, 'model', None) 130 | 131 | # ajax option 132 | self.ajax = getattr(options, 'ajax', False) 133 | self.ajax_source = getattr(options, 'ajax_source', None) 134 | 135 | # id attribute of tag 136 | self.id = getattr(options, 'id', None) 137 | 138 | # build attributes for
tag, use bootstrap 139 | # css class "table table-boarded" as default style 140 | attrs = getattr(options, 'attrs', {}) 141 | attrs['class'] = 'table ' + attrs.get('class', '') 142 | self.attrs = mark_safe(' '.join(['%s="%s"' % (attr_name, attr) 143 | for attr_name, attr in attrs.items()])) 144 | # build attributes for and 145 | thead_attrs = getattr(options, 'thead_attrs', {}) 146 | self.thead_attrs = mark_safe(' '.join(['%s="%s"' % (attr_name, attr) 147 | for attr_name, attr in thead_attrs.items()])) 148 | tbody_attrs = getattr(options, 'tbody_attrs', {}) 149 | self.tbody_attrs = mark_safe(' '.join(['%s="%s"' % (attr_name, attr) 150 | for attr_name, attr in tbody_attrs.items()])) 151 | 152 | # scrolling option 153 | self.scrollable = getattr(options, 'scrollable', False) 154 | self.scrollinner = getattr(options, 'scrollinner', "150%") 155 | self.fixed_columns = getattr(options, 'fixed_columns', None) 156 | self.fixed_columns_width = getattr(options, 'fixed_columns_width', None) 157 | 158 | # inspect sorting option 159 | self.sort = [] 160 | for column, order in getattr(options, 'sort', []): 161 | if not isinstance(column, int): 162 | raise ValueError('Sorting option must be organized by following' 163 | ' forms: [(0, "asc"), (1, "desc")]') 164 | if order not in ('asc', 'desc'): 165 | raise ValueError('Order value must be "asc" or "desc", ' 166 | '"%s" is unsupported.' % order) 167 | self.sort.append((column, order)) 168 | 169 | # options for table add-on 170 | self.search = getattr(options, 'search', True) 171 | self.search_placeholder = getattr(options, 'search_placeholder', None) 172 | 173 | self.info = getattr(options, 'info', True) 174 | self.info_format = getattr(options, 'info_format', None) 175 | 176 | self.pagination = getattr(options, 'pagination', True) 177 | self.page_length = getattr(options, 'page_length', 10) 178 | self.pagination_first = getattr(options, 'pagination_first', None) 179 | self.pagination_last = getattr(options, 'pagination_last', None) 180 | self.pagination_prev = getattr(options, 'pagination_prev', None) 181 | self.pagination_next = getattr(options, 'pagination_next', None) 182 | 183 | self.length_menu = getattr(options, 'length_menu', True) 184 | 185 | self.zero_records = getattr(options, 'zero_records', u'No records') 186 | self.template_name = getattr(options, 'template_name', None) 187 | self.theme_css_file = getattr(options, 'theme_css_file', 'table/css/datatable.bootstrap.css') 188 | self.theme_js_file = getattr(options, 'theme_js_file', 'table/js/bootstrap.dataTables.js') 189 | 190 | 191 | class TableMetaClass(type): 192 | """ Meta class for create Table class instance. 193 | """ 194 | 195 | def __new__(cls, name, bases, attrs): 196 | opts = TableOptions(attrs.get('Meta', None)) 197 | # take class name in lower case as table's id 198 | if opts.id is None: 199 | opts.id = name.lower() 200 | attrs['opts'] = opts 201 | 202 | # extract declared columns 203 | columns = [] 204 | for attr_name, attr in attrs.items(): 205 | if isinstance(attr, SequenceColumn): 206 | columns.extend(attr) 207 | elif isinstance(attr, Column): 208 | columns.append(attr) 209 | columns.sort(key=lambda x: x.instance_order) 210 | 211 | # If this class is subclassing other tables, add their fields as 212 | # well. Note that we loop over the bases in reverse - this is 213 | # necessary to preserve the correct order of columns. 214 | parent_columns = [] 215 | for base in bases[::-1]: 216 | if hasattr(base, "base_columns"): 217 | parent_columns = base.base_columns + parent_columns 218 | base_columns = parent_columns + columns 219 | 220 | # For ajax data source, store columns into global hash map with 221 | # unique token key. So that, columns can be get to construct data 222 | # on views layer. 223 | token = md5(name.encode('utf-8')).hexdigest() 224 | 225 | if opts.ajax: 226 | TableDataMap.register(token, opts.model, copy.deepcopy(base_columns)) 227 | 228 | attrs['token'] = token 229 | attrs['base_columns'] = base_columns 230 | 231 | return super(TableMetaClass, cls).__new__(cls, name, bases, attrs) 232 | 233 | 234 | Table = TableMetaClass(str('Table'), (BaseTable,), {}) 235 | -------------------------------------------------------------------------------- /table/templates/table/simple_table.html: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 |
3 | 4 | {% for row in table.header_rows %} 5 | 6 | {% for header in row %} 7 | 10 | {% endfor %} 11 | 12 | {% endfor %} 13 | 14 | {% if not table.opts.ajax %} 15 | 16 | {% for row in table.rows %} 17 | 18 | {% for column, td in row.items %} 19 | 20 | {% endfor %} 21 | 22 | {% endfor %} 23 | 24 | {% endif %} 25 |
8 | {{ header.text }} 9 |
{% if column.safe %}{{ td }}{% else %}{{ td|safe }}{% endif %}
26 | {% endspaceless %} 27 | -------------------------------------------------------------------------------- /table/templates/table/table.html: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 | {% load static %} 3 | 4 | {% block css %} 5 | 6 | {% endblock css %} 7 | {% block extra_css %}{% endblock extra_css %} 8 | 9 | 10 | 11 | 12 | {% for row in table.header_rows %} 13 | 14 | {% for header in row %} 15 | 18 | {% endfor %} 19 | 20 | {% endfor %} 21 | 22 | {% if not table.opts.ajax %} 23 | 24 | {% for row in table.rows %} 25 | 26 | {% for column, td in row.items %} 27 | 28 | {% endfor %} 29 | 30 | {% endfor %} 31 | 32 | {% endif %} 33 |
16 | {{ header.text }} 17 |
{% if column.safe %}{{ td }}{% else %}{{ td|safe }}{% endif %}
34 | 35 | {% block js %} 36 | 37 | 38 | 39 | 40 | {% endblock js %} 41 | {% block extra_js %}{% endblock extra_js %} 42 | 43 | {% block table_init_js %} 44 | 117 | {% endblock table_init_js %} 118 | {% endspaceless %} 119 | -------------------------------------------------------------------------------- /table/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shymonk/django-datatable/f20a6ed2ce31aa7488ff85b4b0e80fe1ad94ec44/table/templatetags/__init__.py -------------------------------------------------------------------------------- /table/templatetags/table_tags.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from django import template 4 | from django.template import Context 5 | 6 | 7 | register = template.Library() 8 | 9 | 10 | class TableNode(template.Node): 11 | template_name = "table/table.html" 12 | 13 | def __init__(self, table): 14 | self.table = template.Variable(table) 15 | 16 | def render(self, context): 17 | table = self.table.resolve(context) 18 | t = template.loader.get_template( 19 | table.opts.template_name or self.template_name) 20 | context = {'table': table} 21 | return t.render(context) 22 | 23 | 24 | class SimpleTableNode(TableNode): 25 | template_name = "table/simple_table.html" 26 | 27 | 28 | @register.tag 29 | def render_table(parser, token): 30 | try: 31 | tag, table = token.split_contents() 32 | except ValueError: 33 | msg = '%r tag requires a single arguments' % token.split_contents()[0] 34 | raise template.TemplateSyntaxError(msg) 35 | return TableNode(table) 36 | 37 | 38 | @register.tag 39 | def render_simple_table(parser, token): 40 | try: 41 | tag, table = token.split_contents() 42 | except ValueError: 43 | msg = '%r tag requires a single arguments' % token.split_contents()[0] 44 | raise template.TemplateSyntaxError(msg) 45 | return SimpleTableNode(table) 46 | -------------------------------------------------------------------------------- /table/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from table.tests.test_columns import * 2 | from table.tests.test_tables import * 3 | from table.tests.test_views import * 4 | from table.tests.test_widgets import * 5 | -------------------------------------------------------------------------------- /table/tests/test_columns.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from __future__ import unicode_literals 4 | from datetime import date 5 | 6 | from django.test import TestCase 7 | 8 | from table.utils import A 9 | from table.columns.base import Column 10 | from table.columns.linkcolumn import Link, ImageLink 11 | from table.columns.sequencecolumn import SequenceColumn 12 | from table.columns.calendarcolumn import DaysColumn, WeeksColumn, MonthsColumn, CalendarColumn 13 | 14 | 15 | class BaseColumntestCase(TestCase): 16 | def test_create_instance(self): 17 | column = Column("field") 18 | 19 | self.assertEqual(column.field, "field") 20 | self.assertEqual(column.attrs, {}) 21 | 22 | self.assertTrue(column.sortable, True) 23 | self.assertTrue(column.searchable, True) 24 | self.assertTrue(column.safe, True) 25 | self.assertTrue(column.visible, True) 26 | self.assertTrue(column.space, True) 27 | 28 | def test_column_header(self): 29 | column = Column("field", "A", header_attrs={"id": "col-id"}, header_row_order=1) 30 | self.assertEqual(column.header.text, "A") 31 | self.assertEqual(column.header.attrs, 'id="col-id"') 32 | self.assertEqual(column.header.row_order, 1) 33 | 34 | def test_render(self): 35 | class Foo(): 36 | def __init__(self, foo): 37 | self.foo = foo 38 | f = Foo("bar") 39 | column = Column("foo") 40 | self.assertEqual(column.render(f), "bar") 41 | 42 | 43 | class LinkColumnTestCase(TestCase): 44 | def test_link(self): 45 | link = Link(text=A("foo")) 46 | self.assertEqual(link.render({}), "") 47 | self.assertEqual(link.render({"foo": "bar"}), "bar") 48 | 49 | def test_imagelink(self): 50 | image_link = ImageLink(image="test.jpg", image_title="foo") 51 | self.assertEqual(image_link.render({}), '') 52 | image_link = ImageLink(image="test.jpg", image_title=A("foo")) 53 | self.assertEqual(image_link.render({"foo": "bar"}), '') 54 | 55 | 56 | class SequenceColumnTestCase(TestCase): 57 | def setUp(self): 58 | self.column = SequenceColumn("foo", headers=["A", "B", "C"]) 59 | 60 | def test_create_instance(self): 61 | self.assertEqual(self.column.headers, ["A", "B", "C"]) 62 | 63 | def test_length(self): 64 | self.assertEqual(len(self.column), 3) 65 | 66 | def test_getitem(self): 67 | col = self.column[0] 68 | self.assertEqual(col.field, "foo.0") 69 | self.assertEqual(col.header.text, "A") 70 | 71 | def test_extend_columns(self): 72 | columns = [] 73 | columns.extend(self.column) 74 | self.assertEqual(len(columns), 3) 75 | 76 | def test_get_field(self): 77 | self.assertEqual(self.column.get_field(0), "foo.0") 78 | self.assertEqual(self.column.get_field(1), "foo.1") 79 | self.assertEqual(self.column.get_field(2), "foo.2") 80 | 81 | def test_get_column(self): 82 | column0 = self.column.get_column(0) 83 | self.assertEqual(column0.field, "foo.0") 84 | self.assertEqual(column0.header.text, "A") 85 | 86 | def test_columns_property(self): 87 | self.assertTrue(isinstance(self.column.columns, list)) 88 | self.assertEqual(len(self.column.columns), 3) 89 | self.assertEqual(self.column.columns[0].field, "foo.0") 90 | self.assertEqual(self.column.columns[1].field, "foo.1") 91 | self.assertEqual(self.column.columns[2].field, "foo.2") 92 | 93 | 94 | class DaysColumnTestCase(TestCase): 95 | def setUp(self): 96 | self.column1 = DaysColumn(None, date(2012, 12, 18), date(2012, 12, 19)) 97 | self.column2 = DaysColumn(None, date(2013, 12, 30), date(2014, 1, 1)) 98 | 99 | def test_length(self): 100 | self.assertEqual(len(self.column1), 2) 101 | self.assertEqual(len(self.column2), 3) 102 | 103 | def test_headers(self): 104 | self.assertEqual(self.column1.headers, ['18', '19']) 105 | self.assertEqual(self.column2.headers, ['30', '31', '01']) 106 | 107 | 108 | class WeeksColumnTestCase(TestCase): 109 | def setUp(self): 110 | self.column1 = WeeksColumn(None, date(2013, 12, 18), date(2013, 12, 19)) 111 | self.column2 = WeeksColumn(None, date(2013, 12, 30), date(2014, 1, 1)) 112 | 113 | def test_length(self): 114 | self.assertEqual(len(self.column1), 2) 115 | self.assertEqual(len(self.column2), 3) 116 | 117 | def test_headers(self): 118 | self.assertEqual(self.column1.headers, ['Wed', 'Thu']) 119 | self.assertEqual(self.column2.headers, ['Mon', 'Tue', 'Wed']) 120 | 121 | 122 | class MonthsColumnTestCase(TestCase): 123 | def test_length(self): 124 | column1 = MonthsColumn(None, date(2012, 12, 18), date(2013, 07, 01)) 125 | self.assertEqual(len(column1), 8) 126 | column2 = MonthsColumn(None, date(2012, 12, 18), date(2012, 12, 19)) 127 | self.assertEqual(len(column2), 1) 128 | column3 = MonthsColumn(None, date(2012, 1, 18), date(2012, 12, 19)) 129 | self.assertEqual(len(column3), 12) 130 | 131 | def test_headers(self): 132 | column1 = MonthsColumn(None, date(2012, 12, 18), date(2012, 12, 19)) 133 | self.assertEqual(column1.headers, ['December']) 134 | column2 = MonthsColumn(None, date(2012, 12, 18), date(2013, 2, 1)) 135 | self.assertEqual(column2.headers, ['December', 'January', 'February']) 136 | 137 | 138 | class CalendarColumnTestCase(TestCase): 139 | def test_inline_month_columns(self): 140 | column = CalendarColumn(None, date(2012, 12, 18), date(2013, 03, 01)) 141 | self.assertEqual(len(column), 4 + 74 + 74) 142 | self.assertEqual(column.months_column.headers, ['December', 'January', 'February', 'March']) 143 | self.assertEqual(column.months_column[0].header.base_attrs['colspan'], '14') 144 | self.assertEqual(column.months_column[1].header.base_attrs['colspan'], '31') 145 | self.assertEqual(column.months_column[2].header.base_attrs['colspan'], '28') 146 | self.assertEqual(column.months_column[3].header.base_attrs['colspan'], '1') 147 | 148 | column = CalendarColumn(None, date(2014, 5, 4), date(2014, 5, 9)) 149 | self.assertEqual(len(column), 1 + 6 + 6) 150 | self.assertEqual(column.months_column.headers, ['May']) 151 | self.assertEqual(column.months_column[0].header.base_attrs['colspan'], '6') 152 | -------------------------------------------------------------------------------- /table/tests/test_tables.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from __future__ import unicode_literals 4 | 5 | from django.test import TestCase 6 | 7 | from table.tables import Table 8 | from table.columns import Column 9 | 10 | 11 | class TestTable(Table): 12 | foo = Column(field='id', header='ID', header_row_order=0) 13 | bar = Column(field='name', header='NAME', header_row_order=1) 14 | 15 | 16 | class BaseTableTestCase(TestCase): 17 | def setUp(self): 18 | data = [{'id': 1, 'name': 'a'}, {'id': 2, 'name': 'b'}] 19 | self.table = TestTable(data) 20 | 21 | def test_data_source(self): 22 | data = [{'id': 1, 'name': 'a'}, {'id': 2, 'name': 'b'}] 23 | self.assertEqual(self.table.data.list, data) 24 | 25 | def test_rows(self): 26 | first_row = self.table.rows[0].values() 27 | second_row = self.table.rows[1].values() 28 | self.assertEqual(first_row, ['1', 'a']) 29 | self.assertEqual(second_row, ['2', 'b']) 30 | 31 | def test_header_rows(self): 32 | first_header_row = [header.text for header in self.table.header_rows[0]] 33 | second_header_row = [header.text for header in self.table.header_rows[1]] 34 | self.assertEqual(first_header_row, ['ID']) 35 | self.assertEqual(second_header_row, ['NAME']) 36 | 37 | 38 | class TableDataMapTestCase(TestCase): 39 | def test_register(self): 40 | pass 41 | 42 | def test_get_data(self): 43 | pass 44 | 45 | def test_get_columns(self): 46 | pass 47 | 48 | 49 | class TableAddonsTestCase(TestCase): 50 | def test_basic_init(self): 51 | pass 52 | 53 | def test_render_dom(self): 54 | pass 55 | 56 | 57 | class TableOptionsTestCase(TestCase): 58 | def test_basic_init(self): 59 | pass 60 | 61 | 62 | class TableMetaClassTestCase(TestCase): 63 | def test_create_class_obj(self): 64 | pass 65 | -------------------------------------------------------------------------------- /table/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from __future__ import unicode_literals 4 | 5 | from django.db import models 6 | from django.test import TestCase 7 | 8 | from table.utils import Accessor 9 | 10 | 11 | class Publication(models.Model): 12 | title = models.CharField(max_length=30) 13 | 14 | class Meta: 15 | app_label = 'table' 16 | 17 | 18 | class Article(models.Model): 19 | headline = models.CharField(max_length=100) 20 | publications = models.ManyToManyField(Publication) 21 | 22 | class Meta: 23 | app_label = 'table' 24 | 25 | 26 | class AccessorTestCase(TestCase): 27 | 28 | def test_resolve_manager_instance(self): 29 | p1 = Publication(title='The Python Journal') 30 | p1.save() 31 | a1 = Article(headline='Django lets you build Web apps easily') 32 | a1.save() 33 | a1.publications.add(p1) 34 | 35 | self.assertEqual(Accessor('article_set.count').resolve(p1), 1) 36 | -------------------------------------------------------------------------------- /table/tests/test_views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from __future__ import unicode_literals 4 | import json 5 | import django 6 | 7 | from django.test import Client, TestCase 8 | 9 | if django.VERSION >= (1, 10): 10 | from django.urls import reverse 11 | else: 12 | from django.core.urlresolvers import reverse 13 | 14 | from table import Table 15 | from table.columns import Column 16 | from table.models import Person 17 | 18 | 19 | class TestTable(Table): 20 | id = Column('id', header='#') 21 | name = Column('name', header='NAME') 22 | email = Column('email', header='EMAIL', searchable=False) 23 | 24 | class Meta: 25 | model = Person 26 | ajax = True 27 | 28 | 29 | class FeedDataViewTestCase(TestCase): 30 | def setUp(self): 31 | self.table = TestTable() 32 | self.client = Client() 33 | self.url = reverse("feed_data", args=(self.table.token,)) 34 | self.payload = { 35 | "sEcho": "1", 36 | "iColumns": 2, 37 | "iDisplayStart": 0, 38 | "iDisplayLength": 10, 39 | "sSearch": "", 40 | "bRegex": False, 41 | "iSortingCols": 1 42 | } 43 | 44 | Person.objects.create(id=1, name="Tom", email="tom@mail.com") 45 | Person.objects.create(id=2, name="Jerry", email="jerry@mail.com") 46 | 47 | def test_model_data_source(self): 48 | response = self.client.get(self.url, self.payload) 49 | self.assertEqual(response.status_code, 200) 50 | 51 | data = json.loads(response.content) 52 | expect_data = { 53 | "sEcho": "1", 54 | "iTotalRecords": 2, 55 | "iTotalDisplayRecords": 2, 56 | "aaData": [["1", "Tom", "tom@mail.com"], ["2", "Jerry", "jerry@mail.com"]] 57 | } 58 | self.assertEqual(data, expect_data) 59 | 60 | def test_queryset_data_source(self): 61 | pass 62 | 63 | def test_search(self): 64 | url, payload = self.url, self.payload 65 | payload.update({"sSearch": "T"}) 66 | 67 | response = self.client.get(url, payload) 68 | self.assertEqual(response.status_code, 200) 69 | 70 | data = json.loads(response.content) 71 | expect_data = { 72 | "sEcho": "1", 73 | "iTotalRecords": 2, 74 | "iTotalDisplayRecords": 1, 75 | "aaData": [["1", "Tom", "tom@mail.com"]] 76 | } 77 | self.assertEqual(data, expect_data) 78 | 79 | def test_search_fuzzy(self): 80 | url, payload = self.url, self.payload 81 | payload.update({"sSearch": "T 2"}) 82 | 83 | response = self.client.get(url, payload) 84 | self.assertEqual(response.status_code, 200) 85 | 86 | data = json.loads(response.content) 87 | expect_data = { 88 | "sEcho": "1", 89 | "iTotalRecords": 2, 90 | "iTotalDisplayRecords": 0, 91 | "aaData": [] 92 | } 93 | self.assertEqual(data, expect_data) 94 | 95 | def test_unsearchable_column(self): 96 | url, payload = self.url, self.payload 97 | payload.update({"sSearch": "mail"}) 98 | 99 | response = self.client.get(url, payload) 100 | self.assertEqual(response.status_code, 200) 101 | 102 | data = json.loads(response.content) 103 | expect_data = { 104 | "sEcho": "1", 105 | "iTotalRecords": 2, 106 | "iTotalDisplayRecords": 0, 107 | "aaData": [] 108 | } 109 | self.assertEqual(data, expect_data) 110 | 111 | def test_sort_asc(self): 112 | url, payload = self.url, self.payload 113 | payload.update({ 114 | "iSortCol_0": 0, 115 | "sSortDir_0": "asc", 116 | }) 117 | 118 | response = self.client.get(url, payload) 119 | self.assertEqual(response.status_code, 200) 120 | 121 | data = json.loads(response.content) 122 | expect_data = { 123 | "sEcho": "1", 124 | "iTotalRecords": 2, 125 | "iTotalDisplayRecords": 2, 126 | "aaData": [["1", "Tom", "tom@mail.com"], ["2", "Jerry", "jerry@mail.com"]] 127 | } 128 | self.assertEqual(data, expect_data) 129 | 130 | def test_sort_desc(self): 131 | url, payload = self.url, self.payload 132 | payload.update({ 133 | "iSortCol_0": 0, 134 | "sSortDir_0": "desc", 135 | }) 136 | 137 | response = self.client.get(url, payload) 138 | self.assertEqual(response.status_code, 200) 139 | 140 | data = json.loads(response.content) 141 | expect_data = { 142 | "sEcho": "1", 143 | "iTotalRecords": 2, 144 | "iTotalDisplayRecords": 2, 145 | "aaData": [["2", "Jerry", "jerry@mail.com"], ["1", "Tom", "tom@mail.com"]] 146 | } 147 | self.assertEqual(data, expect_data) 148 | 149 | def test_paging(self): 150 | url, payload = self.url, self.payload 151 | 152 | # query 1st page 153 | payload.update({ 154 | "iDisplayStart": 0, 155 | "iDisplayLength": 1, 156 | }) 157 | response = self.client.get(url, payload) 158 | self.assertEqual(response.status_code, 200) 159 | data = json.loads(response.content) 160 | expect_data = { 161 | "sEcho": "1", 162 | "iTotalRecords": 2, 163 | "iTotalDisplayRecords": 2, 164 | "aaData": [["1", "Tom", "tom@mail.com"]] 165 | } 166 | self.assertEqual(data, expect_data) 167 | 168 | # query 2nd page 169 | payload.update({ 170 | "iDisplayStart": 1, 171 | "iDisplayLength": 1, 172 | }) 173 | response = self.client.get(url, payload) 174 | self.assertEqual(response.status_code, 200) 175 | data = json.loads(response.content) 176 | expect_data.update({ 177 | "aaData": [["2", "Jerry", "jerry@mail.com"]] 178 | }) 179 | self.assertEqual(data, expect_data) 180 | 181 | def test_paging_disabled(self): 182 | url, payload = self.url, self.payload 183 | payload.update({ 184 | "iDisplayStart": 0, 185 | "iDisplayLength": -1, 186 | }) 187 | response = self.client.get(url, payload) 188 | self.assertEqual(response.status_code, 200) 189 | data = json.loads(response.content) 190 | expect_data = { 191 | "sEcho": "1", 192 | "iTotalRecords": 2, 193 | "iTotalDisplayRecords": 2, 194 | "aaData": [["1", "Tom", "tom@mail.com"], ["2", "Jerry", "jerry@mail.com"]] 195 | } 196 | self.assertEqual(data, expect_data) 197 | 198 | def test_convert_queryset_to_values_list(self): 199 | pass 200 | -------------------------------------------------------------------------------- /table/tests/test_widgets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from django.test import TestCase 5 | 6 | from table.widgets import * 7 | from table.tables import Table, TableWidgets 8 | 9 | 10 | class TableWidgetsTestCase(TestCase): 11 | """ 12 | Test cases for datatable dom widgets. 13 | """ 14 | def test_searchbox(self): 15 | searchbox = SearchBox() 16 | self.assertEqual(searchbox.dom, "<'col-sm-3 col-md-3 col-lg-3'f>") 17 | self.assertEqual(searchbox.placeholder, "Search") 18 | searchbox = SearchBox(visible=False) 19 | self.assertEqual(searchbox.dom, "<'col-sm-3 col-md-3 col-lg-3'>") 20 | 21 | def test_pagiantion(self): 22 | pagination = Pagination() 23 | self.assertEqual(pagination.first, "First") 24 | self.assertEqual(pagination.last, "Last") 25 | self.assertEqual(pagination.prev, "Prev") 26 | self.assertEqual(pagination.next, "Next") 27 | self.assertEqual( 28 | pagination.dom, 29 | "<'col-sm-6 col-md-6 col-lg-6 col-sm-offset-2 col-md-offset-2 col-lg-offset-2'p>" 30 | ) 31 | pagination = Pagination(visible=False) 32 | self.assertEqual( 33 | pagination.dom, 34 | "<'col-sm-6 col-md-6 col-lg-6 col-sm-offset-2 col-md-offset-2 col-lg-offset-2'>" 35 | ) 36 | 37 | def test_info_label(self): 38 | label = InfoLabel() 39 | self.assertEqual(label.dom, "<'col-sm-3 col-md-3 col-lg-3'i>") 40 | label = InfoLabel(visible=False) 41 | self.assertEqual(label.dom, "<'col-sm-3 col-md-3 col-lg-3'>") 42 | 43 | def test_length_menu(self): 44 | menu = LengthMenu() 45 | self.assertEqual(menu.dom, "<'col-sm-1 col-md-1 col-lg-1'l>") 46 | menu = LengthMenu(visible=False) 47 | self.assertEqual(menu.dom, "<'col-sm-1 col-md-1 col-lg-1'>") 48 | 49 | def test_widgets_dom(self): 50 | class TestTable(Table): 51 | class Meta: 52 | search_placeholder = "test" 53 | info_format = "__TOTAL__" 54 | pagination_first = "F" 55 | pagination_last = "L" 56 | pagination_prev = "P" 57 | pagination_next = "N" 58 | 59 | table = TestTable([]) 60 | widgets = TableWidgets(table) 61 | self.assertEqual(widgets.search_box.placeholder, "test") 62 | self.assertEqual(widgets.info_label.format, "__TOTAL__") 63 | self.assertEqual(widgets.pagination.first, "F") 64 | self.assertEqual(widgets.pagination.last, "L") 65 | self.assertEqual(widgets.pagination.prev, "P") 66 | self.assertEqual(widgets.pagination.next, "N") 67 | self.assertEqual( 68 | widgets.render_dom(), 69 | "<'row'<'col-sm-9 col-md-9 col-lg-9'>" 70 | "<'col-sm-3 col-md-3 col-lg-3'f>>" 71 | "rt" 72 | "<'row'<'col-sm-3 col-md-3 col-lg-3'i>" 73 | "<'col-sm-6 col-md-6 col-lg-6 col-sm-offset-2 col-md-offset-2 col-lg-offset-2'p>" 74 | "<'col-sm-1 col-md-1 col-lg-1'l>>" 75 | ) 76 | 77 | def test_widgets_dom_simple(self): 78 | class SimpleTable(Table): 79 | class Meta: 80 | search = False 81 | pagination = False 82 | info = False 83 | length_menu = False 84 | table = SimpleTable([]) 85 | widgets = TableWidgets(table) 86 | self.assertEqual(widgets.render_dom(), "rt") 87 | -------------------------------------------------------------------------------- /table/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from django.conf.urls import url 4 | 5 | from table.views import FeedDataView 6 | 7 | 8 | urlpatterns = [ 9 | url(r'^ajax/(?P\w{32})/$', FeedDataView.as_view(), name='feed_data'), 10 | ] 11 | -------------------------------------------------------------------------------- /table/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import time 4 | 5 | from django.utils.html import escape 6 | from django.utils.safestring import mark_safe 7 | 8 | 9 | class Accessor(str): 10 | """ A string describing a path from one object to another via attribute/index 11 | accesses. For convenience, the class has an alias `.A` to allow for more concise code. 12 | 13 | Relations are separated by a "." character. 14 | """ 15 | SEPARATOR = '.' 16 | 17 | def resolve(self, context, quiet=True): 18 | """ 19 | Return an object described by the accessor by traversing the attributes 20 | of context. 21 | 22 | """ 23 | try: 24 | obj = context 25 | for level in self.levels: 26 | if isinstance(obj, dict): 27 | obj = obj[level] 28 | elif isinstance(obj, list) or isinstance(obj, tuple): 29 | obj = obj[int(level)] 30 | else: 31 | if callable(getattr(obj, level)): 32 | try: 33 | obj = getattr(obj, level)() 34 | except KeyError: 35 | obj = getattr(obj, level) 36 | else: 37 | # for model field that has choice set 38 | # use get_xxx_display to access 39 | display = 'get_%s_display' % level 40 | obj = getattr(obj, display)() if hasattr(obj, display) else getattr(obj, level) 41 | if not obj: 42 | break 43 | return obj 44 | except Exception as e: 45 | if quiet: 46 | return '' 47 | else: 48 | raise e 49 | 50 | @property 51 | def levels(self): 52 | if self == '': 53 | return () 54 | return self.split(self.SEPARATOR) 55 | 56 | A = Accessor 57 | 58 | 59 | class AttributesDict(dict): 60 | """ 61 | A `dict` wrapper to render as HTML element attributes. 62 | """ 63 | def render(self): 64 | return mark_safe(' '.join([ 65 | '%s="%s"' % (attr_name, escape(attr)) 66 | for attr_name, attr in self.items() 67 | ])) 68 | 69 | 70 | def timeit(func): 71 | def wrap(*args, **kwargs): 72 | ts = time.time() 73 | result = func(*args, **kwargs) 74 | te = time.time() 75 | print('func: %r took: %f ms'.format(func.__name__, (te - ts) * 1000)) 76 | return result 77 | return wrap 78 | -------------------------------------------------------------------------------- /table/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import json 4 | from functools import reduce 5 | 6 | from django.core.serializers.json import DjangoJSONEncoder 7 | from django.db.models import Q 8 | from django.http import HttpResponse 9 | from django.views.generic.list import BaseListView 10 | 11 | from table.forms import QueryDataForm 12 | from table.tables import TableDataMap 13 | 14 | 15 | class JSONResponseMixin(object): 16 | """ 17 | A mixin that can be used to render a JSON response. 18 | """ 19 | def render_to_json_response(self, context, **response_kwargs): 20 | """ 21 | Returns a JSON response, transforming 'context' to make the payload. 22 | """ 23 | return HttpResponse( 24 | self.convert_context_to_json(context), 25 | content_type='application/json', 26 | **response_kwargs 27 | ) 28 | 29 | def convert_context_to_json(self, context): 30 | """ 31 | Convert the context dictionary into a JSON object. 32 | """ 33 | return json.dumps(context, cls=DjangoJSONEncoder) 34 | 35 | 36 | class FeedDataView(JSONResponseMixin, BaseListView): 37 | """ 38 | The view to feed ajax data of table. 39 | """ 40 | 41 | context_object_name = 'object_list' 42 | 43 | def get(self, request, *args, **kwargs): 44 | if not hasattr(self, 'token'): 45 | self.token = kwargs["token"] 46 | self.columns = TableDataMap.get_columns(self.token) 47 | 48 | query_form = QueryDataForm(request.GET) 49 | if query_form.is_valid(): 50 | self.query_data = query_form.cleaned_data 51 | else: 52 | return self.render_to_response({"error": "Query form is invalid."}) 53 | return BaseListView.get(self, request, *args, **kwargs) 54 | 55 | def get_queryset(self): 56 | model = TableDataMap.get_model(self.token) 57 | if model is None: 58 | return None 59 | return model.objects.all() 60 | 61 | def filter_queryset(self, queryset): 62 | def get_filter_arguments(filter_target): 63 | """ 64 | Get `Q` object passed to `filter` function. 65 | """ 66 | queries = [] 67 | fields = [col.field for col in self.columns if col.searchable] 68 | for field in fields: 69 | key = "__".join(field.split(".") + ["icontains"]) 70 | value = filter_target 71 | queries.append(Q(**{key: value})) 72 | return reduce(lambda x, y: x | y, queries) 73 | 74 | filter_text = self.query_data["sSearch"] 75 | if filter_text: 76 | for target in filter_text.split(): 77 | queryset = queryset.filter(get_filter_arguments(target)) 78 | return queryset 79 | 80 | def sort_queryset(self, queryset): 81 | def get_sort_arguments(): 82 | """ 83 | Get list of arguments passed to `order_by()` function. 84 | """ 85 | arguments = [] 86 | for key, value in self.query_data.items(): 87 | if not key.startswith("iSortCol_"): 88 | continue 89 | field = self.columns[value].field.replace('.', '__') 90 | dir = self.query_data["sSortDir_" + key.split("_")[1]] 91 | if dir == "asc": 92 | arguments.append(field) 93 | else: 94 | arguments.append("-" + field) 95 | return arguments 96 | order_args = get_sort_arguments() 97 | if order_args: 98 | queryset = queryset.order_by(*order_args) 99 | return queryset 100 | 101 | def paging_queryset(self, queryset): 102 | start = self.query_data["iDisplayStart"] 103 | length = self.query_data["iDisplayLength"] 104 | if length < 0: 105 | return queryset 106 | else: 107 | return queryset[start: start + length] 108 | 109 | def convert_queryset_to_values_list(self, queryset): 110 | # FIXME: unit test 111 | return [ 112 | [col.render(obj) for col in self.columns] 113 | for obj in queryset 114 | ] 115 | 116 | def get_queryset_length(self, queryset): 117 | return queryset.count() 118 | 119 | def get_context_data(self, **kwargs): 120 | """ 121 | Get context data for datatable server-side response. 122 | See http://www.datatables.net/usage/server-side 123 | """ 124 | sEcho = self.query_data["sEcho"] 125 | 126 | context = super(BaseListView, self).get_context_data(**kwargs) 127 | queryset = context["object_list"] 128 | if queryset is not None: 129 | total_length = self.get_queryset_length(queryset) 130 | queryset = self.filter_queryset(queryset) 131 | display_length = self.get_queryset_length(queryset) 132 | 133 | queryset = self.sort_queryset(queryset) 134 | queryset = self.paging_queryset(queryset) 135 | values_list = self.convert_queryset_to_values_list(queryset) 136 | context = { 137 | "sEcho": sEcho, 138 | "iTotalRecords": total_length, 139 | "iTotalDisplayRecords": display_length, 140 | "aaData": values_list, 141 | } 142 | else: 143 | context = { 144 | "sEcho": sEcho, 145 | "iTotalRecords": 0, 146 | "iTotalDisplayRecords": 0, 147 | "aaData": [], 148 | } 149 | return context 150 | 151 | def render_to_response(self, context, **response_kwargs): 152 | return self.render_to_json_response(context) 153 | -------------------------------------------------------------------------------- /table/widgets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from django.template.loader import get_template 4 | from django.template import Context, Template 5 | from django.utils.safestring import mark_safe 6 | 7 | 8 | class SearchBox(object): 9 | def __init__(self, visible=True, placeholder=None): 10 | self.visible = visible 11 | self.placeholder = placeholder or "Search" 12 | 13 | @property 14 | def dom(self): 15 | if self.visible: 16 | return "<'col-sm-3 col-md-3 col-lg-3'f>" 17 | else: 18 | return "<'col-sm-3 col-md-3 col-lg-3'>" 19 | 20 | 21 | class InfoLabel(object): 22 | def __init__(self, visible=True, format=None): 23 | self.visible = visible 24 | self.format = format or "Total _TOTAL_" 25 | 26 | @property 27 | def dom(self): 28 | if self.visible: 29 | return "<'col-sm-3 col-md-3 col-lg-3'i>" 30 | else: 31 | return "<'col-sm-3 col-md-3 col-lg-3'>" 32 | 33 | 34 | class Pagination(object): 35 | def __init__(self, visible=True, length=10, first=None, 36 | last=None, prev=None, next=None): 37 | self.visible = visible 38 | self.length = length 39 | self.first = first or "First" 40 | self.last = last or "Last" 41 | self.prev = prev or "Prev" 42 | self.next = next or "Next" 43 | 44 | @property 45 | def dom(self): 46 | if self.visible: 47 | return ("<'col-sm-6 col-md-6 col-lg-6 col-sm-offset-2 " 48 | "col-md-offset-2 col-lg-offset-2'p>") 49 | else: 50 | return ("<'col-sm-6 col-md-6 col-lg-6 col-sm-offset-2 " 51 | "col-md-offset-2 col-lg-offset-2'>") 52 | 53 | 54 | class LengthMenu(object): 55 | def __init__(self, visible=True): 56 | self.visible = visible 57 | 58 | @property 59 | def dom(self): 60 | if self.visible: 61 | return "<'col-sm-1 col-md-1 col-lg-1'l>" 62 | else: 63 | return "<'col-sm-1 col-md-1 col-lg-1'>" 64 | --------------------------------------------------------------------------------