Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions datatableview/columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import django
from django.db import models
from django.db.models import Model, Manager, Q
from django.db.models import Model, Manager, Q, QuerySet
from django.db.models.fields import FieldDoesNotExist
from django.core.exceptions import ObjectDoesNotExist
from django.utils.encoding import smart_text
Expand Down Expand Up @@ -57,7 +57,10 @@ def get_column_for_modelfield(model_field):
# inheritance, however, so we need to traverse the internal OneToOneField as well, so this will
# climb the 'pk' field chain until we have something real.
while model_field.related_model:
model_field = model_field.related_model._meta.pk
try :
model_field = model_field.related_model._meta.get_field('name')
except FieldDoesNotExist:
model_field = model_field.related_model._meta.pk
for ColumnClass, modelfield_classes in COLUMN_CLASSES:
if isinstance(model_field, tuple(modelfield_classes)):
return ColumnClass
Expand All @@ -71,6 +74,8 @@ def get_attribute_value(obj, bit):
if callable(value) and not isinstance(value, Manager):
if not hasattr(value, 'alters_data') or value.alters_data is not True:
value = value()
elif callable(value) and isinstance(value, Manager):
value = value.all()
return value

class ColumnMetaclass(type):
Expand Down Expand Up @@ -183,13 +188,19 @@ def get_initial_value(self, obj, **kwargs):

for value in result:
if isinstance(value, Model):
value = (value.pk, value)
value = (value.pk, str(value))
elif isinstance(value, QuerySet):
value = (
', '.join([ str(x) for x in value]),
','.join([ str(x.pk) for x in value]),
)

if value is not None:
if not isinstance(value, (tuple, list)):
value = (value, value)
values.append(value)


if len(values) == 1:
value = values[0]
if value is None and self.empty_value is not None:
Expand Down Expand Up @@ -360,7 +371,7 @@ def search(self, model, term, lookup_types=None):
else:
choices = modelfield.get_flatchoices()
for db_value, label in choices:
if term.lower() in label.lower():
if term.lower() in str(label).lower():
k = '%s__exact' % (sub_source,)
column_queries.append(Q(**{k: str(db_value)}))

Expand All @@ -375,6 +386,9 @@ def search(self, model, term, lookup_types=None):
# Skip attempts to build multi-component searches if we only have one term
continue

if modelfield.get_internal_type() in ['ManyToManyField', 'ForeignKey' ] :
sub_source += '__name'

k = '%s__%s' % (sub_source, lookup_type)
column_queries.append(Q(**{k: coerced_term}))

Expand Down Expand Up @@ -421,13 +435,17 @@ def attributes(self):

class TextColumn(Column):
model_field_class = models.CharField
handles_field_classes = [models.CharField, models.TextField, models.FileField]
handles_field_classes = [models.CharField, models.TextField, models.FileField, models.GenericIPAddressField]

# Add UUIDField if present in this version of Django
try:
handles_field_classes.append(models.UUIDField)
except AttributeError:
pass
try:
handles_field_classes.append(models.GenericIPAddressField)
except AttributeError:
pass

lookup_types = ('icontains', 'in')

Expand Down Expand Up @@ -504,6 +522,11 @@ def prep_search_value(self, term, lookup_type):
return None
return super(BooleanColumn, self).prep_search_value(term, lookup_type)

class ManyColumn(Column):
model_field_class = models.ManyToManyField
handles_field_classes = [models.ManyToManyField]
#lookup_types = ('exact', 'in')
lookup_types = ('name__icontains')

class IntegerColumn(Column):
model_field_class = models.IntegerField
Expand Down
9 changes: 6 additions & 3 deletions datatableview/datatables.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

from .exceptions import ColumnError, SkipRecord
from .columns import (Column, TextColumn, DateColumn, DateTimeColumn, BooleanColumn, IntegerColumn,
FloatColumn, DisplayColumn, CompoundColumn, get_column_for_modelfield)
FloatColumn, DisplayColumn, CompoundColumn, ManyColumn, 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
Expand All @@ -41,14 +41,16 @@ def pretty_name(name):
def columns_for_model(model, fields=None, exclude=None, labels=None, processors=None,
unsortable=None, hidden=None):
field_list = []
opts = model._meta
for f in sorted(opts.fields):
metas_fields = model._meta.many_to_many + model._meta.fields
for f in sorted(metas_fields):
if fields is not None and f.name not in fields:
continue
if exclude and f.name in exclude:
continue

column_class = get_column_for_modelfield(f)
#if f.name == 'services' :
# column_class = ManyColumn
if column_class is None:
raise ColumnError("Unhandled model field %r." % (f,))
if labels and f.name in labels:
Expand Down Expand Up @@ -134,6 +136,7 @@ def __init__(self, options=None):
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')
self.editable_cond = getattr(options, 'editable_cond', False)

# Non-mutable; server behavior customization
self.cache_type = getattr(options, 'cache_type', cache_types.NONE)
Expand Down
15 changes: 13 additions & 2 deletions datatableview/forms.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# -*- encoding: utf-8 -*-

from django import forms
from django.forms import ValidationError
from django.forms import ValidationError, ModelMultipleChoiceField
from django.forms.models import fields_for_model



class XEditableUpdateForm(forms.Form):
"""
Represents only a single field of a given model instance.
Expand All @@ -23,7 +24,6 @@ class XEditableUpdateForm(forms.Form):

def __init__(self, model, data, *args, **kwargs):
super(XEditableUpdateForm, self).__init__(data, *args, **kwargs)

self.model = model
self.set_value_field(model, data.get('name'))

Expand All @@ -32,10 +32,20 @@ def set_value_field(self, model, field_name):
Adds a ``value`` field to this form that uses the appropriate formfield for the named target
field. This will help to ensure that the value is correctly validated.
"""

fields = fields_for_model(model, fields=[field_name])
#we add the validators
for field in self.model._meta.fields :
if field.name == field_name :
for validator in field.validators:
if validator not in fields[field_name].validators :
fields[field_name].validators.append(validator)
self.fields['value'] = fields[field_name]

if isinstance(self.fields['value'], ModelMultipleChoiceField) :
self.fields['value'].required = False
def clean_name(self):

""" Validates that the ``name`` field corresponds to a field on the model. """
field_name = self.cleaned_data['name']
# get_all_field_names is deprecated in Django 1.8, this also fixes proxied models
Expand All @@ -46,3 +56,4 @@ def clean_name(self):
if field_name not in field_names:
raise ValidationError("%r is not a valid field." % field_name)
return field_name

24 changes: 20 additions & 4 deletions datatableview/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,16 @@ def make_xeditable(instance=None, extra_attrs=[], *args, **kwargs):
data = kwargs.get('default_value', instance)
rich_data = kwargs.get('rich_value', data)

field_name = kwargs['field_name'] # sent as a default kwarg to helpers

if kwargs['datatable']._meta.editable_cond :
if not eval(kwargs['datatable']._meta.editable_cond):
if instance._meta.get_field(field_name).get_internal_type() == 'ManyToManyField':
return data
else :
return getattr(instance, 'get_{}_display'.format(field_name), lambda: data)()
#return rich_data

# Compile values to appear as "data-*" attributes on the anchor tag
default_attr_names = ['pk', 'type', 'url', 'source', 'title', 'placeholder']
valid_attr_names = set(default_attr_names + list(extra_attrs))
Expand All @@ -324,11 +334,13 @@ def make_xeditable(instance=None, extra_attrs=[], *args, **kwargs):
k = k[5:]
attrs['data-{0}'.format(k)] = v

attrs['data-xeditable'] = "xeditable"

# Assign default values where they are not provided
if instance._meta.get_field(field_name).get_internal_type() == 'ManyToManyField' and attrs.get('data-type',None) == 'select2' :
attrs['data-xeditable-m2m'] = "xeditable"
else :
attrs['data-xeditable'] = "xeditable"

field_name = kwargs['field_name'] # sent as a default kwarg to helpers
# Assign default values where they are not provided
if isinstance(field_name, (tuple, list)):
# Legacy syntax
field_name = field_name[1]
Expand Down Expand Up @@ -369,23 +381,27 @@ def make_xeditable(instance=None, extra_attrs=[], *args, **kwargs):
else:
field = resolve_orm_path(instance, field_name)


if field.choices:
field_type = 'select'
else:
field_type = XEDITABLE_FIELD_TYPES.get(field.get_internal_type(), 'text')

else:
field_type = 'text'
attrs['data-type'] = field_type


# type=select elements need to fetch their valid choice options from an AJAX endpoint.
# Register the view for this lookup.
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}&pk={pk}".format(**{
'url': kwargs['view'].request.path,
'field_param': kwargs['view'].xeditable_fieldname_param,
'fieldname': field_name,
'pk': instance.pk,
})
if attrs['data-type'] == 'select2':
attrs['data-source'] += '&select2=true'
Expand Down
Loading