Merge branch 'wagtail-separation' into staging

pull/3/head
Matt Westcott 2014-01-29 16:26:20 +00:00
commit 94c6c2defb
37 zmienionych plików z 1400 dodań i 3 usunięć

Wyświetl plik

@ -2,7 +2,7 @@ from taggit.models import Tag
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count, Q
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from verdantsearch import Indexed, Search
from wagtail.wagtailsearch import Indexed, Search
class TagSearchable(Indexed):

Wyświetl plik

@ -19,7 +19,7 @@
<a href="#" class="icon icon-arrow-down">More</a>
<ul>
<li class="menu-redirects"><a href="{% url 'wagtailredirects_index' %}" class="icon icon-redirect">Redirects</a></li>
<li class="menu-editorspicks"><a href="{% url 'verdantsearch_editorspicks_index' %}">Editors Picks</a></li>
<li class="menu-editorspicks"><a href="{% url 'wagtailsearch_editorspicks_index' %}">Editors Picks</a></li>
{% get_wagtailadmin_tab_urls as wagtailadmin_tab_urls %}
{% for name, title in wagtailadmin_tab_urls %}
<li class="menu-{{ title|slugify }}"><a href="{% url name %}" class="icon icon-{{name}}">{{ title }}</a></li>

Wyświetl plik

@ -8,7 +8,7 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import Group
from treebeard.mp_tree import MP_Node
from cluster.models import ClusterableModel
from verdantsearch import Indexed, Searcher
from wagtail.wagtailsearch import Indexed, Searcher
from wagtail.wagtailcore.util import camelcase_to_underscore

Wyświetl plik

@ -0,0 +1,4 @@
from indexed import Indexed
from search import Search
from searcher import Searcher
from signal_handlers import register_signal_handlers

Wyświetl plik

@ -0,0 +1,36 @@
from django import forms
from django.forms.models import inlineformset_factory
import models
class QueryForm(forms.Form):
query_string = forms.CharField(label='Search term(s)/phrase', help_text="Enter the full search string to match. An exact match is required for your Editors Picks to be displayed, wildcards are NOT allowed.", required=True)
class EditorsPickForm(forms.ModelForm):
sort_order = forms.IntegerField(required=False)
def __init__(self, *args, **kwargs):
super(EditorsPickForm, self).__init__(*args, **kwargs)
self.fields['page'].widget = forms.HiddenInput()
class Meta:
model = models.EditorsPick
widgets = {
'description': forms.Textarea(attrs=dict(rows=3)),
}
EditorsPickFormSetBase = inlineformset_factory(models.Query, models.EditorsPick, form=EditorsPickForm, can_order=True, can_delete=True, extra=0)
class EditorsPickFormSet(EditorsPickFormSetBase):
def add_fields(self, form, *args, **kwargs):
super(EditorsPickFormSet, self).add_fields(form, *args, **kwargs)
# Hide delete and order fields
form.fields['DELETE'].widget = forms.HiddenInput()
form.fields['ORDER'].widget = forms.HiddenInput()
# Remove query field
del form.fields['query']

Wyświetl plik

@ -0,0 +1,78 @@
from django.db import models
class Indexed(object):
@classmethod
def indexed_get_parent(cls, require_model=True):
for base in cls.__bases__:
if issubclass(base, Indexed) and (issubclass(base, models.Model) or require_model == False):
return base
@classmethod
def indexed_get_content_type(cls):
# Work out content type
content_type = (cls._meta.app_label + "_" + cls.__name__).lower()
# Get parent content type
parent = cls.indexed_get_parent()
if parent:
parent_content_type = parent.indexed_get_content_type()
return parent_content_type + "_" + content_type
else:
return content_type
@classmethod
def indexed_get_toplevel_content_type(cls):
# Get parent content type
parent = cls.indexed_get_parent()
if parent:
return parent.indexed_get_content_type()
else:
# At toplevel, return this content type
return (cls._meta.app_label + "_" + cls.__name__).lower()
@classmethod
def indexed_get_indexed_fields(cls):
# Get indexed fields for this class as dictionary
indexed_fields = cls.indexed_fields
if isinstance(indexed_fields, tuple):
indexed_fields = list(indexed_fields)
if isinstance(indexed_fields, basestring):
indexed_fields = [indexed_fields]
if isinstance(indexed_fields, list):
indexed_fields = {field: dict(type="string") for field in indexed_fields}
if not isinstance(indexed_fields, dict):
raise ValueError()
# Get indexed fields for parent class
parent = cls.indexed_get_parent(require_model=False)
if parent:
# Add parent fields into this list
parent_indexed_fields = parent.indexed_get_indexed_fields()
indexed_fields = dict(parent_indexed_fields.items() + indexed_fields.items())
return indexed_fields
def indexed_get_document_id(self):
return self.indexed_get_toplevel_content_type() + ":" + str(self.pk)
def indexed_build_document(self):
# Get content type, indexed fields and id
content_type = self.indexed_get_content_type()
indexed_fields = self.indexed_get_indexed_fields()
doc_id = self.indexed_get_document_id()
# Build document
doc = dict(pk=str(self.pk), content_type=content_type, id=doc_id)
for field in indexed_fields.keys():
if hasattr(self, field):
doc[field] = getattr(self, field)
# Check if this field is callable
if hasattr(doc[field], "__call__"):
# Call it
doc[field] = doc[field]()
return doc
indexed_fields = ()
indexed = True

Wyświetl plik

@ -0,0 +1,15 @@
from django.core.management.base import NoArgsCommand
from wagtail.wagtailsearch import models
class Command(NoArgsCommand):
def handle_noargs(self, **options):
# Clean daily hits
print "Cleaning daily hits records... ",
models.QueryDailyHits.garbage_collect()
print "Done"
# Clean queries
print "Cleaning query records... ",
models.Query.garbage_collect()
print "Done"

Wyświetl plik

@ -0,0 +1,67 @@
from django.core.management.base import NoArgsCommand
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from wagtail.wagtailsearch.indexed import Indexed
from wagtail.wagtailsearch.search import Search
class Command(NoArgsCommand):
def handle_noargs(self, **options):
# Print info
print "Getting object list"
# Get list of indexed models
indexed_models = [model for model in models.get_models() if issubclass(model, Indexed)]
# Object set
object_set = {}
# Add all objects to object set and detect any duplicates
# Duplicates are caused when both a model and a derived model are indexed
# Eg, StudentPage inherits from Page and both of these models are indexed
# If we were to add all objects from both models into the index, all the StudentPages will have two entries
for model in indexed_models:
# Get toplevel content type
toplevel_content_type = model.indexed_get_toplevel_content_type()
# Loop through objects
for obj in model.objects.all():
# Check if this object has an "object_indexed" function
if hasattr(obj, "object_indexed"):
if obj.object_indexed() == False:
continue
# Get key for this object
key = toplevel_content_type + ":" + str(obj.pk)
# Check if this key already exists
if key in object_set:
# Conflict, work out who should get this space
# The object with the longest content type string gets the space
# Eg, "wagtailcore.Page-rca.StudentPage" kicks out "wagtailcore.Page"
if len(obj.indexed_get_content_type()) > len(object_set[key].indexed_get_content_type()):
# Take the spot
object_set[key] = obj
else:
# Space free, take it
object_set[key] = obj
# Search object
s = Search()
# Reset the index
print "Reseting index"
s.reset_index()
# Add types
print "Adding types"
for model in indexed_models:
s.add_type(model)
# Add objects to index
print "Adding objects"
s.add_bulk(object_set.values())
# Refresh index
print "Refreshing index"
s.refresh_index()

Wyświetl plik

@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
depends_on = (
("wagtailcore", "0002_initial_data"),
)
def forwards(self, orm):
# Adding model 'Query'
db.create_table(u'wagtailsearch_query', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('query_string', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)),
))
db.send_create_signal(u'wagtailsearch', ['Query'])
# Adding model 'QueryDailyHits'
db.create_table(u'wagtailsearch_querydailyhits', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('query', self.gf('django.db.models.fields.related.ForeignKey')(related_name='daily_hits', to=orm['wagtailsearch.Query'])),
('date', self.gf('django.db.models.fields.DateField')()),
('hits', self.gf('django.db.models.fields.IntegerField')(default=0)),
))
db.send_create_signal(u'wagtailsearch', ['QueryDailyHits'])
# Adding unique constraint on 'QueryDailyHits', fields ['query', 'date']
db.create_unique(u'wagtailsearch_querydailyhits', ['query_id', 'date'])
# Adding model 'EditorsPick'
db.create_table(u'wagtailsearch_editorspick', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('query', self.gf('django.db.models.fields.related.ForeignKey')(related_name='editors_picks', to=orm['wagtailsearch.Query'])),
('page', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['wagtailcore.Page'])),
('sort_order', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)),
('description', self.gf('django.db.models.fields.TextField')(blank=True)),
))
db.send_create_signal(u'wagtailsearch', ['EditorsPick'])
# Adding model 'SearchTest'
db.create_table(u'wagtailsearch_searchtest', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
('content', self.gf('django.db.models.fields.TextField')()),
))
db.send_create_signal(u'wagtailsearch', ['SearchTest'])
# Adding model 'SearchTestChild'
db.create_table(u'wagtailsearch_searchtestchild', (
(u'searchtest_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['wagtailsearch.SearchTest'], unique=True, primary_key=True)),
('extra_content', self.gf('django.db.models.fields.TextField')()),
))
db.send_create_signal(u'wagtailsearch', ['SearchTestChild'])
def backwards(self, orm):
# Removing unique constraint on 'QueryDailyHits', fields ['query', 'date']
db.delete_unique(u'wagtailsearch_querydailyhits', ['query_id', 'date'])
# Deleting model 'Query'
db.delete_table(u'wagtailsearch_query')
# Deleting model 'QueryDailyHits'
db.delete_table(u'wagtailsearch_querydailyhits')
# Deleting model 'EditorsPick'
db.delete_table(u'wagtailsearch_editorspick')
# Deleting model 'SearchTest'
db.delete_table(u'wagtailsearch_searchtest')
# Deleting model 'SearchTestChild'
db.delete_table(u'wagtailsearch_searchtestchild')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'wagtailcore.page': {
'Meta': {'object_name': 'Page'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['contenttypes.ContentType']"}),
'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': u"orm['auth.User']"}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
'search_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'show_in_menus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
},
u'wagtailsearch.editorspick': {
'Meta': {'ordering': "('sort_order',)", 'object_name': 'EditorsPick'},
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['wagtailcore.Page']"}),
'query': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'editors_picks'", 'to': u"orm['wagtailsearch.Query']"}),
'sort_order': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
},
u'wagtailsearch.query': {
'Meta': {'object_name': 'Query'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'query_string': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
},
u'wagtailsearch.querydailyhits': {
'Meta': {'unique_together': "(('query', 'date'),)", 'object_name': 'QueryDailyHits'},
'date': ('django.db.models.fields.DateField', [], {}),
'hits': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'query': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'daily_hits'", 'to': u"orm['wagtailsearch.Query']"})
},
u'wagtailsearch.searchtest': {
'Meta': {'object_name': 'SearchTest'},
'content': ('django.db.models.fields.TextField', [], {}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
u'wagtailsearch.searchtestchild': {
'Meta': {'object_name': 'SearchTestChild', '_ormbases': [u'wagtailsearch.SearchTest']},
'extra_content': ('django.db.models.fields.TextField', [], {}),
u'searchtest_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['wagtailsearch.SearchTest']", 'unique': 'True', 'primary_key': 'True'})
}
}
complete_apps = ['wagtailsearch']

Wyświetl plik

@ -0,0 +1,101 @@
from django.db import models
from django.utils import timezone
from wagtail.wagtailcore.models import Page
from indexed import Indexed
from searcher import Searcher
import datetime
import string
class Query(models.Model):
query_string = models.CharField(max_length=255, unique=True)
def save(self, *args, **kwargs):
# Normalise query string
self.query_string = self.normalise_query_string(self.query_string)
super(Query, self).save(*args, **kwargs)
def add_hit(self):
daily_hits, created = QueryDailyHits.objects.get_or_create(query=self, date=timezone.now().date())
daily_hits.hits = models.F('hits') + 1
daily_hits.save()
@property
def hits(self):
return self.daily_hits.aggregate(models.Sum('hits'))['hits__sum']
@classmethod
def garbage_collect(cls):
"""
Deletes all Query records that have no daily hits or editors picks
"""
cls.objects.filter(daily_hits__isnull=True, editors_picks__isnull=True).delete()
@classmethod
def get(cls, query_string):
return cls.objects.get_or_create(query_string=cls.normalise_query_string(query_string))[0]
@classmethod
def get_most_popular(cls, date_since=None):
return cls.objects.filter(daily_hits__isnull=False).annotate(_hits=models.Sum('daily_hits__hits')).distinct().order_by('-_hits')
@staticmethod
def normalise_query_string(query_string):
# Convert query_string to lowercase
query_string = query_string.lower()
# Strip punctuation characters
query_string = ''.join([c for c in query_string if c not in string.punctuation])
# Remove double spaces
' '.join(query_string.split())
return query_string
class QueryDailyHits(models.Model):
query = models.ForeignKey(Query, db_index=True, related_name='daily_hits')
date = models.DateField()
hits = models.IntegerField(default=0)
@classmethod
def garbage_collect(cls):
"""
Deletes all QueryDailyHits records that are older than 7 days
"""
min_date = timezone.now().date() - datetime.timedelta(days=7)
cls.objects.filter(date__lt=min_date).delete()
class Meta:
unique_together = (
('query', 'date'),
)
class EditorsPick(models.Model):
query = models.ForeignKey(Query, db_index=True, related_name='editors_picks')
page = models.ForeignKey('wagtailcore.Page')
sort_order = models.IntegerField(null=True, blank=True, editable=False)
description = models.TextField(blank=True)
class Meta:
ordering = ('sort_order', )
# Used for tests
class SearchTest(models.Model, Indexed):
title = models.CharField(max_length=255)
content = models.TextField()
indexed_fields = ("title", "content")
title_search = Searcher(["title"])
class SearchTestChild(SearchTest):
extra_content = models.TextField()
indexed_fields = "extra_content"

Wyświetl plik

@ -0,0 +1,243 @@
from indexed import Indexed
from django.db import models
from django.conf import settings
from pyelasticsearch.exceptions import ElasticHttpNotFoundError
from elasticutils import get_es, S
import string
class SearchResults(object):
def __init__(self, model, query, prefetch_related=[]):
self.model = model
self.query = query
self.count = query.count()
self.prefetch_related = prefetch_related
def __getitem__(self, key):
if isinstance(key, slice):
# Get primary keys
pk_list_unclean = [result._source["pk"] for result in self.query[key]]
# Remove duplicate keys (and preserve order)
seen_pks = set()
pk_list = []
for pk in pk_list_unclean:
if pk not in seen_pks:
seen_pks.add(pk)
pk_list.append(pk)
# Get results
results = self.model.objects.filter(pk__in=pk_list)
# Prefetch related
for prefetch in self.prefetch_related:
results = results.prefetch_related(prefetch)
# Put results into a dictionary (using primary key as the key)
results_dict = {str(result.pk): result for result in results}
# Build new list with items in the correct order
results_sorted = [results_dict[str(pk)] for pk in pk_list if str(pk) in results_dict]
# Return the list
return results_sorted
else:
# Return a single item
pk = self.query[key]._source["pk"]
return self.model.objects.get(pk=pk)
def __len__(self):
return self.count
class Search(object):
def __init__(self):
# Get settings
self.es_urls = getattr(settings, "WAGTAILSEARCH_ES_URLS", ["http://localhost:9200"])
self.es_index = getattr(settings, "WAGTAILSEARCH_ES_INDEX", "verdant")
# Get ElasticSearch interface
self.es = get_es(urls=self.es_urls)
self.s = S().es(urls=self.es_urls).indexes(self.es_index)
def reset_index(self):
# Delete old index
try:
self.es.delete_index(self.es_index)
except ElasticHttpNotFoundError:
pass
# Settings
INDEX_SETTINGS = {
"settings": {
"analysis": {
"analyzer": {
"ngram_analyzer": {
"type": "custom",
"tokenizer": "lowercase",
"filter": ["ngram"]
},
"edgengram_analyzer": {
"type": "custom",
"tokenizer": "lowercase",
"filter": ["edgengram"]
}
},
"tokenizer": {
"ngram_tokenizer": {
"type": "nGram",
"min_gram": 3,
"max_gram": 15,
},
"edgengram_tokenizer": {
"type": "edgeNGram",
"min_gram": 2,
"max_gram": 15,
"side": "front"
}
},
"filter": {
"ngram": {
"type": "nGram",
"min_gram": 3,
"max_gram": 15
},
"edgengram": {
"type": "edgeNGram",
"min_gram": 1,
"max_gram": 15
}
}
}
}
}
# Create new index
self.es.create_index(self.es_index, INDEX_SETTINGS)
def add_type(self, model):
# Make sure that the model is indexed
if not model.indexed:
return
# Get type name
content_type = model.indexed_get_content_type()
# Get indexed fields
indexed_fields = model.indexed_get_indexed_fields()
# Make field list
fields = dict({
"pk": dict(type="string", index="not_analyzed", store="yes"),
"content_type": dict(type="string"),
}.items() + indexed_fields.items())
# Put mapping
self.es.put_mapping(self.es_index, content_type, {
content_type: {
"properties": fields,
}
})
def refresh_index(self):
self.es.refresh(self.es_index)
def can_be_indexed(self, obj):
# Object must be a decendant of Indexed and be a django model
if not isinstance(obj, Indexed) or not isinstance(obj, models.Model):
return False
# Check if this objects model has opted out of indexing
if not obj.__class__.indexed:
return False
# Check if this object has an "object_indexed" function
if hasattr(obj, "object_indexed"):
if obj.object_indexed() == False:
return False
return True
def add(self, obj):
# Make sure the object can be indexed
if not self.can_be_indexed(obj):
return
# Build document
doc = obj.indexed_build_document()
# Add to index
self.es.index(self.es_index, obj.indexed_get_content_type(), doc, id=doc["id"])
def add_bulk(self, obj_list):
# Group all objects by their type
type_set = {}
for obj in obj_list:
# Object must be a decendant of Indexed and be a django model
if not self.can_be_indexed(obj):
continue
# Get object type
obj_type = obj.indexed_get_content_type()
# If type is currently not in set, add it
if obj_type not in type_set:
type_set[obj_type] = []
# Add object to set
type_set[obj_type].append(obj.indexed_build_document())
# Loop through each type and bulk add them
for type_name, type_objects in type_set.items():
print type_name, len(type_objects)
self.es.bulk_index(self.es_index, type_name, type_objects)
def delete(self, obj):
# Object must be a decendant of Indexed and be a django model
if not isinstance(obj, Indexed) or not isinstance(obj, models.Model):
return
# Get ID for document
doc_id = obj.indexed_get_document_id()
# Delete document
try:
self.es.delete(self.es_index, obj.indexed_get_content_type(), doc_id)
except ElasticHttpNotFoundError:
pass # Document doesn't exist, ignore this exception
def search(self, query_string, model, fields=None, filters={}, prefetch_related=[]):
# Model must be a descendant of Indexed and be a django model
if not issubclass(model, Indexed) or not issubclass(model, models.Model):
return []
# Clean up query string
query_string = "".join([c for c in query_string if c not in string.punctuation])
# Check that theres still a query string after the clean up
if not query_string:
return []
# Query
if fields:
query = self.s.query_raw({
"query_string": {
"query": query_string,
"fields": fields,
}
})
else:
query = self.s.query_raw({
"query_string": {
"query": query_string,
}
})
# Filter results by this content type
query = query.filter(content_type__prefix=model.indexed_get_content_type())
# Extra filters
if filters:
query = query.filter(**filters)
# Return search results
return SearchResults(model, query, prefetch_related=prefetch_related)

Wyświetl plik

@ -0,0 +1,14 @@
from search import Search
class Searcher(object):
def __init__(self, fields, filters=dict(), **kwargs):
self.fields = fields
self.filters = filters
def __get__(self, instance, cls):
def dosearch(query_string, **kwargs):
search_kwargs = dict(model=cls, fields=self.fields, filters=self.filters)
search_kwargs.update(kwargs)
return Search().search(query_string, **search_kwargs)
return dosearch

Wyświetl plik

@ -0,0 +1,23 @@
from django.dispatch import Signal
from django.db.models.signals import post_save, post_delete
from django.db import models
from search import Search
from indexed import Indexed
def post_save_signal_handler(instance, **kwargs):
Search().add(instance)
def post_delete_signal_handler(instance, **kwargs):
Search().delete(instance)
def register_signal_handlers():
# Get list of models that should be indexed
indexed_models = [model for model in models.get_models() if issubclass(model, Indexed)]
# Loop through list and register signal handlers for each one
for model in indexed_models:
post_save.connect(post_save_signal_handler, sender=model)
post_delete.connect(post_delete_signal_handler, sender=model)

Wyświetl plik

@ -0,0 +1,43 @@
{% extends "wagtailadmin/base.html" %}
{% block titletag %}Add editors pick{% endblock %}
{% block content %}
<header>
<h1>Add editors pick</h1>
</header>
<div class="nice-padding">
<p>Editors picks are a means of recommending specific pages that might not organically come high up in search results. E.g recommending your primary donation page to a user searching with a less common term like "<em>giving</em>".</p>
<p>The "Search term(s)/phrase" field below must contain the full and exact search for which you wish to provide recommended results, <em>including</em> any misspellings/user error. To help, you can choose from search terms that have been popular with users of your site.</p>
<form action="{% url 'wagtailsearch_editorspicks_add' %}" method="POST">
{% csrf_token %}
<ul class="fields">
<li>
{% include "wagtailsearch/queries/chooser_field.html" with field=query_form.query_string only %}
</li>
<li>
{% include "wagtailsearch/editorspicks/includes/editorspicks_formset.html" with formset=editors_pick_formset only %}
</li>
<li><input type="submit" value="Save" /></li>
</ul>
</form>
</div>
{% endblock %}
{% block extra_css %}
{% include "wagtailadmin/pages/_editor_css.html" %}
{% endblock %}
{% block extra_js %}
{% include "wagtailadmin/pages/_editor_js.html" %}
<script type="text/javascript">
{% include "wagtailsearch/editorspicks/includes/editorspicks_formset.js" with formset=editors_pick_formset only %}
{% include "wagtailsearch/queries/chooser_field.js" only %}
(function() {
createQueryChooser('{{ query_form.query_string.auto_id }}');
})();
</script>
{% endblock %}

Wyświetl plik

@ -0,0 +1,15 @@
{% extends "wagtailadmin/base.html" %}
{% block titletag %}Delete {{ query.query_string }}{% endblock %}
{% block content %}
<header>
<h1>Delete <span>{{ query.query_string }}</span></h1>
</header>
<div class="nice-padding">
<p>Are you sure you want to delete all editors picks for this search term?</p>
<form action="{% url 'wagtailsearch_editorspicks_delete' query.id %}" method="POST">
{% csrf_token %}
<input type="submit" value="Yes, delete" class="serious" />
</form>
</div>
{% endblock %}

Wyświetl plik

@ -0,0 +1,40 @@
{% extends "wagtailadmin/base.html" %}
{% block titletag %}Editing {{ query.query_string }}{% endblock %}
{% block content %}
<header>
<h1>Editing <span>{{ query.query_string }}</span></h1>
</header>
<form action="{% url 'wagtailsearch_editorspicks_edit' query.id %}" method="POST" class="nice-padding">
{% csrf_token %}
<ul class="fields">
<li>
{% include "wagtailsearch/queries/chooser_field.html" with field=query_form.query_string only %}
</li>
<li>
{% include "wagtailsearch/editorspicks/includes/editorspicks_formset.html" with formset=editors_pick_formset only %}
</li>
<li>
<input type="submit" value="Save" />
<a href="{% url 'wagtailsearch_editorspicks_delete' query.id %}" class="button button-secondary no">Delete</a>
</li>
</ul>
</form>
{% endblock %}
{% block extra_css %}
{% include "wagtailadmin/pages/_editor_css.html" %}
{% endblock %}
{% block extra_js %}
{% include "wagtailadmin/pages/_editor_js.html" %}
<script type="text/javascript">
{% include "wagtailsearch/editorspicks/includes/editorspicks_formset.js" with formset=editors_pick_formset only %}
{% include "wagtailsearch/queries/chooser_field.js" only %}
(function() {
createQueryChooser('{{ query_form.query_string.auto_id }}');
})();
</script>
{% endblock %}

Wyświetl plik

@ -0,0 +1,27 @@
<li id="inline_child_{{ form.prefix }}"{% if form.DELETE.value %} style="display: none;"{% endif %}>
<ul class="controls">
<li class="icon text-replace teal icon-arrow-up-big inline-child-move-up" id="{{ form.prefix }}-move-up">Move up</li>
<li class="icon text-replace teal icon-arrow-down-big inline-child-move-down" id="{{ form.prefix }}-move-down">Move down</li>
<li class="icon text-replace teal icon-cross" id="{{ form.DELETE.id_for_label }}-button">Delete</li>
</ul>
<fieldset>
<legend>Editors pick</legend>
<ul class="fields">
<li class="model_choice_field">
{% if form.instance.page %}
{% include "wagtailadmin/edit_handlers/page_chooser_panel.html" with field=form.page page=form.instance.page is_chosen=True only %}
{% else %}
{% include "wagtailadmin/edit_handlers/page_chooser_panel.html" with field=form.page is_chosen=False only %}
{% endif %}
</li>
<li class="char_field">
{% include "wagtailadmin/edit_handlers/field_panel_field.html" with field=form.description only %}
</li>
</ul>
{{ form.id }}
{{ form.ORDER }}
{{ form.DELETE }}
</fieldset>
</li>

Wyświetl plik

@ -0,0 +1,14 @@
{{ formset.management_form }}
<ul class="multiple" id="id_{{ formset.prefix }}-FORMS">
{% for form in formset.forms %}
{% include "wagtailsearch/editorspicks/includes/editorspicks_form.html" with form=form only %}
{% endfor %}
</ul>
<script type="text/django-form-template" id="id_{{ formset.prefix }}-EMPTY_FORM_TEMPLATE">
{% include "wagtailsearch/editorspicks/includes/editorspicks_form.html" with form=formset.empty_form only %}
</script>
<p class="add">
<a class="icon icon-plus-inverse" id="id_{{ formset.prefix }}-ADD" value="Add">Add page</a>
</p>

Wyświetl plik

@ -0,0 +1,20 @@
(function() {
function fixPrefix(str) {return str;}
var panel = InlinePanel({
formsetPrefix: fixPrefix("id_{{ formset.prefix }}"),
emptyChildFormPrefix: fixPrefix("{{ formset.empty_form.prefix }}"),
canOrder: true,
onAdd: function(fixPrefix) {
createPageChooser(fixPrefix('id_{{ formset.prefix }}-__prefix__-page'), 'wagtailcore.page', null);
}
});
{% for form in formset.forms %}
createPageChooser(fixPrefix('id_{{ formset.prefix }}-{{ forloop.counter0 }}-page'), 'wagtailcore.page', null);
panel.initChildControls('{{ formset.prefix }}-{{ forloop.counter0 }}');
{% endfor %}
panel.updateMoveButtonDisabledStates();
})();

Wyświetl plik

@ -0,0 +1,51 @@
{% extends "wagtailadmin/base.html" %}
{% block titletag %}Search Terms{% endblock %}
{% block bodyclass %}page-explorer{% endblock %}
{% block content %}
<header>
<div class="row row-flush">
<div class="left col9">
<h1>Editor's Search Picks</h1>
</div>
<div class="right col3">
<a href="{% url 'wagtailsearch_editorspicks_add' %}" class="button icon icon-plus-inverse">Add new editors pick</a>
</div>
</div>
</header>
<table class="listing full-width">
<col />
<col width="40%"/>
<col />
<thead>
<tr>
<th class="title">Search term(s)</th>
<th>Editors picks</th>
<th>Views (past week)</th>
</tr>
</thead>
<tbody>
{% if queries %}
{% for query in queries %}
<tr>
<td class="title">
<h2><a href="{% url 'wagtailsearch_editorspicks_edit' query.id %}" title="Edit this pick">{{ query.query_string }}</a></h2>
</td>
<td>
{% for editors_pick in query.editors_picks.all %}
<a href="{% url 'wagtailadmin_pages_edit' editors_pick.page.id %}" class="nolink">{{ editors_pick.page.title }}</a>{% if not forloop.last %}, {% endif %}
{% empty %}
None
{% endfor %}
</td>
<td>{{ query.hits }}</td>
</tr>
{% endfor %}
{% else %}
<tr><td colspan="3" class="no-results-message"><p>No editors picks have been added. Why not <a href="{% url 'wagtailsearch_editorspicks_add' %}">add one</a>?</td></tr>
{% endif %}
</tbody>
</table>
{% endblock %}

Wyświetl plik

@ -0,0 +1,17 @@
<header>
<h1>Popular search terms</h1>
</header>
<form class="query-search search-bar full-width" action="{% url 'wagtailsearch_queries_chooserresults' %}" method="GET" autocomplete="off">
<ul class="fields">
{% for field in searchform %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field %}
{% endfor %}
<li class="submit"><input type="submit" value="Search" /></li>
</ul>
</form>
<div class="nice-padding">
<div id="query-results">
{% include "wagtailsearch/queries/chooser/results.html" %}
</div>
</div>

Wyświetl plik

@ -0,0 +1,62 @@
function(modal) {
function ajaxifyLinks (context) {
$('.listing a', context).click(function() {
modal.loadUrl(this.href);
return false;
});
$('.pagination a', context).click(function() {
var page = this.getAttribute("data-page");
setPage(page);
return false;
});
}
var searchUrl = $('form.query-search', modal.body).attr('action');
function search() {
$.ajax({
url: searchUrl,
data: {q: $('#id_q').val()},
success: function(data, status) {
$('#query-results').html(data);
ajaxifyLinks($('#query-results'));
}
});
return false;
}
function setPage(page) {
if($('#id_q').val().length){
dataObj = {q: $('#id_q').val(), p: page};
}else{
dataObj = {p: page};
}
$.ajax({
url: searchUrl,
data: dataObj,
success: function(data, status) {
$('#query-results').html(data);
ajaxifyLinks($('#query-results'));
}
});
return false;
}
ajaxifyLinks(modal.body);
$('form.query-search', modal.body).submit(search);
$('a.choose-query', modal.body).click(function() {
modal.respond('queryChosen', $(this).data());
modal.close();
return false;
});
$('#id_q').on('input', function() {
clearTimeout($.data(this, 'timer'));
var wait = setTimeout(search, 200);
$(this).data('timer', wait);
});
}

Wyświetl plik

@ -0,0 +1,27 @@
<table class="listing chooser">
<col />
<col width="40%"/>
<col />
<thead>
<tr>
<th class="title">Terms</th>
<th>Views (past week)</th>
</tr>
</thead>
<tbody>
{% if queries %}
{% for query in queries %}
<tr class="can-choose">
<td class="title">
<h2><a class="choose-query" href="#{{ query.id }}" data-id="{{ query.id }}" data-querystring="{{ query.query_string }}">{{ query.query_string }}</a></h2>
</td>
<td>{{ query.hits }}</td>
</tr>
{% endfor %}
{% else %}
<tr><td colspan="3" class="no-results-message"><p>No results found</p></td></tr>
{% endif %}
</tbody>
</table>
{% include "wagtailadmin/shared/pagination_nav.html" with items=queries is_ajax=1 %}

Wyświetl plik

@ -0,0 +1,9 @@
{% extends "wagtailadmin/edit_handlers/field_panel_field.html" %}
{% block form_field %}
<div class="chooser searchterm-chooser">
{{ field }}
<input id="{{ field.auto_id }}-chooser" type="button" class="button-secondary searchterms-chooser" value="Choose from popular search terms">
</div>
{% endblock %}

Wyświetl plik

@ -0,0 +1,17 @@
function createQueryChooser(id) {
var chooserElement = $('#' + id + '-chooser');
var input = $('#' + id);
chooserElement.click(function() {
var initialUrl = '{% url "wagtailsearch_queries_chooser" %}';
ModalWorkflow({
'url': initialUrl,
'responses': {
'queryChosen': function(queryData) {
input.val(queryData.querystring);
}
}
});
});
}

Wyświetl plik

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<title>Search{% if search_results %} Results{% endif %}</title>
</head>
<body>
<h1>Search{% if search_results %} Results{% endif %}</h1>
<form action="{% url 'wagtailsearch_search' %}" method="get">
<input type="text" name="q"{% if query_string %} value="{{ query_string }}"{% endif %}>
<input type="submit" value="Search">
</form>
{% if search_results %}
<ul>
{% for result in search_results %}
<li><a href="{{ result.specific.url }}">{{ result.specific }}</a></li>
{% endfor %}
</ul>
{% endif %}
</body>
</html>

Wyświetl plik

@ -0,0 +1,51 @@
from search import Search
import models
from django.test import TestCase
class TestSearch(TestCase):
def test_search(self):
# Create search interface and reset the index
s = Search()
s.reset_index()
# Create a couple of objects and add them to the index
testa = models.SearchTest()
testa.title = "Hello World"
testa.save()
s.add(testa)
testb = models.SearchTest()
testb.title = "Hello"
testb.save()
s.add(testb)
testc = models.SearchTestChild()
testc.title = "Hello"
testc.save()
s.add(testc)
# Refresh index
s.refresh_index()
# Ordinary search
results = s.search("Hello", models.SearchTest)
self.assertEqual(len(results), 3)
# Ordinary search on "World"
results = s.search("World", models.SearchTest)
self.assertEqual(len(results), 1)
# Searcher search
results = models.SearchTest.title_search("Hello")
self.assertEqual(len(results), 3)
# Ordinary search on child
results = s.search("Hello", models.SearchTestChild)
self.assertEqual(len(results), 1)
# Searcher search on child
results = models.SearchTestChild.title_search("Hello")
self.assertEqual(len(results), 1)

Wyświetl plik

@ -0,0 +1,12 @@
from django.conf.urls import patterns, url
urlpatterns = patterns("wagtail.wagtailsearch.views",
url(r"^editorspicks/$", "editorspicks.index", name="wagtailsearch_editorspicks_index"),
url(r"^editorspicks/add/$", "editorspicks.add", name="wagtailsearch_editorspicks_add"),
url(r"^editorspicks/(\d+)/$", "editorspicks.edit", name="wagtailsearch_editorspicks_edit"),
url(r"^editorspicks/(\d+)/delete/$", "editorspicks.delete", name="wagtailsearch_editorspicks_delete"),
url(r"^queries/chooser/$", "queries.chooser", name="wagtailsearch_queries_chooser"),
url(r"^queries/chooser/results/$", "queries.chooserresults", name="wagtailsearch_queries_chooserresults"),
)

Wyświetl plik

@ -0,0 +1,7 @@
from django.conf.urls import patterns, url
urlpatterns = patterns("wagtail.wagtailsearch.views.frontend",
url(r"^$", "search", name="wagtailsearch_search"),
url(r"^suggest/$", "suggest", name="wagtailsearch_suggest"),
)

Wyświetl plik

@ -0,0 +1,94 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from wagtail.wagtailsearch import models, forms
@login_required
def index(request):
# Select only queries with editors picks
queries = models.Query.objects.filter(editors_picks__isnull=False).distinct()
return render(request, 'wagtailsearch/editorspicks/index.html', {
'queries': queries,
})
def save_editorspicks(query, new_query, editors_pick_formset):
# Set sort_order
for i, form in enumerate(editors_pick_formset.ordered_forms):
form.instance.sort_order = i
# Save
if editors_pick_formset.is_valid():
editors_pick_formset.save()
# If query was changed, move all editors picks to the new query
if query != new_query:
editors_pick_formset.get_queryset().update(query=new_query)
return True
else:
return False
@login_required
def add(request):
if request.POST:
# Get query
query_form = forms.QueryForm(request.POST)
if query_form.is_valid():
query = models.Query.get(query_form['query_string'].value())
# Save editors picks
editors_pick_formset = forms.EditorsPickFormSet(request.POST, instance=query)
if save_editorspicks(query, query, editors_pick_formset):
return redirect('wagtailsearch_editorspicks_index')
else:
editors_pick_formset = forms.EditorsPickFormSet()
else:
query_form = forms.QueryForm()
editors_pick_formset = forms.EditorsPickFormSet()
return render(request, 'wagtailsearch/editorspicks/add.html', {
'query_form': query_form,
'editors_pick_formset': editors_pick_formset,
})
@login_required
def edit(request, query_id):
query = get_object_or_404(models.Query, id=query_id)
if request.POST:
# Get query
query_form = forms.QueryForm(request.POST)
if query_form.is_valid():
new_query = models.Query.get(query_form['query_string'].value())
# Save editors picks
editors_pick_formset = forms.EditorsPickFormSet(request.POST, instance=query)
if save_editorspicks(query, new_query, editors_pick_formset):
return redirect('wagtailsearch_editorspicks_index')
else:
query_form = forms.QueryForm(initial=dict(query_string=query.query_string))
editors_pick_formset = forms.EditorsPickFormSet(instance=query)
return render(request, 'wagtailsearch/editorspicks/edit.html', {
'query_form': query_form,
'editors_pick_formset': editors_pick_formset,
'query': query,
})
@login_required
def delete(request, query_id):
query = get_object_or_404(models.Query, id=query_id)
if request.POST:
query.editors_picks.all().delete()
return redirect('wagtailsearch_editorspicks_index')
return render(request, 'wagtailsearch/editorspicks/confirm_delete.html', {
'query': query,
})

Wyświetl plik

@ -0,0 +1,68 @@
from django.conf import settings
from django.shortcuts import render
from django.http import HttpResponse
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from wagtail.wagtailcore import models
from wagtail.wagtailsearch import Search
from wagtail.wagtailsearch.models import Query
import json
def search(request):
query_string = request.GET.get("q", "")
page = request.GET.get("p", 1)
# Search
if query_string != "":
search_results = models.Page.search_frontend(query_string)
# Get query object
query = Query.get(query_string)
# Add hit
query.add_hit()
# Pagination
paginator = Paginator(search_results, 10)
if paginator is not None:
try:
search_results = paginator.page(page)
except PageNotAnInteger:
search_results = paginator.page(1)
except EmptyPage:
search_results = paginator.page(paginator.num_pages)
else:
search_results = None
else:
query = None
search_results = None
# Render
if request.is_ajax():
template_name = getattr(settings, "WAGTAILSEARCH_RESULTS_TEMPLATE_AJAX", "wagtailsearch/search_results.html")
else:
template_name = getattr(settings, "WAGTAILSEARCH_RESULTS_TEMPLATE", "wagtailsearch/search_results.html")
return render(request, template_name, dict(query_string=query_string, search_results=search_results, is_ajax=request.is_ajax(), query=query))
def suggest(request):
query_string = request.GET.get("q", "")
# Search
if query_string != "":
search_results = models.Page.title_search_frontend(query_string)[:5]
# Get list of suggestions
suggestions = []
for result in search_results:
search_name = result.specific.search_name
suggestions.append({
"label": result.title,
"type": search_name if search_name else '',
"url": result.url,
})
return HttpResponse(json.dumps(suggestions))
else:
return HttpResponse("[]")

Wyświetl plik

@ -0,0 +1,53 @@
from django.shortcuts import get_object_or_404, render
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.contrib.auth.decorators import login_required
from wagtail.wagtailadmin.modal_workflow import render_modal_workflow
from wagtail.wagtailadmin.forms import SearchForm
from wagtail.wagtailsearch import models
@login_required
def chooser(request, get_results=False):
# Get most popular queries
queries = models.Query.get_most_popular()
# If searching, filter results by query string
if 'q' in request.GET:
searchform = SearchForm(request.GET)
if searchform.is_valid():
q = searchform.cleaned_data['q']
queries = queries.filter(query_string__icontains=models.Query.normalise_query_string(q))
is_searching = True
else:
is_searching = False
else:
searchform = SearchForm()
is_searching = False
# Pagination
p = request.GET.get('p', 1)
paginator = Paginator(queries, 10)
try:
queries = paginator.page(p)
except PageNotAnInteger:
queries = paginator.page(1)
except EmptyPage:
queries = paginator.page(paginator.num_pages)
# Render
if get_results:
return render(request, "wagtailsearch/queries/chooser/results.html", {
'queries': queries,
'is_searching': is_searching,
})
else:
return render_modal_workflow(request, 'wagtailsearch/queries/chooser/chooser.html', 'wagtailsearch/queries/chooser/chooser.js',{
'queries': queries,
'searchform': searchform,
'is_searching': False,
})
def chooserresults(request):
return chooser(request, get_results=True)