Django Haystack autocompletion on two (multiple) fields - python

I use haystack 1.2.6 with Whoosh 2.4 and Django 1.3.
Let's say that we have the below model describing an hypothetical post.
Post(models.Model):
title = models.CharField()
body = models.TextField()
We built our post index like this for autocompletion on body field:
PostIndex(SearchIndex):
text = CharField(document=True, use_template=True)
content_auto = indexes.EdgeNgramField(model_attr='body')
Having read the haystack documentation thoroughly i cannot find if is possible to have autocompletion on both title and body fields.
So ... is it possible or ... ?

I've managed to do it based on this. You just make an EdgeNgramField for each field you want to autocomplete on in your index, then apply the autocompletion to two different search querysets and concatenate them:
sqs = SearchQuerySet().models(Post)
sqs1 = sqs.filter(title_auto=q)
sqs2 = sqs.filter(body_auto=q)
sqs = sqs1 | sqs2
If you have to do extra filtering, you need to do it after the autocompletion one (at least this was the only way it worked for me).

Here's a simpler way, do the concatenation in the template & use EdgeNgramField (although doing that on the body of a post is expensive :
#In search_indexes.py
PostIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.EdgeNgramField(document=True, use_template=True)
#In <app>\template\search\index\<app>\post_text.txt
{{ object.title }} {{object.body}}
Then all such queries will autocomplete on both title & body
sqs = SearchQuerySet().models(Post).autocomplete(text='hello')
P.S. Using Haystack 2.x

Related

Wagtail: Multiple reusable elements on one Page

I need to create a reusable element (cta button) that I can include in many places throughout the page.
These cta buttons are used ~8 times throughout the design. How can I do this without copy-pasting?
Similar to this: Ways to create reusable sets of fields in Wagtail? Except I must be able to use the set several times on a single page.
This is what I am trying to do:
class HomePage(Page):
template = "home/home_page.html"
hero_heading = models.CharField(max_length=50)
hero_subheading = models.CharField(max_length=255)
hero_cta1 = HeroCTA1() # abstract reusable model
hero_cta2 = HeroCTA2()
content_panels = Page.content_panels + [
FieldPanel("hero_heading"),
FieldPanel("hero_subheading"),
hero_cta1.panels,
hero_cta2.panels,
]
My attempt at a reusable CTAButton class:
class CTAButton(models.Model):
text = RichTextField(max_length=25, features=["bold"])
url = models.URLField(null=True, blank=True)
page = models.ForeignKey(
'wagtailcore.Page',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="%(app_label)s_%(class)s_page",
)
panels = MultiFieldPanel(
[
FieldPanel("text"),
FieldPanel("url"),
PageChooserPanel("page"),
],
heading="CTA Button Fields",
classname="collapsible",
)
class Meta:
abstract = True
class HeroCTA1(CTAButton):
pass
class HeroCTA2(CTAButton):
pass
Except this doesn't work :/
I am encountering "HomePage has no field named 'page'"
Shouldn't this break on 'text' since it's before 'page'?
Any advice on how to proceed?
checkout django documentation for model inheritance there are three method you can achieve model inheritance in django wagtail is made from djanog so you can also use abstract model inheritance in that for more information checkout this documentation
https://docs.djangoproject.com/en/3.0/topics/db/models/#model-inheritance
If your page design has 8 places where a CTA button can go, then perhaps it's more useful to treat it as a flexible sequence of elements where CTA buttons can be freely mixed in with other types of content chosen by the page author, rather than a fixed layout with specific elements at specific points. Wagtail's StreamField provides that kind of flexible layout: https://docs.wagtail.io/en/stable/topics/streamfield.html
I followed the suggestion of #gasman and broke the template out into blocks and used a mixin to keep it DRY.
This solution extracts the href function and allows it to be used in multiple CTA blocks, but it hardcodes the naming convention. Future readers can probably find a smarter way to do this. To use multiple elements, such as TwoCTAMixin, I just extend the base mixin and add cta1_text, etc.
CTAStructValue is required to access the value as you would expect with an #property variable. More info here.
def get_cta_href_value(cta, varname="cta"):
"""Function that returns the href link with a value provided.
The value is returned in this order: (url > page > #)."""
url_name = f"{varname}_url"
page_name = f"{varname}_page"
if cta[url_name] and cta[url_name] != "":
return cta[url_name]
elif cta[page_name] and cta[url_name] != "":
return cta[page_name]
return "#" # broken link, return something non-volatile
class CTAStructValue(blocks.StructValue):
"""Calculated properties for CTAMixin."""
#property
def cta_href(self):
return get_cta_href_value(self)
class CTAMixin(blocks.StructBlock):
"""Mixin that includes a single CTA element."""
cta_text = blocks.CharBlock(required=True, help_text="Text to display on the button")
cta_url = blocks.URLBlock(required=False, help_text="URL the button directs to")
cta_page = blocks.PageChooserBlock(required=False, help_text="Page the button directs to")
class Meta:
abstract = True
class SomeSectionBlock(CTAMixin):
section_heading = blocks.CharBlock()
class Meta:
template = "blocks/some_section_block.html"
some_section_block.html
<section>
<div class="wrapper some-section">
<h2>{{ self.section_heading }}</h2>
<a href="{{ self.cta_href }}">
<button>{{ self.cta_text }}</button>
</a>
</div>
</section>

Django - get data from foreign key

I'm working on a Django project and attempting to create some linked models for my data which In think is working, but I cannot seem to work out how to access the linked data.
class One(models.Model)
name = models.CharField(max_length=50)
list = models.ArrayField(models.CharField(max_length=50), blank=True)
def __str__(self):
return self.name
class Many(models.Model)
name = models.CharField(max_length=50)
related = models.ForeignKey(One, null=True, blank=True)
def __str__(self):
return self.name
This is the general relationship I have set up.
What I am trying to do is, in a template have access to a list of all 'Ones', and via each of those, can access each Many and it's related attributes. I can see how to access the attributes for a single 'One', but not how to pass all of them and their related 'Many' models and the related attributes for each. Essentially the output I'd like would have a drop down list with the One's, and when this is submitted some Javascript will use the list in the 'Many' model to do some stuff.
Any advice would be much appreciated.
If you already have the objects of One model class, you can access the many objects using many_set (refer: backward relations):
{% for one_obj in one_objs %}
{% for m_obj in one_obj.many_set.all %}
# do stuff with m_obj here
{% endfor %}
{% endfor %}
One important thing to note here is that this will execute a db query for each m_obj. To make this efficient, you could prefetch the many_set with one_objs in your view.
In your view, use prefetch_related:
one_objs = One.objects.all().prefetch_related('many_set')
You can use Django's "prefetch_related" and Django's "related_name".
Also, this question has been answered here.
Though, here is what you might want, first, change your foreign key definition to this :
related = models.ForeignKey(One, null=True, blank=True, related_name='relateds')
Then you can reverse-fetch the foreign keys:
one = One.objects.get(name="TheOneYouWant").prefetch_related('relateds')
manys = one.relateds
Reverse lookups are accessible through an object's ___set attribte. So to get all the "Many" objects for a given "One" you could do one.many_set
Django reverse lookup of foreign keys
Regarding usage in a template, sets are accessible by adding "all" (since the set returns a queryset, you can run queries against it, including in the template)
Access ForeignKey set directly in template in Django
See the relevant section of the Django Documentation: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward

Django: How to return all models associated with a ForeignKey including all attributes of those models?

I have an app that I want to simply display all the URL links a page has associated with it when that page is visited.
It's similar to reddit in that there are many userpages (aka subreddits) and each page has an infinite possible amount of submitted links associated with it. The newlinkposts records are associated with a certain page via a ForeignKey.
Given a page, wow can I get all the related newlinkpost objects (including their corresponding likes, link comment, and post date) returned, in order to display them in a template?
My newlinkpost object is defined as follows:
class newlinkpost(models.Model):
newlink_tag = models.ForeignKey('userpagename') #tags link to which userpage it belongs to
link_comment = models.CharField(max_length=128) #comment to go along with post
post_date = models.DateField(auto_now=True, auto_now_add=False, null=False) #submission datestamp. later make it track editable posts if edit function is implemented
url = models.URLField(max_length = 1024, null=False) #actual submitted link
link_likes = models.IntegerField(null=False, default=0) #need to later create like button which will +1 the value
def __unicode__(self):
return self.url
When you add a ForeignKey within a model, as well as creating an attribute in the source model (in your case, newlinkpost) allowing you to find the one associated object, Django also creates a corresponding attribute inside the target model (in your case apparently userpagename).
By default this attribute is named after the source table, so in your case it will be newlinkpost_set.
That allows you to ask the question you're looking to answer: which newlinkpost objects have this userpagename?:
all_links = userpagename_instance.newlinkpost_set.all()
If you wish to apply additional filters, you can use the filter method instead:
some_links = userpagename_instance.newlinkpost_set.filter(...)
The newlinkpost_set attribute contains a RelatedManager object, which is a subtype of Manager, allowing you to use the same set of methods you could use on newlinkpost.objects, along with some additional methods allowing you to create new related objects.
Here's an example view using this technique: (this assumes you've got the model classes imported into the views module):
from django.shortcuts import render
def user_page(request, user_id):
page = userpagename.get(pk=user_id)
links = page.newlinkpost_set.all()
return render(
request,
"myapp/user_page.html",
{
page: page,
links: links,
}
)
...and here's an example of using that "links" variable in the template:
<ul>
{% for link in links %}
<li><a href="{{ link.url }}">{{ link.link_comment }} - {{ link.link_likes }} likes</li>
{% endfor %}
</ul>
You just use the reverse relationship.
my_userpagename.newlinkpost_set.all()

Sort Django query result by number of matches

In my Django app, I have Blogs and BlogPosts.
Their models are basically this:
class Blog(models.Model):
name = models.CharField(Entry)
details = models.TextField()
...
class BlogPost(models.Model):
blog = models.ForeignKey(Blog)
title = models.CharField()
...
Given a list of blog names, I would like to return all of the BlogPosts that appear in at least one of the given blogs.
I've figured out how to do this using Q objects. I've created a query like this:
# Return blogs that appear in either nameOfBlog1 or nameOfBlog2
q1 = Q(blog__name = nameOfBlog1)
q2 = Q(blog__name = nameOfBlog2)
Blog.objects.filter(q1 | q2)
This works.
However, I would like to receive the results in the order of the BlogPosts that match the highest number of Blogs. So for example, the BlogPosts that appear in all of the Blogs I'm searching against, should appear first in the list, while BlogPosts that only appear in one of the Blogs should appear at the end.
Is there any way to do this in Django?
try this:
BlogPost.objects.filter(q1 | q2).annotate(blog_times=Count('id')).order_by('blog_times')

Django Haystack - Indexing single field

I am using Django Haystack for search.
I only want to target the title field of my model when searching for results.
At present however, it returns results if the search term is in any of the fields in my model.
For example: searching xyz gives results where xyz is in the bio field.
This should not happen, I only want to return results where xyz is in the title field. Totally ignoring all other fields other than Artist.title for searching on.
artists/models.py :
class Artist(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=100)
strapline = models.CharField(max_length=255)
image = models.ImageField(upload_to=get_file_path, storage=s3, max_length=500)
bio = models.TextField()
artists/search_indexes.py
from haystack import indexes
from app.artists.models import Artist
class ArtistIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True, model_attr='title')
def get_model(self):
return Artist
I guess thinking of it like a SQL query:
SELECT * FROM artists WHERE title LIKE '%{search_term}%'
UPDATE
Following suggestion to remove use_template=True, my search_indexes.py now looks like:
from haystack import indexes
from app.artists.models import Artist
class ArtistIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, model_attr='title')
title = indexes.CharField(model_attr='title')
def get_model(self):
return Artist
But I am having the same problem. (Have tried python manage.py rebuild_index)
This is my Haystack settings if that makes any difference:
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.simple_backend.SimpleEngine',
},
}
model_attr and use_template don't work together. In this case, as you're querying for a single model attribute there's no need to use a template. Templates in search indexes are purely meant to group data.
Thus, you end up with:
class ArtistIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, model_attr='title')
def get_model(self):
return Artist
If you don't have any other use case for your index (ie searches that should match terms elsewhere) you just have to not use_template at all (set the use_template param to False and just ditch your search template) and you'll be done. FWIW note that when passing True for use_template the model_attr param is ignored. Also, you may not have a use for a full text search engine then, you could possibly just use Django's standard QuerySet lookup API, ie Artist.objects.filter(title__icontains=searchterm).
Else - if you still need a 'full' document index for other searches and only want to restrict this one search to the title you can as well add another index.CharField (with document=False, model_attr='title') for the title and only search on this field. How to do so is fully documented in Haystack's SearchQuerySet API doc.
From the Docs
Additionally, we’re providing use_template=True on the text field. This allows us to use a data template (rather than error prone concatenation) to build the document the search engine will use in searching. You’ll need to create a new template inside your template directory called search/indexes/myapp/note_text.txt and place the following inside:
{{ object.title }}
{{ object.user.get_full_name }}
{{ object.body }}
So I guess in this template you can declare which fields should be indexed/ searched upon
Other way is to override the def prepare(self, object) of Index class and explicitly define fields that need to be indexed/ searched upon.
OR just use model_attr
Basically your search_indexes.py file is written wrong. It should be like:-
from haystack import indexes
from app.artists.models import Artist
class ArtistIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
title= indexes.CharField(model_attr='title',null=True)
def get_model(self):
return Artist
def index_queryset(self, using=None):
return self.get_model().objects.all()
Then you have to create a template in your app. The directory structure would be like:
templates/search/indexes/artists/artist_text.txt
and add the following code to the artist_text.txt file:
{{ object.title }}
Now do python manage.py rebuild_index.
Now It will return result only for title.

Categories

Resources