diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..f8c753d0 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,10 @@ +[run] +branch = True +omit = + */migrations/* + +[report] +fail_under = 70 +omit = + */migrations/* + */test* diff --git a/.gitignore b/.gitignore index e123965b..890c0d42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,107 @@ -*.pyc -*.pyo +# Created by .ignore support plugin (hsz.mobi) +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: *.log -.DS_Store -Thumbs.db -*.orig -dist -build -django_datatable_view.egg-info -.tm_properties -.python-version +local_settings.py db.sqlite3 -appengine -docs/_build -.idea -.venv/ \ No newline at end of file + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..aaae84e2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +repos: + - repo: git://github.com/pre-commit/pre-commit-hooks + rev: v1.4.0 + hooks: + - id: check-added-large-files + args: ['--maxkb=500'] + - id: check-byte-order-marker + - id: check-case-conflict + - id: check-merge-conflict + - id: check-symlinks + - id: detect-private-key + - id: flake8 + files: datatableview + exclude: tests/* + args: ['--config=tox.ini'] + - id: fix-encoding-pragma + - id: end-of-file-fixer + exclude: .idea/* + - id: trailing-whitespace + exclude: .idea/* + - id: mixed-line-ending + exclude: .idea/* + - id: double-quote-string-fixer + exclude: .idea/* + - id: check-json + files: .gitignore + - repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.16 # Use the revision sha / tag you want to point at + hooks: + - id: isort + files: datatableview + args: ['--recursive', '--diff'] diff --git a/.travis.yml b/.travis.yml index 6ee980bd..18877ad4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,31 +1,30 @@ -sudo: false +dist: xenial + language: python + +sudo: false + +git: + depth: 1 + quiet: true + +cache: pip + python: - - "2.7" - - "3.4" - - "3.5" - - "3.6" - # - "3.7" -env: - - DJANGO=1.11.16 - - DJANGO=2.0.9 - - DJANGO=2.1.2 + - "3.7" + - "3.8" + - "3.9" + - "3.10-dev" + install: - - pip install -q Django==$DJANGO - # - pip install pep8 - # - pip install pyflakes - - pip install -q -e . -# before_script: -# - "pep8 --exclude=migrations --ignore=E501,E225 datatableview" -# - pyflakes datatableview -script: - - python datatableview/tests/example_project/manage.py test - -matrix: - exclude: - - python: "2.7" - env: DJANGO=2.0.9 - - python: "2.7" - env: DJANGO=2.1.2 - - python: "3.4" - env: DJANGO=2.1.2 + - 'pip install tox-travis pyyaml python-coveralls' + +script: tox + +notifications: + slack: + rooms: + - pivotalenergy:SvNSkVaLVlZeu82utL37wSvy#travis + +after_success: + - coveralls diff --git a/CHANGELOG.md b/CHANGELOG.md index c0762fe5..b3f9a257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -337,7 +337,7 @@ class MyDatatableView(DatatableView): def get_json_response_object(self, object_list, *args, **kwargs): data = super(MyDatatableView, self).get_json_response_object(object_list, *args, **kwargs) - # Keep customizations JSON-compatible! :) + # Keep customizations JSON-compatible! :) data.update({ 'special_arg': self.kwargs['special_arg'], }) diff --git a/LICENSE.txt b/LICENSE.txt index d67afcf6..0d1629fa 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -199,4 +199,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/datatableview/__init__.py b/datatableview/__init__.py index b8348d88..c097377b 100644 --- a/datatableview/__init__.py +++ b/datatableview/__init__.py @@ -7,8 +7,12 @@ __name__ = 'datatableview' __author__ = 'Autumn Valenta' -__version_info__ = (0, 9, 0) +__version_info__ = (1, 0, 0) __version__ = '.'.join(map(str, __version_info__)) __date__ = '2013/11/14 2:00:00 PM' __credits__ = ['Autumn Valenta', 'Steven Klass'] __license__ = 'See the file LICENSE.txt for licensing information.' + +__all__ = ['Datatable', 'ValuesDatatable', 'LegacyDatatable', 'Column', 'TextColumn', + 'DateColumn', 'DateTimeColumn', 'BooleanColumn', 'IntegerColumn', 'FloatColumn', + 'DisplayColumn', 'CompoundColumn', 'CheckBoxSelectColumn', 'SkipRecord'] diff --git a/datatableview/cache.py b/datatableview/cache.py index d74e87ef..2b427a36 100644 --- a/datatableview/cache.py +++ b/datatableview/cache.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import inspect import hashlib import logging @@ -42,11 +43,11 @@ class cache_types(object): cache = caches[CACHE_BACKEND] - hash_slice = None if CACHE_KEY_HASH: hash_slice = slice(None, CACHE_KEY_HASH_LENGTH) + def _hash_key_component(s): return hashlib.sha1(s.encode('utf-8')).hexdigest()[hash_slice] @@ -93,7 +94,7 @@ def get_cache_key(datatable_class, view=None, user=None, **kwargs): kwargs_id = _hash_key_component(kwargs_id) cache_key += '__kwargs_%s' % (kwargs_id,) - log.debug("Cache key derived for %r: %r (from kwargs %r)", datatable_class, cache_key, values) + log.debug('Cache key derived for %r: %r (from kwargs %r)', datatable_class, cache_key, values) return cache_key @@ -102,12 +103,12 @@ def get_cached_data(datatable, **kwargs): """ Returns the cached object list under the appropriate key, or None if not set. """ cache_key = '%s%s' % (CACHE_PREFIX, datatable.get_cache_key(**kwargs)) data = cache.get(cache_key) - log.debug("Reading data from cache at %r: %r", cache_key, data) + log.debug('Reading data from cache at %r: %r', cache_key, data) return data def cache_data(datatable, data, **kwargs): """ Stores the object list in the cache under the appropriate key. """ cache_key = '%s%s' % (CACHE_PREFIX, datatable.get_cache_key(**kwargs)) - log.debug("Setting data to cache at %r: %r", cache_key, data) + log.debug('Setting data to cache at %r: %r', cache_key, data) cache.set(cache_key, data) diff --git a/datatableview/columns.py b/datatableview/columns.py index 1895a4d6..e7017ceb 100644 --- a/datatableview/columns.py +++ b/datatableview/columns.py @@ -1,35 +1,26 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- import re import operator from datetime import datetime -try: - from functools import reduce -except ImportError: - pass +from functools import reduce +import logging -import django from django.db import models from django.db.models import Model, Manager, Q -from django.db.models.fields import FieldDoesNotExist -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, FieldDoesNotExist from django.utils.encoding import smart_text from django.utils.safestring import mark_safe -try: - from django.forms.utils import flatatt -except ImportError: - from django.forms.util import flatatt +from django.forms.utils import flatatt from django.template.defaultfilters import slugify -try: - from django.utils.encoding import python_2_unicode_compatible -except ImportError: - from .compat import python_2_unicode_compatible -import six import dateutil.parser from .utils import resolve_orm_path, DEFAULT_EMPTY_VALUE, DEFAULT_MULTIPLE_SEPARATOR + +log = logging.getLogger(__name__) + # Registry of Column subclasses to their declared corresponding ModelFields. # The registery is an ordered priority list, containing 2-tuples of a Column subclass and a list of # classes that the column will service. @@ -38,17 +29,19 @@ STRPTIME_PLACEHOLDERS = { 'year': ('%y', '%Y'), 'month': ('%m', '%b', '%B'), - 'day': ('%d',),# '%a', '%A'), # day names are hard because they depend on other date info + 'day': ('%d',), # '%a', '%A'), # day names are hard because they depend on other date info 'hour': ('%H', '%I'), 'minute': ('%M',), 'second': ('%S',), 'week_day': ('%w',), } + def register_simple_modelfield(model_field): column_class = get_column_for_modelfield(model_field) COLUMN_CLASSES.insert(0, (column_class, [model_field])) + def get_column_for_modelfield(model_field): """ Return the built-in Column class for a model field class. """ @@ -62,6 +55,7 @@ def get_column_for_modelfield(model_field): if isinstance(model_field, tuple(modelfield_classes)): return ColumnClass + def get_attribute_value(obj, bit): try: value = getattr(obj, bit) @@ -73,8 +67,10 @@ def get_attribute_value(obj, bit): value = value() return value + class ColumnMetaclass(type): """ Column type for automatic registration of column types as ModelField handlers. """ + def __new__(cls, name, bases, attrs): new_class = super(ColumnMetaclass, cls).__new__(cls, name, bases, attrs) if new_class.model_field_class: @@ -85,8 +81,7 @@ def __new__(cls, name, bases, attrs): # Corollary to django.forms.fields.Field -@python_2_unicode_compatible -class Column(six.with_metaclass(ColumnMetaclass)): +class Column(metaclass=ColumnMetaclass): """ Generic table column using CharField for rendering. """ model_field_class = None @@ -196,7 +191,7 @@ def get_initial_value(self, obj, **kwargs): value = self.empty_value elif len(values) > 0: plain_value = [v[0] for v in values] - rich_value = self.separator.join(map(six.text_type, [v[1] for v in values])) + rich_value = self.separator.join(map(str, [v[1] for v in values])) value = (plain_value, rich_value) else: value = self.empty_value @@ -211,14 +206,14 @@ def get_source_value(self, obj, source, **kwargs): ``Column`` instances will have sources of their own and need to return a value per nested source. """ - if hasattr(source, "__call__"): + if hasattr(source, '__call__'): value = source(obj) elif isinstance(obj, Model): value = reduce(get_attribute_value, [obj] + source.split('__')) elif isinstance(obj, dict): # ValuesQuerySet item value = obj[source] else: - raise ValueError("Unknown object type %r" % (repr(obj),)) + raise ValueError('Unknown object type %r' % (repr(obj),)) return [value] def get_processor_kwargs(self, **extra_kwargs): @@ -267,7 +262,7 @@ def expand_source(self, source): def resolve_source(self, model, source): # Try to fetch the leaf attribute. If this fails, the attribute is not database-backed and # the search for the first non-database field should end. - if hasattr(source, "__call__"): + if hasattr(source, '__call__'): return None try: return resolve_orm_path(model, source) @@ -288,15 +283,15 @@ def prep_search_value(self, term, lookup_type): # We avoid making changes that the Django ORM can already do for us multi_terms = None - if isinstance(term, six.text_type): - if lookup_type == "in": + if isinstance(term, str): + if lookup_type == 'in': in_bits = re.split(r',\s*', term) if len(in_bits) > 1: multi_terms = in_bits else: term = None - if lookup_type == "range": + if lookup_type == 'range': range_bits = re.split(r'\s*-\s*', term) if len(range_bits) == 2: multi_terms = range_bits @@ -304,12 +299,14 @@ def prep_search_value(self, term, lookup_type): term = None if multi_terms: - return filter(None, (self.prep_search_value(multi_term, lookup_type) for multi_term in multi_terms)) + return filter(None, (self.prep_search_value(multi_term, lookup_type) for multi_term in + multi_terms)) model_field = self.model_field_class() try: term = model_field.get_prep_value(term) - except: + except Exception as err: + log.warning(f'model_field.get_prep_value({term}) - {err}') term = None return term @@ -410,7 +407,7 @@ def attributes(self): } if self.sort_priority is not None: - attributes['data-config-sorting'] = ','.join(map(six.text_type, [ + attributes['data-config-sorting'] = ','.join(map(str, [ self.sort_priority, self.index, self.sort_direction, @@ -455,7 +452,8 @@ def prep_search_value(self, term, lookup_type): if lookup_type == 'week_day': try: test_term = int(test_term) - 1 # Django ORM uses 1-7, python strptime uses 0-6 - except: + except Exception as err: + log.warning(f'int({test_term}) - 1 -- {err}') return None else: test_term = str(test_term) @@ -479,7 +477,8 @@ def prep_search_value(self, term, lookup_type): class DateTimeColumn(DateColumn): model_field_class = models.DateTimeField handles_field_classes = [models.DateTimeField] - lookup_types = ('exact', 'in', 'range', 'year', 'month', 'day', 'week_day', 'hour', 'minute', 'second') + lookup_types = ( + 'exact', 'in', 'range', 'year', 'month', 'day', 'week_day', 'hour', 'minute', 'second') class TimeColumn(DateColumn): diff --git a/datatableview/compat.py b/datatableview/compat.py deleted file mode 100644 index 3c67aa82..00000000 --- a/datatableview/compat.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- encoding: utf-8 -*- -""" Backports of code left behind by new versions of Django. """ - -import django -from django.utils.html import escape -try: - from django.utils.encoding import escape_uri_path as django_escape_uri_path -except ImportError: - django_escape_uri_path = None - -import six - - -# Django 1.7 removed StrAndUnicode, so it has been purged from this project as well. To bridge the -# gap, we will rely on this utility directly, instead of trying to generate our own replacement -# StrAndUnicode class. -def python_2_unicode_compatible(klass): - """ - A decorator that defines __unicode__ and __str__ methods under Python 2. - Under Python 3 it does nothing. - - To support Python 2 and 3 with a single code base, define a __str__ method - returning text and apply this decorator to the class. - """ - if not six.PY3: - klass.__unicode__ = klass.__str__ - klass.__str__ = lambda self: self.__unicode__().encode('utf-8') - return klass - - -def escape_uri_path(path): - if django_escape_uri_path: - return django_escape_uri_path(path) - return escape(path) diff --git a/datatableview/datatables.py b/datatableview/datatables.py index 5eb17509..1752a5b9 100644 --- a/datatableview/datatables.py +++ b/datatableview/datatables.py @@ -1,36 +1,25 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- import re import copy import operator from collections import OrderedDict -try: - from functools import reduce -except ImportError: - pass -from django.db.models.fields import FieldDoesNotExist -from django.template.loader import render_to_string -from django.db.models import QuerySet -try: - from django.utils.encoding import force_text -except ImportError: - from django.utils.encoding import force_unicode as force_text -try: - from django.utils.encoding import python_2_unicode_compatible -except ImportError: - from .compat import python_2_unicode_compatible +from django.core.exceptions import FieldDoesNotExist -import six +from functools import reduce +from django.template.loader import render_to_string +from django.db.models import QuerySet +from django.utils.encoding import force_text from .exceptions import ColumnError, SkipRecord -from .columns import (Column, TextColumn, DateColumn, DateTimeColumn, BooleanColumn, IntegerColumn, - FloatColumn, DisplayColumn, CompoundColumn, get_column_for_modelfield) +from .columns import (Column, TextColumn, get_column_for_modelfield) from .utils import (OPTION_NAME_MAP, MINIMUM_PAGE_LENGTH, contains_plural_field, split_terms, resolve_orm_path) from .cache import DEFAULT_CACHE_TYPE, cache_types, get_cache_key, cache_data, get_cached_data + def pretty_name(name): if not name: return '' @@ -50,7 +39,7 @@ def columns_for_model(model, fields=None, exclude=None, labels=None, processors= column_class = get_column_for_modelfield(f) if column_class is None: - raise ColumnError("Unhandled model field %r." % (f,)) + raise ColumnError('Unhandled model field %r.' % (f,)) if labels and f.name in labels: label = labels[f.name] else: @@ -77,10 +66,11 @@ def columns_for_model(model, fields=None, exclude=None, labels=None, processors= if fields: field_dict = OrderedDict( [(f, field_dict.get(f)) for f in fields - if (not exclude) or (exclude and f not in exclude)] + if (not exclude) or (exclude and f not in exclude)] ) return field_dict + # Borrowed from the Django forms implementation def get_declared_columns(bases, attrs, with_base_columns=True): """ @@ -94,9 +84,8 @@ def get_declared_columns(bases, attrs, with_base_columns=True): Also integrates any additional media definitions """ local_columns = [ - (column_name, attrs.pop(column_name)) \ - for column_name, obj in list(six.iteritems(attrs)) \ - if isinstance(obj, Column) + (column_name, attrs.pop(column_name)) + for column_name, obj in list(attrs.items()) if isinstance(obj, Column) ] local_columns.sort(key=lambda x: x[1].creation_counter) @@ -106,19 +95,21 @@ def get_declared_columns(bases, attrs, with_base_columns=True): if with_base_columns: for base in bases[::-1]: if hasattr(base, 'base_columns'): - local_columns = list(six.iteritems(base.base_columns)) + local_columns + local_columns = list(base.base_columns.items()) + local_columns else: for base in bases[::-1]: if hasattr(base, 'declared_columns'): - local_columns = list(six.iteritems(base.declared_columns)) + local_columns + local_columns = list(base.declared_columns.items()) + local_columns return OrderedDict(local_columns) + class DatatableOptions(object): """ Contains declarable options for a datatable, some of which can be manipuated by subsequent requests by the user. """ + def __init__(self, options=None): # Non-mutable; server's declared preference is final self.id = getattr(options, 'id', '') @@ -131,7 +122,8 @@ def __init__(self, options=None): self.labels = getattr(options, 'labels', None) self.processors = getattr(options, 'processors', None) self.request_method = getattr(options, 'request_method', 'GET') - self.structure_template = getattr(options, 'structure_template', "datatableview/default_structure.html") + self.structure_template = getattr(options, 'structure_template', + 'datatableview/default_structure.html') self.footer = getattr(options, 'footer', False) self.result_counter_id = getattr(options, 'result_counter_id', 'id_count') @@ -146,6 +138,7 @@ def __init__(self, options=None): default_options = DatatableOptions() + class DatatableMetaclass(type): """ Each declared Datatable object inspects its declared "fields" in order to facilitate an @@ -161,8 +154,9 @@ def __new__(cls, name, bases, attrs): opts = new_class._meta = new_class.options_class(getattr(new_class, 'Meta', None)) if opts.model: columns = columns_for_model(opts.model, opts.columns, opts.exclude, opts.labels, - opts.processors, opts.unsortable_columns, opts.hidden_columns) - none_model_columns = [k for k, v in six.iteritems(columns) if not v] + opts.processors, opts.unsortable_columns, + opts.hidden_columns) + none_model_columns = [k for k, v in columns.items() if not v] missing_columns = set(none_model_columns) - set(declared_columns.keys()) for name, column in declared_columns.items(): @@ -195,7 +189,7 @@ def __new__(cls, name, bases, attrs): opts.search_fields = list(opts.search_fields) for i, column in enumerate(opts.search_fields): # Build a column object - if isinstance(column, six.string_types): + if isinstance(column, str): name = column field = resolve_orm_path(opts.model, name) column = get_column_for_modelfield(field) @@ -212,8 +206,7 @@ def __new__(cls, name, bases, attrs): return new_class -@python_2_unicode_compatible -class Datatable(six.with_metaclass(DatatableMetaclass)): +class Datatable(metaclass=DatatableMetaclass): """ Declaration container for a clientside datatable, containing an optional Meta inner class, class-level field declarations, and callbacks for filtering and post-processing values requested @@ -352,7 +345,8 @@ def normalize_config_ordering(self, config, query_config): for sort_queue_i in range(len(columns_list)): try: - column_index = int(query_config.get(OPTION_NAME_MAP['sort_column'] % sort_queue_i, '')) + column_index = int( + query_config.get(OPTION_NAME_MAP['sort_column'] % sort_queue_i, '')) except ValueError: continue @@ -362,7 +356,8 @@ def normalize_config_ordering(self, config, query_config): if column.name in config['unsortable_columns']: continue - sort_direction = query_config.get(OPTION_NAME_MAP['sort_column_direction'] % sort_queue_i, None) + sort_direction = query_config.get( + OPTION_NAME_MAP['sort_column_direction'] % sort_queue_i, None) if sort_direction == 'asc': sort_modifier = '' @@ -403,7 +398,7 @@ def resolve_virtual_columns(self, *names): an apparent configuration error. """ if names: - raise ColumnError("Unknown column name(s): %r" % (names,)) + raise ColumnError('Unknown column name(s): %r' % (names,)) # Reflection methods for wrapped columns def get_ordering_splits(self): @@ -417,8 +412,6 @@ def get_ordering_splits(self): if self.config['ordering'] is None: return [], [] - db_fields = [] - virtual_fields = [] i = 0 for i, name in enumerate(self.config['ordering']): if name[0] in '+-': @@ -453,7 +446,7 @@ def will_load_from_cache(self, **kwargs): this hint to be accurate. """ cached_data = self.get_cached_data(datatable_class=self.__class__, **kwargs) - return (type(cached_data) is not type(None)) + return not isinstance(cached_data, type(None)) def get_cache_key_kwargs(self, view=None, user=None, **kwargs): """ @@ -544,7 +537,7 @@ def prepare_object_list_for_cache(self, cache_type, object_list): # Create the simplest reproducable query for repeated operations between requests # Note that 'queryset' cache_type is unhandled so that it passes straight through. if cache_type == cache_types.PK_LIST: - model = object_list.model + # model = object_list.model data = tuple(object_list.values_list('pk', flat=True)) # Objects in some other type of data structure should be pickable for cache backend @@ -755,11 +748,13 @@ def flatten(value): def force_distinct(self, object_list): seen = set() + def is_unseen(obj): if obj.pk in seen: return False seen.add(obj.pk) return True + return tuple(obj for obj in object_list if is_unseen(obj)) # Per-record callbacks @@ -825,10 +820,8 @@ def get_record_data(self, obj): if isinstance(value, (tuple, list)): value = value[1] - if six.PY2 and isinstance(value, str): # not unicode - value = value.decode('utf-8') if value is not None: - value = six.text_type(value) + value = str(value) data[str(i)] = value return data @@ -874,7 +867,7 @@ def get_processor_method(self, column, i): column_name = column.name if isinstance(self, LegacyDatatable): - name = force_text(column.label, errors="ignore") + name = force_text(column.label, errors='ignore') if not name: name = column.sources[0] column_name = re.sub(r'[\W_]+', '_', name) @@ -898,7 +891,6 @@ def get_processor_method(self, column, i): return None - # Template rendering features def __str__(self): """ Renders ``structure_template`` with ``self`` as a context variable. """ @@ -933,6 +925,7 @@ class ValuesDatatable(Datatable): Processor callbacks will no longer receive model instances, but instead the dict of selected values. """ + def get_valuesqueryset(self, queryset): # Figure out the full list of ORM path names self.value_queries = OrderedDict({'pk': 'pk'}) diff --git a/datatableview/exceptions.py b/datatableview/exceptions.py index 482d879b..efb2c043 100644 --- a/datatableview/exceptions.py +++ b/datatableview/exceptions.py @@ -1,7 +1,8 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- class ColumnError(Exception): """ Some kind of problem with a datatable column. """ + class SkipRecord(Exception): """ User-raised problem with a record during serialization. """ diff --git a/datatableview/forms.py b/datatableview/forms.py index cd308791..9fa2228a 100644 --- a/datatableview/forms.py +++ b/datatableview/forms.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- from django import forms from django.forms import ValidationError @@ -44,5 +44,5 @@ def clean_name(self): else: field_names = self.model._meta.get_all_field_names() if field_name not in field_names: - raise ValidationError("%r is not a valid field." % field_name) + raise ValidationError('%r is not a valid field.' % field_name) return field_name diff --git a/datatableview/helpers.py b/datatableview/helpers.py index 8c7250db..1969610d 100644 --- a/datatableview/helpers.py +++ b/datatableview/helpers.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- """ A collection of small helper functions for generating small pieces of datatable output in custom methods on a view. @@ -12,21 +12,12 @@ from functools import partial, wraps import operator -from django import get_version from django.db.models import Model -try: - from django.forms.utils import flatatt -except ImportError: - from django.forms.util import flatatt - -import six +from django.forms.utils import flatatt from .utils import resolve_orm_path, XEDITABLE_FIELD_TYPES -if [int(v) for v in get_version().split('.')[0:2]] >= [1, 5]: - from django.utils.timezone import localtime -else: - localtime = None +from django.utils.timezone import localtime def keyed_helper(helper): @@ -69,7 +60,7 @@ def wrapper(instance=None, key=None, attr=None, *args, **kwargs): if attr: if attr == 'self': - key = lambda obj: obj + key = lambda obj: obj # noqa: E731 else: key = operator.attrgetter(attr) @@ -78,11 +69,13 @@ def wrapper(instance=None, key=None, attr=None, *args, **kwargs): @wraps(helper) def helper_wrapper(instance, *args, **kwargs): return helper(key(instance), *args, **kwargs) + return helper_wrapper wrapper._is_wrapped = True return wrapper + @keyed_helper def link_to_model(instance, text=None, *args, **kwargs): """ @@ -116,12 +109,12 @@ def link_to_model(instance, text=None, *args, **kwargs): processor=link_to_model(key=getattr('relatedobject'))) """ if not text: - text = kwargs.get('rich_value') or six.text_type(instance) + text = kwargs.get('rich_value') or str(instance) return u"""{1}""".format(instance.get_absolute_url(), text) @keyed_helper -def make_boolean_checkmark(value, true_value="✔", false_value="✘", *args, **kwargs): +def make_boolean_checkmark(value, true_value='✔', false_value='✘', *args, **kwargs): """ Returns a unicode ✔ or ✘, configurable by pre-calling the helper with ``true_value`` and/or ``false_value`` arguments, based on the incoming value. @@ -169,15 +162,16 @@ def itemgetter(k, ellipsis=False, key=None): processor=itemgetter(slice(None, 30), ellipsis=True)) """ + def helper(instance, *args, **kwargs): default_value = kwargs.get('default_value') if default_value is None: default_value = instance value = default_value[k] - if ellipsis and isinstance(k, slice) and isinstance(value, six.string_types) and \ + if ellipsis and isinstance(k, slice) and isinstance(value, str) and \ len(default_value) > len(value): if ellipsis is True: - value += "..." + value += '...' else: value += ellipsis return value @@ -205,6 +199,7 @@ def attrgetter(attr, key=None): processor=attrgetter('get_address')) """ + def helper(instance, *args, **kwargs): value = instance for bit in attr.split('.'): @@ -243,7 +238,7 @@ def helper(value, *args, **kwargs): else: value = kwargs.get('default_value', value) if not value: # Empty or missing default_value - return "" + return '' if localize: value = localtime(value) return value.strftime(format_string) @@ -266,7 +261,7 @@ def format(format_string, cast=lambda x: x): the ``cast`` function. Examples:: - + # Perform some 0 padding item_number = columns.FloatColumn("Item No.", sources=['number'], processor=format("{:03d})) @@ -283,10 +278,11 @@ def helper(instance, *args, **kwargs): value = instance value = cast(value) return format_string.format(value, obj=instance) + return helper -def make_xeditable(instance=None, extra_attrs=[], *args, **kwargs): +def make_xeditable(instance=None, extra_attrs=[], *args, **kwargs): # noqa: C901 """ Converts the contents of the column into an ```` tag with the required DOM attributes to power the X-Editable UI. @@ -296,9 +292,12 @@ def make_xeditable(instance=None, extra_attrs=[], *args, **kwargs): * ``type`` - Defaults to the basic type of the HTML input ("text", "number", "datetime") * ``title`` - Defaults to an empty string, controls the HTML "title" attribute. - * ``placeholder`` - Defaults to whatever "title" is, controls the HTML "placeholder" attribute. - * ``url`` - Defaults to the ``request.path`` of the view, which will automatically serve the X-Editable interface as long as it inherits from ``XEditableDatatableView``. - * ``source`` - Defaults to the ``request.path`` of the view, which will automatically serve X-Editable requests for ``choices`` data about a field. + * ``placeholder`` - Defaults to whatever "title" is, controls the HTML + "placeholder" attribute. + * ``url`` - Defaults to the ``request.path`` of the view, which will automatically + serve the X-Editable interface as long as it inherits from ``XEditableDatatableView``. + * ``source`` - Defaults to the ``request.path`` of the view, which will automatically + serve X-Editable requests for ``choices`` data about a field. Supplying a list of names via ``extra_attrs`` will enable arbitrary other keyword arguments to be rendered in the HTML as attribute as well. ``extra_attrs`` serves as a whitelist of extra @@ -324,7 +323,7 @@ def make_xeditable(instance=None, extra_attrs=[], *args, **kwargs): k = k[5:] attrs['data-{0}'.format(k)] = v - attrs['data-xeditable'] = "xeditable" + attrs['data-xeditable'] = 'xeditable' # Assign default values where they are not provided @@ -334,7 +333,7 @@ def make_xeditable(instance=None, extra_attrs=[], *args, **kwargs): field_name = field_name[1] if isinstance(field_name, (tuple, list)): raise ValueError("'make_xeditable' helper needs a single-field data column," - " not {0!r}".format(field_name)) + ' not {0!r}'.format(field_name)) attrs['data-name'] = field_name if isinstance(rich_data, Model): @@ -352,14 +351,14 @@ def make_xeditable(instance=None, extra_attrs=[], *args, **kwargs): if not url_provider: url_provider = getattr(instance, provider_name, None) if not url_provider and 'view' in kwargs: - url_provider = lambda field_name: kwargs['view'].request.path + url_provider = lambda field_name: kwargs['view'].request.path # noqa: E731 else: raise ValueError("'make_xeditable' cannot determine a value for 'url'.") if url_provider: attrs['data-url'] = url_provider(field_name=field_name) if 'data-placeholder' not in attrs: - attrs['data-placeholder'] = attrs.get('data-title', "") + attrs['data-placeholder'] = attrs.get('data-title', '') if 'data-type' not in attrs: if hasattr(instance, '_meta'): @@ -382,7 +381,7 @@ def make_xeditable(instance=None, extra_attrs=[], *args, **kwargs): if attrs['data-type'] in ('select', 'select2'): if 'data-source' not in attrs: if 'view' in kwargs: - attrs['data-source'] = "{url}?{field_param}={fieldname}".format(**{ + attrs['data-source'] = '{url}?{field_param}={fieldname}'.format(**{ 'url': kwargs['view'].request.path, 'field_param': kwargs['view'].xeditable_fieldname_param, 'fieldname': field_name, @@ -416,6 +415,7 @@ def make_processor(func, arg=None): ``func``. If you need to sent more arguments, consider wrapping your ``func`` in a ``functools.partial``, and use that as ``func`` instead. """ + def helper(instance, *args, **kwargs): value = kwargs.get('default_value') if value is None: @@ -425,6 +425,8 @@ def helper(instance, *args, **kwargs): else: extra_arg = [] return func(value, *extra_arg) + return helper + through_filter = make_processor diff --git a/datatableview/models.py b/datatableview/models.py index 91d98152..93e61625 100644 --- a/datatableview/models.py +++ b/datatableview/models.py @@ -1,3 +1,3 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- # Required for Django to recognize this app diff --git a/datatableview/static/js/datatableview.min.js b/datatableview/static/js/datatableview.min.js index 692247b3..6588c3dd 100644 --- a/datatableview/static/js/datatableview.min.js +++ b/datatableview/static/js/datatableview.min.js @@ -1 +1 @@ -var datatableview=function(){var defaultDataTableOptions={serverSide:true,paging:true};var optionsNameMap={name:"name","config-sortable":"orderable","config-sorting":"order","config-visible":"visible","config-searchable":"searchable"};var checkGlobalConfirmHook=true;var autoInitialize=false;function initialize($$,opts){$$.each(function(){var datatable=$(this);var options=datatableview.getOptions(datatable,opts);datatable.DataTable(options)});return $$}function getOptions(datatable,opts){var columnOptions=[];var sortingOptions=[];datatable.find("thead th").each(function(){var header=$(this);var options={};for(var i=0;iName', dtv.__str__()) class DT(Datatable): - name = columns.TextColumn("Name", sources=['name']) + name = TextColumn('Name', sources=['name']) class Meta: - model = models.ExampleModel + model = ExampleModel columns = ['name'] ordering = ['-name'] @@ -228,14 +233,14 @@ class Meta: def test_sort_prioritizes_db_source(self): # Defined so that 'pk' order != 'name' order - obj1 = models.ExampleModel.objects.create(name="test name 2") - obj2 = models.ExampleModel.objects.create(name="test name 1") - queryset = models.ExampleModel.objects.all() + obj1 = ExampleModel.objects.create(name='test name 2') + obj2 = ExampleModel.objects.create(name='test name 1') + queryset = ExampleModel.objects.all() class DT(Datatable): - name = columns.TextColumn("Name", sources=['name']) + name = TextColumn('Name', sources=['name']) class Meta: - model = models.ExampleModel + model = ExampleModel columns = ['name'] ordering = ['pk'] @@ -251,9 +256,9 @@ class Meta: def test_sort_uses_all_sources(self): from datetime import timedelta - obj1 = models.ExampleModel.objects.create(name="a") - obj2 = models.ExampleModel.objects.create(name="a") - obj3 = models.ExampleModel.objects.create(name="b") + obj1 = ExampleModel.objects.create(name='a') + obj2 = ExampleModel.objects.create(name='a') + obj3 = ExampleModel.objects.create(name='b') obj1.date_created = obj1.date_created + timedelta(days=3) obj2.date_created = obj2.date_created + timedelta(days=1) obj3.date_created = obj3.date_created + timedelta(days=2) @@ -261,12 +266,12 @@ def test_sort_uses_all_sources(self): obj2.save() obj3.save() - queryset = models.ExampleModel.objects.all() + queryset = ExampleModel.objects.all() class DT(Datatable): - my_column = columns.TextColumn("Data", sources=['name', 'date_created', 'pk']) + my_column = TextColumn('Data', sources=['name', 'date_created', 'pk']) class Meta: - model = models.ExampleModel + model = ExampleModel columns = ['my_column'] dt = DT(queryset, '/', query_config={'order[0][column]': '0', 'order[0][dir]': 'asc'}) # 'iSortingCols': '1', @@ -283,9 +288,9 @@ class Meta: # Swap the order of 'date_created' and 'name' fields in the sources, which will alter the # sort results. class DT(Datatable): - my_column = columns.TextColumn("Data", sources=['date_created', 'name', 'pk']) + my_column = TextColumn('Data', sources=['date_created', 'name', 'pk']) class Meta: - model = models.ExampleModel + model = ExampleModel columns = ['my_column'] dt = DT(queryset, '/', query_config={'order[0][column]': '0', 'order[0][dir]': 'asc'}) # 'iSortingCols': '1', @@ -300,16 +305,16 @@ class Meta: def test_sort_ignores_virtual_sources_when_mixed(self): from datetime import timedelta - obj1 = models.ExampleModel.objects.create(name="a") - obj2 = models.ExampleModel.objects.create(name="b") - obj3 = models.ExampleModel.objects.create(name="a") + obj1 = ExampleModel.objects.create(name='a') + obj2 = ExampleModel.objects.create(name='b') + obj3 = ExampleModel.objects.create(name='a') - queryset = models.ExampleModel.objects.all() + queryset = ExampleModel.objects.all() class DT(Datatable): - my_column = columns.TextColumn("Data", sources=['name', 'get_absolute_url']) + my_column = TextColumn('Data', sources=['name', 'get_absolute_url']) class Meta: - model = models.ExampleModel + model = ExampleModel columns = ['my_column'] dt = DT(queryset, '/', query_config={'order[0][column]': '0', 'order[0][dir]': 'asc'}) # 'iSortingCols': '1', @@ -324,9 +329,9 @@ class Meta: # Swap the sources order, but we expect the same result class DT(Datatable): - my_column = columns.TextColumn("Data", sources=['get_absolute_url', 'name'], processor='get_data') + my_column = TextColumn('Data', sources=['get_absolute_url', 'name'], processor='get_data') class Meta: - model = models.ExampleModel + model = ExampleModel columns = ['my_column'] def get_data(self, obj, **kwargs): @@ -343,16 +348,16 @@ def get_data(self, obj, **kwargs): def test_sort_uses_virtual_sources_when_no_db_sources_available(self): from datetime import timedelta - obj1 = models.ExampleModel.objects.create(name="a") - obj2 = models.ExampleModel.objects.create(name="b") - obj3 = models.ExampleModel.objects.create(name="c") + obj1 = ExampleModel.objects.create(name='a') + obj2 = ExampleModel.objects.create(name='b') + obj3 = ExampleModel.objects.create(name='c') - queryset = models.ExampleModel.objects.all() + queryset = ExampleModel.objects.all() class DT(Datatable): - pk = columns.TextColumn("Data", sources=['get_negative_pk']) + pk = TextColumn('Data', sources=['get_negative_pk']) class Meta: - model = models.ExampleModel + model = ExampleModel columns = ['pk'] dt = DT(queryset, '/', query_config={'order[0][column]': '0', 'order[0][dir]': 'asc'}) # 'iSortingCols': '1', @@ -366,14 +371,14 @@ class Meta: self.assertEqual(list(dt._records), [obj1, obj2, obj3]) def test_get_object_pk(self): - obj1 = models.ExampleModel.objects.create(name="test name 1") - queryset = models.ExampleModel.objects.all() + obj1 = ExampleModel.objects.create(name='test name 1') + queryset = ExampleModel.objects.all() dt = Datatable(queryset, '/') self.assertEqual(dt.get_object_pk(obj1), obj1.pk) def test_get_extra_record_data_passes_through_to_object_serialization(self): - obj1 = models.ExampleModel.objects.create(name="test name 1") - queryset = models.ExampleModel.objects.all() + obj1 = ExampleModel.objects.create(name='test name 1') + queryset = ExampleModel.objects.all() class DT(Datatable): def get_extra_record_data(self, obj): @@ -386,8 +391,8 @@ def get_extra_record_data(self, obj): self.assertEqual(data['_extra_data']['custom'], 'data') def test_get_extra_record_data_passes_through_to_json_response(self): - obj1 = models.ExampleModel.objects.create(name="test name 1") - queryset = models.ExampleModel.objects.all() + obj1 = ExampleModel.objects.create(name='test name 1') + queryset = ExampleModel.objects.all() class DT(Datatable): def get_extra_record_data(self, obj): @@ -406,24 +411,24 @@ class FakeRequest(object): self.assertEqual(data['data'][0]['DT_RowData'], {'custom': 'data'}) def test_get_column_value_forwards_to_column_class(self): - class CustomColumn1(columns.Column): + class CustomColumn1(Column): def value(self, obj, **kwargs): - return "first" + return 'first' - class CustomColumn2(columns.Column): + class CustomColumn2(Column): def value(self, obj, **kwargs): - return "second" + return 'second' class DT(Datatable): - fake1 = CustomColumn1("Fake1", sources=['get_absolute_url']) - fake2 = CustomColumn2("Fake2", sources=['get_absolute_url']) + fake1 = CustomColumn1('Fake1', sources=['get_absolute_url']) + fake2 = CustomColumn2('Fake2', sources=['get_absolute_url']) class Meta: - model = models.ExampleModel + model = ExampleModel columns = ['name', 'fake1', 'fake2'] - obj1 = models.ExampleModel.objects.create(name="test name 1") - queryset = models.ExampleModel.objects.all() + obj1 = ExampleModel.objects.create(name='test name 1') + queryset = ExampleModel.objects.all() dt = DT(queryset, '/') data = dt.get_record_data(obj1) self.assertIn('1', data) @@ -440,14 +445,14 @@ def fake_callback(self): # Test no callback given dt = Datatable([], '/') - f = dt.get_processor_method(columns.Column("Fake", sources=['fake']), i=0) + f = dt.get_processor_method(Column('Fake', sources=['fake']), i=0) self.assertEqual(f, None) class DT(Datatable): def fake_callback(self): pass - column = columns.Column("Fake", sources=['fake'], processor='fake_callback') + column = Column('Fake', sources=['fake'], processor='fake_callback') # Test callback found on self dt = DT([], '/') @@ -463,7 +468,7 @@ def test_get_processor_method_returns_direct_callable(self): def fake_callback(): pass - column = columns.Column("Fake", sources=[], processor=fake_callback) + column = Column('Fake', sources=[], processor=fake_callback) # Test no callback given dt = Datatable([], '/') @@ -486,7 +491,7 @@ def get_column_fake_data(self): def get_column_0_data(self): pass - column = columns.Column("Fake", sources=[]) + column = Column('Fake', sources=[]) column.name = 'fake' # Test implied named callback found first @@ -539,18 +544,18 @@ def get_column_0_data(self): self.assertEqual(f, dt.get_column_fake_data) def test_iter_datatable_yields_columns(self): - class CustomColumn1(columns.Column): + class CustomColumn1(Column): pass - class CustomColumn2(columns.Column): + class CustomColumn2(Column): pass class DT(Datatable): - fake1 = CustomColumn1("Fake1", sources=['get_absolute_url']) - fake2 = CustomColumn2("Fake2", sources=['get_absolute_url']) + fake1 = CustomColumn1('Fake1', sources=['get_absolute_url']) + fake2 = CustomColumn2('Fake2', sources=['get_absolute_url']) class Meta: - model = models.ExampleModel + model = ExampleModel columns = ['name', 'fake1', 'fake2'] dt = DT([], '/') @@ -558,169 +563,169 @@ class Meta: self.assertEqual(list(dt), [dt.columns['name'], dt.columns['fake1'], dt.columns['fake2']]) def test_search_term_basic(self): - obj1 = models.ExampleModel.objects.create(name="test name 1") - obj2 = models.ExampleModel.objects.create(name="test name 2") - obj3 = models.ExampleModel.objects.create(name="test name 12") - queryset = models.ExampleModel.objects.all() + obj1 = ExampleModel.objects.create(name='test name 1') + obj2 = ExampleModel.objects.create(name='test name 2') + obj3 = ExampleModel.objects.create(name='test name 12') + queryset = ExampleModel.objects.all() class DT(Datatable): class Meta: - model = models.ExampleModel + model = ExampleModel columns = ['name'] dt = DT(queryset, '/', query_config={'search[value]': 'test'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj1, obj2, obj3]) + self.assertEqual(list(dt._records), [obj1, obj2, obj3]) dt = DT(queryset, '/', query_config={'search[value]': 'name'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj1, obj2, obj3]) + self.assertEqual(list(dt._records), [obj1, obj2, obj3]) dt = DT(queryset, '/', query_config={'search[value]': '1'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj1, obj3]) + self.assertEqual(list(dt._records), [obj1, obj3]) dt = DT(queryset, '/', query_config={'search[value]': '2'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj2, obj3]) + self.assertEqual(list(dt._records), [obj2, obj3]) dt = DT(queryset, '/', query_config={'search[value]': '12'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj3]) + self.assertEqual(list(dt._records), [obj3]) dt = DT(queryset, '/', query_config={'search[value]': '3'}) dt.populate_records() - self.assertEquals(list(dt._records), []) + self.assertEqual(list(dt._records), []) def test_search_term_boolean(self): - obj1 = models.ExampleModel.objects.create(name="test name 1", value=True) - obj2 = models.ExampleModel.objects.create(name="test name 2", value=True) - obj3 = models.ExampleModel.objects.create(name="test name 12", value=False) - queryset = models.ExampleModel.objects.all() + obj1 = ExampleModel.objects.create(name='test name 1', value=True) + obj2 = ExampleModel.objects.create(name='test name 2', value=True) + obj3 = ExampleModel.objects.create(name='test name 12', value=False) + queryset = ExampleModel.objects.all() class DT(Datatable): - senior = columns.BooleanColumn('Senior:', 'value') + senior = BooleanColumn('Senior:', 'value') class Meta: - model = models.ExampleModel + model = ExampleModel columns = ['name', 'senior'] dt = DT(queryset, '/', query_config={'search[value]': 'True'}) dt.populate_records() - self.assertEquals(len(list(dt._records)), 2) + self.assertEqual(len(list(dt._records)), 2) dt = DT(queryset, '/', query_config={'search[value]': 'false'}) dt.populate_records() - self.assertEquals(len(list(dt._records)), 1) + self.assertEqual(len(list(dt._records)), 1) dt = DT(queryset, '/', query_config={'search[value]': 'SENIOR'}) dt.populate_records() - self.assertEquals(len(list(dt._records)), 2) + self.assertEqual(len(list(dt._records)), 2) dt = DT(queryset, '/', query_config={'search[value]': 'menior'}) dt.populate_records() - self.assertEquals(len(list(dt._records)), 0) + self.assertEqual(len(list(dt._records)), 0) def test_search_multiple_terms_use_AND(self): - obj1 = models.ExampleModel.objects.create(name="test name 1") - obj2 = models.ExampleModel.objects.create(name="test name 2") - obj3 = models.ExampleModel.objects.create(name="test name 12") - queryset = models.ExampleModel.objects.all() + obj1 = ExampleModel.objects.create(name='test name 1') + obj2 = ExampleModel.objects.create(name='test name 2') + obj3 = ExampleModel.objects.create(name='test name 12') + queryset = ExampleModel.objects.all() class DT(Datatable): class Meta: - model = models.ExampleModel + model = ExampleModel columns = ['name'] dt = DT(queryset, '/', query_config={'search[value]': 'test name'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj1, obj2, obj3]) + self.assertEqual(list(dt._records), [obj1, obj2, obj3]) dt = DT(queryset, '/', query_config={'search[value]': 'test 1'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj1, obj3]) + self.assertEqual(list(dt._records), [obj1, obj3]) dt = DT(queryset, '/', query_config={'search[value]': 'test 2'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj2, obj3]) + self.assertEqual(list(dt._records), [obj2, obj3]) dt = DT(queryset, '/', query_config={'search[value]': 'test 12'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj3]) + self.assertEqual(list(dt._records), [obj3]) dt = DT(queryset, '/', query_config={'search[value]': 'test 3'}) dt.populate_records() - self.assertEquals(list(dt._records), []) + self.assertEqual(list(dt._records), []) def test_search_term_queries_all_columns(self): - r1 = models.RelatedModel.objects.create(name="test related 1 one") - r2 = models.RelatedModel.objects.create(name="test related 2 two") - obj1 = models.ExampleModel.objects.create(name="test name 1", related=r1) - obj2 = models.ExampleModel.objects.create(name="test name 2", related=r2) + r1 = RelatedModel.objects.create(name='test related 1 one') + r2 = RelatedModel.objects.create(name='test related 2 two') + obj1 = ExampleModel.objects.create(name='test name 1', related=r1) + obj2 = ExampleModel.objects.create(name='test name 2', related=r2) - queryset = models.ExampleModel.objects.all() + queryset = ExampleModel.objects.all() class DT(Datatable): - related = columns.TextColumn("Related", ['related__name']) + related = TextColumn('Related', ['related__name']) class Meta: - model = models.ExampleModel + model = ExampleModel columns = ['name', 'related'] dt = DT(queryset, '/', query_config={'search[value]': 'test'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj1, obj2]) + self.assertEqual(list(dt._records), [obj1, obj2]) dt = DT(queryset, '/', query_config={'search[value]': 'test name'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj1, obj2]) + self.assertEqual(list(dt._records), [obj1, obj2]) dt = DT(queryset, '/', query_config={'search[value]': 'test 2'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj2]) + self.assertEqual(list(dt._records), [obj2]) dt = DT(queryset, '/', query_config={'search[value]': 'related 2'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj2]) + self.assertEqual(list(dt._records), [obj2]) dt = DT(queryset, '/', query_config={'search[value]': 'test one'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj1]) + self.assertEqual(list(dt._records), [obj1]) dt = DT(queryset, '/', query_config={'search[value]': '2 two'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj2]) + self.assertEqual(list(dt._records), [obj2]) dt = DT(queryset, '/', query_config={'search[value]': 'test three'}) dt.populate_records() - self.assertEquals(list(dt._records), []) + self.assertEqual(list(dt._records), []) def test_search_term_queries_extra_fields(self): - r1 = models.RelatedModel.objects.create(name="test related 1 one") - r2 = models.RelatedModel.objects.create(name="test related 2 two") - obj1 = models.ExampleModel.objects.create(name="test name 1", related=r1) - obj2 = models.ExampleModel.objects.create(name="test name 2", related=r2) + r1 = RelatedModel.objects.create(name='test related 1 one') + r2 = RelatedModel.objects.create(name='test related 2 two') + obj1 = ExampleModel.objects.create(name='test name 1', related=r1) + obj2 = ExampleModel.objects.create(name='test name 2', related=r2) - queryset = models.ExampleModel.objects.all() + queryset = ExampleModel.objects.all() class DT(Datatable): - related = columns.TextColumn("Related", ['related__name']) + related = TextColumn('Related', ['related__name']) class Meta: - model = models.ExampleModel + model = ExampleModel columns = ['related'] search_fields = ['name'] dt = DT(queryset, '/', query_config={'search[value]': 'test'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj1, obj2]) + self.assertEqual(list(dt._records), [obj1, obj2]) dt = DT(queryset, '/', query_config={'search[value]': 'test name 2'}) dt.populate_records() - self.assertEquals(list(dt._records), [obj2]) + self.assertEqual(list(dt._records), [obj2]) class ValuesDatatableTests(DatatableViewTestCase): def test_get_object_pk(self): - obj1 = models.ExampleModel.objects.create(name="test name 1") - queryset = models.ExampleModel.objects.all() + obj1 = ExampleModel.objects.create(name='test name 1') + queryset = ExampleModel.objects.all() dt = ValuesDatatable(queryset, '/') obj_data = queryset.values('pk')[0] self.assertEqual(dt.get_object_pk(obj_data), obj1.pk) diff --git a/datatableview/tests/test_helpers.py b/datatableview/tests/test_helpers.py index 1565cba1..3a65beb5 100644 --- a/datatableview/tests/test_helpers.py +++ b/datatableview/tests/test_helpers.py @@ -1,21 +1,20 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- from datetime import datetime from functools import partial import django +from django.apps import apps from datatableview import helpers -import six - from .testcase import DatatableViewTestCase -from .test_app.models import ExampleModel, RelatedM2MModel -if django.VERSION < (1, 7): - test_data_fixture = 'test_data_legacy.json' -else: - test_data_fixture = 'test_data.json' +ExampleModel = apps.get_model('test_app', 'ExampleModel') +RelatedModel = apps.get_model('test_app', 'RelatedModel') +RelatedM2MModel = apps.get_model('test_app', 'RelatedM2MModel') + +test_data_fixture = 'test_data.json' class HelpersTests(DatatableViewTestCase): @@ -38,7 +37,7 @@ def test_link_to_model(self): self.assertEqual(output, 'ExampleModel 1') # Verify text override - output = helper(instance, text="Special text") + output = helper(instance, text='Special text') self.assertEqual(output, 'Special text') @@ -49,7 +48,7 @@ def test_link_to_model(self): self.assertEqual(output, 'RelatedModel 1') # Verify ``key`` access version of custom text - output = secondary_helper(instance, text="Special text") + output = secondary_helper(instance, text='Special text') self.assertEqual(output, 'Special text') # Verify ``attr`` as 'self' is the identity mapping @@ -67,15 +66,15 @@ def test_make_boolean_checkmark(self): helper = helpers.make_boolean_checkmark # Verify simple use - output = helper("True-ish value") + output = helper('True-ish value') self.assertEqual(output, '✔') - output = helper("") + output = helper('') self.assertEqual(output, '✘') # Verify custom values - output = helper("True-ish value", true_value="Yes", false_value="No") + output = helper('True-ish value', true_value='Yes', false_value='No') self.assertEqual(output, 'Yes') - output = helper("", true_value="Yes", false_value="No") + output = helper('', true_value='Yes', false_value='No') self.assertEqual(output, 'No') def test_format_date(self): @@ -84,15 +83,15 @@ def test_format_date(self): # Verify simple use data = datetime.now() - secondary_helper = helper("%m/%d/%Y") + secondary_helper = helper('%m/%d/%Y') output = secondary_helper(data) - self.assertEqual(output, data.strftime("%m/%d/%Y")) + self.assertEqual(output, data.strftime('%m/%d/%Y')) # Verify that None objects get swallowed without complaint. # This helps promise that the helper won't blow up for models.DateTimeField that are allowed # to be null. output = secondary_helper(None) - self.assertEqual(output, "") + self.assertEqual(output, '') def test_format(self): """ Verifies that format works. """ @@ -100,15 +99,15 @@ def test_format(self): # Verify simple use data = 1234567890 - secondary_helper = helper("{0:,}") + secondary_helper = helper('{0:,}') output = secondary_helper(data) - self.assertEqual(output, "{0:,}".format(data)) + self.assertEqual(output, '{0:,}'.format(data)) # Verify ``cast`` argument - data = "1234.56789" - secondary_helper = helper("{0:.2f}", cast=float) + data = '1234.56789' + secondary_helper = helper('{0:.2f}', cast=float) output = secondary_helper(data) - self.assertEqual(output, "{0:.2f}".format(float(data))) + self.assertEqual(output, '{0:.2f}'.format(float(data))) def test_through_filter(self): """ Verifies that through_filter works. """ @@ -117,15 +116,15 @@ def test_through_filter(self): target_function = lambda data, arg=None: (data, arg) # Verify simple use - data = "Data string" + data = 'Data string' secondary_helper = helper(target_function) output = secondary_helper(data) self.assertEqual(output, (data, None)) # Verify ``arg`` argument - secondary_helper = helper(target_function, arg="Arg data") + secondary_helper = helper(target_function, arg='Arg data') output = secondary_helper(data) - self.assertEqual(output, (data, "Arg data")) + self.assertEqual(output, (data, 'Arg data')) def test_itemgetter(self): """ Verifies that itemgetter works. """ @@ -146,12 +145,12 @@ def test_itemgetter(self): data = str(range(10)) secondary_helper = helper(slice(0, 5), ellipsis=True) output = secondary_helper(data) - self.assertEqual(output, data[:5] + "...") + self.assertEqual(output, data[:5] + '...') # Verify ellipsis can be customized - secondary_helper = helper(slice(0, 5), ellipsis="custom") + secondary_helper = helper(slice(0, 5), ellipsis='custom') output = secondary_helper(data) - self.assertEqual(output, data[:5] + "custom") + self.assertEqual(output, data[:5] + 'custom') # Verify ellipsis does nothing for non-string data types data = range(10) @@ -191,8 +190,8 @@ def test_make_xeditable(self): # Verify chain ends with provision of a value data = ExampleModel.objects.get(pk=1) # This needs a "url" arg because we want to test successful use - output = tertiary_helper(data, url="/", **internals) - self.assertTrue(isinstance(output, six.string_types)) + output = tertiary_helper(data, url='/', **internals) + self.assertTrue(isinstance(output, str)) # Verify that no "view" kwarg means the url is required from the call with self.assertRaises(ValueError) as cm: @@ -212,16 +211,16 @@ def test_make_xeditable(self): # Verify default kwarg names end up as attributes data = ExampleModel.objects.get(pk=1) kwargs = { - 'pk': "PK DATA", - 'type': "TYPE DATA", - 'url': "URL DATA", - 'source': "SOURCE DATA", - 'title': "TITLE DATA", - 'placeholder': "PLACEHOLDER DATA", + 'pk': 'PK DATA', + 'type': 'TYPE DATA', + 'url': 'URL DATA', + 'source': 'SOURCE DATA', + 'title': 'TITLE DATA', + 'placeholder': 'PLACEHOLDER DATA', # Extra stuff not in anticipated to appear in rendered string - 'special': "SPECIAL DATA", - 'data_custom': "DATA-CUSTOM DATA", + 'special': 'SPECIAL DATA', + 'data_custom': 'DATA-CUSTOM DATA', } secondary_helper = helper(**kwargs) output = secondary_helper(data, **internals) diff --git a/datatableview/tests/test_utils.py b/datatableview/tests/test_utils.py index c2a9bdc7..fd10936b 100644 --- a/datatableview/tests/test_utils.py +++ b/datatableview/tests/test_utils.py @@ -1,34 +1,39 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- +from django.apps import apps +from datatableview.columns import Column from .testcase import DatatableViewTestCase -from .test_app import models -from ..columns import Column -from ..views.legacy import DEFAULT_OPTIONS -from .. import utils +from datatableview.utils import get_first_orm_bit,resolve_orm_path + +ExampleModel = apps.get_model('test_app', 'ExampleModel') +RelatedModel = apps.get_model('test_app', 'RelatedModel') +RelatedM2MModel = apps.get_model('test_app', 'RelatedM2MModel') +ReverseRelatedModel = apps.get_model('test_app', 'ReverseRelatedModel') + class UtilsTests(DatatableViewTestCase): def test_get_first_orm_bit(self): """ """ - self.assertEqual(utils.get_first_orm_bit(Column(sources=['field'])), 'field') - self.assertEqual(utils.get_first_orm_bit(Column(sources=['field__otherfield'])), 'field') + self.assertEqual(get_first_orm_bit(Column(sources=['field'])), 'field') + self.assertEqual(get_first_orm_bit(Column(sources=['field__otherfield'])), 'field') def test_resolve_orm_path_local(self): """ Verifies that references to a local field on a model are returned. """ - field = utils.resolve_orm_path(models.ExampleModel, 'name') - self.assertEqual(field, models.ExampleModel._meta.get_field('name')) + field = resolve_orm_path(ExampleModel, 'name') + self.assertEqual(field, ExampleModel._meta.get_field('name')) def test_resolve_orm_path_fk(self): """ Verify that ExampleModel->RelatedModel.name == RelatedModel.name """ - remote_field = utils.resolve_orm_path(models.ExampleModel, 'related__name') - self.assertEqual(remote_field, models.RelatedModel._meta.get_field('name')) + remote_field = resolve_orm_path(ExampleModel, 'related__name') + self.assertEqual(remote_field, RelatedModel._meta.get_field('name')) def test_resolve_orm_path_reverse_fk(self): """ Verify that ExampleModel->>>ReverseRelatedModel.name == ReverseRelatedModel.name """ - remote_field = utils.resolve_orm_path(models.ExampleModel, 'reverserelatedmodel__name') - self.assertEqual(remote_field, models.ReverseRelatedModel._meta.get_field('name')) + remote_field = resolve_orm_path(ExampleModel, 'reverserelatedmodel__name') + self.assertEqual(remote_field, ReverseRelatedModel._meta.get_field('name')) def test_resolve_orm_path_m2m(self): """ Verify that ExampleModel->>>RelatedM2MModel.name == RelatedM2MModel.name """ - remote_field = utils.resolve_orm_path(models.ExampleModel, 'relateds__name') - self.assertEqual(remote_field, models.RelatedM2MModel._meta.get_field('name')) + remote_field = resolve_orm_path(ExampleModel, 'relateds__name') + self.assertEqual(remote_field, RelatedM2MModel._meta.get_field('name')) diff --git a/datatableview/tests/test_views.py b/datatableview/tests/test_views.py index f25eae13..108c8f7d 100644 --- a/datatableview/tests/test_views.py +++ b/datatableview/tests/test_views.py @@ -1,14 +1,17 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- import json from django.urls import reverse -import six +from example_app.views import ZeroConfigurationDatatableView +from example_app.models import Entry +from example_app.views import BootstrapTemplateDatatableView, PrettyNamesDatatableView,\ + SpecificColumnsDatatableView, CustomizedTemplateDatatableView, MultipleTablesDatatableView, \ + SatelliteDatatableView, ColumnBackedByMethodDatatableView, CompoundColumnsDatatableView, \ + HelpersReferenceDatatableView, DefaultCallbackNamesDatatableView, ManyToManyFieldsDatatableView from .testcase import DatatableViewTestCase -from .example_project.example_project.example_app import views -from .example_project.example_project.example_app import models class FakeRequest(object): @@ -26,24 +29,23 @@ class ViewsTests(DatatableViewTestCase): def get_json_response(self, url): response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') content = response.content - if six.PY3: - content = content.decode() + content = content.decode() return json.loads(content) def test_zero_configuration_datatable_view(self): """ Verifies that no column definitions means all local fields are used. """ - view = views.ZeroConfigurationDatatableView + view = ZeroConfigurationDatatableView url = reverse('zero-configuration') view.request = FakeRequest(url) response = self.client.get(url) self.assertEqual( len(list(response.context['datatable'])), - len(models.Entry._meta.local_fields) + len(Entry._meta.local_fields) ) def test_specific_columns_datatable_view(self): """ Verifies that "columns" list matches context object length. """ - view = views.SpecificColumnsDatatableView() + view = SpecificColumnsDatatableView() url = reverse('specific-columns') view.request = FakeRequest(url) response = self.client.get(url) @@ -54,7 +56,7 @@ def test_specific_columns_datatable_view(self): def test_pretty_names_datatable_view(self): """ Verifies that a pretty name definition is used instead of the verbose name. """ - view = views.PrettyNamesDatatableView() + view = PrettyNamesDatatableView() url = reverse('pretty-names') view.request = FakeRequest(url) response = self.client.get(url) @@ -62,7 +64,7 @@ def test_pretty_names_datatable_view(self): len(list(response.context['datatable'])), len(view.get_datatable().columns) ) - self.assertEqual(response.context['datatable'].columns['pub_date'].label, "Publication date") + self.assertEqual(response.context['datatable'].columns['pub_date'].label, 'Publication date') # def test_x_editable_columns_datatable_view(self): # view = views.XEditableColumnsDatatableView @@ -73,7 +75,7 @@ def test_customized_template_datatable_view(self): """ Verify that the custom structure template is getting rendered instead of the default one. """ - view = views.CustomizedTemplateDatatableView() + view = CustomizedTemplateDatatableView() url = reverse('customized-template') view.request = FakeRequest(url) response = self.client.get(url) @@ -83,14 +85,14 @@ def test_bootstrap_template_datatable_view(self): """ Verify that the custom structure template is getting rendered instead of the default one. """ - view = views.BootstrapTemplateDatatableView() + view = BootstrapTemplateDatatableView() url = reverse('bootstrap-template') view.request = FakeRequest(url) response = self.client.get(url) self.assertContains(response, """[:\\w-\\.]+)","xg").exec(code),result=[];if(match.attributes!=null){var attributes,regex=new XRegExp("(? [\\w:\\-\\.]+)"+"\\s*=\\s*"+"(? \".*?\"|'.*?'|\\w+)","xg");while((attributes=regex.exec(code))!=null){result.push(new constructor(attributes.name,match.index+attributes.index,"color1"));result.push(new constructor(attributes.value,match.index+attributes.index+attributes[0].indexOf(attributes.value),"string"))}}if(tag!=null)result.push(new constructor(tag.name,match.index+tag[0].indexOf(tag.name),"keyword"));return result}this.regexList=[{regex:new XRegExp("(\\<|<)\\!\\[[\\w\\s]*?\\[(.|\\s)*?\\]\\](\\>|>)","gm"),css:"color2"},{regex:SyntaxHighlighter.regexLib.xmlComments,css:"comments"},{regex:new XRegExp("(<|<)[\\s\\/\\?]*(\\w+)(?.*?)[\\s\\/\\?]*(>|>)","sg"),func:process}]}Brush.prototype=new SyntaxHighlighter.Highlighter;Brush.aliases=["xml","xhtml","xslt","html"];SyntaxHighlighter.brushes.Xml=Brush;typeof exports!="undefined"?exports.Brush=Brush:null})(); \ No newline at end of file +(function(){typeof require!="undefined"?SyntaxHighlighter=require("shCore").SyntaxHighlighter:null;function Brush(){function process(match,regexInfo){var constructor=SyntaxHighlighter.Match,code=match[0],tag=new XRegExp("(<|<)[\\s\\/\\?]*(?[:\\w-\\.]+)","xg").exec(code),result=[];if(match.attributes!=null){var attributes,regex=new XRegExp("(? [\\w:\\-\\.]+)"+"\\s*=\\s*"+"(? \".*?\"|'.*?'|\\w+)","xg");while((attributes=regex.exec(code))!=null){result.push(new constructor(attributes.name,match.index+attributes.index,"color1"));result.push(new constructor(attributes.value,match.index+attributes.index+attributes[0].indexOf(attributes.value),"string"))}}if(tag!=null)result.push(new constructor(tag.name,match.index+tag[0].indexOf(tag.name),"keyword"));return result}this.regexList=[{regex:new XRegExp("(\\<|<)\\!\\[[\\w\\s]*?\\[(.|\\s)*?\\]\\](\\>|>)","gm"),css:"color2"},{regex:SyntaxHighlighter.regexLib.xmlComments,css:"comments"},{regex:new XRegExp("(<|<)[\\s\\/\\?]*(\\w+)(?.*?)[\\s\\/\\?]*(>|>)","sg"),func:process}]}Brush.prototype=new SyntaxHighlighter.Highlighter;Brush.aliases=["xml","xhtml","xslt","html"];SyntaxHighlighter.brushes.Xml=Brush;typeof exports!="undefined"?exports.Brush=Brush:null})(); diff --git a/datatableview/tests/example_project/example_project/example_app/static/syntaxhighlighter/shCore.css b/demo_app/example_app/static/syntaxhighlighter/shCore.css similarity index 98% rename from datatableview/tests/example_project/example_project/example_app/static/syntaxhighlighter/shCore.css rename to demo_app/example_app/static/syntaxhighlighter/shCore.css index bc8a5785..a22d8e4c 100644 --- a/datatableview/tests/example_project/example_project/example_app/static/syntaxhighlighter/shCore.css +++ b/demo_app/example_app/static/syntaxhighlighter/shCore.css @@ -7,11 +7,11 @@ * * @version * 3.0.83 (July 02 2010) - * + * * @copyright * Copyright (C) 2004-2010 Alex Gorbatchev. * * @license * Dual licensed under the MIT and GPL licenses. */ -.syntaxhighlighter a,.syntaxhighlighter div,.syntaxhighlighter code,.syntaxhighlighter table,.syntaxhighlighter table td,.syntaxhighlighter table tr,.syntaxhighlighter table tbody,.syntaxhighlighter table thead,.syntaxhighlighter table caption,.syntaxhighlighter textarea{-moz-border-radius:0 0 0 0 !important;-webkit-border-radius:0 0 0 0 !important;background:none !important;border:0 !important;bottom:auto !important;float:none !important;height:auto !important;left:auto !important;line-height:1.1em !important;margin:0 !important;outline:0 !important;overflow:visible !important;padding:0 !important;position:static !important;right:auto !important;text-align:left !important;top:auto !important;vertical-align:baseline !important;width:auto !important;box-sizing:content-box !important;font-family:"Consolas","Bitstream Vera Sans Mono","Courier New",Courier,monospace !important;font-weight:normal !important;font-style:normal !important;font-size:1em !important;min-height:inherit !important;min-height:auto !important}.syntaxhighlighter{width:100% !important;margin:1em 0 1em 0 !important;position:relative !important;overflow:auto !important;font-size:1em !important}.syntaxhighlighter.source{overflow:hidden !important}.syntaxhighlighter .bold{font-weight:bold !important}.syntaxhighlighter .italic{font-style:italic !important}.syntaxhighlighter .line{white-space:pre !important}.syntaxhighlighter table{width:100% !important}.syntaxhighlighter table caption{text-align:left !important;padding:.5em 0 .5em 1em !important}.syntaxhighlighter table td.code{width:100% !important}.syntaxhighlighter table td.code .container{position:relative !important}.syntaxhighlighter table td.code .container textarea{box-sizing:border-box !important;position:absolute !important;left:0 !important;top:0 !important;width:100% !important;height:100% !important;border:none !important;background:white !important;padding-left:1em !important;overflow:hidden !important;white-space:pre !important}.syntaxhighlighter table td.gutter .line{text-align:right !important;padding:0 .5em 0 1em !important}.syntaxhighlighter table td.code .line{padding:0 1em !important}.syntaxhighlighter.nogutter td.code .container textarea,.syntaxhighlighter.nogutter td.code .line{padding-left:0 !important}.syntaxhighlighter.show{display:block !important}.syntaxhighlighter.collapsed table{display:none !important}.syntaxhighlighter.collapsed .toolbar{padding:.1em .8em 0 .8em !important;font-size:1em !important;position:static !important;width:auto !important;height:auto !important}.syntaxhighlighter.collapsed .toolbar span{display:inline !important;margin-right:1em !important}.syntaxhighlighter.collapsed .toolbar span a{padding:0 !important;display:none !important}.syntaxhighlighter.collapsed .toolbar span a.expandSource{display:inline !important}.syntaxhighlighter .toolbar{position:absolute !important;right:1px !important;top:1px !important;width:11px !important;height:11px !important;font-size:10px !important;z-index:10 !important}.syntaxhighlighter .toolbar span.title{display:inline !important}.syntaxhighlighter .toolbar a{display:block !important;text-align:center !important;text-decoration:none !important;padding-top:1px !important}.syntaxhighlighter .toolbar a.expandSource{display:none !important}.syntaxhighlighter.ie{font-size:.9em !important;padding:1px 0 1px 0 !important}.syntaxhighlighter.ie .toolbar{line-height:8px !important}.syntaxhighlighter.ie .toolbar a{padding-top:0 !important}.syntaxhighlighter.printing .line.alt1 .content,.syntaxhighlighter.printing .line.alt2 .content,.syntaxhighlighter.printing .line.highlighted .number,.syntaxhighlighter.printing .line.highlighted.alt1 .content,.syntaxhighlighter.printing .line.highlighted.alt2 .content{background:none !important}.syntaxhighlighter.printing .line .number{color:#bbb !important}.syntaxhighlighter.printing .line .content{color:black !important}.syntaxhighlighter.printing .toolbar{display:none !important}.syntaxhighlighter.printing a{text-decoration:none !important}.syntaxhighlighter.printing .plain,.syntaxhighlighter.printing .plain a{color:black !important}.syntaxhighlighter.printing .comments,.syntaxhighlighter.printing .comments a{color:#008200 !important}.syntaxhighlighter.printing .string,.syntaxhighlighter.printing .string a{color:blue !important}.syntaxhighlighter.printing .keyword{color:#069 !important;font-weight:bold !important}.syntaxhighlighter.printing .preprocessor{color:gray !important}.syntaxhighlighter.printing .variable{color:#a70 !important}.syntaxhighlighter.printing .value{color:#090 !important}.syntaxhighlighter.printing .functions{color:#ff1493 !important}.syntaxhighlighter.printing .constants{color:#06c !important}.syntaxhighlighter.printing .script{font-weight:bold !important}.syntaxhighlighter.printing .color1,.syntaxhighlighter.printing .color1 a{color:gray !important}.syntaxhighlighter.printing .color2,.syntaxhighlighter.printing .color2 a{color:#ff1493 !important}.syntaxhighlighter.printing .color3,.syntaxhighlighter.printing .color3 a{color:red !important}.syntaxhighlighter.printing .break,.syntaxhighlighter.printing .break a{color:black !important} \ No newline at end of file +.syntaxhighlighter a,.syntaxhighlighter div,.syntaxhighlighter code,.syntaxhighlighter table,.syntaxhighlighter table td,.syntaxhighlighter table tr,.syntaxhighlighter table tbody,.syntaxhighlighter table thead,.syntaxhighlighter table caption,.syntaxhighlighter textarea{-moz-border-radius:0 0 0 0 !important;-webkit-border-radius:0 0 0 0 !important;background:none !important;border:0 !important;bottom:auto !important;float:none !important;height:auto !important;left:auto !important;line-height:1.1em !important;margin:0 !important;outline:0 !important;overflow:visible !important;padding:0 !important;position:static !important;right:auto !important;text-align:left !important;top:auto !important;vertical-align:baseline !important;width:auto !important;box-sizing:content-box !important;font-family:"Consolas","Bitstream Vera Sans Mono","Courier New",Courier,monospace !important;font-weight:normal !important;font-style:normal !important;font-size:1em !important;min-height:inherit !important;min-height:auto !important}.syntaxhighlighter{width:100% !important;margin:1em 0 1em 0 !important;position:relative !important;overflow:auto !important;font-size:1em !important}.syntaxhighlighter.source{overflow:hidden !important}.syntaxhighlighter .bold{font-weight:bold !important}.syntaxhighlighter .italic{font-style:italic !important}.syntaxhighlighter .line{white-space:pre !important}.syntaxhighlighter table{width:100% !important}.syntaxhighlighter table caption{text-align:left !important;padding:.5em 0 .5em 1em !important}.syntaxhighlighter table td.code{width:100% !important}.syntaxhighlighter table td.code .container{position:relative !important}.syntaxhighlighter table td.code .container textarea{box-sizing:border-box !important;position:absolute !important;left:0 !important;top:0 !important;width:100% !important;height:100% !important;border:none !important;background:white !important;padding-left:1em !important;overflow:hidden !important;white-space:pre !important}.syntaxhighlighter table td.gutter .line{text-align:right !important;padding:0 .5em 0 1em !important}.syntaxhighlighter table td.code .line{padding:0 1em !important}.syntaxhighlighter.nogutter td.code .container textarea,.syntaxhighlighter.nogutter td.code .line{padding-left:0 !important}.syntaxhighlighter.show{display:block !important}.syntaxhighlighter.collapsed table{display:none !important}.syntaxhighlighter.collapsed .toolbar{padding:.1em .8em 0 .8em !important;font-size:1em !important;position:static !important;width:auto !important;height:auto !important}.syntaxhighlighter.collapsed .toolbar span{display:inline !important;margin-right:1em !important}.syntaxhighlighter.collapsed .toolbar span a{padding:0 !important;display:none !important}.syntaxhighlighter.collapsed .toolbar span a.expandSource{display:inline !important}.syntaxhighlighter .toolbar{position:absolute !important;right:1px !important;top:1px !important;width:11px !important;height:11px !important;font-size:10px !important;z-index:10 !important}.syntaxhighlighter .toolbar span.title{display:inline !important}.syntaxhighlighter .toolbar a{display:block !important;text-align:center !important;text-decoration:none !important;padding-top:1px !important}.syntaxhighlighter .toolbar a.expandSource{display:none !important}.syntaxhighlighter.ie{font-size:.9em !important;padding:1px 0 1px 0 !important}.syntaxhighlighter.ie .toolbar{line-height:8px !important}.syntaxhighlighter.ie .toolbar a{padding-top:0 !important}.syntaxhighlighter.printing .line.alt1 .content,.syntaxhighlighter.printing .line.alt2 .content,.syntaxhighlighter.printing .line.highlighted .number,.syntaxhighlighter.printing .line.highlighted.alt1 .content,.syntaxhighlighter.printing .line.highlighted.alt2 .content{background:none !important}.syntaxhighlighter.printing .line .number{color:#bbb !important}.syntaxhighlighter.printing .line .content{color:black !important}.syntaxhighlighter.printing .toolbar{display:none !important}.syntaxhighlighter.printing a{text-decoration:none !important}.syntaxhighlighter.printing .plain,.syntaxhighlighter.printing .plain a{color:black !important}.syntaxhighlighter.printing .comments,.syntaxhighlighter.printing .comments a{color:#008200 !important}.syntaxhighlighter.printing .string,.syntaxhighlighter.printing .string a{color:blue !important}.syntaxhighlighter.printing .keyword{color:#069 !important;font-weight:bold !important}.syntaxhighlighter.printing .preprocessor{color:gray !important}.syntaxhighlighter.printing .variable{color:#a70 !important}.syntaxhighlighter.printing .value{color:#090 !important}.syntaxhighlighter.printing .functions{color:#ff1493 !important}.syntaxhighlighter.printing .constants{color:#06c !important}.syntaxhighlighter.printing .script{font-weight:bold !important}.syntaxhighlighter.printing .color1,.syntaxhighlighter.printing .color1 a{color:gray !important}.syntaxhighlighter.printing .color2,.syntaxhighlighter.printing .color2 a{color:#ff1493 !important}.syntaxhighlighter.printing .color3,.syntaxhighlighter.printing .color3 a{color:red !important}.syntaxhighlighter.printing .break,.syntaxhighlighter.printing .break a{color:black !important} diff --git a/datatableview/tests/example_project/example_project/example_app/static/syntaxhighlighter/shCore.js b/demo_app/example_app/static/syntaxhighlighter/shCore.js similarity index 99% rename from datatableview/tests/example_project/example_project/example_app/static/syntaxhighlighter/shCore.js rename to demo_app/example_app/static/syntaxhighlighter/shCore.js index b47b6454..effcf59d 100644 --- a/datatableview/tests/example_project/example_project/example_app/static/syntaxhighlighter/shCore.js +++ b/demo_app/example_app/static/syntaxhighlighter/shCore.js @@ -7,7 +7,7 @@ * * @version * 3.0.83 (July 02 2010) - * + * * @copyright * Copyright (C) 2004-2010 Alex Gorbatchev. * diff --git a/datatableview/tests/example_project/example_project/example_app/static/syntaxhighlighter/shThemeDefault.css b/demo_app/example_app/static/syntaxhighlighter/shThemeDefault.css similarity index 98% rename from datatableview/tests/example_project/example_project/example_app/static/syntaxhighlighter/shThemeDefault.css rename to demo_app/example_app/static/syntaxhighlighter/shThemeDefault.css index 45cb5646..8f8f3925 100644 --- a/datatableview/tests/example_project/example_project/example_app/static/syntaxhighlighter/shThemeDefault.css +++ b/demo_app/example_app/static/syntaxhighlighter/shThemeDefault.css @@ -7,11 +7,11 @@ * * @version * 3.0.83 (July 02 2010) - * + * * @copyright * Copyright (C) 2004-2010 Alex Gorbatchev. * * @license * Dual licensed under the MIT and GPL licenses. */ -.syntaxhighlighter{background-color:white !important}.syntaxhighlighter .line.alt1{background-color:white !important}.syntaxhighlighter .line.alt2{background-color:white !important}.syntaxhighlighter .line.highlighted.alt1,.syntaxhighlighter .line.highlighted.alt2{background-color:#e0e0e0 !important}.syntaxhighlighter .line.highlighted.number{color:black !important}.syntaxhighlighter table caption{color:black !important}.syntaxhighlighter .gutter{color:#afafaf !important}.syntaxhighlighter .gutter .line{border-right:3px solid #6ce26c !important}.syntaxhighlighter .gutter .line.highlighted{background-color:#6ce26c !important;color:white !important}.syntaxhighlighter.printing .line .content{border:none !important}.syntaxhighlighter.collapsed{overflow:visible !important}.syntaxhighlighter.collapsed .toolbar{color:blue !important;background:white !important;border:1px solid #6ce26c !important}.syntaxhighlighter.collapsed .toolbar a{color:blue !important}.syntaxhighlighter.collapsed .toolbar a:hover{color:red !important}.syntaxhighlighter .toolbar{color:white !important;background:#6ce26c !important;border:none !important}.syntaxhighlighter .toolbar a{color:white !important}.syntaxhighlighter .toolbar a:hover{color:black !important}.syntaxhighlighter .plain,.syntaxhighlighter .plain a{color:black !important}.syntaxhighlighter .comments,.syntaxhighlighter .comments a{color:#008200 !important}.syntaxhighlighter .string,.syntaxhighlighter .string a{color:blue !important}.syntaxhighlighter .keyword{color:#069 !important}.syntaxhighlighter .preprocessor{color:gray !important}.syntaxhighlighter .variable{color:#a70 !important}.syntaxhighlighter .value{color:#090 !important}.syntaxhighlighter .functions{color:#ff1493 !important}.syntaxhighlighter .constants{color:#06c !important}.syntaxhighlighter .script{font-weight:bold !important;color:#069 !important;background-color:none !important}.syntaxhighlighter .color1,.syntaxhighlighter .color1 a{color:gray !important}.syntaxhighlighter .color2,.syntaxhighlighter .color2 a{color:#ff1493 !important}.syntaxhighlighter .color3,.syntaxhighlighter .color3 a{color:red !important}.syntaxhighlighter .keyword{font-weight:bold !important} \ No newline at end of file +.syntaxhighlighter{background-color:white !important}.syntaxhighlighter .line.alt1{background-color:white !important}.syntaxhighlighter .line.alt2{background-color:white !important}.syntaxhighlighter .line.highlighted.alt1,.syntaxhighlighter .line.highlighted.alt2{background-color:#e0e0e0 !important}.syntaxhighlighter .line.highlighted.number{color:black !important}.syntaxhighlighter table caption{color:black !important}.syntaxhighlighter .gutter{color:#afafaf !important}.syntaxhighlighter .gutter .line{border-right:3px solid #6ce26c !important}.syntaxhighlighter .gutter .line.highlighted{background-color:#6ce26c !important;color:white !important}.syntaxhighlighter.printing .line .content{border:none !important}.syntaxhighlighter.collapsed{overflow:visible !important}.syntaxhighlighter.collapsed .toolbar{color:blue !important;background:white !important;border:1px solid #6ce26c !important}.syntaxhighlighter.collapsed .toolbar a{color:blue !important}.syntaxhighlighter.collapsed .toolbar a:hover{color:red !important}.syntaxhighlighter .toolbar{color:white !important;background:#6ce26c !important;border:none !important}.syntaxhighlighter .toolbar a{color:white !important}.syntaxhighlighter .toolbar a:hover{color:black !important}.syntaxhighlighter .plain,.syntaxhighlighter .plain a{color:black !important}.syntaxhighlighter .comments,.syntaxhighlighter .comments a{color:#008200 !important}.syntaxhighlighter .string,.syntaxhighlighter .string a{color:blue !important}.syntaxhighlighter .keyword{color:#069 !important}.syntaxhighlighter .preprocessor{color:gray !important}.syntaxhighlighter .variable{color:#a70 !important}.syntaxhighlighter .value{color:#090 !important}.syntaxhighlighter .functions{color:#ff1493 !important}.syntaxhighlighter .constants{color:#06c !important}.syntaxhighlighter .script{font-weight:bold !important;color:#069 !important;background-color:none !important}.syntaxhighlighter .color1,.syntaxhighlighter .color1 a{color:gray !important}.syntaxhighlighter .color2,.syntaxhighlighter .color2 a{color:#ff1493 !important}.syntaxhighlighter .color3,.syntaxhighlighter .color3 a{color:red !important}.syntaxhighlighter .keyword{font-weight:bold !important} diff --git a/datatableview/tests/example_project/example_project/example_app/templates/500.html b/demo_app/example_app/templates/500.html similarity index 100% rename from datatableview/tests/example_project/example_project/example_app/templates/500.html rename to demo_app/example_app/templates/500.html diff --git a/datatableview/tests/example_project/example_project/example_app/templates/base.html b/demo_app/example_app/templates/base.html similarity index 99% rename from datatableview/tests/example_project/example_project/example_app/templates/base.html rename to demo_app/example_app/templates/base.html index f2808b5a..dbf5958f 100644 --- a/datatableview/tests/example_project/example_project/example_app/templates/base.html +++ b/demo_app/example_app/templates/base.html @@ -5,7 +5,7 @@ {% block title %}{% endblock title %} {% block static %} {# jQuery #} - + {# datatables.js #} diff --git a/demo_app/example_app/templates/blank.html b/demo_app/example_app/templates/blank.html new file mode 100644 index 00000000..e69de29b diff --git a/datatableview/tests/example_project/example_project/example_app/templates/custom_table_template.html b/demo_app/example_app/templates/custom_table_template.html similarity index 100% rename from datatableview/tests/example_project/example_project/example_app/templates/custom_table_template.html rename to demo_app/example_app/templates/custom_table_template.html diff --git a/datatableview/tests/example_project/example_project/example_app/templates/demos/bootstrap_template.html b/demo_app/example_app/templates/demos/bootstrap_template.html similarity index 98% rename from datatableview/tests/example_project/example_project/example_app/templates/demos/bootstrap_template.html rename to demo_app/example_app/templates/demos/bootstrap_template.html index 37c1fc56..38e343ed 100644 --- a/datatableview/tests/example_project/example_project/example_app/templates/demos/bootstrap_template.html +++ b/demo_app/example_app/templates/demos/bootstrap_template.html @@ -6,5 +6,5 @@ - + {% endblock static %} diff --git a/datatableview/tests/example_project/example_project/example_app/templates/demos/col_reorder.html b/demo_app/example_app/templates/demos/col_reorder.html similarity index 100% rename from datatableview/tests/example_project/example_project/example_app/templates/demos/col_reorder.html rename to demo_app/example_app/templates/demos/col_reorder.html diff --git a/datatableview/tests/example_project/example_project/example_app/templates/demos/columns_reference.html b/demo_app/example_app/templates/demos/columns_reference.html similarity index 100% rename from datatableview/tests/example_project/example_project/example_app/templates/demos/columns_reference.html rename to demo_app/example_app/templates/demos/columns_reference.html diff --git a/datatableview/tests/example_project/example_project/example_app/templates/demos/configure_datatable_object.html b/demo_app/example_app/templates/demos/configure_datatable_object.html similarity index 98% rename from datatableview/tests/example_project/example_project/example_app/templates/demos/configure_datatable_object.html rename to demo_app/example_app/templates/demos/configure_datatable_object.html index 5a11ee93..82820293 100644 --- a/datatableview/tests/example_project/example_project/example_app/templates/demos/configure_datatable_object.html +++ b/demo_app/example_app/templates/demos/configure_datatable_object.html @@ -42,4 +42,4 @@

{% include "meta/hidden_columns.html" %}

{% include "meta/footer.html" %}

{% include "meta/structure_template.html" %} -{% endblock description %} \ No newline at end of file +{% endblock description %} diff --git a/datatableview/tests/example_project/example_project/example_app/templates/demos/configure_values_datatable_object.html b/demo_app/example_app/templates/demos/configure_values_datatable_object.html similarity index 94% rename from datatableview/tests/example_project/example_project/example_app/templates/demos/configure_values_datatable_object.html rename to demo_app/example_app/templates/demos/configure_values_datatable_object.html index 78c8373a..e2514495 100644 --- a/datatableview/tests/example_project/example_project/example_app/templates/demos/configure_values_datatable_object.html +++ b/demo_app/example_app/templates/demos/configure_values_datatable_object.html @@ -21,7 +21,7 @@ # Aliases have been given to the object for column names that didn't match the actual # ``sources`` names. Note how 'blog' is a list, because it had multiple sources. - 'publication_date': datetime.date(2013, 1, 1), + 'publication_date': datetime.date(2013, 1, 1), 'blog': [2, u'Second Blog'], } diff --git a/datatableview/tests/example_project/example_project/example_app/templates/demos/css_styling.html b/demo_app/example_app/templates/demos/css_styling.html similarity index 95% rename from datatableview/tests/example_project/example_project/example_app/templates/demos/css_styling.html rename to demo_app/example_app/templates/demos/css_styling.html index 03423f11..a080fd41 100644 --- a/datatableview/tests/example_project/example_project/example_app/templates/demos/css_styling.html +++ b/demo_app/example_app/templates/demos/css_styling.html @@ -21,4 +21,4 @@ } </style> -{% endblock description %} \ No newline at end of file +{% endblock description %} diff --git a/datatableview/tests/example_project/example_project/example_app/templates/demos/custom_model_fields.html b/demo_app/example_app/templates/demos/custom_model_fields.html similarity index 100% rename from datatableview/tests/example_project/example_project/example_app/templates/demos/custom_model_fields.html rename to demo_app/example_app/templates/demos/custom_model_fields.html diff --git a/datatableview/tests/example_project/example_project/example_app/templates/demos/helpers_reference.html b/demo_app/example_app/templates/demos/helpers_reference.html similarity index 100% rename from datatableview/tests/example_project/example_project/example_app/templates/demos/helpers_reference.html rename to demo_app/example_app/templates/demos/helpers_reference.html diff --git a/datatableview/tests/example_project/example_project/example_app/templates/demos/multi_filter.html b/demo_app/example_app/templates/demos/multi_filter.html similarity index 100% rename from datatableview/tests/example_project/example_project/example_app/templates/demos/multi_filter.html rename to demo_app/example_app/templates/demos/multi_filter.html diff --git a/datatableview/tests/example_project/example_project/example_app/templates/demos/multiple_tables.html b/demo_app/example_app/templates/demos/multiple_tables.html similarity index 100% rename from datatableview/tests/example_project/example_project/example_app/templates/demos/multiple_tables.html rename to demo_app/example_app/templates/demos/multiple_tables.html diff --git a/datatableview/tests/example_project/example_project/example_app/templates/demos/select_row.html b/demo_app/example_app/templates/demos/select_row.html similarity index 100% rename from datatableview/tests/example_project/example_project/example_app/templates/demos/select_row.html rename to demo_app/example_app/templates/demos/select_row.html diff --git a/datatableview/tests/example_project/example_project/example_app/templates/demos/x_editable_columns.html b/demo_app/example_app/templates/demos/x_editable_columns.html similarity index 100% rename from datatableview/tests/example_project/example_project/example_app/templates/demos/x_editable_columns.html rename to demo_app/example_app/templates/demos/x_editable_columns.html diff --git a/datatableview/tests/example_project/example_project/example_app/templates/example_base.html b/demo_app/example_app/templates/example_base.html similarity index 100% rename from datatableview/tests/example_project/example_project/example_app/templates/example_base.html rename to demo_app/example_app/templates/example_base.html diff --git a/datatableview/tests/example_project/example_project/example_app/templates/index.html b/demo_app/example_app/templates/index.html similarity index 100% rename from datatableview/tests/example_project/example_project/example_app/templates/index.html rename to demo_app/example_app/templates/index.html diff --git a/datatableview/tests/example_project/example_project/example_app/templates/javascript_initialization.html b/demo_app/example_app/templates/javascript_initialization.html similarity index 99% rename from datatableview/tests/example_project/example_project/example_app/templates/javascript_initialization.html rename to demo_app/example_app/templates/javascript_initialization.html index 79092a3c..868b447d 100644 --- a/datatableview/tests/example_project/example_project/example_app/templates/javascript_initialization.html +++ b/demo_app/example_app/templates/javascript_initialization.html @@ -29,7 +29,7 @@

Initialization

 $(function(){
     datatableview.initialize($('.mytable'));
-    
+
     // Or, if there are common options that should be given to all select elements,
     // you can specify them now.  data-* API attributes on the table columns will potentially
     // override the individual options.
diff --git a/datatableview/tests/example_project/example_project/example_app/templates/meta/columns.html b/demo_app/example_app/templates/meta/columns.html
similarity index 100%
rename from datatableview/tests/example_project/example_project/example_app/templates/meta/columns.html
rename to demo_app/example_app/templates/meta/columns.html
diff --git a/datatableview/tests/example_project/example_project/example_app/templates/meta/footer.html b/demo_app/example_app/templates/meta/footer.html
similarity index 100%
rename from datatableview/tests/example_project/example_project/example_app/templates/meta/footer.html
rename to demo_app/example_app/templates/meta/footer.html
diff --git a/datatableview/tests/example_project/example_project/example_app/templates/meta/hidden_columns.html b/demo_app/example_app/templates/meta/hidden_columns.html
similarity index 100%
rename from datatableview/tests/example_project/example_project/example_app/templates/meta/hidden_columns.html
rename to demo_app/example_app/templates/meta/hidden_columns.html
diff --git a/datatableview/tests/example_project/example_project/example_app/templates/meta/model.html b/demo_app/example_app/templates/meta/model.html
similarity index 100%
rename from datatableview/tests/example_project/example_project/example_app/templates/meta/model.html
rename to demo_app/example_app/templates/meta/model.html
diff --git a/datatableview/tests/example_project/example_project/example_app/templates/meta/ordering.html b/demo_app/example_app/templates/meta/ordering.html
similarity index 100%
rename from datatableview/tests/example_project/example_project/example_app/templates/meta/ordering.html
rename to demo_app/example_app/templates/meta/ordering.html
diff --git a/datatableview/tests/example_project/example_project/example_app/templates/meta/page_length.html b/demo_app/example_app/templates/meta/page_length.html
similarity index 100%
rename from datatableview/tests/example_project/example_project/example_app/templates/meta/page_length.html
rename to demo_app/example_app/templates/meta/page_length.html
diff --git a/datatableview/tests/example_project/example_project/example_app/templates/meta/search_fields.html b/demo_app/example_app/templates/meta/search_fields.html
similarity index 100%
rename from datatableview/tests/example_project/example_project/example_app/templates/meta/search_fields.html
rename to demo_app/example_app/templates/meta/search_fields.html
diff --git a/datatableview/tests/example_project/example_project/example_app/templates/meta/structure_template.html b/demo_app/example_app/templates/meta/structure_template.html
similarity index 100%
rename from datatableview/tests/example_project/example_project/example_app/templates/meta/structure_template.html
rename to demo_app/example_app/templates/meta/structure_template.html
diff --git a/datatableview/tests/example_project/example_project/example_app/templates/meta/unsortable_columns.html b/demo_app/example_app/templates/meta/unsortable_columns.html
similarity index 100%
rename from datatableview/tests/example_project/example_project/example_app/templates/meta/unsortable_columns.html
rename to demo_app/example_app/templates/meta/unsortable_columns.html
diff --git a/datatableview/tests/example_project/example_project/example_app/templates/migration_guide.html b/demo_app/example_app/templates/migration_guide.html
similarity index 99%
rename from datatableview/tests/example_project/example_project/example_app/templates/migration_guide.html
rename to demo_app/example_app/templates/migration_guide.html
index 81871a35..3fb5364e 100644
--- a/datatableview/tests/example_project/example_project/example_app/templates/migration_guide.html
+++ b/demo_app/example_app/templates/migration_guide.html
@@ -263,7 +263,7 @@ 
See ValuesDatatable ValuesDatatable as the base class for your table, rather than the default Datatable.

- +

This saves Django the trouble of instantiating model instances for each row, and might even encourage the developer to think about their data with fewer layers of abstraction. diff --git a/datatableview/tests/example_project/example_project/example_app/templates/valid_column_formats.html b/demo_app/example_app/templates/valid_column_formats.html similarity index 99% rename from datatableview/tests/example_project/example_project/example_app/templates/valid_column_formats.html rename to demo_app/example_app/templates/valid_column_formats.html index 4be157e9..8e08b683 100644 --- a/datatableview/tests/example_project/example_project/example_app/templates/valid_column_formats.html +++ b/demo_app/example_app/templates/valid_column_formats.html @@ -58,7 +58,7 @@

Deprecated column definition guide

], }
- +

Be careful with virtual columns that might cause database queries per-row. That doesn't scale very well! diff --git a/demo_app/example_app/templatetags/__init__.py b/demo_app/example_app/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/datatableview/tests/example_project/example_project/example_app/templatetags/example_app_tags.py b/demo_app/example_app/templatetags/example_app_tags.py similarity index 79% rename from datatableview/tests/example_project/example_project/example_app/templatetags/example_app_tags.py rename to demo_app/example_app/templatetags/example_app_tags.py index 3c49d2dd..102ee0eb 100644 --- a/datatableview/tests/example_project/example_project/example_app/templatetags/example_app_tags.py +++ b/demo_app/example_app/templatetags/example_app_tags.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- from django import template from django import get_version @@ -8,6 +8,6 @@ if get_version().split('.') < ['1', '5']: - @register.simple_tag(name="url") + @register.simple_tag(name='url') def django_1_4_url_simple(url_name): return reverse(url_name) diff --git a/datatableview/tests/example_project/example_project/example_app/urls.py b/demo_app/example_app/urls.py similarity index 76% rename from datatableview/tests/example_project/example_project/example_app/urls.py rename to demo_app/example_app/urls.py index ed2e4e4f..49e95832 100644 --- a/datatableview/tests/example_project/example_project/example_app/urls.py +++ b/demo_app/example_app/urls.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- import re @@ -18,14 +18,14 @@ continue if is_demo: name = re.sub(r'([a-z]|[A-Z]+)(?=[A-Z])', r'\1-', attr).lower() - name = name.replace("-datatable-view", "") + name = name.replace('-datatable-view', '') urls.append(url(r'^{name}/$'.format(name=name), View.as_view(), name=name)) urlpatterns = [ - url(r'^$', views.IndexView.as_view(), name="index"), + url(r'^$', views.IndexView.as_view(), name='index'), url(r'^reset/$', views.ResetView.as_view()), - url(r'^migration-guide/$', views.MigrationGuideView.as_view(), name="migration-guide"), - url(r'^column-formats/$', views.ValidColumnFormatsView.as_view(), name="column-formats"), - url(r'^javascript-initialization/$', views.JavascriptInitializationView.as_view(), name="js-init"), - url(r'^satellite/$', views.SatelliteDatatableView.as_view(), name="satellite"), + url(r'^migration-guide/$', views.MigrationGuideView.as_view(), name='migration-guide'), + url(r'^column-formats/$', views.ValidColumnFormatsView.as_view(), name='column-formats'), + url(r'^javascript-initialization/$', views.JavascriptInitializationView.as_view(), name='js-init'), + url(r'^satellite/$', views.SatelliteDatatableView.as_view(), name='satellite'), ] + urls diff --git a/datatableview/tests/example_project/example_project/example_app/views.py b/demo_app/example_app/views.py similarity index 96% rename from datatableview/tests/example_project/example_project/example_app/views.py rename to demo_app/example_app/views.py index 4fd5ed47..da8f8608 100644 --- a/datatableview/tests/example_project/example_project/example_app/views.py +++ b/demo_app/example_app/views.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- from os import sep import os.path @@ -24,11 +24,11 @@ def get(self, request, *args, **kwargs): from django.http import HttpResponse call_command('syncdb') call_command('loaddata', 'initial_data.json') - return HttpResponse("Done.") + return HttpResponse('Done.') class IndexView(TemplateView): - template_name = "index.html" + template_name = 'index.html' def get_context_data(self, **kwargs): context = super(IndexView, self).get_context_data(**kwargs) @@ -56,15 +56,15 @@ def get_context_data(self, **kwargs): class MigrationGuideView(TemplateView): - template_name = "migration_guide.html" + template_name = 'migration_guide.html' class JavascriptInitializationView(TemplateView): - template_name = "javascript_initialization.html" + template_name = 'javascript_initialization.html' class ValidColumnFormatsView(TemplateView): - template_name = "valid_column_formats.html" + template_name = 'valid_column_formats.html' class DemoMixin(object): @@ -73,9 +73,9 @@ class DemoMixin(object): def get_template_names(self): """ Try the view's snake_case name, or else use default simple template. """ - name = self.__class__.__name__.replace("DatatableView", "") + name = self.__class__.__name__.replace('DatatableView', '') name = re.sub(r'([a-z]|[A-Z]+)(?=[A-Z])', r'\1_', name) - return ["demos/" + name.lower() + ".html", "example_base.html"] + return ['demos/' + name.lower() + '.html', 'example_base.html'] def get_context_data(self, **kwargs): context = super(DemoMixin, self).get_context_data(**kwargs) @@ -100,7 +100,7 @@ def get_context_data(self, **kwargs): alert = True else: p.append(line) - description = "\n\n".join(" ".join(p) for p in paragraphs) + description = '\n\n'.join(' '.join(p) for p in paragraphs) context['description'] = re.sub(r'``(.*?)``', r'\1', description) return context @@ -179,8 +179,8 @@ class ConfigureValuesDatatableObject(DemoMixin, DatatableView): """ model = Entry class datatable_class(ValuesDatatable): - blog = columns.CompoundColumn("Blog", sources=['blog__id', 'blog__name']) - publication_date = columns.DateColumn("Publication Date", sources=['pub_date']) + blog = columns.CompoundColumn('Blog', sources=['blog__id', 'blog__name']) + publication_date = columns.DateColumn('Publication Date', sources=['pub_date']) class Meta: model = Entry @@ -251,7 +251,7 @@ class ConfigureDatatableOptions(DemoMixin, LegacyDatatableView): datatable_options = { 'columns': [ 'id', - ("Publication Date", 'pub_date'), + ('Publication Date', 'pub_date'), 'headline', ], } @@ -334,7 +334,7 @@ class datatable_class(Datatable): class Meta: columns = ['blog', 'headline', 'pub_date', 'n_comments', 'rating'] labels = { - 'pub_date': "Publication date", + 'pub_date': 'Publication date', } implementation = u""" @@ -407,8 +407,8 @@ class CustomColumnsDatatableView(DemoMixin, DatatableView): """ model = Entry class datatable_class(Datatable): - blog = columns.TextColumn("Blog", sources=['blog__name']) - age = columns.TextColumn("Age", sources=None, processor='get_entry_age') + blog = columns.TextColumn('Blog', sources=['blog__name']) + age = columns.TextColumn('Age', sources=None, processor='get_entry_age') class Meta: columns = ['blog', 'headline', 'age'] @@ -455,7 +455,7 @@ class ColumnBackedByMethodDatatableView(DemoMixin, DatatableView): """ model = Entry class datatable_class(Datatable): - pub_date = columns.DateColumn("Publication date", sources=['get_pub_date']) + pub_date = columns.DateColumn('Publication date', sources=['get_pub_date']) class Meta: columns = ['blog', 'headline', 'pub_date'] @@ -507,7 +507,7 @@ class ProcessorsDatatableView(DemoMixin, DatatableView): """ model = Entry class datatable_class(Datatable): - age = columns.TextColumn("Age", sources=['pub_date'], processor='get_entry_age') + age = columns.TextColumn('Age', sources=['pub_date'], processor='get_entry_age') class Meta: columns = ['blog', 'headline', 'pub_date', 'age'] @@ -516,7 +516,7 @@ class Meta: } def format_pub_date(self, instance, **kwargs): - return instance.pub_date.strftime("%m/%d/%Y") + return instance.pub_date.strftime('%m/%d/%Y') def get_entry_age(self, instance, **kwargs): return timesince(instance.pub_date) @@ -599,12 +599,12 @@ class CompoundColumnsDatatableView(DemoMixin, DatatableView): """ model = Entry class datatable_class(Datatable): - headline_blog = columns.TextColumn("Headline (Blog)", sources=['headline', 'blog__name'], - processor=helpers.format("{0[0]} ({0[1]})")) - headline_pub = columns.CompoundColumn("Headline (Published)", sources=[ + headline_blog = columns.TextColumn('Headline (Blog)', sources=['headline', 'blog__name'], + processor=helpers.format('{0[0]} ({0[1]})')) + headline_pub = columns.CompoundColumn('Headline (Published)', sources=[ columns.TextColumn(source='headline'), columns.DateColumn(source='pub_date') - ], processor=helpers.format("{0[0]} @ {0[1]}")) + ], processor=helpers.format('{0[0]} @ {0[1]}')) class Meta: columns = ['id', 'headline_blog', 'headline_pub'] @@ -644,17 +644,17 @@ class ManyToManyFieldsDatatableView(DemoMixin, DatatableView): """ model = Entry class datatable_class(Datatable): - author_names_text = columns.TextColumn("Author Names", sources=['authors__name'], processor='get_author_names') - author_names_links = columns.TextColumn("Author Links", sources=['authors__name'], processor='get_author_names_as_links') + author_names_text = columns.TextColumn('Author Names', sources=['authors__name'], processor='get_author_names') + author_names_links = columns.TextColumn('Author Links', sources=['authors__name'], processor='get_author_names_as_links') class Meta: columns = ['id', 'headline', 'author_names_text', 'author_names_links'] def get_author_names(self, instance, *args, **kwargs): - return ", ".join([author.name for author in instance.authors.all()]) + return ', '.join([author.name for author in instance.authors.all()]) def get_author_names_as_links(self, instance, *args, **kwargs): - return ", ".join([helpers.link_to_model(author) for author in instance.authors.all()]) + return ', '.join([helpers.link_to_model(author) for author in instance.authors.all()]) implementation = u""" class MyDatatable(Datatable): @@ -713,7 +713,7 @@ def get_column_body_text_data(self, instance, *args, **kwargs): return instance.body_text[:30] def get_column_pub_date_data(self, instance, *args, **kwargs): - return instance.pub_date.strftime("%m/%d/%Y") + return instance.pub_date.strftime('%m/%d/%Y') implementation = u""" class DefaultCallbackNamesDatatableView(DatatableView): @@ -827,9 +827,9 @@ class HelpersReferenceDatatableView(DemoMixin, XEditableDatatableView): """ model = Entry class datatable_class(Datatable): - blog_name = columns.TextColumn("Blog name", sources=['blog__name'], processor=helpers.link_to_model) - age = columns.TextColumn("Age", sources=['pub_date'], processor=helpers.through_filter(timesince)) - interaction = columns.IntegerColumn("Interaction", sources=['get_interaction_total'], processor=helpers.make_boolean_checkmark) + blog_name = columns.TextColumn('Blog name', sources=['blog__name'], processor=helpers.link_to_model) + age = columns.TextColumn('Age', sources=['pub_date'], processor=helpers.through_filter(timesince)) + interaction = columns.IntegerColumn('Interaction', sources=['get_interaction_total'], processor=helpers.make_boolean_checkmark) class Meta: columns = ['id', 'blog_name', 'headline', 'body_text', 'pub_date', 'mod_date', 'age', @@ -840,8 +840,8 @@ class Meta: 'headline': helpers.make_xeditable, 'body_text': helpers.itemgetter(slice(0, 30)), 'pub_date': helpers.format_date('%A, %b %d, %Y'), - 'n_comments': helpers.format("{0:,}"), - 'n_pingbacks': helpers.format("{0:,}"), + 'n_comments': helpers.format('{0:,}'), + 'n_pingbacks': helpers.format('{0:,}'), } implementation = u""" @@ -892,7 +892,7 @@ class Meta: def get_datatable(self): datatable = super(PerRequestOptionsDatatableView, self).get_datatable() - datatable.columns['blog'] = columns.TextColumn("Blog Name", sources=['blog__name']) + datatable.columns['blog'] = columns.TextColumn('Blog Name', sources=['blog__name']) del datatable.columns['id'] return datatable @@ -989,7 +989,7 @@ class CustomColumnQueriesDatatableView(DemoMixin, DatatableView): model = Entry class datatable_class(Datatable): - headline = HeadlineColumn("Headline", sources=['headline']) + headline = HeadlineColumn('Headline', sources=['headline']) class Meta: columns = ['id', 'headline'] @@ -1030,12 +1030,12 @@ class ChoicesFieldsDatatableView(DemoMixin, DatatableView): """ model = Entry class datatable_class(Datatable): - status_display = columns.TextColumn("Status Display", sources=['get_status_display']) + status_display = columns.TextColumn('Status Display', sources=['get_status_display']) class Meta: columns = ['id', 'headline', 'status', 'status_display', 'is_published'] labels = { - 'status': "Status Value", + 'status': 'Status Value', } implementation = u""" @@ -1224,7 +1224,7 @@ class SatelliteDatatableView(DatatableView): """ External view powering the embedded table for ``EmbeddedTableDatatableView``. """ - template_name = "blank.html" + template_name = 'blank.html' model = Entry class datatable_class(Datatable): class Meta: @@ -1305,7 +1305,7 @@ class ColReorderDatatableView(DemoMixin, DatatableView): model = Entry class datatable_class(Datatable): - blog = columns.TextColumn("Blog", sources=['blog__name']) + blog = columns.TextColumn('Blog', sources=['blog__name']) class Meta: columns = ['headline', 'blog'] @@ -1330,7 +1330,7 @@ class MultiFilterDatatableView(DemoMixin, DatatableView): model = Entry class datatable_class(Datatable): - blog = columns.TextColumn("Blog", sources=['blog__name']) + blog = columns.TextColumn('Blog', sources=['blog__name']) class Meta: columns = ['headline', 'blog'] @@ -1353,7 +1353,7 @@ class SelectRowDatatableView(DemoMixin, DatatableView): class datatable_class(Datatable): select_data = columns.CheckBoxSelectColumn() - blog = columns.TextColumn("Blog", sources=['blog__name']) + blog = columns.TextColumn('Blog', sources=['blog__name']) class Meta: columns = ['select_data', 'headline', 'blog'] @@ -1383,7 +1383,7 @@ class CustomizedTemplateDatatableView(DemoMixin, DatatableView): class datatable_class(Datatable): class Meta: columns = ['id', 'headline', 'blog', 'pub_date'] - structure_template = "custom_table_template.html" + structure_template = 'custom_table_template.html' implementation = u""" class MyDatatable(Datatable): @@ -1432,7 +1432,7 @@ class BootstrapTemplateDatatableView(DemoMixin, DatatableView): class datatable_class(Datatable): class Meta: columns = ['id', 'headline', 'blog', 'pub_date'] - structure_template = "datatableview/bootstrap_structure.html", + structure_template = 'datatableview/bootstrap_structure.html', implementation = u""" class MyDatatable(Datatable): @@ -1457,7 +1457,7 @@ class datatable_class(Datatable): class Meta: columns = ['id', 'headline', 'blog', 'pub_date'] labels = { - 'pub_date': "Publication Date", + 'pub_date': 'Publication Date', } implementation = u""" @@ -1472,5 +1472,3 @@ class CSSStylingDatatableView(DatatableView): model = Entry datatable_class = MyDatatable """ - - diff --git a/demo_app/manage.py b/demo_app/manage.py new file mode 100755 index 00000000..07336e21 --- /dev/null +++ b/demo_app/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo_app.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + 'available on your PYTHONPATH environment variable? Did you ' + 'forget to activate a virtual environment?' + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/demo_app/test_app/__init__.py b/demo_app/test_app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/datatableview/tests/test_app/fixtures/test_data_legacy.json b/demo_app/test_app/fixtures/test_data.json similarity index 100% rename from datatableview/tests/test_app/fixtures/test_data_legacy.json rename to demo_app/test_app/fixtures/test_data.json diff --git a/datatableview/tests/test_app/models.py b/demo_app/test_app/models.py similarity index 73% rename from datatableview/tests/test_app/models.py rename to demo_app/test_app/models.py index 916b8b4b..2948038a 100644 --- a/datatableview/tests/test_app/models.py +++ b/demo_app/test_app/models.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- from django.db import models @@ -9,17 +9,14 @@ class ExampleModel(models.Model): related = models.ForeignKey('RelatedModel', blank=True, null=True, on_delete=models.CASCADE) relateds = models.ManyToManyField('RelatedM2MModel', blank=True) - def __unicode__(self): - return "ExampleModel %d" % (self.pk,) - def __str__(self): - return "ExampleModel %d" % (self.pk,) + return 'ExampleModel %d' % (self.pk,) def __repr__(self): return "" % (self.pk, self.name,) def get_absolute_url(self): - return "#{pk}".format(pk=self.pk) + return '#{pk}'.format(pk=self.pk) def get_negative_pk(self): return -1 * self.pk @@ -28,14 +25,11 @@ def get_negative_pk(self): class RelatedModel(models.Model): name = models.CharField(max_length=15) - def __unicode__(self): - return "RelatedModel %d" % (self.pk,) - def __str__(self): - return "RelatedModel %d" % (self.pk,) + return 'RelatedModel %d' % (self.pk,) def get_absolute_url(self): - return "#{pk}".format(pk=self.pk) + return '#{pk}'.format(pk=self.pk) class RelatedM2MModel(models.Model): diff --git a/docs/datatableview/datatables.rst b/docs/datatableview/datatables.rst index 76224512..0829f2ef 100644 --- a/docs/datatableview/datatables.rst +++ b/docs/datatableview/datatables.rst @@ -236,5 +236,5 @@ ValuesLegacyDatatable :py:attr:`~datatableview.columns.Column.processor` callback. The mapped values may be direct references to callables, or strings that name a method on the Datatable or view. - :Example: + :Example: ``processors = {'name': 'get_name_data'}`` diff --git a/docs/datatableview/views.rst b/docs/datatableview/views.rst index 0c3d2025..1cd3e3e5 100644 --- a/docs/datatableview/views.rst +++ b/docs/datatableview/views.rst @@ -40,7 +40,7 @@ Use :py:class:`LegacyDatatableView` as your view's base class instead of :py:cla .. autoattribute:: datatable_class :annotation: = LegacyDatatable - + The :py:class:`~datatableview.datatables.LegacyDatatable` will help convert the more extravagant legacy tuple syntaxes into full :py:class:`~datatableview.columns.Column` instances. diff --git a/docs/index.rst b/docs/index.rst index 1711e3c9..c6a04de3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,4 +23,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/requirements.txt b/requirements.txt index 9e218e75..fcc7a122 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ -django>=1.11 -python-dateutil>=2.1 +python-decouple +python-dateutil +six diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 00000000..e5e56fa5 --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,10 @@ +psycopg2-binary +jsonfield2 +python-decouple +mock +flake8 +tox +isort +coverage +pre-commit +coveralls diff --git a/setup.py b/setup.py index c5949bca..995224d9 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- """setup.py: Django django-datatables-view""" from setuptools import setup, find_packages @@ -7,7 +7,7 @@ long_description = f.read() setup(name='django-datatable-view', - version='0.9.0', + version='1.0.0', description='This package is used in conjunction with the jQuery plugin ' '(http://http://datatables.net/), and supports state-saving detection' ' with (http://datatables.net/plug-ins/api). The package consists of ' @@ -18,7 +18,7 @@ author='Autumn Valenta', author_email='avalenta@pivotalenergysolutions.com', url='https://github.com/pivotal-energy-solutions/django-datatable-view', - download_url='https://github.com/pivotal-energy-solutions/django-datatable-view/tarball/django-datatable-view-0.9.0', + # download_url='https://github.com/pivotal-energy-solutions/django-datatable-view/tarball/django-datatable-view-0.9.0', license='Apache License (2.0)', classifiers=[ 'Development Status :: 2 - Pre-Alpha', @@ -30,8 +30,8 @@ 'Programming Language :: Python', 'Topic :: Software Development', ], - packages=find_packages(exclude=['tests', 'tests.*']), + packages=find_packages(), package_data={'datatableview': ['static/js/*.js', 'templates/datatableview/*.html']}, include_package_data=True, - install_requires=['django>=1.11', 'python-dateutil>=2.1'], + install_requires=['django>=2.1', 'python-dateutil'], ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..5878507e --- /dev/null +++ b/tox.ini @@ -0,0 +1,69 @@ +# Tox (https://tox.readthedocs.io/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = + {py37}-django{21,22,30} + {py38}-django{21,22,30} + {py39}-django{22,30,31} + {py310}-django{22,30,31} + pre-commit + +[cleanup] +commands = + find {toxinidir} -type f -name "*.pyc" -delete + find {toxinidir} -type d -name "__pycache__" -delete + find {toxinidir} -type f -path "*.egg-info*" -delete + find {toxinidir} -type d -path "*.egg-info" -delete + +[testenv:pre-commit] +skip_install = true +deps = pre-commit +commands = + pre-commit run --all-files --show-diff-on-failure + {[cleanup]commands} + +[testenv] +passenv = TRAVIS TRAVIS_* +whitelist_externals = + find +setenv = + PYTHONWARNINGS=once::DeprecationWarning + GIT_AUTHOR_NAME = "test" + GIT_COMMITTER_NAME = "test" + GIT_AUTHOR_EMAIL = "test@example.com" + GIT_COMMITTER_EMAIL = "test@example.com" +commands = + coverage erase + coverage run --source={envsitepackagesdir}/datatableview demo_app/manage.py test --noinput --settings=demo_app.settings_test datatableview.tests + coverage report -m + pre-commit install + {[cleanup]commands} +deps = + django21: Django>=2.1,<2.2 + django22: Django>=2.2,<2.3 + django30: Django>=3.0 + django31: Django>=3.1 + -rrequirements.txt + -rrequirements_dev.txt + +[travis] +python = + 3.7: py37 + 3.8: py38 + 3.9: py39 + 3.10: py310 + +[flake8] +max-line-length = 120 +max-complexity = 20 +ignore = F403,F405,W504 +exclude = + .venv/* + demo_app/* + */migrations/* + */tests/* + manage.py + setup.py