Django haystack including related content and also boosting it - django

I'm playing with django-haystack and trying to implement it with elasticsearch (I already did actually). My model has title, content and also a tags field which is a ManyRelatedManager:
tags = models.ManyToManyField( 'Tag', through = 'PostTags' )
My search index object is built as follows:
class PostIndex( indexes.SearchIndex, indexes.Indexable ):
text = indexes.CharField( document = True, use_template = True )
title = indexes.CharField( model_attr = 'title', boost = 1.125 )
content = indexes.CharField( model_attr = 'content' )
date_added = indexes.DateTimeField( model_attr = 'date_added' )
My first question is...how do I include the tags in the PostIndex object? I want to give the tags a much higher boost compared to the title and content.
post_text.txt template:
{{ object.title }}
{{ object.content }}
{% for tag in object.tags.all %}
{{ tag.name }}
{% endfor %}

You could add a multi facet field for the tags and populate & boost that at indexing time.
In your PostIndex model:
tags = FacetMultiValueField(boost = 2)
and
def prepare_tags(self, obj):
return [t.name for t in obj.tags.all()]

Related

How to iterate in subitems of a object in a django template

views.py:
from django.views import generic
from .models import Servico
class ServicoView(generic.DetailView):
model = Servico
context_object_name = 'servico'
template_name = 'servico.html'
models.py:
from djongo import models
class PublicoAlvo(models.Model):
def __str__(self):
return ''
alvo1 = models.CharField(max_length = 126)
alvo2 = models.CharField(max_length = 126, blank = True, default = '')
class Meta:
abstract = True
class Servico(models.Model):
t_id = models.CharField(primary_key = True, unique = True, max_length = 252)
alvos = models.EmbeddedField(
model_container = PublicoAlvo
)
urls.py:
from django.urls import path
from . import views
urlpatterns = [
path('servicos/<slug:pk>/', views.ServicoView.as_view(), name = 'servico')
]
I think these are the relevant files in my Django app folder. Back to the question, how can I iterate over the values that are going to be stored in servico.alvos in my template? If I want to show t_id, I just use {{ servico.t_id }} and it works fine. I could write something like:
<HTML1>
{{ servico.alvos.alvo1 }}
<HTML2>
<HTML1>
{{ servico.alvos.alvo2 }}
<HTML2>
And that would show the values that I want, but would make things uglier, since I would have to write a lot of repeated standard HTML (that I indicated as and ) to format each value inside servico.alvos, and more limited (imagine if I decide to change the model and add more 6 values in the PublicoAlvo class). I tried the following:
{% for alvo in servico.alvos.all %}
<HTML1>
{{ alvo }}
<HTML2>
{% endfor %}
and
{% for alvo in servico.alvos.items %}
<HTML1>
{{ alvo }}
<HTML2>
{% endfor %}
But I get nothing printed. When I try:
{% for alvo in servico.alvos %}
<HTML1>
{{ alvo }}
<HTML2>
{% endfor %}
I get 'PublicoAlvo' object is not iterable
Is there a way to get what I want using a loop in my template or changing something in my models.py?
Try
{{ servico.t_id }}
{{ servico.alvos }}
Then in your models.py
class PublicoAlvo(models.Model):
def __str__(self):
# Option 1: List all <alvo> fields
return ", ".join(
[
self.alvo1,
self.alvo2,
]
)
# Option 2: If you don't like manually listing each <alvo> field
# return ", ".join(
# [
# getattr(self, f"alvo{index}") for index in range(1, 3)
# ]
# )
...
This might give something like
The value for alvo1, While for alvo2 is this one
Update
You could also try
{% for key, value in servico.alvos.items %}
{% if key|slice:":4" == "alvo" %}
{{ value }}<br>
{% endif %}
{% endfor %}
This might show something like
The value for alvo1
While for alvo2 is this one
Based on the first answer of Niel Godfrey Ponciano, I was able to solve the problem.
models.py:
from djongo import models
class PublicoAlvo(models.Model):
def __str__(self):
return ''
def list(self):
return [value for key, value in self.__dict__.items() if not key.startswith('_')]
alvo1 = models.CharField(max_length = 126)
alvo2 = models.CharField(max_length = 126, blank = True, default = '')
class Meta:
abstract = True
class Servico(models.Model):
t_id = models.CharField(primary_key = True, unique = True, max_length = 252)
alvos = models.EmbeddedField(
model_container = PublicoAlvo
)
And then I can iterate over servico.alvos.list using a for in the template just by adding the list method that returns the relevant fields (variables) values in my class.

haystack search not indexing multivaluefield django

I am trying to use manytomanyfield for fetch searching but when I am trying to update index, it shows no attribute error
#search_index.py
class ProductCreateModelIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True,template_name='search/indexes/catalogue/product_text.txt')
user = indexes.CharField(model_attr='user',faceted=True )
title = indexes.EdgeNgramField(model_attr='title')
description = indexes.CharField(model_attr='description',null = True)
category = indexes.CharField(model_attr='category',faceted=True )
brand = indexes.CharField(model_attr='brand_name',faceted=True )
availability_regions = indexes.MultiValueField(faceted=True )
model = indexes.EdgeNgramField(model_attr='model_name',null = True)
def prepare_availability_regions(self, obj):
return [ availability_regions.name for a in obj.availability_regions_set.all()]
when I am trying to update it shows me this
return [ availability_regions.name for a in obj.availability_regions_set.all()]
AttributeError: 'ProductCreateModel' object has no attribute 'availability_regions_set'
here is my product_text.txt
{{object.title}}
{% for a in availability_regions %}
{{ a }}
{% endfor %}
{{object.category}}
{{object.model_name}}
{{object.brand_name}}
{{ object.description|default:"" }}
I just follow the tutoria, I don't know how to do it in right way, would you tell me the right way to do it and where can i find the documentation of this ?

Django taggit - retrieving tags into a template

I've set up django-taggit and it's working fine, all tags are listed under tags in admin and I can add tags through the admin and in a form.
I'm having real trouble listing the tags in a template (basically I want a long list of all objects with title, url and tags.
Currently I have a method called return tags attached to the model which should return a list of tags for me to iterate over in the template. Well... that is theory...
Model.py
class DefaultResource(models.Model):
#
# This class is the parent class for all resources in the media manager
#
title = models.CharField(max_length=100)
created_date = models.DateTimeField(auto_now_add=True, auto_now=False)
edited_date = models.DateTimeField(auto_now_add=False,auto_now=True)
level = models.ManyToManyField(AssoeLevel)
agebracket= models.ManyToManyField(AgeBracket)
pathway= models.ManyToManyField(AssoePathway)
tags = TaggableManager()
slug = models.SlugField(max_length=100,editable=False,blank=True)
updownvotes = RatingField(can_change_vote=True)
views = models.DecimalField(max_digits=20,decimal_places=2,default=0,blank=True)
score = models.DecimalField(max_digits=20,decimal_places=4,default=0,blank=True)
icon = models.CharField(max_length=254,editable=False,blank=True)
def return_tags(self):
taglist = self.tags.names()
return taglist
view.py
def index(request):
context = RequestContext(request)
default_resource_list = DefaultResource.objects.order_by('-score')
context_dict = {'default_resource_list':default_resource_list}
return render_to_response('mediamanager/index.html', context_dict, context)
index.html
{% for resource in default_resource_list %}
{% for tag in resource.return_tags %}
{{ tag }}
{% endfor %}
{% endfor %}
Currently this is returning an empty list.
I've also tried putting the following into the template
{% for tag in resource.tags.all %}
{{tag.name}}
{% endfor %}
But this also returns an empty list
I am still trying to figure this out, as I want a list of all tags as links to posts that contain that tag. I've only been able to do this by using django-taggit AND django-taggit-templatetags.
Depending on the version of Django you are running try these:
For earlier versions (before 1.5)
Django-taggit-templatetags
For 1.5 or greater (I'm running 1.10 and this worked great)
Django-taggit-templatetags2
Next I am going to try Django tagging, as the docs are much more complete.
To Jon Clements, I am new to StackOverflow and not sure why my answer was deleted. If you have a better solution to this please advise.
This works on django-taggit 1.3.0:
{% for tag in posts.tags.all %}
{{ tag.name }}
{% endfor %}

Django prefetch related no duplicates with intermediate table

I have a question I am trying to solve for one day now.
With the models
class Quote(models.Model):
text = models.TextField()
source = models.ForeignKey(Source)
tags = models.ManyToManyField(Tag)
...
class Source(models.Model):
title = models.CharField(max_length=100)
...
class Tag(models.Model):
name = models.CharField(max_length=30,unique=True)
slug = models.SlugField(max_length=40,unique=True)
...
I am trying to model the world of quotes. with relationships: one Source having many Quotes, one Quote having many Tags.
Problem is:
How do I get all Tags that are contained in a Source (through the contained Quotes) ?
with the minimum possible queries.
with the amount of times they are contained in that source
I have tried the naive one with no prefetch related, with a model method
def source_tags(self):
tags = Tag.objects.filter(quote__source__id=self.id).distinct().annotate(usage_count=Count('quote'))
return sorted(tags, key=lambda tag:-tag.usage_count)
And in the template:
{% for tag in source.source_tags|slice:":5" %}
source.quote
{% endfor %}
Now I have
sources = Source.objects.all().prefetch_related('quote_set__tags')
And in the template I have no idea how to iterate correctly to get the Tags for one source, and how I would go about counting them instead of listing duplicate tags.
This will get the result in a single SQL query:
# views.py
from django.db.models import Count
from .models import Source
def get_tag_count():
"""
Returns the count of tags associated with each source
"""
sources = Source.objects.annotate(tag_count=Count('quote__tags')) \
.values('title', 'quote__tags__name', 'tag_count') \
.order_by('title')
# Groupe the results as
# {source: {tag: count}}
grouped = {}
for source in sources:
title = source['title']
tag = source['quote__tags__name']
count = source['tag_count']
if not title in grouped:
grouped[title] = {}
grouped[title][tag] = count
return grouped
# in template.html
{% for source, tags in sources.items %}
<h3>{{ source }}</h3>
{% for tag, count in tags.items %}
{% if tag %}
<p>{{ tag }} : {{ count }}</p>
{% endif %}
{% endfor %}
{% endfor %}
Complementary tests :)
# tests.py
from django.test import TestCase
from .models import Source, Tag, Quote
from .views import get_tag_count
class SourceTags(TestCase):
def setUp(self):
abc = Source.objects.create(title='ABC')
xyz = Source.objects.create(title='XYZ')
inspire = Tag.objects.create(name='Inspire', slug='inspire')
lol = Tag.objects.create(name='lol', slug='lol')
q1 = Quote.objects.create(text='I am inspired foo', source=abc)
q2 = Quote.objects.create(text='I am inspired bar', source=abc)
q3 = Quote.objects.create(text='I am lol bar', source=abc)
q1.tags = [inspire]
q2.tags = [inspire]
q3.tags = [inspire, lol]
q1.save(), q2.save(), q3.save()
def test_count(self):
# Ensure that only 1 SQL query is done
with self.assertNumQueries(1):
sources = get_tag_count()
self.assertEqual(sources['ABC']['Inspire'], 3)
self.assertEqual(sources['ABC']['lol'], 1)
I have basically used the annotate and values functions from the ORM. They are very powerful because they automatically perform the joins. They are also very efficient because they hit the database only once, and return only those fields which are specified.

Django Admin change_list filtering multiple ManyToMany

In the Django-Admin you have the possibility to define list_filter on fields of the model. This is working for ManyToMany-Fields as well.
class ModelA(models.Model):
name = models.CharField(max_length=100, verbose_name="Name")
class ModelB(models.Model):
model_a_relation = models.ManyToManyField(ModelA)
class ModelBAdmin(ModelAdmin):
list_filter = [model_a_relation, ]
admin.site.register(ModelB, ModelBAdmin)
Now, I can filter my list of elements of ModelB by relation to ModelA in the Admin object_list of ModelB.
Now my question: Is it possible to filter by multiple objects of ModelA?
In the change_view of ModelB I use django-autocomplete-light to define relations. Can I use this widget to filter in change_list, too?
I imagine the query in the background of this filter like ModelB.objects.filter(model_a_relation__in=names), where names is a list of the chosen objects of ModelA.
Thanks, Horst
I made a really dirty try to solve my issue. It works and I want to share it with you.
For better understanding I use new Models in this example:
class Tag(models.Model):
name = models.CharField(max_length=100, verbose_name="Name")
class Book(models.Model):
tags = models.ManyToManyField(Tag)
read = models.BooleanField()
class BookAdmin(ModelAdmin):
list_filter = ['read', ]
admin.site.register(Book, BookAdmin)
At first, I overwrote the changelist_view of the BookAdmin.
def changelist_view(self, request, extra_context=None):
extra_context = extra_context if extra_context else {}
q = request.GET.copy()
tags = Tag.objects.all().values('id', 'name')
current_tags = q.get('tags__id__in', [])
tag_query = request.GET.copy()
if current_tags:
tag_query.pop('tags__id__in')
current_tags = current_tags.split(',')
all_tag = False
else:
all_tag = True
for tag in tags:
if str(tag['id']) in current_tags:
tag['selected'] = True
temp_list = list(current_tags)
temp_list.remove(str(tag['id']))
tag['tag_ids'] = ','.join(temp_list)
else:
tag['selected'] = False
tag['tag_ids'] = ','.join(current_tags)
extra_context['tag_query'] = '?' if len(tag_query.urlencode()) == 0 else '?' + tag_query.urlencode() + '&'
extra_context['all_tag'] = all_tag
extra_context['tags'] = tags
return super(BookAdmin, self).changelist_view(request, extra_context=extra_context)
As you can see, I look in GET, whether there some Tags are chosen or not. Then I build new GET-Parameter for each possible Tag.
And then there is my overwriten change_list.html
{% extends "admin/change_list.html" %}
{% block content %}
{{ block.super }}
<h3 id="custom_tag_h3"> Fancy Tag filter</h3>
<ul id="custom_tag_ul">
<li{% if all_tag %} class="selected"{% endif %}>
All
</li>
{% for tag in tags %}
<li{% if tag.selected %} class="selected"{% endif %}>
{{ tag.name }}
</li>
{% endfor %}
</ul>
<script type="text/javascript">
$('#changelist-filter').append($('#custom_tag_h3'));
$('#custom_tag_h3').after($('#custom_tag_ul'));
</script>
{% endblock content %}
This way I have a filter looking like the boolean read-filter where I can activate more than one option. By clicking on the filter, a new id is added to the query. Another click on already selected option removes id from query. Click on All removes the hole tags_in-parameter from URL.
Another option to allow users to use an autocomplete in the changelist view:
setup ModelAdmin.search_fields to search in the fields served by the Autocomplete, ie.
class ModelBAdmin(ModelAdmin):
search_fields = ['model_a_relation__name']
override the changelist tomplate to spawn an autocomplete on the search input:
<script type="text/javascript">
$(document).ready(function() {
$('#searchbar').yourlabsAutocomplete({
url: '{% url 'autocomplete_light_autocomplete' 'YourAutocompleteName' %}',
choiceSelector: '[data-value]',
}).input.bind('selectChoice', function(e, choice, autocomplete) {
$(this).val(choice.text())
.parents('form').submit();
});
});
</script>
ensure that there is no JS error after adding this script !