Adding User model to the StructBlock class - django

I'm trying to create a simple blog page with image,content,date posted and user who posted.
But I don't think that wagtail allows me to put in the user model into my block.
class HomePage(Page):
template_name = 'home/home_page.html'
max_count = 1
subtitle = models.CharField(max_length=200)
content = StreamField(
[
("title_and_text_block", AnnouncementBlock())
]
)
content_panels = Page.content_panels + [
FieldPanel("subtitle"),
StreamFieldPanel("content")
]
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, args, kwargs)
context['posts'] = HomePage.objects.live().public()
return context;
from wagtail.core import blocks
from wagtail.images.blocks import ImageChooserBlock
class AnnouncementBlock(blocks.StructBlock):
title = blocks.CharBlock(required=True, help_text='Add your title')
content = blocks.RichTextBlock(required=True, features=["bold", "italic", "link"])
image_thumbnail = ImageChooserBlock(required=False, help_text='Announcement Thumbnail')
class Meta:
template = "streams/title_and_text_block.html"
icon = "edit"
label = "Announcement"
My goal is everytime user posted a new announcement his/her name should appear. not sure how i can do that since in the block i cannot add the User model so that the user's detail will be saved along with the content/image etc.
something like this.
from wagtail.core import blocks
from wagtail.images.blocks import ImageChooserBlock
from django.conf import settings
class AnnouncementBlock(blocks.StructBlock):
title = blocks.CharBlock(required=True, help_text='Add your title')
content = blocks.RichTextBlock(required=True, features=["bold", "italic", "link"])
image_thumbnail = ImageChooserBlock(required=False, help_text='Announcement Thumbnail')
#USER is not allowed here. error on the model
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
class Meta:
template = "streams/title_and_text_block.html"
icon = "edit"
label = "Announcement"
Please help thanks

I think you can't use a ForeignKey inside a Streamfield Block class.
If you're OK with displaying a simple user select widget, try to subclass a ChooserBlock (see here).
If you need to assign the logged in user automatically instead, you might be able to write your own block type but 1- it's more complicated as you will have to figure out how Streamfields work internally and 2- if I remember correctly, you can't access the request object from inside a block definition.

Related

How to get page view count on wagtail/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.

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

Change / Filter dropdown list based based on ownership

This has got to be a common requirement but nothing I've found on SO or in Django docs that works for me. I'm really new to Django. My problem: I need to change the list of Areas that are presented in a form dropdown list according to the company that has Area ownership.
In my app:
Accounts (i.e Users) are members of Company. A Company manages an Area. Within the Area are a number of Routes. Routes can be added/updated/removed or assigned to different Areas by the Company.
So on my forms I want to make sure that only Areas that belong to a Company are displayed in the dropdown list. Essentially a user would select an Area, then CRUD routes associated with the Area.
class Company(models.Model):
name = models.CharField(...
account_number = models.CharField(...
...
class Account(models.Model):
name = models.OneToOneField(User...
company = models.ForeignKey(Company)
...
class Area(models.Model):
owner = models.ForeignKey(Company)
number = modes.PositiveIntegerField(...
class Route(models.Model):
owner = models.ForeignKey(Company)
area = models.ForeignKey(Area)
In forms.py
class RouteCreateForm(forms.ModelForm):
class Meta:
model= Route
fields= [
'area',
'route_number',
...
]
Adding:
self.fields['area'].queryset = Area.objects.filter(owner_id = 2)
provides the correct filtering but of course is not dynamic.
I've a lot of variations on :
def __init__(self, *args, **kwargs):
??how to pass in user to ultimately get to owner_id??
self.fields['area'].queryset = Area.objects.filter(owner_id = owner_id)
but can't get the middle right. I've also tried passing in 'user' but the only results in a TypeError at /account/setup/route/create
init() missing 1 required positional argument: 'user'
If you are using generic CreateView, you can modify your form per request by overriding get_form() on your view. That would look like this:
class RouteCreateView(generic.CreateView):
form_class = RouteCreateForm
def get_form(self):
form = super(RouteCreateView, self).get_form()
form.fields['area'].queryset = Area.objects.filter(owner=self.request.user)
return form

Django-ViewFlow: How to add CRUD views to flow

I've recently come across the Viewflow library for Django which I appears to be a very powerful tool for creating complex workflows.
My app is a simple ticketing system were the workflow is started by creating a Ticket, then a user should be able to create zero or more WorkLog's associated with the ticket via a CRUD page(s), similar to the standard Django admin change_list/detail.
What should the template for the list view look like? I would like to have the UI integrated into the library's frontend.
The flow clearly utilises the following views:
1) CreateView for Ticket
2a) ListView of WorkLog's, template has controls 'back', 'add' (goes to step 2b), 'done' (goes to step 3).
2b) CreateView for WorkLog
3) End
Code:
models.py:
class TicketProcess(Process):
title = models.CharField(max_length=100)
category = models.CharField(max_length=150)
description = models.TextField(max_length=150)
planned = models.BooleanField()
worklogs = models.ForeignKey('WorkLog', null=True)
class WorkLog(models.Model):
ref = models.CharField(max_length=32)
description = models.TextField(max_length=150)
views.py:
class WorkLogListView(FlowListMixin, ListView):
model = WorkLog
class WorkLogCreateView(FlowMixin, CreateView):
model = WorkLog
fields = '__all__'
flows.py:
from .views import WorkLogCreateView
from .models import TicketProcess
#frontend.register
class TicketFlow(Flow):
process_class = TicketProcess
start = (
flow.Start(
CreateProcessView,
fields = ['title', 'category', 'description', 'planned']
).Permission(
auto_create=True
).Next(this.resolution)
)
add_worklog = (
flow.View(
WorkLogListView
).Permission(
auto_create=True
).Next(this.end)
)
end = flow.End()
You can handle that in a different view, or in the same view, just don't call activation.done on a worklog adding request. You can do it by checking what button was pressed in the request.POST data.
#flow.flow_view
def worklog_view(request):
request.activation.prepare(request.POST or None, user=request.user)
if '_logitem' in request.POST:
WorkLog.objects.create(...)
elif request.POST:
activation.done()
request.activation.done()
return redirect(get_next_task_url(request, request.activation.process))
return render(request, 'sometemplate.html', {'activation': request.activation})

Django admin sharing inlines between apps

I have a few apps in my project that I want to be reusable.
First, I have a base content App, which defines how content can be added to a ContentContainer. This allows other models to inherit ContentContainer to get the ability to show content.
Within the content app, I have a Page model that inherits ContentContainer. In another app called events, I have an Event model that also inherites ContentContainer. Basically, my events app depends on my content app (Which is what I want).
This all works great in modeling. I have this in my content app:
class ContentContainer(admin.ModelAdmin):
#No fields, it just gets referred to by ContentItem
class Meta:
ordering = ['modified']
class ContentItem(TimeStampedModel):
name = models.CharField(max_length=1500)
page_order = models.IntegerField()
container = models.ForeignKey(ContentContainer, blank=True)
#make inheritance know the model type
objects = InheritanceManager()
class Meta:
ordering = [ 'page_order', 'modified', 'name']
def __unicode__(self):
return self.name
def render(self):
return self.name
class TextContent(ContentItem):
text = models.CharField(max_length=5000000)
def render(self):
return '<p>%s</p>' % self.text
Then in my Events app I do this:
class Event(AnnouncementBase, Addressable, ContentContainer):
cost = CurrencyField(decimal_places=2, max_digits=10, blank=True, default=0.00)
start_date = models.DateField(default = datetime.now().date())
start_time = models.TimeField(default = datetime.now().time())
end_date = models.DateField(blank=True, default=None, null = True)
end_time = models.TimeField(blank=True, default=None, null = True)
rsvp_deadline = models.DateTimeField(blank=True, default=None, null = True)
class Meta:
ordering = ['start_date', 'start_time', 'title']
So now events can have content to render.
Here's where things get confusing. I have a slew of inlines defined in admin.py in the content app. They work great there. Also, if I copy an paste them into admin.py in the events app they work there too.
However, I don't want to duplicate code. I want to import the inlines from admin.py into events.py In contents admin.py I have this:
class TextContentInline(admin.TabularInline):
model = models.TextContent
extra = 1
class PageAdmin(ContainerAdmin):
model = models.Page
inlines = [LinkContentInline, TextContentInline]
There are a bunch of these inlines. How can I share them between my admin.py models? If I try to import them in the events admin.py I get an error that says "The model Page is already registered". I've tried about 5 different things I could think of an none of them work. I am wondering if there is no way to do this. Oh I'm using Django 1.3 too.
The "already registered" error happens because when a module is imported, Python executes all the statements in the top level - and one of those is the admin.site.register, which is therefore called multiple times.
It's easy to fix this - just catch the exception and ignore it:
try:
admin.site.register(MyModel, MyModelAdmin)
except admin.sites.AlreadyRegistered:
pass
An alternative is to keep your inline classes in a completely separate module file - admin_inlines.py, perhaps - and import them from there into every admin that needs them.