Alright! I've gone through the documentation, and waded through a lot of 
the Django source code that implements the AUTH_USER_MODEL setting (via 
Meta.swappable, and Model._meta.swapped, etc) and while I am not saying 
that this isn't a good idea, it seems very heavyweight for what I was 
trying to do. In particular, there are a lot of places in the contrib.admin 
code that checks for model._meta.swapped and disables functionality if it 
indeed is swapped out, even outside the case of the User model.

Ie, I fear that if I use this paradigm, I'll continually be discovering 
circumstances where this or that neato feature in the admin screens stops 
working for me.

What Meta.swappable seems to promote is the idea of being able to replace a 
model with another, while still being able to access that model in all the 
same locations it would have been visible in before being swapped, AND 
ensuring that whenever any application requests a auth.User object, they 
actually get an instance of the replaced object (with some limitations). 

That's totally awesome, but far more than what I needed, which was a way to 
allow my app (artshow) to have a reference to a settings-configurable 
Person model with no changes to either the artshow app, or the app that 
supplies the Model. Ie, I'm looking for a configurable "has-a" 
relationship, than a configurable "is-a" relationship. "has-a" seems a 
*lot*simpler!

So, I'd like to present to you what I've done with my own code. I've gone 
through several iterations and have boiled it down to what I think is the 
simplest possible representation. Feedback and comments are gratefully 
received, and if you think it's useful, I'll provide a HOWTO incorporating 
all the (useful) feedback.

Requirements for a solution:

   - Zero changes are allowed to the component/application that is 
   providing the model (the target)
   - All references to the configurable model in the source app are foreign 
   keys (models.ForeignKey, models.OneToOneField, etc)

Implementation:

   - The documentation for the source app provides a list of "attribute, 
   field and method" requirements for the target. If the target cannot provide 
   these, a "Proxy Model" needs to be created.
   - If the source app needs to run any search queries on the target, a 
   proxy is certainly required to provide methods to return tuples of strings 
   (for "search_fields" in admin models) and a Q object (for searching via 
   querysets) 
   - A setting is provided to indicate which target model is providing the 
   functionality. This points to either the TargetModel or the ProxyModel if 
   required.
   - The model module for the source app creates an alias for the 
   replaceable model using: ReplaceableModel = models.loading.get_model ( 
   *settings.SETTING_NAME.split('.',1) )
      - This works better than using settings.SETTING_NAME everywhere, as 
      it allows views, etc, to use "models.ReplaceableModel.objects..." etc.
   - The ProxyModel is created specifically to match the requirements of 
   the source app, and the features of the target app. This is set up as a 
   Meta.proxy = True
      - If the source app needs to display some kind of attribute in 
      templates, and it is not provided by the Target Model, a method (or 
      property) is created in the proxy.
      - If the source app needs to create different kinds of searches in 
      admin screens, since search_fields needs real fields to search on, a 
method 
      on the proxy is created. returning a tuple of strings: e.g.: 
@staticmethod 
      def get_blah_search_fields ( prefix ): return ( prefix+"attr1", 
      prefix+"attr2" )
      - If the source app needs to create queryset style fields, a similar 
      method is provided returning a Q object. e.g.: @staticmethod def 
      get_blah_search_q ( search_str, prefix ): return 
      Q(**{prefix+"attr1__icontains":search_str} | 
      Q(**{prefix+"attr2__icontains":search_str})
      - __unicode__ can be overridden if a customized default 
      representation is required.
   
In addition, the source app probably needs a good way of finding and 
attaching instances of the Replaceable Model. With no change, the above 
will supply a selection box or a pop-up window (with raw_id_fields), but 
I've found using django-ajax-selects to be excellent. If the TargetModel 
supplies a LookupChannel for those objects, you'll need to create a new 
LookupChannel with a 'model' attribute of "ReplaceableModel" instead. 
Usually simply inheriting from the existing LookupChannel and overriding 
the model attribute works. Otherwise the LookupChannel will return the 
wrong kinds of objects and you won't be able to do assignments, etc.

Now, the above isn't really documentation, I've got a branch with all the 
above changes checked into github that people can poke at:

https://github.com/chmarr/artshow-jockey/tree/peeps2

The parts relevant to this discussion are:

   - ARTSHOW_PERSON_CLASS in artshowproject.common_settings.py
   - AJAX_LOOKUP_CHANNELS in the same, and in particular the 
   "artshow_person" channel, which points to artshow_shims.lookups
   - A simple contact manager application called "peeps". This is suppled 
   as the "default" implementation for Person. I've coded this in a way that 
   peeps knows *nothing* about any other application.
   - The entire artshow_shim component/app, which provides:
      - models.Person, which inherits from peeps.models.Person, a 
      customised __unicode__ method, and two search methods
      - lookups.PersonLookup, which inherits from 
      peeps.lookups.PersonLookup, and simply overrides the "model" attribute
   - artshow.models.Person is implemented as:
      - Person = models.loading.get_model ( *settings.ARTSHOW_PERSON_CLASS.
      split('.',1) )
      - Inside views, etc, for artshow, if I ever need to to a search on a 
   model that includes fields from the "person" it has a foreign key with, 
   instead of doing search_fields = ( "attr1", "attr2", "person__name", 
   "person__email" ) I do search_fields = ( "attr1", "attr2" ) + 
   models.Person.get_search_fields ( "person__" )
   - Similarly for filters, etc: Artist.objects.filter ( 
   Q(artistname__icontains=txt) | models.Person.get_search_q ( txt, "person__" 
   ) )
   - You can see some of the above two cases in the artshow/admin.py file


Advantages:

   - Zero changes to the target implementation
   - The "source app" doesn't need to be modified if the implementation 
   details in the target implementation change. Simply, the proxy model is 
   updated to provide the necessary functionality. 

Disadvantages:

   - This makes some queries (eg: the search_fields, and Q filters) a touch 
   more complex. Ie, if you're making a query that makes assumptions of the 
   internals of the replaceable module, make sure that they're either simple 
   enough to be handled with a property that emulates the behavior, which 
   generally works if you're only seeking *one* attribute, or use methods 
   that return customisable tuples for your purposes.
   

I'd love to hear your comments! I apologise if people were eagerly awaiting 
documentation on how to make swappable models useful for their own 
projects! :)


-- 
You received this message because you are subscribed to the Google Groups 
"Django users" group.
To view this discussion on the web visit 
https://groups.google.com/d/msg/django-users/-/2bwVo5S2ciUJ.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/django-users?hl=en.

Reply via email to