Merge pull request 'search' (#145) from search into dev
Reviewed-on: https://gitea.planet-casio.com/devs/PCv5/pulls/145
This commit is contained in:
commit
db065f83f3
|
@ -1,13 +1,59 @@
|
|||
from flask_login import current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField
|
||||
from wtforms.fields.datetime import DateField
|
||||
from wtforms.validators import InputRequired, Optional
|
||||
from wtforms.fields import StringField, SubmitField, SelectField, SelectMultipleField, DateField
|
||||
from app.models.forum import Forum
|
||||
|
||||
|
||||
# TODO: compléter le formulaire de recherche avancée
|
||||
class SearchForm(FlaskForm):
|
||||
class Meta:
|
||||
csrf = False
|
||||
q = StringField('Rechercher', validators=[InputRequired()])
|
||||
|
||||
|
||||
class AdvancedSearchForm(SearchForm):
|
||||
date = DateField('Date', validators=[Optional()])
|
||||
def generate_choices():
|
||||
choices = {'Forum': [
|
||||
'Actualités',
|
||||
'Aide et questions',
|
||||
'Forum des projets',
|
||||
'Vie communautaire',
|
||||
], 'Programmes': [
|
||||
'Jeux',
|
||||
'Utilitaires',
|
||||
'Logiciels'
|
||||
], 'Utilisateurs': [
|
||||
'Tous'
|
||||
], 'Tutoriels': [
|
||||
'Basic',
|
||||
'C/C++',
|
||||
'Arduino',
|
||||
'Python'
|
||||
], 'Sprites': [
|
||||
'Personnages',
|
||||
'Environnements',
|
||||
'Objets',
|
||||
'Interfaces'
|
||||
]}
|
||||
# Forum reserved for admins and moderators
|
||||
f = Forum.query.filter_by(url='/admin').first()
|
||||
if (current_user.is_authenticated and current_user.can_access_forum(f)):
|
||||
choices['Forum'].append('Administration')
|
||||
# Forum reserved to members of CreativeCalc
|
||||
f = Forum.query.filter_by(url='/creativecalc').first()
|
||||
if (current_user.is_authenticated and current_user.can_access_forum(f)):
|
||||
choices['Forum'].append('CreativeCalc')
|
||||
return choices
|
||||
|
||||
sortBy = SelectField('Trier',
|
||||
choices={'Pertinence': ['Pertinence'],
|
||||
'Date': ['Date croissante',
|
||||
'Date décroissante'],
|
||||
'Ordre Alphabétique': [
|
||||
'Alphabétique croissant',
|
||||
'Alphabétique décroissant',]},
|
||||
validators=[Optional()])
|
||||
date = DateField('Date de publication', validators=[Optional()])
|
||||
scope = SelectMultipleField('', choices=generate_choices, validators=[Optional()])
|
||||
submit = SubmitField('Affiner la recherche')
|
||||
|
|
|
@ -6,7 +6,7 @@ from app.routes.admin import index, groups, account, forums, \
|
|||
attachments, config, members, polls, login_as
|
||||
from app.routes.forum import index, topic
|
||||
from app.routes.polls import vote, delete
|
||||
from app.routes.posts import edit
|
||||
from app.routes.posts import edit, redirect
|
||||
from app.routes.programs import index, submit, program
|
||||
from app.routes.api import markdown
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
from app import app
|
||||
from app.models.comment import Comment
|
||||
from app.models.thread import Thread
|
||||
from flask import redirect, url_for
|
||||
|
||||
|
||||
@app.route('/post/<int:postid>', methods=['GET', 'POST'])
|
||||
def redirect_post(postid):
|
||||
c = Comment.query.get_or_404(postid)
|
||||
|
||||
owner = c.thread.owner_post
|
||||
|
||||
if owner.type == 'topic':
|
||||
# Is a topic
|
||||
comments = Comment.query.where(
|
||||
Comment.thread_id == c.thread.id,
|
||||
Comment.date_created <= c.date_created
|
||||
).order_by(
|
||||
Comment.date_created.asc()
|
||||
).paginate(per_page=Thread.COMMENTS_PER_PAGE, error_out=False)
|
||||
url = url_for('forum_topic', f=owner.forum, page=(owner, comments.pages), _anchor=str(c.id))
|
||||
else:
|
||||
# Is a program
|
||||
url = url_for('program_view', page=owner.name)
|
||||
|
||||
return redirect(url, 301)
|
|
@ -1,9 +1,65 @@
|
|||
from app import app
|
||||
from app.forms.search import AdvancedSearchForm
|
||||
from app import app, db
|
||||
from app.forms.search import AdvancedSearchForm, SearchForm
|
||||
from app.models.post import Post
|
||||
from app.models.comment import Comment
|
||||
from app.models.topic import Topic
|
||||
from app.models.forum import Forum
|
||||
from app.models.program import Program
|
||||
from app.utils.render import render
|
||||
from sqlalchemy import text, func
|
||||
from flask import request
|
||||
from flask_sqlalchemy import Pagination
|
||||
|
||||
|
||||
@app.route('/rechercher')
|
||||
def search():
|
||||
form = AdvancedSearchForm()
|
||||
return render('search.html', form=form)
|
||||
SEARCH_RESULTS_PER_PAGE = 20
|
||||
|
||||
def paginate(data, page, per_page):
|
||||
# Based on page and per_page info, calculate start and end index of items to keep
|
||||
start_index = (page - 1) * per_page
|
||||
end_index = start_index + per_page
|
||||
|
||||
# Get the paginated list of items
|
||||
items = data[start_index:end_index]
|
||||
|
||||
# Create Pagination object
|
||||
return Pagination(None, page, per_page, len(data), items)
|
||||
|
||||
def websearch_to_tsquery_multilang(search):
|
||||
return func.websearch_to_tsquery('french', search).op('||')(func.websearch_to_tsquery('english', search))
|
||||
|
||||
def to_tsvector_multilang(text):
|
||||
return func.to_tsvector('french', text).op('||')(func.to_tsvector('english', text))
|
||||
|
||||
|
||||
@app.route('/rechercher/')
|
||||
@app.route('/rechercher/<int:page>/')
|
||||
def search(page=1):
|
||||
form = AdvancedSearchForm(request.args)
|
||||
results = list()
|
||||
if form.validate():
|
||||
tsquery = websearch_to_tsquery_multilang(form.q.data)
|
||||
# Topics are sorted first in results
|
||||
topic_query = db.session.query(Topic).where(
|
||||
to_tsvector_multilang(Topic.title).bool_op('@@')(tsquery)
|
||||
).group_by(
|
||||
Topic.id,
|
||||
Post.id
|
||||
)
|
||||
# Programms follow directly in the results
|
||||
program_query = db.session.query(Program).where(
|
||||
to_tsvector_multilang(Program.name).bool_op('@@')(tsquery)
|
||||
).group_by(
|
||||
Program.id,
|
||||
Post.id
|
||||
)
|
||||
# Comments are less important than topics and programs
|
||||
comment_query = db.session.query(Comment).where(
|
||||
to_tsvector_multilang(Comment.text).bool_op('@@')(tsquery)
|
||||
).group_by(
|
||||
Comment.id,
|
||||
Post.id
|
||||
)
|
||||
results = list(topic_query) + list(program_query) + list(comment_query)
|
||||
|
||||
results = paginate(results, page, SEARCH_RESULTS_PER_PAGE)
|
||||
return render('search.html', form=form, results=results)
|
||||
|
|
|
@ -158,7 +158,7 @@ input[type="submit"]:focus {
|
|||
left: 50%;
|
||||
padding: 8px;
|
||||
position: absolute;
|
||||
transform: translateY(-100%);
|
||||
transform: translateY(-200%);
|
||||
transition: transform 0.3s;
|
||||
background: var(--links);
|
||||
color: var(--warn-text);
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
.search-page > form {
|
||||
display: grid;
|
||||
grid-template-areas: 'search search submit''date sort scope''results results scope';
|
||||
grid-template-rows: 40% 40% 20%;
|
||||
grid-template-rows: 5em 5em 100%;
|
||||
}
|
||||
.search-page > form input,
|
||||
.search-page > form select {
|
||||
width: 100%;
|
||||
height: 2rem;
|
||||
}
|
||||
.search-page > form label {
|
||||
margin-right: 1em;
|
||||
}
|
||||
.search-page > form div.query {
|
||||
grid-area: search;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.search-page > form div.query label {
|
||||
margin-right: 1em;
|
||||
}
|
||||
.search-page > form div.submit {
|
||||
grid-area: submit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 1em;
|
||||
}
|
||||
.search-page > form div.submit input#submit {
|
||||
width: fit-content;
|
||||
}
|
||||
.search-page > form div.date {
|
||||
grid-area: date;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.search-page > form div.date input#date {
|
||||
width: 80%;
|
||||
}
|
||||
.search-page > form div.sort {
|
||||
grid-area: sort;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 2em;
|
||||
}
|
||||
.search-page > form div.scope {
|
||||
grid-area: scope;
|
||||
width: 80%;
|
||||
margin-left: 1em;
|
||||
}
|
||||
.search-page > form div.scope select {
|
||||
width: 100%;
|
||||
height: 31rem;
|
||||
overflow: auto;
|
||||
}
|
||||
.search-page > form div.search-results {
|
||||
grid-area: results;
|
||||
width: 100%;
|
||||
min-height: 50vh;
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
.search-page > form {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
'search search submit'
|
||||
'date sort scope'
|
||||
'results results scope';
|
||||
grid-template-rows: 40% 40% 20%;
|
||||
grid-template-rows: 5em 5em 100%;
|
||||
|
||||
input, select {
|
||||
width: 100%;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
& div.query {
|
||||
grid-area: search;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& label {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
& div.submit {
|
||||
grid-area: submit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 1em;
|
||||
|
||||
& input#submit {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
& div.date {
|
||||
grid-area: date;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& input#date {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
& div.sort {
|
||||
grid-area: sort;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
& div.scope {
|
||||
grid-area: scope;
|
||||
width: 80%;
|
||||
margin-left: 1em;
|
||||
|
||||
& select {
|
||||
width: 100%;
|
||||
height: 31rem;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
& div.search-results {
|
||||
grid-area: results;
|
||||
width: 100%;
|
||||
min-height: 50vh;
|
||||
}
|
||||
}
|
|
@ -1,21 +1,69 @@
|
|||
{% extends "base/base.html" %}
|
||||
{% import "widgets/pagination.html" as widget_pagination with context %}
|
||||
|
||||
{% set tabtitle = "Recherche avancée" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form">
|
||||
<section class="search-page">
|
||||
<h1>Recherche avancée</h1>
|
||||
|
||||
<form action="" method="get">
|
||||
<div>
|
||||
<form action="{{ url_for('search') }}" method="get">
|
||||
{{ form.csrf_token }}
|
||||
<div class="query">
|
||||
{{ form.q.label }}
|
||||
{{ form.q(value=request.args.get('q')) }}
|
||||
{% for error in form.q.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
<div class="submit">
|
||||
{{ form.submit(class_="bg-ok") }}
|
||||
{% for error in form.submit.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="date">
|
||||
{{ form.date.label }}
|
||||
{{ form.date }}
|
||||
{{ form.date(value=request.args.get('date')) }}
|
||||
{% for error in form.date.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="sort">
|
||||
{{ form.sortBy.label }}
|
||||
{{ form.sortBy(value=request.args.get('sortBy')) }}
|
||||
{% for error in form.sortBy.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="scope">
|
||||
{{ form.scope.label }}
|
||||
{{ form.scope(value=request.args.get('scope')) }}
|
||||
{% for error in form.scope.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="search-results">
|
||||
{{ widget_pagination.paginate(results, 'search', None, {
|
||||
'q': request.args.get('q'),
|
||||
'date': request.args.get('date'),
|
||||
'sortBy': request.args.get('sortBy')}) }}
|
||||
{% for i in results.items %}
|
||||
<div>
|
||||
{{ i.id }} {{ i.title }}<br>
|
||||
{% if i.forum %}
|
||||
<a href="{{ url_for('forum_topic', f=i.forum, page=(i , 'fin')) }}">{{ i.title }}</a>
|
||||
{% elif i.thread %}
|
||||
<a href="{{ url_for('redirect_post', postid=i.id) }}">{{ i.thread.owner_topic[0].title }}</a>
|
||||
{% endif %}
|
||||
{{ i.headline }}<br>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{{ widget_pagination.paginate(results, 'search', None, {
|
||||
'q': request.args.get('q'),
|
||||
'date': request.args.get('date'),
|
||||
'sortBy': request.args.get('sortBy')}) }}
|
||||
</div>
|
||||
<div>{{ form.submit(class_="bg-ok") }}</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -19,6 +19,7 @@ def render(*args, styles=[], scripts=[], modules=[], **kwargs):
|
|||
'css/debugger.css',
|
||||
'css/programs.css',
|
||||
'css/editor.css',
|
||||
'css/search.css',
|
||||
]
|
||||
scripts_ = [
|
||||
'scripts/trigger_menu.js',
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
"""Search functions
|
||||
|
||||
Revision ID: a803745f7840
|
||||
Revises: 5ffc4e562ed8
|
||||
Create Date: 2023-06-27 23:10:06.088917
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a803745f7840'
|
||||
down_revision = '5ffc4e562ed8'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute("""CREATE FUNCTION websearch_to_tsquery_multilang(text) RETURNS tsquery AS $$
|
||||
SELECT websearch_to_tsquery('french', $1) ||
|
||||
websearch_to_tsquery('english', $1) ||
|
||||
websearch_to_tsquery('simple', $1)
|
||||
$$ LANGUAGE sql IMMUTABLE;""")
|
||||
op.execute("""CREATE FUNCTION to_tsvector_multilang(text) RETURNS tsvector AS $$
|
||||
SELECT to_tsvector('french', $1) ||
|
||||
to_tsvector('english', $1) ||
|
||||
to_tsvector('simple', $1)
|
||||
$$ LANGUAGE sql IMMUTABLE;""")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.execute("DROP FUNCTION websearch_to_tsquery_multilang(text);")
|
||||
op.execute("DROP FUNCTION to_tsvector_multilang(text);")
|
|
@ -0,0 +1,24 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: dc99ddd17cf3
|
||||
Revises: a803745f7840, a8f539a93bd5
|
||||
Create Date: 2023-08-08 19:46:55.441606
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'dc99ddd17cf3'
|
||||
down_revision = ('a803745f7840', 'a8f539a93bd5')
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
pass
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
Loading…
Reference in New Issue