def __init__(self, to=None, to_base=None, **kwargs): """ Initialise the tag options and store Takes all tag options as keyword arguments, as long as there is no ``to`` tag model specified. Undocumented keyword argument ``_set_tag_meta`` is used internally to update tag model options with those specified in keyword arguments when a ``to`` tag model is specified. However, this is intended for internal use only (when migrating), so if you must use it, use it with care. """ # Save tag model data self.tag_model = to self.tag_model_base = to_base # Extract options from kwargs options = {} for key, default in constants.OPTION_DEFAULTS.items(): # Look in kwargs, then in tag_meta if key in kwargs: options[key] = kwargs.pop(key) # Detect if _set_tag_meta is set set_tag_meta = kwargs.pop("_set_tag_meta", False) # Detect whether we need to automatically generate a tag model if self.tag_model: self.auto_tag_model = False # Tag model might be a string - set options later self.tag_options = None self._deferred_options = [set_tag_meta, options] else: self.auto_tag_model = True self.tag_options = TagOptions(**options) self._deferred_options = None # If the tag model was not specified, we need to specify one. # However, we can't reliably auto-generate a unique and repeatable # model name for tags here in __init__ - we can only do that in # contribute_to_class once we know the name of the field in the model. # We'll therefore use the string '-'; Django will not do anything about # resolving it until contribute_to_class, at which point we'll replace # it with a reference to the real tag model. kwargs["to"] = self.tag_model if self.tag_model else "-" # Call super __init__ super(BaseTagField, self).__init__(**kwargs) # Make a note that this has not been contributed to a class yet self.contributed = False # This attribute will let us tell South to supress undesired M2M fields self.south_supression = True
def __new__(cls, name, bases, attrs): # Set up as normal new_cls = super(TagModelBase, cls).__new__(cls, name, bases, attrs) # TagMeta takes priority for the model new_tag_options = None if 'TagMeta' in attrs: tag_meta = {} tag_meta = dict( (key, val) for key, val in attrs['TagMeta'].__dict__.items() if key in constants.OPTION_DEFAULTS ) if 'tree' in tag_meta: raise ValueError('Cannot set tree option in TagMeta') new_tag_options = TagOptions(**tag_meta) # Failing that, look for a direct tag_options setting # It will probably have been passed by BaseTagField.contribute_to_class elif 'tag_options' in attrs: new_tag_options = attrs['tag_options'] # Otherwise start a new one else: new_tag_options = TagOptions() # See if there's anything to inherit # This also means that tag_options will be available on abstract models if hasattr(new_cls, 'tag_options'): # Inherit by setting missing values in place new_tag_options.set_missing(new_cls.tag_options) # Assign new_cls.tag_options = new_tag_options # Check for self-referential tag fields on this model fields = new_cls._meta.fields + new_cls._meta.many_to_many for field in fields: # Can't test for subclass of field here - would be circular import if hasattr(field, 'tag_model') and field.tag_model == new_cls: # This method is being called after the tag field's # contribute_to_class and _process_deferred_options. This means # that the field is using tag_options from the inherited model. # Change it to use this one field.tag_options = new_tag_options return new_cls
def __init__(self, to=None, to_base=None, **kwargs): """ Initialise the tag options and store Takes all tag options as keyword arguments, as long as there is no ``to`` tag model specified. Undocumented keyword argument ``_set_tag_meta`` is used internally to update tag model options with those specified in keyword arguments when a ``to`` tag model is specified. However, this is intended for internal use only (when migrating), so if you must use it, use it with care. """ # Save tag model data self.tag_model = to self.tag_model_base = to_base # Extract options from kwargs options = {} for key, default in constants.OPTION_DEFAULTS.items(): # Look in kwargs, then in tag_meta if key in kwargs: options[key] = kwargs.pop(key) # Detect if _set_tag_meta is set set_tag_meta = kwargs.pop('_set_tag_meta', False) # Detect whether we need to automatically generate a tag model if self.tag_model: self.auto_tag_model = False # Tag model might be a string - set options later self.tag_options = None self._deferred_options = [set_tag_meta, options] else: self.auto_tag_model = True self.tag_options = TagOptions(**options) self._deferred_options = None # If the tag model was not specified, we need to specify one. # However, we can't reliably auto-generate a unique and repeatable # model name for tags here in __init__ - we can only do that in # contribute_to_class once we know the name of the field in the model. # We'll therefore use the string '-'; Django will not do anything about # resolving it until contribute_to_class, at which point we'll replace # it with a reference to the real tag model. kwargs['to'] = self.tag_model if self.tag_model else '-' # Call super __init__ super(BaseTagField, self).__init__(**kwargs) # Make a note that this has not been contributed to a class yet self.contributed = False # This attribute will let us tell South to supress undesired M2M fields self.south_supression = True
def formfield(self, form_class, **kwargs): """ Common actions for TagField and SingleTagField to set up a formfield """ required = not self.blank if hasattr(self, 'required'): required = self.required # Update tag options, if necessary tag_options = self.tag_options if 'tag_options' in kwargs: new_options = kwargs.pop('tag_options') if not isinstance(new_options, TagOptions): new_options = TagOptions(**new_options) tag_options += new_options # Start off with defaults options = { # Arguments the TagField base (CharField) would expect "label": capfirst(self.verbose_name), "help_text": self.help_text, "required": required, # Also pass tag options "tag_options": tag_options } # Update with kwargs options.update(kwargs) # Add in list of tags for autocomplete, if appropriate if 'autocomplete_tags' in kwargs: options['autocomplete_tags'] = kwargs['autocomplete_tags'] elif not tag_options.autocomplete_view: tags = self.tag_model.objects.all() if tag_options.autocomplete_initial: tags = tags.initial() options['autocomplete_tags'] = tags # Create the field instance return form_class(**options)
class BaseTagField(object): """ Mixin for TagField and SingleTagField """ # List of fields which are forbidden from __init__ forbidden_fields = () def __init__(self, to=None, **kwargs): """ Initialise the tag options and store Takes all tag options as keyword arguments, as long as there is no ``to`` tag model specified. Undocumented keyword argument ``_set_tag_meta`` is used internally to update tag model options with those specified in keyword arguments when a ``to`` tag model is specified. However, this is intended for internal use only (when migrating), so if you must use it, use it with care. """ # Save tag model self.tag_model = to # Extract options from kwargs options = {} for key, default in constants.OPTION_DEFAULTS.items(): # Look in kwargs, then in tag_meta if key in kwargs: options[key] = kwargs.pop(key) # Detect if _set_tag_meta is set set_tag_meta = kwargs.pop('_set_tag_meta', False) # Detect whether we need to automatically generate a tag model if self.tag_model: self.auto_tag_model = False # Tag model might be a string - set options later self.tag_options = None self._deferred_options = [set_tag_meta, options] else: self.auto_tag_model = True self.tag_options = TagOptions(**options) self._deferred_options = None # If the tag model was not specified, we need to specify one. # However, we can't reliably auto-generate a unique and repeatable # model name for tags here in __init__ - we can only do that in # contribute_to_class once we know the name of the field in the model. # We'll therefore use the string '-'; Django will not do anything about # resolving it until contribute_to_class, at which point we'll replace # it with a reference to the real tag model. kwargs['to'] = self.tag_model if self.tag_model else '-' # Call super __init__ super(BaseTagField, self).__init__(**kwargs) # Make a note that this has not been contributed to a class yet self.contributed = False # This attribute will let us tell South to supress undesired M2M fields self.south_supression = True if django.VERSION < (1, 9): remote_field = property(lambda self: self.rel) def do_related_class(self, other, cls): """ Process tag model now it has been resolved if it was a string """ # Set up relation as normal super(BaseTagField, self).do_related_class(other, cls) # Make sure tag model is the related model, in case it was a string if django.VERSION < (1, 8): # Django 1.7 or earlier self.tag_model = self.related.parent_model else: # Django 1.8 or later self.tag_model = self.remote_field.model # Check class type of tag model if not issubclass(self.tag_model, BaseTagModel): raise ValueError('Tag model must be a subclass of TagModel') # Process the deferred options self._process_deferred_options(is_to_self=self.tag_model == cls) def _process_deferred_options(self, is_to_self=False): """ Process tag options once we have the related model If the field is explicitly referring to itself, is_to_self will be True """ # See if tag options were deferred if self._deferred_options is None: return # Get deferred options set_tag_meta, options = self._deferred_options self._deferred_options = None # Set options if options: if set_tag_meta: # Tag option arguments must be used to update the tag model # (and any other tag fields which use it) - used during # migration when the tag model doesn't know its own options self.tag_model.tag_options.update(options) else: raise ValueError( 'Cannot set tag options on explicit tag model %r' % (self.tag_model, )) # Link to model options by reference in case they get updated later self.tag_options = self.tag_model.tag_options def contribute_to_class(self, cls, name): """ Create the tag model if necessary, then initialise and contribute the field to the class """ # # Get or create the tag model # # Create a new tag model if we need to if self.auto_tag_model: # Make sure a TagField is only contributed once if the model is # not explicitly set. This isn't normal for model fields, but in # this case the name of the model (and therefore db) would depend # on the load order, which could change. Rather than risk problems # later, ban it outright to save developers from themselves. # If it causes problems for anyone, they can explicitly set a tag # model and avoid this being a problem. if self.contributed: raise AttributeError( "The tag field %r is already attached to a model" % self) # Generate a list of attributes for the new tag model model_attrs = { # Module should be the same as the main model '__module__': cls.__module__, # Give it access to the options 'tag_options': self.tag_options, } # Build new tag model # Name is _Tagulous_MODELNAME_FIELDNAME model_name = "%s_%s_%s" % ( constants.MODEL_PREFIX, cls._meta.object_name, name, ) model_cls = TagModel if self.tag_options.tree: model_cls = TagTreeModel self.tag_model = type(str(model_name), (model_cls, ), model_attrs) # Give it a verbose name, for admin filters verbose_name_singular = (self.tag_options.verbose_name_singular or self.verbose_name or name) verbose_name_plural = self.tag_options.verbose_name_plural if not verbose_name_plural: verbose_name_plural = (verbose_name_singular or self.verbose_name or name) if not verbose_name_plural.endswith('s'): verbose_name_plural += 's' # Get object verbose name object_name = cls._meta.verbose_name self.tag_model._meta.verbose_name = '%s %s' % ( object_name, verbose_name_singular, ) self.tag_model._meta.verbose_name_plural = '%s %s' % ( object_name, verbose_name_plural, ) # Make no attempt to enforce max length of verbose_name - no good # automatic solution, and the limit may change, see # https://code.djangoproject.com/ticket/17763 # If it's a problem, contrib.auth will raise a ValidationError # # Build the tag field # # Update the rel on the field if django.VERSION < (1, 9): # Django 1.8 or earlier self.remote_field.to = self.tag_model else: # Django 1.9 and later self.remote_field.model = self.tag_model # Contribute to class super(BaseTagField, self).contribute_to_class(cls, name) self.contributed = True def formfield(self, form_class, **kwargs): """ Common actions for TagField and SingleTagField to set up a formfield """ required = not self.blank if hasattr(self, 'required'): required = self.required # Update tag options, if necessary tag_options = self.tag_options if 'tag_options' in kwargs: new_options = kwargs.pop('tag_options') if not isinstance(new_options, TagOptions): new_options = TagOptions(**new_options) tag_options += new_options # Start off with defaults options = { # Arguments the TagField base (CharField) would expect "label": capfirst(self.verbose_name), "help_text": self.help_text, "required": required, # Also pass tag options "tag_options": tag_options } # Update with kwargs options.update(kwargs) # Add in list of tags for autocomplete, if appropriate if 'autocomplete_tags' in kwargs: options['autocomplete_tags'] = kwargs['autocomplete_tags'] elif not tag_options.autocomplete_view: tags = self.tag_model.objects.all() if tag_options.autocomplete_initial: tags = tags.initial() options['autocomplete_tags'] = tags # Create the field instance return form_class(**options) def get_manager_name(self): """ Get the field name for the Manager """ return "_%s_tagulous" % self.name def deconstruct(self): """ Deconstruct field options to a dict for __init__ """ name, path, args, kwargs = super(BaseTagField, self).deconstruct() # Find tag model options if isinstance(self.tag_options, TagOptions): # When deconstruct is called on a real field on a real model, # we will have a concrete tag model, so use its tag options items = self.tag_options.items(with_defaults=False) # Freeze initial as a string, not array if 'initial' in items: items['initial'] = self.tag_options.initial_string elif self._deferred_options is not None: # When deconstruct is called on a ModelState field, the options # will have been deferred set_tag_meta, options = self._deferred_options items = options else: # pragma: no cover # It should never get here - raise an exception so we can debug raise ValueError('Unexpected state') # Add tag model options to kwargs kwargs.update(items) # Can't freeze lambdas. It should work if it's a function at the top # level of a module, but there's no easy way to differentiate. For # safety and consistency, strip callable arguments and mention in # documentation. if 'get_absolute_url' in kwargs: del kwargs['get_absolute_url'] # Remove forbidden fields # This shouldn't be needed, but strip them just in case for forbidden in self.forbidden_fields: # pragma: no cover if forbidden in kwargs: del kwargs[forbidden] # Always store _set_tag_meta=True, so migrating tag fields can set tag # models' TagMeta kwargs['_set_tag_meta'] = True return name, path, args, kwargs
class BaseTagField(object): """ Mixin for TagField and SingleTagField """ # List of fields which are forbidden from __init__ forbidden_fields = () def __init__(self, to=None, **kwargs): """ Initialise the tag options and store Takes all tag options as keyword arguments, as long as there is no ``to`` tag model specified. Undocumented keyword argument ``_set_tag_meta`` is used internally to update tag model options with those specified in keyword arguments when a ``to`` tag model is specified. However, this is intended for internal use only (when migrating), so if you must use it, use it with care. """ # Save tag model self.tag_model = to # Extract options from kwargs options = {} for key, default in constants.OPTION_DEFAULTS.items(): # Look in kwargs, then in tag_meta if key in kwargs: options[key] = kwargs.pop(key) # Detect if _set_tag_meta is set set_tag_meta = kwargs.pop('_set_tag_meta', False) # Detect whether we need to automatically generate a tag model if self.tag_model: self.auto_tag_model = False # Tag model might be a string - set options later self.tag_options = None self._deferred_options = [set_tag_meta, options] else: self.auto_tag_model = True self.tag_options = TagOptions(**options) self._deferred_options = None # If the tag model was not specified, we need to specify one. # However, we can't reliably auto-generate a unique and repeatable # model name for tags here in __init__ - we can only do that in # contribute_to_class once we know the name of the field in the model. # We'll therefore use the string '-'; Django will not do anything about # resolving it until contribute_to_class, at which point we'll replace # it with a reference to the real tag model. kwargs['to'] = self.tag_model if self.tag_model else '-' # Call super __init__ super(BaseTagField, self).__init__(**kwargs) # Make a note that this has not been contributed to a class yet self.contributed = False # This attribute will let us tell South to supress undesired M2M fields self.south_supression = True if django.VERSION < (1, 9): remote_field = property(lambda self: self.rel) def do_related_class(self, other, cls): """ Process tag model now it has been resolved if it was a string """ # Set up relation as normal super(BaseTagField, self).do_related_class(other, cls) # Make sure tag model is the related model, in case it was a string if django.VERSION < (1, 8): # Django 1.7 or earlier self.tag_model = self.related.parent_model else: # Django 1.8 or later self.tag_model = self.remote_field.model # Check class type of tag model if not issubclass(self.tag_model, BaseTagModel): raise ValueError('Tag model must be a subclass of TagModel') # Process the deferred options self._process_deferred_options(is_to_self=self.tag_model == cls) def _process_deferred_options(self, is_to_self=False): """ Process tag options once we have the related model If the field is explicitly referring to itself, is_to_self will be True """ # See if tag options were deferred if self._deferred_options is None: return # Get deferred options set_tag_meta, options = self._deferred_options self._deferred_options = None # Set options if options: if set_tag_meta: # Tag option arguments must be used to update the tag model # (and any other tag fields which use it) - used during # migration when the tag model doesn't know its own options self.tag_model.tag_options.update(options) else: raise ValueError( 'Cannot set tag options on explicit tag model %r' % ( self.tag_model, ) ) # Link to model options by reference in case they get updated later self.tag_options = self.tag_model.tag_options def contribute_to_class(self, cls, name): """ Create the tag model if necessary, then initialise and contribute the field to the class """ # # Get or create the tag model # # Create a new tag model if we need to if self.auto_tag_model: # Make sure a TagField is only contributed once if the model is # not explicitly set. This isn't normal for model fields, but in # this case the name of the model (and therefore db) would depend # on the load order, which could change. Rather than risk problems # later, ban it outright to save developers from themselves. # If it causes problems for anyone, they can explicitly set a tag # model and avoid this being a problem. if self.contributed: raise AttributeError( "The tag field %r is already attached to a model" % self ) # Generate a list of attributes for the new tag model model_attrs = { # Module should be the same as the main model '__module__': cls.__module__, # Give it access to the options 'tag_options': self.tag_options, } # Build new tag model # Name is _Tagulous_MODELNAME_FIELDNAME model_name = "%s_%s_%s" % ( constants.MODEL_PREFIX, cls._meta.object_name, name, ) model_cls = TagModel if self.tag_options.tree: model_cls = TagTreeModel self.tag_model = type(str(model_name), (model_cls,), model_attrs) # Give it a verbose name, for admin filters verbose_name_singular = ( self.tag_options.verbose_name_singular or self.verbose_name or name ) verbose_name_plural = self.tag_options.verbose_name_plural if not verbose_name_plural: verbose_name_plural = ( verbose_name_singular or self.verbose_name or name ) if not verbose_name_plural.endswith('s'): verbose_name_plural += 's' # Get object verbose name object_name = cls._meta.verbose_name self.tag_model._meta.verbose_name = '%s %s' % ( object_name, verbose_name_singular, ) self.tag_model._meta.verbose_name_plural = '%s %s' % ( object_name, verbose_name_plural, ) # Make no attempt to enforce max length of verbose_name - no good # automatic solution, and the limit may change, see # https://code.djangoproject.com/ticket/17763 # If it's a problem, contrib.auth will raise a ValidationError # # Build the tag field # # Update the rel on the field if django.VERSION < (1, 9): # Django 1.8 or earlier self.remote_field.to = self.tag_model else: # Django 1.9 and later self.remote_field.model = self.tag_model # Contribute to class super(BaseTagField, self).contribute_to_class(cls, name) self.contributed = True def formfield(self, form_class, **kwargs): """ Common actions for TagField and SingleTagField to set up a formfield """ required = not self.blank if hasattr(self, 'required'): required = self.required # Update tag options, if necessary tag_options = self.tag_options if 'tag_options' in kwargs: new_options = kwargs.pop('tag_options') if not isinstance(new_options, TagOptions): new_options = TagOptions(**new_options) tag_options += new_options # Start off with defaults options = { # Arguments the TagField base (CharField) would expect "label": capfirst(self.verbose_name), "help_text": self.help_text, "required": required, # Also pass tag options "tag_options": tag_options } # Update with kwargs options.update(kwargs) # Add in list of tags for autocomplete, if appropriate if 'autocomplete_tags' in kwargs: options['autocomplete_tags'] = kwargs['autocomplete_tags'] elif not tag_options.autocomplete_view: tags = self.tag_model.objects.all() if tag_options.autocomplete_initial: tags = tags.initial() options['autocomplete_tags'] = tags # Create the field instance return form_class(**options) def get_manager_name(self): """ Get the field name for the Manager """ return "_%s_tagulous" % self.name def deconstruct(self): """ Deconstruct field options to a dict for __init__ """ name, path, args, kwargs = super(BaseTagField, self).deconstruct() # Find tag model options if isinstance(self.tag_options, TagOptions): # When deconstruct is called on a real field on a real model, # we will have a concrete tag model, so use its tag options items = self.tag_options.items(with_defaults=False) # Freeze initial as a string, not array if 'initial' in items: items['initial'] = self.tag_options.initial_string elif self._deferred_options is not None: # When deconstruct is called on a ModelState field, the options # will have been deferred set_tag_meta, options = self._deferred_options items = options else: # pragma: no cover # It should never get here - raise an exception so we can debug raise ValueError('Unexpected state') # Add tag model options to kwargs kwargs.update(items) # Can't freeze lambdas. It should work if it's a function at the top # level of a module, but there's no easy way to differentiate. For # safety and consistency, strip callable arguments and mention in # documentation. if 'get_absolute_url' in kwargs: del kwargs['get_absolute_url'] # Remove forbidden fields # This shouldn't be needed, but strip them just in case for forbidden in self.forbidden_fields: # pragma: no cover if forbidden in kwargs: del kwargs[forbidden] # Always store _set_tag_meta=True, so migrating tag fields can set tag # models' TagMeta kwargs['_set_tag_meta'] = True return name, path, args, kwargs