Merge pull request 'user notes (implements #258)' (#382) from h3ndrik/pyfedi:user_note1 into main

Reviewed-on: https://codeberg.org/rimu/pyfedi/pulls/382
This commit is contained in:
rimu 2024-12-21 00:13:58 +00:00
commit 0b8251524c
6 changed files with 126 additions and 3 deletions

View file

@ -710,6 +710,7 @@ class User(UserMixin, db.Model):
cover = db.relationship('File', lazy='joined', foreign_keys=[cover_id], single_parent=True, cascade="all, delete-orphan") cover = db.relationship('File', lazy='joined', foreign_keys=[cover_id], single_parent=True, cascade="all, delete-orphan")
instance = db.relationship('Instance', lazy='joined', foreign_keys=[instance_id]) instance = db.relationship('Instance', lazy='joined', foreign_keys=[instance_id])
conversations = db.relationship('Conversation', lazy='dynamic', secondary=conversation_member, backref=db.backref('members', lazy='joined')) conversations = db.relationship('Conversation', lazy='dynamic', secondary=conversation_member, backref=db.backref('members', lazy='joined'))
user_notes = db.relationship('UserNote', lazy='dynamic', foreign_keys="UserNote.target_id")
ap_id = db.Column(db.String(255), index=True) # e.g. username@server ap_id = db.Column(db.String(255), index=True) # e.g. username@server
ap_profile_id = db.Column(db.String(255), index=True, unique=True) # e.g. https://server/u/username ap_profile_id = db.Column(db.String(255), index=True, unique=True) # e.g. https://server/u/username
@ -1022,6 +1023,7 @@ class User(UserMixin, db.Model):
db.session.query(PostBookmark).filter(PostBookmark.user_id == self.id).delete() db.session.query(PostBookmark).filter(PostBookmark.user_id == self.id).delete()
db.session.query(PostReplyBookmark).filter(PostReplyBookmark.user_id == self.id).delete() db.session.query(PostReplyBookmark).filter(PostReplyBookmark.user_id == self.id).delete()
db.session.query(ModLog).filter(ModLog.user_id == self.id).delete() db.session.query(ModLog).filter(ModLog.user_id == self.id).delete()
db.session.query(UserNote).filter(or_(UserNote.user_id == self.id, UserNote.target_id == self.id)).delete()
def purge_content(self, soft=True): def purge_content(self, soft=True):
files = File.query.join(Post).filter(Post.user_id == self.id).all() files = File.query.join(Post).filter(Post.user_id == self.id).all()
@ -1078,6 +1080,13 @@ class User(UserMixin, db.Model):
def has_read_post(self, post): def has_read_post(self, post):
return self.read_post.filter(read_posts.c.read_post_id == post.id).count() > 0 return self.read_post.filter(read_posts.c.read_post_id == post.id).count() > 0
@cache.memoize(timeout=500)
def get_note(self, by_user):
user_note = self.user_notes.filter(UserNote.target_id == self.id, UserNote.user_id == by_user.id).first()
if user_note:
return user_note.body
else:
return None
class ActivityLog(db.Model): class ActivityLog(db.Model):

View file

@ -27,6 +27,12 @@
<span class="fe fe-warning orangered" title="Low reputation."> </span> <span class="fe fe-warning orangered" title="Low reputation."> </span>
{% endif -%} {% endif -%}
{% endif -%} {% endif -%}
{% if current_user.is_authenticated -%}
{% set user_note = user.get_note(current_user) %}
{% if user_note -%}
<span class="user_note" title="{{ _('User note: %(note)s', note=user_note) }}">[{{ user_note | truncate(12, True) }}]</span>
{% endif -%}
{% endif -%}
{% endif -%} {% endif -%}
</span> </span>
{% endmacro -%} {% endmacro -%}

View file

@ -0,0 +1,68 @@
{% 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 col-login mx-auto">
<div class="card mt-5">
<div class="card-body p-6">
<div class="card-title">{{ _('Edit note for "%(user_name)s"', user_name=user.display_name()) }}</div>
<div class="card-body">
<strong>{{ _('Emoji quick access') }}</strong>
<div>
<button id="thumbsup" class="emojitoggle">👍</button>
<button id="thumbsdown" class="emojitoggle">👎</button>
<button id="smile" class="emojitoggle">😄</button>
<button id="party-popper" class="emojitoggle">🎉</button>
<button id="frown" class="emojitoggle">😕</button>
<button id="red-heart" class="emojitoggle">❤️</button>
<button id="rocket" class="emojitoggle">🚀</button>
<button id="eyes" class="emojitoggle">👀</button>
</div><div>
<button id="star" class="emojitoggle"></button>
<button id="medal" class="emojitoggle">🥇</button>
<button id="check" class="emojitoggle">☑️</button>
<button id="fire" class="emojitoggle">🔥</button>
<button id="robot" class="emojitoggle">🤖</button>
<button id="ghost" class="emojitoggle">👻</button>
<button id="clown" class="emojitoggle">🤡</button>
<button id="poo" class="emojitoggle">💩</button>
</div><div>
<button id="speech-bubble" class="emojitoggle">💬</button>
<button id="anger-bubble" class="emojitoggle">🗯️</button>
<button id="hundred" class="emojitoggle">💯</button>
<button id="rofl" class="emojitoggle">🤣</button>
<button id="zany" class="emojitoggle">🤪</button>
<button id="warning" class="emojitoggle">⚠️</button>
<button id="no-entry" class="emojitoggle"></button>
<button id="vomit" class="emojitoggle">🤮</button>
</div>
{{ render_form(form) }}
<div class="row mt-5"><small class="field_hint">{{ _('This note appears next to their username. It\'s meant just for you and not displayed to anyone else.') }}</small></div>
</div>
</div>
</div>
</div>
</div>
<script nonce="{{ session['nonce'] }}" type="text/javascript">
function addtext(text) {
var note = document.getElementById("note");
newtext = note.value.replaceAll(text, "");
if (newtext == note.value) {
note.value += text;
} else {
note.value = newtext;
}
}
document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll("button.emojitoggle").forEach(function(button) {
var emoji = button.textContent || button.innerText;
button.addEventListener('click', function() {addtext(emoji);});
});
});
</script>
{% endblock %}

View file

@ -100,6 +100,7 @@
{% endif -%} {% endif -%}
{% endif -%} {% endif -%}
<li><a class="dropdown-item" href="{{ url_for('user.report_profile', actor=user.link()) }}" rel="nofollow">{{ _('Report') }}</a></li> <li><a class="dropdown-item" href="{{ url_for('user.report_profile', actor=user.link()) }}" rel="nofollow">{{ _('Report') }}</a></li>
<li><a class="dropdown-item" href="{{ url_for('user.edit_user_note', actor=user.link()) }}" rel="nofollow">{{ _('Edit note') }}</a></li>
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
@ -118,6 +119,7 @@
{% if current_user.is_authenticated and current_user.is_admin() and user.reputation %}{{ _('Reputation') }}: <span title="{{ _('Reputation: The Karma of the account. Total up votes minus down votes they got.') }}">{{ user.reputation | round | int }}</span><br />{% endif %} {% if current_user.is_authenticated and current_user.is_admin() and user.reputation %}{{ _('Reputation') }}: <span title="{{ _('Reputation: The Karma of the account. Total up votes minus down votes they got.') }}">{{ user.reputation | round | int }}</span><br />{% endif %}
{{ _('Posts') }}: {{ user.post_count }}<br /> {{ _('Posts') }}: {{ user.post_count }}<br />
{{ _('Comments') }}: {{ user.post_reply_count }}<br /> {{ _('Comments') }}: {{ user.post_reply_count }}<br />
{% if current_user.is_authenticated %}{{ _('User note') }}: {{ user.get_note(current_user) }}<br />{% endif %}
</p> </p>
<div class="profile_bio"> <div class="profile_bio">
{{ user.about_html|safe }} {{ user.about_html|safe }}

View file

@ -147,3 +147,8 @@ class RemoteFollowForm(FlaskForm):
instance_type = SelectField(_l('Instance type'), choices=type_choices, render_kw={'class': 'form-select'}) instance_type = SelectField(_l('Instance type'), choices=type_choices, render_kw={'class': 'form-select'})
submit = SubmitField(_l('View profile on remote instance')) submit = SubmitField(_l('View profile on remote instance'))
class UserNoteForm(FlaskForm):
note = StringField(_l('User note'), validators=[Optional(), Length(max=50)])
submit = SubmitField(_l('Save note'))

View file

@ -16,10 +16,10 @@ from app.constants import *
from app.email import send_verification_email from app.email import send_verification_email
from app.models import Post, Community, CommunityMember, User, PostReply, PostVote, Notification, utcnow, File, Site, \ from app.models import Post, Community, CommunityMember, User, PostReply, PostVote, Notification, utcnow, File, Site, \
Instance, Report, UserBlock, CommunityBan, CommunityJoinRequest, CommunityBlock, Filter, Domain, DomainBlock, \ Instance, Report, UserBlock, CommunityBan, CommunityJoinRequest, CommunityBlock, Filter, Domain, DomainBlock, \
InstanceBlock, NotificationSubscription, PostBookmark, PostReplyBookmark, read_posts, Topic InstanceBlock, NotificationSubscription, PostBookmark, PostReplyBookmark, read_posts, Topic, UserNote
from app.user import bp from app.user import bp
from app.user.forms import ProfileForm, SettingsForm, DeleteAccountForm, ReportUserForm, \ from app.user.forms import ProfileForm, SettingsForm, DeleteAccountForm, ReportUserForm, \
FilterForm, KeywordFilterEditForm, RemoteFollowForm, ImportExportForm FilterForm, KeywordFilterEditForm, RemoteFollowForm, ImportExportForm, UserNoteForm
from app.user.utils import purge_user_then_delete, unsubscribe_from_community from app.user.utils import purge_user_then_delete, unsubscribe_from_community
from app.utils import get_setting, render_template, markdown_to_html, user_access, markdown_to_text, shorten_string, \ from app.utils import get_setting, render_template, markdown_to_html, user_access, markdown_to_text, shorten_string, \
is_image_url, ensure_directory_exists, gibberish, file_get_contents, community_membership, user_filters_home, \ is_image_url, ensure_directory_exists, gibberish, file_get_contents, community_membership, user_filters_home, \
@ -1330,3 +1330,36 @@ def user_read_posts_delete():
db.session.commit() db.session.commit()
flash(_('Reading history has been deleted')) flash(_('Reading history has been deleted'))
return redirect(url_for('user.user_read_posts')) return redirect(url_for('user.user_read_posts'))
@bp.route('/u/<actor>/note', methods=['GET', 'POST'])
@login_required
def edit_user_note(actor):
actor = actor.strip()
if '@' in actor:
user: User = User.query.filter_by(ap_id=actor, deleted=False).first()
else:
user: User = User.query.filter_by(user_name=actor, deleted=False, ap_id=None).first()
if user is None:
abort(404)
form = UserNoteForm()
if form.validate_on_submit() and not current_user.banned:
text = form.note.data.strip()
usernote = UserNote.query.filter(UserNote.target_id == user.id, UserNote.user_id == current_user.id).first()
if usernote:
usernote.body = text
else:
usernote = UserNote(target_id=user.id, user_id=current_user.id, body=text)
db.session.add(usernote)
db.session.commit()
cache.delete_memoized(User.get_note, user, current_user)
flash(_('Your changes have been saved.'), 'success')
goto = request.args.get('redirect') if 'redirect' in request.args else f'/u/{actor}'
return redirect(goto)
elif request.method == 'GET':
form.note.data = user.get_note(current_user)
return render_template('user/edit_note.html', title=_('Edit note'), form=form, user=user,
menu_topics=menu_topics(), site=g.site)