mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
search posts
This commit is contained in:
parent
bb9eb31e9d
commit
3085dfc29a
14 changed files with 227 additions and 4 deletions
|
@ -87,6 +87,9 @@ def create_app(config_class=Config):
|
||||||
from app.chat import bp as chat_bp
|
from app.chat import bp as chat_bp
|
||||||
app.register_blueprint(chat_bp)
|
app.register_blueprint(chat_bp)
|
||||||
|
|
||||||
|
from app.search import bp as search_bp
|
||||||
|
app.register_blueprint(search_bp)
|
||||||
|
|
||||||
def get_resource_as_string(name, charset='utf-8'):
|
def get_resource_as_string(name, charset='utf-8'):
|
||||||
with app.open_resource(name) as f:
|
with app.open_resource(name) as f:
|
||||||
return f.read().decode(charset)
|
return f.read().decode(charset)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import flask
|
||||||
from flask import json, current_app
|
from flask import json, current_app
|
||||||
from flask_babel import _
|
from flask_babel import _
|
||||||
from sqlalchemy import or_, desc
|
from sqlalchemy import or_, desc
|
||||||
|
from sqlalchemy.orm import configure_mappers
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
import click
|
import click
|
||||||
|
|
|
@ -222,6 +222,7 @@ class Topic(db.Model):
|
||||||
machine_name = db.Column(db.String(50), index=True)
|
machine_name = db.Column(db.String(50), index=True)
|
||||||
name = db.Column(db.String(50))
|
name = db.Column(db.String(50))
|
||||||
num_communities = db.Column(db.Integer, default=0)
|
num_communities = db.Column(db.Integer, default=0)
|
||||||
|
parent_id = db.Column(db.Integer)
|
||||||
communities = db.relationship('Community', lazy='dynamic', backref='topic', cascade="all, delete-orphan")
|
communities = db.relationship('Community', lazy='dynamic', backref='topic', cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
|
||||||
|
@ -483,7 +484,7 @@ class User(UserMixin, db.Model):
|
||||||
ap_inbox_url = db.Column(db.String(255))
|
ap_inbox_url = db.Column(db.String(255))
|
||||||
ap_domain = db.Column(db.String(255))
|
ap_domain = db.Column(db.String(255))
|
||||||
|
|
||||||
search_vector = db.Column(TSVectorType('user_name', 'bio', 'keywords'))
|
search_vector = db.Column(TSVectorType('user_name', 'about', 'keywords'))
|
||||||
activity = db.relationship('ActivityLog', backref='account', lazy='dynamic', cascade="all, delete-orphan")
|
activity = db.relationship('ActivityLog', backref='account', lazy='dynamic', cascade="all, delete-orphan")
|
||||||
posts = db.relationship('Post', lazy='dynamic', cascade="all, delete-orphan")
|
posts = db.relationship('Post', lazy='dynamic', cascade="all, delete-orphan")
|
||||||
post_replies = db.relationship('PostReply', lazy='dynamic', cascade="all, delete-orphan")
|
post_replies = db.relationship('PostReply', lazy='dynamic', cascade="all, delete-orphan")
|
||||||
|
|
5
app/search/__init__.py
Normal file
5
app/search/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('search', __name__)
|
||||||
|
|
||||||
|
from app.search import routes
|
0
app/search/forms.py
Normal file
0
app/search/forms.py
Normal file
46
app/search/routes.py
Normal file
46
app/search/routes.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
from flask import request, flash, json, url_for, current_app, redirect, g
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from flask_babel import _
|
||||||
|
from sqlalchemy import or_
|
||||||
|
|
||||||
|
from app.models import Post
|
||||||
|
from app.search import bp
|
||||||
|
from app.utils import moderating_communities, joined_communities, render_template, blocked_domains
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/search', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def run_search():
|
||||||
|
if request.args.get('q') is not None:
|
||||||
|
q = request.args.get('q')
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
low_bandwidth = request.cookies.get('low_bandwidth', '0') == '1'
|
||||||
|
|
||||||
|
posts = Post.query.search(q)
|
||||||
|
if current_user.ignore_bots:
|
||||||
|
posts = posts.filter(Post.from_bot == False)
|
||||||
|
if current_user.show_nsfl is False:
|
||||||
|
posts = posts.filter(Post.nsfl == False)
|
||||||
|
if current_user.show_nsfw is False:
|
||||||
|
posts = posts.filter(Post.nsfw == False)
|
||||||
|
|
||||||
|
domains_ids = blocked_domains(current_user.id)
|
||||||
|
if domains_ids:
|
||||||
|
posts = posts.filter(or_(Post.domain_id.not_in(domains_ids), Post.domain_id == None))
|
||||||
|
posts = posts.paginate(page=page, per_page=100 if current_user.is_authenticated and not low_bandwidth else 50,
|
||||||
|
error_out=False)
|
||||||
|
|
||||||
|
next_url = url_for('search.run_search', page=posts.next_num, q=q) if posts.has_next else None
|
||||||
|
prev_url = url_for('search.run_search', page=posts.prev_num, q=q) if posts.has_prev and page != 1 else None
|
||||||
|
|
||||||
|
return render_template('search/results.html', title=_('Search results for %(q)s', q=q), posts=posts, q=q,
|
||||||
|
next_url=next_url, prev_url=prev_url,
|
||||||
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
|
site=g.site)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return render_template('search/start.html', title=_('Search'),
|
||||||
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
|
site=g.site)
|
|
@ -1273,4 +1273,15 @@ fieldset legend {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.redo_search {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.redo_search input[type=search] {
|
||||||
|
width: unset;
|
||||||
|
display: inline;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: initial;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/*# sourceMappingURL=structure.css.map */
|
/*# sourceMappingURL=structure.css.map */
|
||||||
|
|
|
@ -951,3 +951,14 @@ fieldset {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.redo_search {
|
||||||
|
display: inline;
|
||||||
|
input[type=search] {
|
||||||
|
width: unset;
|
||||||
|
display: inline;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: initial;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -477,7 +477,7 @@ div.navbar {
|
||||||
}
|
}
|
||||||
|
|
||||||
#outer_container {
|
#outer_container {
|
||||||
margin-top: -4px;
|
margin-top: -1px;
|
||||||
}
|
}
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
#outer_container {
|
#outer_container {
|
||||||
|
|
|
@ -78,7 +78,7 @@ div.navbar {
|
||||||
}
|
}
|
||||||
|
|
||||||
#outer_container {
|
#outer_container {
|
||||||
margin-top: -4px;
|
margin-top: -1px;
|
||||||
@include breakpoint(tablet) {
|
@include breakpoint(tablet) {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
padding-top: 0.25rem;
|
padding-top: 0.25rem;
|
||||||
|
|
79
app/templates/search/results.html
Normal file
79
app/templates/search/results.html
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||||
|
{% extends 'themes/' + theme() + '/base.html' %}
|
||||||
|
{% else %}
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% endif %} %}
|
||||||
|
{% from 'bootstrap/form.html' import render_form %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-8 position-relative main_pane">
|
||||||
|
<h1>{{ _('Search results for') }} <form method="get" class="redo_search"><input type="search" name="q" value="{{ q }}" class="form-control"></form></h1>
|
||||||
|
<div class="post_list">
|
||||||
|
{% for post in posts.items %}
|
||||||
|
{% include 'post/_post_teaser.html' %}
|
||||||
|
{% else %}
|
||||||
|
<p>{{ _('No posts match your search.') }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav aria-label="Pagination" class="mt-4" role="navigation">
|
||||||
|
{% if prev_url %}
|
||||||
|
<a href="{{ prev_url }}" class="btn btn-primary" rel="nofollow">
|
||||||
|
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if next_url %}
|
||||||
|
<a href="{{ next_url }}" class="btn btn-primary" rel="nofollow">
|
||||||
|
{{ _('Next page') }} <span aria-hidden="true">→</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside id="side_pane" class="col-12 col-md-4 side_pane" role="complementary">
|
||||||
|
<!-- <div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get">
|
||||||
|
<input type="search" name="search" class="form-control mt-2" placeholder="{{ _('Search') }}" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>{{ _('Active communities') }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for community in active_communities %}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<a href="/c/{{ community.link() }}"><img src="{{ community.icon_image() }}" class="community_icon rounded-circle" loading="lazy" alt="" />
|
||||||
|
{{ community.display_name() }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p class="mt-4"><a class="btn btn-primary" href="/communities">{{ _('Explore communities') }}</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>{{ _('About %(site_name)s', site_name=g.site.name) }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p><strong>{{ g.site.description|safe }}</strong></p>
|
||||||
|
<p>{{ g.site.sidebar|safe }}</p>
|
||||||
|
{% if rss_feed %}
|
||||||
|
<p class="mt-4">
|
||||||
|
<a class="no-underline" href="{{ rss_feed }}" rel="nofollow"><span class="fe fe-rss"></span> RSS feed</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "_inoculation_links.html" %}
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
24
app/templates/search/start.html
Normal file
24
app/templates/search/start.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||||
|
{% extends 'themes/' + theme() + '/base.html' %}
|
||||||
|
{% else %}
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% endif %} %}
|
||||||
|
{% from 'bootstrap/form.html' import render_form %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col mx-auto">
|
||||||
|
<div class="card mt-5">
|
||||||
|
<div class="card-body p-6">
|
||||||
|
<div class="card-title">{{ _('Search for posts') }}</div>
|
||||||
|
<form action="" method="get" class="form" role="form">
|
||||||
|
<div class="form-group required"><label class="form-control-label visually-hidden" for="search_term" aria-label="Search here">Search</label>
|
||||||
|
<input autofocus="" class="form-control" id="search_term" name="q" required="" type="search" value="">
|
||||||
|
</div>
|
||||||
|
<input class="btn btn-primary btn-md" id="submit" name="submit" type="submit" value="Search">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
42
migrations/versions/a88efa63415b_sub_topics.py
Normal file
42
migrations/versions/a88efa63415b_sub_topics.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
"""sub topics
|
||||||
|
|
||||||
|
Revision ID: a88efa63415b
|
||||||
|
Revises: 2629cf0e2965
|
||||||
|
Create Date: 2024-03-01 19:18:49.137449
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy_searchable import sync_trigger, drop_trigger
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a88efa63415b'
|
||||||
|
down_revision = '2629cf0e2965'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('topic', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('parent_id', sa.Integer(), nullable=True))
|
||||||
|
|
||||||
|
conn = op.get_bind()
|
||||||
|
sync_trigger(conn, 'community', 'search_vector', ['name', 'title', 'description', 'rules'])
|
||||||
|
sync_trigger(conn, 'user', 'search_vector', ['user_name', 'about', 'keywords'])
|
||||||
|
sync_trigger(conn, 'post', 'search_vector', ['title', 'body'])
|
||||||
|
sync_trigger(conn, 'post_reply', 'search_vector', ['body'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('topic', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('parent_id')
|
||||||
|
|
||||||
|
conn = op.get_bind()
|
||||||
|
drop_trigger(conn, 'community', 'search_vector')
|
||||||
|
drop_trigger(conn, 'user', 'search_vector')
|
||||||
|
drop_trigger(conn, 'post', 'search_vector')
|
||||||
|
drop_trigger(conn, 'post_reply', 'search_vector')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -11,7 +11,7 @@ flask-babel==3.1.0
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
pyjwt==2.8.0
|
pyjwt==2.8.0
|
||||||
SQLAlchemy-Searchable==1.4.1
|
SQLAlchemy-Searchable==2.1.0
|
||||||
SQLAlchemy-Utils==0.41.1
|
SQLAlchemy-Utils==0.41.1
|
||||||
cryptography==41.0.3
|
cryptography==41.0.3
|
||||||
Bootstrap-Flask==2.3.0
|
Bootstrap-Flask==2.3.0
|
||||||
|
|
Loading…
Reference in a new issue