On 03/04/2012, at 8:35 AM, Jacob Kaplan-Moss wrote:

> Hi folks --
> 
> I've written up a proposal for how *I* would like to address refactoring 
> auth.user: https://gist.github.com/2245327.
> 
> In essence, this does two things:
> 
> * Vastly "prunes" the required fields on auth.user. The only things left are 
> an "identifier" (which could be username, email, url, uuid, whatever), and a 
> password.
> * Introduces a new "profile" system that provides a way to contribute extra 
> related fields. Multiple profiles are supported, along with some syntactic 
> sugar for dealing with multiple profiles in a reasonably reusable way.
> 
> And that's about it. I'm deliberately trying to find a middle ground between 
> "do the minimum to allow people to move on" and "throw out and rewrite 
> django.contrib.auth entirely". I'm not expecting everyone to be thrilled by 
> this idea, but I'm hoping that this is "Good Enough" for almost everyone.
> 
> For more please see the document. Please do try to read the whole thing: I've 
> had a few rounds of feedback incorporated already, and there's even an FAQ at 
> the end.

I've added a summary of this option to the wiki:

https://code.djangoproject.com/wiki/ContribAuthImprovements#Solution5:Profile-basedsingleusermodel

As always, feel free to correct/update as necessary.

>From my reading of the proposal, here are some questions/edge cases. For some 
>of these questions, I fully expect the answer may be "You just can't do that"; 
>however, given that they're plausible edge cases, it's worth being explicit 
>about what we're aiming at.

 * Auto-profile creation:

What if my profile is:

class MyProfile(Profile):
   first_name = CharField(max_length=100)
   last_name = CharField(max_length=100)

i.e., first_name and last_name are both required fields. How does the profile 
get automatically instantiated in this case? Doesn't the auto-instantiation of 
profiles essentially mean that there can be no required fields on a profile 
(or, at least, on an auto-instantiated profile)?

 * Regarding AUTH_PROFILE and collisions:

What if I have 2+ profiles, but no clear order of precedence? e.g., 

class MyProfile1(Profile):
    name = CharField()
    email = EmailField()
    color = CharField()

class MyProfile2(Profile):
    name = CharField(unique=True)
    email = EmailField()
    age = IntegerField()

Lets say that for some internal logic reason for profile processing, I need 
both email and age on MyProfile2, but name from MyProfile1. Under these 
circumstances, there's no way I can specify a single AUTH_PROFILE ordering that 
will satisfy my requirements. Is this a case where I have to resort to explicit 
naming?

Or, alternatively, should we be treating AUTH_PROFILE as a mechanism for purely 
specifying the resolution to specific lookup problems? i.e., instead of just 
specifying an order, we specify which model we want ambiguous fields to come 
from:

AUTH_PROFILE_LOOKUP = {
    'name': 'myapp1.MyProfile1',
    'email': 'myapp1.MyProfile2',
    'color': 'myapp1.MyProfile2',
}

That way, if a field is unambiguous, it's returned from whatever model provides 
it; if a field is ambiguous, we return the one specified; and if the ambiguity 
isn't resolved, we return a KeyError (or catch this case as a validation error 
on startup).

 * Required fields and AUTH_PROFILE

Combining the previous two points; what if MyProfile2 has a required field, but 
it isn't a field selected by data[]? e.g, in the previous example, 
MyProfile2.name is a required unique field; but if it isn't instantiated with 
useful data, the profile instances can't exist.

== Commentary == 

For me, the previous three edge cases essentially point to the fact that there 
can ultimately only be 1 profile for "core" user information. There will almost 
certainly be uses for multiple profiles (e.g., to store OpenID credentials) -- 
but then, this is already the case (I've got more than one project in the wild 
with multiple "UserProfile" objects, without using AUTH_USER_PROFILE). However, 
as soon as there are overlaps between profiles, you're going to end up with 
partially populated profiles, and eventually someone will write a 'unified' 
profile model for each project that doesn't have the overlap ...

... at which point, we've essentially arrived at a swappable User model, just 
with a required join to get profile data, and a bunch of magic attributes and 
ORM properties to hide the fact that the join exists.

I understand Django's history has examples where swappable models caused 
problems that we don't want to revisit. However, as I understand it, these 
problems were largely caused by the fact that it was *easy* to accidentally 
swap out a model, and that change wouldn't be effectively communicated to other 
developers on the same codebase until the magic smoke escaped.

It's also worth pointing out that the problems with swappable models aren't 
really avoided by the profile-based approach. If I've got a project with N user 
profile models, grabbing user.data['name'] from one of them, another developer 
can very easily modify the order of AUTH_PROFILES, or modify the apps in 
INSTALLED_APPS to add to or change the user profiles that are currently in 
effect, or alter the precedence in AUTH_PROFILES to override the attribute we 
need. If the objection to swappable user models is that it's easy to swap out 
models, then it's just as easy to change the expectations for User models via a 
profile -- possibly easier, because you aren't modifying a single 
"AUTH_USER_MODEL" setting; you're changing one of several settings whose 
consequences *may* include changing the operation of the User model. 
AUTH_USER_MODEL at least has the consequences written on the box.

(User models are hard. Lets go shopping. :-)

One possible way to avoid this problem would be to make it harder to make these 
sorts of changes -- i.e., add project/install-level tracking for certain key 
settings. So, when you do a syncdb, we set a key-value pair in the database for 
the current value of AUTH_USER_MODEL (or whatever mechanism needs to be 
persisted). Django checks this value against the current 
settings.AUTH_USER_MODEL value on startup; if the setting value doesn't match 
the installation value, we can throw an error on startup.

Would this approach temper your objection to swappable models? 

At the end of the day, I'm happy to go with a BDFL judgement on this. However, 
it's interesting to note that almost every time this problem arises, a 
"swappable" model of some sort is usually the initially proposed solution. 
That's because it's an obvious solution -- an obvious solution with problems, 
but an obvious solution, nonetheless. The alternative being proposed here isn't 
*clearly* better -- it's really just a matter of making a strategic choice to 
pick a different set of pain points. 

To make matters works, it seems to me that it will require a lot more work to 
execute -- far from being "minimalist", it's invasive on auth code, ORM code, 
and (in the long term) the internals of every app that uses User. 

For example, any code written against the fields on the existing auth.User will 
need to be updated to use the new data[] access mechanism. Yes, this will be 
introduced gradually over releases, but it doesn't change the fact that every 
app that currently accesses user.is_staff will need to update to 
user.data['is_staff'] at some point in the next few releases. By comparison, 
the swappable user approach generally means no changes to app code (or just 
modifying ForeignKey(User) to LazyForeignKey(User), or 
ForeignKey(settings.USER_MODEL)), and documenting that is_staff is a required 
attribute on User for using the app (a property that can be validated as a 
project integration requirement by a test suite).

There are also some aspects about this proposal that make my teeth itch -- 
filter(data__...), for example, strikes me as code that will be hard to write, 
hard to debug, and a PITA to maintain. And if you can filter(data__...), then 
you need to be able to order_by('data__...') as well. But all this complexity 
is required if you want to maintain decoupling of profiles. It also requires 
adding a feature to the ORM to support a specific use case in User, rather than 
a generic problem (although I suppose we could implement it in such a way as to 
make "profiles" an option for any other model that needs them).

If we're going to make a choice to avoid the "obvious" solution, I think we 
need to be very clear (both now, and for posterity) why we made the choices we 
have made, rather than trying to address or ameliorate the problems with the 
"obvious" solution.

Yours
Russ Magee %-)

-- 
You received this message because you are subscribed to the Google Groups 
"Django developers" group.
To post to this group, send email to django-developers@googlegroups.com.
To unsubscribe from this group, send email to 
django-developers+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/django-developers?hl=en.

Reply via email to