Commit acfbdd1a authored by Skia's avatar Skia

Merge branch 'feature/forum-search' into 'master'

Forum search

See merge request !181
parents a96aeba1 835782fd
Pipeline #1654 passed with stage
in 10 minutes and 32 seconds
...@@ -366,7 +366,11 @@ class User(AbstractBaseUser): ...@@ -366,7 +366,11 @@ class User(AbstractBaseUser):
return False return False
if group_id == settings.SITH_GROUP_ROOT_ID and self.is_superuser: if group_id == settings.SITH_GROUP_ROOT_ID and self.is_superuser:
return True return True
return self.groups.filter(name=group_name).exists() return group_name in self.cached_groups_names
@cached_property
def cached_groups_names(self):
return [g.name for g in self.groups.all()]
@cached_property @cached_property
def is_root(self): def is_root(self):
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
# #
# Copyright 2016,2017 # Copyright 2016,2017
# - Skia <skia@libskia.so> # - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
# #
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, # Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr. # http://ae.utbm.fr.
...@@ -27,6 +28,7 @@ from django.db import models ...@@ -27,6 +28,7 @@ from django.db import models
from haystack import indexes, signals from haystack import indexes, signals
from core.models import User from core.models import User
from forum.models import ForumMessage, ForumMessageMeta
class UserIndex(indexes.SearchIndex, indexes.Indexable): class UserIndex(indexes.SearchIndex, indexes.Indexable):
...@@ -44,13 +46,68 @@ class UserIndex(indexes.SearchIndex, indexes.Indexable): ...@@ -44,13 +46,68 @@ class UserIndex(indexes.SearchIndex, indexes.Indexable):
return "last_update" return "last_update"
class UserOnlySignalProcessor(signals.BaseSignalProcessor): class IndexSignalProcessor(signals.BaseSignalProcessor):
def setup(self): def setup(self):
# Listen only to the ``User`` model. # Listen only to the ``User`` model.
models.signals.post_save.connect(self.handle_save, sender=User) models.signals.post_save.connect(self.handle_save, sender=User)
models.signals.post_delete.connect(self.handle_delete, sender=User) models.signals.post_delete.connect(self.handle_delete, sender=User)
# Listen only to the ``ForumMessage`` model.
models.signals.post_save.connect(self.handle_save, sender=ForumMessageMeta)
models.signals.post_delete.connect(self.handle_delete, sender=ForumMessage)
# Listen to the ``ForumMessageMeta`` model pretending it's a ``ForumMessage``.
models.signals.post_save.connect(
self.handle_forum_message_meta_save, sender=ForumMessageMeta
)
models.signals.post_delete.connect(
self.handle_forum_message_meta_delete, sender=ForumMessageMeta
)
def teardown(self): def teardown(self):
# Disconnect only for the ``User`` model. # Disconnect only for the ``User`` model.
models.signals.post_save.disconnect(self.handle_save, sender=User) models.signals.post_save.disconnect(self.handle_save, sender=User)
models.signals.post_delete.disconnect(self.handle_delete, sender=User) models.signals.post_delete.disconnect(self.handle_delete, sender=User)
# Disconnect only to the ``ForumMessage`` model.
models.signals.post_save.disconnect(self.handle_save, sender=ForumMessage)
models.signals.post_delete.disconnect(self.handle_delete, sender=ForumMessage)
# Disconnect to the ``ForumMessageMeta`` model pretending it's a ``ForumMessage``.
models.signals.post_save.disconnect(
self.handle_forum_message_meta_save, sender=ForumMessageMeta
)
models.signals.post_delete.disconnect(
self.handle_forum_message_meta_delete, sender=ForumMessageMeta
)
def handle_forum_message_meta_save(self, sender, instance, **kwargs):
super(IndexSignalProcessor, self).handle_save(
ForumMessage, instance.message, **kwargs
)
def handle_forum_message_meta_delete(self, sender, instance, **kwargs):
super(IndexSignalProcessor, self).handle_delete(
ForumMessage, instance.message, **kwargs
)
class BigCharFieldIndex(indexes.CharField):
"""
Workaround to avoid xapian.InvalidArgument: Term too long (> 245)
See https://groups.google.com/forum/#!topic/django-haystack/hRJKcPNPXqw/discussion
"""
def prepare(self, term):
return bytes(super(BigCharFieldIndex, self).prepare(term), "utf-8")[
:245
].decode("utf-8", errors="ignore")
class ForumMessageIndex(indexes.SearchIndex, indexes.Indexable):
text = BigCharFieldIndex(document=True, use_template=True)
auto = indexes.EdgeNgramField(use_template=True)
date = indexes.DateTimeField(model_attr="date")
def get_model(self):
return ForumMessage
...@@ -1403,6 +1403,18 @@ textarea { ...@@ -1403,6 +1403,18 @@ textarea {
} }
} }
.search_bar {
margin: 10px 0px;
display: flex;
height: 20p;
align-items: center;
}
.search_check {
margin-left: 10px;
}
.search_bouton {
margin-left: 10px;
}
.category { .category {
margin-top: 5px; margin-top: 5px;
background: $secondary-color; background: $secondary-color;
......
{{ object.topic }}
{{ object.title }}
{{ object.message }}
{{ object.author }}
{{ object.topic }}
{{ object.title }}
{{ object.message }}
{{ object.author }}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
# #
# Copyright 2016,2017 # Copyright 2016,2017
# - Skia <skia@libskia.so> # - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
# #
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, # Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr. # http://ae.utbm.fr.
...@@ -176,6 +177,7 @@ class CanViewMixin(View): ...@@ -176,6 +177,7 @@ class CanViewMixin(View):
""" """
def dispatch(self, request, *arg, **kwargs): def dispatch(self, request, *arg, **kwargs):
try: try:
self.object = self.get_object() self.object = self.get_object()
if can_view(self.object, request.user): if can_view(self.object, request.user):
...@@ -184,8 +186,10 @@ class CanViewMixin(View): ...@@ -184,8 +186,10 @@ class CanViewMixin(View):
except: except:
pass pass
# If we get here, it's a ListView # If we get here, it's a ListView
l_id = [o.id for o in self.get_queryset() if can_view(o, request.user)] queryset = self.get_queryset()
if not l_id and self.get_queryset().count() != 0:
l_id = [o.id for o in queryset if can_view(o, request.user)]
if not l_id and queryset.count() != 0:
raise PermissionDenied raise PermissionDenied
self._get_queryset = self.get_queryset self._get_queryset = self.get_queryset
......
...@@ -71,6 +71,8 @@ def notification(request, notif_id): ...@@ -71,6 +71,8 @@ def notification(request, notif_id):
def search_user(query, as_json=False): def search_user(query, as_json=False):
if query == "":
return []
res = SearchQuerySet().models(User).autocomplete(auto=query)[:20] res = SearchQuerySet().models(User).autocomplete(auto=query)[:20]
return [r.object for r in res] return [r.object for r in res]
......
...@@ -331,9 +331,9 @@ class ForumMessage(models.Model): ...@@ -331,9 +331,9 @@ class ForumMessage(models.Model):
return user.can_edit(self.topic.forum) return user.can_edit(self.topic.forum)
def can_be_viewed_by(self, user): def can_be_viewed_by(self, user):
return ( # No need to check the real rights since it's already done by the Topic view
not self._deleted # and it impacts performances too much
) # No need to check the real rights since it's already done by the Topic view return not self._deleted
def can_be_moderated_by(self, user): def can_be_moderated_by(self, user):
return self.topic.forum.is_owned_by(user) or user.id == self.author.id return self.topic.forum.is_owned_by(user) or user.id == self.author.id
......
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from 'forum/macros.jinja' import display_forum, display_topic %} {% from 'forum/macros.jinja' import display_forum, display_breadcrumb, display_topic, display_search_bar %}
{% block title %} {% block title %}
{{ forum }} {{ forum }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div> {{ display_breadcrumb(forum) }}
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
{% for f in forum.get_parent_list()|reverse %}
> <a href="{{ f.get_absolute_url() }}">{{ f }}</a>
{% endfor %}
> <a href="{{ forum.get_absolute_url() }}">{{ forum }}</a>
</div>
<div id="forum"> <div id="forum">
<h3>{{ forum.name }}</h3> <h3>{{ forum.name }}</h3>
<p> <p>
...@@ -22,6 +16,7 @@ ...@@ -22,6 +16,7 @@
{% if not forum.is_category %} {% if not forum.is_category %}
<a class="ib button" href="{{ url('forum:new_topic', forum_id=forum.id) }}">{% trans %}New topic{% endtrans %}</a> <a class="ib button" href="{{ url('forum:new_topic', forum_id=forum.id) }}">{% trans %}New topic{% endtrans %}</a>
{% endif %} {% endif %}
{{ display_search_bar(request) }}
</p> </p>
{% if forum.children.exists() %} {% if forum.children.exists() %}
<div> <div>
......
...@@ -85,9 +85,20 @@ ...@@ -85,9 +85,20 @@
</div> </div>
{% endmacro %} {% endmacro %}
{% macro display_breadcrumb(forum, topic="") %}
<p>
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
{% for f in forum.get_parent_list()|reverse %}
> <a href="{{ f.get_absolute_url() }}">{{ f }}</a>
{% endfor %}
> <a href="{{ forum.get_absolute_url() }}">{{ forum }}</a>
{% if topic != "" %} > <a href="{{ topic.get_absolute_url() }}">{{ topic }}</a>{%- endif -%}
</p>
{% endmacro %}
{% macro display_message(m, user, unread=False) %} {% macro display_message(m, user, unread=False) %}
{% if user.can_view(m) %} {% if user.can_view(m) %}
<div id="msg_{{ m.id }}" class="message {% if unread %}unread{% endif %}"> <div id="msg_{{ m.id }}" class="message {% if unread %}unread{% endif %}">
<div class="msg_author {% if m.deleted %}deleted{% endif %}"> <div class="msg_author {% if m.deleted %}deleted{% endif %}">
{% if m.author.avatar_pict %} {% if m.author.avatar_pict %}
<img src="{{ m.author.avatar_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}" id="picture" /> <img src="{{ m.author.avatar_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}" id="picture" />
...@@ -143,7 +154,7 @@ ...@@ -143,7 +154,7 @@
</div> </div>
</div> </div>
{% else %} {% else %}
<div id="msg_{{ m.id }}" class="message"> <div id="msg_{{ m.id }}" class="message">
<div class="msg_author deleted"> <div class="msg_author deleted">
</div> </div>
<div class="msg_content deleted"> <div class="msg_content deleted">
...@@ -155,3 +166,12 @@ ...@@ -155,3 +166,12 @@
{{ m.mark_as_read(user) or "" }} {{ m.mark_as_read(user) or "" }}
{% endmacro %} {% endmacro %}
{% macro display_search_bar(request) %}
<form class="search_bar" action="{{ url('forum:search') }}" method="GET">
<input type="text" placeholder="{% trans %}Search{% endtrans %}" name="query" value="{{ request.GET.query|default('') }}"/>
<input type="checkbox" class="search_check" name="order" value="date" {% if request.GET.order|default("") == "date" or (request.GET.order|default("") == "" and request.GET.query|default("") == "") -%}
checked
{%- endif -%}> {% trans %}Order by date{% endtrans %}<br>
<input type="submit" class="search_bouton" value="{% trans %}Search{% endtrans %}"/>
</form>
{% endmacro %}
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link %} {% from 'core/macros.jinja' import user_profile_link %}
{% from 'forum/macros.jinja' import display_forum %} {% from 'forum/macros.jinja' import display_forum, display_search_bar %}
{% block title %} {% block title %}
{% trans %}Forum{% endtrans %} {% trans %}Forum{% endtrans %}
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
<p> <p>
<a class="ib button" href="{{ url('forum:last_unread') }}">{% trans %}View last unread messages{% endtrans %}</a> <a class="ib button" href="{{ url('forum:last_unread') }}">{% trans %}View last unread messages{% endtrans %}</a>
<a class="ib button" href="{{ url('forum:favorite_topics') }}">{% trans %}Favorite topics{% endtrans %}</a> <a class="ib button" href="{{ url('forum:favorite_topics') }}">{% trans %}Favorite topics{% endtrans %}</a>
{{ display_search_bar(request) }}
</p> </p>
{% if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID) or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %} {% if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID) or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
<p> <p>
...@@ -34,6 +35,7 @@ ...@@ -34,6 +35,7 @@
</div> </div>
</div> </div>
</div> </div>
{% for f in forum_list %} {% for f in forum_list %}
<div> <div>
{{ display_forum(f, user, True) }} {{ display_forum(f, user, True) }}
......
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from 'forum/macros.jinja' import display_message %} {% from 'forum/macros.jinja' import display_message, display_search_bar %}
{% block title %} {% block title %}
{% if topic %} {% if topic %}
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
{% block content %} {% block content %}
{% if topic %} {% if topic %}
{{ display_search_bar(request) }}
<p> <p>
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a> <a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
{% for f in topic.forum.get_parent_list() %} {% for f in topic.forum.get_parent_list() %}
......
{% extends "core/base.jinja" %}
{% from 'forum/macros.jinja' import display_message, display_breadcrumb, display_search_bar %}
{% block content %}
<div id="forum">
{{ display_search_bar(request) }}
{% if object_list|length != 0 %}
<br>
<div class="search-results">
{% for m in object_list %}
{{ display_breadcrumb(m.topic.forum, m.topic) }}
{{ display_message(m, user) }}
{% endfor %}
</div>
{% else %}
{% trans %}No result found{% endtrans %}
{% endif %}
</div>
{% endblock %}
\ No newline at end of file
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link %} {% from 'core/macros.jinja' import user_profile_link %}
{% from 'forum/macros.jinja' import display_message %} {% from 'forum/macros.jinja' import display_message, display_breadcrumb, display_search_bar %}
{% block title %} {% block title %}
{{ topic }} {{ topic }}
...@@ -26,15 +26,8 @@ ...@@ -26,15 +26,8 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<p> {{ display_breadcrumb(topic.forum, topic) }}
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a> <h3>{{ topic.title }}</h3>
{% for f in topic.forum.get_parent_list()|reverse %}
> <a href="{{ f.get_absolute_url() }}">{{ f }}</a>
{% endfor %}
> <a href="{{ topic.forum.get_absolute_url() }}">{{ topic.forum }}</a>
> <a href="{{ topic.get_absolute_url() }}">{{ topic }}</a>
</p>
<h3>{{ topic.title }}</h3>
<div id="forum"> <div id="forum">
<p>{{ topic.description }}</p> <p>{{ topic.description }}</p>
<p> <p>
...@@ -46,6 +39,7 @@ ...@@ -46,6 +39,7 @@
{% endif %} {% endif %}
</p> </p>
{{ display_search_bar(request) }}
<p style="text-align: right; background: #d8e7f3;"> <p style="text-align: right; background: #d8e7f3;">
{% for p in msgs.paginator.page_range %} {% for p in msgs.paginator.page_range %}
<span class="ib" style="background: {% if p == msgs.number %}white{% endif %}; margin: 0;"><a href="?page={{ p }}">{{ p }}</a></span> <span class="ib" style="background: {% if p == msgs.number %}white{% endif %}; margin: 0;"><a href="?page={{ p }}">{{ p }}</a></span>
......
...@@ -26,8 +26,10 @@ from django.conf.urls import url ...@@ -26,8 +26,10 @@ from django.conf.urls import url
from forum.views import * from forum.views import *
urlpatterns = [ urlpatterns = [
url(r"^$", ForumMainView.as_view(), name="main"), url(r"^$", ForumMainView.as_view(), name="main"),
url(r"^search/$", ForumSearchView.as_view(), name="search"),
url(r"^new_forum$", ForumCreateView.as_view(), name="new_forum"), url(r"^new_forum$", ForumCreateView.as_view(), name="new_forum"),
url(r"^mark_all_as_read$", ForumMarkAllAsRead.as_view(), name="mark_all_as_read"), url(r"^mark_all_as_read$", ForumMarkAllAsRead.as_view(), name="mark_all_as_read"),
url(r"^last_unread$", ForumLastUnread.as_view(), name="last_unread"), url(r"^last_unread$", ForumLastUnread.as_view(), name="last_unread"),
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
# #
# Copyright 2016,2017,2018 # Copyright 2016,2017,2018
# - Skia <skia@libskia.so> # - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
# #
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, # Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr. # http://ae.utbm.fr.
...@@ -36,9 +37,56 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger ...@@ -36,9 +37,56 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from ajax_select import make_ajax_field from ajax_select import make_ajax_field
from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin from core.views import (
CanViewMixin,
CanEditMixin,
CanEditPropMixin,
CanCreateMixin,
can_view,
)
from core.views.forms import MarkdownInput from core.views.forms import MarkdownInput
from forum.models import Forum, ForumMessage, ForumTopic, ForumMessageMeta from forum.models import Forum, ForumMessage, ForumTopic, ForumMessageMeta
from haystack.query import RelatedSearchQuerySet
class ForumSearchView(ListView):
template_name = "forum/search.jinja"
def get_queryset(self):
query = self.request.GET.get("query", "")
order_by = self.request.GET.get("order", "")
if query == "":
return []
queryset = RelatedSearchQuerySet().models(ForumMessage).autocomplete(auto=query)
if order_by == "date":
queryset = queryset.order_by("-date")
queryset = queryset.load_all()
queryset = queryset.load_all_queryset(
ForumMessage,
ForumMessage.objects.all()
.prefetch_related("topic__forum__edit_groups")
.prefetch_related("topic__forum__view_groups")
.prefetch_related("topic__forum__owner_club"),
)
# Filter unauthorized responses
resp = []
count = 0
max_count = 30
for r in queryset:
if count >= max_count:
return resp
if can_view(r.object, self.request.user) and can_view(
r.object.topic, self.request.user
):
resp.append(r.object)
count += 1
return resp
class ForumMainView(ListView): class ForumMainView(ListView):
......
This diff is collapsed.
...@@ -190,7 +190,7 @@ HAYSTACK_CONNECTIONS = { ...@@ -190,7 +190,7 @@ HAYSTACK_CONNECTIONS = {
} }
} }
HAYSTACK_SIGNAL_PROCESSOR = "core.search_indexes.UserOnlySignalProcessor" HAYSTACK_SIGNAL_PROCESSOR = "core.search_indexes.IndexSignalProcessor"
SASS_PRECISION = 8 SASS_PRECISION = 8
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment