From 759bd97d95404dfe3189ea0b7c5dd3bec95ff714 Mon Sep 17 00:00:00 2001 From: michaelglenister Date: Wed, 18 Feb 2026 11:39:26 +0200 Subject: [PATCH 1/4] Removing ES --- .github/workflows/test.yml | 2 +- bin/wait-for-deps.sh | 1 - bin/wait-for-elasticsearch.sh | 13 -- docker-compose.yml | 19 -- global_test_case.py | 11 - nuntium/forms.py | 11 - nuntium/search_indexes.py | 26 --- nuntium/subdomain_urls.py | 4 +- .../templates/nuntium/instance_search.html | 27 +-- nuntium/templates/nuntium/search.html | 27 +-- .../search/indexes/nuntium/answer_text.txt | 2 - .../search/indexes/nuntium/message_text.txt | 2 - nuntium/tests/answers_search_test.py | 72 ------ nuntium/tests/messages_search_test.py | 209 ------------------ nuntium/urls.py | 4 +- nuntium/views.py | 71 +++--- requirements.txt | 3 - setup.py | 2 - writeit/settings.py | 27 --- 19 files changed, 67 insertions(+), 466 deletions(-) delete mode 100755 bin/wait-for-elasticsearch.sh delete mode 100644 nuntium/search_indexes.py delete mode 100644 nuntium/templates/search/indexes/nuntium/answer_text.txt delete mode 100644 nuntium/templates/search/indexes/nuntium/message_text.txt delete mode 100644 nuntium/tests/answers_search_test.py delete mode 100644 nuntium/tests/messages_search_test.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aedb11da..e652c0de 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: - name: Run tests run: docker compose run --rm web bin/wait-for-deps.sh coverage run --source=nuntium,contactos,mailit,instance,mailreporter manage.py test nuntium contactos mailit instance mailreporter - - run: docker compose logs db elasticsearch + - run: docker compose logs db if: ${{ always() }} # Run codecov passing appropriate codecov.io CI environment variables to container diff --git a/bin/wait-for-deps.sh b/bin/wait-for-deps.sh index b23d3ca1..e7924083 100755 --- a/bin/wait-for-deps.sh +++ b/bin/wait-for-deps.sh @@ -4,6 +4,5 @@ set -o errexit set -o nounset source $(dirname $0)/wait-for-postgres.sh -source $(dirname $0)/wait-for-elasticsearch.sh exec "$@" diff --git a/bin/wait-for-elasticsearch.sh b/bin/wait-for-elasticsearch.sh deleted file mode 100755 index b732c23a..00000000 --- a/bin/wait-for-elasticsearch.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -elasticsearch_ready() { - wget -q --waitretry=5 --retry-connrefused -T 10 -O - ELASTICSEARCH_URL -} - -until elasticsearch_ready; do - >&2 echo 'Waiting for elasticsearch to become available...' - sleep 1 -done ->&2 echo 'elasticsearch is available' - -exec "$@" diff --git a/docker-compose.yml b/docker-compose.yml index 989ab502..94790d30 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,6 @@ services: command: bin/wait-for-deps.sh ./manage.py runserver 0.0.0.0:8000 depends_on: - db - - elasticsearch - rabbitmq environment: - CELERY_BROKER_URL=amqp://guest:guest@rabbitmq// @@ -23,8 +22,6 @@ services: - DJANGO_DEBUG_TOOLBAR - DJANGO_SECRET_KEY=not-secret-in-dev - DJANGO_TESTING - - ELASTICSEARCH_INDEX=writeinpublic - - ELASTICSEARCH_URL=http://elasticsearch:9200/ - EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend - SESSION_COOKIE_DOMAIN=127.0.0.1.nip.io @@ -36,14 +33,11 @@ services: command: bin/wait-for-deps.sh celery -A writeit worker depends_on: - db - - elasticsearch - rabbitmq environment: - DATABASE_URL=postgresql://writeinpublic:devpassword@db/writeinpublic - DJANGO_DEBUG=True - DJANGO_SECRET_KEY=not-secret-in-dev - - ELASTICSEARCH_INDEX=writeinpublic - - ELASTICSEARCH_URL=http://elasticsearch:9200/ - EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend beat: @@ -54,14 +48,11 @@ services: command: bin/wait-for-deps.sh celery -A writeit beat --pidfile= -s /var/celerybeat/celerybeat-schedule depends_on: - db - - elasticsearch - rabbitmq environment: - DATABASE_URL=postgresql://writeinpublic:devpassword@db/writeinpublic - DJANGO_DEBUG=True - DJANGO_SECRET_KEY=not-secret-in-dev - - ELASTICSEARCH_INDEX=writeinpublic - - ELASTICSEARCH_URL=http://elasticsearch:9200/ - EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend db: @@ -73,15 +64,6 @@ services: volumes: - db-data:/var/lib/postgresql/data - elasticsearch: - image: elasticsearch:1 - volumes: - - elasticsearch-data:/usr/share/elasticsearch/data - environment: - - ES_MAX_MEM=1g - ports: - - 9200 - rabbitmq: image: rabbitmq:3.7.28-management volumes: @@ -96,6 +78,5 @@ volumes: web-attachments: web-coverage: db-data: - elasticsearch-data: rabbitmq-data: beat-data: diff --git a/global_test_case.py b/global_test_case.py index 41be9da2..e5a203af 100644 --- a/global_test_case.py +++ b/global_test_case.py @@ -1,8 +1,6 @@ import re from django.test import TestCase -from unittest import skipUnless -from django.core.management import call_command from tastypie.test import ResourceTestCase from django.conf import settings from django.contrib.sites.models import Site @@ -13,7 +11,6 @@ from django.test import RequestFactory from django.test.client import Client import logging -from haystack.signals import BaseSignalProcessor _LOCALS = threading.local() @@ -150,11 +147,6 @@ class ResourceGlobalTestCase(WriteItTestCaseMixin, ResourceTestCase): pass -@skipUnless(settings.LOCAL_ELASTICSEARCH, "No local elasticsearch") -class SearchIndexTestCase(GlobalTestCase): - def setUp(self): - super(SearchIndexTestCase, self).setUp() - call_command('rebuild_index', verbosity=0, interactive=False) from djcelery.contrib.test_runner import CeleryTestSuiteRunner from django_nose import NoseTestSuiteRunner @@ -169,9 +161,6 @@ def run_tests(self, test_labels, extra_tests=None, **kwargs): return super(WriteItTestRunner, self).run_tests(test_labels, extra_tests, **kwargs) -class CeleryTestingSignalProcessor(BaseSignalProcessor): - pass - from vcr import VCR diff --git a/nuntium/forms.py b/nuntium/forms.py index 8b9846d1..cd7c70bb 100644 --- a/nuntium/forms.py +++ b/nuntium/forms.py @@ -9,7 +9,6 @@ from popolo.models import Person from django.forms import ValidationError from django.utils.translation import ugettext as _, ungettext, pgettext_lazy -from haystack.forms import SearchForm from django.utils.html import format_html from django.utils.encoding import force_text from django.utils.safestring import mark_safe @@ -193,16 +192,6 @@ class Meta: fields = [] -class MessageSearchForm(SearchForm): - pass - - -class PerInstanceSearchForm(SearchForm): - def __init__(self, *args, **kwargs): - self.writeitinstance = kwargs.pop('writeitinstance', None) - super(PerInstanceSearchForm, self).__init__(*args, **kwargs) - self.searchqueryset = self.searchqueryset.filter(writeitinstance=self.writeitinstance.id) - class PopitParsingFormMixin(object): def get_scheme(self, hostname, scheme): diff --git a/nuntium/search_indexes.py b/nuntium/search_indexes.py deleted file mode 100644 index d7206a16..00000000 --- a/nuntium/search_indexes.py +++ /dev/null @@ -1,26 +0,0 @@ -# coding=utf-8 -from haystack import indexes -from celery_haystack.indexes import CelerySearchIndex -from .models import Message, Answer - - -class MessageIndex(CelerySearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True) - writeitinstance = indexes.IntegerField(model_attr='writeitinstance__id') - - def get_model(self): - return Message - - def index_queryset(self, using=None): - return self.get_model().public_objects.all() - - -class AnswerIndex(CelerySearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True) - writeitinstance = indexes.IntegerField(model_attr='message__writeitinstance__id') - - def get_model(self): - return Answer - - def index_queryset(self, using=None): - return self.get_model().objects.filter(message__in=Message.public_objects.all()) diff --git a/nuntium/subdomain_urls.py b/nuntium/subdomain_urls.py index 52610b45..1bbf83ff 100644 --- a/nuntium/subdomain_urls.py +++ b/nuntium/subdomain_urls.py @@ -15,7 +15,7 @@ MessageThreadsView, MessagesFromPersonView, MessagesPerPersonView, - PerInstanceSearchView, + per_instance_search, WriteMessageView, WriteSignView, WriteItInstanceDetailView, @@ -127,7 +127,7 @@ url(r'^from/(?P[-\w]+)/?$', MessagesFromPersonView.as_view(), name='all-messages-from-the-same-author-as'), url(r'^to/(?P[-\d]+)/$', MessagesPerPersonView.as_view(), name='thread_to'), - url(r'^search/$', PerInstanceSearchView(), name='instance_search'), + url(r'^search/$', per_instance_search, name='instance_search'), url(r'^attachment/(?P[-\d]+)/$', download_attachment_view, name='attachment'), url(r'^manage/', include(managepatterns)), url(r'^accounts/logout/$', 'django.contrib.auth.views.logout', kwargs={'next_page': '/'}, name='logout'), diff --git a/nuntium/templates/nuntium/instance_search.html b/nuntium/templates/nuntium/instance_search.html index d93ca474..77637626 100644 --- a/nuntium/templates/nuntium/instance_search.html +++ b/nuntium/templates/nuntium/instance_search.html @@ -3,20 +3,15 @@ {% load subdomainurls %} {% block content %} {% load markdown_deux_tags %} -
    -{% for message in messages %} -
  • {{ message }}
  • -{% endfor %} -
+ {% if query %}

{% trans 'Results' %}

- {% for result in page.object_list %} + {% for result in page %}
- {% if result.model_name == 'message' %} - {% include 'nuntium/message/message_in_list.html' with message=result.object %} - {% endif %} - {% if result.model_name == 'answer' %} - {% include 'nuntium/answer/answer_in_list.html' with answer=result.object %} + {% if result.subject %} + {% include 'nuntium/message/message_in_list.html' with message=result %} + {% else %} + {% include 'nuntium/answer/answer_in_list.html' with answer=result %} {% endif %}
{% empty %} @@ -37,11 +32,11 @@

{% trans 'Results' %}

{% endblock content %} {% block search_form %} - -{% endblock search_form%} +{% endblock search_form %} diff --git a/nuntium/templates/nuntium/search.html b/nuntium/templates/nuntium/search.html index 47c8177e..60c8a535 100644 --- a/nuntium/templates/nuntium/search.html +++ b/nuntium/templates/nuntium/search.html @@ -3,35 +3,24 @@ {% load subdomainurls %} {% block content %} {% load markdown_deux_tags %} -
    -{% for message in messages %} -
  • {{ message }}
  • -{% endfor %} -
-
+ - {{ form.as_table }} - - + +
  - -
- - {% if query %}

{% trans 'Results' %}

- {% for result in page.object_list %} + {% for result in page %}
- {% if result.model_name == 'message' %} - {% include 'nuntium/message/message_in_list.html' with message=result.object %} - {% endif %} - {% if result.model_name == 'answer' %} - {% include 'nuntium/answer/answer_in_list.html' with answer=result.object %} + {% if result.subject %} + {% include 'nuntium/message/message_in_list.html' with message=result %} + {% else %} + {% include 'nuntium/answer/answer_in_list.html' with answer=result %} {% endif %}
{% empty %} diff --git a/nuntium/templates/search/indexes/nuntium/answer_text.txt b/nuntium/templates/search/indexes/nuntium/answer_text.txt deleted file mode 100644 index d35a122d..00000000 --- a/nuntium/templates/search/indexes/nuntium/answer_text.txt +++ /dev/null @@ -1,2 +0,0 @@ -{{ object.content }} -{{ object.person.name }} \ No newline at end of file diff --git a/nuntium/templates/search/indexes/nuntium/message_text.txt b/nuntium/templates/search/indexes/nuntium/message_text.txt deleted file mode 100644 index b5275ccf..00000000 --- a/nuntium/templates/search/indexes/nuntium/message_text.txt +++ /dev/null @@ -1,2 +0,0 @@ -{{ object.subject }} -{{ object.content }} \ No newline at end of file diff --git a/nuntium/tests/answers_search_test.py b/nuntium/tests/answers_search_test.py deleted file mode 100644 index 03858812..00000000 --- a/nuntium/tests/answers_search_test.py +++ /dev/null @@ -1,72 +0,0 @@ -# coding=utf-8 -from global_test_case import GlobalTestCase as TestCase, SearchIndexTestCase -from ..search_indexes import AnswerIndex -from subdomains.utils import reverse -from ..models import Answer, Message -from haystack import indexes -import urllib -import urlparse - - -class AnswerIndexTestCase(TestCase): - def setUp(self): - super(AnswerIndexTestCase, self).setUp() - self.index = AnswerIndex() - for message in Message.objects.all(): - message.confirmated = True - message.save() - - def test_index_parts(self): - self.assertIsInstance(self.index, indexes.SearchIndex) - self.assertIsInstance(self.index, indexes.Indexable) - - self.assertEquals(self.index.get_model(), Answer) - - public_answers = Answer.objects.filter(message__in=Message.public_objects.all()) - first_answer = public_answers[0] - public_answers_list = list(public_answers) - - self.assertQuerysetEqual(self.index.index_queryset(), [repr(r) for r in public_answers_list]) - - self.assertTrue(self.index.text.document) - self.assertTrue(self.index.text.use_template) - - indexed_text = self.index.text.prepare_template(first_answer) - - self.assertTrue(first_answer.content in indexed_text) - self.assertTrue(first_answer.person.name in indexed_text) - self.assertEquals(self.index.writeitinstance.model_attr, 'message__writeitinstance__id') - - self.assertEquals(self.index.writeitinstance.prepare(first_answer), first_answer.message.writeitinstance.id) - - -class SearchAnswerAccess(SearchIndexTestCase): - def setUp(self): - super(SearchAnswerAccess, self).setUp() - - def test_access_the_url(self): - # I don't like this test that much - # because it seems to be likely to break if I add more - # public messages - # if it ever fails - # well then I'll fix it - - # Based on - # http://stackoverflow.com/questions/2506379/add-params-to-given-url-in-python - url = reverse('search_messages', subdomain=None) - url += "/" - params = {'q': 'Public Answer'} - - url_parts = list(urlparse.urlparse(url)) - url_parts[4] = urllib.urlencode(params) - url_with_parameters = urlparse.urlunparse(url_parts) - response = self.client.get(url_with_parameters) - self.assertEquals(response.status_code, 200) - - #the first one the one that says "Public Answer" in example_data.yml - expected_answer = Answer.objects.get(id=1) - self.assertIn('page', response.context) - results = response.context['page'].object_list - - self.assertGreaterEqual(len(results), 1) - self.assertEquals(results[0].object.id, expected_answer.id) diff --git a/nuntium/tests/messages_search_test.py b/nuntium/tests/messages_search_test.py deleted file mode 100644 index a4e08ad2..00000000 --- a/nuntium/tests/messages_search_test.py +++ /dev/null @@ -1,209 +0,0 @@ -# coding=utf-8 -from global_test_case import GlobalTestCase as TestCase, SearchIndexTestCase -from ..search_indexes import MessageIndex -from django.contrib.contenttypes.models import ContentType - -from ..models import Message -from ..forms import MessageSearchForm, PerInstanceSearchForm -from haystack import indexes -from haystack.fields import CharField -from haystack.forms import SearchForm -from subdomains.utils import reverse -from instance.models import WriteItInstance -from ..views import MessageSearchView, PerInstanceSearchView -from haystack.views import SearchView -from popolo.models import Person -import urllib -import urlparse - - -class MessagesSearchTestCase(TestCase): - def setUp(self): - super(MessagesSearchTestCase, self).setUp() - self.first_message = Message.objects.get(id=1) - self.writeitinstance1 = WriteItInstance.objects.get(id=1) - self.person1 = Person.objects.get(id=1) - self.person2 = Person.objects.get(id=2) - self.index = MessageIndex() - for message in Message.objects.all(): - message.confirmated = True - message.save() - - def test_messages_index(self): - public_messages = list(Message.objects.filter(public=True)) - - self.assertIsInstance(self.index, indexes.SearchIndex) - self.assertIsInstance(self.index, indexes.Indexable) - - self.assertEquals(self.index.get_model(), Message) - - self.assertQuerysetEqual(self.index.index_queryset(), [repr(r) for r in public_messages], ordered=False) - - self.assertIsInstance(self.index.text, CharField) - - self.assertTrue(self.index.text.use_template) - - indexed_text = self.index.text.prepare_template(self.first_message) - - self.assertTrue(self.first_message.subject in indexed_text) - self.assertTrue(self.first_message.content in indexed_text) - - self.assertEquals(self.index.writeitinstance.model_attr, 'writeitinstance__id') - - self.assertEquals(self.index.writeitinstance.prepare(self.first_message), self.first_message.writeitinstance.id) - - def test_it_does_not_search_within_private_messages(self): - message = Message.objects.create( - content='Content 1', - author_name='Felipe', - author_email="falvarez@votainteligente.cl", - subject='Fiera es una perra feroz', - public=False, - writeitinstance=self.writeitinstance1, - persons=[self.person1], - ) - - self.assertNotIn(message, self.index.index_queryset()) - - def test_it_does_not_search_within_non_confirmated_messages(self): - non_message = Message.objects.create( - content='Content 1', - author_name='Felipe', - author_email="falvarez@votainteligente.cl", - subject='Fiera es una perra feroz', - writeitinstance=self.writeitinstance1, - persons=[self.person1], - ) - - confirmated_message = Message.objects.create( - content='Content 1', - author_name='Felipe', - author_email="falvarez@votainteligente.cl", - subject='Fiera es una perra feroz2', - confirmated=True, - writeitinstance=self.writeitinstance1, - persons=[self.person1], - ) - - self.assertIn(confirmated_message, self.index.index_queryset()) - self.assertNotIn(non_message, self.index.index_queryset()) - - def test_it_does_not_search_within_non_moderated_messages(self): - self.writeitinstance1.config.moderation_needed_in_all_messages = True - self.writeitinstance1.save() - message = Message.objects.create( - content='Content 1', - author_name='Felipe', - author_email="falvarez@votainteligente.cl", - subject='Fiera es una perra feroz2', - writeitinstance=self.writeitinstance1, - persons=[self.person1], - ) - message.recently_confirmated() - - # message is confirmed (or whatever way you spell it) - # but it was written in an instance in wich all messages need moderation, - # and therefore it needs to be moderated before it is shown in the searches - - self.assertNotIn(message, self.index.index_queryset()) - - -class MessageSearchFormTestCase(TestCase): - def setUp(self): - super(MessageSearchFormTestCase, self).setUp() - - def test_it_is_a_search_form(self): - form = MessageSearchForm() - - self.assertIsInstance(form, SearchForm) - - -class MessageSearchViewTestCase(TestCase): - def setUp(self): - super(MessageSearchViewTestCase, self).setUp() - - def test_access_the_search_url(self): - url = reverse('search_messages') - url += '/' - response = self.client.get(url) - - self.assertEquals(response.status_code, 200) - self.assertIsInstance(response.context['form'], MessageSearchForm) - - def test_search_view(self): - view = MessageSearchView() - - self.assertIsInstance(view, SearchView) - self.assertEquals(view.form_class, MessageSearchForm) - self.assertEquals(view.template, 'nuntium/search.html') - - -class SearchMessageAccess(SearchIndexTestCase): - def setUp(self): - super(SearchMessageAccess, self).setUp() - - def test_access_the_url(self): - # I don't like this test that much - # because it seems to be likely to break if I add more - # public messages - # if it ever fails - # well then I'll fix it - url = reverse('search_messages') - url += "/" - data = {'q': 'Content'} - - url_parts = list(urlparse.urlparse(url)) - url_parts[4] = urllib.urlencode(data) - url_with_parameters = urlparse.urlunparse(url_parts) - - response = self.client.get(url_with_parameters) - self.assertEquals(response.status_code, 200) - - # the first one the one that says "Public Answer" in example_data.yml - expected_answer = Message.objects.get(id=2) - self.assertIn('page', response.context) - results = response.context['page'].object_list - - self.assertGreaterEqual(len(results), 1) - self.assertEquals(results[0].object.id, expected_answer.id) - - -class PerInstanceSearchFormTestCase(SearchIndexTestCase): - def setUp(self): - super(PerInstanceSearchFormTestCase, self).setUp() - self.writeitinstance = WriteItInstance.objects.get(id=1) - - def test_per_instance_search_form(self): - form = PerInstanceSearchForm(writeitinstance=self.writeitinstance) - self.assertIsInstance(form, SearchForm) - - ids_of_messages_returned_by_searchqueryset = [] - content_type = ContentType.objects.get(model='message') - - for result in form.searchqueryset: - if result.content_type() == content_type.app_label + ".message": - ids_of_messages_returned_by_searchqueryset.append(result.object.id) - - public_messages = Message.public_objects.filter(writeitinstance=self.writeitinstance) - - ids_of_public_messages_in_writeitinstance = [r.id for r in public_messages] - - self.assertItemsEqual(ids_of_public_messages_in_writeitinstance, ids_of_messages_returned_by_searchqueryset) - - def test_per_instance_search_view(self): - view = PerInstanceSearchView() - self.assertIsInstance(view, SearchView) - self.assertEquals(view.form_class, PerInstanceSearchForm) - self.assertEquals(view.template, 'nuntium/instance_search.html') - - def test_per_instance_search_url(self): - url = reverse('instance_search', subdomain=self.writeitinstance.slug) - - response = self.client.get(url) - - self.assertEquals(response.status_code, 200) - - self.assertTemplateUsed(response, 'nuntium/instance_search.html') - - self.assertIn('form', response.context) - self.assertIsInstance(response.context['form'], PerInstanceSearchForm) diff --git a/nuntium/urls.py b/nuntium/urls.py index 4c0d4fad..0949df82 100644 --- a/nuntium/urls.py +++ b/nuntium/urls.py @@ -4,7 +4,7 @@ from nuntium.views import ( HelpView, HomeTemplateView, - MessageSearchView, + search_messages, WriteItInstanceListView, VersionView, ) @@ -19,7 +19,7 @@ url(r'^instances/?$', WriteItInstanceListView.as_view(template_name='nuntium/template_list.html'), name='instance_list'), url(r'^contact/$', ContactUsView.as_view(), name='contact_us'), - url(r'^search/?$', MessageSearchView(), name='search_messages'), + url(r'^search/?$', search_messages, name='search_messages'), url(r'^help/(?P\w+)/?$', HelpView.as_view(), name='help_section'), url(r'^help/?$', HelpView.as_view()), diff --git a/nuntium/views.py b/nuntium/views.py index 1265850f..d13e455b 100644 --- a/nuntium/views.py +++ b/nuntium/views.py @@ -11,15 +11,15 @@ from subdomains.utils import reverse from django.http import Http404, HttpResponseRedirect, HttpResponse from formtools.wizard.views import NamedUrlSessionWizardView -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import get_object_or_404, redirect, render -from haystack.views import SearchView from itertools import chain +from django.contrib.postgres.search import SearchVector, SearchQuery +from django.core.paginator import Paginator from django.db.models import Q from instance.models import PopoloPerson, WriteItInstance from popolo.models import Membership from .models import Confirmation, Message, Moderation, Answer -from .forms import MessageSearchForm, PerInstanceSearchForm from nuntium import forms @@ -258,31 +258,46 @@ def get_redirect_url(self, **kwargs): return url -class MessageSearchView(SearchView): - def __init__(self, *args, **kwargs): - super(MessageSearchView, self).__init__(*args, **kwargs) - self.form_class = MessageSearchForm - self.template = 'nuntium/search.html' - - -class PerInstanceSearchView(SearchView): - def __init__(self, *args, **kwargs): - super(PerInstanceSearchView, self).__init__(*args, **kwargs) - self.form_class = PerInstanceSearchForm - self.template = 'nuntium/instance_search.html' - - def __call__(self, *args, **kwargs): - request = args[0] - self.slug = request.subdomain - return super(PerInstanceSearchView, self).__call__(*args, **kwargs) - - def build_form(self, form_kwargs=None): - self.writeitinstance = WriteItInstance.objects.get(slug=self.slug) - if form_kwargs is None: - form_kwargs = {} - form_kwargs['writeitinstance'] = self.writeitinstance - - return super(PerInstanceSearchView, self).build_form(form_kwargs) +def _search_results(query, writeitinstance=None): + if not query: + return [] + search_query = SearchQuery(query) + messages = Message.public_objects.annotate( + search=SearchVector('subject', 'content'), + ).filter(search=search_query) + answers = Answer.objects.filter( + message__in=Message.public_objects.all() + ).annotate( + search=SearchVector('content', 'person__name'), + ).filter(search=search_query) + if writeitinstance is not None: + messages = messages.filter(writeitinstance=writeitinstance) + answers = answers.filter(message__writeitinstance=writeitinstance) + return sorted( + chain(messages, answers), + key=lambda x: x.created, reverse=True + ) + + +def search_messages(request): + query = request.GET.get('q', '') + results = _search_results(query) + paginator = Paginator(results, 20) + page = paginator.get_page(request.GET.get('page', 1)) + return render(request, 'nuntium/search.html', { + 'query': query, 'page': page, + }) + + +def per_instance_search(request): + writeitinstance = get_object_or_404(WriteItInstance, slug=request.subdomain) + query = request.GET.get('q', '') + results = _search_results(query, writeitinstance=writeitinstance) + paginator = Paginator(results, 20) + page = paginator.get_page(request.GET.get('page', 1)) + return render(request, 'nuntium/instance_search.html', { + 'query': query, 'page': page, 'writeitinstance': writeitinstance, + }) class MessagesPerPersonView(ListView): diff --git a/requirements.txt b/requirements.txt index 31647a23..6c4c47f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,6 @@ asn1crypto==0.24.0 backport-collections==0.1 billiard==3.3.0.23 celery==3.1.25 -celery-haystack==0.10 cffi==1.7.0 codecov==2.1.13 contextlib2==0.5.3 @@ -23,7 +22,6 @@ django-dirtyfields==1.0 django-downloadview==1.6 django-extensions==1.6.7 django-formtools==1.0 -django-haystack==2.4.0 django-jsonfield==1.4.1 django-markdown-deux==1.0.5 django-model-utils==3.1.2 @@ -34,7 +32,6 @@ django-pipeline==1.5.4 django-plugins==0.3.0 django-subdomains==2.0.4 django-tastypie==0.13.3 -elasticsearch==1.6.0 email-reply-parser==0.3.0 enum34==1.1.6 flufl.bounce==2.3 diff --git a/setup.py b/setup.py index b1d5a0ef..6a2ede92 100644 --- a/setup.py +++ b/setup.py @@ -11,8 +11,6 @@ 'django-markdown-deux', 'requests', 'django-extensions', -'django-haystack', -'pyelasticsearch', 'celery', 'pyyaml', 'django-celery', diff --git a/writeit/settings.py b/writeit/settings.py index 7ffcc6c7..db671e8e 100644 --- a/writeit/settings.py +++ b/writeit/settings.py @@ -9,7 +9,6 @@ from django.utils.translation import to_locale import environ -from urlparse import urlparse env = environ.Env() @@ -200,7 +199,6 @@ "django.contrib.staticfiles", "social.apps.django_app.default", "annoying", - "celery_haystack", "djcelery", "debug_toolbar", "instance", @@ -217,8 +215,6 @@ "tastypie", "markdown_deux", "django_extensions", - # Searching. - "haystack", "pipeline", # Uncomment the next line to enable the admin: "django_admin_bootstrapped", @@ -235,21 +231,6 @@ if TESTING: INSTALLED_APPS += ("django_nose",) -# SEARCH INDEX WITH ELASTICSEARCH -HAYSTACK_SIGNAL_PROCESSOR = "celery_haystack.signals.CelerySignalProcessor" - -ELASTICSEARCH_URL = env.str("ELASTICSEARCH_URL") -ELASTICSEARCH_INDEX = env.str("ELASTICSEARCH_INDEX") - -HAYSTACK_CONNECTIONS = { - "default": { - "ENGINE": "haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine", - "URL": ELASTICSEARCH_URL, - "PORT": urlparse(os.environ.get("ELASTICSEARCH_URL")).port, - "INDEX_NAME": ELASTICSEARCH_INDEX, - }, -} - # Testing with django TEST_RUNNER = "global_test_case.WriteItTestRunner" @@ -415,12 +396,6 @@ CELERY_TIMEZONE = TIME_ZONE CELERY_ENABLE_UTC = True CELERY_CREATE_MISSING_QUEUES = True -CELERY_HAYSTACK_TRANSACTION_SAFE = True -CELERY_HAYSTACK_DEFAULT_ALIAS = None -CELERY_HAYSTACK_RETRY_DELAY = 5 * 60 -CELERY_HAYSTACK_MAX_RETRIES = 1 -CELERY_HAYSTACK_DEFAULT_TASK = "celery_haystack.tasks.CeleryHaystackSignalHandler" - # These can be set independently, but most often one will be set to True and # the other to False. Setting both to the same boolean value will have # undefined behaviour. @@ -428,9 +403,7 @@ API_BASED = False if TESTING: - LOCAL_ELASTICSEARCH = True CELERY_ALWAYS_EAGER = True - ELASTICSEARCH_INDEX += "-test" USE_X_FORWARDED_HOST = True SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") From b2c2ef73446b1230b09c8f1044f3f62cd616f691 Mon Sep 17 00:00:00 2001 From: michaelglenister Date: Wed, 18 Feb 2026 15:19:54 +0200 Subject: [PATCH 2/4] Fix apt sources --- Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 78499c1a..4bd555e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,9 +2,12 @@ FROM python:2.7 ENV PYTHONUNBUFFERED 1 +RUN echo "deb http://archive.debian.org/debian buster main" > /etc/apt/sources.list \ + && echo "deb http://archive.debian.org/debian-security buster/updates main" >> /etc/apt/sources.list + COPY pkglist /tmp/ RUN apt-get update \ - && apt-get install -y $(cat /tmp/pkglist) \ + && apt-get install -y --no-install-recommends $(cat /tmp/pkglist) \ # cleaning up unused files && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* From 8e662667e20f1340b52e41a866a2af9279b707b5 Mon Sep 17 00:00:00 2001 From: michaelglenister Date: Wed, 18 Feb 2026 15:21:03 +0200 Subject: [PATCH 3/4] Searching messages - reference the correct package --- nuntium/views.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/nuntium/views.py b/nuntium/views.py index d13e455b..93b9f471 100644 --- a/nuntium/views.py +++ b/nuntium/views.py @@ -14,7 +14,6 @@ from django.shortcuts import get_object_or_404, redirect, render from itertools import chain -from django.contrib.postgres.search import SearchVector, SearchQuery from django.core.paginator import Paginator from django.db.models import Q from instance.models import PopoloPerson, WriteItInstance @@ -261,15 +260,14 @@ def get_redirect_url(self, **kwargs): def _search_results(query, writeitinstance=None): if not query: return [] - search_query = SearchQuery(query) - messages = Message.public_objects.annotate( - search=SearchVector('subject', 'content'), - ).filter(search=search_query) + messages = Message.public_objects.filter( + Q(subject__icontains=query) | Q(content__icontains=query) + ) answers = Answer.objects.filter( message__in=Message.public_objects.all() - ).annotate( - search=SearchVector('content', 'person__name'), - ).filter(search=search_query) + ).filter( + Q(content__icontains=query) | Q(person__name__icontains=query) + ) if writeitinstance is not None: messages = messages.filter(writeitinstance=writeitinstance) answers = answers.filter(message__writeitinstance=writeitinstance) From 5d114fc88f69e0743740d7ef4ec43dbe94682094 Mon Sep 17 00:00:00 2001 From: michaelglenister Date: Wed, 18 Feb 2026 15:50:21 +0200 Subject: [PATCH 4/4] Static dir perms fix --- docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 94790d30..3ed63f4d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,7 @@ services: web: build: . + user: root volumes: - .:/app - web-attachments:/app/attachments @@ -27,6 +28,7 @@ services: worker: build: . + user: root volumes: - .:/app - web-attachments:/app/attachments @@ -42,6 +44,7 @@ services: beat: build: . + user: root volumes: - .:/app - beat-data:/var/celerybeat