Wagtail: Get previous or next sibling - python

I'm creating a page with wagtail where I need to know the previous and next sibling of the current page:
In my portrait page model, I tried to define two methods to find the correct urls, but I'm missing a crucial part. To get the first sibling, I can just do the following:
class PortraitPage(Page):
...
def first_portrait(self):
return self.get_siblings().live().first().url
There is the first() and last() method, but there doesn't seem to be a next() or previous() method to get the direct neighbours (in the order that they are arranged in the wagtail admin).
Is there any way to achieve this?

Django-Treebeard provides get_next_sibling and get_prev_sibling which will return your direct siblings in the tree, but these are not necessarily your next published sibling. To request those you can use:
prev = page.get_prev_siblings().live().first()
next = page.get_next_siblings().live().first()
Which can obviously also be chained with any other queryset operations.

After going through the debugger for a while, I found out that wagtail already has two methods: get_prev_sibling() and get_next_sibling().
So the methods could look like this (accounting for the first page in the previous method and the last item in the next method):
def prev_portrait(self):
if self.get_prev_sibling():
return self.get_prev_sibling().url
else:
return self.get_siblings().last().url
def next_portrait(self):
if self.get_next_sibling():
return self.get_next_sibling().url
else:
return self.get_siblings().first().url

Here's a version handling non-published siblings.
def next_portrait(self):
next_sibling = self.get_next_sibling()
if next_sibling and next_sibling.live:
return next_sibling.url
else:
next_published_siblings = self.get_next_siblings(
inclusive=False
).live()
if len(next_published_siblings):
return next_published_siblings[0].url
return self.get_siblings().live().first().url
def prev_portrait(self):
previous_sibling = self.get_prev_sibling()
if previous_sibling and previous_sibling.live:
return previous_sibling.url
else:
previous_published_siblings = self.get_prev_siblings(
inclusive=False
).live()
if len(previous_published_siblings):
return previous_published_siblings[0].url
return self.get_siblings().live().last().url

You can define properties in your class that inherits from Page
Siblings
If you want the siblings of an instance of Page, you can use the following (based on Danielle Madeley's answer):
class PortraitPage(Page):
# ...
#property
def next_sibling(self):
return self.get_next_siblings().live().first()
#property
def prev_sibling(self):
return self.get_prev_siblings().live().first()
Siblings of the same class
If you want the the siblings of PortraitPage, specify self.__class__ in the type method as follows:
class PortraitPage(Page):
# ...
#property
def next_sibling(self):
return self.get_next_siblings().type(self.__class__).live().first()
#property
def prev_sibling(self):
return self.get_prev_siblings().type(self.__class__).live().first()
Template
If you want to use them in a template, after defining the properties, do the following:
{# This is a template #}
Previous Sibling: {{ page.next_sibling }}
Next Sibling: {{ page.prev_sibling }}

self.get_siblings() falls over if you want to do any filtering based on properties only found in the subclass since the PageQuerySet results are of type Page.
For me, I have a blog index page that can be filtered by category or tag.
The bog detail page has cards for the next and previous blog post at the end.
I wanted those prev/next posts to be according to the filter I landed on that page from.
To get around this, you need to query the objects belonging to the subclass (eg BlogDetailPage), filter those, then get the prev/next post using class.objects.sibling_of(self):
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)
siblings = self.__class__.objects.sibling_of(self).live()
category_filter = request.GET.get("category", None)
tag_filter = request.GET.get("tag", None)
if category_filter:
siblings = siblings.filter(categories__slug__in=category_filter.split(","))
context["filter"] = '?category=' + category_filter
elif tag_filter:
siblings = siblings.filter(tags__slug__in=tag_filter.split(','))
context["filter"] = '?tag=' + tag_filter
else:
context["filter"] = ''
context["next_post"] = siblings.filter(path__gt=self.path).first()
context["previous_post"] = siblings.filter(path__lt=self.path).first()
return context
I used self.__class__.objects.sibling_of(self) as this is in a super class with sub classed blog pages.

Related

OTree / Django send player to results/ exit page if StringField input not in my dict

I’m working with OTree with self: https://otree.readthedocs.io/en/self/
On the first page of my experiment, I ask the players to provide an access code / their matriculation number via StringField. If their input is not in my dict in Constants, I want to send them directly to a page where I tell them „Sorry, you can’t participate“ with the only option for them to click the next button and exit the experiment.
I have tried the following:
in models.py
class Constants(BaseConstants):
name_in_url = 'prisoners_test1'
players_per_group = 2
num_rounds = 1
matriculation_dict = {
'123': ('Adam Smith', 'Economics'),
'124': ('Ada Lovelace', 'Programming'),
'125': ('Charles Babbage', 'Mathematics'),
}
class Player(BasePlayer):
matriculation = models.StringField(label='Please provide your Matriculation Number')
access = models.BooleanField()
def matriculation_not_found(self):
if self.matriculation in Constants.matriculation_dict:
self.access = True
else: self.access = False
in pages.py
class ExcludedPlayer(Page):
def is_displayed(self):
return self.player.access == False
page_sequence = [Matriculation, ExcludedPlayer, P1_Decision, P2_Decision, ResultsWaitPage, Results]
The problem is that the value of access is not updated through my if statement.
My second problem is that even if the page ExcludedPlayer is displayed (b/c I set initial value of access = False), the player is directed to the other pages (P1_Decision, ResultsWaitPage, Results) after clicking next. How can I end the game for the excluded player?
Thank you for your help!
To your first problem:
To update the access field, you need to call your matriculation_not_found method somewhere. A good place for this is the otree built-in method before_next_page in your Matriculation class:
class Matriculation(Page):
def before_next_page(self):
self.player.matriculation_not_found()
Or in newer otree versions (no-self format):
class Matriculation(Page):
#staticmethod
def before_next_page(player, timeout_happened):
player.matriculation_not_found()
To your second problem:
The easiest way to prevent the excluded players from seeing the upcoming pages is to remove the next-button. Simply delete the following line from the ExcludedPlayer.html template:
{{ next_button }}
If for some reason you don't want that, you can also check on each of the upcoming pages in the is_displayed method whether access is allowed or not. For example for the P1_Decision page:
class P1_Decision(Page):
def is_displayed(self):
return self.player.access
And again the same in the new no-self format:
class P1_Decision(Page):
#staticmethod
def is_displayed(player):
return player.access
Another alternative would be to swap out the ExcludedPlayers page to a later app (let's call it 'exculuded_players_app') and skip the pages (and apps) in between using the app_after_this_page method:
class Matriculation(Page):
def before_next_page(self):
self.player.matriculation_not_found()
def app_after_this_page(self, upcoming_apps):
if not self.player.access:
return 'exculuded_players_app'
And again the same in the new no-self format:
class Matriculation(Page):
#staticmethod
def before_next_page(player, timeout_happened):
player.matriculation_not_found()
#staticmethod
def app_after_this_page(player, upcoming_apps):
if not player.access:
return 'exculuded_players_app'

Wagtail/Django: Query filter to return only the pages the user has acess/permissions?

I'm going through the documentation at: http://docs.wagtail.io/en/v2.7.1/reference/pages/queryset_reference.html.
Is there a filter to return only the pages the user has access to? I can only see public() and not_public().
I have some pages which privacy is set to Private (accessible to users in specific groups). And would like to exclude them from the query results.
There is no such filter in PageQuerySet. You can however create your own QuerySet that adds an authorized filter and use that. The following code comes from the Joyous events EventQuerySet and is based upon PageQuerySet.public_q and BaseViewRestriction.accept_request. It gets all the restrictions that could apply, excludes the ones that the user passes, and then filters out the pages with the remaining restrictions.
from wagtail.core.query import PageQuerySet
from wagtail.core.models import Page, PageManager, PageViewRestriction
class MyQuerySet(PageQuerySet):
def authorized_q(self, request):
PASSWORD = PageViewRestriction.PASSWORD
LOGIN = PageViewRestriction.LOGIN
GROUPS = PageViewRestriction.GROUPS
KEY = PageViewRestriction.passed_view_restrictions_session_key
restrictions = PageViewRestriction.objects.all()
passed = request.session.get(KEY, [])
if passed:
restrictions = restrictions.exclude(id__in=passed,
restriction_type=PASSWORD)
if request.user.is_authenticated:
restrictions = restrictions.exclude(restriction_type=LOGIN)
if request.user.is_superuser:
restrictions = restrictions.exclude(restriction_type=GROUPS)
else:
membership = request.user.groups.all()
if membership:
restrictions = restrictions.exclude(groups__in=membership,
restriction_type=GROUPS)
q = models.Q()
for restriction in restrictions:
q &= ~self.descendant_of_q(restriction.page, inclusive=True)
return q
def authorized(self, request):
self.request = request
if request is None:
return self
else:
return self.filter(self.authorized_q(request))
You could then set this to be your model's default QuerySet.
class MyPage(Page):
objects = PageManager.from_queryset(MyQuerySet)()
Then when filtering your MyPage objects you can say MyPage.objects.live().authorized(request).all()
Hope that is helpful. May contain bugs.

Django find item in queryset and get next

I'm trying to take an object, look up a queryset, find the item in that queryset, and find the next one.
#property
def next_object_url(self):
contacts = Model.objects.filter(owner=self.owner).order_by('-date')
place_in_query = list(contacts.values_list('id', flat=True)).index(self.id)
next_contact = contacts[place_in_query + 1]
When I add this to the model and run it, here's what I get for each variable for one instance.
CURRENT = Current Object
NEXT = Next Object
contacts.count = 1114
self.id = 3533 #This is CURRENT.id
place_in_query = 36
contacts[place_in_query] = NEXT
next_contact = CURRENT
What am i missing / what dumb mistake am i making?
In your function, contacts is a QuerySet. The actual objets are not fetched in the line:
contacts = Model.objects.filter(owner=self.owner).order_by('-date')
because you don’t use a function like list(), you don’t iterate the QuerySet yet... It is evaluated later. This is probably the reason of your problem.
Since you need to search an ID in the list of contacts and the find the next object in that list, I think there is no way but fetch all the contact and use a classic Python loop to find yours objects.
#property
def next_object_url(self):
contacts = list(Model.objects.filter(owner=self.owner).order_by('-date').all())
for curr_contact, next_contact in zip(contacts[:-1], contacts[1:]):
if curr_contact.id == self.id:
return next_contact
else:
# not found
raise ContactNotFoundError(self.id)
Another solution would be to change your database model in order to add a notion of previous/next contact at database level…

Why is this class and corresponding attribute not being destroyed between requests?

I can not understand this behavior at all. I asked a question yesterday or the day before thinking it was something with bottle.py, but after trying all kinds of possible solutions, even converting my app over to flask, I have pinpointed the behavior to a single, very simple, class, but I have NO idea why this is happening. It's confusing the hell out of me, but I would really like to understand this if anyone can please shed some light on it.
Ok, so first I have a class called Page which simplifies setting up templates a bit, this is the offending class:
class Page:
"""The page object constructs the webpage and holds associated variables and templates"""
def __init__(self, template=None, name = '', title='',template_vars={}, stylesheets=[], javascript=[]):
# Accepts template with or without .html extension, but must be added for lookup
self.stylesheet_dir = '/css'
self.javascript_dir = '/js'
self.template = template
self.template_vars = {}
self.name = name
self.title = title
self.stylesheets = stylesheets
self.javascript = javascript
self.template_vars['debug'] = _Config.debug
self.template_vars['title'] = self.title
self.template_vars['stylesheets'] = self.stylesheets
self.template_vars['javascript'] = self.javascript
def render(self):
"""Should be called after the page has been constructed to render the template"""
if not self.template.endswith(_Config.template_extension):
self.template += '.' + _Config.template_extension
if not self.title:
if self.name:
self.title = _Config.website + _Config.title_delimiter + self.name
else:
# If title not given use template name
self.title = _Config.website + _Config.title_delimiter + self.template.rstrip('.html')
try:
template = env.get_template(self.template)
except AttributeError:
raise (AttributeError, 'template not set')
rendered_page = template.render(self.template_vars)
return rendered_page
def add_stylesheet(self, name, directory=None):
# Sanitize name
if not name.endswith('.css'):
name += '.css'
if name.startswith('/'):
name = name.lstrip('/')
if not directory:
directory = self.stylesheet_dir
self.template_vars['stylesheets'].append(directory + '/' + name)
def add_javascript(self, name, directory=None):
# Sanitize name
if not name.endswith('.js'):
name += '.js'
if name.startswith('/'):
name = name.lstrip('/')
if not directory:
directory = self.javascript_dir
self.template_vars['javascript'].append(directory + '/' + name)
And here is an example of a route that the problem is exhibited:
#route('/create_account', method=['GET','POST'])
def create_account():
dbsession = db.Session()
page = Page('create account')
page.add_javascript('create_account.js')
if request.method == 'GET':
page.template = 'create_account'
page.template_vars['status'] = None
document = page.render()
dbsession.close()
return document
elif request.method == 'POST':
# Omitted
The problem lies with the method Page.add_javascript(). The next time I go to the /create_account page it is not creating a new Page object and instead reusing the old one. I know this because if I go to the page twice I will have two entires for the create_account.js in my returned html document(the template simply runs a for loop and creates a tag for any js files passed in that list). If I go 3 times it'll be listed 3 times, 40, 40 and so on. Now however if I simply change it to use the initializer and not the add_javascript method the problem goes away.
#route('/create_account', method=['GET','POST'])
def create_account():
dbsession = db.Session()
page = Page('create account', javascript=['create_account.js'])
if request.method == 'GET':
page.template = 'create_account'
page.template_vars['status'] = None
document = page.render()
dbsession.close()
return document
elif request.method == 'POST':
However I suspect something is still wrong and just for my own sanity I need to understand what the hell is going on here. What is possibly happeneing behind the scenes where the add_javascript method would be called twice on the same page object? The method is called immediately after creating a new instance of the page object, where is it possibly getting the old contents of template_vars from?
The problem is that you use mutable defaults for your Page.__init__ function. See http://docs.python-guide.org/en/latest/writing/gotchas/#mutable-default-arguments.
So you do get a new Page instance on each request, but the lists/dictionaries that hold your javascript etc are re-used.
Replace list/dict default argument values with None, check for None in __init__.

sqlalchemy access parent class attribute

Looking at the bottom of the post you can see i have three classes. The code here is pseudo code written on the fly and untested however it adequately shows my problem. If we need the actual classes I can update this question tomorrow when at work. So ignore syntax issues and code that only represents a thought rather than the actual "code" that would do what i describe there.
Question 1
If you look at the Item search class method you can see that when the user does a search i call search on the base class then based on that result return the correct class/object. This works but seems kludgy. Is there a better way to do this?
Question 2
If you look at the KitItem class you can see that I am overriding the list price. If the flag calc_list is set to true then I sum the list price of the components and return that as the list price for the kit. If its not marked as true I want to return the "base" list price. However as far as I know there is no way to access a parent attribute since in a normal setup it would be meaningless but with sqlalchemy and shared table inheritance it could be useful.
TIA
class Item(DeclarativeBase):
__tablename__ = 'items'
item_id = Column(Integer,primary_key=True,autoincrement=True)
sku = Column(Unicode(50),nullable=False,unique=True)
list_price = Column(Float)
cost_price = Column(Float)
item_type = Column(Unicode(1))
__mapper_args__ = {'polymorphic_on': item_type}
__
def __init__(self,sku,list_price,cost_price):
self.sku = sku
self.list_price = list_price
self.cost_price = cost_price
#classmethod
def search(cls):
"""
" search based on sku, description, long description
" return item as proper class
"""
item = DBSession.query(cls).filter(...) #do search stuff here
if item.item_type == 'K': #Better way to do this???
return DBSession.query(KitItem).get(item.item_id)
class KitItem(Item):
__mapper_args__ = {'polymorphic_identity': 'K'}
calc_list = Column(Boolean,nullable=False,default=False)
#property
def list_price(self):
if self.calc_list:
list_price = 0.0
for comp in self.components:
list_price += comp.component.list_price * comp.qty
return list_price
else:
#need help here
item = DBSession.query(Item).get(self.item_id)
return item.list_price
class KitComponent(DeclarativeBase):
__tablename__ = "kit_components"
kit_id = Column(Integer,ForeignKey('items.item_id'),primarykey=True)
component_id = Column(Integer,ForeignKey('items.item_id'),primarykey=True)
qty = Column(Integer,nullable=False, default=1)
kit = relation(KitItem,backref=backref("components"))
component = relation(Item)
Answer-1: in fact you do not need to do anything special here: given that you configured your inheritance hierarchy properly, your query will already return proper class for every row (Item or KitItem). This is the advantage of the ORM part. What you could do though is to configure the query to immediatelly load also the additional columns which do belong to children of Item (from your code this is only calc_list column), which you can do by specifying with_polymorphic('*'):
#classmethod
def search(cls):
item = DBSession.query(cls).with_polymorphic('*').filter(...) #do search stuff here
return item
Read more on this in Basic Control of Which Tables are Queried.
To see the difference, enabled SQL logging, and compare your tests scripts with and without with_polymorphic(...) - you will most probably require less SQL statements being executed.
Answer-2: I would not override one entry attributed with one which is purely computed. Instead I would just create another computed attribute (lets call it final_price), which would look like following for each of two classes:
class Item(Base):
...
#property
def total_price(self):
return self.list_price
class KitItem(Item):
...
#property
def total_price(self):
if self.calc_list:
_price = 0.0
for comp in self.components:
_price += comp.component.list_price * comp.qty
return _price
else:
# #note: again, you do not need to perform any query here at all, as *self* is that you need
return self.list_price
Also in this case, you might think of configuring the relationship KitItem.components to be eagerly loaded, so that the calculation of the total_price will not trigger additional SQL. But you have to decide yourself if this is beneficial for your use cases (again, analyse the SQLs generated in your scenario).

Categories