I have a categorypage -> articlepage hierarchy in wagtail. The article page has an author field, which currently shows all users in the system. I want to filter the authors of the article based on the group of the parent category page.
models.py
from django.contrib.auth.models import Group
class CategoryPage(Page): # query service, api
blurb = models.CharField(max_length=300, blank=False)
content_panels = Page.content_panels + [
FieldPanel('blurb', classname="full")
]
subpage_types = ['cms.ArticlePage']
class ArticlePage(Page): # how to do X with query service
...
author = models.ForeignKey(User, on_delete=models.PROTECT, default=1,
# limit_choices_to=get_article_editors,
help_text="The page author (you may plan to hand off this page for someone else to write).")
def get_article_editors():
# get article category
# get group for category
g = Group.objects.get(name='??')
return {'groups__in': [g, ]}
This question (limit_choices_to) is almost what I'm after, but I'm not sure how to retrieve the group for the parent page before the article itself is created?
This question seems to do the trick in accessing parent pages on creation, but I remain unsure of how to find the groups that can edit the parent page.
Unfortunately I am not aware of a way for the limit_choices_to function to receive a reference to the parent objects. Your second link is on the right track, we will need to provide our own base form to the page and tweak the queryset of the author field.
from django.contrib.auth.models import Group
from wagtail.wagtailadmin.forms import WagtailAdminPageForm
from wagtail.wagtailcore.models import Page
class ArticlePageForm(WagtailAdminPageForm):
def __init__(self, data=None, files=None, parent_page=None, *args, **kwargs):
super().__init__(data, files, parent_page, *args, **kwargs)
# Get the parent category page if `instance` is present, fallback to `parent_page` otherwise.
# We're trying to get the parent category page from the `instance` first
# because `parent_page` is only set when the page is new (haven't been saved before).
instance = kwargs.get('instance')
category_page = instance.get_parent() if instance and instance.pk else parent_page
if not category_page:
return # Do not continue if we failed to find the parent category page.
# Get the groups which have permissions on the parent category page.
groups = Group.objects.filter(page_permissions__page_id=category_page.pk).distinct()
if not groups:
return # Do not continue if we failed to find any groups.
# Filter the queryset of the `author` field.
self.fields['author'].queryset = self.fields['author'].queryset.filter(groups__in=groups)
class ArticlePage(Page):
base_form_class = ArticlePageForm
A quick note on the way we query the groups:
When you set page permissions in Wagtail, you actually create a GroupPagePermission which has 2 main attributes, group and page. The related_name of the group's foreign key of the GroupPagePermission is defined as page_permissions and every time you create a ForeignKey like with page, it actually creates a field called page_id. Therefore we can follow the relationship and filter the groups by page_permissions__page_id with the ID of the parent category page.
Related
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.
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
I need to make an "owners" login for the admin. Say we have this model structure:
class Product(models.Model):
owner = models.ManyToManyField(User)
name = models.CharField(max_length=255)
description = models.CharField(max_length=255)
photos = models.ManyToManyField(Photo, through='ProductPhoto')
class Photo(models.Model):
order = models.IntegerField()
image = models.ImageField(upload_to='photos')
alt = models.CharField(max_length=255)
class ProductPhoto(models.Model):
photo = models.ForeignKey(Photo)
product = models.ForeignKey(Product)
We have a group called Owners that some users are part of. The ProductPhoto is a TabularInline on the Product admin page.
Now, owners need permission to edit
(primary goal) only products where product__in=user.products (so basically, only products owned by them).
(secondary goal) only the description and photos of products
How would I do this with Django's admin/permission system?
This is row (or object) level permission. Django provides basic support for object permissions but it is up to you to implement the code.
Luckily, there are a few apps that provide drop-in object-level permission framework. django-guardian is one that I have used before. This page on djangopackages.com provides some more that you can try out.
You may implement using get_form. For complex rule, you can add this too: https://github.com/dfunckt/django-rules
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
# permission check;
if form.base_fields and not request.user.is_superuser:
# when creating or updating by non-reviewer (except superuser)
# allow only reviewer to allow updating
form.base_fields['usertype'].disabled = True
I'm building a django tastypie api, and I have a problem with adding elements in ManyToMany relationships
Example,
models.py
class Picture(models.db):
""" A picture of people"""
people = models.ManyToManyField(Person, related_name='pictures',
help_text="The people in this picture",
)
class Person(models.db):
""" A model to represet a person """
name = models.CharField(max_length=200,
help_text="The name of this person",
)
resources:
class PictureResource(ModelResource):
""" API Resource for the Picture model """
people = fields.ToManyField(PersonResource, 'people', null=True,
related_name="pictures", help_text="The people in this picture",
)
class PersonResource(ModelResource):
""" API Resource for the Person model """
pictures = fields.ToManyField(PictureResource, 'pictures', null=True,
related_name="people", help_text="The pictures were this person appears",
)
My problem is that I would like to have an add_person end point in my picture resource.
If I use PUT, then I need to specify all the data in the picture
If I use PATCH, I still need to specify all the people in the picture.
Of course I could simply generate the /api/picture/:id/add_people URL and there I could handle my problem. The problem with that is that it does not feel clean.
Another solution would be to generate the /api/picture/:id/people end point, and there I could do GET, POST, PUT, like it's a new resource, but I don't know how to implement this and it seems strange to create new people under this resource.
Any thoughts?
I implemented this by overriding the save_m2m function of the API Resource. Here is an example using your models.
def save_m2m(self, bundle):
for field_name, field_object in self.fields.items():
if not getattr(field_object, 'is_m2m', False):
continue
if not field_object.attribute:
continue
if field_object.readonly:
continue
# Get the manager.
related_mngr = getattr(bundle.obj, field_object.attribute)
# This is code commented out from the original function
# that would clear out the existing related "Person" objects
#if hasattr(related_mngr, 'clear'):
# Clear it out, just to be safe.
#related_mngr.clear()
related_objs = []
for related_bundle in bundle.data[field_name]:
# See if this person already exists in the database
try:
person = Person.objects.get(name=related_bundle.obj.name)
# If it doesn't exist, then save and use the object TastyPie
# has already prepared for creation
except Person.DoesNotExist:
person = related_bundle.obj
person.save()
related_objs.append(person)
related_mngr.add(*related_objs)
I'm stuck trying to figure how to do the following:
I have a few entities:
PurchaseItem (an item in user's cart),
Order (an order - combines one or many PurchaseItems),
OrderStatusHistory (that's status items for the Order - instead of changing, I create new ones to be able to retrospectively preview how status changed over time).
I don't want any of these to be created via admin - they are all created via public interface, but I have to show the Order and its attributes in the admin panel:
I need to be able to show list of orders. That's simple.
When I click on an order or something I want to be able to view the order's details:
list of Purchase items.
I need to be able to change the status of the order - selecting from a drop down or something - however, this action show be triggering a new statusHistory item creation.
Is this all possible with admin interface or should I forget about it and create my own implementation with pages and all?
My models look like this:
class Order(models.Model):
dateCreated = models.DateTimeField(null=False,default=datetime.now())
items = models.ManyToManyField(PurchaseItem)
user_name = models.CharField(null=True,blank=True,max_length=200)
phone = models.CharField(null=False,blank=False,max_length=11,validators=[validate_phone])
phone_ext = models.CharField(null=True,blank=True,max_length=5,validators=[validate_phone_ext])
email = models.CharField(null=False,blank=False,max_length=100,validators=[validators.EmailValidator])
addressCity = models.CharField(null=False,blank=False,max_length=100)
addressStreet = models.CharField(null=False,blank=False,max_length=200)
notes = models.TextField(null=True,blank=True)
accessKey = models.CharField(max_length=32,default=CreateAccessKey())
class PurchaseItem(models.Model):
picture = models.ForeignKey(Picture, null=False)
paperType = models.CharField(null=False,max_length=200)
printSize = models.CharField(null=False,max_length=200)
quantity = models.IntegerField(default=1, validators=[validators.MinValueValidator(1)])
price = models.DecimalField(decimal_places=2,max_digits=8)
dateCreated = models.DateTimeField(null=False)
cost = models.DecimalField(decimal_places=2,max_digits=8)
class OrderStatusHistory(models.Model):
orderId = models.ForeignKey(Order)
dateSet = models.DateTimeField(null=False,default=datetime.now())
status = models.IntegerField(choices=OrderStatus,default=0,null=False,blank=False)
comment = models.TextField(null=True,blank=True)
The following inline setup doesn't work because Order doesn't have a FK to PurchaseItems:
class OrderStatusHistoryAdmin(admin.StackedInline):
model = OrderStatusHistory
class PurchaseItemAdmin(admin.StackedInline):
model = PurchaseItem
class OrderAdmin(admin.ModelAdmin):
model = Order
inlines = [OrderStatusHistoryAdmin,PurchaseItemAdmin]
admin.site.register(Order,OrderAdmin)
Part 1
Use Inlines, that's very straight forward and django excels at this.
Part 2
Sure you could override your save for example and check if the drop down item has changed. If it has, generate your order status history object.
def save(self, *args, **kwargs):
if self._initial_data['status'] != self.__dict__['status']:
self.orderstatushistory_set.create("Status Changed!")
super(Order, self).save(*args, **kwargs)
You could do the same thing in the ModelAdmin too
def save_model(self, request, obj, form, change):
if obj._initial_data['status'] != obj.__dict__['status']:
# create whatever objects you wish!
Part 1:
You can 'nest' models with TabularInline or StackedInline admin models.
class OrderAdmin(admin.ModelAdmin):
model = Order
inlines = [
OrderStatusAdmin,
PurchaseItemAdmin
]
class OrderStatusAdmin(admin.StackedInline):
model = OrderStatus
class PurchaseAdmin(admin.StackedInline):
model = PurchaseItem
More information can be found here: http://docs.djangoproject.com/en/dev/ref/contrib/admin/#inlinemodeladmin-objects
Part 2:
I need to be able to change the status of the order - selecting from a drop down or something - however, this action show be triggering a new statusHistory item creation.
For this you can use signals. There is a post_save and pre_save. So each time you save an order you can add extra logic. The pre_save signal has a sender and an instance so I think you can compare the status of the sender and the instance to be saved and if it changed you can add an other OrderStatus model.
More info can be found here:
http://docs.djangoproject.com/en/dev/ref/signals/#pre-save