Sitemap Generation: Update lastmod/last_published_at of Page Manually - python

I have a PublicationsPage and a PublicationPage class.
The PublicationsPage shows a list of its direct children and renders them in a short preview list at http://mysite/publications.
class PublicationsPage(Page):
# ...
def get_context(self, request):
context = super().get_context(request)
publications = PublicationPage.objects.child_of(self).live()
context['publications'] = publications
return context
This means whenever a new PublicationPage is added/deleted/modified the list updates accordingly. But since I am not updating the PublicationsPage the lastmod/last_published_at attribute of the /publications location never changes. Wouldn't this be missleading for a search engine?
A really hacky attempt could be to update the last_published_at date of the parent page every time I touch a child entry.
class PublicationPage(Page):
# ...
def save(self, *args, **kwargs):
result = super().save(*args, **kwargs)
from datetime import datetime
parent_page = self.get_parent()
parent_page.last_published_at = datetime.now()
parent_page.save()
return result
Any other suggestions?

When generating the sitemap, you can set the property lastmod, which accepts a method, called for every item of the sitemap.
Therefore, when generating the PublicationsPage sitemap, set that property to a method that queries all the children of each PublicationsPage item and return the latest date.

You can define get_sitemap_urls on your Page model, something like this:
class PublicationsPage(Page):
def get_sitemap_urls(self, request=None):
# Get the defaults sitemap URLS.
urls = super().get_sitemap_urls(request=request)
# Get the last modifications time of your publications.
publications = PublicationPage.objects.child_of(self).live()
last_publication = publications.order_by('-last_published_at').first()
# Update the default entry.
if last_publication:
urls[0]['lastmod'] = last_publication.last_published_at
# Return the urls.
return urls

Related

Django-Haystack & Elasticsearch brake with queries containing special characters

So I've been trying to fix a bug that really annoys me: Django-Haystack & Elasticsearch queries are working with accents but it brakes everytime with queries containing special characters like dash - and apostrophes '.
For example let's use Baie-d'Urfé as the query.
Here's my code:
forms.py
class FacetedProductSearchForm(FacetedSearchForm):
def __init__(self, *args, **kwargs):
data = dict(kwargs.get("data", []))
self.ptag = data.get('ptags', [])
self.q_from_data = data.get('q', '')
super(FacetedProductSearchForm, self).__init__(*args, **kwargs)
def search(self):
sqs = super(FacetedProductSearchForm, self).search()
# Ideally we would tell django-haystack to only apply q to destination
# ...but we're not sure how to do that, so we'll just re-apply it ourselves here.
q = self.q_from_data
sqs = sqs.filter(destination=Exact(q))
print('should be applying q: {}'.format(q))
print(sqs)
if self.ptag:
print('filtering with tags')
print(self.ptag)
sqs = sqs.filter(ptags__in=[Exact(tag) for tag in self.ptag])
return sqs
Using FacetedSearch in View.py
class FacetedSearchView(BaseFacetedSearchView):
form_class = FacetedProductSearchForm
facet_fields = ['ptags']
template_name = 'search_result.html'
paginate_by = 30
context_object_name = 'object_list'
And my search_indexes.py
class ProductIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.EdgeNgramField(
document=True, use_template=True,
template_name='search/indexes/product_text.txt')
destination = indexes.CharField(model_attr="destination") #boost=1.125
# Tags
ptags = indexes.MultiValueField(model_attr='_ptags', faceted=True)
# for auto complete
content_auto = indexes.EdgeNgramField(model_attr='destination')
# Spelling suggestions
suggestions = indexes.FacetCharField()
def get_model(self):
return Product
def index_queryset(self, using=None):
"""Used when the entire index for model is updated."""
return self.get_model().objects.filter(timestamp__lte=timezone.now())
Any ideas on how to fix this?
Thanks a lot!
The problem seems to be related to Elasticsearch itself, so what I did is remove all my Elasticsearch instances and reformulated my search view to simple postgresql queries.
Final observation after solving this:
50$ / month saved and a search engine working like a charm!

Django CursorPagination for ordering by several fields

I am using Django DRF's CursorPagination for lazy loading of my data, and currently my goal is to sort the data by more than one field.
This is how my code looks like now:
class EndlessPagination(CursorPagination):
ordering_param = ''
def set_ordering_param(self, request):
self.ordering = request.query_params.get(self.ordering_param, None)
if not self.ordering:
raise ValueError('Url must contain a parameter named ' +
self.ordering_param)
if self.ordering.startswith("\"") or self.ordering.endswith("\""):
raise ValueError('Ordering parameter should not include quotation marks'
def paginate_queryset(self, queryset, request, view=None):
# This function is designed to set sorting param right in the URL
self.set_ordering_param(request)
return super(EndlessPagination, self).paginate_queryset(queryset, request, view)
This code works fine for urls like my_url/sms/270380?order_by=-timestamp, but what if I want to sort by several fields ?
Use str.split() to split the url params
class EndlessPagination(CursorPagination):
ordering_param = 'order_by'
def set_ordering_param(self, request):
ordering_param_list = request.query_params.get(self.ordering_param, None)
self.ordering = ordering_param_list.split(',')
# here, "self.ordering" will be a "list", so, you should update the validation logic
"""
if not self.ordering:
raise ValueError('Url must contain a parameter named ' +
self.ordering_param)
if self.ordering.startswith("\"") or self.ordering.endswith("\""):
raise ValueError('Ordering parameter should not include quotation marks'
"""
def paginate_queryset(self, queryset, request, view=None):
# This function is designed to set sorting param right in the URL
self.set_ordering_param(request)
return super(EndlessPagination, self).paginate_queryset(queryset, request, view)
Example URLs
1. my_url/sms/270380?order_by=-timestamp
2. my_url/sms/270380?order_by=-timestamp,name
3. my_url/sms/270380?order_by=-name,foo,-bar
UPDATE-1
First of all thanks to you for giving a chance to dig deep :)
As you said, me too didn't see comma seperated query_params in popular APIs. So, Change the url format to something like,my_url/sms/270380??order_by=-name&order_by=foo&order_by=-bar
At this time, the request.query_params['order_by'] will be a list equal to ['-name','foo','-bar']. So, you don't want to use the split() function, hence your set_ordering_param() method become,
def set_ordering_param(self, request):
self.ordering = request.query_params.get(self.ordering_param, None)
#...... your other validations

Django-tables2 - column name is changed after refreshing page

I'm facing a very weird behaviour of Django and Django-tables2.
I use table to help to render multiple types of documents.
For simplicity, there are two types of documents - 'faktura' and 'dobropis'.
'dobropis' has to have first column labeled "Dobropisujeme Vám" and 'faktura' - "Názov položky" + those columns are equal.
So I'm checking, if the type of Document is 'faktura' or 'dobropis' inside Table __init__ function and accordingly set
self.base_columns['column'].verbose_name = ...
The weird thing is that it works, but only after second refresh.
Situation: I've opened 'faktura' page - I can see 'Názov položky' which is ok. Then I open 'dobropis' page and I see 'Názov položky' label again. Then, if I refresh again, there is 'Dobropisujeme Vám'. Then, after each refresh, I see correct label - 'Dobropisujeme Vám'. But, if I open 'faktura' page, there is 'Dobropisujeme Vám' too for the first time, after second refresh it goes normal.
class PolozkyTable(tables.Table):
nazov_polozky = tables.columns.Column(orderable=False)
pocet = tables.columns.Column(orderable=False)
jednotka = tables.columns.Column(orderable=False)
cena = tables.columns.Column(orderable=False, verbose_name=u'Jednotková cena')
zlava = tables.columns.Column(orderable=False)
dph = tables.columns.Column(orderable=False)
celkom = tables.columns.Column(orderable=False)
class Meta:
model = Polozka
fields = ['nazov_polozky', 'pocet', 'jednotka', 'cena', 'zlava', 'dph', 'celkom']
attrs = {'class': 'table table-striped table-hover'}
def __init__(self, *args, **kwargs):
typ = kwargs.pop('typ')
super(PolozkyTable, self).__init__(*args, **kwargs)
if typ == 'dobropis':
self.base_columns['nazov_polozky'].verbose_name = u'Dobropisujeme Vám'
self.exclude = ['zlava', 'dph']
else:
self.base_columns['nazov_polozky'].verbose_name = u'Názov položky'
def render_cena(self, record):
return '{} {}'.format(record.cena, record.doklad.get_fakturacna_mena_display().encode('utf-8'))
def render_celkom(self, record):
return '{} {}'.format(record.celkom, record.doklad.get_fakturacna_mena_display().encode('utf-8'))
#login_required
def doklad_detail(request):
doklad = get_object_or_404(Doklad, pk=request.GET.get('id'))
polozky_table = PolozkyTable(doklad.polozky.all(),typ=doklad.typ)
return render(request, 'pdf/{}_pdf_template.html'.format(doklad.typ),
{'doklad': doklad, 'polozky_table': polozky_table})
There is no cache in this project and I really don't know what it could be.
Do you know?
EDIT:
Moreover, when I open 'faktura' after restarting the server, it shows camelcased 'Názov Položky' instead of 'Názov položky'. This is another weird thing because I searched whole project for case sensitive 'Názov Položky' and there is no such string.
EDIT2:
I've solved this creating separate table for 'dobropis' but I'm still curious which caused this problem. My colleague had the same problem on his PC.
You shouldn't be modifying base_columns in the __init__ method -- you are changing the value on the class, so it affects other views as well.
The base_columns have already been copied in the super() call before you alter them, so the changes only show up the next time you access the view.
It looks like you should be modifying self.columns instead. That should only affect the instance. Try setting header instead of verbose_name.
self.columns['nazov_polozky'].header = u'Dobropisujeme Vám'

Why is this Django QuerySet returning no results?

I have this Class in a project, and I'm trying to get previous and next elements of current one.
def get_context(self, request, *args, **kwargs):
context = super(GuidePage, self).get_context(request, *args, **kwargs)
context = get_article_context(context)
all_guides = GuidePage.objects.all().order_by("-date")
context['all_guides'] = all_guides
context['next_guide'] = all_guides.filter(date__lt=self.date)
context['prev_guide'] = all_guides.filter(date__gt=self.date)
print context['next_guide']
print context['prev_guide']
return context
These two lines:
context['prev_guide'] = all_guides.filter(date__lt=self.date)
context['next_guide'] = all_guides.filter(date__gt=self.date)
are returning empty results as printed in the console:
(QuerySet[])
(QuerySet[])
What am I missing?
EDIT:
I changed lt and gt to lte and gte. As I understand that will include results that are also equal in date.
In this case I got ALL elements. All elements were created the same day, but, of course, at different times, so they should be different by minutes. Is this difference not taking into account when filtering for greater/lesser ?
If you want to filter not only by date, but time also, you must change the relevant field in your model to be of DateTimeField instead of DateField.
Like this:
from django.db import models
class MyModel(models.Model):
date_time = models.DateTimeField()
Now, you can do stuff like all_guides.filter(date_time__lte=self.date_time) or all_guides.filter(date_time__gte=self.date_time).
Carefull of the two underscores __.

Plone: Change default schemata of a content type

I am using the Products.FacultyStaffDirectory product to manage contacts in my website.
One of the content types, i.e. FacultyStaffDirectory, has the description field in the 'categorization' tab when you are in edit mode. Now I need to remove the description field and put it in the default tab.
To do this, I'm using archetypes.schemaextender product to edit the field. In particular, I am using ISchemaModifier.
I have implemented the code but the field is still being shown in the categorization tab. May be something I missed. Below is the code:
This is the class that contains the description field that I want to modify:
# -*- coding: utf-8 -*-
__author__ = """WebLion <support#weblion.psu.edu>"""
__docformat__ = 'plaintext'
from AccessControl import ClassSecurityInfo
from Products.Archetypes.atapi import *
from zope.interface import implements
from Products.FacultyStaffDirectory.interfaces.facultystaffdirectory import IFacultyStaffDirectory
from Products.FacultyStaffDirectory.config import *
from Products.CMFCore.permissions import View, ManageUsers
from Products.CMFCore.utils import getToolByName
from Products.ATContentTypes.content.base import ATCTContent
from Products.ATContentTypes.content.schemata import ATContentTypeSchema, finalizeATCTSchema
from Products.membrane.at.interfaces import IPropertiesProvider
from Products.membrane.utils import getFilteredValidRolesForPortal
from Acquisition import aq_inner, aq_parent
from Products.FacultyStaffDirectory import FSDMessageFactory as _
schema = ATContentTypeSchema.copy() + Schema((
LinesField('roles_',
accessor='getRoles',
mutator='setRoles',
edit_accessor='getRawRoles',
vocabulary='getRoleSet',
default = ['Member'],
multiValued=1,
write_permission=ManageUsers,
widget=MultiSelectionWidget(
label=_(u"FacultyStaffDirectory_label_FacultyStaffDirectoryRoles", default=u"Roles"),
description=_(u"FacultyStaffDirectory_description_FacultyStaffDirectoryRoles", default=u"The roles all people in this directory will be granted site-wide"),
i18n_domain="FacultyStaffDirectory",
),
),
IntegerField('personClassificationViewThumbnailWidth',
accessor='getClassificationViewThumbnailWidth',
mutator='setClassificationViewThumbnailWidth',
schemata='Display',
default=100,
write_permission=ManageUsers,
widget=IntegerWidget(
label=_(u"FacultyStaffDirectory_label_personClassificationViewThumbnailWidth", default=u"Width for thumbnails in classification view"),
description=_(u"FacultyStaffDirectory_description_personClassificationViewThumbnailWidth", default=u"Show all person thumbnails with a fixed width (in pixels) within the classification view"),
i18n_domain="FacultyStaffDirectory",
),
),
))
FacultyStaffDirectory_schema = OrderedBaseFolderSchema.copy() + schema.copy() # + on Schemas does only a shallow copy
finalizeATCTSchema(FacultyStaffDirectory_schema, folderish=True)
class FacultyStaffDirectory(OrderedBaseFolder, ATCTContent):
"""
"""
security = ClassSecurityInfo()
implements(IFacultyStaffDirectory, IPropertiesProvider)
meta_type = portal_type = 'FSDFacultyStaffDirectory'
# Make this permission show up on every containery object in the Zope instance. This is a Good Thing, because it easy to factor up permissions. The Zope Developer's Guide says to put this here, not in the install procedure (http://www.zope.org/Documentation/Books/ZDG/current/Security.stx). This is because it isn't "sticky", in the sense of being persisted through the ZODB. Thus, it has to run every time Zope starts up. Thus, when you uninstall the product, the permission doesn't stop showing up, but when you actually remove it from the Products folder, it does.
security.setPermissionDefault('FacultyStaffDirectory: Add or Remove People', ['Manager', 'Owner'])
# moved schema setting after finalizeATCTSchema, so the order of the fieldsets
# is preserved. Also after updateActions is called since it seems to overwrite the schema changes.
# Move the description field, but not in Plone 2.5 since it's already in the metadata tab. Although,
# decription and relateditems are occasionally showing up in the "default" schemata. Move them
# to "metadata" just to be safe.
if 'categorization' in FacultyStaffDirectory_schema.getSchemataNames():
FacultyStaffDirectory_schema.changeSchemataForField('description', 'categorization')
else:
FacultyStaffDirectory_schema.changeSchemataForField('description', 'metadata')
FacultyStaffDirectory_schema.changeSchemataForField('relatedItems', 'metadata')
_at_rename_after_creation = True
schema = FacultyStaffDirectory_schema
# Methods
security.declarePrivate('at_post_create_script')
def at_post_create_script(self):
"""Actions to perform after a FacultyStaffDirectory is added to a Plone site"""
# Create some default contents
# Create some base classifications
self.invokeFactory('FSDClassification', id='faculty', title='Faculty')
self.invokeFactory('FSDClassification', id='staff', title='Staff')
self.invokeFactory('FSDClassification', id='grad-students', title='Graduate Students')
# Create a committees folder
self.invokeFactory('FSDCommitteesFolder', id='committees', title='Committees')
# Create a specialties folder
self.invokeFactory('FSDSpecialtiesFolder', id='specialties', title='Specialties')
security.declareProtected(View, 'getDirectoryRoot')
def getDirectoryRoot(self):
"""Return the current FSD object through acquisition."""
return self
security.declareProtected(View, 'getClassifications')
def getClassifications(self):
"""Return the classifications (in brains form) within this FacultyStaffDirectory."""
portal_catalog = getToolByName(self, 'portal_catalog')
return portal_catalog(path='/'.join(self.getPhysicalPath()), portal_type='FSDClassification', depth=1, sort_on='getObjPositionInParent')
security.declareProtected(View, 'getSpecialtiesFolder')
def getSpecialtiesFolder(self):
"""Return a random SpecialtiesFolder contained in this FacultyStaffDirectory.
If none exists, return None."""
specialtiesFolders = self.getFolderContents({'portal_type': 'FSDSpecialtiesFolder'})
if specialtiesFolders:
return specialtiesFolders[0].getObject()
else:
return None
security.declareProtected(View, 'getPeople')
def getPeople(self):
"""Return a list of people contained within this FacultyStaffDirectory."""
portal_catalog = getToolByName(self, 'portal_catalog')
results = portal_catalog(path='/'.join(self.getPhysicalPath()), portal_type='FSDPerson', depth=1)
return [brain.getObject() for brain in results]
security.declareProtected(View, 'getSortedPeople')
def getSortedPeople(self):
""" Return a list of people, sorted by SortableName
"""
people = self.getPeople()
return sorted(people, cmp=lambda x,y: cmp(x.getSortableName(), y.getSortableName()))
security.declareProtected(View, 'getDepartments')
def getDepartments(self):
"""Return a list of FSDDepartments contained within this site."""
portal_catalog = getToolByName(self, 'portal_catalog')
results = portal_catalog(portal_type='FSDDepartment')
return [brain.getObject() for brain in results]
security.declareProtected(View, 'getAddableInterfaceSubscribers')
def getAddableInterfaceSubscribers():
"""Return a list of (names of) content types marked as addable using the
IFacultyStaffDirectoryAddable interface."""
return [type['name'] for type in listTypes() if IFacultyStaffDirectoryAddable.implementedBy(type['klass'])]
security.declarePrivate('getRoleSet')
def getRoleSet(self):
"""Get the roles vocabulary to use."""
portal_roles = getFilteredValidRolesForPortal(self)
allowed_roles = [r for r in portal_roles if r not in INVALID_ROLES]
return allowed_roles
#
# Validators
#
security.declarePrivate('validate_id')
def validate_id(self, value):
"""Ensure the id is unique, also among groups globally."""
if value != self.getId():
parent = aq_parent(aq_inner(self))
if value in parent.objectIds():
return _(u"An object with id '%s' already exists in this folder") % value
groups = getToolByName(self, 'portal_groups')
if groups.getGroupById(value) is not None:
return _(u"A group with id '%s' already exists in the portal") % value
registerType(FacultyStaffDirectory, PROJECTNAME)
Below is the portion of code from the class that I have implemented to change the description field's schemata:
from Products.Archetypes.public import ImageField, ImageWidget, StringField, StringWidget, SelectionWidget, TextField, RichWidget
from Products.FacultyStaffDirectory.interfaces.facultystaffdirectory import IFacultyStaffDirectory
from archetypes.schemaextender.field import ExtensionField
from archetypes.schemaextender.interfaces import ISchemaModifier, ISchemaExtender, IBrowserLayerAwareExtender
from apkn.templates.interfaces import ITemplatesLayer
from zope.component import adapts
from zope.interface import implements
class _ExtensionImageField(ExtensionField, ImageField): pass
class _ExtensionStringField(ExtensionField, StringField): pass
class _ExtensionTextField(ExtensionField, TextField): pass
class FacultyStaffDirectoryExtender(object):
"""
Adapter to add description field to a FacultyStaffDirectory.
"""
adapts(IFacultyStaffDirectory)
implements(ISchemaModifier, IBrowserLayerAwareExtender)
layer = ITemplatesLayer
fields = [
]
def __init__(self, context):
self.context = context
def getFields(self):
return self.fields
def fiddle(self, schema):
desc_field = schema['description'].copy()
desc_field.schemata = "default"
schema['description'] = desc_field
And here is the code from my configure.zcml:
<adapter
name="apkn-FacultyStaffDirectoryExtender"
factory="apkn.templates.extender.FacultyStaffDirectoryExtender"
provides="archetypes.schemaextender.interfaces.ISchemaModifier"
/>
Is there something that I'm missing in this?
def fiddle(self, schema):
schema['description'].schemata = 'default'
should be sufficient. The copy() operation does not make any sense here.
In order to check if the fiddle() method is actually used: use pdb or add debug print statements.
It turns out that there was nothing wrong with the code.
I got it to work by doing the following:
Restarted the plone website
Re-installed the product
Clear and rebuilt my catalog
This got it working somehow.

Categories