Get Django CMS plugin information from Haystack search results - django

My search results page needs to display information about the Plugins where the query was found, too. I found this question with a similar problem, but I don't only need the contents, I need to know stuff about the plugin - i.e. what's it called, where it is on the page and stuff. Basically I would like a reference to the plugin where the query was located, but I can only find information about the page and title. I haven't been able to find it anywhere on the SearchQuerySet object and in the vicinity - also coming up empty in the documentation for Haystack. Is it possible and how?
Stack I'm using: Elasticsearch 2.4, django-haystack 2.8, aldryn-search 1.0 (for CMS indexing).

I ended up writing a new index for CMSPlugins. Not sure how much use my code is, but maybe it'll help someone out.
from django.conf import settings
from aldryn_search.helpers import get_plugin_index_data
from aldryn_search.utils import clean_join, get_index_base
from cms.models import CMSPlugin
class CMSPluginIndex(get_index_base()):
haystack_use_for_indexing = True
index_title = True
object_actions = ('publish', 'unpublish')
def get_model(self):
return CMSPlugin
def get_index_queryset(self, language):
return CMSPlugin.objects.select_related(
'placeholder'
).prefetch_related(
'placeholder__page_set'
).filter(
placeholder__page__publisher_is_draft=False,
language=language
).exclude(
plugin_type__in=settings.HAYSTACK_EXCLUDED_PLUGINS
).distinct()
def get_search_data(self, obj, language, request):
current_page = obj.placeholder.page
text_bits = []
plugin_text_content = self.get_plugin_search_text(obj, request)
text_bits.append(plugin_text_content)
page_meta_description = current_page.get_meta_description(fallback=False, language=language)
if page_meta_description:
text_bits.append(page_meta_description)
page_meta_keywords = getattr(current_page, 'get_meta_keywords', None)
if callable(page_meta_keywords):
text_bits.append(page_meta_keywords())
return clean_join(' ', text_bits)
def get_plugin_search_text(self, base_plugin, request):
plugin_content_bits = get_plugin_index_data(base_plugin, request)
return clean_join(' ', plugin_content_bits)
def prepare_pub_date(self, obj):
return obj.placeholder.page.publication_date
def prepare_login_required(self, obj):
return obj.placeholder.page.login_required
def get_url(self, obj):
parent_obj = self.ancestors_queryset(obj).first()
if not parent_obj:
return obj.placeholder.page.get_absolute_url()
return # however you get the URL in your project
def get_page_title_obj(self, obj):
return obj.placeholder.page.title_set.get(
publisher_is_draft=False,
language=obj.language
)
def ancestors_queryset(self, obj):
return obj.get_ancestors().filter(
plugin_type=# Some plugins that I wanted to find
).order_by(
'-depth'
)
def get_title(self, obj):
parent_obj = self.ancestors_queryset(obj).first()
if not parent_obj:
return self.get_page_title_obj(obj).title
return # get title from parent obj if you want to
def prepare_site_id(self, obj):
return obj.placeholder.page.node.site_id
def get_description(self, obj):
return self.get_page_title_obj(obj).meta_description or None

If you are using aldryn-search, you only need to define in PLACEHOLDERS_SEARCH_LIST all the placeholders you want to check, therefore all plugins inside will be checked:
PLACEHOLDERS_SEARCH_LIST = {
'*': {
'include': ['content'],
'exclude': [''],
},
}

Related

query string, changes required in urls.py and view

A basic django question, but stumping me.
I have an existing view for an endpoint.
http://localhost:8001/v1/subject - which returns all subjects from the subject model.
I want to serve a new endpoint...
http://localhost:8001/v1/subject?owner_ids=60,61,62
What would be the changes required in the "urls.py" definition and
'''
def query_set
'''
method in views/subject.py,
I have added the method but it's not working as intended.
Here's the urls.py definition, any changes required to accommodate a query string?
router.register(r'subjects', views.SubjectsViewSet)
Should I do something like this?
url(r'^v1/subjects/',
views.SubjectViewSet.as_view({'get': 'list'}))
Also, here's my views/subjects.py file...the logic in def get_queryset may work, but how do I wire the urls.py entry so that the querystring localhost:8001/v1/subjects?owner_ids=60,61,62 is served together with the regular localhost:8001/v1/subjects ?
class SubjectViewSet(Subject.get_viewset()):
pagination_class = SubjectPagination
def get_queryset(self, *args, **kwargs):
owner_id_list =
self.request.GET['owner_ids'].split(',')
owner_id_list_integer = []
for i in owner_id_list:
owner_id_list_integer.append(int(i))
queryset = Subject.objects.all()
if owner_id_list is None:
return None
else:
return queryset.filter(organization__in=owner_id_list)
SubjectUserRoleViewSet = Subject.get_by_user_role_viewset(
SubjectViewSet, GroupRoleMap, Role)
Please, try this way.
def get_queryset(self, *args, **kwargs):
splited = self.request.GET['owner_ids'].split(',')
filtered_nums = list(filter(None, splited))
try:
get_nums = list(map(int, filtered_nums))
except ValueError as exc:
return Subject.objects.none()
else:
return Subject.objects.filter(organization__in=get_nums)
I hope, I do not have a mistake.

Disabling options in django-autocomplete-light

Just started using django-autocomplete-light (autocomplete.ModelSelect2) and while I have managed to get it working, I wondered if it is possible to pass disabled options?
I have a list of customers to choose from but some, for various reasons, shouldn't be selected they shouldn't be able to use them. I know I could filter these non-selectable customers out, but this wouldn't be very usable as the user might think that the customer isn't in the database. If so, could someone point me in the right direction as I'm not sure where to start.
It says in the Select2 documentation that disabling options should be possible. Presumably if I could also send a 'disabled':true within the returned json response that might do it.
OK, so here is what I came up with and it works.
view.py
The Select2ViewMixin is subclassed and then a 'disabled' attribute is added to the customer details. This value provided by the ParentAutocomplete view.
from dal import autocomplete
from dal_select2.views import Select2ViewMixin
from dal.views import BaseQuerySetView
class CustomSelect2ViewMixin(Select2ViewMixin):
def get_results(self, context):
return [
{
'id': self.get_result_value(result),
'text': self.get_result_label(result),
'selected_text': self.get_selected_result_label(result),
'disabled': self.is_disabled_choice(result), # <-- this gets added
} for result in context['object_list']
]
class CustomSelect2QuerySetView(CustomSelect2ViewMixin, BaseQuerySetView):
"""Adds ability to pass a disabled property to a choice."""
class ParentAutocomplete(CustomSelect2QuerySetView):
def get_queryset(self):
qs = Customer.objects.all()
if self.q:
qs = qs.filter(org_name__icontains=self.q)
return qs.order_by('org_name', 'org_city')
def get_result_label(self, item):
return item.selector_name
def get_selected_result_label(self, item):
return item.selector_name
def is_disabled_choice(self, item): # <-- this is where we determine if the record is selectable or not.
customer_id = self.forwarded.get('customer_id', None)
return not (item.can_have_children and not str(item.pk) == customer_id)
form.py
The form is then used as normal.
from dal import autocomplete
class CustomerBaseForm(forms.ModelForm):
customer_id= forms.IntegerField(required=False, widget=forms.HiddenInput)
class Meta:
model = Customer
widgets = {
'parent':autocomplete.ModelSelect2(
url='customer:parent-autocomplete',
forward=['customer_id'],
)
}
Hopefully this might be useful to someone.

Custom Django RSS not working

I'm a noob, so please excuse me if this is a silly request. I am trying to create custom RSS feeds for each category of a website, but somehow I can't manage to pass the parameter (a category slug) in order to properly build the requested feed. The RSS should be located at an address like this: http://www.website.com/category-name/feed
Here's what I have:
In urls.py:
from project.feeds import FeedForCategory
urlpatterns = patterns('category.views',
#...
url(r'^(?P<category_slug>[a-zA-Z0-9\-]+)/feed/?$', FeedForCategory),
)
In feeds.py:
from django.contrib.syndication.feeds import Feed
class FeedForCategory(Feed):
def get_object(self, request, category_slug):
return get_object_or_404(Category, slug_name=category_slug)
def title(self, obj):
return "website.com - latest stuff"
def link(self, obj):
return "/articles/"
def description(self, obj):
return "The latest stuff from Website.com"
def get_absolute_url(self, obj):
return settings.SITE_ADDRESS + "/articles/%s/" % obj.slug_name
def items(self, obj):
return Article.objects.filter(category=category_slug)[:10]
The error that I get is: "_ init _() got an unexpected keyword argument 'category_slug'", but the traceback isn't helpful, it only shows some base python stuff.
Thank you.
From the doc: https://docs.djangoproject.com/en/dev/ref/contrib/syndication/
You need to pass an instance of the feed object to your url patterns. So do this in urls.py:
from project.feeds import FeedForCategory
urlpatterns = patterns('category.views',
#...
url(r'^(?P<category_slug>[a-zA-Z0-9\-]+)/feed/?$', FeedForCategory()),
)

Django admin filter using F() expressions

does someone know how to filter in admin based on comparison on model fields - F() expressions?
Let's assume we have following model:
class Transport(models.Model):
start_area = models.ForeignKey(Area, related_name='starting_transports')
finish_area = models.ForeignKey(Area, related_name='finishing_transports')
Now, what I would like to do is to make admin filter which allows for filtering of in-area and trans-area objects, where in-area are those, whose start_area and finish_area are the same and trans-area are the others.
I have tried to accomplish this by creating custom FilterSpec but there are two problems:
FilterSpec is bound to only one field.
FilterSpec doesn't support F() expressions and exclude.
The second problem might be solved by defining custom ChangeList class, but I see no way to solve the first one.
I also tried to "emulate" the filter straight in the ModelAdmin instance by overloading queryset method and sending extra context to the changelist template where the filter itself would be hard-coded and printed by hand. Unfortunately, there seems to be problem, that Django takes out my GET parameters (used in filter link) as they are unknown to the ModelAdmin instance and instead, it puts only ?e=1 which is supposed to signal some error.
Thanks anyone in advance.
EDIT: It seems that functionality, which would allow for this is planned for next Django release, see http://code.djangoproject.com/ticket/5833. Still, does someone have a clue how to accomplish that in Django 1.2?
it's not the best way*, but it should work
class TransportForm(forms.ModelForm):
transports = Transport.objects.all()
list = []
for t in transports:
if t.start_area.pk == t.finish_area.pk:
list.append(t.pk)
select = forms.ModelChoiceField(queryset=Page.objects.filter(pk__in=list))
class Meta:
model = Transport
The solution involves adding your FilterSpec and as you said implementing your own ChangeList. As the filter name is validated, you must name your filter with a model field name. Below you will see a hack allowing to use the default filter for the same field.
You add your FilterSpec before the standard FilterSpecs.
Below is a working implementation running on Django 1.3
from django.contrib.admin.views.main import *
from django.contrib import admin
from django.db.models.fields import Field
from django.contrib.admin.filterspecs import FilterSpec
from django.db.models import F
from models import Transport, Area
from django.contrib.admin.util import get_fields_from_path
from django.utils.translation import ugettext as _
# Our filter spec
class InAreaFilterSpec(FilterSpec):
def __init__(self, f, request, params, model, model_admin, field_path=None):
super(InAreaFilterSpec, self).__init__(
f, request, params, model, model_admin, field_path=field_path)
self.lookup_val = request.GET.get('in_area', None)
def title(self):
return 'Area'
def choices(self, cl):
del self.field._in_area
yield {'selected': self.lookup_val is None,
'query_string': cl.get_query_string({}, ['in_area']),
'display': _('All')}
for pk_val, val in (('1', 'In Area'), ('0', 'Trans Area')):
yield {'selected': self.lookup_val == pk_val,
'query_string': cl.get_query_string({'in_area' : pk_val}),
'display': val}
def filter(self, params, qs):
if 'in_area' in params:
if params['in_area'] == '1':
qs = qs.filter(start_area=F('finish_area'))
else:
qs = qs.exclude(start_area=F('finish_area'))
del params['in_area']
return qs
def in_area_test(field):
# doing this so standard filters can be added with the same name
if field.name == 'start_area' and not hasattr(field, '_in_area'):
field._in_area = True
return True
return False
# we add our special filter before standard ones
FilterSpec.filter_specs.insert(0, (in_area_test, InAreaFilterSpec))
# Defining my own change list for transport
class TransportChangeList(ChangeList):
# Here we are doing our own initialization so the filters
# are initialized when we request the data
def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_editable, model_admin):
#super(TransportChangeList, self).__init__(request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_editable, model_admin)
self.model = model
self.opts = model._meta
self.lookup_opts = self.opts
self.root_query_set = model_admin.queryset(request)
self.list_display = list_display
self.list_display_links = list_display_links
self.list_filter = list_filter
self.date_hierarchy = date_hierarchy
self.search_fields = search_fields
self.list_select_related = list_select_related
self.list_per_page = list_per_page
self.model_admin = model_admin
# Get search parameters from the query string.
try:
self.page_num = int(request.GET.get(PAGE_VAR, 0))
except ValueError:
self.page_num = 0
self.show_all = ALL_VAR in request.GET
self.is_popup = IS_POPUP_VAR in request.GET
self.to_field = request.GET.get(TO_FIELD_VAR)
self.params = dict(request.GET.items())
if PAGE_VAR in self.params:
del self.params[PAGE_VAR]
if TO_FIELD_VAR in self.params:
del self.params[TO_FIELD_VAR]
if ERROR_FLAG in self.params:
del self.params[ERROR_FLAG]
if self.is_popup:
self.list_editable = ()
else:
self.list_editable = list_editable
self.order_field, self.order_type = self.get_ordering()
self.query = request.GET.get(SEARCH_VAR, '')
self.filter_specs, self.has_filters = self.get_filters(request)
self.query_set = self.get_query_set()
self.get_results(request)
self.title = (self.is_popup and ugettext('Select %s') % force_unicode(self.opts.verbose_name) or ugettext('Select %s to change') % force_unicode(self.opts.verbose_name))
self.pk_attname = self.lookup_opts.pk.attname
# To be able to do our own filter,
# we need to override this
def get_query_set(self):
qs = self.root_query_set
params = self.params.copy()
# now we pass the parameters and the query set
# to each filter spec that may change it
# The filter MUST delete a parameter that it uses
if self.has_filters:
for filter_spec in self.filter_specs:
if hasattr(filter_spec, 'filter'):
qs = filter_spec.filter(params, qs)
# Now we call the parent get_query_set()
# method to apply subsequent filters
sav_qs = self.root_query_set
sav_params = self.params
self.root_query_set = qs
self.params = params
qs = super(TransportChangeList, self).get_query_set()
self.root_query_set = sav_qs
self.params = sav_params
return qs
class TransportAdmin(admin.ModelAdmin):
list_filter = ('start_area','start_area')
def get_changelist(self, request, **kwargs):
"""
Overriden from ModelAdmin
"""
return TransportChangeList
admin.site.register(Transport, TransportAdmin)
admin.site.register(Area)
Unfortunately, FilterSpecs are very limited currently in Django. Simply, they weren't created with customization in mind.
Thankfully, though, many have been working on a patch to FilterSpec for a long time. It missed the 1.3 milestone, but it looks like it's now finally in trunk, and should hit with the next release.
#5833 (Custom FilterSpecs)
If you want to run your project on trunk, you can take advantage of it now, or you might be able to patch your current installation. Otherwise, you'll have to wait, but at least it's coming soon.

Django sitemaps with no associated object (just a view)

I'm setting up some Django sitemaps. It works really well for all the objects I have, but I'm curious about how I should do it if I'd like to put something in the sitemap that has no object associated with it.
For instance, I have a category listing, and I can just return a queryset of all categories. URLs would be example.com/cats/12 or what have you. I also have a sort of pseudo root category that isn't associated with a category object. That page (example.com/cats/) is just a view that includes all sub categories with no parent, and a list of products. Point is, I can't use get_absolute_url because there is no "root" object. My solution was to get the queryset as a list, add a "None" object, then get the appropriate URL:
class CatsSitemap(Sitemap):
changefreq = "weekly"
priority = 0.4
def items(self):
cats = list(Category.objects.all())
cats.append(None)
return cats
def location(self, obj):
if(obj != None):
return reverse('cats_sub_category', args=[obj.pk])
else:
return reverse('cats_root')
Does anyone see a problem with this? Will returning them as a list kill performance? Realistically we'll have perhaps hundreds of categories, but probably not many more than that. Too much?
Maybe I found a better way to do it, which will certainly be useful in the future with other named views that don't have associated objects. In a sitemaps.py, put:
class NamedURLSitemap(Sitemap):
priority = 1.0
changefreq = "daily"
def __init__(self, names):
self.names = names
def items(self):
return self.names
def lastmod(self, obj):
return datetime.datetime.now()
def location(self, obj):
return reverse(obj)
And in the urls.py, put:
'cat-roots': NamedURLSitemap(['cats_root']),
This simplifies the Sitemap code for categories:
class CatsSitemap(Sitemap):
changefreq = "weekly"
priority = 0.4
def items(self):
return Category.objects.all()
def location(self, obj):
return reverse('cats_sub_category', args=[obj.pk])
Any opinion on this or the former solution?
Slight improvement to your answer which includes the priority and changefreq for each page as part of the list passed to init:
class NamedURLSitemap(Sitemap):
def __init__(self, pages):
"""
Parameters:
``pages``
A list of three-tuples containing name, priority, and changefreq:
e.g. [('home', 0.5, 'daily'), ('search', 0.5, 'never')]
"""
self.pages = pages
def items(self):
return self.pages
def lastmod(self, obj):
return datetime.datetime.now()
def location(self, obj):
return reverse(obj[0])
def priority(self, obj):
return obj[1]
def changefreq(self, obj):
return obj[2]