fourmatt
fourmatt

Reputation: 17

Django - Pass two Models into one view, and display both models

I'm trying to pass two models into a create view, where i am trying to get the the primary key from the URL to retrieve the details from a food truck model so it can be displayed in the page, and where a user can write a review about food truck. Also, I'd like a list of the reviews to be displayed on the page.

views.py

class TruckReviewView(CreateView):
    model = Review
    template_name = 'truckReviews/detail.html'
    fields = ['speedOfService', 'qualityAndTaste', 'valueForMoney', 'comment']

    def get_queryset(self):
        self.pk = self.kwargs['pk']
        queryset = super(TruckReviewView, self).get_queryset()
        return queryset

    def get_context_data(self, **kwargs):
        context = super(TruckReviewView, self).get_context_data(**kwargs)
        context['truck'] = FoodTrucks.objects.get(truckID=get_queryset())
        context['reviews'] = Review.objects.get(truckID=get_queryset())
        return context

urls.py

urlpatterns = [
    path('', TruckListView.as_view(), name='reviews-home'),
    path('truck/<int:pk>/', TruckReviewView.as_view(), name='truck-detail'),
    path('about/', About.as_view(), name='reviews-about'),
]

models.py

class FoodTrucks(models.Model):
    truckID = models.IntegerField(primary_key=True, unique=True, null=False)
    name = models.CharField(max_length=25)
    category = models.CharField(max_length=20)
    bio = models.TextField()
    avatarSRC = models.TextField(default=None)
    avatarALT = models.CharField(max_length=20, default=None)
    avatarTitle = models.CharField(max_length=20, default=None)
    coverPhotoSRC = models.TextField(default=None)
    coverPhotoALT = models.CharField(max_length=20, default=None)
    coverPhotoTitle = models.CharField(max_length=20, default=None)
    website = models.TextField(default=None)
    facebook = models.CharField(max_length=100, default=None)
    instagram = models.CharField(max_length=30, default=None)
    twitter = models.CharField(max_length=15, default=None)


class Review(models.Model):
    reviewID = models.AutoField(primary_key=True, unique=True, serialize=False, null=False)
    truckID = models.ForeignKey(FoodTrucks, on_delete=models.CASCADE)
    userID = models.ForeignKey(User, on_delete=models.CASCADE)
    datePosted = models.DateTimeField(default=timezone.now)
    speedOfService = models.IntegerField()
    qualityAndTaste = models.IntegerField()
    valueForMoney = models.IntegerField()
    comment = models.TextField(max_length=128)

I've tried to use get_queryset to get the pk from the URL and pass the pk into get_context_data and target the specific truck with that ID in the database.

Upvotes: 0

Views: 995

Answers (3)

user1600649
user1600649

Reputation:

The difficulty comes from the fact that you combine a list view and create view. If you want to combine this into one view, then you need to do a bit mixing and matching with different mixins of the Class Based Views.

It can be done, but it's not trivial. If you're new to Django, then this may be overshooting things. I've renamed fields an such and did it as an exercise. I haven't bothered with the form submission, it shouldn't be that difficult to implement as the other views don't deal with the methods involved (form_valid, get_success_url, etc). You can use it as a guide to see what you should be learning. The above linked site is extremely convenient to see how things are mixed together.

The result below will provide the variables "foodtruck", "reviews" and "form" to the template.

import typing as t

from django.views import generic
from .models import FoodTruck, Review
from .forms import ReviewForm

if t.TYPE_CHECKING:
    from django.http import HttpRequest, HttpResponse
    from django.contrib.auth.models import AbstractUser

    class AuthenticatedRequest(HttpRequest):
        user: AbstractUser = ...


class FoodTruckDetailReviewListCreateView(
    generic.list.MultipleObjectMixin, generic.edit.CreateView,
):
    template_name = "foodtrucks/detail.html"
    model = Review
    list_model = Review
    context_list_name = "reviews"
    context_object_name = "foodtruck"
    detail_model = FoodTruck
    form_class = ReviewForm

    def get(self, request: "AuthenticatedRequest", *args, **kwargs) -> "HttpResponse":
        """
        Combine the work of BaseListView and BaseDetailView

        Combines the get implementation of BaseListView and BaseDetailView, but
        without the response rendering. Then hands over control to CreateView's
        method to do the final rendering.

        Some functionality is stripped, because we don't need it.

        :param request: The incoming request
        :return: A response, which can be a redirect
        """
        # BaseListView
        self.object_list = self.get_queryset()
        # BaseDetailView
        self.object = self.get_object()
        context = self.get_context_data(
            object=self.object, object_list=self.object_list
        )
        # CreateView sets self.object to None, but we override form_kwargs, so
        # we can leave it at a value.

        return self.render_to_response(context=context)

    def get_template_names(self):
        # Bypass logic in superclasses that we don't need
        return [self.template_name]

    def get_object(self, queryset=None):
        # We provide the queryset to superclasses with the other model
        return super().get_object(queryset=self.detail_model.objects.all())

    def get_queryset(self):
        # This only gets called by MultipleObjectMixin
        pk = self.kwargs.get(self.pk_url_kwarg)
        if pk is None:
            raise AttributeError(
                "Unable to filter on food truck: {} is missing in url.".format(
                    self.pk_url_kwarg
                )
            )
        queryset = self.list_model.objects.filter(food_truck_id=pk)
        # print(str(queryset.query))
        return queryset

    def get_context_data(self, **kwargs):
        if "object" in kwargs:
            kwargs[self.context_object_name] = kwargs["object"]
        if "object_list" in kwargs:
            kwargs[self.context_list_name] = kwargs["object_list"]

        return super().get_context_data(**kwargs)

    def get_form_kwargs(self):
        # Bypass ModelFormMixin, which passes in self.object as instance if it
        # is set.
        return super(generic.edit.ModelFormMixin, self).get_form_kwargs()

And as a reference, this is what I changed the models to:

import uuid

from django.contrib.auth import get_user_model
from django.db import models
from django.utils import timezone


class FoodTruck(models.Model):
    name = models.CharField(max_length=25)
    category = models.CharField(max_length=20)
    bio = models.TextField()
    avatar_url = models.URLField(blank=True)
    avatar_alt_text = models.CharField(max_length=20, blank=True)
    avatar_title = models.CharField(max_length=20, blank=True)
    cover_photo_url = models.URLField(blank=True)
    cover_photo_alt_text = models.CharField(max_length=20, default="No photo provided")
    cover_photo_title = models.CharField(max_length=20, default="No photo provided")
    website = models.URLField(blank=True)
    facebook = models.CharField(max_length=100, blank=True)
    instagram = models.CharField(max_length=30, blank=True)
    # https://9to5mac.com/2017/11/10/twitter-display-name-limit/
    twitter = models.CharField(max_length=50, blank=True)

    def __str__(self):
        return self.name


class Review(models.Model):
    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4)
    food_truck = models.ForeignKey(
        FoodTruck, on_delete=models.CASCADE, related_name="reviews"
    )
    user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
    posted_at = models.DateTimeField(default=timezone.now)
    speed_of_service = models.IntegerField()
    quality_and_taste = models.IntegerField()
    value_for_money = models.IntegerField()
    comment = models.TextField(max_length=128)

    def __str__(self):
        return "Review about {} by {}".format(
            self.food_truck.name, self.user.get_full_name()
        )

And finally the form (with a bit of trickery to inject bootstrap classes):

class ReviewForm(forms.ModelForm):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        for field in self.fields.values():
            if not field.widget.is_hidden:
                field.widget.attrs.setdefault("class", "form-control")

    class Meta:
        model = Review
        exclude = ("uuid", "user", "food_truck", "posted_at")

Upvotes: 1

Higor Rossato
Higor Rossato

Reputation: 2046

I'm sorry but your question is a bit confusing. If you're trying to get details from a model you should use a DetailView. Also, on a DetailView assuming you want the details of a Review since you have the truck on a review you could simply override the get_context_data and set truck in the context by doing self.object.truck.

If you're trying to create a review then it's right to use the CreateView but that should only be for the Review model.

To list you should use a ListView.

So, to my understanding, you have a truckID and want to create a review for that. In that case, it'd have a CreateView for Review model.

Have a look at the CreateView, DetailView and ListView docs

Upvotes: 1

K. John
K. John

Reputation: 129

First, there's not need to create a truckID and reviewID primary key fields because Django creates a unique id field for each object automatically on which you can simply do .get(id=1) or .filter(id=1) etc.

Just like it is completely useless to put ID in fields with Foreign Key or any relational fields because Django will automatically take the name and append _id to it. For instance, just user would become user_id or truck would be truck_id in backend on which you can do .get(user__id=1) or .get(user_id=1) for example.

You should review this section of your code. You're actually not doing anything with the primary key:

 def get_queryset(self):
    queryset = super().get_queryset()
    try:
       item = queryset.get(id=self.kwargs['pk'])
    except:
       ...
    else:
       # Do something with item here
       ...
    finally:
       return queryset

or, with get_context_data:

  def get_context_data(self, **kwargs):
     context = super().get_context_data(**kwargs)
     queryset = super().get_queryset()

     try:
        item = queryset.get(id=kwargs['pk'])
     except:
        ...
     else:
        # Do something with item here
        context['item'] = item
     finally:
        return context

Upvotes: 1

Related Questions