How to pass pk of detail view into fk of form - django

Good afternoon, I am fairly new to Django and I am not sure how to go about this.I have a Django 2.2 project with these models:
class Equipment(models.Model):
name = models.CharField(
max_length=15,
unique=True,
verbose_name='asset name')
asset_cat = models.ForeignKey('category',on_delete=models.PROTECT,verbose_name='asset category')
asset_loc = models.ForeignKey('location',on_delete=models.PROTECT,verbose_name='asset location')
state = models.ForeignKey('status',on_delete=models.PROTECT,verbose_name='status')
brand = models.CharField(
max_length=15,
unique=False,
blank=True)
model = models.CharField(
max_length=12,
unique=False,
blank=True,
verbose_name='model number')
def __str__(self):
return "{}".format(self.name)
def get_absolute_url(self):
return reverse('equipment-detail', args=[str(self.id)])
class Meta:
ordering = ['asset_cat', 'name']
verbose_name_plural = 'pieces of equipment'
class Action(models.Model):
name = models.ForeignKey('equipment',on_delete=models.PROTECT,verbose_name='asset name',blank=False)
dt = models.DateTimeField(
auto_now_add=True,
verbose_name='date and time of incident')
incident = models.TextField(
blank=True,
null=True)
CHANGE = 'CHANGE'
SERVICE = 'SERVICE'
ACTION_CHOICES = (
(CHANGE, 'CHANGE'),
(SERVICE, 'SERVICE')
)
act = models.TextField(
blank=True,
choices=ACTION_CHOICES,
null=True,
verbose_name='action taken')
act_detail = models.TextField(
verbose_name='action detail',
blank=False)
result = models.TextField(
blank=True,
null=True)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('service-detail', args=[str(self.id)])
class Meta:
ordering = ['-dt']
verbose_name_plural = 'service calls'
I have an Equipment Detail View like this:
class EquipmentDetailView(generic.DetailView):
model = Equipment
template_name = 'equipment_detail.html'
def get_context_data(self, **kwargs):
context = super(EquipmentDetailView, self).get_context_data(**kwargs)
return context
The detail view has two buttons: edit and service. If I click edit I have a model form that allows me to edit that instance of the Equipment model successfully.
However, when I click the service button, my form comes up to create an instance of the Action model, but when I submit it tells me that the null value in name_id violates the not null constraint.
It looks like my question is, how can I pass equipment.id from the Equipment Detail view to action.name of the action create form and keep the service button concept?
Action Form:
class ServiceForm(forms.ModelForm):
class Meta:
model = Action
fields = ['incident', 'act_detail', 'result']
Action (actually service) view:
class EquipmentServiceView(generic.CreateView):
template_name = 'equipment_service.html'
form_class = ServiceForm
queryset = Action.objects.all()

Assuming you don't want to go with simpliest solution to include name in form fields and have urls setup as:
/equipment/<id> - equipment detail view
/service - service (or action) create view
There are several ways of passing equipment id:
1) From url
We are going to change url to accept equipment_id. That means instead of /service you will have url /equipment/<equipment_id>/service.
Probably best solution - you will use URL according to REST architecture and will have very clear structure. Client can access page from anywhere (like just copy paste link from mail) and it will work.
urls.py:
urlpatterns = [
path('equipment/<int:pk>', EquipmentDetailView.as_view(), name='equipment-detail'),
path('equipment/<int:equipment_pk>/service', EquipmentServiceView.as_view(), name='service-create')
]
Your service button should look like this: service
and finally your view:
class EquipmentServiceView(CreateView):
template_name = 'equipment_service.html'
form_class = ServiceForm
queryset = Action.objects.all()
def form_valid(self, form):
equipment_pk = self.kwargs['equipment_pk']
equipment = get_object_or_404(Equipment, pk=equipment_pk)
self.object = form.save(commit=False)
self.object.name = equipment
self.object.save()
return super().form_valid(form)
2) Session data
In case you want to preserve service url without adding equipment_id, you can store equipment id either in session data(on your server) or in cookies(on client). That's not exactly good - client have to go to EquipmentDetailView prior to creating Service, but this will keep your urls intact.
views.py:
class EquipmentDetailView(DetailView):
model = Equipment
template_name = 'equipment_detail.html'
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
request.session['last_equipment_pk'] = self.object.pk
return response
class EquipmentServiceView(CreateView):
template_name = 'equipment_service.html'
form_class = ServiceForm
queryset = Action.objects.all()
def form_valid(self, form):
equipment_pk = self.request.session.get('last_equipment_pk')
equipment = get_object_or_404(Equipment, pk=equipment_pk)
self.object = form.save(commit=False)
self.object.name = equipment
self.object.save()
return super().form_valid(form)
P.S.: name is bad field name for ForeignField - should be something like equipment or so. Those labels usually associate with CharField and expected to be strings.

Related

How to update two models in one form in django?

Okay, so first of all the situation is not quite easy as in the title. So I want to create form which can create object based on Cycle model and update only one field in Post model (field in question is 'cycle_title'). What is more its isn't only one post where this post have to be updated but there are several of it (all post's titles are saved in Cycle.post).
views
class CycleCreateView(LoginRequiredMixin, BSModalCreateView):
template_name = 'blog/cycle_form.html'
form_class = CycleForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update(user=self.request.user)
return kwargs
def form_valid(self, form, **kwargs):
form.instance.author = self.request.user
return super().form_valid(form)
def get_success_url(self):
reverse_user = self.request.user
return reverse('profile', kwargs={'username': reverse_user})
forms
class CycleForm(BSModalForm):
def __init__(self, *args, user=None, **kwargs):
super().__init__(*args, **kwargs)
if user is not None:
self.fields['posts'].queryset = Post.objects.filter(author=user)
class Meta:
model = Cycle
fields = ['title', 'description', 'posts']
widgets = {
'posts': forms.CheckboxSelectMultiple(),
}
models
class Post(models.Model):
title = models.CharField(max_length=100, unique=True)
content = MDTextField()
date_posted = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE)
cycle_title = models.CharField(max_length=100, default='')
class Cycle(models.Model):
title = models.CharField(max_length=100, unique=True)
description = models.TextField(max_length=500, default="Brak opisu")
date_created = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE)
posts = models.ManyToManyField(Post)
I was thinking about a solution like this:
for i in form.cleaned_data['posts']:
post = Post.objects.get(title=form.cleaned_data['title'][i])
post.cycle_title = form.cleaned_data['title']
post.save()
But I doubt if it is good way to resolve this issue
A package has been built just to handle this exact scenario, django-shapeshifter. You can find it here:
https://github.com/kennethlove/django-shapeshifter
The basic premise is to create two model forms, then include them in the same template. The example given shows how to update a User and a Profile model from the same view and form. It sounds like that is a match for your problem. Full disclosure, I'm a contributor to this package, but is was created exactly because of frustrations like your own!

ForeignKey model query filter

Using filter(), how do I get the foreign key property 'recipient' of the current authenticated user ?
Models.py:
class Message(models.Model):
recipient = models.ForeignKey(CustomUser, on_delete = models.CASCADE,related_name = 'recipient',null = True)
sender = models.ManyToManyField(CustomUser,related_name = 'messages')
date = models.DateTimeField(auto_now_add=True, blank=True)
subject = models.CharField(max_length = 1000, blank = True)
message = models.TextField(blank=True, null=True)
unread = models.BooleanField(default = True)
class CustomUser(User):
user_bio = models.TextField(blank=True, null=True)
birth_date = models.DateField(null=True, blank=True)
def __str__(self):
return self.username
Views.py:
### Inbox list class
class InboxListView(ListView):
'''
This view lets the user view all the messages created in a list
'''
model = Message# [Message,SafeTransaction] # I want to be able to draw from two models/objects #
template_name = "myInbox/inbox.html"
paginate_by = 5
def get_context_data(self, **kwargs):
context = super(InboxListView, self).get_context_data(**kwargs)
context['message_list'] = Message.objects.filter(recipient=CustomUser.SOMETHING_idk)#INCORRECT FILTRATION, FIX ASAP!!!!!!!!!!!!!#
context['now'] = timezone.now()
context['safeTransaction_list'] = SafeTransaction.objects.all()
return context
Specifically this line is what I need puzzled out :
context['message_list']=Message.objects.filter(recipient=CustomUser.SOMETHING_idk)
What can I put as the filter parameter for a specific message recipient ?
I tried something along the lines of CustomUser or CustomUser.pk, or request.authenticated_as_the_thing_I_want_specifically. etcetera.
I seem to be a bit lost with it.
any help at all is appreciated.
It looks like you've created a custom auth user model? Assuming that's been setup correctly you should be able to do the following:
def get_context_data(self, **kwargs):
context = super(InboxListView, self).get_context_data(**kwargs)
context.update({
'message_list': Message.objects.filter(recipient=self.request.user),
'now': timezone.now(),
'safeTransactionList': SafeTransaction.objects.all(),
})
return context
Some additional things of note:
now is available as a django template tag. You can {% now %} in your template code.
With the way the Django ORM works you would be able to use the following in your template code {% for message in request.user.recipient.all %} or {% for message in request.user.messages.all %} instead of making the ORM call in get_context_data to iterate over message_list for the logged in user.

How do I limit the ListView slug to a number of choices in Django?

Here is the code
models.py
class Submission(models.Model):
CAR = 'car'
TRUCK = 'truck'
VAN = 'van'
SUV = 'suv'
CAR_TYPES = (
(CAR, 'Car'),
(TRUCK, 'Truck'),
(VAN, 'Van'),
(SUV, 'SUV'),
)
submission_type = models.CharField(_('Submission Type'), max_length=20, choices=MEDIA_TYPES, default=CAR)
title = models.CharField(_('Title'), max_length=100, blank=False)
description = models.TextField(_('Description'))
user = models.ForeignKey(User, related_name='user_submission')
thumbnail = models.ImageField()
date_submitted = models.DateTimeField(default=timezone.now)
views.py
class SubmissionCategoryList(ListView):
model = Submission
template_name = 'submission/submit_cat.html'
def get_queryset(self):
queryset = super(SubmissionCategoryList, self).get_queryset()
return queryset.filter(submission_type=self.kwargs['slug']).order_by('-date_submitted')
def get_context_data(self, **kwargs):
context = super(SubmissionCategoryList, self).get_context_data(**kwargs)
return context
urls.py
url(r'^(?P<slug>[\w-]+)/$', SubmissionCategoryList.as_view(), name='submit_category'),
The code works fine. When I go to localhost:8000/car/ It shows the list view for only the CARS submission_type, etc. But, when I type in a url that isn't a part of the choices in CAR_TYPES, for example, localhost:8000/boat/, django still shows the template for this view. My question is: How do I limit the number of choices the slug should accept? And, if it is not a part of the CAR_TYPES choices, how do I get it to ignore this view?
Have you tried validating your "slug" in views?
views.py
def get_queryset(self):
if self.kwargs['slug'] is not None and self.kways['slug'].lower() in project.settings.CAR_TYPE:
# return your queryset.filter ...
else:
# return other template or redirect to other views.
project.settings.py
CAR_TYPE = ['car', 'truck', 'van', 'suv']
or, simply just edit url regex:
(?P<slug>car|truck|van|suv)
not the best approach but definitely solve your problem.
Ok so thanks to Anzel I figured out a great solution to this problem. I'm putting this here for anyone who needs this.
I created a reference.py file that looks like this. You can call it whatever you like, I just call it reference because it's what it does.
from enum import Enum
class CarTypes(Enum):
CAR = 'car'
TRUCK = 'truck'
VAN = 'van'
SUV = 'suv'
I changed my models.py to import reference.py
I changed choices in submission_type to choices=tuple([(auto.name, auto.value) for auto in CarTypes])
from .reference import CarTypes
class Submission(models.Model):
submission_type = models.CharField(_('Submission Type'), max_length=20, choices=tuple([(auto.name, auto.value) for auto in CarTypes]), default=CarTypes.CAR)
title = models.CharField(_('Title'), max_length=100, blank=False)
description = models.TextField(_('Description'))
user = models.ForeignKey(User, related_name='user_submissions')
thumbnail = models.ImageField()
date_submitted = models.DateTimeField(default=timezone.now)
And here is the final working views.py for categories
from .reference import CarTypes
class SubmissionCategoryList(ListView):
model = Submission
template_name = 'submission/submit_cat.html'
CAR_TYPES = [auto.value for auto in CarTypes]
def get_queryset(self):
if self.kwargs['slug'] in self.CAR_TYPES:
queryset = super(SubmissionCategoryList, self).get_queryset()
return queryset.filter(submission_type=self.kwargs['slug']).order_by('-date_submitted')
else:
return Http404
def get_context_data(self, **kwargs):
context = super(SubmissionCategoryList, self).get_context_data(**kwargs)
context['submission_type'] = self.kwargs['slug']
return context
You can put this same queryset in a DetailView and it should work when a user searches site.com/car/2 or something like that

Set a form field to the current logged user id

I have a class Task with the following implementation:
class Task(models.Model):
author = models.ForeignKey(Author, unique=False)
name = models.CharField(max_length=255)
completed = models.BooleanField(default=False)
deadline = models.DateTimeField(null=True, blank=True)
pub_date = models.DateTimeField(auto_now_add=True, editable=False)
edit_date = models.DateTimeField(auto_now_add=True, auto_now=True, editable=False)
tag = models.ManyToManyField(Tag, related_name='tags', null=True, blank=True, default=None)
# group = models.ForeignKey(Group, blank=True, default=None)
def __str__(self):
return u'%s' % (self.name)
def toggle_complete(self):
self.completed = not self.completed
def is_past_deadline(self):
return timezone.now() > self.deadline
And I am trying to do a simple form that creates a new Task with a Title. But, as you can see, the author attribute can not be null (and don't want to, of course).
Author is implemented as follows:
class Author(models.Model):
user = models.OneToOneField(User, primary_key=True)
name = models.CharField(max_length=30)
def __str__(self):
return u'%s' % (self.user)
I tried and tried to hide the author field and, overriding methods like get_form_kwargs, form_valid, get_form to set it to the current logged user, but I always fail. Simply, the id is neither sent as post data (as seein in the debug trace), nor fetched from the view itself.
My best result has been showing the author field, creating the user correctly, but getting a "success_url" not found, even with the model having a get_absolute_url method declared.
The view I am working with is implemented like:
class HomeView(CreateView, MultipleObjectMixin):
# common
model = models.Task
template_name = 'home.html'
#form
form_class = TaskForm
# list
object_list = model.objects.all()
context_object_name = 'tasks'
paginate_by = 40
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated():
return HttpResponseRedirect(reverse('taskr:index'))
return super(HomeView, self).dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super(HomeView, self).get_form_kwargs()
kwargs['initial']['author_id'] = self.request.user.id
return kwargs
def form_valid(self, form):
task = form.save(commit=False)
task.user = models.Author.objects.get(user=self.request.user) # use your own profile here
task.save()
return HttpResponseRedirect(self.get_success_url())
For the record, the MultipleObjectMixing part of the view works flawlessly.
I am desperate, is there any good resource for Django forms, one like http://ccbv.co.uk/? Thanks.
After a good night sleep, while cleaning up, I tried fixing the form_valid in the CreateView descendant and I got it right.
The trick is in
task.user = models.Author.objects.get(user=self.request.user)
and it failed to me because of desperate copy-pasting. The problem was that my Task model has no user attribute, but an author. So
task.author = models.Author.objects.get(user=self.request.user)
fixes it all.
Sorry for the stupid question.

Enforcing related model creation

I'd like to add multiple dealers support from the dashboard following the oscar's documentation:
You’ll need to enforce creating of a StockRecord with every Product.
When a Product is created, Stockrecord.partner gets set
to self.request.user.partner (created if necessary), and hence the
connection is made
I don't know how to enforce the StockRecord creation. oscar has a dashboard that replaces the django admin, this is an excerpt(first lines) of the view used to create/update a Product:
class ProductCreateUpdateView(generic.UpdateView):
"""
Dashboard view that bundles both creating and updating single products.
Supports the permission-based dashboard.
"""
template_name = 'dashboard/catalogue/product_update.html'
model = Product
context_object_name = 'product'
form_class = ProductForm
category_formset = ProductCategoryFormSet
image_formset = ProductImageFormSet
recommendations_formset = ProductRecommendationFormSet
stockrecord_formset = StockRecordFormSet
So the Product creation view would display the StockRecord formset, but I can create/update the Product without creating a StockRecord object. I'd like to show an error message when this occurs.
StockRecord form/formset:
class StockRecordForm(forms.ModelForm):
def __init__(self, product_class, *args, **kwargs):
super(StockRecordForm, self).__init__(*args, **kwargs)
# If not tracking stock, we hide the fields
if not product_class.track_stock:
del self.fields['num_in_stock']
del self.fields['low_stock_threshold']
else:
self.fields['price_excl_tax'].required = True
self.fields['num_in_stock'].required = True
class Meta:
model = StockRecord
exclude = ('product', 'partner', 'num_allocated')
BaseStockRecordFormSet = inlineformset_factory(
Product, StockRecord, form=StockRecordForm, extra=1)
class StockRecordFormSet(BaseStockRecordFormSet):
def __init__(self, product_class, *args, **kwargs):
self.product_class = product_class
super(StockRecordFormSet, self).__init__(*args, **kwargs)
def _construct_form(self, i, **kwargs):
kwargs['product_class'] = self.product_class
return super(StockRecordFormSet, self)._construct_form(
i, **kwargs)
StockRecord model(excerpt):
class AbstractStockRecord(models.Model):
product = models.ForeignKey(
'catalogue.Product', related_name="stockrecords",
verbose_name=_("Product"))
partner = models.ForeignKey(
'partner.Partner', verbose_name=_("Partner"),
related_name='stockrecords')
partner_sku = models.CharField(_("Partner SKU"), max_length=128)
price_currency = models.CharField(
_("Currency"), max_length=12, default=settings.OSCAR_DEFAULT_CURRENCY)
price_excl_tax = models.DecimalField(
_("Price (excl. tax)"), decimal_places=2, max_digits=12,
blank=True, null=True)
price_retail = models.DecimalField(
_("Price (retail)"), decimal_places=2, max_digits=12,
blank=True, null=True)
What you want to do is ensure there is at least 1 valid formset submitted when saving the main form?