From b93a8010bcabcf3cb5814ebcd7d460aa39779afa Mon Sep 17 00:00:00 2001
From: echoduck <bekaluteshome@gmail.com>
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 <bekaluteshome@gmail.com>
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<identifier>[\w-]+)/(?P<token>[\w-]+)/$', MessageSenderConfirmationView.as_view(), name='sender_confirm'),
     url(r'^send/confirmed/$', MessageSenderConfirmedView.as_view(), name='sender_confirmed'),
     url(r'^recipient/(?P<identifier>[\w-]+)/(?P<token>[\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 @@
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
 
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link href="https://fonts.googleapis.com/css?family=Montserrat|Open+Sans" rel="stylesheet">
     <link href="{% static 'bootstrap/css/bootstrap.min.css' %}" rel="stylesheet">
     <link href="{% static 'css/all.css' %}" rel="stylesheet">
     <link href="{% static 'css/datepicker.css' %}" rel="stylesheet">
@@ -41,6 +42,8 @@
                 <li role="presentation" {% ifequal request.path url %}class="active"{% endifequal %}><a href="{{ url }}">FAQ</a></li>
                 {% url 'messaging:archive' as url %}
                 <li role="presentation" {% ifequal request.path url %}class="active"{% endifequal %}><a href="{{ url }}">Happiness Archive</a></li>
+                {% url 'messaging:search' as url %}
+                <li role="presentation" {% if url in request.path %}class="active"{% endif %}><a href="{{ url }}">Search the Archive</a></li>
             {% if user.is_authenticated %}
                 {% url 'messaging:received_messages' as url %}
                 <li role="presentation" {% ifequal request.path url %}class="active"{% endifequal %}><a href="{{ url }}">My Received Packets</a></li>
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 %}
+<h2>Search</h2>
+<form method="get"  class="form-inline">
+    {% for field in form %}
+    <div class="form-group">
+        {{ field.errors }}
+        {{ field.label_tag }} {{ field }}
+    </div>
+    {% endfor %}
+    <input type="submit" value="Search">
+</form>
+{% if query %}
+<h3>Results</h3>
+
+{% for result in object_list %}
+<h4>
+    {% 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 %}
+</h4>
+
+<blockquote>
+    {{ result.object.message|linebreaksbr  }}
+</blockquote>
+
+{% empty %}
+<p>No messages found. Why not change that?</p>
+<p class="text-center">
+    <a class="btn btn-primary btn-lg btn-responsive" href="{% url 'messaging:send' %}" role="button">Send some happiness now! <img class="emoji" src="static/images/emoji/loveletter.png" alt="💌" title="Love letter" aria-label="Emoji: Love letter"></a>
+</p>
+{% endfor %}
+
+{% if page.has_previous or page.has_next %}
+<div>
+    {% if page.has_previous %}<a href="?q={{ query }}&amp;page={{ page.previous_page_number }}">{% endif %}&laquo; Previous{% if page.has_previous %}</a>{% endif %}
+    |
+    {% if page.has_next %}<a href="?q={{ query }}&amp;page={{ page.next_page_number }}">{% endif %}Next &raquo;{% if page.has_next %}</a>{% endif %}
+</div>
+{% endif %}
+{% else %}
+<p>
+    Search for messages here!
+</p>
+{% endif %}
+{% endblock %}

From 31995f7c6dfc41a267360b3ebc6728b439fbaa67 Mon Sep 17 00:00:00 2001
From: echoduck <bekaluteshome@gmail.com>
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: <b>1</b>")
+        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: <b>1</b>")
+        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: <b>0</b>")
+        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,
+}

