From b93a8010bcabcf3cb5814ebcd7d460aa39779afa Mon Sep 17 00:00:00 2001 From: echoduck Date: Mar 10 2019 16:44:50 +0000 Subject: [PATCH 1/3] Switched testing DB to PostgreSQL --- diff --git a/Dockerfile b/Dockerfile index ebff58d..c39c717 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM python:2-alpine #Install required system packages -RUN apk add --update --no-cache build-base bash readline libffi-dev ncurses-dev openssl-dev +RUN apk add --update --no-cache build-base bash readline libffi-dev ncurses-dev python2-dev postgresql-dev # Install required Python packages COPY ./requirements /requirements @@ -20,7 +20,6 @@ RUN chmod +x generate_client_secrets.sh RUN ./generate_client_secrets.sh RUN ./manage.py collectstatic --noinput -RUN python manage.py migrate # Expose Django port EXPOSE 8000 diff --git a/docker-compose.yml b/docker-compose.yml index b392ffb..75bcfd0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,10 +2,11 @@ version: "3" services: web: build: . - command: python manage.py runserver 0.0.0.0:8000 + command: bash -c "python manage.py migrate --noinput && python manage.py runserver 0.0.0.0:8000" ports: - "8000:8000" links: + - db - redis redis: image: redis:alpine @@ -15,4 +16,9 @@ services: build: . command: celery worker -A happinesspackets -l info links: + - db - redis + db: + image: postgres:10-alpine + environment: + POSTGRES_PASSWORD: example diff --git a/happinesspackets/settings/dev.py b/happinesspackets/settings/dev.py index 7b37258..7e577cc 100644 --- a/happinesspackets/settings/dev.py +++ b/happinesspackets/settings/dev.py @@ -10,12 +10,18 @@ CRISPY_FAIL_SILENTLY = False DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': PROJECT_DIR.child('db.sqlite3'), + 'ENGINE': 'django.db.backends.postgresql', + 'HOST': 'db', + 'NAME': 'postgres', + 'USER': 'postgres', + 'PASSWORD': 'example', + 'ATOMIC_REQUESTS': True, + 'CONN_MAX_AGE': 300, } } -CELERY_EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" +CELERY_EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" SECRET_KEY = 'only-for-testing' diff --git a/requirements/base.txt b/requirements/base.txt index ccf4320..aacca21 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -35,3 +35,4 @@ opbeat==3.3 mozilla-django-oidc==1.0.0 celery[redis]==4.2.1 django-celery-email==2.0.1 +psycopg2==2.7.5 # PostgreSQL driver diff --git a/requirements/production.txt b/requirements/production.txt index e842a39..a9df940 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -1,4 +1,3 @@ -r base.txt -psycopg2==2.6.1 gunicorn==19.4.1 python-memcached==1.57 From 095a1b30adaafb8855594da787b42d419f63d459 Mon Sep 17 00:00:00 2001 From: echoduck Date: Mar 10 2019 16:47:12 +0000 Subject: [PATCH 2/3] Added search functionality with django-haystack --- diff --git a/Dockerfile b/Dockerfile index c39c717..1cfd14c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,20 @@ FROM python:2-alpine +# Set current working directory +WORKDIR /app + #Install required system packages RUN apk add --update --no-cache build-base bash readline libffi-dev ncurses-dev python2-dev postgresql-dev # Install required Python packages -COPY ./requirements /requirements -RUN pip install -r /requirements/dev.txt +COPY ./requirements /app/requirements +RUN pip install -r requirements/dev.txt # Set correct DJANGO_SETTINGS_MODULE ENV DJANGO_SETTINGS_MODULE=happinesspackets.settings.dev # Copy project files into container -COPY . / - +COPY . /app # Check if client_secrets.json is present, and generate if not RUN apk add --update --no-cache curl diff --git a/docker-compose.yml b/docker-compose.yml index 75bcfd0..fc46c19 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "3" services: web: build: . - command: bash -c "python manage.py migrate --noinput && python manage.py runserver 0.0.0.0:8000" + command: bash -c "python manage.py migrate --noinput && python manage.py rebuild_index --noinput && python manage.py runserver 0.0.0.0:8000" ports: - "8000:8000" links: diff --git a/happinesspackets/messaging/search_indexes.py b/happinesspackets/messaging/search_indexes.py new file mode 100644 index 0000000..7f47a51 --- /dev/null +++ b/happinesspackets/messaging/search_indexes.py @@ -0,0 +1,11 @@ +from haystack import indexes +from .models import Message + +class MessageIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, use_template=True) + + def get_model(self): + return Message + + def index_queryset(self, using=None): + return self.get_model().objects.filter(sender_approved_public=True, recipient_approved_public=True, admin_approved_public=True) diff --git a/happinesspackets/messaging/urls.py b/happinesspackets/messaging/urls.py index e662216..a8ae2a6 100644 --- a/happinesspackets/messaging/urls.py +++ b/happinesspackets/messaging/urls.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import url -from .views import (StartView, MessageSendView, MessageSenderConfirmationSentView, MessageSenderConfirmationView, +from .views import (StartView, MessageSearchView, MessageSendView, MessageSenderConfirmationSentView, MessageSenderConfirmationView, MessageSenderConfirmedView, MessageRecipientMessageUpdate, FaqView, ArchiveView, InspirationView, BlacklistEmailView, ReceivedMessagesView, SentMessagesView) @@ -19,4 +19,5 @@ urlpatterns = [ url(r'^send/confirmation/(?P[\w-]+)/(?P[\w-]+)/$', MessageSenderConfirmationView.as_view(), name='sender_confirm'), url(r'^send/confirmed/$', MessageSenderConfirmedView.as_view(), name='sender_confirmed'), url(r'^recipient/(?P[\w-]+)/(?P[\w-]+)/$', MessageRecipientMessageUpdate.as_view(), name='recipient_message_update'), + url(r'^search/?$', MessageSearchView.as_view(), name='search'), ] diff --git a/happinesspackets/messaging/views.py b/happinesspackets/messaging/views.py index 914d182..4e5c401 100644 --- a/happinesspackets/messaging/views.py +++ b/happinesspackets/messaging/views.py @@ -14,12 +14,19 @@ from django.views.generic import FormView, TemplateView, UpdateView, ListView from django.contrib.auth.mixins import LoginRequiredMixin from django.db.models import Q +from haystack.generic_views import SearchView +from haystack.forms import SearchForm + from .forms import MessageSendForm, MessageRecipientForm from .models import Message, BLACKLIST_HMAC_SALT, BlacklistedEmail, strip_email logger = logging.getLogger(__name__) +class MessageSearchView(SearchView): + template_name = 'search/search.html' + form_class = SearchForm + class ArchiveListView(ListView): model = Message paginate_by = 5 diff --git a/happinesspackets/settings/base.py b/happinesspackets/settings/base.py index e1e6202..34dc815 100644 --- a/happinesspackets/settings/base.py +++ b/happinesspackets/settings/base.py @@ -114,6 +114,7 @@ INSTALLED_APPS = [ 'django_extensions', 'crispy_forms', + 'haystack', 'happinesspackets.messaging', 'djcelery_email', ] @@ -196,6 +197,15 @@ CELERY_BROKER_URL = 'redis://' + REDIS_HOST + ':' + REDIS_PORT + '/0' BROKER_TRANSPORT_OPTIONS = {'visibility_timeout': 3600} CELERY_RESULT_BACKEND = 'redis://' + REDIS_HOST + ':' + REDIS_PORT + '/0' +HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', + 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), + }, +} + +HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' + def get_env_variable(var_name): """ Get the environment variable or return exception """ try: diff --git a/requirements/base.txt b/requirements/base.txt index aacca21..eba87d5 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -36,3 +36,8 @@ mozilla-django-oidc==1.0.0 celery[redis]==4.2.1 django-celery-email==2.0.1 psycopg2==2.7.5 # PostgreSQL driver + +# Search engine +Whoosh==2.7.4 +django-haystack==2.8.1 + diff --git a/templates/base.html b/templates/base.html index e44f537..2edcdf8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -6,6 +6,7 @@ + @@ -41,6 +42,8 @@
  • FAQ
  • {% url 'messaging:archive' as url %}
  • Happiness Archive
  • + {% url 'messaging:search' as url %} +
  • Search the Archive
  • {% if user.is_authenticated %} {% url 'messaging:received_messages' as url %}
  • My Received Packets
  • diff --git a/templates/search/indexes/messaging/message_text.txt b/templates/search/indexes/messaging/message_text.txt new file mode 100644 index 0000000..a01ae2c --- /dev/null +++ b/templates/search/indexes/messaging/message_text.txt @@ -0,0 +1 @@ +{{object.message}} diff --git a/templates/search/search.html b/templates/search/search.html new file mode 100644 index 0000000..7f14816 --- /dev/null +++ b/templates/search/search.html @@ -0,0 +1,49 @@ +{% extends 'base.html' %} + +{% block content %} +

    Search

    +
    + {% for field in form %} +
    + {{ field.errors }} + {{ field.label_tag }} {{ field }} +
    + {% endfor %} + +
    +{% if query %} +

    Results

    + +{% for result in object_list %} +

    + {% if result.object.sender_approved_public_named and result.object.recipient_approved_public_named %} + From {{ result.object.sender_name }} to {{ result.object.recipient_name }} + {% else %} + Anonymous message + {% endif %} +

    + +
    + {{ result.object.message|linebreaksbr }} +
    + +{% empty %} +

    No messages found. Why not change that?

    +

    + Send some happiness now! 💌 +

    +{% endfor %} + +{% if page.has_previous or page.has_next %} +
    + {% if page.has_previous %}{% endif %}« Previous{% if page.has_previous %}{% endif %} + | + {% if page.has_next %}{% endif %}Next »{% if page.has_next %}{% endif %} +
    +{% endif %} +{% else %} +

    + Search for messages here! +

    +{% endif %} +{% endblock %} From 31995f7c6dfc41a267360b3ebc6728b439fbaa67 Mon Sep 17 00:00:00 2001 From: echoduck Date: Mar 10 2019 16:48:01 +0000 Subject: [PATCH 3/3] Added SearchView tests, and disabled debug toolbar during tests to make testing responses more compact. --- diff --git a/happinesspackets/messaging/tests/test_views.py b/happinesspackets/messaging/tests/test_views.py index 2d1c364..aa58124 100644 --- a/happinesspackets/messaging/tests/test_views.py +++ b/happinesspackets/messaging/tests/test_views.py @@ -7,28 +7,77 @@ from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.test import TestCase from django.utils.crypto import salted_hmac +from haystack.management.commands import update_index + from .test_models import MessageModelFactory, BlacklistedEmailFactory from ..models import Message, BLACKLIST_HMAC_SALT, BlacklistedEmail +class SearchViewTest(TestCase): + url = reverse('messaging:search') + + def test_anonymous_message_indexed(self): + MessageModelFactory(sender_approved_public=True, sender_approved_public_named=False, + recipient_approved_public=True, recipient_approved_public_named=True, + admin_approved_public=True) + msg = MessageModelFactory(sender_approved_public=True, sender_approved_public_named=True, + recipient_approved_public=True, recipient_approved_public_named=False, + admin_approved_public=True) + update_index.Command().handle(using=['default']) + response = self.client.get(self.url,{'q':msg.message}) + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, msg.sender_name) + self.assertNotContains(response, msg.recipient_name) + self.assertContains(response, msg.message) + self.assertNotContains(response, msg.sender_email) + self.assertNotContains(response, msg.recipient_email) + + def test_named_message_indexed(self): + msg = MessageModelFactory(sender_approved_public=True, sender_approved_public_named=True, + recipient_approved_public=True, recipient_approved_public_named=True, + admin_approved_public=True) + update_index.Command().handle(using=['default']) + response = self.client.get(self.url,{'q':msg.message}) + self.assertEqual(response.status_code, 200) + self.assertContains(response, msg.sender_name) + self.assertContains(response, msg.recipient_name) + self.assertContains(response, msg.message) + self.assertNotContains(response, msg.sender_email) + self.assertNotContains(response, msg.recipient_email) + + def test_private_message_not_indexed(self): + MessageModelFactory(sender_approved_public=True, sender_approved_public_named=True, + recipient_approved_public=True, recipient_approved_public_named=True, + admin_approved_public=False) + MessageModelFactory(sender_approved_public=False, sender_approved_public_named=True, + recipient_approved_public=True, recipient_approved_public_named=True, + admin_approved_public=True) + msg = MessageModelFactory(sender_approved_public=True, sender_approved_public_named=True, + recipient_approved_public=False, recipient_approved_public_named=True, + admin_approved_public=True) + update_index.Command().handle(using=['default']) + response = self.client.get(self.url,{'q':msg.message}) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context['object_list']),0) + class MessageCounterTest(TestCase): url = reverse('messaging:start') def test_message_sent_included(self): msg = MessageModelFactory(status="sent") response = self.client.get(self.url) - self.assertContains(response,"Packets sent: 1") + self.assertEqual(response.context['packets_sent'],1) def test_message_read_included(self): msg = MessageModelFactory(status="read") response = self.client.get(self.url) - self.assertContains(response,"Packets sent: 1") + self.assertEqual(response.context['packets_sent'],1) def test_message_to_be_confirmed_excluded(self): msg = MessageModelFactory(status="pending_sender_confirmation") response = self.client.get(self.url) - self.assertContains(response,"Packets sent: 0") + self.assertEqual(response.context['packets_sent'],0) class StartViewTest(TestCase): diff --git a/happinesspackets/settings/tsting.py b/happinesspackets/settings/tsting.py index 0ec3596..6995f4d 100644 --- a/happinesspackets/settings/tsting.py +++ b/happinesspackets/settings/tsting.py @@ -14,3 +14,7 @@ AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', ) +#Disables toolbar for tests +DEBUG_TOOLBAR_CONFIG = { + "SHOW_TOOLBAR_CALLBACK" : lambda request: False, +}