From 3085dfc29a544c62e28d779e29e1523d7eab603e Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Fri, 1 Mar 2024 20:32:29 +1300 Subject: [PATCH] search posts --- app/__init__.py | 3 + app/cli.py | 1 + app/models.py | 3 +- app/search/__init__.py | 5 ++ app/search/forms.py | 0 app/search/routes.py | 46 +++++++++++ app/static/structure.css | 11 +++ app/static/structure.scss | 11 +++ app/static/styles.css | 2 +- app/static/styles.scss | 2 +- app/templates/search/results.html | 79 +++++++++++++++++++ app/templates/search/start.html | 24 ++++++ .../versions/a88efa63415b_sub_topics.py | 42 ++++++++++ requirements.txt | 2 +- 14 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 app/search/__init__.py create mode 100644 app/search/forms.py create mode 100644 app/search/routes.py create mode 100644 app/templates/search/results.html create mode 100644 app/templates/search/start.html create mode 100644 migrations/versions/a88efa63415b_sub_topics.py diff --git a/app/__init__.py b/app/__init__.py index 9c9b00fa..cf0732c9 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -87,6 +87,9 @@ def create_app(config_class=Config): from app.chat import bp as 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'): with app.open_resource(name) as f: return f.read().decode(charset) diff --git a/app/cli.py b/app/cli.py index 4c828c8b..42c5a5f6 100644 --- a/app/cli.py +++ b/app/cli.py @@ -8,6 +8,7 @@ import flask from flask import json, current_app from flask_babel import _ from sqlalchemy import or_, desc +from sqlalchemy.orm import configure_mappers from app import db import click diff --git a/app/models.py b/app/models.py index 487dfe96..6b8a36ae 100644 --- a/app/models.py +++ b/app/models.py @@ -222,6 +222,7 @@ class Topic(db.Model): machine_name = db.Column(db.String(50), index=True) name = db.Column(db.String(50)) 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") @@ -483,7 +484,7 @@ class User(UserMixin, db.Model): ap_inbox_url = 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") posts = db.relationship('Post', lazy='dynamic', cascade="all, delete-orphan") post_replies = db.relationship('PostReply', lazy='dynamic', cascade="all, delete-orphan") diff --git a/app/search/__init__.py b/app/search/__init__.py new file mode 100644 index 00000000..8c2a94f6 --- /dev/null +++ b/app/search/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('search', __name__) + +from app.search import routes diff --git a/app/search/forms.py b/app/search/forms.py new file mode 100644 index 00000000..e69de29b diff --git a/app/search/routes.py b/app/search/routes.py new file mode 100644 index 00000000..e4a96cf8 --- /dev/null +++ b/app/search/routes.py @@ -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) diff --git a/app/static/structure.css b/app/static/structure.css index 19556312..39ba5112 100644 --- a/app/static/structure.css +++ b/app/static/structure.css @@ -1273,4 +1273,15 @@ fieldset legend { 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 */ diff --git a/app/static/structure.scss b/app/static/structure.scss index fb51b917..c061882c 100644 --- a/app/static/structure.scss +++ b/app/static/structure.scss @@ -950,4 +950,15 @@ fieldset { text-align: right; margin-bottom: 10px; display: block; +} + +.redo_search { + display: inline; + input[type=search] { + width: unset; + display: inline; + font-size: inherit; + line-height: initial; + max-width: 100%; + } } \ No newline at end of file diff --git a/app/static/styles.css b/app/static/styles.css index 05b5da78..678b1cb5 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -477,7 +477,7 @@ div.navbar { } #outer_container { - margin-top: -4px; + margin-top: -1px; } @media (min-width: 992px) { #outer_container { diff --git a/app/static/styles.scss b/app/static/styles.scss index 1dfd8689..f28ac5dd 100644 --- a/app/static/styles.scss +++ b/app/static/styles.scss @@ -78,7 +78,7 @@ div.navbar { } #outer_container { - margin-top: -4px; + margin-top: -1px; @include breakpoint(tablet) { margin-top: 1rem; padding-top: 0.25rem; diff --git a/app/templates/search/results.html b/app/templates/search/results.html new file mode 100644 index 00000000..6ce6dd07 --- /dev/null +++ b/app/templates/search/results.html @@ -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 %} +
+
+

{{ _('Search results for') }}

+
+ {% for post in posts.items %} + {% include 'post/_post_teaser.html' %} + {% else %} +

{{ _('No posts match your search.') }}

+ {% endfor %} +
+ + +
+ + +
+{% endblock %} diff --git a/app/templates/search/start.html b/app/templates/search/start.html new file mode 100644 index 00000000..529bafeb --- /dev/null +++ b/app/templates/search/start.html @@ -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 %} +
+
+
+
+
{{ _('Search for posts') }}
+
+
+ +
+ +
+
+
+
+
+{% endblock %} diff --git a/migrations/versions/a88efa63415b_sub_topics.py b/migrations/versions/a88efa63415b_sub_topics.py new file mode 100644 index 00000000..71d9d829 --- /dev/null +++ b/migrations/versions/a88efa63415b_sub_topics.py @@ -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 ### diff --git a/requirements.txt b/requirements.txt index 14c1c0c4..4ccc950b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ flask-babel==3.1.0 psycopg2-binary requests==2.31.0 pyjwt==2.8.0 -SQLAlchemy-Searchable==1.4.1 +SQLAlchemy-Searchable==2.1.0 SQLAlchemy-Utils==0.41.1 cryptography==41.0.3 Bootstrap-Flask==2.3.0