flaskbb-plugin-like 1.0.0.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ .Python
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Unit test / coverage reports
24
+ htmlcov/
25
+ .tox/
26
+ .coverage
27
+ .coverage.*
28
+ .cache
29
+ .pytest_cache/
30
+ .ruff_cache/
31
+ nosetests.xml
32
+ coverage.xml
33
+ *,cover
34
+ .hypothesis/
35
+
36
+ # Translations
37
+ *.mo
38
+ *.pot
39
+
40
+ *.log
41
+
42
+ .vscode
43
+ .venv/
@@ -0,0 +1,33 @@
1
+ Copyright (c) 2018 Михаил Лебедев
2
+ Copyright (c) 2026 Peter Justin
3
+
4
+ Some rights reserved.
5
+
6
+ Redistribution and use in source and binary forms of the software as well
7
+ as documentation, with or without modification, are permitted provided
8
+ that the following conditions are met:
9
+
10
+ * Redistributions of source code must retain the above copyright
11
+ notice, this list of conditions and the following disclaimer.
12
+
13
+ * Redistributions in binary form must reproduce the above
14
+ copyright notice, this list of conditions and the following
15
+ disclaimer in the documentation and/or other materials provided
16
+ with the distribution.
17
+
18
+ * The names of the contributors may not be used to endorse or
19
+ promote products derived from this software without specific
20
+ prior written permission.
21
+
22
+ THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
23
+ CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
24
+ NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
25
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
26
+ OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
27
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
28
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
29
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
30
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
31
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
32
+ SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
33
+ DAMAGE.
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.5
2
+ Name: flaskbb-plugin-like
3
+ Version: 1.0.0.dev0
4
+ Summary: Like functionality for FlaskBB - let users like posts and see who liked them
5
+ Project-URL: Homepage, https://github.com/flaskbb/flaskbb-plugin-like
6
+ Project-URL: Repository, https://github.com/flaskbb/flaskbb-plugin-like
7
+ Project-URL: Issues, https://github.com/flaskbb/flaskbb-plugin-like/issues
8
+ Project-URL: Changelog, https://github.com/flaskbb/flaskbb-plugin-like/blob/master/CHANGELOG.md
9
+ Author-email: Peter Justin <peter.justin@outlook.com>
10
+ Maintainer-email: Peter Justin <peter.justin@outlook.com>
11
+ License-Expression: BSD-3-Clause
12
+ License-File: LICENSE
13
+ Keywords: flaskbb,like,likes,plugin
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Environment :: Web Environment
16
+ Classifier: Framework :: Flask
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Intended Audience :: End Users/Desktop
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Requires-Python: >=3.12
26
+ Requires-Dist: flaskbb>=3.0.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ Like
30
+ ====
31
+
32
+ Like functionality for FlaskBB. Lets users like posts and see who liked them.
33
+
34
+
35
+ Installation
36
+ ------------
37
+
38
+ Install *flaskbb-plugin-like* with
39
+ ``pip install flaskbb-plugin-like``
40
+
41
+ And run the migrations:
42
+ ``flaskbb db upgrade``
43
+
44
+
45
+ Credits
46
+ -------
47
+ Forked from [flaskbb-plugin-vanity](https://github.com/micha030201/flaskbb-plugin-vanity) by Михаил Лебедев.
48
+
49
+ License
50
+ -------
51
+ This project is licensed under the terms of the [BSD License](/LICENSE).
@@ -0,0 +1,23 @@
1
+ Like
2
+ ====
3
+
4
+ Like functionality for FlaskBB. Lets users like posts and see who liked them.
5
+
6
+
7
+ Installation
8
+ ------------
9
+
10
+ Install *flaskbb-plugin-like* with
11
+ ``pip install flaskbb-plugin-like``
12
+
13
+ And run the migrations:
14
+ ``flaskbb db upgrade``
15
+
16
+
17
+ Credits
18
+ -------
19
+ Forked from [flaskbb-plugin-vanity](https://github.com/micha030201/flaskbb-plugin-vanity) by Михаил Лебедев.
20
+
21
+ License
22
+ -------
23
+ This project is licensed under the terms of the [BSD License](/LICENSE).
@@ -0,0 +1,115 @@
1
+ """
2
+ like
3
+ ~~~~
4
+
5
+ Like functionality for FlaskBB. Lets users like posts and see who liked
6
+ them, via a button on the post menu.
7
+
8
+ :copyright: (c) 2026 by Peter Justin, (c) 2018 by Михаил Лебедев.
9
+ :license: BSD License, see LICENSE for more details.
10
+ """
11
+
12
+ import os
13
+
14
+ from flask import Flask
15
+ from flask_babelplus import gettext as _
16
+ from flask_login import current_user
17
+ from flaskbb.settings import BoolSetting, SettingGroup
18
+ from flaskbb.display.navigation import NavigationLink
19
+ from flaskbb.forum.models import Post
20
+ from flaskbb.user.models import User
21
+ from flaskbb.utils.helpers import real, render_template
22
+ from pluggy import HookimplMarker
23
+
24
+ from .forms import LikeActionForm
25
+ from .utils import (
26
+ DEFAULT_ALLOW_SELF_LIKE,
27
+ can_like_post,
28
+ has_liked,
29
+ likes_given_count,
30
+ likes_received_count,
31
+ )
32
+ from .views import like_bp
33
+
34
+ __version__ = "1.0.0"
35
+
36
+ hookimpl = HookimplMarker("flaskbb")
37
+
38
+
39
+ @hookimpl
40
+ def flaskbb_load_migrations():
41
+ return os.path.join(os.path.dirname(__file__), "migrations")
42
+
43
+
44
+ @hookimpl
45
+ def flaskbb_load_translations():
46
+ return os.path.join(os.path.dirname(__file__), "translations")
47
+
48
+
49
+ @hookimpl
50
+ def flaskbb_load_blueprints(app: Flask):
51
+ app.register_blueprint(
52
+ like_bp, url_prefix=app.config.get("PLUGIN_LIKE_URL_PREFIX", "/like")
53
+ )
54
+
55
+
56
+ SETTINGS = SettingGroup(
57
+ key="like",
58
+ name="Like Settings",
59
+ description="Settings for the like plugin.",
60
+ settings=(
61
+ BoolSetting(
62
+ key="ALLOW_SELF_LIKE",
63
+ value=DEFAULT_ALLOW_SELF_LIKE,
64
+ name="Allow liking your own posts",
65
+ description="If enabled, a user may like their own posts.",
66
+ ),
67
+ ),
68
+ )
69
+
70
+
71
+ @hookimpl
72
+ def flaskbb_load_setting_groups():
73
+ return SETTINGS
74
+
75
+
76
+ @hookimpl
77
+ def flaskbb_tpl_post_menu_before(post: Post):
78
+ user = real(current_user)
79
+ can_like = user.is_authenticated and can_like_post(user, post)
80
+ already_liked = user.is_authenticated and has_liked(user, post)
81
+ return render_template(
82
+ "like/_like_button.html",
83
+ post=post,
84
+ can_like=can_like,
85
+ already_liked=already_liked,
86
+ form=LikeActionForm(),
87
+ )
88
+
89
+
90
+ @hookimpl
91
+ def flaskbb_tpl_post_author_info_after(user: User | None, post: Post):
92
+ if user is None:
93
+ return None
94
+
95
+ return render_template(
96
+ "like/_like_counts.html",
97
+ user=user,
98
+ likes_given=likes_given_count(user),
99
+ likes_received=likes_received_count(user),
100
+ )
101
+
102
+
103
+ @hookimpl
104
+ def flaskbb_tpl_scripts():
105
+ return render_template("like/_scripts.html")
106
+
107
+
108
+ @hookimpl
109
+ def flaskbb_tpl_profile_sidebar_links(user: User):
110
+ return NavigationLink(
111
+ endpoint="like.liked_posts",
112
+ name=_("Liked posts"),
113
+ icon="fa fa-heart",
114
+ urlforkwargs={"username": user.username},
115
+ )
@@ -0,0 +1,18 @@
1
+ """
2
+ like.forms
3
+ ~~~~~~~~~~
4
+
5
+ Forms used to like/unlike a post. Their only real job is to carry the
6
+ CSRF token - which post to (un)like comes from the URL.
7
+
8
+ :copyright: (c) 2026 by Peter Justin.
9
+ :license: BSD License, see LICENSE for more details.
10
+ """
11
+
12
+ from flask_wtf import FlaskForm
13
+
14
+
15
+ class LikeActionForm(FlaskForm):
16
+ """Used in _like_button.html to like or unlike a post.
17
+ It just contains the hidden CSRF field
18
+ """
@@ -0,0 +1,85 @@
1
+ """Add like_association and the user like counters
2
+
3
+ Revision ID: b7d2e4f6a9c1
4
+ Revises:
5
+ Create Date: 2026-07-31 12:00:00.000000
6
+
7
+ """
8
+
9
+ import sqlalchemy as sa
10
+ from alembic import op
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = "b7d2e4f6a9c1"
14
+ down_revision = None
15
+ branch_labels = ("like",)
16
+ depends_on = "8ad96e49dc6" # flaskbb core init migration - creates posts/users
17
+
18
+
19
+ def upgrade():
20
+ con = op.get_bind()
21
+ inspector = sa.inspect(con.engine)
22
+
23
+ if not inspector.has_table("like_association"):
24
+ op.create_table(
25
+ "like_association",
26
+ sa.Column("post_id", sa.Integer(), nullable=False),
27
+ sa.Column("user_id", sa.Integer(), nullable=False),
28
+ sa.ForeignKeyConstraint(
29
+ ["post_id"],
30
+ ["posts.id"],
31
+ name=op.f("fk_like_association_post_id_posts"),
32
+ ondelete="CASCADE",
33
+ ),
34
+ sa.ForeignKeyConstraint(
35
+ ["user_id"],
36
+ ["users.id"],
37
+ name=op.f("fk_like_association_user_id_users"),
38
+ ondelete="CASCADE",
39
+ ),
40
+ sa.PrimaryKeyConstraint(
41
+ "post_id", "user_id", name=op.f("pk_like_association")
42
+ ),
43
+ )
44
+
45
+ # a fresh install that went through db.create_all() already has them
46
+ user_columns = {c["name"] for c in inspector.get_columns("users")}
47
+ with op.batch_alter_table("users", schema=None) as batch_op:
48
+ if "likes_given" not in user_columns:
49
+ batch_op.add_column(
50
+ sa.Column(
51
+ "likes_given", sa.Integer(), nullable=False, server_default="0"
52
+ )
53
+ )
54
+ if "likes_received" not in user_columns:
55
+ batch_op.add_column(
56
+ sa.Column(
57
+ "likes_received", sa.Integer(), nullable=False, server_default="0"
58
+ )
59
+ )
60
+
61
+ op.execute(
62
+ """
63
+ UPDATE users SET likes_given = (
64
+ SELECT COUNT(*) FROM like_association
65
+ WHERE like_association.user_id = users.id
66
+ )
67
+ """
68
+ )
69
+ op.execute(
70
+ """
71
+ UPDATE users SET likes_received = (
72
+ SELECT COUNT(*) FROM like_association
73
+ JOIN posts ON posts.id = like_association.post_id
74
+ WHERE posts.user_id = users.id
75
+ )
76
+ """
77
+ )
78
+
79
+
80
+ def downgrade():
81
+ op.drop_table("like_association")
82
+
83
+ with op.batch_alter_table("users", schema=None) as batch_op:
84
+ batch_op.drop_column("likes_received")
85
+ batch_op.drop_column("likes_given")
@@ -0,0 +1,131 @@
1
+ """
2
+ like.models
3
+ ~~~~~~~~~~~
4
+
5
+ The model for a user's like on a post.
6
+
7
+ :copyright: (c) 2026 by Peter Justin, (c) 2018 by Михаил Лебедев.
8
+ :license: BSD License, see LICENSE for more details.
9
+ """
10
+
11
+ from typing import TYPE_CHECKING, cast
12
+
13
+ from flaskbb.extensions import db
14
+ from flaskbb.forum.models import Post
15
+ from flaskbb.user.models import User
16
+ from flaskbb.utils.database import BaseModel
17
+ from sqlalchemy import Connection, ForeignKey, Integer, Table, event, update
18
+ from sqlalchemy.ext.associationproxy import association_proxy
19
+ from sqlalchemy.orm import Mapped, Mapper, mapped_column, relationship
20
+
21
+
22
+ class PostLike(BaseModel):
23
+ __tablename__ = "like_association"
24
+
25
+ post_id: Mapped[int] = mapped_column(
26
+ ForeignKey("posts.id", ondelete="CASCADE"), primary_key=True
27
+ )
28
+ user_id: Mapped[int] = mapped_column(
29
+ ForeignKey("users.id", ondelete="CASCADE"), primary_key=True
30
+ )
31
+
32
+ # Both sides load via `selectin`, never `joined` - see
33
+ # https://github.com/flaskbb/flaskbb/issues/503. `joined` folds into
34
+ # whatever query loads a Post/User, including flaskbb's own hand-built
35
+ # outerjoin in Topic.get_posts, and corrupts its column layout.
36
+ # `selectin` always runs as its own separate, batched query instead, so
37
+ # it can chain (post -> liked_by_users -> user) without ever touching a
38
+ # query this plugin doesn't own.
39
+ post: Mapped["Post"] = relationship(
40
+ "Post",
41
+ backref=db.backref(
42
+ "liked_by_users", lazy="selectin", cascade="all, delete-orphan"
43
+ ),
44
+ )
45
+ user: Mapped["User"] = relationship(
46
+ "User",
47
+ lazy="selectin",
48
+ backref=db.backref("user_liked_posts", cascade="all, delete-orphan"),
49
+ )
50
+
51
+
52
+ Post.likers = association_proxy(
53
+ "liked_by_users",
54
+ "user",
55
+ creator=lambda user: PostLike(user=user), # pyright: ignore
56
+ )
57
+
58
+ # Denormalized like counters on the core `users` table. Counting from
59
+ # like_association on every render costs a query per distinct post author
60
+ # per page; these are two columns that come along with the User row that
61
+ # flaskbb has already loaded.
62
+ #
63
+ # Assigning a mapped_column onto an already-mapped class goes through
64
+ # declarative's _add_attribute, which appends the column to the `users`
65
+ # Table and adds the property to User's mapper - the same monkey-patching
66
+ # this plugin already does with Post.likers above. like/migrations adds
67
+ # the actual DB columns.
68
+ User.likes_given = mapped_column(Integer, default=0, server_default="0", nullable=False)
69
+ User.likes_received = mapped_column(
70
+ Integer, default=0, server_default="0", nullable=False
71
+ )
72
+
73
+
74
+ if TYPE_CHECKING:
75
+
76
+ class LikeUser(User):
77
+ """A ``User`` with the two counters added above. They are added at
78
+ import time, so a type checker cannot see them on ``User`` itself -
79
+ cast to this to read or write them.
80
+ """
81
+
82
+ likes_given: int = 0
83
+ likes_received: int = 0
84
+
85
+ else:
86
+ LikeUser = User
87
+
88
+
89
+ def _apply_delta(connection: Connection, like: PostLike, delta: int) -> None:
90
+ """Two people liking the same post at once can't lose an increment. Runs
91
+ on the flush's connection, in the same transaction as the row itself.
92
+
93
+ The post's author is looked up as a subquery instead of via
94
+ ``like.post.user_id``: emitting a lazy load from inside a flush event is
95
+ not allowed, and on the delete path the Post may already be gone from the
96
+ session. If the post has no author (guest post, or the author's row went
97
+ first), the subquery is NULL and the UPDATE matches nothing.
98
+ """
99
+ users = cast(Table, User.__table__)
100
+ posts = cast(Table, Post.__table__)
101
+ author_id = (
102
+ db.select(posts.c.user_id).where(posts.c.id == like.post_id).scalar_subquery()
103
+ )
104
+ connection.execute(
105
+ update(users)
106
+ .where(users.c.id == like.user_id)
107
+ .values(likes_given=users.c.likes_given + delta)
108
+ )
109
+ connection.execute(
110
+ update(users)
111
+ .where(users.c.id == author_id)
112
+ .values(likes_received=users.c.likes_received + delta)
113
+ )
114
+
115
+
116
+ @event.listens_for(PostLike, "after_insert")
117
+ def _increment_like_counts(
118
+ mapper: Mapper[PostLike], connection: Connection, target: PostLike
119
+ ) -> None:
120
+ _apply_delta(connection, target, 1)
121
+
122
+
123
+ @event.listens_for(PostLike, "after_delete")
124
+ def _decrement_like_counts(
125
+ mapper: Mapper[PostLike], connection: Connection, target: PostLike
126
+ ) -> None:
127
+ # Keeps the counters honest on paths this plugin never sees: deleting a
128
+ # post or a user cascades through the ORM relationships above, which
129
+ # deletes each PostLike individually and fires this. A raw SQL delete
130
+ # against like_association bypasses it - see recalculate_like_counts.
131
+ _apply_delta(connection, target, -1)
@@ -0,0 +1,58 @@
1
+ (function () {
2
+ "use strict";
3
+
4
+ function patchCounts(counts) {
5
+ for (const entry of counts) {
6
+ const blocks = document.querySelectorAll(
7
+ `.like-counts[data-user-id="${entry.user_id}"]`
8
+ );
9
+ for (const block of blocks) {
10
+ block.querySelector(".like-counts-given").textContent = entry.given;
11
+ block.querySelector(".like-counts-received").textContent =
12
+ entry.received;
13
+ }
14
+ }
15
+ }
16
+
17
+ // Delegated, because the widget this fires on is replaced by the response
18
+ // of the request it just sent.
19
+ document.addEventListener("submit", async (event) => {
20
+ const form = event.target.closest("form[data-like-form]");
21
+ if (!form) {
22
+ return;
23
+ }
24
+ event.preventDefault();
25
+
26
+ const widget = form.closest(".like-widget");
27
+ const button = form.querySelector("button[type=submit]");
28
+ button.disabled = true;
29
+
30
+ let response;
31
+ try {
32
+ response = await fetch(form.action, {
33
+ method: "POST",
34
+ headers: { "X-Requested-With": "XMLHttpRequest" },
35
+ // Carries the form's CSRF token, so the view's LikeActionForm
36
+ // validates exactly like it does for a plain post.
37
+ body: new FormData(form),
38
+ });
39
+ } catch (error) {
40
+ // Offline or the request never landed - nothing changed server side,
41
+ // so let the user try again.
42
+ button.disabled = false;
43
+ return;
44
+ }
45
+
46
+ if (!response.ok) {
47
+ // A stale token, or the post was liked/unliked from somewhere else in
48
+ // the meantime. Submit for real so the user lands on the same page the
49
+ // no-JS flow would have given them.
50
+ form.submit();
51
+ return;
52
+ }
53
+
54
+ const data = await response.json();
55
+ widget.outerHTML = data.widget;
56
+ patchCounts(data.counts);
57
+ });
58
+ })();
@@ -0,0 +1,41 @@
1
+ {% set likes = post.likers | length %}
2
+ <div class="like-widget d-inline" data-post-id="{{ post.id }}">
3
+ <a href="#" data-bs-toggle="modal" data-bs-target="#likers-post-{{ post.id }}">
4
+ <small>{{ _("%(count)s like%(plural)s", count=likes, plural="" if likes == 1 else "s") }}</small>
5
+ </a>
6
+ <div class="modal fade" id="likers-post-{{ post.id }}" tabindex="-1" aria-hidden="true">
7
+ <div class="modal-dialog" role="document">
8
+ <div class="modal-content">
9
+ <div class="modal-header">
10
+ <h5 class="modal-title">{{ _("Users who liked this post") }}</h5>
11
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
12
+ </div>
13
+ <div class="modal-body">
14
+ {% for liker in post.likers %}
15
+ <div><a href="{{ liker.url }}">{{ liker.username }}</a></div>
16
+ {% else %}
17
+ <div>{{ _("No likes yet") }}</div>
18
+ {% endfor %}
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </div>
23
+
24
+ {% if can_like %}
25
+ {# No data-bs-toggle="tooltip" here: aurora instantiates its tooltips once
26
+ on page load and doesn't expose bootstrap to plugin scripts, so a button
27
+ swapped in by like.js would end up without one. A plain title attribute
28
+ survives the swap. #}
29
+ <form class="d-inline"
30
+ data-like-form
31
+ method="post"
32
+ action="{{ url_for('like.unlike_post' if already_liked else 'like.like_post', post_id=post.id) }}">
33
+ {{ form.hidden_tag() }}
34
+ {% if already_liked %}
35
+ <button type="submit" class="btn btn-icon fas fa-heart text-red" title="{{ _('Withdraw like') }}" aria-label="{{ _('Withdraw like') }}"></button>
36
+ {% else %}
37
+ <button type="submit" class="btn btn-icon far fa-heart text-red" title="{{ _('Like') }}" aria-label="{{ _('Like') }}"></button>
38
+ {% endif %}
39
+ </form>
40
+ {% endif %}
41
+ </div>
@@ -0,0 +1,4 @@
1
+ <div class="like-counts" data-user-id="{{ user.id }}">
2
+ <div>{{ _("Likes given") }}: <span class="like-counts-given">{{ likes_given }}</span></div>
3
+ <div>{{ _("Likes received") }}: <span class="like-counts-received">{{ likes_received }}</span></div>
4
+ </div>
@@ -0,0 +1 @@
1
+ <script src="{{ url_for('like.static', filename='like.js') }}"></script>
@@ -0,0 +1,56 @@
1
+ {% extends theme("user/profile_layout.html") %}
2
+ {%- from theme("_macros/pagination.html") import render_pagination %}
3
+
4
+ {% block breadcrumb %}
5
+ <ol class="breadcrumb flaskbb-breadcrumb bg-light">
6
+ <li class="breadcrumb-item"><a href="{{ url_for('forum.index') }}">{% trans %}Forum{% endtrans %}</a></li>
7
+ <li class="breadcrumb-item"><a href="{{ user.url }}">{{ user.username }}</a></li>
8
+ <li class="breadcrumb-item active">{{ _("Liked posts") }}</li>
9
+ </ol>
10
+ {% endblock %}
11
+
12
+ {% block profile_content %}
13
+ <div class="col-12 profile-content">
14
+
15
+ {% for post in posts.items %}
16
+ <div class="card page mb-3">
17
+ <div class="card-header page-header topic-header fw-normal">
18
+ {% trans trimmed
19
+ post_url=post.url,
20
+ topic_title=post.topic.title,
21
+ forum_url=post.topic.forum.url,
22
+ forum_title=post.topic.forum.title
23
+ %}
24
+ <strong><a href="{{ post_url }}">{{ topic_title }}</a></strong>
25
+ in forum <a href="{{ forum_url }}">{{ forum_title }}</a>
26
+ {% endtrans %}
27
+ </div>
28
+ <div class="card-body page-body topic-content">
29
+ <div class="col-12 ps-3">
30
+ <div class="topic-created">
31
+ {{ post.date_created|format_datetime }}
32
+ </div>
33
+ <div class="topic-content">
34
+ {{ post.content|markup }}
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ {% else %}
40
+ <div class="row">
41
+ <div class="col-12">
42
+ <div class="alert-message alert-message-info" role="alert">
43
+ {{ _("No posts liked yet") }}
44
+ </div>
45
+ </div>
46
+ </div>
47
+ {% endfor %}
48
+
49
+ {% if posts.items %}
50
+ <div class="col-12 controls-col">
51
+ {{ render_pagination(posts, url_for('like.liked_posts', username=user.username)) }}
52
+ </div>
53
+ {% endif %}
54
+
55
+ </div>
56
+ {% endblock %}