Django: access generic UpdateView with slug instead of pk - python

The code for using the pk is the following:
views.py
class CreatorUpdate(LoginRequiredMixin, UpdateView):
model = Creator
fields = ['first_name', 'last_name', 'location', 'country']
success_url = reverse_lazy('index')
# these two methods only give access to the users own profile but not the others (prevents url hacking)
def user_passes_test(self, request):
if request.user.is_authenticated:
self.object = self.get_object()
return self.object.user == request.user
return False
def dispatch(self, request, *args, **kwargs):
if not self.user_passes_test(request):
return redirect_to_login(request.get_full_path())
return super(CreatorUpdate, self).dispatch(request, *args, **kwargs)
urls.py
path('creator/<int:pk>/update/', views.CreatorUpdate.as_view(), name='creator-update')
HTML snippet to call the URL:
{{ user.get_username }}
Now I would like to use a slug (username) instead of the pk to access the UpdateView. I can successfully pass the username to the URL:
{{ user.get_username }}
but seem not to be able to match it in urls.py
path('creator/<slug:slug>/update/', views.CreatorUpdate.as_view(), name='creator-update')
with the same logic. I also tried to set a SlugField in my Creator model like this:
from django.template.defaultfilters import slugify
class Creator(models.Model):
...
username = models.CharField(max_length=100)
slug = models.SlugField()
def save(self, *args, **kwargs):
self.slug = slugify(self.username)
super(Creator, self).save(*args, **kwargs)
...
And setting slug_field = 'slug' in the UpdateView also does not get it.
These modification result in a 404 error: raised by catalog.views.CreatorUpdate: No creator found matching the query
When adjusting the HTML part to {{ user.get_username }} I get a NoReverseMatcherror Reverse for 'creator-update' with arguments '('',)' not found.
What is the logic to use a slug to access a view?

The error is that the user object has no attribute slug. The Creator model has the slug field. Create a ListView of Creator Model then check if each creator object has a slug.

Correct models.py:
class Creator(models.Model):
...
slug = models.SlugField()
def save(self, *args, **kwargs):
self.slug = slugify(self.user)
super(Creator, self).save(*args, **kwargs)

Related

Creating a DetailView of a profile with a queryset of posts created by that user

I'm creating a twitter-like app and I'm stuck on creating a UserProfileView which is supposed to display a certain User's profile, along with a list of posts made by that user below. Though I can't really figure out a way to create a proper view for that.
I'm trying to use class based views for that, the one I'll be inheriting from is probably DetailView (for profile model) and something inside of that which retrieves a queryset of posts made by that user:
My profile model looks like this:
class Profile(models.Model):
user = models.OneToOneField(
User, on_delete=models.CASCADE, primary_key=True)
display_name = models.CharField(max_length=32)
profile_picture = models.ImageField(
default='assets/default.jpg', upload_to='profile_pictures')
slug = models.SlugField(max_length=150, default=user)
def get_absolute_url(self):
return reverse("profile", kwargs={"pk": self.pk})
Post model:
class Post(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
date_posted = models.DateField(auto_now_add=True)
content = models.TextField(max_length=280)
image = models.FileField(upload_to='post_images/', blank=True, null=True)
def __str__(self) -> str:
return f'Post by {self.author} on {self.date_posted} - {self.content[0:21]}'
def get_absolute_url(self):
return reverse("post-detail", kwargs={"pk": self.pk})
I've tried creating this method:
class UserProfileView(DetailView):
model = Profile
context_object_name = 'profile'
template_name = 'users/profile.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user_posts'] = Post.objects.filter(author=Profile.user)
return context
But this one sadly doesn't work, raising an error of
"TypeError: Field 'id' expected a number but got <django.db.models.fields.related_descriptors.ForwardOneToOneDescriptor object at 0x000001A5ACE80250>."
'ForwardOneToOneDescriptor' object has no attribute 'id' is returned if I replace the filter argument with author=Profile.user.id
I'm not sure whether it's a problem with the way I filtered Posts, or how I used get_context_data.
The object is stored as self.object, so you can filter with:
class UserProfileView(DetailView):
model = Profile
context_object_name = 'profile'
template_name = 'users/profile.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user_posts'] = Post.objects.filter(author_id=self.object.user_id)
return context
An alternative might be to use a ListView for the Posts instead, to make use of Django's pagination:
from django.shortcuts import get_object_or_404
from django.views.generic import ListView
class UserProfileView(ListView):
model = Post
context_object_name = 'posts'
template_name = 'users/profile.html'
paginate_by = 10
def get_queryset(self, *args, **kwargs):
return (
super()
.get_queryset(*args, **kwargs)
.filter(author__profile__slug=self.kwargs['slug'])
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['profile'] = get_object_or_404(Profile, slug=self.kwargs['slug'])
return context
Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.

Problem with mixing Detail View and Form Mixin django

I am trying to create a comment system for the blog portion of my app with Django. I have attempted to mix my detail view with the form mixin and I'm struggling a bit. When the form is submitted, it doesn't save and no error is present. If any of you can help I would greatly appreciate it.
Here is my View
class DetailPostView(FormMixin, DetailView):
model = Post
template_name = "blog/post_detail.html"
context_object_name = "posts"
form_class = CommentForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["form"] = CommentForm
return context
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def get_success_url(self):
return reverse("post-detail", kwargs={"pk": self.object.pk})
The model
class Comment(models.Model):
comment = models.ForeignKey(Post, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
content = models.TextField()
author = models.CharField(max_length=50)
created_on = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_on"]
def __str__(self):
return self.title
The reason that this happens is because you construct a new form that you pass to the context data, as a result, it will not render any errors, since you construct a form without validating the request data and render that form, you thus do not display the form that rejected the data in the first place.
But you do not need to do that. Django's FormMixin [Django-doc] already takes care of that. You thus should not override the .get_context_data(…) method [Django-doc].
Another problem is that you did not save your form, you can override a the form_valid method, or you can inherit from ModelFormMixin [Django-doc].
Finally you better first create the form, and then assign self.object, otherwise it will pass this as an instance to the form:
from django.views.generic.edit import ModelFormMixin
class DetailPostView(ModelFormMixin, DetailView):
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'posts'
form_class = CommentForm
# no get_context_data override
def post(self, request, *args, **kwargs):
# first construct the form to avoid using it as instance
form = self.get_form()
self.object = self.get_object()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def get_success_url(self):
return reverse('post-detail', kwargs={'pk': self.object.pk})

ModelMultipleChoiceField django and debug mode

i'm trying to implement a ModelMultipleChoiceField in my application, like that: Link
model.py
class Services(models.Model):
id = models.AutoField(primary_key=True)
type = models.CharField(max_length=300)
class Professionals_Services(models.Model):
professional = models.ForeignKey(User, on_delete=models.CASCADE)
service = models.ForeignKey(Services, on_delete=models.CASCADE)
form.py
class ProfileServicesUpdateForm(forms.ModelForm):
service = forms.ModelMultipleChoiceField(required=False, queryset=Services.objects.all())
class Meta:
model = Professionals_Services
fields = ['service']
def clean(self):
# this condition only if the POST data is cleaned, right?
cleaned_data = super(ProfileServicesUpdateForm, self).clean()
print(cleaned_data.get('service'))
view.py
class EditProfileServicesView(CreateView):
model = Professionals_Services
form_class = ProfileServicesUpdateForm
context_object_name = 'services'
template_name = 'accounts/edit-profile.html'
#method_decorator(login_required(login_url=reverse_lazy('professionals:login')))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(self.request, *args, **kwargs)
def post(self, request, *args, **kwargs):
form = self.form_class(data=request.POST)
if form.is_valid():
services = form.save(commit=False)
services.save()
html
<select class="ui search fluid dropdown" multiple="" name="service" id="id_service">
{% for service in services_list %}
<option value="{{ service.id }}">{{ service.type }}</option>
{% endfor %}
</select>
For development i'm using Pycham Professionals(latest version) with docker, when i run the application and i try to make a POST the answer is:
Cannot assign "<QuerySet [<Services: Services object (2)>, <Services: Services object (5)>, <Services: Services object (6)>, <Services: Services object (7)>]>": "Professionals_Services.service" must be a "Services" instance.
But if i run the application in debug mode and with a breakpoints on the if form.is_valid():
the application works fine
That's because the validate is equal to Unknown not in debug
you know how to fix?
Your service is a ForeignKey:
service = models.ForeignKey(Services, on_delete=models.CASCADE)
A ForeignKey means that you select a single element, not multiple ones. You use a ManyToManyField [Django-doc] to select multiple elements:
class Professionals_Services(models.Model):
professional = models.ForeignKey(User, on_delete=models.CASCADE)
service = models.ManyToManyField(Service)
You should also not override the post method, and you can make use of the LoginRequiredMixin [Django-doc] to ensure that the user is logged in:
from django.contrib.auth.mixins import LoginRequiredMixin
class EditProfileServicesView(LoginRequiredMixin, CreateView):
login_url = reverse_lazy('professionals:login')
model = Professionals_Services
form_class = ProfileServicesUpdateForm
context_object_name = 'services'
template_name = 'accounts/edit-profile.html'
def form_valid(self, form):
form.instance.user = self.request.user
return super().form_valid(form)
In your Form you should also return the cleaned data:
class ProfileServicesUpdateForm(forms.ModelForm):
service = forms.ModelMultipleChoiceField(required=False, queryset=Services.objects.all())
class Meta:
model = Professionals_Services
fields = ['service']
def clean(self):
# this condition only if the POST data is cleaned, right?
cleaned_data = super(ProfileServicesUpdateForm, self).clean()
print(cleaned_data.get('service'))
return cleaned_data
Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.
Note: Models in Django are written in PerlCase, not snake_case,
so you might want to rename the model from Professionals_Services to ProfessionalService.
Note: normally a Django model is given a singular name, so Services instead of Service.

Reverse to the same page with pk as slug field after submit. Error:Reverse with no arguments not found

following scenario: I do have a edit profile page which pulls the data from db so the user can change them. After submitting everything gets stored in the db but the reverse doesn't seem to work. Basically I don't know how to give the pk along to be able to call the same site with reverse.
views.py
class EditUserProfileView(UpdateView):
model = UserProfileInfo
form_class = UserProfileForm
template_name = "accounts/user_profile.html"
def get_object(self, *args, **kwargs):
user = get_object_or_404(User, pk=self.kwargs['pk'])
return user.userprofileinfo
def get_success_url(self, *args, **kwargs):
if 'pk' in self.kwargs:
pk = self.kwargs['pk']
else:
slug = 'main'
return reverse("accounts:edit")
urls.py
app_name = 'accounts'
urlpatterns=[
url(r'^edit/(?P<pk>\d+)/$', views.EditUserProfileView.as_view(), name="edit-user-profile"),
]
html from where I call the edit-user-profile
<li class="nav-item nav-link">Hello {{user.first_name}}</li>
Cheers
Both reverse and reverse_lazy have the keyword arguments args and kwargsthat you can pass a list or dict of view arguments:
return reverse("accounts:edit-user-profile", kwargs={'pk': pk})

Django: Extending User Model - Inline User fields in UserProfile

Is there a way to display User fields under a form that adds/edits a UserProfile model? I am extending default Django User model like this:
class UserProfile(models.Model):
user = models.OneToOneField(User, unique=True)
about = models.TextField(blank=True)
I know that it is possible to make a:
class UserProfileInlineAdmin(admin.TabularInline):
and then inline this in User ModelAdmin but I want to achieve the opposite effect, something like inverse inlining, displaying the fields of the model pointed by the OneToOne Relationship (User) in the page of the model defining the relationship (UserProfile). I don't care if it would be in the admin or in a custom view/template. I just need to know how to achieve this.
I've been struggling with ModelForms and Formsets, I know the answer is somewhere there, but my little experience in Django doesn't allow me to come up with the solution yet. A little example would be really helpful!
This has been brought up before.
Here's a blog post with what I think is my favorite solution. The gist is to use two ModelForms, and render them into a single <form> tag in the template making use of the prefix kwarg:
http://collingrady.wordpress.com/2008/02/18/editing-multiple-objects-in-django-with-newforms/
Here's another method which I like a bit less, but is also valid. They use two separate <form>s on the page, with different actions and two submit buttons:
Proper way to handle multiple forms on one page in Django
This one talks more specifically about Users and UserProfiles:
How to create a UserProfile form in Django with first_name, last_name modifications?
Update
Here is what I ended up with
# models.py
class UserProfile(models.Model):
favorite_color = models.CharField(max_length=30)
user = models.OneToOneField(User)
# forms.py
class UserProfileForm(forms.ModelForm):
class Meta:
model = UserProfile
# we fill the 'user' value in UserCreateView.form_valid
exclude = ('user',)
# views.py
from django.contrib.auth.forms import UserCreationForm
class UserCreateView(FormView):
# url to redirect to after successful form submission
success_url = reverse_lazy('user_list')
template_name = "userform.html"
def get_context_data(self, *args, **kwargs):
data = super(UserCreateView, self).get_context_data(*args, **kwargs)
data['userform'] = self.get_form(UserCreationForm, 'user')
data['userprofileform'] = self.get_form(UserProfileForm, 'userprofile')
return data
def post(self, request, *args, **kwargs):
forms = dict((
('userform', self.get_form(UserCreationForm, 'user')),
('userprofileform', self.get_form(UserProfileForm, 'userprofile')),
))
if all([f.is_valid() for f in forms.values()]):
return self.form_valid(forms)
else:
return self.form_invalid(forms)
def get_form(self, form_class, prefix):
return form_class(**self.get_form_kwargs(prefix))
def get_form_kwargs(self, prefix):
kwargs = super(UserCreateView, self).get_form_kwargs()
kwargs.update({'prefix': prefix})
return kwargs
def form_valid(self, forms):
user = forms['userform'].save()
userprofile = forms['userprofileform'].save(commit=False)
userprofile.user_id = user.id
userprofile.save()
return HttpResponseRedirect(self.get_success_url())
def get(self, request, *args, **kwargs):
return self.render_to_response(self.get_context_data())
# userform.html
<form action="" method="POST" class="form">
{% csrf_token %}
{{ userform.as_p }}
{{ userprofileform.as_p }}
<button type="submit">Submit</button>
</form>
# urls.py
...
url(r'^create/$', UserCreateView.as_view(), name='user_create'),
...

Categories