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):
return False
if group_id == settings.SITH_GROUP_ROOT_ID and self.is_superuser:
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
def is_root(self):
......
......@@ -2,6 +2,7 @@
#
# Copyright 2016,2017
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
......@@ -27,6 +28,7 @@ from django.db import models
from haystack import indexes, signals
from core.models import User
from forum.models import ForumMessage, ForumMessageMeta
class UserIndex(indexes.SearchIndex, indexes.Indexable):
......@@ -44,13 +46,68 @@ class UserIndex(indexes.SearchIndex, indexes.Indexable):
return "last_update"
class UserOnlySignalProcessor(signals.BaseSignalProcessor):
class IndexSignalProcessor(signals.BaseSignalProcessor):
def setup(self):
# Listen only to the ``User`` model.
models.signals.post_save.connect(self.handle_save, 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):
# Disconnect only for the ``User`` model.
models.signals.post_save.disconnect(self.handle_save, 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 {
}
}
.search_bar {
margin: 10px 0px;
display: flex;
height: 20p;
align-items: center;
}
.search_check {
margin-left: 10px;
}
.search_bouton {
margin-left: 10px;
}
.category {
margin-top: 5px;
background: $secondary-color;
......
{{ object.topic }}
{{ object.title }}
{{ object.message }}
{{ object.author }}
{{ object.topic }}
{{ object.title }}
{{ object.message }}
{{ object.author }}
......@@ -2,6 +2,7 @@
#
# Copyright 2016,2017
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
......@@ -176,6 +177,7 @@ class CanViewMixin(View):
"""
def dispatch(self, request, *arg, **kwargs):
try:
self.object = self.get_object()
if can_view(self.object, request.user):
......@@ -184,8 +186,10 @@ class CanViewMixin(View):
except:
pass
# If we get here, it's a ListView
l_id = [o.id for o in self.get_queryset() if can_view(o, request.user)]
if not l_id and self.get_queryset().count() != 0:
queryset = self.get_queryset()
l_id = [o.id for o in queryset if can_view(o, request.user)]
if not l_id and queryset.count() != 0:
raise PermissionDenied
self._get_queryset = self.get_queryset
......
......@@ -71,6 +71,8 @@ def notification(request, notif_id):
def search_user(query, as_json=False):
if query == "":
return []
res = SearchQuerySet().models(User).autocomplete(auto=query)[:20]
return [r.object for r in res]
......
......@@ -331,9 +331,9 @@ class ForumMessage(models.Model):
return user.can_edit(self.topic.forum)
def can_be_viewed_by(self, user):
return (
not self._deleted
) # No need to check the real rights since it's already done by the Topic view
# No need to check the real rights since it's already done by the Topic view
# and it impacts performances too much
return not self._deleted
def can_be_moderated_by(self, user):
return self.topic.forum.is_owned_by(user) or user.id == self.author.id
......
{% 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 %}
{{ forum }}
{% endblock %}
{% block content %}
<div>
<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>
{{ display_breadcrumb(forum) }}
<div id="forum">
<h3>{{ forum.name }}</h3>
<p>
......@@ -22,6 +16,7 @@
{% if not forum.is_category %}
<a class="ib button" href="{{ url('forum:new_topic', forum_id=forum.id) }}">{% trans %}New topic{% endtrans %}</a>
{% endif %}
{{ display_search_bar(request) }}
</p>
{% if forum.children.exists() %}
<div>
......
......@@ -85,9 +85,20 @@
</div>
{% 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) %}
{% 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 %}">
{% if m.author.avatar_pict %}
<img src="{{ m.author.avatar_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}" id="picture" />
......@@ -143,7 +154,7 @@
</div>
</div>
{% else %}
<div id="msg_{{ m.id }}" class="message">
<div id="msg_{{ m.id }}" class="message">
<div class="msg_author deleted">
</div>
<div class="msg_content deleted">
......@@ -155,3 +166,12 @@
{{ m.mark_as_read(user) or "" }}
{% 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" %}
{% 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 %}
{% trans %}Forum{% endtrans %}
......@@ -15,6 +15,7 @@
<p>
<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>
{{ display_search_bar(request) }}
</p>
{% if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID) or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
<p>
......@@ -34,6 +35,7 @@
</div>
</div>
</div>
{% for f in forum_list %}
<div>
{{ display_forum(f, user, True) }}
......
{% extends "core/base.jinja" %}
{% from 'forum/macros.jinja' import display_message %}
{% from 'forum/macros.jinja' import display_message, display_search_bar %}
{% block title %}
{% if topic %}
......@@ -11,6 +11,7 @@
{% block content %}
{% if topic %}
{{ display_search_bar(request) }}
<p>
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
{% 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" %}
{% 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 %}
{{ topic }}
......@@ -26,15 +26,8 @@
{% endblock %}
{% block content %}
<p>
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
{% 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>
{{ display_breadcrumb(topic.forum, topic) }}
<h3>{{ topic.title }}</h3>
<div id="forum">
<p>{{ topic.description }}</p>
<p>
......@@ -46,6 +39,7 @@
{% endif %}
</p>
{{ display_search_bar(request) }}
<p style="text-align: right; background: #d8e7f3;">
{% 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>
......
......@@ -26,8 +26,10 @@ from django.conf.urls import url
from forum.views import *
urlpatterns = [
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"^mark_all_as_read$", ForumMarkAllAsRead.as_view(), name="mark_all_as_read"),
url(r"^last_unread$", ForumLastUnread.as_view(), name="last_unread"),
......
......@@ -2,6 +2,7 @@
#
# Copyright 2016,2017,2018
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
......@@ -36,9 +37,56 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
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 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):
......
This diff is collapsed.
......@@ -190,7 +190,7 @@ HAYSTACK_CONNECTIONS = {
}
}
HAYSTACK_SIGNAL_PROCESSOR = "core.search_indexes.UserOnlySignalProcessor"
HAYSTACK_SIGNAL_PROCESSOR = "core.search_indexes.IndexSignalProcessor"
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