Django - Add entry to ManyToMany field through class based view - python

I want to add / remove members from a Team model. Members are specified as a ManyToManyField. I use django-rules to specify permissions, so team owners should be able to add/remove members.
# models.py
from django.db import models
from rules.contrib.models import RulesModel
from django.conf import settings
class Team(RulesModel):
name = models.CharField(max_length=80)
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
help_text="Owner can view, change or delete this team.",
related_name="team_owner",
)
members = models.ManyToManyField(
settings.AUTH_USER_MODEL, blank=True, related_name="team_members"
)
The permissions are specified as following:
import rules
#rules.predicate
def is_team_owner(user, obj):
return obj.owner == user
rules.add_perm("teamapp.change_team", is_team_owner)
I've specified some generic views (CreateView, DetailView, UpdateView and DeleteView) to manage the Team. Now I want two separate views to add and remove members on the same.
# views.py
from django.views.generic import (
CreateView,
DetailView,
UpdateView,
ListView,
DeleteView,
)
from rules.contrib.views import PermissionRequiredMixin
from django.contrib.auth import get_user_model
from .models import Team
class TeamMemberAddView(PermissionRequiredMixin, UpdateView):
model = Team
permission_required = "teamapp.change_team"
raise_exception = True
fields = ["members"]
def form_valid(self, form):
user = get_user_model()
new_member = user.objects.get(pk=1)
self.object.members.add(new_member)
return super(TeamMemberAddView, self).form_valid(form)
Which generic view can I use to add / remove members? Which approach is recommended here? I wanted 1 dedicated view to select an existing User to be added, and some links on the list view to delete members. My approach fails, because it does not add members, it only updates to the last User selected. So the ManyToMany table only contains one record.

TL;DR: replace the last line of form_valid by return HttpResponseRedirect(self.get_success_url())
It's important to understand how form_valid of UpdateView works. I recommend to visualize the methods on ccbv.co.uk.
From ModelFormMixin:
If the form is valid, save the associated model.
def form_valid(self, form):
"""If the form is valid, save the associated model."""
self.object = form.save()
return super().form_valid(form)
It means that the object will be saved with the data submitted by the form. UpdateView will restrict the changes to the fields variable:
fields = ["members"]
From FormMixin:
If the form is valid, redirect to the supplied URL.
def form_valid(self, form):
"""If the form is valid, redirect to the supplied URL."""
return HttpResponseRedirect(self.get_success_url())
For your concrete case (add a many-to-many relationship), you need to bypass the model saving from ModelFormMixin by simply returning the supplied URL after adding the relationship (last line changed):
def form_valid(self, form):
user = get_user_model()
new_member = user.objects.get(pk=1)
self.object.members.add(new_member)
return HttpResponseRedirect(self.get_success_url())
Side note: your form seems to provide the member object you want to add, so you could use this instead of including it in the url. Try:
def form_valid(self, form):
for member in form.cleaned_data['members'].all():
self.object.members.add(member.id)
return HttpResponseRedirect(self.get_success_url())

Related

If i am trying to create a diray app using django, how can i get it show show me different diary entries based on the current logged in user?

im currently in the process of building a diary app in django. I have created multiple users (e.g User A and User B). However, when user A logs in, user A can see User B's entries.
How can i lock it down, so only User B can see User B's entries and when User A logs in, User A can have a personal entry view? (do i need to create a different view?)
views.py for my diary app:
from django.urls import reverse_lazy
from django.views.generic import (
ListView,
DetailView,
CreateView,
UpdateView,
DeleteView,
)
from .models import Entry
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
# Create your views here.
class ELV(ListView):
model = Entry
queryset = Entry.objects.all().order_by("-date_created") #takes all the entries and orders it by date
template_name = 'entries\entry_list.html'
class EDV(DetailView):
model = Entry
template_name = 'entries\entry_detail.html'
class ECV(CreateView):
model = Entry
fields = ["title", "content"]
success_url = reverse_lazy("entry-list")
template_name = 'entries\entry_form.html'
class EUV(UpdateView):
model = Entry
fields = ["title", "content"]
template_name = 'entries\entry_update_form.html'
def get_success_url(self):
return reverse_lazy(
"entry-detail",
kwargs={"pk": self.object.pk}
)
class EntryDeleteView(DeleteView):
model = Entry
success_url = reverse_lazy("entry-list")
template_name = 'entries\entry_delete.html'
Does it have anything to do with user sessions? - i'm not sure, please help!
Yes, you can do this easily. But before this you have to add an extra field in your Entry model.
# models.py
from django.contrib.auth import get_user_model
User = get_user_model()
class Entry(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='entries', default=None)
# your other remaining fields
and make sure to run makemigrations and migrate commands after adding the new field. And in your views.py file, add a get_queryset(self)
# Create your views here.
class ELV(ListView):
model = Entry
template_name = 'entries\entry_list.html'
def get_queryset(self):
return Entry.objects.filter(user=self.request.user).order_by("-date_created")
PS: And it's a good idea to add a LoginRequiredMixin to make sure only the authenticated got the access you ListView
from django.contrib.auth.mixins import LoginRequiredMixin
class ELV(LoginRequiredMixin, ListView):
# ...
Here is docs link
I also needed to add this bit to my entry create view (ECV), once done, worked like a charm!
def form_valid(self, form):
form.instance.user = self.request.user
return super(ECV, self).form_valid(form)

Django form_valid method

In my form's view I wonder what if I don't add product_form.save method in the code below and what if I add that:
def form_valid(self, form):
product_form = form.save(commit=False)
product_form.user = self.request.user
product_form.save() # what if I delete this?
return super().form_valid(form)
In what view do you use this? If it is a CreateView [Django-doc] or UpdateView [Django-doc], it is fine. For a (simple) FormView [Django-doc], it is not since a FormView does not save the form.
You however do not need to save the form with commit=False to retrieve the instance. A simple one-liner to set the user is:
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView
class MyCreateView(LoginRequiredMixin, CreateView):
# …
def form_valid(self, form):
form.instance.user = self.request.user
return super().form_valid(form)
The name product_form in product_form = form.save(commit=False) is also somewhat misleading: the .save() method does not return a Form, but a Product object, hence it is a Product. But you do not need to save that product in the database, since a CreateView and UpdateView will call the .save() function of the form to save the model together with many-to-many relations that are specified in the form.

Django CBV Detailview

Hello Everybody excuse my english....
I am facing a problem with django.
I need to restrict object so only their owners can print it.
Model.py
class Post(models.Model):
title = models.CharField(max_length=50, blank=False)
prenom = models.CharField(max_length=255, blank=False)
user = models.ForeignKey(User, null=False)
View.py
class detailpost(DetailView):
model = Post
template_name = 'detail-post.html'
context_object_name = 'post'
url.py
url(r'detail-post/(?P<pk>[-\d]+)$', views.detailpost.as_view(), name='detailpost'),
This works properly but the problem is that every users can access to the post of another user (http://localhost:8000/detail-post/1). So my question is how can i do some stuff befor rendering the page and see if the post belongs to the current user if yes we print it else we redirect the user to another page.
You can use the LoginRequiredMixin (new in Django 1.9) to make sure that only logged in users can access the view.
Then override the get_queryset method, and filter the queryset so that it only includes posts by the logged-in user.
from django.contrib.auth.mixins import LoginRequiredMixin
class DetailPost(LoginRequiredMixin, DetailView):
model = Post
template_name = 'detail-post.html'
context_object_name = 'post'
def get_queryset(self):
queryset = super(DetailPost, self).get_queryset()
return queryset.filter(owner=self.request.user)
If the user views a post that does not belong to them, they will see a 404 page. If you must redirect the user instead of showing a 404, then you'll have to take a different approach.
Note that I have renamed your class DetailPost (CamelCase is recommended for classes in Django. You'll have to update your urls.py as well.
You can override get() or post() method in your view class.
from django.shortcuts import redirect
class detailpost(DetailView):
model = Post
template_name = 'detail-post.html'
context_object_name = 'post'
def get(self, request, *args, **kwargs):
self.post = Post.objects.get(pk=self.kwargs['pk'])
if self.post.user != request.user or not request.user.is_superuser:
return redirect('login')
else:
return super(detailpost, self).get(request, *args, **kwargs)
You should override 'get()' method in your 'detailpost' class, so that it would be something like below:
def get(self, request, *args, **kwargs):
queryset = self.model._default_manager.filter(user=request.user)
self.object = self.get_object(queryset)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
It seems like it is the only way to pass User from Request instance to filter queryset.
I did not find that DetailView uses self.request

Django ModelForms: Seeding a FK related field in the form

I have a Workshop Django app. Each Workshop can have multiple attendees via a related WorkshopAttendee model (simplified below). I am using Django's ModelForm in a Class Based View, and in forms.py I am using crispy-forms:
models.py (relevant parts)
class Workshop(models.Model):
title = models.CharField(max_length=100)
information = models.TextField()
location = models.TextField()
class WorkshopAttendee(models.Model):
workshop = models.ForeignKey(Workshop)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
views.py (relevant parts)
from django.views.generic.edit import FormView
from workshop.forms import WorkshopAttendeeForm
class WorkshopAttendeeFormView(FormView):
def form_valid(self, form):
# Clean the data
form_data = form.cleaned_data
form.save(commit=False)
return super(WorkshopAttendeeFormView, self).form_valid(form)
forms.py
from django.forms import ModelForm
from workshop.models import WorkshopAttendee
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, HTML, Field, \
Fieldset, Button, Hidden, Submit, Reset
from crispy_forms.bootstrap import FormActions
# Create the form class
class WorkshopAttendeeForm(ModelForm):
def __init__(self, *args, **kwargs):
# Crispy form Layouts, Fieldsets, etc
super(WorkshopAttendeeForm, self).__init__(*args, **kwargs)
class Meta:
model = WorkshopAttendee
urls.py (relevant parts)
urlpatterns = patterns('',
url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[\w\-.]+)/(?P<action>[\w\-.]+)/$',
WorkshopAttendeeFormView.as_view(
form_class=WorkshopAttendeeForm,
success_url="success/",
template_name='workshop/workshop_registration.html',
), name='workshop-attendee-register'
),
url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[\w\-.]+)/$',
WorkshopDetailView.as_view(
context_object_name='workshop_detail',
queryset=Workshop.objects.select_related(),
template_name='workshop/workshop_detail.html',
), name='workshop-detail'
),
)
My question is, how can I seed the form with the workshop_id (i.e. the FK relation) in a hidden form field? Obviously because the form hasn't been submitted yet, there is no FK relation yet. But in the URL I have the kwarg of Workshop slug. I can hard-code the workshop_id as a Hidden() crispy-forms field on a per workshop basis, but this is totally unDRY. Any ideas? I don't think I can use the select_related() or prefetch_related() methods on the model in urls.py, so maybe I have to somehow get both models into the form view somehow?
I don't feel like this is an edge-case scenario, and I am sure someone else has had a similar app workflow.
Thanks in advance.
UPDATE
After further research it appears that I can do this, using Django Formsets. Not figured out exactly how yet..... hints welcome.
You don't need to pass the PK - you got it already in your URL as the slug.
So let's say this is your URL: http://example.com/workshops/awesome-workshop-slug/sign_in/
Your urls.py should look like this:
url(r'^workshop/(?P<workshop_slug>\w+)/sign_in/$',
# ../workshops/awesome-workshop-slug/sign_in/
view = 'workshop_signin',
name = 'workshop-signin',
),
Ok, in your views.py you're able to do this:
#login_required
def workshop_signin(request, workshop_slug, template='workshop/sign_in.html'):
"""Register user to workshop."""
form = WorkshopForm()
workshop = Workshop.objects.filter(slug=workshop_slug)[0]
if request.method == 'POST':
form = WorkshopForm(request.POST, instance=workshop)
if form.is_valid():
messages.info(request, 'Yay!')
kwargs = {
'workshop_form': form,
}
return render_to_response(template, kwargs, context_instance=RequestContext(request))
*untested quick and dirty code
Turns out I was overly complicating this.
All I needed to do was modify the form_valid method for Django's GCBV FormView to this:
workshop/views.py
from django.views.generic.edit import FormView
from workshop.models import Workshop, WorkshopAttendee
from workshop.forms import WorkshopAttendeeForm
from django.http import HttpResponseRedirect
class WorkshopAttendeeFormView(FormView):
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.workshop = Workshop.objects.filter(slug=self.kwargs['slug'])[0]
self.object.save()
return HttpResponseRedirect(self.get_success_url())
Which basically does not save the form on submit, but instead overrides it and first updates the object it is about to save (the WorkshopAttendee object) with the relevant Workshop (based on the Workshop slug field, which is unique), then saves the updated object (self.object.save) and kicks me to the success url.
Big thanks to #init3 for his helpful pointers. Much appreciated.

Django Login Required to view

I am building a small application which needs user profiles, I've used the build in user system from Django. But I have a problem regarding that even if you are not logged in you can still view the profile also another thing is that each user should only see his profile not others I need some tips on this
views.py
class UserProfileDetailView(DetailView):
model = get_user_model()
slug_field = "username"
template_name = "user_detail.html"
def get_object(self, queryset=None):
user = super(UserProfileDetailView, self).get_object(queryset)
UserProfile.objects.get_or_create(user=user)
return user
class UserProfileEditView(UpdateView):
model = UserProfile
form_class = UserProfileForm
template_name = "edit_profile.html"
def get_object(self, queryset=None):
return UserProfile.objects.get_or_create(user=self.request.user)[0]
def get_success_url(self):
return reverse("profile", kwargs={"slug": self.request.user})
Since you are using the Class Based Generic View, you need to add decorator #login_required in your urls.py
#urls.py
from django.contrib.auth.decorators import login_required
from app_name import views
url(r'^test/$', login_required(views.UserProfileDetailView.as_view()), name='test'),
Have you checked out the login_required decorator? Docs are here.
Since it seems you are using Class Based Views, you need to decorate in the urlconf, see here for more info.
At this moment you can add LoginRequiredMixin for your custom view.
Example:
class MyListView(LoginRequiredMixin, ListView): # LoginRequiredMixin MUST BE FIRST
pass
Doc: https://docs.djangoproject.com/en/4.1/topics/auth/default/#the-loginrequiredmixin-mixin
The below is what you should typically do
#login_required
def my_view(request, uid):
# uid = user id taken from profile url
me = User.objects.get(pk=uid)
if me != request.user:
raise Http404

Categories