Browse Source

Add hooks for posts/registration validation and search filters (#1311)

* Add tests for custom validators hooks

* Add tests for search filter hook
Rafał Pitoń 5 years ago
parent
commit
cb03eeda84

+ 8 - 0
misago/core/tests/test_context_processors_hook.py

@@ -0,0 +1,8 @@
+from ..context_processors import hooks
+
+
+def test_context_processors_hook_can_inject_context(mocker):
+    plugin = mocker.Mock(return_value={"plugin": True})
+    mocker.patch("misago.core.context_processors.context_processors_hooks", [plugin])
+    context = hooks(None)
+    assert context == {"plugin": True}

+ 4 - 0
misago/hooks.py

@@ -1,3 +1,7 @@
 apipatterns = []
 urlpatterns = []
 context_processors = []
+
+new_registrations_validators = []
+post_search_filters = []
+post_validators = []

+ 5 - 0
misago/threads/filtersearch.py

@@ -1,5 +1,6 @@
 from django.utils.module_loading import import_string
 
+from .. import hooks
 from ..conf import settings
 
 filters_list = settings.MISAGO_POST_SEARCH_FILTERS
@@ -8,6 +9,10 @@ SEARCH_FILTERS = list(map(import_string, filters_list))
 
 def filter_search(search, filters=None):
     filters = filters or SEARCH_FILTERS
+
     for search_filter in filters:
         search = search_filter(search) or search
+    for search_filter in hooks.post_search_filters:
+        search = search_filter(search) or search
+
     return search

+ 44 - 19
misago/threads/tests/test_search.py

@@ -5,6 +5,17 @@ from ...categories.models import Category
 from ...users.test import AuthenticatedUserTestCase
 
 
+def index_post(post):
+    if post.id == post.thread.first_post_id:
+        post.set_search_document(post.thread.title)
+    else:
+        post.set_search_document()
+    post.save(update_fields=["search_document"])
+
+    post.update_search_vector()
+    post.save(update_fields=["search_vector"])
+
+
 class SearchApiTests(AuthenticatedUserTestCase):
     def setUp(self):
         super().setUp()
@@ -13,16 +24,6 @@ class SearchApiTests(AuthenticatedUserTestCase):
 
         self.api_link = reverse("misago:api:search")
 
-    def index_post(self, post):
-        if post.id == post.thread.first_post_id:
-            post.set_search_document(post.thread.title)
-        else:
-            post.set_search_document()
-        post.save(update_fields=["search_document"])
-
-        post.update_search_vector()
-        post.save(update_fields=["search_vector"])
-
     def test_no_query(self):
         """api handles no search query"""
         response = self.client.get(self.api_link)
@@ -51,7 +52,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
         """api handles short search query"""
         thread = test.post_thread(self.category)
         post = test.reply_thread(thread, message="Lorem ipsum dolor.")
-        self.index_post(post)
+        index_post(post)
 
         response = self.client.get("%s?q=ip" % self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -67,7 +68,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
         """api handles query miss"""
         thread = test.post_thread(self.category)
         post = test.reply_thread(thread, message="Lorem ipsum dolor.")
-        self.index_post(post)
+        index_post(post)
 
         response = self.client.get("%s?q=elit" % self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -83,7 +84,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
         """hidden posts are extempt from search"""
         thread = test.post_thread(self.category)
         post = test.reply_thread(thread, message="Lorem ipsum dolor.", is_hidden=True)
-        self.index_post(post)
+        index_post(post)
 
         response = self.client.get("%s?q=ipsum" % self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -101,7 +102,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
         post = test.reply_thread(
             thread, message="Lorem ipsum dolor.", is_unapproved=True
         )
-        self.index_post(post)
+        index_post(post)
 
         response = self.client.get("%s?q=ipsum" % self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -117,7 +118,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
         """api handles search query"""
         thread = test.post_thread(self.category)
         post = test.reply_thread(thread, message="Lorem ipsum dolor.")
-        self.index_post(post)
+        index_post(post)
 
         response = self.client.get("%s?q=ipsum" % self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -134,10 +135,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
     def test_thread_title_search(self):
         """api searches threads by title"""
         thread = test.post_thread(self.category, title="Atmosphere of mars")
-        self.index_post(thread.first_post)
+        index_post(thread.first_post)
 
         post = test.reply_thread(thread, message="Lorem ipsum dolor.")
-        self.index_post(post)
+        index_post(post)
 
         response = self.client.get("%s?q=mars atmosphere" % self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -155,7 +156,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
         """api handles complex query that uses fulltext search facilities"""
         thread = test.post_thread(self.category)
         post = test.reply_thread(thread, message="Atmosphere of Mars")
-        self.index_post(post)
+        index_post(post)
 
         response = self.client.get("%s?q=Mars atmosphere" % self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -176,7 +177,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
             thread, message="You just do MMM in 4th minute and its pwnt"
         )
 
-        self.index_post(post)
+        index_post(post)
 
         response = self.client.get("%s?q=MMM" % self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -207,3 +208,27 @@ class SearchProviderApiTests(SearchApiTests):
         self.api_link = reverse(
             "misago:api:search", kwargs={"search_provider": "threads"}
         )
+
+
+def test_post_search_filters_hook_is_used_by_threads_search(
+    db, user_client, mocker, thread
+):
+    def search_filter(search):
+        return search.replace("apple phone", "iphone")
+
+    mocker.patch(
+        "misago.threads.filtersearch.hooks.post_search_filters", [search_filter]
+    )
+
+    post = test.reply_thread(thread, message="Lorem ipsum iphone dolor met.")
+    index_post(post)
+
+    response = user_client.get("/api/search/?q=apple phone")
+
+    reponse_json = response.json()
+    assert "threads" in [p["id"] for p in reponse_json]
+    for provider in reponse_json:
+        if provider["id"] == "threads":
+            results = provider["results"]["results"]
+            assert len(results) == 1
+            assert results[0]["id"] == post.id

+ 15 - 0
misago/threads/tests/test_validate_post.py

@@ -1,3 +1,4 @@
+from django.core.exceptions import ValidationError
 from django.urls import reverse
 
 from ...categories.models import Category
@@ -86,3 +87,17 @@ class ValidatePostTests(AuthenticatedUserTestCase):
                 "post": ["This field may not be blank."],
             },
         )
+
+
+def test_post_validators_hook_is_used_by_posting_api(db, user_client, mocker, thread):
+    def validator(context, data):
+        raise ValidationError("ERROR FROM PLUGIN")
+
+    mocker.patch("misago.threads.validators.hooks.post_validators", [validator])
+
+    response = user_client.post(
+        f"/api/threads/{thread.id}/posts/", {"post": "Lorem ipsum dolor met"}
+    )
+
+    assert response.status_code == 400
+    assert response.json() == {"non_field_errors": ["ERROR FROM PLUGIN"]}

+ 3 - 0
misago/threads/validators.py

@@ -3,6 +3,7 @@ from django.utils.module_loading import import_string
 from django.utils.translation import gettext as _
 from django.utils.translation import ngettext
 
+from .. import hooks
 from ..categories import THREADS_ROOT_NAME
 from ..categories.models import Category
 from ..categories.permissions import can_browse_category, can_see_category
@@ -108,5 +109,7 @@ def validate_post(context, data, validators=None):
 
     for validator in validators:
         data = validator(context, data) or data
+    for validator in hooks.post_validators:
+        data = validator(context, data) or data
 
     return data

+ 20 - 0
misago/users/tests/test_user_create_api.py

@@ -553,3 +553,23 @@ class UserCreateTests(UserTestCase):
         self.assertTrue(test_user.check_password(password))
 
         self.assertIn("Welcome", mail.outbox[0].subject)
+
+
+@override_dynamic_settings(account_activation="none")
+def test_new_registrations_validators_hook_is_used_by_registration_api(
+    db, client, mocker
+):
+    def validator(request, cleaned_data, add_error):
+        add_error("username", "ERROR FROM PLUGIN")
+
+    mocker.patch(
+        "misago.users.validators.hooks.new_registrations_validators", [validator]
+    )
+
+    response = client.post(
+        "/api/users/",
+        {"username": "User", "email": "user@example.com", "password": "PASSW0RD123"},
+    )
+
+    assert response.status_code == 400
+    assert response.json() == {"username": ["ERROR FROM PLUGIN"]}

+ 4 - 0
misago/users/validators.py

@@ -12,6 +12,7 @@ from django.utils.translation import gettext_lazy as _
 from django.utils.translation import ngettext
 from requests.exceptions import RequestException
 
+from .. import hooks
 from ..conf import settings
 from .bans import get_email_ban, get_username_ban
 
@@ -157,6 +158,9 @@ def raise_validation_error(*_):
 
 def validate_new_registration(request, cleaned_data, add_error=None, validators=None):
     validators = validators or REGISTRATION_VALIDATORS
+
     add_error = add_error or raise_validation_error
     for validator in validators:
         validator(request, cleaned_data, add_error)
+    for validator in hooks.new_registrations_validators:
+        validator(request, cleaned_data, add_error)