Wagtail Page model transitive search on custom fields [duplicate] - python

I'm using Wagtail, and I want to filter a selection of child pages by a Foreign Key. I've tried the following and I get the error django.core.exceptions.FieldError: Cannot resolve keyword 'use_case' into field when I try children = self.get_children().specific().filter(use_case__slug=slug):
class AiLabResourceMixin(models.Model):
parent_page_types = ['AiLabResourceIndexPage']
use_case = models.ForeignKey(AiLabUseCase, on_delete=models.PROTECT)
content_panels = ArticlePage.content_panels + [
FieldPanel('use_case', widget=forms.Select())
]
class Meta:
abstract = True
class AiLabCaseStudy(AiLabResourceMixin, ArticlePage):
pass
class AiLabBlogPost(AiLabResourceMixin, ArticlePage):
pass
class AiLabExternalLink(AiLabResourceMixin, ArticlePage):
pass
class AiLabResourceIndexPage(RoutablePageMixin, BasePage):
parent_page_types = ['AiLabHomePage']
subpage_types = ['AiLabCaseStudy', 'AiLabBlogPost', 'AiLabExternalLink']
max_count = 1
#route(r'^$')
def all_resources(self, request):
children = self.get_children().specific()
return render(request, 'ai_lab/ai_lab_resource_index_page.html', {
'page': self,
'children': children,
})
#route(r'^([a-z0-9]+(?:-[a-z0-9]+)*)/$')
def filter_by_use_case(self, request, slug):
children = self.get_children().specific().filter(use_case__slug=slug)
return render(request, 'ai_lab/ai_lab_resource_index_page.html', {
'page': self,
'children': children,
})
I've seen this answer, but this assumes I only have one type of page I want to filter. Using something like AiLabCaseStudy.objects.filter(use_case__slug=slug) works, but this only returns AiLabCaseStudys, not AiLabBlogPosts or AiLabExternalLinks.
Any ideas?

At the database level, there is no efficient way to run the filter against all page types at once. Since AiLabResourceMixin is defined as abstract = True, this class has no representation of its own within the database - instead, the use_case field is defined separately for each of AiLabCaseStudy, AiLabBlogPost and AiLabExternalLink. As a result, there's no way for Django or Wagtail to turn .filter(use_case__slug=slug) into a SQL query, since use_case refers to three different places in the database.
A couple of possible ways around this:
If your data model allows, restructure it to use multi-table inheritance - this looks fairly similar to your current definition, except without the abstract = True:
class AiLabResourcePage(ArticlePage):
use_case = models.ForeignKey(AiLabUseCase, on_delete=models.PROTECT)
class AiLabCaseStudy(AiLabResourcePage):
pass
class AiLabBlogPost(AiLabResourcePage):
pass
class AiLabExternalLink(AiLabResourcePage):
pass
AiLabResourcePage will then exist in its own right in the database, and you can query its use_case field with an expression like: AiLabResourcePage.objects.child_of(self).filter(use_case__slug=slug).specific(). There'll be a small performance impact here, since Django has to pull data from one additional table to construct these page objects.
Run a preliminary query on each specific page type to retrieve the matching page IDs, before running the final query with specific():
case_study_ids = list(AiLabCaseStudy.objects.child_of(self).filter(use_case__slug=slug).values_list('id', flat=True))
blog_post_ids = list(AiLabBlogPost.objects.child_of(self).filter(use_case__slug=slug).values_list('id', flat=True))
external_link_ids = list(AiLabExternalLink.objects.child_of(self).filter(use_case__slug=slug).values_list('id', flat=True))
children = Page.objects.filter(id__in=(case_study_ids + blog_post_ids + external_link_ids)).specific()

Try:
children = self.get_children().filter(use_case__slug=slug).specific()

Related

Django: Search results with django-tables2 and django-filter

I'd like to retrieve a model's objects via a search form but add another column for search score. I'm unsure how to achieve this using django-tables2 and django-filter.
In the future, I'd like the user to be able to use django-filter to help filter the search result. I can access the form variables from PeopleSearchListView but perhaps it's a better approach to integrate a django form for form handling?
My thought so far is to handle to the get request in get_queryset() and then modify the queryset before it's sent to PeopleTable, but adding another column to the queryset does not seem like a standard approach.
tables.py
class PeopleTable(tables.Table):
score = tables.Column()
class Meta:
model = People
template_name = 'app/bootstrap4.html'
exclude = ('id',)
sequence = ('score', '...')
views.py
class PeopleFilter(django_filters.FilterSet):
class Meta:
model = People
exclude = ('id',)
class PeopleSearchListView(SingleTableMixin, FilterView):
table_class = PeopleTable
model = People
template_name = 'app/people.html'
filterset_class = PeopleFilter
def get_queryset(self):
p = self.request.GET.get('check_this')
qs = People.objects.all()
####
# Run code to score users against "check_this".
# The scoring code I'm using is complex, so below is a simpler
# example.
# Modify queryset using output of scoring code?
####
for person in qs:
if person.first_name == 'Phil' and q == 'Hey!':
score = 1
else:
score = 0
return qs
urls.py
urlpatterns = [
...
path('search/', PeopleSearchListView.as_view(), name='search_test'),
... ]
models.py
class People(models.model):
first_name = models.CharField(max_length=200)
last_name = models.CharField(max_length=200)
Edit:
The scoring algorithm is a bit more complex than the above example. It requires a full pass over all of the rows in the People table to generate a score matrix, before finally comparing each scored row with the search query. It's not a one-off score. For example:
def get_queryset(self):
all = []
for person in qs:
all.append(person.name)
# Do something complex with all,
# e.g., measure cosine distance between every person,
# and finally compare to the get request
scores = measure_cosine(all, self.request.GET.get('check_this'))
# We now have the scores for each person.
So you can add extra columns when you initialise the table.
I've got a couple of tables which do this based on events in the system;
def __init__(self, *args, **kwargs):
"""
Override the init method in order to add dynamic columns as
we need to declare one column per existent event on the system.
"""
extra_columns = []
events = Event.objects.filter(
enabled=True,
).values(
'pk', 'title', 'city'
)
for event in events:
extra_columns.append((
event['city'],
MyColumn(event_pk=event['pk'])
))
if extra_columns:
kwargs.update({
'extra_columns': extra_columns
})
super().__init__(*args, **kwargs)
So you could add your score column similar to this when a score has been provided. Perhaps passing your scores into the table from the view so you can identify they're present and add the column, then use the data when rendering the column.
extra_columns doesn't appear to be in the tables2 docs, but you can find the code here; https://github.com/jieter/django-tables2/blob/master/django_tables2/tables.py#L251
When you define a new column for django-tables2 which is not included in table data or queryset, you should provide a render method to calculate it's value.
You don't have to override get_queryset if a complex filtering, preprocess or join required.
In your table class:
class PeopleTable(tables.Table):
score = tables.Column(accessor="first_name")
class Meta:
model = People
def render_score(self, record):
return 1 if record["first_name"] == "Phil" and q == "Hey!" else 0
In your view you can override and provide complex data as well as special filtering or aggregates with get_context_data:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["filter"] = self.filter
aggs = {
"score": Function("..."),
"other": Sum("..."),
}
_data = (
People.objects.filter(**params)
.values(*values)
.annotate(**aggs)
.order_by(*values)
.distinct()
)
df = pandas.DataFrame(_data)
df = df....
chart_data = df.to_json()
data = df.to_dict()...
self.table = PeopleTable(data)
context["table"] = self.table
context['chart_data']=chart_data
return context

Django-filter 2 use #property to filter?

I've got this filter:
class SchoolFilter(django_filters.FilterSet):
class Meta:
model = School
fields = {
'name': ['icontains'],
'special_id': ['icontains'],
}
Where special_id is a #property of the School Model:
#property
def special_id(self):
type = self.type
unique_id = self.unique_id
code = self.code
if unique_id < 10:
unique_id = f'0{unique_id}'
if int(self.code) < 10:
code = f'0{self.code}'
special_id = f'{code}{type}{id}'
return special_id
I've tried to google some answers, but couldn't find anything. Right now If I use my filter like I do I only receive this error:
'Meta.fields' contains fields that are not defined on this FilterSet: special_id
How could I define the property as a field for this FilterSet? Is it even possible for me to use django-filter with a #property?
Thanks for any answer!
Update:
Figured it out. Not the prettiest solution, but ayyy
class SchoolFilter(django_filters.FilterSet):
special_id = django_filters.CharFilter(field_name="special_id", method="special_id_filter", label="Special School ID")
def special_id_filter(self, queryset, name, value):
schools_pk = []
for obj in queryset:
if obj.special_id == value:
schools_pk.append(obj.pk)
queryset = queryset.filter(pk__in=schools_pk)
return queryset
class Meta:
model = School
fields = {
'name': ['icontains'],
'special_id': ['icontains'],
}
You can't. FilterSet will only filter on actual fields, since FilterSet alters a QuerySet.
QuerySets do a database call based on the filters applied, which means you can only filter on fields actually stored in the database.
You could annotate your QuerySet to add the special_id, but an annotation like this is pretty complex to chain together.
A better way to do this would be to create a custom filter on your FilterSet, but I'm not exactly sure how to do this. If you can explain what special_id is, and exactly why you want to search it through icontains, then I could maybe point you in the right direction.
This is an implementation of a MethodFilter, which I think is similar what you want.
FilterSet operates by filtering queryset (adding where conditions to the underlying sql). Which means, FilterSet can operate only on Columns that are present in the database. Here the special_id is a computed property (It is not a column, it is calculated on the fly using other fields/columns), So it wont work.
The work around is to make special_id a normal field/column, compute the value at runtime and write to database at the time of save.

Django-CMS Child plugin to show filtered data from DB table

I have two plugins ProductSelector(parent) and SpecificationSelector(child). I want to set the child up so that when you add it to the parent the Specifications that are shown are the only ones for the product (parent). Right now it pulls in all the specifications from the table. These lines let me filter the data to get what I want.
edit: I found an error that i fixed in the code. I had the PluginBase names the same as the model. This allowed me to use ProductSelector.objects.get(cmsplugin_ptr=instance.parent) in the child to get the parent instance. I still need to figure out how to pass the filtered specification list to the "PluginAdmin Interface"
product = ProductSelector.objects.get(cmsplugin_ptr=instance.parent)
specification = Specifications.objects.filter(product_name__product_name__iexact = product.product_name)
However, I haven't figured out how to send that filtered list to the plugin admin interface.
class ProductSelectorPlugin(CMSPluginBase):
model = ProductSelector
name = "Product Selector"
render_template = "product_selector.html"
allow_children = True
child_classes = ['SpecificationSelectorPlugin']
def render(self, context, instance, placeholder):
context['instance'] = instance
return context
plugin_pool.register_plugin(ProductSelectorPlugin)
class SpecificationSelectorPlugin(CMSPluginBase):
model = SpecificationSelector
render_template = "specification_selector.html"
formfield_overrides = {models.ManyToManyField: {'widget': CheckboxSelectMultiple},}
def render(self, context, instance, placeholder):
product = ProductSelector.objects.get(cmsplugin_ptr=instance.parent)
specification = Specifications.objects.filter(product_name__product_name__iexact = product.product_name)
context['instance'] = instance
return context
plugin_pool.register_plugin(SpecificationSelectorPlugin)
models.py
class ProductSelector(CMSPlugin):
product_name = models.ForeignKey(Product, help_text = "Select the product you want to place")
new_product = models.BooleanField(blank=True)
class SpecificationSelector(CMSPlugin):
specification = models.ManyToManyField(Specifications, blank=True)
def __unicode__(self):
return unicode(self.specification)
Here is an screenshot the Django-cms plugins in the placeholder. Currently it is showing all specs in the table, but I just want it to be the specs for that particular product.
http://imgur.com/3R1LobC
Thank you in advance for the help.
CMSPluginBase inhertis from ModelAdmin which means that you can override the form rendered when adding and editing your plugin.
So you can create a ModelForm subclass like so:
class SpecificationSelectorPluginForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(SpecificationSelectorPluginForm, self).__init__(*args, **kwargs)
if self.instance.parent_id:
# Assume that the parent is a product instance
parent_plugin = self.instance.parent
product = parent_plugin.get_plugin_instance()[0]
if product:
# It's possible that product is an orphan plugin.
specifications = Specifications.objects.filter(
product_name__product_name__iexact=product.product_name)
self.fields['specification'].queryset = specifications
then change your SpecificationSelectorPlugin to use this form like so:
class SpecificationSelectorPlugin(CMSPluginBase):
form = SpecificationSelectorPluginForm
The above will only work if the specification plugin is a direct child of the product plugin.

Foreign Key Resource from dynamic field

I've got an API endpoint called TrackMinResource, which returns the minimal data for a music track, including the track's main artist returned as an ArtistMinResource. Here are the definitions for both:
class TrackMinResource(ModelResource):
artist = fields.ForeignKey(ArtistMinResource, 'artist', full=True)
class Meta:
queryset = Track.objects.all()
resource_name = 'track-min'
fields = ['id', 'artist', 'track_name', 'label', 'release_year', 'release_name']
include_resource_uri = False
cache = SimpleCache(public=True)
def dehydrate(self, bundle):
bundle.data['full_artist_name'] = bundle.obj.full_artist_name()
if bundle.obj.image_url != settings.NO_TRACK_IMAGE:
bundle.data['image_url'] = bundle.obj.image_url
class ArtistMinResource(ModelResource):
class Meta:
queryset = Artist.objects.all()
resource_name = 'artist-min'
fields = ['id', 'artist_name']
cache = SimpleCache(public=True)
def get_resource_uri(self, bundle_or_obj):
return '/api/v1/artist/' + str(bundle_or_obj.obj.id) + '/'
The problem is, the artist field on Track (previously a ForeignKey) is now a model method called main_artist (I've changed the structure of the database somewhat, but I'd like the API to return the same data as it did before). Because of this, I get this error:
{"error": "The model '<Track: TrackName>' has an empty attribute 'artist' and doesn't allow a null value."}
If I take out full=True from the 'artist' field of TrackMinResource and add null=True instead, I get null values for the artist field in the returned data. If I then assign the artist in dehydrate like this:
bundle.data['artist'] = bundle.obj.main_artist()
...I just get the artist name in the returned JSON, rather than a dict representing an ArtistMinResource (along with the associated resource_uris, which I need).
Any idea how to get these ArtistMinResources into my TrackMinResource? I can access an ArtistMinResource that comes out fine using the URL endpoint and asking for it by ID. Is there a function for getting that result from within the dehydrate function for TrackMinResource?
You can use your ArtistMinResource in TrackMinResource's dehydrate like this (assuming that main_artist() returns the object that your ArtistMinResource represents):
artist_resource = ArtistMinResource()
artist_bundle = artist_resource.build_bundle(obj=bundle.obj.main_artist(), request=request)
artist_bundle = artist_resource.full_dehydrate(artist_bundle)
artist_json = artist_resource.serialize(request=request, data=artist_bundle, format='application/json')
artist_json should now contain your full artist representation. Also, I'm pretty sure you don't have to pass the format if you pass the request and it has a content-type header populated.

Google app engine python problem

I'm having a problem with the datastore trying to replicate a left join to find items from model a that don't have a matching relation in model b:
class Page(db.Model):
url = db.StringProperty(required=True)
class Item(db.Model):
page = db.ReferenceProperty(Page, required=True)
name = db.StringProperty(required=True)
I want to find any pages that don't have any associated items.
You cannot query for items using a "property is null" filter. However, you can add a boolean property to Page that signals if it has items or not:
class Page(db.Model):
url = db.StringProperty(required=True)
has_items = db.BooleanProperty(default=False)
Then override the "put" method of Item to flip the flag. But you might want to encapsulate this logic in the Page model (maybe Page.add_item(self, *args, **kwargs)):
class Item(db.Model):
page = db.ReferenceProperty(Page, required=True)
name = db.StringProperty(required=True)
def put(self):
if not self.page.has_items:
self.page.has_items = True
self.page.put()
return db.put(self)
Hence, the query for pages with no items would be:
pages_with_no_items = Page.all().filter("has_items =", False)
The datastore doesn't support joins, so you can't do this with a single query. You need to do a query for items in A, then for each, do another query to determine if it has any matching items in B.
Did you try it like :
Page.all().filter("item_set = ", None)
Should work.

Categories

Resources