Django ModelMultipleChoiceField for another model's many-to-many - django

I'm working on a DIY application (Django 1.5) and I've reached a roadblock. The main models involved are Guide, Tool, Item, Step. A Guide can have many Tools and Items, and a Tool or Item can belong to many Guides. The same goes for a Step - it can have many Tools and Items, and a Tool or Item can belong to many Steps. A Guide has many Steps and a Step belongs to a Guide.
Guide many-to-many Items
Guide many-to-many Tools
Guide one-to-many Steps
Step many-to-many Items
Step many-to-many Tools
The roadblock...
At the Guide-level, I want the Tool and Item options to be limitless. But at the Step-level, I want the Tool and Item options to be limited to those assigned to the Guide it belongs to. Basically, when creating/editing a Step, I want to list checkboxes for all the Tools and Items available through the Guide. The user selects those that are needed for the current Step. Each Step will have different combinations of Tools and Items (thus the need for checkboxes).
I discovered the ModelMultipleChoiceField for the Step's ModelForm class. There I can specify a queryset. BUT, how do I gain access to the instance of the Guide model to retrieve its Tools and Items so that I can properly build selections? I would like to provide queries similar to what you would do in a View...
Guide.objects.get(pk=n).tools.all()
Guide.objects.get(pk=n).items.all()
How can I achieve that via ModelMultipleChoiceField? I hope I was able to explained this clearly.
Thanks in advance for any help.
class Tool(models.Model):
name = models.CharField(max_length=100)
...
class Item(models.Model):
name = models.CharField(max_length=100)
...
class Guide(models.Model):
models.CharField(max_length=100)
description = models.CharField(max_length=500)
tools = models.ManyToManyField(Tool, null=True, blank=True)
items = models.ManyToManyField(Item, null=True, blank=True)
...
class Step(models.Model):
title = models.CharField(max_length=100)
body = models.TextField()
guide = models.ForeignKey(Guide)
tools = models.ManyToManyField(Tool, null=True, blank=True)
items = models.ManyToManyField(Item, null=True, blank=True)
EDIT: 5/2
After further reading, it looks like I have to override the __init__ method of ModelMultipleChoiceField, where I gain a reference to self.instance, allowing me to create my query like, self.instance.guide.tools.all() and self.instance.guide.items.all(). And then create the fields via fields['field_name'].
I'm at work now so I won't be able to try this out until later tonight. I'll report back my findings.

What I ended up doing is the following. I defined a method in my ModelForm class for creating the ModelMultipleChoiceField. The reason is at the point of requesting the Create Step page, there is no Guide associated with the Step, not until you save (POST...assuming validation is successful). But I do have access to the slug field of the Guide that the Step will be created for. I get if from the URL. And the slug field is unique in my app. So I pass the slug from my view to the form via the new method I created. From there, I'm able to get the tools assigned to the guide and make those options available on the form, in my template.
forms.py
class NewStepForm(forms.ModelForm):
...
def create_tools_field(self, slug):
self.fields['tools'] = forms.ModelMultipleChoiceField(
queryset=Guide.objects.get(slug=slug).tools.all(),
widget=forms.CheckboxSelectMultiple(attrs={'class': 'unstyled'})
)
...
views.py
class NewStepView(View):
form_class = NewStepForm
initial = {'key': 'value'}
template_name = "guides/guide_step_new.html"
#method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(NewStepView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
slug = kwargs.get('slug')
form = self.form_class(initial=self.initial)
form.create_tools_field(slug)
return render(request, self.template_name, {'form': form})

Related

Django ManyToMany relationship, filter options of modelmultiplechoicefield

I am currently rewriting a web app that is utilizing a hierarchy of relationships which is making it extremely painful to create a reliable work flow for the creation of objects. The current set up is as follows:
Customer Hierarchy:
Organization -> Location -> Room
Contacts can belong to one or many of these entries at any level of the hierarchy. Ex Jim can be assigned to Organization and location.
With this I need to filter the django many to many field that is populated with any contact that doesn't belong anywhere OR belongs to a parent or child level of the customer hierarchy.
I have attempted inline formsets which fails on the many to many model as well as limit_choices_to={'Organization':_get_self_pk} . This works but doesn't allow for the use of django admin style on the fly creation of contacts. I have also attempted to use a queryset in the init function for create, but my form has a nested inline formset that doesn't allow me to use the self.field['contact'] to inject the queryset. (Key Error, contacts doesn't exist as a field)
Models.py
class Organization(AbstractExclusions, AbstractManyToManyCommonInfo, AbstractCommonInfo, AbstractOrganizationLocationCommonInfo, AbstractAcvitivyInfo):
....
contact = models.ManyToManyField('Contact', blank=True)
class Location(AbstractManyToManyCommonInfo, AbstractCommonInfo, AbstractOrganizationLocationCommonInfo, AbstractLocationRoomCommonInfo, AbstractAcvitivyInfo, AbstractExclusions):
....
contact = models.ManyToManyField('Contact', blank=True)
class Room(AbstractManyToManyCommonInfo, AbstractCommonInfo, AbstractLocationRoomCommonInfo, AbstractAcvitivyInfo, AbstractExclusions):
....
contact = models.ManyToManyField('Contact', blank=True)
class Contact(AbstractUserInfo):
phone_number = PhoneNumberField(verbose_name=_('Phone Number'))
is_user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.CASCADE, verbose_name=_('Link To Authenticated User'),)
role = models.ForeignKey(refModels.Role, verbose_name=_("Role"), on_delete=models.CASCADE, null=True, blank=True)
This question was answered by creating a special init function as shown below:
def __init__(self, *args, **kwargs):
super(OrganizationForm, self).__init__(*args, **kwargs)
if 'initial' in kwargs:
try:
self.fields['contact'].queryset = (custModels.Contact.objects.filter(Q(organization = None) | Q(organization = self.instance.id)))
except:
pass

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

Django admin - giving users access to specific objects/fields?

I need to make an "owners" login for the admin. Say we have this model structure:
class Product(models.Model):
owner = models.ManyToManyField(User)
name = models.CharField(max_length=255)
description = models.CharField(max_length=255)
photos = models.ManyToManyField(Photo, through='ProductPhoto')
class Photo(models.Model):
order = models.IntegerField()
image = models.ImageField(upload_to='photos')
alt = models.CharField(max_length=255)
class ProductPhoto(models.Model):
photo = models.ForeignKey(Photo)
product = models.ForeignKey(Product)
We have a group called Owners that some users are part of. The ProductPhoto is a TabularInline on the Product admin page.
Now, owners need permission to edit
(primary goal) only products where product__in=user.products (so basically, only products owned by them).
(secondary goal) only the description and photos of products
How would I do this with Django's admin/permission system?
This is row (or object) level permission. Django provides basic support for object permissions but it is up to you to implement the code.
Luckily, there are a few apps that provide drop-in object-level permission framework. django-guardian is one that I have used before. This page on djangopackages.com provides some more that you can try out.
You may implement using get_form. For complex rule, you can add this too: https://github.com/dfunckt/django-rules
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
# permission check;
if form.base_fields and not request.user.is_superuser:
# when creating or updating by non-reviewer (except superuser)
# allow only reviewer to allow updating
form.base_fields['usertype'].disabled = True

Django complex query comparing 2 models

This may be a design question.
Question is "What is the best way to find offers that needs to have feedback sent by logged in user". In Feedbacks site there are 3 tabs: "Sent", "Received", "Send feedback".
"Send feedback" tab there's a table with "Offer id","username(buyer/sender" and "Send feedback" link pointing to feedback form.
Here's the code which should help understand what I mean.
Offers are displayed until some user buys it.
Offer is being closed, and new Order (storing order details) instance is created for this offer.
I'm trying to implement a Feedback app, where both sides of offer transaction can
send feedback about transaction.
Let's skip the "ended" or "running" offer problem.
class Offer(models.Model):
"""Offer is displayed for 5 days, then it's being ended by run everyday cron script.
If someone buys the offer end_time is being set, and offer is treated as ended.
Both sides of transaction may send feedback.
"""
whose = models.ForeignKey(User, verbose_name="User who has created the offer")
end_time = models.DateTimeField(blank=True, null=True, help_text="")
field = ()
fields2 = ()
order = models.ForeignKey(Order, balnk=True, null=True, help_text="Order details")
class Order(models.Model):
"""stores order details like user, date, ip etc."""
order_1_field_details = ()
who = models.ForeignKey(User, verbose_name="User who bought the offer")
offer_id = models.PositiveIntegerField("know it's unnecessary")
offer_data = models.TextField('offer data dict here')
class Feedback(models.Model):
offer_id = models.PositiveIntegerField()
sent_by = models.ForeignKey(User, verbose_name="Offer sender")
received_by = models.ForeignKey(User, verbose_name="Offer receiver")
def get_offer(self):
try:
Offer.objects.get(id=self.offer_id)
except Offer.DoesNotExist:
return None # offer moved to archive
In first draft there was a offer = models.ForeignKey(Offer) instead of offer_id field,
but I am going to move some old offers from Offer table to another one for archiving.
I would like the feedback stay even if I 'archive' the offer. In feedback list there will be an 'Offer id" link and for offers older than 60 days user will see "moved to archive" when clicking "details".
All I can think of at the moment is getting offers which hasn't expired, but there was a buyer.
ended() is a manager returning self.filter(end_date__isnull=False)
offers_with_buyer = models.Q(Offer.objects.ended().filter(whose__exact=request.user, order__isnull=False) | models.Q(Offer.objects.ended().filter(order__who__exact=request.user)
How do I check if there's a feedback for these offers ?
I know I should return user and offer id from queryset above and check if they exist in Feedback.offer_id and Feedback.sent_by.. or maybe I should change model design completely ...
First, how you're handling the end date is very contrived. If the offer ends 5 days after it's created, then just set that automatically:
from datetime import datetime, timedelta
class Offer(models.Model):
...
def save(self, *args, **kwargs):
self.end_date = datetime.now() + timedelta(days=5)
super(Offer, self).save(*args, **kwargs)
Then, just modify your ended manager to return instead: self.filter(end_date__lte=datetime.now())
However, I generally prefer to add additional methods to my default manager to deal with the data in various ways:
class OfferQuerySet(models.query.QuerySet):
def live(self):
return self.filter(end_date__gt=datetime.now())
def ended(self):
return self.filter(end_date__lte=datetime.now())
class OfferManager(models.Manager):
use_for_related_fields = True
def get_query_set(self):
return OffersQuerySet(self.model)
def live(self, *args, **kwargs):
return self.get_query_set().live(*args, **kwargs)
def ended(self, *args, **kwargs):
return self.get_query_set().ended(*args, **kwargs)
(Defining a custom QuerySet and then using methods on the Manager that just proxy to the QuerySet is the way the Django core team does it, and allows you to use the methods anywhere in the chain, instead of just the front.)
As far as "archiving" your Offers go, it's extremely bad practice to divvy out similar data into two different models/tables. It's exponentially increases the order of complexity in your app. If you want to "archive" an offer. Add a field such as:
is_archived = models.BooleanField(default=False)
You can then create another method in your Manager and QuerySet subclasses to filter out just archived or live offers:
def live(self):
return self.filter(is_archived=False, end_date__gt=datetime.now())
def archived(self):
return self.filter(is_archived=True)
If you really need another model (such as for a separate view in the admin for just archived offers) you can create a proxy model:
class ArchivedOfferManager(models.Manager):
def get_query_set(self):
return super(ArchivedOfferManager, self).get_query_set().filter(is_archived=True)
def create(self, **kwargs):
kwargs['is_archived'] = True
return super(ArchivedOfferManager, self).create(**kwargs)
class ArchivedOffer(models.Model)
class Meta:
proxy = True
objects = ArchivedOfferManager()
def save(self, *args, **kwargs):
self.is_archived = True
super(ArchivedOffer, self).save(*args, **kwargs)

Django: Get all blogs and their latest blog entries

Suppose I have these two models:
class Blog(models.Model):
name = models.CharField(max_length=64)
# ...
class Entry(models.Model):
blog = models.ForeignKey(Blog)
added = models.DateTimeField(auto_now_add=True)
# ...
Now I want to get a list of all blogs along with the latest blog entry for each respective blog. What's the best way to do it?
If it makes any difference, we can assume that each blog has at least one entry.
There is a roundabout (and very hackish) way to do this. If you don't mind de-normalizing you can add an optional latest_entry field to your Blog model. You can then override Entry.save() to update this field for the corresponding Blog instance every time an Entry instance is created. Alternately you can add a signal to do this.
def save(self, *args, **kwargs):
if not self.pk: #This object is being created, not updated.
self.blog.latest_entry = self
models.Model.save(self, *args, **kwargs)
Yep, it is not clean. But it will reduce the number of queries. Then you can do:
[(blog, blog.latest_entry) for blog in Blog.objects.all()]
You can use the latest() method of the related manager:
blogs = Blog.objects.all()
a_blog = blogs[0]
latest_entry = a_blog.entry_set.latest()
# or...
latest_entries = [blog.entry_set.latest() for blog in blogs]
Something like that.