Haystack - Why does RealtimeSearchIndex sometimes not update my saved object - django-haystack

I'm using Haystack and Whoosh with Django
Within search_index.py I have this
class PageIndex(RealTimeSearchIndex):
text = CharField(document=True, use_template=True)
creator = CharField(model_attr='creator')
created = DateTimeField(model_attr='created')
org = CharField(model_attr='organisation')
site.register(Page, PageIndex)
My template looks like this
{{ object.name }}
{{ object.description }}
{{ object.template|striptags }}
{% for k,v in object.get_variables.items %}
{{ v }}
{% endfor %}
If I save the Page with an updated name or description then it updates straight away and includes the variables from get_variables.items in the template. However if I update just the variable then it doesn't update.
Is it because variable is another object that's related to it and even though I am saving on the same page it does not pick up a change to the Page? If so how do I force to update the Page item when I'm updating related objects?

I concur with Daniel Hepper, but I think the easiest solution here is to attach a listener to your related model's post_save signal (see https://docs.djangoproject.com/en/dev/topics/signals/) and in that, reindex the model.
E.g, in myapp/models.py, given model MyRelatedModel which has a foreignkey to MyModel
from myapp.search_indexes import MyModelIndex
def reindex_mymodel(sender, **kwargs):
MyModelIndex().update_object(kwargs['instance'].mymodel)
models.signals.post_save.connect(reindex_mymodel, sender=MyRelatedModel)

A RealTimeSearchIndex only updates the search index when a model it is registered on is saved or deleted, or to be more precise, when the post_save/post_delete signal of the model is emitted. These signals are not emitted if a related model is deleted/saved or when a bulk update/delete operation is executed.
To solve your problem, you could create a subclass of RealTimeSearchIndex that also updates the index on post_save/post_delete signals of the related model.

Just a note for more recent viewers of this post ---- RealTimeSearchIndex has been deprecated.
See here for the Haystack post about it.

For recent viewers, here's a solution based on the new RealtimeSignalProcessor:
In myapp/signals.py:
class RelatedRealtimeSignalProcessor(RealtimeSignalProcessor):
def handle_save(self, sender, instance, **kwargs):
if hasattr(instance, 'reindex_related'):
for related in instance.reindex_related:
related_obj = getattr(instance, related)
self.handle_save(related_obj.__class__, related_obj)
return super(RelatedRealtimeSignalProcessor, self).handle_save(sender, instance, **kwargs)
def handle_delete(self, sender, instance, **kwargs):
if hasattr(instance, 'reindex_related'):
for related in instance.reindex_related:
related_obj = getattr(instance, related)
self.handle_delete(related_obj.__class__, related_obj)
return super(RelatedRealtimeSignalProcessor, self).handle_delete(sender, instance, **kwargs)
In settings.py:
HAYSTACK_SIGNAL_PROCESSOR = 'myapp.signals.RelatedRealtimeSignalProcessor'
In models.py:
class Variable(models.Model):
reindex_related = ('page',)
page = models.ForeignKey(Page)
Now when a Variable is saved, the index for the related Page will also be updated.
(TODO: This doesn't work for extended relationships like foo__bar, or for many-to-many fields. But it should be straightforward to extend it to handle those if you need to.)

Related

Correct way to define the initial value of a form field in Django from a database, when database values may change

I am trying to create an interview feedback capture app using Django. The interview feedbacks follow a template. The template would evolve with time. Therefore whenever a new interview feedback template is available, it is updated to the database by an admin user.
Whenever an interviewer opens the app, he should see the latest value of the template available in the database as the initial value of the feedback form.
Currently I am able to provide the initial value of the feedback field using the 'initial' argument of the feedback field.
Below is the code (I am interested in the R1Form):
from django import forms
from .models import R1
from .model_templates import template_R1
from ckeditor.widgets import CKEditorWidget
class DateInput(forms.DateInput):
input_type = 'date'
class R1Form(forms.ModelForm):
interview_date = forms.DateField(widget = DateInput())
feedback = forms.CharField(widget = CKEditorWidget(), initial = template_R1.objects.all().last().template)
class Meta:
model = R1
fields = ['interview_date', 'interviewers', 'comment', 'recommended_level', 'progress']
The problem with this approach if that if the template is updated the form field still shows an earlier snapshot of the template when the django server was started.
Is there any other way the form field would show dynamic values as soon as the template is updated in the database? I believe somehow if the initial value could be passed from the views.py, then this could be resolved?
Okay so, I found an elegant solution.
I used the init method of the form class to initialize the feedback field:
class R1Form(forms.ModelForm):
interview_date = forms.DateField(widget=DateInput())
feedback = forms.CharField(widget=CKEditorWidget())
class Meta:
model = R1
fields = ['interview_date', 'interviewers', 'feedback', 'comment', 'recommended_level', 'progress']
And from the views, I used the get_initial method to pass the initial value of the form as the template value from the database.
class R1CreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
model = R1
template_name = "interviews/r1/create.html"
def get_initial(self):
"""Return the initial data to use for forms on this view."""
return {'feedback': template_R1.objects.all().last().template}

Display smileys with html code from TextChoices models

I hope you're well,
I've created a TextChoices models for my UserProfile. If display data {{ user.userprofile.smiley }} I got the code 🍆 but not 🍆. Do you have any idea?
I've added this to my settings.py (DB) 'OPTIONS': {'charset': 'utf8mb4'}, the issue is still here. Also change my MYSQL smiley field to utf8mb4_bin...
class Smileys(models.TextChoices):
AUBERGINE = '🍆'
AVOCADO = '🥑'
BROCCOLI = '🥦'
...
class UserProfile(models.Model):
smiley = models.CharField(max_length=20,choices=Smileys.choices,default=Smileys.CHERRY,)
...
Use the safe template filter when you want to disable HTML escaping for a string
{{ user.userprofile.smiley|safe }}
You may expose yourself to XSS attacks if you use this filter on malicious user provided data so be careful

How to capture many to many field values via get_initial command

I am slowly progressing in my django journey, but this one has me stumped. I am trying to populate a CreateView with a different model via a copy command using the get_initial override. All of the attributes copy as I would expect with the exception of the ManytoMany fields. I've researched this topic most of today, and found the following which is very close to what I'm trying to figure out KeyError: 'manager' in django get_initial.
My View...
class BookView(LoginRequiredMixin,CreateView):
model = Book
template_name = 'book/titles.html'
form_class = BookForm
def get_initial(self):
initial = super(BookView, self).get_initial()
author = author.objects.get(pk=self.kwargs["pk"])
initial = author.__dict__.copy()
initial.update({
"author": author.name,
}}
for field in self.form_class.base_fields.items():
value = getattr(self.get_object(), field)
if field == 'author':
value = self.get_object().author.all()
initial.update({field: value})
return initial
I incorporated the suggested change based on the issue that I found on SO, but I still am getting a 'manager" KeyError. I am ultimately trying to populate the manytomanyfield in my model and then save the values, but to no avail. Any suggests are appreciated!
What a difference a day makes....
def get_initial(self):
initial = super(BookView, self).get_initial()
author = author.objects.get(pk=self.kwargs["pk"])
initial = author.__dict__.copy()
initial.update({
"author": author.name.all(),
}}
return initial
I added a .all() after the reference to the manytomanyfield in my initial get and also update the form to get the field in question. Much cleaner than a few hacks I kinda got working along the way.

Django-Haystack - How to use Haystack with django-comments?

I'm struggling with Django-Haystack.
I need to do an Index that have Articles and Comment articles. My doubt is how can I put in a document based index the Articles and the Comments.
How can I search for keywords in the comments and in the articles and output the article with that keywords(article comments, article)?
It is possible?
Best Regards,
The first thing to do is forget the notion that a SearchIndex must correspond exactly to a model. It's only sourced from one.
The simplest way to do this would be to add the comments to the indexed document using a template. This presume your Article model as a title field:
class ArticleIndex(SearchIndex, indexes.Indexable):
text = CharField(document=True, use_template=True)
title = CharField(model_attr='title')
def get_model(self):
return Article
Note the keyword argument use_template is set to true. The default value for this is search/indexes/{app_label}/{model_name}_{field_name}.txt. In that template just output the content you want to index. E.g.
{{ object.title|safe }}
{{ object.body|safe }}
{% for comment in object.comments.all %}
{{ comment|safe }}
{% endfor %}
While I'm afraid the specific reverse relation name here is probably wrong, that's the gist of what you want to do. Again, this is a simple way of accomplishing what you've specifically stated.
This is what worked for me:
In your models.py, presuming comments are attached to an Article, you want a method that returns comments attached to it (there is no easy way to do this):
class Article:
def comments(self):
ids = [self.id]
ctype = ContentType.objects.get_for_model(Article)
comments = Comment.objects.filter(content_type=ctype,
object_pk__in=ids,
is_removed=False)
return comments
In your search_indexes.py, make sure the ArticleIndex has use_template=True:
from django.contrib.contenttypes.models import ContentType
from django.contrib.comments.models import Comment
class ArticleIndex(SearchIndex):
text = CharField(use_template=True)
In your index template, e.g. templates/search/indexes/article_text.txt:
{% for comment in object.comments.all %}
{{ comment }}
{% endfor %}
Now, the only remaining problem is to update that specific index object when a comment is added or removed. Here we use signals:
In your models.py:
from django.dispatch import receiver
from haystack import site
from django.contrib.comments.signals import (comment_was_posted,
comment_was_flagged)
#receiver(comment_was_posted)
def comment_posted(sender, **kwargs):
site.get_index(Article).update_object(kwargs['comment'].content_object)
#receiver(comment_was_flagged)
def comment_flagged(sender, **kwargs):
site.get_index(Article).update_object(kwargs['comment'].content_object)

Unable to order Haystack/Whoosh results (and it's extremely slow)

I'm using Haystack and Whoosh to search a custom app with city data from the Geonames project.
I only have a small amount of the Geonames city data imported (22917 records). I'd like to order the results by a city's population and I'm having trouble getting good results.
When I use order_by on my SearchQuerySet, the results are extremely slow. It also orders properly with against the 'name' field but not 'population', so I think I'm probably just doing something wrong.
Here's the search index:
class EntryIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
name = indexes.CharField(indexed=False, model_attr='ascii_name')
population = indexes.CharField(indexed=False, model_attr='population')
django_id = indexes.CharField(indexed=False, model_attr='id')
def get_model(self):
return Entry
def index_queryset(self):
return self.get_model().objects.all()
Here's the template:
{{ object.ascii_name }}
{{ object.alternate_names }}
{{ object.country.name }}
{{ object.country.iso }}
{{ object.admin1_division.ascii_name }}
{{ object.admin1_division.name }}
{{ object.admin1_division.code }}
{{ object.admin2_division.ascii_name }}
{{ object.admin2_division.name }}
Here's the relevant view code:
query = request.GET.get('q', '')
results = SearchQuerySet().models(Entry).auto_query(query).order_by('population')
When I take the order_by off the query, it returns in less than one second. With it on, it takes almost 10 seconds to complete, and the results are not ordered by population. Ordering by name works, but it also takes ~10 seconds.
Note: I've also tried with the built-in Haystack search view, and it's very slow when I try to order by population:
qs = SearchQuerySet().order_by('-population')
urlpatterns = patterns('',
...
url(r'^demo2/$', SearchView(searchqueryset=qs)),
)
I'm doing nearly the same thing, and ordering works fast and correctly for me.
The only thing you're doing that differs significantly is:
query = request.GET.get('q', '')
results = SearchQuerySet().models(Entry).auto_query(query).order_by('population')
Since you specify a request, I'm assuming you've created your own view. You shouldn't need a custom view. I have this implemented with this in my urls.py:
from haystack.forms import ModelSearchForm
from haystack.query import SearchQuerySet
from haystack.views import SearchView, search_view_factory
sqs = SearchQuerySet().models(MyModel).order_by('-weight')
urlpatterns += patterns('',
url(r'^search/$', search_view_factory(
view_class=SearchView,
template='search/search.html',
searchqueryset=sqs,
form_class=ModelSearchForm
), name='search'),
)
I found I could not order results using order_by either. I was getting what seemed like a strange partial sorting. I eventually realised that the default ordering was by relevance ranking. The order_by I was using was presumably only sorting within each rank. This point is not really brought out in the Haystack documentation.
I guess the lesson is probably that if you want your results order to ignore relevance you need to post process your results before displaying them.
Probably a bit off topic, but I was a little surprised your index population field is a CharField. Does this match with your model?
I know I'm three years late, but recently I faced the same issue with a project I've been given.
I guess the only problem is the indexed=False parameter you are passing to the population CharField.
I fixed my problem by removing that.

Resources