kopia lustrzana https://github.com/wagtail/wagtail
Merge branch 'wagtail-separation' into staging
commit
94c6c2defb
|
@ -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):
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
from indexed import Indexed
|
||||
from search import Search
|
||||
from searcher import Searcher
|
||||
from signal_handlers import register_signal_handlers
|
|
@ -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']
|
|
@ -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
|
|
@ -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"
|
|
@ -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()
|
|
@ -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']
|
|
@ -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"
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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();
|
||||
})();
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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>
|
|
@ -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)
|
|
@ -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"),
|
||||
)
|
|
@ -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"),
|
||||
)
|
|
@ -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,
|
||||
})
|
|
@ -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("[]")
|
|
@ -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)
|
Ładowanie…
Reference in New Issue