def test_main(self): class Klass1: pass class Klass2: pass class Klass3: pass ckm = ClassKeyedMap([(Klass1, 1), (Klass2, 2)]) self.assertEqual(1, ckm[Klass1]) self.assertEqual(2, ckm[Klass2]) self.assertEqual(2, len(ckm)) self.assertIsNone(ckm[Klass3]) self.assertEqual(3, len(ckm)) self.assertIsNone(ckm.default) self.assertTrue(hasattr(ckm, '__contains__')) self.assertIn(Klass1, ckm) empty = ClassKeyedMap() self.assertIs(True, bool(ckm)) self.assertIs(False, bool(empty)) self.assertEqual(0, len(empty)) self.assertNotIn(Klass1, empty) keys_set = {Klass1, Klass2, Klass3} self.assertEqual(keys_set, set(ckm)) self.assertEqual(keys_set, set(ckm.keys())) self.assertEqual({1, 2, None}, set(ckm.values())) self.assertEqual({(Klass1, 1), (Klass2, 2), (Klass3, None)}, set(ckm.items()) ) r = repr(ckm) self.assertTrue(r.startswith('ClassKeyedMap('))
class _EnumerableRegistry: """Registry which manages the choices available for (enumerable) model fields. Eg: will be used to propose available choices for filter-conditions related to a ForeignKey. The registry has a default behaviour for enumerable fields (it gets all the instances of the related instances -- using the attribute "limit_choices_too"). The behaviour can be overridden for: - The model related to the ForeignKey/ManyToManyField (see register_related_model()). - The type (class) of the model-field which we want to enumerate (see register_field_type()). - The model-field which we want to enumerate (see register_field()). """ class RegistrationError(Exception): pass def __init__(self): self._enums_4_fields = {} self._enums_4_field_types = ClassKeyedMap() self._enums_4_models = {} def __str__(self): res = '_EnumerableRegistry:' if self._enums_4_fields: res += '\n * Specific fields:' for field, enumerator_cls in self._enums_4_fields.items(): res += '\n - {field} -> {e_module}.{e_type}'.format( field=field, e_module=enumerator_cls.__module__, e_type=enumerator_cls.__qualname__, ) if self._enums_4_field_types: res += '\n * Field types:' for field_type, enumerator_cls in self._enums_4_field_types.items( ): res += '\n - {f_module}.{f_type} -> {e_module}.{e_type}'.format( f_module=field_type.__module__, f_type=field_type.__name__, e_module=enumerator_cls.__module__, e_type=enumerator_cls.__qualname__, ) if self._enums_4_models: res += '\n * Related models:' for model, enumerator_cls in self._enums_4_models.items(): res += '\n - {app}.{model} -> {e_module}.{e_type}'.format( app=model._meta.app_label, model=model.__name__, e_module=enumerator_cls.__module__, e_type=enumerator_cls.__qualname__, ) return res @staticmethod def _check_model(model): # TODO: and registered as an entity ?? if not issubclass(model, CremeEntity): raise ValueError('This model is not a CremeEntity: {}.{}'.format( model.__module__, model.__name__, )) @staticmethod def _check_field(field): if not field.get_tag('viewable'): # TODO: unit test (needs new field) raise ValueError('This field is not viewable: {}'.format(field)) # TODO: we probably should manage fields with is_relation==False but with # a 'choices' attribute. Wait to add the feature in EntityFilterForm too. if not field.get_tag('enumerable'): raise ValueError('This field is not enumerable: {}'.format(field)) def _get_field(self, model, field_name): field = model._meta.get_field( field_name) # Can raise FieldDoesNotExist self._check_field(field) return field def _enumerator(self, field): enumerator_cls = self._enums_4_fields.get(field) or \ self._enums_4_field_types[field.__class__] or \ self._enums_4_models.get(field.remote_field.model, QSEnumerator) return enumerator_cls(field) def enumerator_by_field(self, field): """Get an Enumerator instance corresponding to a model field. @param field: Model field instance. @return: Instance of Enumerator. @raises: ValueError if the model or the field are invalid. """ self._check_model(field.model) self._check_field(field) return self._enumerator(field) def enumerator_by_fieldname(self, model, field_name): """Get an Enumerator instance corresponding to a model field. @param model: Model inheriting CremeEntity. @param field_name: Name of a field (string). @return: Instance of Enumerator. @raises: ValueError if the model or the field are invalid. @raises: FieldDoesNotExist. """ self._check_model(model) return self._enumerator(self._get_field(model, field_name)) def register_field(self, model, field_name, enumerator_class): """Customise the class of the enumerator returned by the methods enumerator_by_field[name] for a specific field. @param model: Model inheriting 'CremeEntity'. @param field_name: Name of a field of <model>. @param enumerator_class: Class inheriting 'Enumerator'. @return: self (to chain calls to register_*() methods). @raises: ValueError if the model or the field are invalid. @raises: FieldDoesNotExist. """ assert issubclass(enumerator_class, Enumerator) field = self._get_field(model, field_name) if self._enums_4_fields.setdefault( field, enumerator_class) is not enumerator_class: raise self.RegistrationError( '_EnumerableRegistry: this field is already registered: {model}.{field}' .format( model=model.__name__, field=field_name, )) return self def register_field_type(self, field_class, enumerator_class): """Customise the class of the enumerator returned by the methods enumerator_by_field[name] for a specific field class. @param field_class: Class inheriting 'django.db.models.Field'. @param enumerator_class: Class inheriting 'Enumerator'. @return: self (to chain calls to register_*() methods). """ assert issubclass(enumerator_class, Enumerator) self._enums_4_field_types[field_class] = enumerator_class return self def register_related_model(self, model, enumerator_class): """Customise the class of the enumerator returned by the methods enumerator_by_field[name] for ForeignKeys/ManyToManyFields which reference a specific model. @param model: Model (class inheriting 'django.db.models.Model'). @param enumerator_class: Class inheriting 'Enumerator'. @return: self (to chain calls to register_*() methods). """ assert issubclass(enumerator_class, Enumerator) if self._enums_4_models.setdefault( model, enumerator_class) is not enumerator_class: raise self.RegistrationError( '_EnumerableRegistry: this model is already registered: {}'. format(model)) return self
class RegularFieldSearchRegistry(AbstractListViewSearchFieldRegistry): """Class of ListViewSearchFieldRegistry specialized for cells representing model fields (CharField, BooleanField, ForeignKey...). The returned search-field can be customised depending on (from greater priority to lesser): - A model-field (eg: the field "name" of your model <Book>). - The class of the model-field (eg: fields which have class inheriting CharField). There is a special case for model-fields which have choices. """ DEFAULT_REGISTRATIONS = ( (models.CharField, lv_form.RegularCharField), (models.TextField, lv_form.RegularCharField), (models.IntegerField, lv_form.RegularIntegerField), (models.PositiveIntegerField, lv_form.RegularPositiveIntegerField), (models.PositiveSmallIntegerField, lv_form.RegularPositiveIntegerField), (models.FloatField, lv_form.RegularFloatField), (models.DecimalField, lv_form.RegularDecimalField), (models.BooleanField, lv_form.RegularBooleanField), # (models.NullBooleanField, RegularBooleanField), (models.DateField, lv_form.RegularDateField), # (models.TimeField, ), TODO (models.ForeignKey, RegularRelatedFieldSearchRegistry), (models.ManyToManyField, RegularRelatedFieldSearchRegistry), # (models.OneToOneField, RegularRelatedFieldSearchRegistry), TODO # (models.IPAddressField, ...) # TODO: what about ? # TODO: needs JSONField management in the RDBMS... # (fields.DurationField, ), # (fields.DatePeriodField, ), # No search # (models.FileField, ), # (models.ImageField, ), ) def __init__(self, to_register=DEFAULT_REGISTRATIONS, choice_sfield_builder=lv_form.RegularChoiceField): self._builders_4_modelfields = {} self._builders_4_modelfieldtypes = ClassKeyedMap(default=None) self.register_choice_builder(choice_sfield_builder) for model_field_cls, builder in to_register: self.register_model_field_type(type=model_field_cls, sfield_builder=builder) def builder_4_model_field(self, *, model, field_name): field = model._meta.get_field(field_name) return self._builders_4_modelfields.get(field) def builder_4_model_field_type(self, model_field): return self._builders_4_modelfieldtypes[model_field] @property def choice_builder(self): return self._choice_builder def get_field(self, *, cell, user, **kwargs): model_field = cell.field_info[-1] return self._build_field( builder=self._builders_4_modelfields.get(model_field) or (self._choice_builder if model_field.choices else self._builders_4_modelfieldtypes[type(model_field)]), cell=cell, user=user, **kwargs) def pretty(self, indent=0): indent_str = ' ' * indent res = '{indent}{name}:\n{indent} Choice:\n{choice}\n{indent} Field types:'.format( indent=indent_str, name=type(self).__name__, choice=self._pretty_builder(self._choice_builder, indent=indent + 4), ) for field_type, builder in self._builders_4_modelfieldtypes.items(): res += '\n{} [{}.{}]:\n{}'.format( indent_str, field_type.__module__, field_type.__name__, self._pretty_builder(builder, indent=indent + 6), ) res += '\n{} Fields:'.format(indent_str) modelfields = self._builders_4_modelfields if modelfields: for field, builder in modelfields.items(): res += '\n{} [{}]:\n{}'.format( indent_str, field, self._pretty_builder(builder, indent=indent + 6), ) else: res += '\n{} (empty)'.format(indent_str) return res def register_choice_builder(self, sfield_builder): self._choice_builder = self._instantiate_builder(sfield_builder) return self def register_model_field(self, *, model, field_name, sfield_builder): field = model._meta.get_field(field_name) self._builders_4_modelfields[field] = self._instantiate_builder( sfield_builder) # TODO ? # if self._enums_4_fields.setdefault(field, enumerator_class) is not enumerator_class: # raise self.RegistrationError( # '_EnumerableRegistry: this field is already registered: {model}.{field}'.format( # model=model.__name__, field=field_name, # ) # ) return self def register_model_field_type(self, *, type, sfield_builder): self._builders_4_modelfieldtypes[type] = self._instantiate_builder( sfield_builder) return self
class RegularFieldSorterRegistry(AbstractCellSorter): """Class of sorter for all types of EntityCellRegularField. Sub-sorters can be registered to customise the behaviour for specific model-fields & model-field classes. """ DEFAULT_SORTERS = ( (models.AutoField, RegularFieldSorter), (models.BooleanField, RegularFieldSorter), (models.DecimalField, RegularFieldSorter), (models.FloatField, RegularFieldSorter), (models.IntegerField, RegularFieldSorter), (models.CharField, RegularFieldSorter), (models.TextField, RegularFieldSorter), (models.DateField, RegularFieldSorter), (models.TimeField, RegularFieldSorter), (models.ForeignKey, ForeignKeySorterRegistry), # No sorting # models.ManyToManyField # models.OneToOneField (models.CommaSeparatedIntegerField, VoidSorter), # models.FilePathField # models.BinaryField # models.UUIDField # (fields.DurationField, VoidSorter), TODO ? (fields.DatePeriodField, VoidSorter ), # TODO: needs JSONField management in the RDBMS... # TODO: what about ? # (models.DurationField, ...) # (models.IPAddressField, ...) # (models.GenericIPAddressField, ...) # (models.SlugField, ...) # (models.URLField, ...) ) def __init__(self, to_register=DEFAULT_SORTERS): # self._sorters_4_modelfields = {} # TODO: when order is kept (py3.6+) self._sorters_4_modelfields = OrderedDict() self._sorters_4_modelfieldtypes = ClassKeyedMap(default=None) for model_field_cls, sorter_cls in to_register: self.register_model_field_type(type=model_field_cls, sorter_cls=sorter_cls) def get_field_name(self, cell): field = cell.field_info[-1] sorter = self._sorters_4_modelfields.get(field) or \ self._sorters_4_modelfieldtypes[type(field)] return None if sorter is None else sorter.get_field_name(cell=cell) def pretty(self, indent=0): indent_str = ' ' * indent res = '{indent}{name}:\n{indent} Field types:'.format( indent=indent_str, name=type(self).__name__, ) for field_type, sorter in self._sorters_4_modelfieldtypes.items(): res += '\n{} [{}.{}]:\n{}'.format( indent_str, field_type.__module__, field_type.__name__, sorter.pretty(indent=indent + 6), ) res += '\n{} Fields:'.format(indent_str) modelfields = self._sorters_4_modelfields if modelfields: for field, sorter in modelfields.items(): res += '\n{} [{}]:\n{}'.format( indent_str, field, sorter.pretty(indent=indent + 6), ) else: res += '\n{} (empty)'.format(indent_str) return res def register_model_field(self, *, model, field_name, sorter_cls): field = model._meta.get_field(field_name) self._sorters_4_modelfields[field] = sorter_cls() # TODO ? # if self._enums_4_fields.setdefault(field, enumerator_class) is not enumerator_class: # raise self.RegistrationError( # '_EnumerableRegistry: this field is already registered: {model}.{field}'.format( # model=model.__name__, field=field_name, # ) # ) return self def register_model_field_type(self, *, type, sorter_cls): self._sorters_4_modelfieldtypes[type] = sorter_cls() return self def sorter_4_model_field(self, *, model, field_name): field = model._meta.get_field(field_name) return self._sorters_4_modelfields.get(field) def sorter_4_model_field_type(self, model_field): return self._sorters_4_modelfieldtypes[model_field]