Rafał Pitoń 8 лет назад
Родитель
Сommit
477189b1ac

+ 6 - 8
misago/acl/migrations/0003_default_roles.py

@@ -51,10 +51,9 @@ def create_default_roles(apps, schema_editor):
                 'can_edit_polls': 1
             },
 
-            # delete users
-            'misago.users.permissions.delete': {
-                'can_delete_users_newer_than': 0,
-                'can_delete_users_with_less_posts_than': 0,
+            # search
+            'misago.search.permissions': {
+                'can_search': 1,
             },
         })
     role.save()
@@ -87,10 +86,9 @@ def create_default_roles(apps, schema_editor):
                 'can_download_other_users_attachments': True,
             },
 
-            # delete users
-            'misago.users.permissions.delete': {
-                'can_delete_users_newer_than': 0,
-                'can_delete_users_with_less_posts_than': 0,
+            # search
+            'misago.search.permissions': {
+                'can_search': 1,
             },
         })
     role.save()

+ 6 - 0
misago/conf/defaults.py

@@ -64,6 +64,7 @@ INSTALLED_APPS = (
     'misago.categories',
     'misago.threads',
     'misago.readtracker',
+    'misago.search',
     'misago.faker',
 )
 
@@ -124,6 +125,7 @@ MISAGO_ACL_EXTENSIONS = (
     'misago.threads.permissions.polls',
     'misago.threads.permissions.threads',
     'misago.threads.permissions.privatethreads',
+    'misago.search.permissions',
 )
 
 MISAGO_MARKUP_EXTENSIONS = ()
@@ -159,6 +161,10 @@ MISAGO_THREAD_TYPES = (
     'misago.threads.threadtypes.privatethread.PrivateThread',
 )
 
+MISAGO_SEARCH_EXTENSIONS = (
+    'misago.users.search.SearchUsers',
+)
+
 
 # Internationalization
 

+ 1 - 0
misago/search/__init__.py

@@ -0,0 +1 @@
+from .searchprovider import SearchProvider

+ 6 - 0
misago/search/api.py

@@ -0,0 +1,6 @@
+def count_results(request):
+    pass
+
+
+def search_results(request, page=0):
+    pass

+ 39 - 0
misago/search/permissions.py

@@ -0,0 +1,39 @@
+from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ungettext
+
+from misago.acl import algebra
+from misago.acl.models import Role
+from misago.core import forms
+
+
+"""
+Admin Permissions Form
+"""
+class PermissionsForm(forms.Form):
+    legend = _("Search")
+
+    can_search = forms.YesNoSwitch(
+        label=_("Can search site"),
+        initial=1
+    )
+
+
+def change_permissions_form(role):
+    if isinstance(role, Role):
+        return PermissionsForm
+    else:
+        return None
+
+
+"""
+ACL Builder
+"""
+def build_acl(acl, roles, key_name):
+    new_acl = {
+        'can_search': 0
+    }
+    new_acl.update(acl)
+
+    return algebra.sum_acls(new_acl, roles=roles, key=key_name,
+        can_search=algebra.greater
+    )

+ 6 - 0
misago/search/searchprovider.py

@@ -0,0 +1,6 @@
+class SearchProvider(object):
+    def __init__(self, request):
+        self.request = request
+
+    def allow_search(self):
+        pass

+ 57 - 0
misago/search/searchproviders.py

@@ -0,0 +1,57 @@
+from importlib import import_module
+
+from django.core.exceptions import PermissionDenied
+
+from misago.conf import settings
+
+
+class SearchProviders(object):
+    def __init__(self, search_providers):
+        self._initialized = False
+        self._providers = []
+
+        self.providers = search_providers
+
+    def initialize_providers(self):
+        if self._initialized:
+            return
+        self._initialized = True
+
+        for modulename in self.providers:
+            classname = modulename.split('.')[-1]
+            module_path = '.'.join(modulename.split('.')[:-1])
+
+            try:
+                module = import_module(module_path)
+            except ImportError:
+                raise ImportError(
+                    'search module %s could not be imported' % modulename)
+
+            try:
+                classdef = getattr(module, classname)
+                self._providers.append(classdef)
+            except AttributeError:
+                raise ImportError(
+                    'search module %s could not be imported' % modulename)
+
+    def get_providers(self, request):
+        if not self._initialized:
+            self.initialize_providers()
+
+        providers = []
+        for provider in self._providers:
+            providers.append(provider(request))
+        return providers
+
+    def get_allowed_providers(self, request):
+        allowed_providers = []
+        for provider in self.get_providers(request):
+            try:
+                provider.allow_search()
+                allowed_providers.append(provider)
+            except PermissionDenied:
+                pass
+        return allowed_providers
+
+
+searchproviders = SearchProviders(settings.MISAGO_SEARCH_EXTENSIONS)

+ 0 - 0
misago/search/tests/__init__.py


+ 49 - 0
misago/search/tests/test_search_views.py

@@ -0,0 +1,49 @@
+from django.urls import reverse
+
+from misago.acl.testutils import override_acl
+from misago.users.testutils import AuthenticatedUserTestCase
+
+
+class LandingTests(AuthenticatedUserTestCase):
+    """
+    todo:
+
+    - no search providers registered
+    - no search providers allowed
+    - redirect to first search provider
+    """
+    def setUp(self):
+        super(LandingTests, self).setUp()
+
+        self.test_link = reverse('misago:search')
+
+    def test_no_permission(self):
+        """view validates permission to search forum"""
+        override_acl(self.user, {
+            'can_search': 0
+        })
+
+        response = self.client.get(self.test_link)
+        self.assertContains(
+            response, "have permission to search site", status_code=403)
+
+
+class SearchTests(AuthenticatedUserTestCase):
+    """
+    todo:
+
+    - no search providers registered
+    - search provider name not found
+    - search provider disallowed
+    - noscript view displayed
+    """
+    def test_no_permission(self):
+        """view validates permission to search forum"""
+        override_acl(self.user, {
+            'can_search': 0
+        })
+
+        response = self.client.get(
+            reverse('misago:search', kwargs={'search_provider': 'users'}))
+        self.assertContains(
+            response, "have permission to search site", status_code=403)

+ 66 - 0
misago/search/tests/test_searchproviders.py

@@ -0,0 +1,66 @@
+from django.core.exceptions import PermissionDenied
+from django.test import TestCase
+
+from misago.conf import settings
+
+from ..searchprovider import SearchProvider
+from ..searchproviders import SearchProviders
+
+
+class MockProvider(SearchProvider):
+    pass
+
+
+class DisallowedProvider(SearchProvider):
+    def allow_search(self):
+        raise PermissionDenied()
+
+
+class SearchProvidersTests(TestCase):
+    def test_initialize_providers(self):
+        """initialize_providers initializes providers"""
+        searchproviders = SearchProviders(settings.MISAGO_SEARCH_EXTENSIONS)
+        searchproviders.initialize_providers()
+
+        self.assertTrue(searchproviders._initialized)
+
+        self.assertEqual(
+            len(searchproviders._providers), len(settings.MISAGO_SEARCH_EXTENSIONS))
+
+        for i, provider in enumerate(searchproviders._providers):
+            classname = settings.MISAGO_SEARCH_EXTENSIONS[i].split('.')[-1]
+            self.assertEqual(provider.__name__, classname)
+
+    def test_get_providers(self):
+        """get_providers returns initialized providers"""
+        searchproviders = SearchProviders([])
+
+        searchproviders._initialized = True
+        searchproviders._providers = [MockProvider, MockProvider, MockProvider]
+
+        self.assertEqual(
+            [m.__class__ for m in searchproviders.get_providers(True)],
+            searchproviders._providers)
+
+    def test_providers_are_init_with_request(self):
+        """providers constructor is provided with request"""
+        searchproviders = SearchProviders([])
+
+        searchproviders._initialized = True
+        searchproviders._providers = [MockProvider]
+
+        self.assertEqual(
+            searchproviders.get_providers('REQUEST')[0].request, 'REQUEST')
+
+    def test_get_allowed_providers(self):
+        """
+        get_allowed_providers returns only providers that didn't raise in allow_search
+        """
+        searchproviders = SearchProviders([])
+
+        searchproviders._initialized = True
+        searchproviders._providers = [MockProvider, DisallowedProvider, MockProvider]
+
+        self.assertEqual(
+            [m.__class__ for m in searchproviders.get_allowed_providers(True)],
+            [MockProvider, MockProvider])

+ 16 - 0
misago/search/tests/testproviders.py

@@ -0,0 +1,16 @@
+from django.core.exceptions import PermissionDenied
+
+from ..search import SearchProvider
+
+
+class AllowedProvider(SearchProvider):
+    name = "Allowed provider"
+    url = 'allowed-provider'
+
+
+class ForbiddenProvider(SearchProvider):
+    name = "Forbidden provider"
+    url = 'forbidden-provider'
+
+    def allow_search(self):
+        raise PermissionDenied("You can't search this, dave")

+ 10 - 0
misago/search/urls/__init__.py

@@ -0,0 +1,10 @@
+from django.conf.urls import url
+
+from ..views import landing, search
+
+
+urlpatterns = [
+    url(r'^search/$', landing, name='search'),
+    url(r'^search/(?P<search_provider>[-a-zA-Z0-9]+)/$', search, name='search'),
+]
+

+ 0 - 0
misago/search/urls/api.py


+ 40 - 0
misago/search/views.py

@@ -0,0 +1,40 @@
+from django.core.exceptions import PermissionDenied
+from django.http import Http404
+from django.shortcuts import redirect, render
+from django.urls import reverse
+from django.utils import six
+from django.utils.translation import ugettext as _
+
+from .searchproviders import searchproviders
+
+
+def landing(request):
+    allowed_providers = searchproviders.get_allowed_providers(request)
+    if not request.user.acl['can_search'] or not allowed_providers:
+        raise PermissionDenied(_("You don't have permission to search site."))
+
+    default_provider = allowed_providers[0]
+    return redirect('misago:search', search_provider=default_provider.url)
+
+
+def search(request, search_provider):
+    if not request.user.acl['can_search']:
+        raise PermissionDenied(_("You don't have permission to search site."))
+
+    for provider in searchproviders.get_providers(request):
+        if provider.url == search_provider:
+            provider.allow_search()
+            break
+    else:
+        raise Http404()
+
+    request.frontend_context['SEARCH_PROVIDERS'] = []
+    for provider in searchproviders.get_allowed_providers(request):
+        request.frontend_context['SEARCH_PROVIDERS'].append({
+            'name': six.text_type(provider.name),
+            'url': reverse('misago:search', kwargs={'search_provider': provider.url}),
+            'api': provider.url,
+            'results': None
+        })
+
+    return render(request, 'misago/search.html')

+ 3 - 1
misago/templates/misago/navbar.html

@@ -40,11 +40,13 @@
           {% trans "Users" %}
         </a>
       </li>
+      {% if user.acl.can_search %}
       <li>
-        <a href="#">
+        <a href="{% url 'misago:search' %}">
           {% trans "Search" %}
         </a>
       </li>
+      {% endif %}
     </ul>
 
     <div id="user-menu-mount"></div>

+ 25 - 0
misago/templates/misago/search.html

@@ -0,0 +1,25 @@
+{% extends "misago/base.html" %}
+{% load i18n %}
+
+
+{% block title %}{% trans "Search site" %} | {{ block.super }}{% endblock %}
+
+
+{% block content %}
+<div class="page page-error page-error-noscript">
+  <div class="container">
+    <div class="message-panel">
+
+      <div class="message-icon">
+        <span class="material-icon">code</span>
+      </div>
+
+      <div class="message-body">
+        <p class="lead">{% trans "To search site enable JavaScript" %}</p>
+        <p>{% trans "Site search is unavailable without JavaScript enabled." %}</p>
+      </div>
+
+    </div>
+  </div>
+</div>
+{% endblock content %}

+ 1 - 0
misago/urls.py

@@ -10,6 +10,7 @@ urlpatterns = [
     url(r'^', include('misago.users.urls')),
     url(r'^', include('misago.categories.urls')),
     url(r'^', include('misago.threads.urls')),
+    url(r'^', include('misago.search.urls')),
 
     # "misago:index" link symbolises "root" of Misago links space
     # any request with path that falls below this one is assumed to be directed

+ 8 - 0
misago/users/search.py

@@ -0,0 +1,8 @@
+from django.utils.translation import ugettext_lazy as _
+
+from misago.search import SearchProvider
+
+
+class SearchUsers(SearchProvider):
+    name = _("Search users")
+    url = 'users'

+ 18 - 16
misago/users/urls/__init__.py

@@ -1,5 +1,7 @@
 from django.conf.urls import include, url
+
 from misago.core.views import home_redirect
+
 from ..views import activation, auth, avatarserver, forgottenpassword, lists, options, profile
 
 
@@ -9,8 +11,8 @@ urlpatterns = [
     url(r'^login/$', auth.login, name='login'),
     url(r'^logout/$', auth.logout, name='logout'),
 
-    url(r'^request-activation/$', activation.request_activation, name="request-activation"),
-    url(r'^activation/(?P<pk>\d+)/(?P<token>[a-zA-Z0-9]+)/$', activation.activate_by_token, name="activate-by-token"),
+    url(r'^request-activation/$', activation.request_activation, name='request-activation'),
+    url(r'^activation/(?P<pk>\d+)/(?P<token>[a-zA-Z0-9]+)/$', activation.activate_by_token, name='activate-by-token'),
 
     url(r'^forgotten-password/$', forgottenpassword.request_reset, name='forgotten-password'),
     url(r'^forgotten-password/(?P<pk>\d+)/(?P<token>[a-zA-Z0-9]+)/$', forgottenpassword.reset_password_form, name='forgotten-password-change-form'),
@@ -32,31 +34,31 @@ urlpatterns += [
 
 urlpatterns += [
     url(r'^users/', include([
-        url(r'^$', lists.landing, name="users"),
-        url(r'^active-posters/$', lists.active_posters, name="users-active-posters"),
-        url(r'^(?P<slug>[-a-zA-Z0-9]+)/$', lists.rank, name="users-rank"),
-        url(r'^(?P<slug>[-a-zA-Z0-9]+)/(?P<page>\d+)/$', lists.rank, name="users-rank"),
+        url(r'^$', lists.landing, name='users'),
+        url(r'^active-posters/$', lists.active_posters, name='users-active-posters'),
+        url(r'^(?P<slug>[-a-zA-Z0-9]+)/$', lists.rank, name='users-rank'),
+        url(r'^(?P<slug>[-a-zA-Z0-9]+)/(?P<page>\d+)/$', lists.rank, name='users-rank'),
     ]))
 ]
 
 
 urlpatterns += [
     url(r'^user/(?P<slug>[a-zA-Z0-9]+)-(?P<pk>\d+)/', include([
-        url(r'^$', profile.landing, name="user"),
-        url(r'^posts/$', profile.posts, name="user-posts"),
-        url(r'^threads/$', profile.threads, name="user-threads"),
-        url(r'^followers/$', profile.followers, name="user-followers"),
-        url(r'^follows/$', profile.follows, name="user-follows"),
-        url(r'^username-history/$', profile.username_history, name="username-history"),
-        url(r'^ban-details/$', profile.user_ban, name="user-ban"),
+        url(r'^$', profile.landing, name='user'),
+        url(r'^posts/$', profile.posts, name='user-posts'),
+        url(r'^threads/$', profile.threads, name='user-threads'),
+        url(r'^followers/$', profile.followers, name='user-followers'),
+        url(r'^follows/$', profile.follows, name='user-follows'),
+        url(r'^username-history/$', profile.username_history, name='username-history'),
+        url(r'^ban-details/$', profile.user_ban, name='user-ban'),
     ]))
 ]
 
 
 urlpatterns += [
     url(r'^user-avatar/', include([
-        url(r'^(?P<hash>[a-f0-9]+)/(?P<size>\d+)/(?P<pk>\d+)\.png$', avatarserver.serve_user_avatar, name="user-avatar"),
-        url(r'^(?P<secret>[a-f0-9]+):(?P<hash>[a-f0-9]+)/(?P<pk>\d+)\.png$', avatarserver.serve_user_avatar_source, name="user-avatar-source"),
-        url(r'^(?P<size>\d+)\.png$', avatarserver.serve_blank_avatar, name="blank-avatar"),
+        url(r'^(?P<hash>[a-f0-9]+)/(?P<size>\d+)/(?P<pk>\d+)\.png$', avatarserver.serve_user_avatar, name='user-avatar'),
+        url(r'^(?P<secret>[a-f0-9]+):(?P<hash>[a-f0-9]+)/(?P<pk>\d+)\.png$', avatarserver.serve_user_avatar_source, name='user-avatar-source'),
+        url(r'^(?P<size>\d+)\.png$', avatarserver.serve_blank_avatar, name='blank-avatar'),
     ]))
 ]