I'm a newbie with django and working on a project where I need to display registered user's last visited pages on their profile page. I have achieved that within my extended user class by adding a new many2many field to my main object which I want to keep history for. In my view, whenever a member makes a request I add the object to member's history. But this doesn't give me the result that I want. Items are not ordered and if user is not logged in it gives User DoesNotExist error. I know there is a better way then this but I could't find it. Probably I'm not on the correct way. I appreciate any help or ideas.
class myObjectView(View):
model = myObject
template_name = 'app/myobject_detail.html'
def get(self, request, **kwargs):
cat = Category.objects.all()
sec = Section.objects.all()
self.item = myObject.objects.get(slug = self.kwargs[u'slug'])
user = User.objects.get(username=request.user.username)
if user.is_authenticated():
if self.item in user.member.history.all():
user.member.history.remove(self.item)
user.member.history.add(self.item)
user.save()
else:
user.member.history.add(self.item)
user.save()
Your approach has drawbacks but is not a bad one if you need long term persistance.
You could easily add an ordering field in your m2m through table (look at docs) to add some sense of ordering. You could also order your m2m through table by its PK, as large PK values would mean newer entries given your current code of removing items and adding them again.
ordered_item_history = (user.member.history.through.objects
.filter(user=user, myObject=self.item)
.order_by('pk').values_list('myObject', flat=True))
That said the easiest way to do something like this is in the session.
request.session.setdefault('history', []).append(myObj)
request.session.modified = True
Now in any view, you can access this ever-growing list of object history via request.session['history']. Modify as necessary to eliminate duplicates.
Related
i am building an application that has a model with three fields Company,Name, position. in the same model i want to have company name as one field while the user can add name and positions for multiple candidates. the reason am trying to do that is because i didnt find any proper way to set automatically select the foreign key based on the company name entered since foreign key is a drop down list and couldnt figure out the way to make foreign key field equal to company name entered.
appreciate help and suggestions if any for the approach i have in mind.
You need two forms (or more usefully one form and one formset). Use form prefixes to make them distinguishable. Pass both to the template, say as selectform and candidate_formset and in the template, use
{{selectform}}
{{candidate_formset}}
The first is a company-select form. It might, for example, be
class CompanySelectForm(forms.ModelForm):
class Meta:
model = Candidate
fields = ['company']
The second is a form, or probably a formset, for entering one, or (via a formset) as many candidates as there are to be entered. It will look like
class CandidateForm(forms.ModelForm):
class Meta:
model = Candidate
fields = ['name','position']
Now, you use commit=False (docs) to create objects but not save them. First, process CandidateSelectForm, which will give you a Candidate object with a valid company instance, but not save it. Then process the formset of CandidateForm, again with commit=False, which will give you a list of candidate instances with no company, again unsaved. Finally for each candidate in this list, set the company field of every candidate to the one on the object retrieved by CandidateSelectForm and save it.
It will probably be easier to write a plain view function, than messing around with method overrides trying to get the class-based views to process two forms this way.
Edit - added on request.
The view could be modelled on this one in the Django doc. I've made the obvious changes in line with the earlier part of the answer, but it's probably full of errors and I'm not going to debug it further here
def get_name(request):
# if this is a POST request we need to process the form data
if request.method == 'POST':
# create form instances and populate with data from the request:
cs_form = CompanySelectForm(request.POST, prefix="cs")
cand_form = CandidateSelectForm( request.POST, prefix="cand")
# check whether it's valid:
if cs_form.is_valid() and cand_form.is_valid():
selector = cs_form.save(commit=False)
candidate = cand_form(commit=False)
candidate.company = selector.company
candidate.save()
return HttpResponseRedirect('/thanks/')
# if a GET (or any other method) we'll create a blank form
else:
cs_form = CompanySelectForm( prefix='cs')
cand_form = CandidateSelectForm( prefix='cand')
return render(request, 'name.html', {
"select_form": cs_form,
"candidate_form": cand_form,
})
Once you have got this working for a single candidate, you can progress to turning candidate_form into a formset, documented here which will let you enter any number of candidates to be attached to the one selected company.
I have a django page that displays a list of links. Each link points to the detail page of the respective object. The link contains the pk/id of that object (something like ../5/detailObject/). The list is generated on the backend and has some filtering baked into it, e.g. only generate a link if that object has state x, etc.
Clicking on the links works, but users can still manipulate the url and pass a valid link with an incorrect state (a wrong pk/id is being handled with the get or 404 shortcut).
What is the best practice for handling this kind of scenario with django? Should that kind of filtering be placed in the object's model class instead of using function-based views as I do now?
Function based view:
If you want to restrict a set of objects to a particular user (for instance a user's orders), then you would need to set up the Order model to foreign key to the User model and then look up the order by both id and user:
views.py:
def get_order(request, id=0)
if request.method == 'GET':
try:
order = Order.objects.get(user=request.user, pk=id)
except Order.DoesNotExist:
return redirect(...)
And set up a url to handle:
url(r'^order/(?P<id>\d+)/$', views.get_order, name='get_order_by_id'),
As far as adding a slug field on the model after the fact, set up a second url:
url(r'^order/(?P<slug>[\w-]+)/$', views.get_order, name='get_order_by_slug')
And change the above view logic to first do a lookup by pk if pk is greater than 0 and then redirect back to the function using the slug from the looked up order (this assumes all looked-up records have slugs):
def get_order(request, slug='', id=0)
if request.method == 'GET':
try:
if id > 0:
order = Order.objects.get(user=request.user, pk=id)
return redirect(reverse('get_order_by_slug'), permanent=True, slug=order.slug)
order = Order.objects.get(user=request.user, slug=slug)
except Order.DoesNotExist:
return redirect(...)
You should also put unique=True on the slug field and ensure that the user is authenticated by placing the #login_required decorator on your view.
To restrict orders by a particular status, you could:
Create a set of statuses for your Order model, and then you could:
Pass a value for a kwarg in the view when you filter, or
Create a custom manager on the Order model
There are several ways you could create your statuses:
as a set of choices on the Order model
use the SmartChoices library
as a database field
If you create choices on the Order model, it could be something like this:
class Order(models.model):
STATUSES = (
('PLCD', 'Placed'),
('INTR', 'In Transit'),
('DLVR', 'Delivered')
)
status = models.CharField(max_length=4, default='', choices=STATUSES)
An acquaintance who is a very seasoned Django professional told me about the SmartChoices library. I have not used it yet but would like to try it at some point. The database field option would be my least preferred way of doing this because that seems to me like moving programming variables into the database; however, it would work.
I am creating an expense submission system, which will be multi-user.
For the purpose of this question, there are two models: Claim and Journey. A user creates a claim and each claim can have multiple journeys. I have made a gist of the code snippet as it's quite long.
In this snippet, I have sucessfully:
Made ClaimListView.get_queryset filter by current user, so whoever's logged in can only see a list of their own claims.
Made ClaimCreateView.form_valid set the correct user when the form is submitted.
Made ClaimDetailView.get_queryset filter by current user. If someone tries the url for another user's claim detail, they get a 404 (perfect!)
Done the same as above for JourneyListView
Done the same as above for JourneyDetailView - again 404 if not authroised :D
However, when I access JourneyCreateView via the URL, the dropdown box for claim still shows claims for the other users.
How should I filter the user within the JourneyCreateView class, so that the claim field only shows claims assigned to the current user?
The closest to a solution I've got is this answer which suggests overriding the __init__ function in the JourneyForm which would leave me with this:
class JourneyForm(forms.ModelForm):
class Meta:
model = Journey
fields = ['date', 'distance','claim']
def __init__(self,alloweduser,*args,**kwargs):
super (JourneyForm,self ).__init__(self,*args,**kwargs) # populates the post
self.fields['claim'].queryset = Claim.objects.filter(tech_id=alloweduser)
However I'm not sure how to pass the alloweduser in from JourneyCreateView or, more to the point, obtain the current user in this class.
form_valid isn't any use in this case, as I'm trying to obtain the user prior to the form being submitted.
In views, the request the view is handling is stored in self.request, so you can obtain the user with self.request.user, and its id with self.request.user.id.
A Django view with the FormMixin [Django-doc] has a method that can be overwritten to pass parameters: get_form_kwargs() [Django-doc].
So we can implement this as:
from django.views.generic.edit import CreateView
class JourneyCreateView(CreateView):
model = Journey
form_class = JourneyForm
def get_form_kwargs(self, *args, **kwargs):
kwargs = super().get_form_kwargs(*args, **kwargs)
kwargs['alloweduser'] = self.request.user.id
return kwargs
# ...
I'm using django-filter which is working great but I am having a problem filtering my drop down list of choices (which is based on a model) by the current user. It's a fairly basic and common scenario where you have a child table which has a many to one relationship to a parent table. I want to filter the table of child records by selecting a parent. This is all fairly easy, standard stuff. The fly in ointment is when the parent records are created by different users and you only want to show the parent records in the drop down list that belongs to the current user.
Here is my code from filters.py
import django_filters
from django import forms
from .models import Project, Task
from django_currentuser.middleware import get_current_user, get_current_authenticated_user
class MasterListFilter(django_filters.FilterSet):
project = django_filters.ModelChoiceFilter(
label='Projects',
name='project_fkey',
queryset=Project.objects.filter(deleted__isnull=True, user_fkey=3).distinct('code')
)
class Meta:
model = Task
fields = ['project']
#property
def qs(self):
parent = super(MasterListFilter, self).qs
user = get_current_user()
return parent.filter(master=True, deleted__isnull=True, user_fkey=user.id)
This bit works fine:
#property
def qs(self):
parent = super(MasterListFilter, self).qs
user = get_current_user()
return parent.filter(master=True, deleted__isnull=True, user_fkey=user.id)
This filters my main list so that only records that have a master flag set, have not been deleted and belong to the current user are shown. This is exactly what I want.
This following bit also works and gives me the filtered drop down list that I am looking for because I have hardcoded 3 as the user.id
queryset=Project.objects.filter(deleted__isnull=True, user_fkey=3).distinct('code'),
Obviously I don't want to have a hardcoded id. I need to get the value of the current user. Following the same logic used for filtering the main table I end up with this.
class MasterListFilter(django_filters.FilterSet):
**user = get_current_user()**
project = django_filters.ModelChoiceFilter(
label='Projects',
name='project_fkey',
queryset=Project.objects.filter(deleted__isnull=True, user_fkey=**user.id**).distinct('code')
)
However this is unreliable as sometimes it shows the correct list and sometimes it doesn't. For example if I login and it's not showing the list (ie it shows just '---------') and then I restart my apache2 service, it starts to work again, then at some point it drops out again. Clearly this is not a long term solution.
So how do I reliably get the current user into my filter.py so that I can use it to filter my drop down filter list.
Thanks in advance and happy coding.
EDIT:
So following Wiesion's suggestion I changed my code as suggested but I still get a None Type Error saying that user has no attribute ID. BAsically it seems I'm not getting the current user. So going back to the docs and trying to merge their suggestion with Wiesion (whose explanation makes total sense - Thanks Wiesion) I came up with the following:
def Projects(request):
if request is None:
return Project.objects.none()
return lambda req: Project.objects.filter(deleted__isnull=True, user_fkey=req.user.id)
class MasterListFilter(django_filters.FilterSet):
project = django_filters.ModelChoiceFilter(
label='Projects',
name='project_fkey',
queryset=Projects
)
class Meta:
model = Task
fields = ['project']
This kind of works in theory but gives me nothing in the drop down list because
if request is None:
is returning True and therefore giving me an empty list.
So...can anyone see where I'm going wrong which is preventing me from accessing the request? Clearly the second portion of code is working based on qs that is passed from my view so maybe I need to pass in something else too? My view.py code is below:
def masterlist(request, page='0'):
#Check to see if we have clicked a button inside the form
if request.method == 'POST':
return redirect ('tasks:tasklist')
else:
# Pre-filtering of user and Master = True etc is done in the MasterListFilter in filters.py
# Then we compile the list for Filtering by.
f = MasterListFilter(request.GET, queryset=Task.objects.all())
# Then we apply the complete list to the table, configure it and then render it.
mastertable = MasterTable(f.qs)
if int(page) > 0:
RequestConfig(request, paginate={'page': page, 'per_page': 10}).configure(mastertable)
else:
RequestConfig(request, paginate={'page': 1, 'per_page': 10}).configure(mastertable)
return render (request,'tasks/masterlist.html',{'mastertable': mastertable, 'filter': f})
Thanks.
From the docs
The queryset argument also supports callable behavior. If a callable
is passed, it will be invoked with Filterset.request as its only
argument. This allows you to easily filter by properties on the
request object without having to override the FilterSet.__init__.
This is not tested at all, but i think something along these lines this is what you need:
class MasterListFilter(django_filters.FilterSet):
project = django_filters.ModelChoiceFilter(
label='Projects',
name='project_fkey',
queryset=lambda req: Project.objects.filter(
deleted__isnull=True, user_fkey=req.user.id).distinct('code'),
)
class Meta:
model = Task
fields = ['project']
Also if it's depending from webserver restarts - did you check caching issues? (In case, django-debug-toolbar gives great insights about that)
EDIT
The unpredictable behaviour most probably happens because you are retrieving the user within the class MasterListFilter definition, so get_current_user() is executed at class loading time, not during an actual request and all subsequent calls to qs will retrieve that query. Generally everything request-related should never be in a class definition, but in a method/lambda. So a lambda which receives the request argument and creates the query only then should exactly cover what you need.
EDIT 2
Regarding your edit, the following code has some issues:
def Projects(request):
if request is None:
return Project.objects.none()
return lambda req: Project.objects.filter(deleted__isnull=True, user_fkey=req.user.id)
This either returns an empty object manager, or a callable - but the method Project itself is already a callable, so your ModelChoiceFilter will receive only an object manager when the request object is None, otherwise a lambda, but it is expecting to receive an object manager - it can't iterate over a lambda so it should give you some is not iterable error. So basically you could try:
def project_qs(request):
# you could add some logging here to see what the arguments look like
if not request or not 'user' in request:
return Project.objects.none()
return Project.objects.filter(deleted__isnull=True, user_fkey=request.user.id)
# ...
queryset=project_qs
# ...
As stated in the following thread, you have to pass the request to the filter instance in the view: Customize queryset in django-filter ModelChoiceFilter (select) and ModelMultipleChoiceFilter (multi-select) menus based on request
ex:
myFilter = ReportFilter(request.GET, request=request, queryset=reports)
In a Django app, I'm having a model Bet which contains a ManyToMany relation with the User model of Django:
class Bet(models.Model):
...
participants = models.ManyToManyField(User)
User should be able to start new bets using a form. Until now, bets have exactly two participants, one of which is the user who creates the bet himself. That means in the form for the new bet you have to chose exactly one participant. The bet creator is added as participant upon saving of the form data.
I'm using a ModelForm for my NewBetForm:
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
widgets = {
'participants': forms.Select()
}
def save(self, user):
... # save user as participant
Notice the redefined widget for the participants field which makes sure you can only choose one participant.
However, this gives me a validation error:
Enter a list of values.
I'm not really sure where this comes from. If I look at the POST data in the developer tools, it seems to be exactly the same as if I use the default widget and choose only one participant. However, it seems like the to_python() method of the ManyToManyField has its problems with this data. At least there is no User object created if I enable the Select widget.
I know I could work around this problem by excluding the participants field from the form and define it myself but it would be a lot nicer if the ModelForm's capacities could still be used (after all, it's only a widget change). Maybe I could manipulate the passed data in some way if I knew how.
Can anyone tell me what the problem is exactly and if there is a good way to solve it?
Thanks in advance!
Edit
As suggested in the comments: the (relevant) code of the view.
def new_bet(request):
if request.method == 'POST':
form = NewBetForm(request.POST)
if form.is_valid():
form.save(request.user)
... # success message and redirect
else:
form = NewBetForm()
return render(request, 'bets/new.html', {'form': form})
After digging in the Django code, I can answer my own question.
The problem is that Django's ModelForm maps ManyToManyFields in the model to ModelMultipleChoiceFields of the form. This kind of form field expects the widget object to return a sequence from its value_from_datadict() method. The default widget for ModelMultipleChoiceField (which is SelectMultiple) overrides value_from_datadict() to return a list from the user supplied data. But if I use the Select widget, the default value_from_datadict() method of the superclass is used, which simply returns a string. ModelMultipleChoiceField doesn't like that at all, hence the validation error.
To solutions I could think of:
Overriding the value_from_datadict() of Select either via inheritance or some class decorator.
Handling the m2m field manually by creating a new form field and adjusting the save() method of the ModelForm to save its data in the m2m relation.
The seconds solution seems to be less verbose, so that's what I will be going with.
I don't mean to revive a resolved question but I was working a solution like this and thought I would share my code to help others.
In j0ker's answer he lists two methods to get this to work. I used method 1. In which I borrowed the 'value_from_datadict' method from the SelectMultiple widget.
forms.py
from django.utils.datastructures import MultiValueDict, MergeDict
class M2MSelect(forms.Select):
def value_from_datadict(self, data, files, name):
if isinstance(data, (MultiValueDict, MergeDict)):
return data.getlist(name)
return data.get(name, None)
class WindowsSubnetForm(forms.ModelForm):
port_group = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=PortGroup.objects.all())
class Meta:
model = Subnet
The problem is that ManyToMany is the wrong data type for this relationship.
In a sense, the bet itself is the many-to-many relationship. It makes no sense to have the participants as a manytomanyfield. What you need is two ForeignKeys, both to User: one for the creator, one for the other user ('acceptor'?)
You can modify the submitted value before (during) validation in Form.clean_field_name. You could use this method to wrap the select's single value in a list.
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
widgets = {
'participants': forms.Select()
}
def save(self, user):
... # save user as participant
def clean_participants(self):
data = self.cleaned_data['participants']
return [data]
I'm actually just guessing what the value proivded by the select looks like, so this might need a bit of tweaking, but I think it will work.
Here are the docs.
Inspired by #Ryan Currah I found this to be working out of the box:
class M2MSelect(forms.SelectMultiple):
def render(self, name, value, attrs=None, choices=()):
rendered = super(M2MSelect, self).render(name, value=value, attrs=attrs, choices=choices)
return rendered.replace(u'multiple="multiple"', u'')
The first one of the many to many is displayed and when saved only the selected value is left.
I found an easyer way to do this inspired by #Ryan Currah:
You just have to override "allow_multiple_selected" attribut from SelectMultiple class
class M2MSelect(forms.SelectMultiple):
allow_multiple_selected = False
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
participants = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=User.objects.all())