We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
test_post_reorder in editors picks unit tests failing on Sqlite
Running the unit tests under sqlite:

```
DATABASE_ENGINE=django.db.backends.sqlite3 ./runtests.py
```

results in this test failure:

```
FAIL: test_post_reorder (wagtail.wagtailsearch.tests.test_editorspicks.TestEditorsPicksEditView)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/vagrant/wagtail/wagtail/wagtailsearch/tests/test_editorspicks.py", line 222, in test_post_reorder
    self.assertEqual(models.Query.get("Hello").editors_picks.all()[0], self.editors_pick_2)
AssertionError: <EditorsPick: EditorsPick object> != <EditorsPick: EditorsPick object>

----------------------------------------------------------------------
Ran 446 tests in 36.358s

FAILED (failures=1, skipped=9, expected failures=1)
Destroying test database for alias 'default'...
```


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `wagtail/wagtailsearch/views/editorspicks.py`
Content:
```
1 from django.shortcuts import render, redirect, get_object_or_404
2 from django.contrib.auth.decorators import permission_required
3 from django.contrib import messages
4 
5 from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
6 from django.utils.translation import ugettext as _
7 from django.views.decorators.vary import vary_on_headers
8 
9 from wagtail.wagtailsearch import models, forms
10 from wagtail.wagtailadmin.forms import SearchForm
11 
12 
13 @permission_required('wagtailadmin.access_admin')
14 @vary_on_headers('X-Requested-With')
15 def index(request):
16     is_searching = False
17     page = request.GET.get('p', 1)
18     query_string = request.GET.get('q', "")
19 
20     queries = models.Query.objects.filter(editors_picks__isnull=False).distinct()
21 
22     # Search
23     if query_string:
24         queries = queries.filter(query_string__icontains=query_string)
25         is_searching = True
26 
27     # Pagination
28     paginator = Paginator(queries, 20)
29     try:
30         queries = paginator.page(page)
31     except PageNotAnInteger:
32         queries = paginator.page(1)
33     except EmptyPage:
34         queries = paginator.page(paginator.num_pages)
35 
36     if request.is_ajax():
37         return render(request, "wagtailsearch/editorspicks/results.html", {
38             'is_searching': is_searching,
39             'queries': queries,
40             'query_string': query_string,
41         })
42     else:
43         return render(request, 'wagtailsearch/editorspicks/index.html', {
44             'is_searching': is_searching,
45             'queries': queries,
46             'query_string': query_string,
47             'search_form': SearchForm(data=dict(q=query_string) if query_string else None, placeholder=_("Search editor's picks")),
48         })
49 
50 
51 def save_editorspicks(query, new_query, editors_pick_formset):
52     # Save
53     if editors_pick_formset.is_valid():
54         # Set sort_order
55         for i, form in enumerate(editors_pick_formset.ordered_forms):
56             form.instance.sort_order = i
57 
58         editors_pick_formset.save()
59 
60         # If query was changed, move all editors picks to the new query
61         if query != new_query:
62             editors_pick_formset.get_queryset().update(query=new_query)
63 
64         return True
65     else:
66         return False
67 
68 
69 @permission_required('wagtailadmin.access_admin')
70 def add(request):
71     if request.POST:
72         # Get query
73         query_form = forms.QueryForm(request.POST)
74         if query_form.is_valid():
75             query = models.Query.get(query_form['query_string'].value())
76 
77             # Save editors picks
78             editors_pick_formset = forms.EditorsPickFormSet(request.POST, instance=query)
79             if save_editorspicks(query, query, editors_pick_formset):
80                 messages.success(request, _("Editor's picks for '{0}' created.").format(query))
81                 return redirect('wagtailsearch_editorspicks_index')
82             else:
83                 if len(editors_pick_formset.non_form_errors()):
84                     messages.error(request, " ".join(error for error in editors_pick_formset.non_form_errors()))  # formset level error (e.g. no forms submitted)
85                 else:
86                     messages.error(request, _("Recommendations have not been created due to errors"))  # specific errors will be displayed within form fields
87         else:
88             editors_pick_formset = forms.EditorsPickFormSet()
89     else:
90         query_form = forms.QueryForm()
91         editors_pick_formset = forms.EditorsPickFormSet()
92 
93     return render(request, 'wagtailsearch/editorspicks/add.html', {
94         'query_form': query_form,
95         'editors_pick_formset': editors_pick_formset,
96     })
97 
98 
99 @permission_required('wagtailadmin.access_admin')
100 def edit(request, query_id):
101     query = get_object_or_404(models.Query, id=query_id)
102 
103     if request.POST:
104         # Get query
105         query_form = forms.QueryForm(request.POST)
106         # and the recommendations
107         editors_pick_formset = forms.EditorsPickFormSet(request.POST, instance=query)
108 
109         if query_form.is_valid():
110             new_query = models.Query.get(query_form['query_string'].value())
111 
112             # Save editors picks
113             if save_editorspicks(query, new_query, editors_pick_formset):
114                 messages.success(request, _("Editor's picks for '{0}' updated.").format(new_query))
115                 return redirect('wagtailsearch_editorspicks_index')
116             else:
117                 if len(editors_pick_formset.non_form_errors()):
118                     messages.error(request, " ".join(error for error in editors_pick_formset.non_form_errors()))  # formset level error (e.g. no forms submitted)
119                 else:
120                     messages.error(request, _("Recommendations have not been saved due to errors"))  # specific errors will be displayed within form fields
121 
122     else:
123         query_form = forms.QueryForm(initial=dict(query_string=query.query_string))
124         editors_pick_formset = forms.EditorsPickFormSet(instance=query)
125 
126     return render(request, 'wagtailsearch/editorspicks/edit.html', {
127         'query_form': query_form,
128         'editors_pick_formset': editors_pick_formset,
129         'query': query,
130     })
131 
132 
133 @permission_required('wagtailadmin.access_admin')
134 def delete(request, query_id):
135     query = get_object_or_404(models.Query, id=query_id)
136 
137     if request.POST:
138         query.editors_picks.all().delete()
139         messages.success(request, _("Editor's picks deleted."))
140         return redirect('wagtailsearch_editorspicks_index')
141 
142     return render(request, 'wagtailsearch/editorspicks/confirm_delete.html', {
143         'query': query,
144     })
145
```

Path: `wagtail/wagtailsearch/models.py`
Content:
```
1 import datetime
2 
3 from django.db import models
4 from django.utils import timezone
5 from django.utils.encoding import python_2_unicode_compatible
6 
7 from wagtail.wagtailsearch.indexed import Indexed
8 from wagtail.wagtailsearch.utils import normalise_query_string, MAX_QUERY_STRING_LENGTH
9 
10 
11 @python_2_unicode_compatible
12 class Query(models.Model):
13     query_string = models.CharField(max_length=MAX_QUERY_STRING_LENGTH, unique=True)
14 
15     def save(self, *args, **kwargs):
16         # Normalise query string
17         self.query_string = normalise_query_string(self.query_string)
18 
19         super(Query, self).save(*args, **kwargs)
20 
21     def add_hit(self, date=None):
22         if date is None:
23             date = timezone.now().date()
24         daily_hits, created = QueryDailyHits.objects.get_or_create(query=self, date=date)
25         daily_hits.hits = models.F('hits') + 1
26         daily_hits.save()
27 
28     def __str__(self):
29         return self.query_string
30 
31     @property
32     def hits(self):
33         hits = self.daily_hits.aggregate(models.Sum('hits'))['hits__sum']
34         return hits if hits else 0
35 
36     @classmethod
37     def garbage_collect(cls):
38         """
39         Deletes all Query records that have no daily hits or editors picks
40         """
41         cls.objects.filter(daily_hits__isnull=True, editors_picks__isnull=True).delete()
42 
43     @classmethod
44     def get(cls, query_string):
45         return cls.objects.get_or_create(query_string=normalise_query_string(query_string))[0]
46 
47     @classmethod
48     def get_most_popular(cls, date_since=None):
49         # TODO: Implement date_since
50         return cls.objects.filter(daily_hits__isnull=False).annotate(_hits=models.Sum('daily_hits__hits')).distinct().order_by('-_hits')
51 
52 
53 class QueryDailyHits(models.Model):
54     query = models.ForeignKey(Query, db_index=True, related_name='daily_hits')
55     date = models.DateField()
56     hits = models.IntegerField(default=0)
57 
58     @classmethod
59     def garbage_collect(cls):
60         """
61         Deletes all QueryDailyHits records that are older than 7 days
62         """
63         min_date = timezone.now().date() - datetime.timedelta(days=7)
64 
65         cls.objects.filter(date__lt=min_date).delete()
66 
67     class Meta:
68         unique_together = (
69             ('query', 'date'),
70         )
71 
72 
73 class EditorsPick(models.Model):
74     query = models.ForeignKey(Query, db_index=True, related_name='editors_picks')
75     page = models.ForeignKey('wagtailcore.Page')
76     sort_order = models.IntegerField(null=True, blank=True, editable=False)
77     description = models.TextField(blank=True)
78 
79     class Meta:
80         ordering = ('sort_order', )
81 
82 
83 # Used for tests
84 
85 class SearchTest(models.Model, Indexed):
86     title = models.CharField(max_length=255)
87     content = models.TextField()
88     live = models.BooleanField(default=False)
89 
90     indexed_fields = ("title", "content", "callable_indexed_field", "live")
91 
92     def callable_indexed_field(self):
93         return "Callable"
94 
95 
96 class SearchTestChild(SearchTest):
97     extra_content = models.TextField()
98 
99     indexed_fields = "extra_content"
100
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/wagtail/wagtailsearch/models.py b/wagtail/wagtailsearch/models.py
--- a/wagtail/wagtailsearch/models.py
+++ b/wagtail/wagtailsearch/models.py
@@ -76,6 +76,9 @@
     sort_order = models.IntegerField(null=True, blank=True, editable=False)
     description = models.TextField(blank=True)
 
+    def __repr__(self):
+        return 'EditorsPick(query="' + self.query.query_string + '", page="' + self.page.title + '")'
+
     class Meta:
         ordering = ('sort_order', )
 
diff --git a/wagtail/wagtailsearch/views/editorspicks.py b/wagtail/wagtailsearch/views/editorspicks.py
--- a/wagtail/wagtailsearch/views/editorspicks.py
+++ b/wagtail/wagtailsearch/views/editorspicks.py
@@ -55,6 +55,9 @@
         for i, form in enumerate(editors_pick_formset.ordered_forms):
             form.instance.sort_order = i
 
+            # Make sure the form is marked as changed so it gets saved with the new order
+            form.has_changed = lambda: True
+
         editors_pick_formset.save()
 
         # If query was changed, move all editors picks to the new query

{"golden_diff": "diff --git a/wagtail/wagtailsearch/models.py b/wagtail/wagtailsearch/models.py\n--- a/wagtail/wagtailsearch/models.py\n+++ b/wagtail/wagtailsearch/models.py\n@@ -76,6 +76,9 @@\n     sort_order = models.IntegerField(null=True, blank=True, editable=False)\n     description = models.TextField(blank=True)\n \n+    def __repr__(self):\n+        return 'EditorsPick(query=\"' + self.query.query_string + '\", page=\"' + self.page.title + '\")'\n+\n     class Meta:\n         ordering = ('sort_order', )\n \ndiff --git a/wagtail/wagtailsearch/views/editorspicks.py b/wagtail/wagtailsearch/views/editorspicks.py\n--- a/wagtail/wagtailsearch/views/editorspicks.py\n+++ b/wagtail/wagtailsearch/views/editorspicks.py\n@@ -55,6 +55,9 @@\n         for i, form in enumerate(editors_pick_formset.ordered_forms):\n             form.instance.sort_order = i\n \n+            # Make sure the form is marked as changed so it gets saved with the new order\n+            form.has_changed = lambda: True\n+\n         editors_pick_formset.save()\n \n         # If query was changed, move all editors picks to the new query\n", "issue": "test_post_reorder in editors picks unit tests failing on Sqlite\nRunning the unit tests under sqlite:\n\n```\nDATABASE_ENGINE=django.db.backends.sqlite3 ./runtests.py\n```\n\nresults in this test failure:\n\n```\nFAIL: test_post_reorder (wagtail.wagtailsearch.tests.test_editorspicks.TestEditorsPicksEditView)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"/home/vagrant/wagtail/wagtail/wagtailsearch/tests/test_editorspicks.py\", line 222, in test_post_reorder\n    self.assertEqual(models.Query.get(\"Hello\").editors_picks.all()[0], self.editors_pick_2)\nAssertionError: <EditorsPick: EditorsPick object> != <EditorsPick: EditorsPick object>\n\n----------------------------------------------------------------------\nRan 446 tests in 36.358s\n\nFAILED (failures=1, skipped=9, expected failures=1)\nDestroying test database for alias 'default'...\n```\n\n", "before_files": [{"content": "from django.shortcuts import render, redirect, get_object_or_404\nfrom django.contrib.auth.decorators import permission_required\nfrom django.contrib import messages\n\nfrom django.core.paginator import Paginator, EmptyPage, PageNotAnInteger\nfrom django.utils.translation import ugettext as _\nfrom django.views.decorators.vary import vary_on_headers\n\nfrom wagtail.wagtailsearch import models, forms\nfrom wagtail.wagtailadmin.forms import SearchForm\n\n\n@permission_required('wagtailadmin.access_admin')\n@vary_on_headers('X-Requested-With')\ndef index(request):\n    is_searching = False\n    page = request.GET.get('p', 1)\n    query_string = request.GET.get('q', \"\")\n\n    queries = models.Query.objects.filter(editors_picks__isnull=False).distinct()\n\n    # Search\n    if query_string:\n        queries = queries.filter(query_string__icontains=query_string)\n        is_searching = True\n\n    # Pagination\n    paginator = Paginator(queries, 20)\n    try:\n        queries = paginator.page(page)\n    except PageNotAnInteger:\n        queries = paginator.page(1)\n    except EmptyPage:\n        queries = paginator.page(paginator.num_pages)\n\n    if request.is_ajax():\n        return render(request, \"wagtailsearch/editorspicks/results.html\", {\n            'is_searching': is_searching,\n            'queries': queries,\n            'query_string': query_string,\n        })\n    else:\n        return render(request, 'wagtailsearch/editorspicks/index.html', {\n            'is_searching': is_searching,\n            'queries': queries,\n            'query_string': query_string,\n            'search_form': SearchForm(data=dict(q=query_string) if query_string else None, placeholder=_(\"Search editor's picks\")),\n        })\n\n\ndef save_editorspicks(query, new_query, editors_pick_formset):\n    # Save\n    if editors_pick_formset.is_valid():\n        # Set sort_order\n        for i, form in enumerate(editors_pick_formset.ordered_forms):\n            form.instance.sort_order = i\n\n        editors_pick_formset.save()\n\n        # If query was changed, move all editors picks to the new query\n        if query != new_query:\n            editors_pick_formset.get_queryset().update(query=new_query)\n\n        return True\n    else:\n        return False\n\n\n@permission_required('wagtailadmin.access_admin')\ndef add(request):\n    if request.POST:\n        # Get query\n        query_form = forms.QueryForm(request.POST)\n        if query_form.is_valid():\n            query = models.Query.get(query_form['query_string'].value())\n\n            # Save editors picks\n            editors_pick_formset = forms.EditorsPickFormSet(request.POST, instance=query)\n            if save_editorspicks(query, query, editors_pick_formset):\n                messages.success(request, _(\"Editor's picks for '{0}' created.\").format(query))\n                return redirect('wagtailsearch_editorspicks_index')\n            else:\n                if len(editors_pick_formset.non_form_errors()):\n                    messages.error(request, \" \".join(error for error in editors_pick_formset.non_form_errors()))  # formset level error (e.g. no forms submitted)\n                else:\n                    messages.error(request, _(\"Recommendations have not been created due to errors\"))  # specific errors will be displayed within form fields\n        else:\n            editors_pick_formset = forms.EditorsPickFormSet()\n    else:\n        query_form = forms.QueryForm()\n        editors_pick_formset = forms.EditorsPickFormSet()\n\n    return render(request, 'wagtailsearch/editorspicks/add.html', {\n        'query_form': query_form,\n        'editors_pick_formset': editors_pick_formset,\n    })\n\n\n@permission_required('wagtailadmin.access_admin')\ndef edit(request, query_id):\n    query = get_object_or_404(models.Query, id=query_id)\n\n    if request.POST:\n        # Get query\n        query_form = forms.QueryForm(request.POST)\n        # and the recommendations\n        editors_pick_formset = forms.EditorsPickFormSet(request.POST, instance=query)\n\n        if query_form.is_valid():\n            new_query = models.Query.get(query_form['query_string'].value())\n\n            # Save editors picks\n            if save_editorspicks(query, new_query, editors_pick_formset):\n                messages.success(request, _(\"Editor's picks for '{0}' updated.\").format(new_query))\n                return redirect('wagtailsearch_editorspicks_index')\n            else:\n                if len(editors_pick_formset.non_form_errors()):\n                    messages.error(request, \" \".join(error for error in editors_pick_formset.non_form_errors()))  # formset level error (e.g. no forms submitted)\n                else:\n                    messages.error(request, _(\"Recommendations have not been saved due to errors\"))  # specific errors will be displayed within form fields\n\n    else:\n        query_form = forms.QueryForm(initial=dict(query_string=query.query_string))\n        editors_pick_formset = forms.EditorsPickFormSet(instance=query)\n\n    return render(request, 'wagtailsearch/editorspicks/edit.html', {\n        'query_form': query_form,\n        'editors_pick_formset': editors_pick_formset,\n        'query': query,\n    })\n\n\n@permission_required('wagtailadmin.access_admin')\ndef delete(request, query_id):\n    query = get_object_or_404(models.Query, id=query_id)\n\n    if request.POST:\n        query.editors_picks.all().delete()\n        messages.success(request, _(\"Editor's picks deleted.\"))\n        return redirect('wagtailsearch_editorspicks_index')\n\n    return render(request, 'wagtailsearch/editorspicks/confirm_delete.html', {\n        'query': query,\n    })\n", "path": "wagtail/wagtailsearch/views/editorspicks.py"}, {"content": "import datetime\n\nfrom django.db import models\nfrom django.utils import timezone\nfrom django.utils.encoding import python_2_unicode_compatible\n\nfrom wagtail.wagtailsearch.indexed import Indexed\nfrom wagtail.wagtailsearch.utils import normalise_query_string, MAX_QUERY_STRING_LENGTH\n\n\n@python_2_unicode_compatible\nclass Query(models.Model):\n    query_string = models.CharField(max_length=MAX_QUERY_STRING_LENGTH, unique=True)\n\n    def save(self, *args, **kwargs):\n        # Normalise query string\n        self.query_string = normalise_query_string(self.query_string)\n\n        super(Query, self).save(*args, **kwargs)\n\n    def add_hit(self, date=None):\n        if date is None:\n            date = timezone.now().date()\n        daily_hits, created = QueryDailyHits.objects.get_or_create(query=self, date=date)\n        daily_hits.hits = models.F('hits') + 1\n        daily_hits.save()\n\n    def __str__(self):\n        return self.query_string\n\n    @property\n    def hits(self):\n        hits = self.daily_hits.aggregate(models.Sum('hits'))['hits__sum']\n        return hits if hits else 0\n\n    @classmethod\n    def garbage_collect(cls):\n        \"\"\"\n        Deletes all Query records that have no daily hits or editors picks\n        \"\"\"\n        cls.objects.filter(daily_hits__isnull=True, editors_picks__isnull=True).delete()\n\n    @classmethod\n    def get(cls, query_string):\n        return cls.objects.get_or_create(query_string=normalise_query_string(query_string))[0]\n\n    @classmethod\n    def get_most_popular(cls, date_since=None):\n        # TODO: Implement date_since\n        return cls.objects.filter(daily_hits__isnull=False).annotate(_hits=models.Sum('daily_hits__hits')).distinct().order_by('-_hits')\n\n\nclass QueryDailyHits(models.Model):\n    query = models.ForeignKey(Query, db_index=True, related_name='daily_hits')\n    date = models.DateField()\n    hits = models.IntegerField(default=0)\n\n    @classmethod\n    def garbage_collect(cls):\n        \"\"\"\n        Deletes all QueryDailyHits records that are older than 7 days\n        \"\"\"\n        min_date = timezone.now().date() - datetime.timedelta(days=7)\n\n        cls.objects.filter(date__lt=min_date).delete()\n\n    class Meta:\n        unique_together = (\n            ('query', 'date'),\n        )\n\n\nclass EditorsPick(models.Model):\n    query = models.ForeignKey(Query, db_index=True, related_name='editors_picks')\n    page = models.ForeignKey('wagtailcore.Page')\n    sort_order = models.IntegerField(null=True, blank=True, editable=False)\n    description = models.TextField(blank=True)\n\n    class Meta:\n        ordering = ('sort_order', )\n\n\n# Used for tests\n\nclass SearchTest(models.Model, Indexed):\n    title = models.CharField(max_length=255)\n    content = models.TextField()\n    live = models.BooleanField(default=False)\n\n    indexed_fields = (\"title\", \"content\", \"callable_indexed_field\", \"live\")\n\n    def callable_indexed_field(self):\n        return \"Callable\"\n\n\nclass SearchTestChild(SearchTest):\n    extra_content = models.TextField()\n\n    indexed_fields = \"extra_content\"\n", "path": "wagtail/wagtailsearch/models.py"}], "after_files": [{"content": "from django.shortcuts import render, redirect, get_object_or_404\nfrom django.contrib.auth.decorators import permission_required\nfrom django.contrib import messages\n\nfrom django.core.paginator import Paginator, EmptyPage, PageNotAnInteger\nfrom django.utils.translation import ugettext as _\nfrom django.views.decorators.vary import vary_on_headers\n\nfrom wagtail.wagtailsearch import models, forms\nfrom wagtail.wagtailadmin.forms import SearchForm\n\n\n@permission_required('wagtailadmin.access_admin')\n@vary_on_headers('X-Requested-With')\ndef index(request):\n    is_searching = False\n    page = request.GET.get('p', 1)\n    query_string = request.GET.get('q', \"\")\n\n    queries = models.Query.objects.filter(editors_picks__isnull=False).distinct()\n\n    # Search\n    if query_string:\n        queries = queries.filter(query_string__icontains=query_string)\n        is_searching = True\n\n    # Pagination\n    paginator = Paginator(queries, 20)\n    try:\n        queries = paginator.page(page)\n    except PageNotAnInteger:\n        queries = paginator.page(1)\n    except EmptyPage:\n        queries = paginator.page(paginator.num_pages)\n\n    if request.is_ajax():\n        return render(request, \"wagtailsearch/editorspicks/results.html\", {\n            'is_searching': is_searching,\n            'queries': queries,\n            'query_string': query_string,\n        })\n    else:\n        return render(request, 'wagtailsearch/editorspicks/index.html', {\n            'is_searching': is_searching,\n            'queries': queries,\n            'query_string': query_string,\n            'search_form': SearchForm(data=dict(q=query_string) if query_string else None, placeholder=_(\"Search editor's picks\")),\n        })\n\n\ndef save_editorspicks(query, new_query, editors_pick_formset):\n    # Save\n    if editors_pick_formset.is_valid():\n        # Set sort_order\n        for i, form in enumerate(editors_pick_formset.ordered_forms):\n            form.instance.sort_order = i\n\n            # Make sure the form is marked as changed so it gets saved with the new order\n            form.has_changed = lambda: True\n\n        editors_pick_formset.save()\n\n        # If query was changed, move all editors picks to the new query\n        if query != new_query:\n            editors_pick_formset.get_queryset().update(query=new_query)\n\n        return True\n    else:\n        return False\n\n\n@permission_required('wagtailadmin.access_admin')\ndef add(request):\n    if request.POST:\n        # Get query\n        query_form = forms.QueryForm(request.POST)\n        if query_form.is_valid():\n            query = models.Query.get(query_form['query_string'].value())\n\n            # Save editors picks\n            editors_pick_formset = forms.EditorsPickFormSet(request.POST, instance=query)\n            if save_editorspicks(query, query, editors_pick_formset):\n                messages.success(request, _(\"Editor's picks for '{0}' created.\").format(query))\n                return redirect('wagtailsearch_editorspicks_index')\n            else:\n                if len(editors_pick_formset.non_form_errors()):\n                    messages.error(request, \" \".join(error for error in editors_pick_formset.non_form_errors()))  # formset level error (e.g. no forms submitted)\n                else:\n                    messages.error(request, _(\"Recommendations have not been created due to errors\"))  # specific errors will be displayed within form fields\n        else:\n            editors_pick_formset = forms.EditorsPickFormSet()\n    else:\n        query_form = forms.QueryForm()\n        editors_pick_formset = forms.EditorsPickFormSet()\n\n    return render(request, 'wagtailsearch/editorspicks/add.html', {\n        'query_form': query_form,\n        'editors_pick_formset': editors_pick_formset,\n    })\n\n\n@permission_required('wagtailadmin.access_admin')\ndef edit(request, query_id):\n    query = get_object_or_404(models.Query, id=query_id)\n\n    if request.POST:\n        # Get query\n        query_form = forms.QueryForm(request.POST)\n        # and the recommendations\n        editors_pick_formset = forms.EditorsPickFormSet(request.POST, instance=query)\n\n        if query_form.is_valid():\n            new_query = models.Query.get(query_form['query_string'].value())\n\n            # Save editors picks\n            if save_editorspicks(query, new_query, editors_pick_formset):\n                messages.success(request, _(\"Editor's picks for '{0}' updated.\").format(new_query))\n                return redirect('wagtailsearch_editorspicks_index')\n            else:\n                if len(editors_pick_formset.non_form_errors()):\n                    messages.error(request, \" \".join(error for error in editors_pick_formset.non_form_errors()))  # formset level error (e.g. no forms submitted)\n                else:\n                    messages.error(request, _(\"Recommendations have not been saved due to errors\"))  # specific errors will be displayed within form fields\n\n    else:\n        query_form = forms.QueryForm(initial=dict(query_string=query.query_string))\n        editors_pick_formset = forms.EditorsPickFormSet(instance=query)\n\n    return render(request, 'wagtailsearch/editorspicks/edit.html', {\n        'query_form': query_form,\n        'editors_pick_formset': editors_pick_formset,\n        'query': query,\n    })\n\n\n@permission_required('wagtailadmin.access_admin')\ndef delete(request, query_id):\n    query = get_object_or_404(models.Query, id=query_id)\n\n    if request.POST:\n        query.editors_picks.all().delete()\n        messages.success(request, _(\"Editor's picks deleted.\"))\n        return redirect('wagtailsearch_editorspicks_index')\n\n    return render(request, 'wagtailsearch/editorspicks/confirm_delete.html', {\n        'query': query,\n    })\n", "path": "wagtail/wagtailsearch/views/editorspicks.py"}, {"content": "import datetime\n\nfrom django.db import models\nfrom django.utils import timezone\nfrom django.utils.encoding import python_2_unicode_compatible\n\nfrom wagtail.wagtailsearch.indexed import Indexed\nfrom wagtail.wagtailsearch.utils import normalise_query_string, MAX_QUERY_STRING_LENGTH\n\n\n@python_2_unicode_compatible\nclass Query(models.Model):\n    query_string = models.CharField(max_length=MAX_QUERY_STRING_LENGTH, unique=True)\n\n    def save(self, *args, **kwargs):\n        # Normalise query string\n        self.query_string = normalise_query_string(self.query_string)\n\n        super(Query, self).save(*args, **kwargs)\n\n    def add_hit(self, date=None):\n        if date is None:\n            date = timezone.now().date()\n        daily_hits, created = QueryDailyHits.objects.get_or_create(query=self, date=date)\n        daily_hits.hits = models.F('hits') + 1\n        daily_hits.save()\n\n    def __str__(self):\n        return self.query_string\n\n    @property\n    def hits(self):\n        hits = self.daily_hits.aggregate(models.Sum('hits'))['hits__sum']\n        return hits if hits else 0\n\n    @classmethod\n    def garbage_collect(cls):\n        \"\"\"\n        Deletes all Query records that have no daily hits or editors picks\n        \"\"\"\n        cls.objects.filter(daily_hits__isnull=True, editors_picks__isnull=True).delete()\n\n    @classmethod\n    def get(cls, query_string):\n        return cls.objects.get_or_create(query_string=normalise_query_string(query_string))[0]\n\n    @classmethod\n    def get_most_popular(cls, date_since=None):\n        # TODO: Implement date_since\n        return cls.objects.filter(daily_hits__isnull=False).annotate(_hits=models.Sum('daily_hits__hits')).distinct().order_by('-_hits')\n\n\nclass QueryDailyHits(models.Model):\n    query = models.ForeignKey(Query, db_index=True, related_name='daily_hits')\n    date = models.DateField()\n    hits = models.IntegerField(default=0)\n\n    @classmethod\n    def garbage_collect(cls):\n        \"\"\"\n        Deletes all QueryDailyHits records that are older than 7 days\n        \"\"\"\n        min_date = timezone.now().date() - datetime.timedelta(days=7)\n\n        cls.objects.filter(date__lt=min_date).delete()\n\n    class Meta:\n        unique_together = (\n            ('query', 'date'),\n        )\n\n\nclass EditorsPick(models.Model):\n    query = models.ForeignKey(Query, db_index=True, related_name='editors_picks')\n    page = models.ForeignKey('wagtailcore.Page')\n    sort_order = models.IntegerField(null=True, blank=True, editable=False)\n    description = models.TextField(blank=True)\n\n    def __repr__(self):\n        return 'EditorsPick(query=\"' + self.query.query_string + '\", page=\"' + self.page.title + '\")'\n\n    class Meta:\n        ordering = ('sort_order', )\n\n\n# Used for tests\n\nclass SearchTest(models.Model, Indexed):\n    title = models.CharField(max_length=255)\n    content = models.TextField()\n    live = models.BooleanField(default=False)\n\n    indexed_fields = (\"title\", \"content\", \"callable_indexed_field\", \"live\")\n\n    def callable_indexed_field(self):\n        return \"Callable\"\n\n\nclass SearchTestChild(SearchTest):\n    extra_content = models.TextField()\n\n    indexed_fields = \"extra_content\"\n", "path": "wagtail/wagtailsearch/models.py"}]}