Displaying arbitrary data

I have a logging table in my app, that shows how/when listed items were viewed. The same logging table also logs different types of clickthroughs, and will probably (in the future) log edits, etc. So far, the logging seems to work as I expect. Because the same logging table is used for different actions, and its results are also related to different tables (according to the action taken) it doesn’t seem to me that Django really likes that kind of setup, ie a couple of records might look like this:

id    fkey   vtype    ip…
1.   30.     edit.      192.168.1.1
2.   30.     click.     10.0.0.1

Where the fkey is the id in another table and vtype is the class of item logged – actually vtype is the name of a url in urls.py. As you can see the fkeys in different tables can clash, so when retrieving data one also has to look for the vtype, so as to get the relevant data.

I want to use the data from the logging to display on a page the number of visits that page has had over a 30 day period. Because this logging data is not directly related to the page model, I put a method in the page model to retrieve the data:

models.py

class Page(models.Model):
    ...

    @property    
    def viewcount(self):
        timeoffset = (datetime.now(timezone.utc) - timedelta(days=30))
        thecount = Viewlog.objects.filter(fkey=self.id,vtype='page',moddate__gte=timeoffset).count()
        return thecount

This works, in that I can put {{ object.viewcount }} on a template and it shows relevant data.

However, I have three questions:

  1. Is this the correct way to put arbitrary data on a template?

  2. Because viewcount is a property of the page model I think it is getting called/created every time the model is accessed, which is ok for a detail view, but not a really great idea for a list view. Can I differentiate when this property is called?

  3. As you will note, in setting the count variable, the vtype value is a hard coded string "page". When the log entry saved into the table, the method that does that enters the vtype string from the url name:

vtype=(request.resolver_match.url_name)

I would like to be able to do that in the log query too, but cannot work out how to get the request parameter into the viewcount method. Either request is not available, or it causes an exception if I put it in the expected parameters.

I have tried to get the url name in other ways, eg by using self.get_absolute_url and then trying to resolve that, but that seems to produce 404 errors.

Any guidance gratefully received.

You do not need to identify the field as a property. You can remove the property decorator and still display it in your template as {{ object.viewcount }}.

Either way, I don’t see where that method is going to be called in any situation other than when the value is retrieved. I have no reason to believe that it would be called “every time the model is accessed”. What are you seeing that is giving you that impression?

I’m not understanding what you’re trying to do with the request in the viewcount method. There are many situations where models can be used where no request is involved - it doesn’t make sense to try and access it there. If you could explain more what it is you’re trying to do - and perhaps include some of the code snippets that you tried and did not work, we might be able to offer some suggestions.

I think I am not understanding how {{ object.viewcount }} is instantiated. Does it come about because the Page model is called, or because it is on the template?

The viewcount method gets the number of times the page has been viewed in the past 30 days. A requirement for accessing that data is that the query includes the pk of the page row, and the url name that was used to access that page.

I want to use request in viewcount so that I can access the url name to use in the query:

def viewcount(self, request):
    timeoffset = (datetime.now(timezone.utc) - timedelta(days=30))
        thecount = Viewlog.objects.filter(fkey=self.id,vtype=request.resolver_match.url_name,moddate__gte=timeoffset).count()
        return thecount

Rather than the vtype parameter being hardcoded as it was in the initial example I gave. The reason that is important, is because the log table also has records for other url names with the pk of that page row, that are to be differentiated. ie they are edit or preview (or perhaps others).

So, I think it is clear why I want the url name when viewcount is run. The problem is that url name only seems to be available through the request object.

I tried getting url name through self.get_absolute_url, but that caused a 404 error (I can’t find the code snippets I used now). However, any suitable method for getting url name or using request in viewcount would be most welcome.

It’s not “instantiated”, it’s not an object. It’s a method returning a value, and so it would be called when the template is being rendered. (The @property decorator is just syntactic sugar, it doesn’t fundamentally affect what the function is.)

Looking at the definition of that method, that query probably doesn’t even belong in the model at all. I would just execute that query in the view and inject the value to the displayed into the context.

Perhaps we have different concepts of what “instantiated” means, I’m just using it in the normal English meaning: be represented by an actual example. ie {{ object.viewcount }} does not exist until it is instantiated, or created.

Anyway, I tried your idea, and put the value into the context:

    def get_context_data(self, **kwargs):
        context = super(ItemDetailView, self).get_context_data(**kwargs)
        pk = self.kwargs['pk']
        timeoffset = (datetime.now(timezone.utc) - timedelta(days=30))
        context['viewcount'] = Viewlog.objects.filter(linkid=self.kwargs['pk'],vtype=(self.request.resolver_match.url_name),moddate__gte=timeoffset).count()
        return context

Then in the template {{ viewcount }}, and I get the url_name easily enough.

However, there is a little bit I do not quite understand. In the query I use:

def get_context_data(self, **kwargs):
    ...
    ...linkid=self.kwargs['pk']...
    ...

Where, while testing I used:

def dispatch(self, request, *args, **kwargs):
    ...
    ...linkid=kwargs['pk']...
    ...

What I found was that in get_context_data there was no pk in kwargs. So, I had to use self.kwargs['pk'], after some digging and a fair amount of frustration. This seems a bit odd to me, but could someone point me to a resource where I can read why this is.

Thanks

In the context of programming in an Object Oriented language, the term “instantiated” is used to describe the process of creating an instance of an object.

In this particular case, object.viewcount does exist - it’s a method of a class. You’re not “creating” anything at that point. You’re directing the Django template engine to retrieve the “viewcount” attribute from an object named “object”.

Assuming a CBV, the get method doesn’t pass pk as a keyword arg to get_context_data. The only keyword arg supplied in a DetailView is object. By this point in the processing of the CBV, the class attribute kwargs has already been set and so the various methods don’t have to keep passing it around - every method has all the keyword args supplied to the initializer available in self.kwargs.

The essence of this is documented in the Django Built-in class-based generic views page. (As a general rule-of-thumb, it’s never necessary to override dispatch. There are usually better places to inject functionality.)

If you’re using class-based views, the best resources for understanding how they work are the Classy Class-Based view site and the CBV Diagrams pages