Paginating Django View with Foreign Key Data - django

I'm trying to build a simple Django gallery that allows photos to be added to an album. Everything thus far is working fine (upload photo, add to album, paginated all photos listing, photo details/display pages, etc), until I try to display the 'album' pages. I can get the page to render the the album and all of the associated photos, but if I try to paginate the album, things start to get weird.
Here are my models:
# models.py
class Albums(models.Model):
id = models.AutoField(primary_key=True, unique=True)
name = models.CharField(max_length=500)
slug = models.SlugField(max_length=500, unique=True)
description = models.TextField(blank=True, null=True)
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
return super().save(*args, **kwargs)
class Photos(models.Model):
id = models.AutoField(primary_key=True, unique=True)
album = models.ForeignKey(
Albums, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Album"
)
photo = models.ImageField(upload_to="photos/")
slug = models.CharField(max_length=16, null=False, editable=False) # I know I'm not using a SlugField here; that's on purpose
title = models.CharField(max_length=500)
description = models.TextField(blank=True, null=True)
upload_date = models.DateTimeField(
default=timezone.now, verbose_name="Date uploaded"
)
def __str__(self):
return self.title
def save(self, *args, **kwargs):
[...] # stuff to get the image's EXIF data and use that to set the slug
self.slug = str(datetime.strftime(img_date, "%Y%m%d%H%M%S"))
[...] # plus a bunch of other stuff that happens on save, not important here
super(Photos, self).save()
def delete(self, *args, **kwargs):
[...] # a bunch of stuff that happens on delete, not important here
super(Photos, self).delete()
And the view that's making me miserable:
# views.py
class PhotoAlbum(DetailView):
context_object_name = "album"
model = Albums
slug_field = "slug"
template_name = "page.html"
def get_photos(self, **kwargs):
# get the integer id of the album
a = Albums.objects.filter(slug=self.kwargs["slug"])
a_id = a.values()[0]["id"]
# get the list go photos in that album
photos = Photos.objects.all().order_by("-capture_date").filter(album_id=a_id)
return photos
def get_context_data(self, **kwargs):
context = super(PhotoAlbum, self).get_context_data(**kwargs)
context["photos"] = self.get_photos()
return context
These work in a basic template...
# album.html
<h2>{{ album.name }}</h2>
<p>{{ album.description }}</p>
<ul>
{% for photo in photos %}
<li>{{ photo.title }}</li>
{% endfor %}
</ul>
...but if I try to paginate the photos by changing...
photos = Photos.objects.all().order_by("-capture_date").filter(album_id=a_id)
...to...
photos = Paginator(Photos.objects.all().order_by("-capture_date").filter(album_id=a_id), 10)
...I get nothing.
In searching around, almost all of the questions I've seen relating to pagination are for DRF, so have kind of hit a wall here. I had all sorts of trouble getting the photo-to-album relationship to display (the get_photos() function), so am guessing part of my problem lies with that. Any suggestions on how to get this working?

Paginator will give you a paginator object, not the objects you are paginating. To get the objects, you first need to specify the page, so for example:
paginator = Paginator(Photos.objects.all().order_by("-capture_date").filter(album_id=a_id), 10)
photos = paginator.page(1)
photos here will then contain Photos objects that are on page 1.

Based on Brian Destura's comment, I went back and started again using a ListView and after re-reading the Django docs and a bit of messing about, arrived at this solution for my view which, based on a little testing, works for me:
class AlbumDetail(ListView):
context_object_name = "photos"
template_name = "page.html"
paginate_by = 6
def get_queryset(self):
a = Albums.objects.filter(slug=self.kwargs["slug"])
a_id = a.values()[0]["id"]
self.albums = get_object_or_404(Albums, slug=self.kwargs["slug"])
return Photos.objects.filter(album_id=a_id).order_by("-capture_date")
def get_context_data(self, **kwargs):
context = super(AlbumDetail, self).get_context_data(**kwargs)
context["album"] = self.albums
return context

viwes.py
class PhotoAlbum(ListView):
context_object_name = "album"
model = Albums
slug_field = "slug"
template_name = "page.html"
paginate_by = 5
page.html
This is pagination logic
{%if is_paginated %}
{%if page_obj.has_previous %}
<a style="margin-left: 20px; padding: 10px 20px;" class=" btn btn-primary" href="?page={{page_obj.previous_page_number}}"> πŸ’€{{page_obj.num_pages}} previous</a>
{% endif %}
{%if page_obj.has_next %}
{% if page_obj.num_pages == 1 %}
<a style="margin-left: 930px; padding: 10px 28px;" class=" btn btn-primary" href="?page={{page_obj.next_page_number}}">next πŸ’‚</a>
{% else %}
<a style="margin-left: 880px; padding: 10px 28px;" class=" btn btn-primary" href="?page={{page_obj.next_page_number}}">next πŸ’‚</a>
{% endif %}
{% endif %}
{% endif %}

Related

How to do a POST request in a DetailView

im following a tutorial for the project but it does it on function views and im trying to do it on class based views
i get a ( The view blog.views.PostDetailView didn't return an HttpResponse object. It returned None instead.) error but thats not my concern now ... because the data(new comments) arent getting saved
so how can i save them with the post request and redirect to the same page of the DetailView
my urls
app_name = 'blog'
urlpatterns = [
path('', views.PostListView.as_view(), name='blog-home'),
path('blog/<slug:slug>/', views.PostDetailView.as_view() , name='post-detail'),
]
my models
class Post(models.Model):
options = (
('draft', 'Draft'),
('published', 'Published')
)
title = models.CharField(max_length=250)
slug = models.SlugField(max_length=250, unique_for_date='publish_date')
publish_date = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts')
content = models.TextField()
status = models.CharField(max_length=10, choices=options, default='draft')
class Meta:
ordering = ('-publish_date',)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post-detail', kwargs={'slug': self.slug})
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
publish_date = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ('-publish_date',)
def __str__(self):
return f'Comment By {self.author}/{self.post}'
my forms
class AddCommentForm(forms.ModelForm):
content = forms.CharField(label ="", widget = forms.Textarea(
attrs ={
'class':'form-control',
'placeholder':'Comment here !',
'rows':4,
'cols':50
}))
class Meta:
model = Comment
fields =['content']
my views
class PostDetailView( DetailView):
model = Post
context_object_name = 'post'
template_name='blog/post_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
comments = Comment.objects.filter(post=self.object)
context['comments'] = comments
context['form'] = AddCommentForm()
return context
def post(self, request, *args, **kwargs):
pass
def form_valid(self, form):
form.instance.author = self.post.author
user_comment.post = self.post
user_comment.save()
return super().form_valid(form)
html
<form method="POST">
<div class="col-12">
<hr>
{% with comments.count as total_comments %}
<legend class="border-bottom mb-4">{{total_comments }} comment{{total_comments|pluralize }}</legend>
{% endwith %}
{% for c in comments%}
<div class ="col-md-12 mb-1rem" >
<p class="mb-0"><strong>{{c.author}}:</strong> {{c.content}}</p>
<small class="text-muted">{{ c.publish_date|date:'f A, Y'}}</small>
</div>
<br>
{% endfor %}
</div>
<hr>
{% csrf_token %}
<fieldset class="form-group">
<legend class="border-bottom mb-4">New Comment</legend>
{{ form|crispy }}
</fieldset>
<div class="form-group">
<button class="btn btn-dark btn-lg mt-1" type="submit">Publish</button>
</div>
</form>
The DetailView has no form_valid method.
It just shows the objects of the model.
Form processing is in the GenericEdit View class.
There are many ways, but...
In this code, you can create a GenericEdit View(CreateView or UpdateView ) url, process the form there(form_valid), and then
success_url = reverse_lazy('form:detail') # in GenericEdit View class
return the template.
To sum up,
add path in urls.py
like this... πŸ‘‡
path('blog/<slug:slug>/update', views.PostUpdateView.as_view(), name='post-update'),
add update or create url in form action.
ex. ☞ action="{% url 'blog:post-update' %}"
make GenericEdit View class☞ views.PostUpdateView
ps.You can also use forms.py
"""
When you use Class Base View in django, it is recommended to refer to this site.
https://ccbv.co.uk/
and,
DetailView refer is here
https://ccbv.co.uk/projects/Django/3.0/django.views.generic.detail/DetailView/
"""
I know it's been a long time, but I think someone might need the answer in future.
I'm using django 3.2.16.
in your post method inside DetailView:
Update your post method to:
def post(self, request, *args, **kwargs):
# Get the current pk from the method dictionary
pk = kwargs.get('pk')
if request.method == 'POST':
# Get the current object
obj = self.model.objects.get(id=pk)
# Alter the field Value
some_value = request.POST.get('some_value_from_html_input')
obj.field = some_value
# Save the object
obj.save()
# Redirect to you current View after update
return redirect(current_details_view, pk=pk)

Wagtail: How can I display Categories from Child Pages on a List Page

I'm trying to get the hang of Wagtail and have a page that lists all of the posts on the site.
I want to display a category tab on the card that holds the info for each post.
The text and title are pulling correctly, but I can't get the category data.
I have this code, but it pulls every category on the site, not for each post.
{% for cat in categories %}
<li class="d-inline-flex">
<a href="{{self.get_parent.url}}?category={{cat.slug}}">
<span class="badge badge-pill badge-primary pt-2 pb-2">{{cat.name}}</span>
</a>
</li>
{% endfor %}
class LessonCategory(models.Model):
""" Set Up Lesson Category In Wagtail Admin """
name = models.CharField(max_length=100)
slug = models.SlugField(
unique=True,
max_length=40,
verbose_name="slug",
allow_unicode=True,
help_text="Add a category slug",
)
panels = [
FieldPanel('name'),
FieldPanel('slug'),
]
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
ordering = ["name"]
def __str__(self):
return self.name
Can anyone point me in the right direction?
Edit: Page List Model
class LessonListPage(RoutablePageMixin, Page):
""" Lists all the lessons on one page """
template = "lesson/lesson_list_page.html"
custom_title = models.CharField(
max_length=100,
blank=False,
null=False,
help_text='Overwrites the default title',
)
content_panels = Page.content_panels + [
FieldPanel("custom_title"),
]
def get_context(self, request, *args, **kwargs):
"""Adding custom stuff to our context"""
context = super().get_context(request, *args, **kwargs)
context["posts"] = LessonDetailPage.objects.live().public()
#Change 'public' to adjust which posts are displayed
context["categories"] = LessonCategory.objects.all()
return context
#route(r'^latest/$')
def latest_lesson_posts(self, request, *args, **kwargs):
context = self.get_context(request, *args, **kwargs)
# #todo Adjust the number of lessons that are displayed at one time
context["latest_lessons"] = LessonDetailPage.objects.live().public()[:10]
return render(request, "lesson/latest_lessons.html", context)
{% for cat in post.categories.all %}
<li class="d-inline-flex">
<a href="{{cat.get_parent.url}}?category={{cat.slug}}">
<span class="badge badge-pill badge-primary pt-2 pb-2">
{{cat.name}}
</span>
</a>
</li>
{% endfor %}
Changing 'categories' --> 'post.categories.all' did the trick.

Filtering foreign Key content in django templated using if statement

I have two models with one being a foreign key to another. A user can only submit an answer. am trying to use the if statement to check if an answer exit for a user then the submit answer button should change the update button the template.
class Assignment(models.Model):
title = models.CharField(max_length=120)
slug = models.SlugField(max_length=500)
course = models.ForeignKey(Course, on_delete=models.CASCADE)
class_or_level = models.ForeignKey(StudentClass, on_delete=models.CASCADE)
teacher = models.ForeignKey(Teacher, on_delete=models.CASCADE)
file = models.FileField(upload_to='assignment', blank=True, null=True)
Text = models.TextField()
date_added = models.DateTimeField(auto_now_add=True)
date_expire = models.DateTimeField()
class Answer(models.Model):
slug = models.SlugField(max_length=500)
assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE)
student = models.ForeignKey(User, on_delete=models.CASCADE)
file = models.FileField(upload_to='assignment')
date_added = models.DateTimeField(auto_now_add=True)
My View
class AssignmentView(LoginRequiredMixin, ListView):
template_name = 'assignment.html'
context_object_name = 'all_couses'
now = timezone.now()
queryset = Course.objects.all()
def get_context_data(self, **kwargs):
now = timezone.now()
context = super(AssignmentView, self).get_context_data(**kwargs)
context.update({
'assignment_list': Assignment.objects.filter(class_or_level=self.request.user.student.class_or_level, date_expire__gte=now).order_by('-date_expire'),
})
return context
this the template> What the users submitted answer to show if he does submit one else show the form like to submit answer
{% for assignment in assignment_list %}
<h4>{{ assignment.title|truncatewords:12 }}</h4>
{% if assignment.answer %}
{{ assignment.answer.file }}
<button> Update Answer</button>
{% else %}
<button> Summit Answer</button>
{% endif %}
{% endfor %}
You can change your view or create a template tag to identify if an assignment has a response of a specific user, I decided to change the view and add a new variable to the assignment object that will have the information of if the user who requested the page has an answer to that assignment:
class AssignmentView(LoginRequiredMixin, ListView):
template_name = 'assignment.html'
context_object_name = 'all_couses'
now = timezone.now()
queryset = Course.objects.all()
def get_context_data(self, **kwargs):
now = timezone.now()
context = super(AssignmentView, self).get_context_data(**kwargs)
assignment_list = Assignment.objects.filter(
class_or_level=self.request.user.student.class_or_level,
date_expire__gte=now
).order_by('-date_expire')
for assignment in assignment_list:
try:
assignment.student_answer = Answer.objects.get(
assignment=assignment,
student=self.request.user
)
except Answer.DoesNotExist:
pass
context.update({
'assignment_list': assignment_list,
})
return context
Then, inside of your template you can do the following:
{% for assignment in assignment_list %}
<h4>{{ assignment.title|truncatewords:12 }}</h4>
{% if assignment.student_answer %}
{{ assignment.student_answer.file }}
<button> Update Answer</button>
{% else %}
<button> Summit Answer</button>
{% endif %}
{% endfor %}
By doing that you'll achieve what you're looking for.

Repeater objects appearing on all detail views - django

I have two models with a foreign key, one to many relationship so that I can get a repeater model (images) in the admin. The image repeater works fine, my problem is that the images for the field - video_stills saved on one of the film post/objects repeats across all film posts.
Here is my code:
model.py
from __future__ import unicode_literals
from django.db import models
from embed_video.fields import EmbedVideoField
from sorl.thumbnail import ImageField
class Timestamp(models.Model):
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
# Film Model
class Film(Timestamp):
title = models.CharField(max_length=255)
order = models.PositiveIntegerField(default=0, blank=False, null=False)
meta_description = models.TextField('Meta Description', max_length=170,
help_text='Content for description meta tag - Max 170 Characters')
slug = models.SlugField(unique=True)
image = models.ImageField(upload_to='thumb')
video = EmbedVideoField(blank=True)
director = models.CharField(max_length=255,blank=True)
cinematographer = models.CharField(max_length=255,blank=True)
producer = models.CharField(max_length=255,blank=True)
publish = models.BooleanField(default=False)
date_published = models.DateTimeField()
# thumb for admin
def image_thumb(self):
return '<img src="%s" height="200" width="300"/>' % (self.image.url)
image_thumb.short_description = 'Image'
image_thumb.allow_tags = True
# override the admin name + add ordering
class Meta(object):
ordering = ('order',)
verbose_name_plural = "Film Projects"
def __unicode__(self):
return self.title
# helper method
def get_absolute_url(self):
return "/film/%s/" % self.slug
def save(self, *args, **kwargs):
super(Film, self).save(*args, **kwargs)
# Film Stills Image Model
class FilmStillsImage(models.Model):
video_stills = models.FileField(upload_to = 'images')
film = models.ForeignKey(Film)
class Meta(object):
verbose_name_plural = "Film Stills"
views.py
# film single
def film_detail(request, slug):
film = get_object_or_404(Film, slug=slug)
# get all repeater images
film_grabs = FilmStillsImage.objects.all()
try:
next_film = film.get_next_by_date_published()
except Film.DoesNotExist:
next_film = None
try:
previous_film = film.get_previous_by_date_published()
except Film.DoesNotExist:
previous_film = None
return render(request, 'film/film_detail.html', {
'film': film,
'film_grabs': film_grabs,
'next_film': next_film,
'previous_film': previous_film
})
film_detail.html
<div class="section project-stills">
{% if film_grabs %}
<div class="row">
{% for filmstillsimage in film_grabs %}
<div class="grid-item four">
{% thumbnail filmstillsimage.video_stills "600x338" crop="center" as im %}
<img src="{{ im.url }}">
{% endthumbnail %}
</div>
{% if forloop.counter|divisibleby:4 %}
</div>
<div class="row">
{% endif %}
{% endfor %}
</div>
{% else %}
<p>Nothing found.</p>
{% endif %}
</div>
admin.py
class FilmStillsImageAdmin(admin.ModelAdmin):
pass
class FilmStillsImageInline(admin.StackedInline):
model = FilmStillsImage
max_num = 8
extra = 0
class FilmAdmin(SortableAdminMixin, admin.ModelAdmin):
model = Film
list_display = ('title', 'meta_description', 'image_thumb', 'video', 'director', 'cinematographer', 'producer', )
inlines = [FilmStillsImageInline]
prepopulated_fields = {'slug': ('title',)}
I'm sure its a simple fix, I have a feeling its because of objects.all() in my views, populating it to each item.
Apologies if its a stupid question and thanks for any help!!
You're passing all film grabs to the detail template, so naturally they'll all display. Instead of doing that, you should just follow the reverse relationship in the template to show the ones that are actually related:
FilmStillsImage
<div class="section project-stills">
<div class="row">
{% for filmstillsimage in film.filmstillsimage_set.all %}
....
and there is no need to pass film_grabs from the view at all.

Foreign method access in django template

I m beginner.
I'm trying to access a related item of the model Product in the template layer of a ProductDetailView. How can I retrieve the ImageFields of the Products' Brand's BrandImages? I have to traverse one forward and one reverse ForeignKey.
Edited to include get_logo_url
What is wrong with the get_logo_url function?
products/models.py
class Product(models.Model):
brand = TreeForeignKey('Brand', verbose_name='parent category', related_name='products', default='')
title = models.CharField(max_length=120)
description = models.TextField(max_length=500, blank=True, null=True)
price = models.DecimalField(decimal_places=2, max_digits=20)
active = models.BooleanField(default=True)
category = TreeForeignKey('Category', verbose_name='parent category', related_name='products', default='')
slug = models.SlugField(default='')
objects = ProductManager()
class Meta:
unique_together = ('slug', 'category')
def get_absolute_url(self):
return reverse("product_detail", kwargs={"pk":self.pk})
def __unicode__(self):
return self.title
def get_image_url(self):
img = self.productimage_set.first()
if img:
return img.image.url
return img
brands/models.py
def image_upload_to(instance, filename):
title = instance.brand.title
slug = slugify(title)
file_extension = filename.split(".")[1]
new_filename = "%s.%s" % (instance.id, file_extension)
return "products/%s/%s" % (slug, new_filename)
class BrandImage(models.Model):
brand = models.ForeignKey('Brand', related_name='brandimages')
is_slider = models.BooleanField(default=False)
is_featured = models.BooleanField(default=False)
is_logo = models.BooleanField(default=False)
image = models.ImageField(upload_to=image_upload_to)
def __unicode__(self):
return self.brand.title
def get_logo_url(self):
if is_logo:
img = self.brandimage_set.first()
if img:
return img.image.url
return img
def thumb(self):
if self.image:
return u'<img src="%s" width=120 height=120 />' % (self.image.url)
else:
return u'No image file found'
thumb.allow_tags = True
class Brand(MPTTModel):
title = models.CharField(max_length=50, default='')
parent = TreeForeignKey('self', null=True, blank=True, verbose_name='parent brand', related_name='brands')
slug = models.SlugField(unique=True)
def get_absolute_url(self):
return reverse('brands', kwargs={'path': self.get_path()})
def __unicode__(self):
return self.title
template
<div class="rightpart">
<div class="prodbrand h2">
<h1>{{ product.brand }}</h1>
<div class="brandlogo">
{% for image in product.brand.brandimages.all %}
<img src="{{image.get_logo_url }}"/>
{% endfor %}
</div>
</div>
<div class="prodtitle"><h2>{{ product.title }}</h2>
</div>
views.py
class ProductDetailView(DetailView):
model = Product
template_name = 'products/product.html'
def get_context_data(self , *args , **kwargs):
context = super(ProductDetailView , self).get_context_data(*args,**kwargs)
instance = self.get_object()
context["related"] = Product.objects.get_related(instance)
return context
urls.py
url(r'^$', ProductDetailView.as_view(), name='products'),
is there a way to access foreign fields in django templates like this?
As you are using a ListView to display your products there's several things to notice:
get_context_data() must return a dictionary: return context is missing
super().get_context_data should be called with *args,**kwargs incase you decide to subclass the ProductListView at a later point in time.
super().get_context_data will contain a object_list key which contains the list of objects returned by get_queryset(), in your case objects of class Product.
When accessing a property from a template, django will attempt to call it without parameters if it is callable. This is often useful e.g.: for {{ request.user.is_authenticated }} or product.brand.brandimages.all
Your template should look like this:
product_list.html
{% for product in object_list %}
<div class="rightpart">
<div class="prodbrand h2">
<h1>{{ product.brand }}</h1>
<div class="brandlogo">
{% for image in product.brand.brandimages.all %}
<img src="{{image.image.url}}"/>
{% endfor %}
</div><!-- End brandlogos -->
</div><!-- End prodbrand -->
<div class="prodtitle">
<h2>{{ product.title }}</h2>
</div>
</div><!-- End rightpart -->
{% endfor %}
Take into account that this will incur several database lookups from the template. You generally want to avoid the case that your presentation layer reaches into the database, which is why you should prefer to do the database lookup in the corresponding View. Also for property access consider using select_related and prefetch_related as appropriate to avoid unneeded database queries.
views.py
class ProductListView(ListView):
model = Product
queryset = Product.objects.all().active()
def get_context_data(self, *args, **kwargs):
context = super(ProductListView, self).get_context_data(*args, **kwargs)
context["now"] = timezone.now()
return context
def get_queryset(self, *args, **kwargs):
# We call super() for potential subclasses
queryset = super(ProductListView, self).get_context_data(*args, **kwargs)
queryset.prefetch_related('brand__brandimages')
return queryset