Renato Maretić
Renato Maretić

Reputation: 355

Django: Queries and calculations inside CreateView

I am relatively new to Django, so I am not sure if what I am asking possible.

I am building a website with functionality to rate users and write reviews about them. I have model for users (that has average rating field) and a model for reviews (with fields of author, user_profile, grade and review). I am using CreateView for making reviews.

I am trying to do the following:

  1. To make query to get all previous grades of that person (from Reviews model).

  2. Make calculations (sum all previous grades, add the new one and all that divide by number of grades (including new grade))

  3. Save new average grade to UserProfile model

  4. Save review to Reviews model

  5. Redirect user to current detail view

Models.py

class UserProfile(models.Model):
    ...
    avg_grade = models.FloatField(blank=True, null=True)
    ...

class Reviews(models.Model):
    user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
    grade = models.PositiveIntegerField()
    review = models.CharField(max_length=256, null=True, blank=True)
    author = models.CharField(max_length=256)

In views.py I managed to make query of grades of that user, but not sure where to do calculations for new average grade (if this is possible inside a Class-Based-View):

class CreateReview(LoginRequiredMixin, CreateView):
    form_class = Forma_recenzije
    success_url = reverse_lazy('detail')
    template_name = 'accounts/recenzija.html'

    def get_queryset(self):
        u = UserProfile.objects.get(id=int(self.kwargs['pk']))
        return Reviews.objects.filter(user_profile=u)

    def form_valid(self, form):
        form.instance.author = self.request.user
        form.instance.user_profile = UserProfile.objects.get(id=int(self.kwargs['pk']))
        return super(CreateReview, self).form_valid(form)

urlpatterns:

[...
    url(r'^dadilje/(?P<pk>[-\w]+)/$', views.DadiljaDetailView.as_view(), name="detail"),
    url(r'^dadilje/(?P<pk>[-\w]+)/recenzija$', views. CreateReview.as_view(), name="recenzije")
...
]

Upvotes: 0

Views: 1400

Answers (3)

Alasdair
Alasdair

Reputation: 308809

You can calculate the new average once you have called super(), before you return the response.

def form_valid(self, form):
    form.instance.author = self.request.user
    user_profile = UserProfile.objects.get(id=int(self.kwargs['pk']))
    form.instance.user_profile = user_profile`
    response = super(CreateReview, self).form_valid(form)
    avg_grade = Review.objects.filter(user_profile=user_profile).aggregate(Avg('grade'))['grade__avg']
    user_profile.avg_grade = avg_grade
    user_profile.save()
    return response

Or, if you find that calling super() makes it hard to see what's going on, you could explicitly save the form and redirect instead:

def form_valid(self, form):
    form.instance.author = self.request.user
    user_profile = UserProfile.objects.get(id=int(self.kwargs['pk']))
    form.instance.user_profile = user_profile`
    review = form.save()
    avg_grade = Review.objects.filter(user_profile=user_profile).aggregate(Avg('grade'))['grade__avg']
    user_profile.avg_grade = avg_grade
    user_profile.save()
    return HttpResponseRedirect(self.success_url)

Note that you may not have to store the avg_grade on the user profile -- you could use annotate to calculate the averages when you need them.

Upvotes: 1

markwalker_
markwalker_

Reputation: 12849

For the kind of things you want to do Django has signals which you can listen out for.

As an example, you could have a function that listens out for when UserProfile has been saved which clears the cache keys related to that profile.

These functions are usually added to a signals.py within your apps, or in models.py files after you've defined your model.

Signals have to be loaded after your models, so if using a signals.py the way I tend to do it is in apps.py;

class MyAppConfig(AppConfig):
    """App config for the members app. """
    name = 'my app'
    verbose_name = _("My App")

    def ready(self):
        """ Import signals once the app is ready """
        # pylint: disable=W0612
        import myapp.signals  # noqa

Here's an example of a signal receivers, pre_save happens just before the object is saved, so you could run your calcs at this point;

@receiver(pre_save, sender=UserProfile)
def userprofile_pre_save(sender, instance, **kwargs):
    """
    Calc avg score
    """
    reviews = Reviews.objects.filter(user_profile=instance).aggregate(Avg('grade'))

    instance.avg_grade = reviews['grade_avg']

You'd probably want your receiver on the Review change, but the above was an easy example!!

If you're new to django this might be a bit complex, but give this a read; https://simpleisbetterthancomplex.com/tutorial/2016/07/28/how-to-create-django-signals.html

Upvotes: 1

Kirill Ermolov
Kirill Ermolov

Reputation: 810

I think best solution here is use django signals

As result you separate view and domain logic and your calculations will apply after each change (not only changes in view)

Also if your calculation will take a lot of time you may easily move this functionality in async job (e.g. celery )

Upvotes: 0

Related Questions