django admin add data with fixed value in some field - django

class Facilites(models.Model):
id = models.CharField(max_length=32, primary_key=True)
name = models.CharField(max_length=128)
class Objects(models.Model):
name = models.CharField(max_length=64)
facilityid = models.ForeignKey(Facilities)
class Admins(models.Model):
user = models.OneToOneField(User)
facilities = models.ManyToManyField(Facilities)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Admins.objects.create(user=instance)
post_save.connect(create_user_profile, sender=User)
What i want is to have users (admins) only be able to add or modify "facilityid" in Objects to values specified in their Admins.facilities.
So if some user is named UserA and has facilities = ('FacA', 'FacB'), when he is adding a new object to DB, he shoudln't be able to add something like Object('Random object', 'FacC')
Also, he shouldn't be able to modify existing objects to facilities he doesn't belong to.
I have filtered the Objects with:
def queryset(self, request):
qs = super(ObjectsAdmin, self).queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(facitityid__id__in = request.user.get_profile().facilities.all())
so users can only see the object that belong to their facilities. But i have no idea how to prevent them from adding/editing object out of their facilities.
edit:
found the answer here: https://stackoverflow.com/a/3048563/1421572
It turns out that ModelAdmin.formfield_for_foreignkey was the right answer in this situation: http://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey

I would do this with either a pre-made facility list (i.e. You could create an integer field that is hooked to FACILITY_CHOICES for the user to select from.)
If only admins can do it then permissions sounds quite viable. You can also do form validation to check for errors against the db. Depending on how many facilities you have you may want a different approach.
You can do this same technique with a models.CharField as well. So perhaps assign a 3 letter facility code to each facility and require the entry to match one of the 3 letter strings. You could even have the list in a .txt file to read from. There are really so many ways to do this. I will provide an example of a pre-made facility list and accessing the facility a particular user belongs to from the api / template:
NYC_FACILITY = 0
LA_FACILITY = 1
ATL_FACILITY = 2
FACILITY_CHOICES = (
(NYC_FACILITY, 'NYC'),
(LA_FACILITY, 'LA'),
(ATL_FACILITY, 'ATL'),
class Facility(models.Model):
name = models.IntegerField(choices=FACILITY_CHOICES, default="NYC")
class Meta:
order_by = ['name']
verbose_name_plural = "facilities"
verbose_name = "facility"
def __unicode__(self):
return self.name
As far as viewing the facilities page that a particular user belongs to you will have a m2m one to one or FK relationship between the objects. If FK or m2m relationship then you will have access to additional methods of that model type. get_related However, I'm not going to use get_related in my example. Once you are in an instance you then have access to entry_set.
# models.py
from django.auth import User
class Person(User):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
facility_loc = models.ForeignKey('Facility') # ForeignKey used assuming only one person can belong to a facility.
slug = models.SlugField(unique=True)
def get_absolute_url(self):
return "/%s/%s/" % self.facility_loc % self.slug
# views.py - TemplateView is automatically given a context variable called params which parses data from the URL. So, I'll leave the regex in the URLConf up to you.
class UserFacilityView(TemplateView):
model = Facility
template_name = "user_facility.html"
Now in your template you should be able to access facility_set from a User instance or user_set from a facility instance.

Related

Django models - how to track a field on an object when value of the field depends on the different users?

I want a Book object with the field is_read, but the value of the is_read depends on the user. When I first created this app I was only thinking of one user (me).
class Book(models.Model):
title = models.CharField(max_length=50)
author = models.CharField(max_length=50)
is_read = models.BooleanField(default=False)
But if the app has multiple users then of course the is_read needs to change according to the user.
new models
class Book(models.Model):
title = models.CharField(max_length=50)
author = models.CharField(max_length=50)
class IsRead(models.Model):
user = models.ForeignKey(User, on_delete=CASCADE)
book = models.ForeignKey(Book, on_delete=CASCADE)
is_read = models.BooleanField(default=False)
I think adding the IsRead class will help but I need an IsRead object to automatically be created every time a new user or book is created. Not only that, but every time a new user is created, I have to iterate through all the books. Or if a new book is added, I have to iterate through all the users. This seems like a lot of db work just to keep track of who has read what books.
Even if the above is the correct strategy I don't know how to I would do it. I did try to overwrite AdminModel to save the IsRead but this did not work. I did not get any errors, but the IsRead did not save.
class BookAdmin(admin.ModelAdmin):
list_display = ('title', 'author')
def save_model(self, request, obj, form, change):
obj.user = request.user
IsRead.objects.update_or_create(book=id, user=request.user, defaults={'book': self.book, 'user': obj.user, 'my_status': False})
super(BookAdmin, self).save_model(request, obj, form, change
Your proposed solution of using an IsRead class to track progress will be very expensive for your db not to mention there will be a lot of unnecessary rows saved. The creation for these models can be synced, however, using signals.
You can do it something like this:
#receiver(post_save, sender=Book)
def on_book_create(sender, instance, created, *args, **kwargs):
if not created or kwargs.get("raw"):
return
# fetch all users
# loop through users and create an IsRead object
and do a counterpart signal for on_create_user too where you fetch all books and create an IsRead object linked to that user.
A better way is to leverage the power of Many-to-Many relationship in django. Start by adding a field for this relationship
class Book(models.Model):
...
readers = models.ManyToManyField(User)
You can then add readers to a book instance like this:
# 1, 2, 3 are user pks
book.readers.add(1, 2, 3)
# or a user instance
book.readers.add(user)
After that, we can add custom functions to the User and Book models to give us if a book was read by a user or if a user has read a certain book.
class User(models.Model):
...
def has_read(self, book_id):
return self.book_set.filter(pk=book_id).exists()
class Book(models.Model):
...
def is_read(self, user_id):
return self.readers.filter(pk=user_id).exists()
To check, we can do it like so:
user.has_read(book.pk)
# or like this
book.is_read(user.pk)
And as a bonus, you can get the books read by the user user.book_set.all() as well as all readers of that book book.readers.all()
Lastly, for this:
I was thinking more of is_read is the status of the book being read. A
m2m relationship between Book and User would be more like the user
owns the book.
If you want to represent a user's ownership to a certain book you can add a different many-to-many field like owners or something. Just like in real life, your relationship to a book is not limited to your ownership with it.
Adding another many-to-many relationship using the same model requires the related_name parameter to be set though. This is to avoid confusing django which relationship you want to get.
class Book(models.Model):
...
readers = models.ManyToManyField(User, related_name="read_books")
owners = models.ManyToManyField(User, related_name="owned_books")
You can have query book->user relationship using the field name book.readers.all() or book.owners.all() but for the reverse, you need to reference the related name for each: user.read_books.all() or user.owned_books.all().
This is not really far from your original IsRead class as django creates a pivot table for book and user models behind the scenes which looks something like this:
And instead of having a dedicated is_read field to check for the read status, the existence of the record implies it that's why we use .exists()
Putting them all together to something like:
class User(models.Model):
name = models.CharField(max_length=50)
def has_read(self, book_id):
return self.read_books.filter(pk=book_id).exists()
def owns_book(self, book_id):
return self.owned_books.filter(pk=book_id).exists()
class Book(models.Model):
title = models.CharField(max_length=50)
author = models.CharField(max_length=50)
readers = models.ManyToManyField(User, related_name="read_books")
owners = models.ManyToManyField(User, related_name="owned_books")
def is_read(self, user_id):
return self.readers.filter(pk=user_id).exists()
def is_owned(self, user.id):
return self.owners.filter(pk=user.id).exists()

How to I automatically filter out is_deleted records in an associated table in Django?

I am using soft deletes on one of my models in Django, and I am overwriting the default manager to always return active records only, using something like:
class ActiveRecordManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_deleted=False)
class Tag(models.Model):
is_deleted = models.BooleanField(default=False, db_index=True)
objects = ActiveRecordManager()
class Photo(models.Model):
tag = models.ForeignKey(Tag, on_delete=models.CASCADE, related_name="photos")
objects = ActiveRecordManager()
All works well. However, when I do:
tag = Tag.objects.get(pk=100)
And then I try to get the associated photos:
photos = tag.photos.all()
Then I get photos that are deleted. I only want to return objects that are not deleted (so my regular objects list. I was reading about _base_mangers in Django, which seems to control this, but the documentation recommends against filtering objects out:
If you override the get_queryset() method and filter out any rows,
Django will return incorrect results. Don’t do that. A manager that
filters results in get_queryset() is not appropriate for use as a base
manager.
But what I am not clear about is how I am supposed to filter these results. Any thoughts?
UPDATE:
I was asked to explain how this question is different from this one:
How to use custom manager with related objects?
In this 8 year old question they mention a deprecated method. That deprecated method is superseded by the method I outline below (base_managers) which according to the documentation I should not use. If people think I should use it, can you please elaborate?
why not use custom query methods instead of overriding manager as it may produce problems for example in admin pages?
class ActiveModelQuerySet(models.QuerySet):
def not_active(self, *args, **kwargs):
return self.filter(is_deleted=True, *args, **kwargs)
def active(self, *args, **kwargs):
return self.filter(is_deleted=False, *args, **kwargs)
class Tag(models.Model):
is_deleted = models.BooleanField(default=False, db_index=True)
objects = ActiveModelQuerySet().as_manager()
class Photo(models.Model):
tag = models.ForeignKey(Tag, on_delete=models.CASCADE, related_name="photos")
is_deleted = models.BooleanField(default=False, db_index=True)
objects = ActiveModelQuerySet().as_manager()
you can then filter your models however you want
tag = Tag.objects.active(pk=100)
deleted_tags = Tag.objects.not_active()
photos = tag.photos.active()
also note that you need is_deleted attribute in all your models that have the soft delete functionality like Photo in your case

How to remove the instance of ManyToMany field?

I have two models:
class Organisation(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL,related_name='organisation_user',on_delete=models.CASCADE)
name = models.CharField(max_length=100,blank=True)
location = models.CharField(max_length=100,blank=True)
qualification_status = (
('Pending for verification','Pending for verification'),
('Verified','Verified'),
)
qualification = models.CharField(max_length=100,choices=qualification_status,default='Pending for verification',blank=True)
members = models.ManyToManyField(settings.AUTH_USER_MODEL,related_name='organisation_members',blank=True)
class Organisation_member(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE,null=True,blank=True)
organisation = models.ForeignKey(Organisation,on_delete=models.CASCADE,related_name='organisation_staff')
member_name = models.ForeignKey(settings.AUTH_USER_MODEL,related_name='organisation_staff_member',on_delete=models.CASCADE,null=True,blank=True)
is_admin = models.BooleanField(default=False)
I have this signal for creation of Organisation_member model:
#receiver(post_save, sender=Organisation)
def organisation_admin(sender, instance, created, **kwargs):
for member in instance.members.all():
if Organisation_member.objects.filter(user=instance.user,organisation=Organisation.objects.filter(user=instance.user,name=instance.name).first(),member_name=member).exists():
pass
else:
Organisation_member.objects.update_or_create(User=instance.User,organisation=Organisation.objects.filter(user=instance.user,name=instance.name).first(),member_name=member,is_admin=False)
The signal indicates that when I add a member in my manytomany field it will automatically create a Organisation_member object of the selected member.
The signal works perfectly fine.
My problem is the reverse i.e. when I try to delete an object of Organisation_member it should also remove the member from the manytomany relationship of the parent model.
I have tried this:
#login_required()
def delete_members(request, pk):
user_organisation = get_object_or_404(Organisation, user=request.user)
member_to_delete = Organisation_member.objects.filter(pk=pk)
if member_to_delete.exists():
member_to_delete[0].delete()
for member in user_organisation.members.all():
user_organisation.members.remove(member=member_to_delete[0])
return redirect(reverse('userprofile:organisation_member_list'))
But it does not removes the member from the parent model..
Anyone who knows the solution please help.
Thank you
Try something like this:
#login_required()
def delete_members(request, pk):
user_organisation = get_object_or_404(Organisation, user=request.user)
member_to_delete = get_object_or_404(Organisation_member, pk=pk)
user_organisation.members.remove(member_to_delete.member_name)
member_to_delete.delete()
return redirect(reverse('userprofile:organisation_member_list'))
Refer Django offical doc
The standard way of using this kind of relationship would be to specify Organisation_member as the through model for members in Organisation. Then the database takes care of it for you without any signals.
class Organisation(models.Model):
members = models.ManyToManyField(
settings.AUTH_USER_MODEL,
through='Organisation_member')
The only problem is that you currently have two references to User in Organisation_member: member_name and user. Why? You probably only need one and should delete the other. If there is a reason for having two different users in Organisation_member, you need to specify the fields to use for the relationship:
members = models.ManyToManyField(
settings.AUTH_USER_MODEL,
through='Organisation_member',
through_fields=('organisation', 'member_name'))
That's it, Organisation_members are automatically added and removed whenever you add or remove a user to or from members.

Setting default value for Foreign Key attribute

What is the best way to set a default value for a foreign key field in a model? Suppose I have two models, Student and Exam with student having exam_taken as foreign key. How would I ideally set a default value for it? Here's a log of my effort
class Student(models.Model):
....
.....
exam_taken = models.ForeignKey("Exam", default=1)
Works, but have a hunch there's a better way.
def get_exam():
return Exam.objects.get(id=1)
class Student(models.Model):
....
.....
exam_taken = models.ForeignKey("Exam", default=get_exam)
But this fails with tables does not exist error while syncing.
Any help would be appreciated.
I would modify #vault's answer above slightly (this may be a new feature). It is definitely desirable to refer to the field by a natural name. However instead of overriding the Manager I would simply use the to_field param of ForeignKey:
class Country(models.Model):
sigla = models.CharField(max_length=5, unique=True)
def __unicode__(self):
return u'%s' % self.sigla
class City(models.Model):
nome = models.CharField(max_length=64, unique=True)
nation = models.ForeignKey(Country, to_field='sigla', default='IT')
As already implied in #gareth's answer, hard-coding a default id value might not always be the best idea:
If the id value does not exist in the database, you're in trouble. Even if that specific id value does exist, the corresponding object may change. In any case, when using a hard-coded id value, you'd have to resort to things like data-migrations or manual editing of existing database content.
To prevent that, you could use get_or_create() in combination with a unique field (other than id).
Here's one way to do it:
from django.db import models
class Exam(models.Model):
title = models.CharField(max_length=255, unique=True)
description = models.CharField(max_length=255)
#classmethod
def get_default_pk(cls):
exam, created = cls.objects.get_or_create(
title='default exam',
defaults=dict(description='this is not an exam'),
)
return exam.pk
class Student(models.Model):
exam_taken = models.ForeignKey(
to=Exam, on_delete=models.CASCADE, default=Exam.get_default_pk
)
Here an Exam.title field is used to get a unique object, and an Exam.description field illustrates how we can use the defaults argument (for get_or_create) to fully specify the default Exam object.
Note that we return a pk, as suggested by the docs:
For fields like ForeignKey that map to model instances, defaults should be the value of the field they reference (pk unless to_field is set) instead of model instances.
Also note that default callables are evaluated in Model.__init__() (source). So, if your default value depends on another field of the same model, or on the request context, or on the state of the client-side form, you should probably look elsewhere.
I use natural keys to adopt a more natural approach:
<app>/models.py
from django.db import models
class CountryManager(models.Manager):
"""Enable fixtures using self.sigla instead of `id`"""
def get_by_natural_key(self, sigla):
return self.get(sigla=sigla)
class Country(models.Model):
objects = CountryManager()
sigla = models.CharField(max_length=5, unique=True)
def __unicode__(self):
return u'%s' % self.sigla
class City(models.Model):
nome = models.CharField(max_length=64, unique=True)
nation = models.ForeignKey(Country, default='IT')
In my case, I wanted to set the default to any existing instance of the related model. Because it's possible that the Exam with id 1 has been deleted, I've done the following:
class Student(models.Model):
exam_taken = models.ForeignKey("Exam", blank=True)
def save(self, *args, **kwargs):
try:
self.exam_taken
except:
self.exam_taken = Exam.objects.first()
super().save(*args, **kwargs)
If exam_taken doesn't exist, django.db.models.fields.related_descriptors.RelatedObjectDoesNotExist will be raised when a attempting to access it.
The issue with most of these approaches are that they use HARD CODED values or lambda methods inside the Model which are not supported anymore since Django Version 1.7.
In my opinion, the best approach here is to use a sentinel method which can also be used for the on_delete argument.
So, in your case, I would do
# Create or retrieve a placeholder
def get_sentinel_exam():
return Exam.objects.get_or_create(name="deleted",grade="N/A")[0]
# Create an additional method to return only the id - default expects an id and not a Model object
def get_sentinel_exam_id():
return get_sentinel_exam().id
class Exam(models.Model):
....
# Making some madeup values
name=models.CharField(max_length=200) # "English", "Chemistry",...
year=models.CharField(max_length=200) # "2012", "2022",...
class Student(models.Model):
....
.....
exam_taken = models.ForeignKey("Exam",
on_delete=models.SET(get_sentinel_exam),
default=get_sentinel_exam_id
)
Now, when you just added the exam_taken field uses a guaranteed existing value while also, when deleting the exam, the Student themself are not deleted and have a foreign key to a deleted value.
You could use this pattern:
class Other(models.Model):
DEFAULT_PK=1
name=models.CharField(max_length=1024)
class FooModel(models.Model):
other=models.ForeignKey(Other, default=Other.DEFAULT_PK)
Of course you need to be sure that there is a row in the table of Other. You should use a datamigration to be sure it exists.
I'm looking for the solution in Django Admin, then I found this:
class YourAdmin(admin.ModelAdmin)
def get_changeform_initial_data(self, request):
return {'owner': request.user}
this also allows me to use the current user.
see django docs
the best way I know is to use lambdas
class TblSearchCase(models.Model):
weights = models.ForeignKey('TblSearchWeights', models.DO_NOTHING, default=lambda: TblSearchWeights.objects.get(weight_name='value_you_want'))
so you can specify the default row..
default=lambda: TblSearchWeights.objects.get(weight_name='value_you_want')

how to handle multiple profiles per user?

I'm doing something that doesn't feel very efficient. From my code below, you can probably see that I'm trying to allow for multiple profiles of different types attached to my custom user object (Person). One of those profiles will be considered a default and should have an accessor from the Person class. Storing an is_default field on the profile doesn't seem like it would be the best way to keep track of a default, is it?
from django.db import models
from django.contrib.auth.models import User, UserManager
class Person(User):
public_name = models.CharField(max_length=24, default="Mr. T")
objects = UserManager()
def save(self):
self.set_password(self.password)
super(Person, self).save()
def _getDefaultProfile(self):
def_teacher = self.teacher_set.filter(default=True)
if def_teacher: return def_teacher[0]
def_student = self.student_set.filter(default=True)
if def_student: return def_student[0]
def_parent = self.parent_set.filter(default=True)
if def_parent: return def_parent[0]
return False
profile = property(_getDefaultProfile)
def _getProfiles(self):
# Inefficient use of QuerySet here. Tolerated because the QuerySets should be very small.
profiles = []
if self.teacher_set.count(): profiles.append(list(self.teacher_set.all()))
if self.student_set.count(): profiles.append(list(self.student_set.all()))
if self.parent_set.count(): profiles.append(list(self.parent_set.all()))
return profiles
profiles = property(_getProfiles)
class BaseProfile(models.Model):
person = models.ForeignKey(Person)
is_default = models.BooleanField(default=False)
class Meta:
abstract = True
class Teacher(BaseProfile):
user_type = models.CharField(max_length=7, default="teacher")
class Student(BaseProfile):
user_type = models.CharField(max_length=7, default="student")
class Parent(BaseProfile):
user_type = models.CharField(max_length=7, default="parent")
First of all you could make things a lot more easy by not declaring the BaseProfile abstract:
from django.db import models
from django.contrib.auth.models import User, UserManager
class Person(User):
public_name = models.CharField(max_length=24, default="Mr. T")
objects = UserManager()
def save(self):
self.set_password(self.password)
super(Person, self).save()
def _getDefaultProfile(self):
try:
return self.baseprofile_set.get(default=True)
except ObjectDoesNotExist:
return False
profile = property(_getDefaultProfile)
def _getProfiles(self):
return self.baseprofile_set.all()
profiles = property(_getProfiles)
class BaseProfile(models.Model):
person = models.ForeignKey(Person)
is_default = models.BooleanField(default=False)
class Teacher(BaseProfile):
user_type = models.CharField(max_length=7, default="teacher")
class Student(BaseProfile):
user_type = models.CharField(max_length=7, default="student")
class Parent(BaseProfile):
user_type = models.CharField(max_length=7, default="parent")
The way this is nicer? Your properties didn't know anyway what type they were returning, so the abstract baseclass only made you have an incredible annoying overhead there.
If you now are wondering how the hell you can get the data from the specific profiles since I made anything returned BaseProfile? You can do something like this:
try:
#note the lowercase teacher referal
print myuser.profile.teacher.someteacherfield
except Teacher.DoesNotExist:
print "this is not a teacher object!"
Also I do hope you didn't use the user_type field solely for this purpose, because django has it built in better as you can see. I also hope you really have some other unique fields in your derived profile classes because otherwise you should throw them away and just past a usertype field into BaseProfile (look at choices to do this good).
Now as for the is_default, imho this method is as good as any. You can always try to add custom constraints to your dbms itself, saying there sould be 0 or 1 records containing the same FK and is_default=True (there is no django way to do this). What I also would say is, add a method make_default and in that method make sure the is_default is unique for that person (e.g. by first setting is_default to False on all profiles with the same FK). This will save you a lot of possible sorrow. You can also add this check in the save() method of BaseProfile.
Another way you could do it is by adding a Foreign Key to the Person Model that points to the default Profile. While this will ensure default to be unique on django level, it can also provide denormalization and corruption of your data, even on a more annoying level, so I'm no big fan of it. But again, if you do all adding/removing/updating of profiles through predefined methods (will be more complex now!) you should be safe.
Finally, maybe you have good reasons to inherit from User, but the default way to extend the User functionality is not this, it's described here.