Not sure if this is a bug in Django, or it just doesn't support what I'm trying to do (or how i'm doing it).
A snippet of my model:
class UserProfile(models.Model):
user = models.OneToOneField(User, primary_key=True, related_name='profile'
login_hash = models.CharField(max_length=36, blank=True, null=True, default=uuid.uuid4())
...
As you see, i've set the default for login_hash to a call to uuid.uuid4()
works fine... however, multiple calls to the UserProfile (creating new users quickly, even seemingly a few minutes, but i've not an official time) will result in the same login_hash for multiple users.
It appears that django (i'm on 1.7.4) is caching the result of uuid4() for some period of time. not good for what i'm trying to do.
SOLUTION:
that i'm using. I've simply set an 'on insert' trigger on the database, so that when i insert a new record, the database generates the UUID, but only on inserts/new records.
Is there a way to do it within django so that i can keep it database agnostic?
works fine... however, multiple calls to the UserProfile (creating new users quickly, even seemingly a few minutes, but i've not an official time) will result in the same login_hash for multiple users.
As the code is currently written you're calling uuid.uuid4() at the point UserProfile is imported. It'll be called once and the resulting value will be the default for all new creations.
What you instead what to do is pass a callable as the default. Like so: default=uuid.uuid4.
Also, for CharField I'd strongly suggest not allowing NULL values as well as blank values. It's also not clear if you really do want to allow blank values for this field, but let's assume that you do. You should end up with this:
login_hash = models.CharField(max_length=36, blank=True, default=uuid.uuid4)
Related
I have a strange situation where Django seems to be giving me records that do not actually exist in the database when queried via a related_name on a foreign key. Here's a simplified example:
Let's say I have a Person model and a Pet model, where each Pet has an owner, which is a foreign key on Person:
class Pet(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name =models.CharField(max_length=50, null=False, db_index=True)
owner = models.ForeignKey("Person", null=False, related_name="pets", on_delete=models.CASCADE)
relationship = models.IntegerField(null=False, choices=PetRelationship.choices(), db_index=True)
Now, I have the below function that retrieves a person's pets:
def pet_tester(person):
for pet in person.pets.filter(relationship=PetRelationship.FRIENDLY):
pet_id = pet.id
LOGGER.info(f"*************** pet.id = {pet_id}")
LOGGER.info(f"******* Pet exists? = {Pet.objects.filter(id=pet_id).exists()}")
...
Note that this is NOT a "minimal reproducible example". I can only reproduce it in my much larger application.
For some reason (in the real app), the "exists" query is coming back as False. The output is like this:
*************** pet.id = 123e4567-e89b-12d3-a456-426614174000
******* Pet exists? = False
If I query the actual database (Postgresql) directly (outside of Django), that pet ID sure enough does NOT exist. However, the person.pets.filter query is returning it just the same.
I do not understand how this is even possible. It JUST retrieved the pet from the database (or so it would seem - it even has a UUID), but it's not really there after all when I immediately try to query it back.
It seems like the Django "filter" results are somehow inconsistent with the underlying database, but I don't know why. This is very reproducible in my app (it happens every time, consistently). It doesn't appear to be a corrupt database, as it happens consistently even on brand new machines, with the app and database freshly installed.
I know you don't know what else my app is doing. I obviously can't post all the source code, but I can assure you that the beginning of the actual function is pretty much verbatim of the pet_tester function above. I only changed the class/field names. There are no extra lines in the actual app between the "for" line and the 3 lines below it.
I'm mostly looking for ideas about how this can possibly happen, and what to look for in the app's other code.
It turns out that there is another service that is deleting the "pets" from the database. There must be some kind of caching in Django, and since the deletion happened outside of Django, Django didn't invalidate its "person.pets" cache.
In my Django app, I want to allow users to see which profiles they view and which profiles view them. In my Profile model I have created 2 fields that accomplish this.
viewed = models.ManyToManyField('self', null=True, blank=True, related_name='viewed_profiles', symmetrical=False)
visitors = models.ManyToManyField('self', null=True, blank=True, related_name='visitors_profiles', symmetrical=False)
I also have the code set up in my views.py file to add profiles to these fields as necessary. However, I would like to only track and display the most recent 25 or so viewed and visitor profiles. Is there a way to query these fields ordered by date added and delete everything past the first 25 results? Is this possible without creating another field to track the order of the profiles viewed?
Take a look at the documentation on Querysets for details of how to do this. You can use order_by to order your objects by date, and use Python's array slicing syntax to limit the number of results.
An example of showing the most recently added items in your view might look something like this:
viewed = Profile.objects.order_by("-date_added")[:25]
This doesn't delete everything after 25 - it just fetches the 25 most recent objects (assuming your Profile model has a field called date_added).
EDIT: Oops, I think I misread your question.
I think what you'd need to do is have an intermediate model - Django allows you to use a third model as an intermediate one between two different models in a many-to-many relationship. Then you could add the time viewed to that model and store it that way. There's a good example in the documentation.
I wouldn't really bother deleting the old ones unless database space was likely to be an issue, but if you need to for any reason, I guess you could set up a signal that was triggered by a new view being created and have that call a function that deletes all but the 25 most recent.
Django doesn't track the date added for a ManyToMany relationship, so it's not possible to do this reliably without adding a field. To achieve this you'll need to do is add a date field on your ManyToMany intermediary table, then order by that - for example
class ProfileViewed(models.Model):
viewed = models.ForeignKey('Profile')
viewer = models.ForeignKey('Profile')
date_added = models.DateField(auto_now_add=True)
class Profile(models.Model):
...
viewed = models.ManyToManyField('self', null=True, blank=True, related_name='viewed_profiles', symmetrical=False, through=ProfileViewed)
Then you can order your results like so:
profile = Profile.objects.get(...)
views = ProfileViewed.objects.filter(viewed=profile).order_by('date_added')
Through the on_delete option, Django provides various alternatives for what to do with objects that have a foreign key to an object that is being deleted.
I'm wondering if there is a way I could do something similar, but conditionally. Here's the scenario. I am utilizing Django 1.5's new custom User model and all my users have a ForeignKey to Site. Like so:
class TenantSiteUser(AbstractUser):
site = models.ForeignKey(Site, null=True)
If a site is deleted, then I'd prefer to delete all the non-superusers linked to that site (i.e., KASKADE-like behavoir), since their existence is now meaningless. But if its a superuser, I'd prefer to just set the user's site to null (i.e., SET_NULL) and let them keep existing, since that's probably me or someone I work with and we tend to not want to unintentionally delete ourselves.
Is there something I can override to manually do a check and implement this type of on_delete behavior?
EDIT: Here's the code that ended up working for me, based on #Kevin's answer and some study of how the existing handlers work:
def NULLIFY_SUPERUSERS_ELSE_CASCADE(collector, field, sub_objs, using):
superusers = []
for user in sub_objs:
if user.is_superuser:
sub_objs = list(sub_objs)
sub_objs.remove(user)
superusers.append(user)
CASCADE(collector, field, sub_objs, using)
if len(superusers):
collector.add_field_update(field, None, superusers)
class TenantSiteUser(AbstractUser):
site = models.ForeignKey(Site, null=True, on_delete=NULLIFY_SUPERUSERS_ELSE_CASCADE)
The options Django provides (CASCADE, PROTECT etc.) are all functions - here's where they're defined for 1.5.
I haven't tested it, but it should be possible to write your own NULL_OR_CASCADE function and pass that in as your field's on_delete argument.
I'm not entirely sure how this engine works, but let me set up a scenario. I have a Django-Mongo project with say a model class like:
class BaseModel(models.Model):
created_at = models.DateTimeField(null=False, auto_now_add=True)
modified_at = models.DateTimeField(null=False, auto_now=True)
I create a bunch of base models and everything is all lovely in the project and I get lots of data. Then, later the project grows and we need to expand BaseModel. It suddenly becomes:
class BaseModel(models.Model):
created_at = models.DateTimeField(null=False, auto_now_add=True)
modified_at = models.DateTimeField(null=False, auto_now=True)
active = models.BooleanField(default=True)
So I get that mongo will allow me to change this without having to touch the database, but say the very next thing I do is do a query like:
BaseModel.objects.filter(active=True)
Should I expect old records that needed to use the default but have not been access yet to show up in this query? Or do I need to do anything specific for the query engine to realize that the value I'm looking for matches the default?
Ok, found the answer out after some trial and error. When Django executes the query, it looks against what Mongo has in the database and ignores any default settings. However, when you already get an object, Django puts the default into the object in memory (but not in the Mongo record).
This seems kind of gross. What it means is, before we can query upon that refactored field we need to do something like this once:
for mod in BaseModel.objects.all():
mod.save()
Needless to say, this is a really really expensive query potentially. Thankfully it only has to be run once, but still.
If there are better alternatives that'd be great.
So I've got a UserProfile in Django that has certain fields that are required by the entire project - birthday, residence, etc. - and it also contains a lot of information that doesn't actually have any importance as far as logic goes - hometown, about me, etc. I'm trying to make my project a bit more flexible and applicable to more situations than my own, and I'd like to make it so that administrators of a project instance can add any fields they like to a UserProfile without having to directly modify the model. That is, I'd like an administrator of a new instance to be able to create new attributes of a user on the fly based on their specific needs. Due to the nature of the ORM, is this possible?
Well a simple solution is to create a new model called UserAttribute that has a key and a value, and link it to the UserProfile. Then you can use it as an inline in the django-admin. This would allow you to add as many new attributes to a UserProfile as you like, all through the admin:
models.py
class UserAttribute(models.Model):
key = models.CharField(max_length=100, help_text="i.e. Age, Name etc")
value = models.TextField(max_length=1000)
profile = models.ForeignKey(UserProfile)
admin.py
class UserAttributeInline(admin.StackedInline):
model = UserAttribute
class UserProfile(admin.ModelAdmin):
inlines = [UserAttibuteInline,]
This would allow an administrator to add a long list of attributes. The limitations are that you cant's do any validation on the input(outside of making sure that it's valid text), you are also limited to attributes that can be described in plain english (i.e. you won't be able to perform much login on them) and you won't really be able to compare attributes between UserProfiles (without a lot of Database hits anyway)
You can store additional data in serialized state. This can save you some DB hits and simplify your database structure a bit. May be the best option if you plan to use the data just for display purposes.
Example implementation (not tested)::
import yaml
from django.db import models
class UserProfile(models.Model):
user = models.OneToOneField('auth.User', related_name='profile')
_additional_info = models.TextField(default="", blank=True)
#property
def additional_info(self):
return yaml.load(self._additional_info)
#additional_info.setter
def additional_info(self, user_info_dict):
self._additional_info = yaml.dump(user_info_dict)
When you assign to profile.additional_info, say, a dictionary, it gets serialized and stored in _additional_info instead (don't forget to save the instance later). And then, when you access additional_info, you get that python dictionary.
I guess, you can also write a custom field to deal with this.
UPDATE (based on your comment):
So it appears that the actual problem here is how to automatically create and validate forms for user profiles. (It remains regardless on whether you go with serialized options or complex data structure.)
And since you can create dynamic forms without much trouble[1], then the main question is how to validate them.
Thinking about it... Administrator will have to specify validators (or field type) for each custom field anyway, right? So you'll need some kind of a configuration option—say,
CUSTOM_PROFILE_FIELDS = (
{
'name': 'user_ip',
'validators': ['django.core.validators.validate_ipv4_address'],
},
)
And then, when you're initializing the form, you define fields with their validators according to this setting.
[1] See also this post by Jacob Kaplan-Moss on dynamic form generation. It doesn't deal with validation, though.