How to get page view count on wagtail/django? - django

I am planning to create a page within a blog website where it arranges and displays the all blog posts based on page view count. Not sure how to pull it off.
models.py
class BlogPost(Page):
date = models.DateField(verbose_name="Post date")
categories = ParentalManyToManyField("blog.BlogCategory", blank=True)
tags = ClusterTaggableManager(through="blog.BlogPageTag", blank=True)
body = RichTextField(blank=False)
main_image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=False,
on_delete=models.SET_NULL,
related_name='+')
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)
blogposts = self.get_siblings().live().public().order_by('-first_published_at')
context['blogposts'] = blogposts
return context
content_panels = Page.content_panels + [
FieldPanel('date'),
FieldPanel('categories', widget=forms.CheckboxSelectMultiple),
FieldPanel('tags'),
ImageChooserPanel('main_image'),
FieldPanel('body', classname="full"),
]

As mentioned in another answer, you can add view_count field to your model. Then you can leverage Wagtail's hooks to increment the value in database.
New field in the model:
class BlogPage(Page):
view_count = models.PositiveBigIntegerField(default=0, db_index=True)
Register the before_serve_page hook:
#hooks.register("before_serve_page")
def increment_view_count(page, request, serve_args, serve_kwargs):
if page.specific_class == BlogPost:
BlogPost.objects.filter(pk=page.pk).update(view_count=F('view_count') + 1)
In this approach database takes responsibility to correctly increment view_count so you don't have to worry about locking and incrementing the value yourself.

If you wanted to count views in a slightly more featured way you could use the django-hitcount package.
Your wagtail_hooks.py file would then become:
from hitcount.models import HitCount
from hitcount.views import HitCountMixin
from wagtail.core import hooks
from home.models import BlogPage
#hooks.register("before_serve_page")
def increment_view_count(page, request, serve_args, serve_kwargs):
if page.specific_class == BlogPage:
hit_count = HitCount.objects.get_for_object(page)
hit_count_response = HitCountMixin.hit_count(request, hit_count)
You need to add HitCountMixin to your Page definition, i.e.
from hitcount.models import HitCountMixin
class BlogPage(Page, HitCountMixin):
This allows you to count hits but avoid duplication from the same IP, to reset hits with a management command, and to set the 'active' period for a page.
You will also need to pip install django-hitcount and add it to your INSTALLED_APPS in settings.py.

As a naive solution, you could add a field view_count to your BlogPage model, this would be a IntegerField.
You will need a way to update this value every time the page is served, you can add some logic to the get_context method you have already used. However, the serve method would be more appropriate, be sure to check that the serve is not being called as a preview by checking request.is_preview.
In regards to querying (ordering by this view_count), this can be done by updating your query.
blogposts = self.get_siblings().live().public().order_by('-view_count')
You can make this new field visible, but not easily editable (client side validation only) by adding it via a FieldPanel with a custom widget. Using the Wagtail settings_panels this can be made available in the non content panel.
Example Model
models.py
from django.db import models
from django import forms
from wagtail.core.models import Page
class BlogPage(Page):
# ... other fields
view_count = models.IntegerField(blank=False, default=0)
content_panels = Page.content_panels + [
# ... existing content panels
]
settings_panels = Page.settings_panels + [
FieldPanel(
'view_count',
# show the view count in the settings tab but do not allow it to be edited
# note: can be easily edited by savvy users, but only if they can also access admin
widget=forms.NumberInput(attrs={'disabled': 'disabled', 'readonly': 'readonly'})
)
]
Please note: this implementation does not take into consideration any other case where serve may be called but does not mean 'a unique user saw my blog post'. You may want to investigate a proper Django analytics solution or some kind of integration with client side analytics such as Google Analytics or Heap.

Related

Using Django, should I implement two different url mapping logics in my REST API and web application, and how?

I have a Book model an instance of which several instances of a Chapter model can be linked through Foreign Key:
class Book(models.Model):
pass
class Chapter(models.Model):
book = models.ForeignKey(to=Book, ...)
class Meta:
order_with_respect_to = "book"
I decided to use Django both for the RESTful API, using Django Rest Framework, and for the web application, using Django Template. I want them separate as the way should be open for another potential application to consume the API.
For several reasons including administration purposes, the API calls for a url mapping logic of this kind:
mylibrary.org/api/books/
mylibrary.org/api/books/<book_id>/
mylibrary.org/api/chapters/
mylibrary.org/api/chapters/<chapter_id>/
For my web application, however, I would like the user to access the books and their contents through this url mapping logic:
mylibrary.org/books/
mylibrary.org/books/<book_id>-esthetic-slug/
mylibrary.org/books/<book_id>-esthetic-slug/chapter_<chapter_order>/
The idea is the router to fetch the book from the <book_id>, whatever the slug might be, and the chapter according to its order and not its ID.
Now I need some advice if this is desirable at all or if I am bound to encounter obstacles.
For example, how and where should the web app's <book_id>/<chapter_order> be "translated" into the API's <chapter_id>? Or if I want the web app's list of books to offer automatically generated slugged links to the books, should it be done at the API or the web app level?
I'm pretty new to Django / DRF and web development in general.
The double URLs are not a problem at all. It is used whenever the program renders views and has an API (perhaps for a mobile app) as well. What I usually do is have the id field be in the URL of the 'web' pages too, it is simpler for me to keep track of. However if you want to have a 'nice' slug in the URL, try this:
"""
models.py
"""
import uuid
from django.db import models
class Book(models.Model):
id = models.UUIDField(primary_key=True, null=False, editable=False, default=uuid.uuid4)
slug = models.SlugField(max_length=50)
class Chapter(models.Model):
id = models.UUIDField(primary_key=True, null=False, editable=False, default=uuid.uuid4)
book = models.ForeignKey(to=Book, ...)
sort = models.PositiveIntegerField()
class Meta:
order_with_respect_to = "book"
unique_together = [("book", "sort"), ] # so that there is never two chapters with the same number
"""
urls.py
"""
from django.urls import path
from . import apiviews, views
urlpatterns = [
# 'web' URLs
path("books/", views.books, "book_list"), # mylibrary.org/books/
path("books/<str:book_slug>", views.book, "book"), # mylibrary.org/books/harry-potter-and-the-prisoner-of-azkaban
path("books/<str:book_slug>/chapter_<int:chapter>", views.chapter, "chapter"), # mylibrary.org/books/harry-potter-and-the-prisoner-of-azkaban/chapter_2
# API URLs
path("api/books/", apiviews.books, "api_book_list"), # mylibrary.org/api/books/
path("api/books/<uuid:id>", apiviews.book, "api_book"), # mylibrary.org/api/books/b21a0af6-4e6c-4c67-b44d-50f8b52cd459
path("api/chapters/", apiviews.chapters, "api_chapters"), # mylibrary.org/api/chapters/
path("api/chapters/<uuid:id>", apiviews.chapter, "api_chapter"), # mylibrary.org/api/chapters/c76a89ae-ad73-4752-991c-8a82e47d3307
]
"""
views.py
"""
from .models import Book, Chapter
def books(request):
book_list = Book.objects.all()
def book(request, book_slug)
try:
book = Book.objects.get(slug=book_slug)
except Book.DoesNotExist:
pass
def chapter(request, book_slug, chapter)
try:
book = Book.objects.get(slug=book_slug)
chapter = book.chapter_set.get(sort=chapter)
except Book.DoesNotExist:
pass
except Chapter.DoesNotExist:
pass
"""
apiviews.py
"""
from .models import Book, Chapter
def books(request):
book_list = Book.objects.all()
def book(request, id)
try:
book = Book.objects.get(id=id)
except Book.DoesNotExist:
pass
def chapters(request):
chapter_list = Chapter.objects.all()
def chapter(request, id)
try:
chapter = Chapter.objects.get(id=id)
except Chapter.DoesNotExist:
pass
These files reflect how you suggested the URLs. I personally (and I don't say that is the correct way) would have the API views set up like this:
mylibrary.org/books/
mylibrary.org/books/b21a0af6-4e6c-4c67-b44d-50f8b52cd459
mylibrary.org/books/b21a0af6-4e6c-4c67-b44d-50f8b52cd459/chapters/
mylibrary.org/books/b21a0af6-4e6c-4c67-b44d-50f8b52cd459/chapters/3
# and then for example:
mylibrary.org/books/b21a0af6-4e6c-4c67-b44d-50f8b52cd459/comments/
mylibrary.org/books/b21a0af6-4e6c-4c67-b44d-50f8b52cd459/comments/295
I find this structure more comprehensive but I can't see any pros or cons against your version. Both work just as fine and there is a good reasoning behind both of them. One being "API is just a way to access the database objects and so I should be able to pull an arbitrary chapter if I know the ID", the other "I have books and books have chapters, so a chapter is a sub-thing of books".

Putting tags in a StructBlock

I want to be able to add tagging to a custom StructBlock that I've created.
The current model looks like this
class MapsIndicatorBlock(blocks.StructBlock):
text_icon = blocks.CharBlock(
label='Maps/Indicators Text or Icon',
required=False
)
pop_up_title = blocks.CharBlock(
label='Pop-Up Title',
required=False
)
pop_up_text = blocks.RichTextBlock(
label ='Pop-Up Text/Image',
required=False
)
pop_up_colour = blocks.CharBlock(
choices=constants.BOOTSTRAP4_BUTTON_COLOUR_CHOICES,
default='btn btn-primary',
max_length=128,
required=False
)
tags = TaggableManager()
objects = models.Manager()
class Meta:
template = 'cityregiontable/map_indicator_block.html'
The TaggableManager() was designed to be used with models.model not blocks.StructBlock.
I have tried to create a way to create the tags using the following to no avail. I get the error RE: not being able to find the model for MapsIndicatorBlock. This is correct as MapsIndicatorBlock is a block, not a model.
class MITag(TaggedItemBase):
content_object = models.ForeignKey(
'MapsIndicatorBlock',
on_delete=models.CASCADE,
related_name='tagged_mi_block'
)
How can I allow a block to be have metadat tags?
Based on the docs for custom block types as a starting point we are able to generate a custom FieldBlock that leverages the existing Wagtail AdminTagWidget.
This widget does almost all of the work for you, it will pull in the available tags for autocomplete plus will save any new tags created on the fly.
It is possible to read out these tags and make them available more conveniently with a model #property or similar. Remember Streamfields store data as JSON so you do not get any of the model / database linking out of the box.
Limitations
The caveat is that the saved tags are stored as the raw strings, this means if you have some more complex use cases of tags you will have to do a bit more work to get this integrated. e.g. a tag page that shows all pages that use that tag or advanced tag editing in Wagtail's ModelAdmin.
In these cases, you can either work out a way to 'sync' the Page's tags with the StreamField tag and maybe abstract this work out to a mixin. Alternatively, you can rework your query on your tags page to also include those with the streamfield data you want.
Example Code
from itertools import chain
from django import forms
from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel
from wagtail.admin.widgets import AdminTagWidget
from wagtail.core.blocks import CharBlock, FieldBlock, StructBlock, RichTextBlock
from wagtail.core.fields import StreamField
from wagtail.core.models import Page
class TagsBlock(FieldBlock):
"""
Basic Stream Block that will use the Wagtail tags system.
Stores the tags as simple strings only.
"""
def __init__(self, required=False, help_text=None, **kwargs):
# note - required=False is important if you are adding this tag to an existing streamfield
self.field = forms.CharField(widget=AdminTagWidget, required=False)
super().__init__(**kwargs)
class MapBlock(StructBlock):
title = CharBlock(label="Title", required=False)
content = RichTextBlock(label="Content", required=False)
tags = TagsBlock(label="Tags", required=False)
class Meta:
icon = 'site'
class LocationPage(Page):
"""
Detail for a specific location.
"""
# ... other fields
# this is the stream field added
map_info = StreamField([('Map', MapBlock(required=False))], blank=True)
#property
def get_tags(self):
"""
Helpful property to pull out the tags saved inside the struct value
Important: makes some hard assumptions about the names & structure
Does not get the id of the tag, only the strings as a list
"""
tags_all = [block.value.get('tags', '').split(',') for block in self.test_b]
tags = list(chain.from_iterable(tags_all))
return tags
# Fields to show to the editor in the admin view
content_panels = [
FieldPanel('title', classname="full"),
StreamFieldPanel('map_info'),
# ... others
]
# ... rest of page model
Thanks to this similar question about tags in streamfields, answering that helped me answer this one.
Creating a TagsBlock for StreamField

Wagtail - how to preopulate fields in admin form?

I would like to pre-populate fields in wagtail page admin. Particularly I would like to take username of currently logged admin/editor user and fill it in the form as a string. A simplified version of my page looks like this:
class ItemPage(Page):
author = models.CharField(max_length=255, default="")
content_panels = Page.content_panels + [
FieldPanel('author'),
]
I do not want to set a default value in the author field in the model - it should be user specific.
I do not want to use the save method or signal after the model is saved/altered. The user should see what is there and should have the ability to change it. Also, the pages will be generated automatically without the admin interface.
I think that I need something like https://stackoverflow.com/a/14322706/3960850 but not in Django, but with the Wagtail ModelAdmin.
How to achieve this in Wagtail?
Here is an example based on gasmans comment and the documentation that accompanies the new code they linked:
from wagtail.admin.views.pages import CreatePageView, register_create_page_view
from myapp.models import ItemPage
class ItemPageCreateView(CreatePageView):
def get_page_instance(self):
page = super().get_page_instance()
page.author = 'Some default value'
return page
register_create_page_view(ItemPage, ItemPageCreateView.as_view())
You could also do this by overriding the models init method, but the above is much nicer
class ItemPage(Page):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
try:
author = kwargs['owner'].username
except (AttributeError, KeyError):
pass
else:
self.author = author

How to manage django admin for country section with same back and frontend listing page

I am new in Django. I am creating a app countries which will have listing page in both frontend and backend. I have manage frontend by writing code in root urls.py
path('countries/', include(('countries.urls', 'countries'), namespace = 'countries'))
And in countries/urls.py
path('', views.index, name='index'),
in models.py I write country model for frontend and In views.py write index function for frontend.
This is my models.py
class Country(models.Model):
iso_code = models.CharField(max_length=2, unique=True)
name = models.CharField(max_length=255, unique=True)
is_featured = models.IntegerField(max_length=1)
class Meta:
db_table = 'countries'
And in views.py I write
def index(request):
countries = Country.objects.all().order_by('id')
context = {
"countries" : countries
}
return render(request, 'countries/index.html', context)
If I run http://127.0.0.1:8000/countries/ then it will load country listing page in frontend.
Now, I want http://127.0.0.1:8000/admin/countries/ to see backend listing page with custom admin template.
Please help me if someone know
If I add path('admin/countries/', include(('countries.urls', 'countries'), namespace = 'countries')), in urls.py then http://127.0.0.1:8000/admin/countries/ is also take same template page and does not show admin template.
In the admin.py of your countries app, do this:
Although note that this is for django 2.x. You didn't specifiy your version, but django 1.x isn't much different from the following example. I just skimmed 1.11 quick, and the 2 attributes I checked in the options section were the same here. If you get a bug, change to the version you need, and fix the attribute.
from .models import Country
from django.contrib import admin
#admin.register(Country)
class CountryAdmin(admin.ModelAdmin):
list_display = ['iso_code', 'name', 'is_featured']
list_editable = ['name'] # Add more here if you want to edit them inline.
list_filter = ['iso_code'] # add more to be able to filter your model
list_per_page = 10 # paginates the amount that show up per page
search_fields = ['name', 'iso_code'] # field names searched

Lazy loading a model field's choices

I'm building a Django app to pull in data via an API to track live results of an event with the added ability to override that data before it is displayed.
The first task of the app is to make a request and store the response in the database so I've setup a model;
class ApiData(models.Model):
event = models.CharField(
_("Event"),
max_length=100,
)
key = models.CharField(
_("Data identifier"),
max_length=255,
help_text=_("Something to identify the json stored.")
)
json = JSONField(
load_kwargs={'object_pairs_hook': collections.OrderedDict},
blank=True,
null=True,
)
created = models.DateTimeField()
Ideally I would like it so that objects are created in the admin and the save method populates the ApiData.json field after creating an API request based on the other options in the object.
Because these fields would have choices based on data returned from the API I wanted to lazy load the choices but at the moment I'm just getting a standard Charfield() in my form.
Is this the correct approach for lazy loading model field choices? Or should I just create a custom ModelForm and load the choices there? (That's probably the more typical approach I guess)
def get_event_choices():
events = get_events()
choices = []
for event in events['events']:
choices.append((event['name'], event['title']),)
return choices
class ApiData(models.Model):
# Fields as seen above
def __init__(self, *args, **kwargs):
super(ApiData, self).__init__(*args, **kwargs)
self._meta.get_field_by_name('event')[0]._choices = lazy(
get_event_choices, list
)()
So I went for a typical approach to get this working by simply defining a form for the model admin to use;
# forms.py
from django import forms
from ..models import get_event_choices, ApiData
from ..utils.api import JsonApi
EVENT_CHOICES = get_event_choices()
class ApiDataForm(forms.ModelForm):
"""
Form for collecting the field choices.
The Event field is populated based on the events returned from the API.
"""
event = forms.ChoiceField(choices=EVENT_CHOICES)
class Meta:
model = ApiData
# admin.py
from django.contrib import admin
from .forms.apidata import ApiDataForm
from .models import ApiData
class ApiDataAdmin(admin.ModelAdmin):
form = ApiDataForm
admin.site.register(ApiData, ApiDataAdmin)