Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve delete comment/photo implementation #7

Merged
merged 12 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# DATABASE_URL=sqlite:////home/moments/database/data.db
# MAIL_SERVER=smtp.example.com
# MAIL_USERNAME=example
# MAIL_PASSWORD=example-password
36 changes: 31 additions & 5 deletions moments/blueprints/admin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from flask import Blueprint, current_app, flash, render_template, request, abort
from flask import Blueprint, current_app, flash, render_template, request, abort, redirect, url_for
from flask_login import login_required
from sqlalchemy import func, select

Expand Down Expand Up @@ -163,10 +163,12 @@ def manage_photo(order):
per_page = current_app.config['MOMENTS_MANAGE_PHOTO_PER_PAGE']
order_rule = 'flag'
if order == 'by_time':
pagination = db.paginate(select(Photo).order_by(Photo.created_at.desc()), page=page, per_page=per_page)
pagination = db.paginate(select(Photo).order_by(Photo.created_at.desc()), page=page, per_page=per_page, error_out=False)
order_rule = 'time'
else:
pagination = db.paginate(select(Photo).order_by(Photo.flag.desc()), page=page, per_page=per_page)
pagination = db.paginate(select(Photo).order_by(Photo.flag.desc()), page=page, per_page=per_page, error_out=False)
if page > pagination.pages:
return redirect(url_for('.manage_photo', page=pagination.pages, order_rule=order_rule))
photos = pagination.items
return render_template('admin/manage_photo.html', pagination=pagination, photos=photos, order_rule=order_rule)

Expand All @@ -191,9 +193,33 @@ def manage_comment(order):
per_page = current_app.config['MOMENTS_MANAGE_COMMENT_PER_PAGE']
order_rule = 'flag'
if order == 'by_time':
pagination = db.paginate(select(Comment).order_by(Comment.created_at.desc()), page=page, per_page=per_page)
pagination = db.paginate(select(Comment).order_by(Comment.created_at.desc()), page=page, per_page=per_page, error_out=False)
order_rule = 'time'
else:
pagination = db.paginate(select(Comment).order_by(Comment.flag.desc()), page=page, per_page=per_page)
pagination = db.paginate(select(Comment).order_by(Comment.flag.desc()), page=page, per_page=per_page, error_out=False)
if page > pagination.pages:
return redirect(url_for('.manage_comment', page=pagination.pages, order_rule=order_rule))
comments = pagination.items
return render_template('admin/manage_comment.html', pagination=pagination, comments=comments, order_rule=order_rule)


@admin_bp.route('/delete/photo/<int:photo_id>', methods=['POST'])
@login_required
@permission_required('MODERATE')
def delete_photo(photo_id):
photo = db.session.get(Photo, photo_id) or abort(404)
db.session.delete(photo)
db.session.commit()
flash('Photo deleted.', 'info')
return redirect_back()


@admin_bp.route('/delete/comment/<int:comment_id>', methods=['POST'])
@login_required
@permission_required('MODERATE')
def delete_comment(comment_id):
comment = db.session.get(Comment, comment_id) or abort(404)
db.session.delete(comment)
db.session.commit()
flash('Comment deleted.', 'info')
return redirect_back()
3 changes: 2 additions & 1 deletion moments/core/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from moments.core.extensions import db
from moments.models import Role
from moments.lorem import fake_admin, fake_collect, fake_comment, fake_follow, fake_photo, fake_tag, fake_user


def register_commands(app):
Expand Down Expand Up @@ -35,6 +34,8 @@ def init_app_command():
@click.option('--comment', default=100, help='Quantity of comments, default is 100.')
def lorem_command(user, follow, photo, tag, collect, comment):
"""Generate fake data."""
from moments.lorem import fake_admin, fake_collect, fake_comment, fake_follow, fake_photo, fake_tag, fake_user

db.drop_all()
db.create_all()

Expand Down
25 changes: 13 additions & 12 deletions moments/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,26 @@

def register_error_handlers(app):
@app.errorhandler(400)
def bad_request(e):
return render_template('errors/400.html'), 400
def bad_request(error):
return render_template('errors/400.html', description=error.description), 400

@app.errorhandler(403)
def forbidden(e):
return render_template('errors/403.html'), 403
def forbidden(error):
return render_template('errors/403.html', description=error.description), 403

@app.errorhandler(404)
def page_not_found(e):
return render_template('errors/404.html'), 404
def page_not_found(error):
return render_template('errors/404.html', description=error.description), 404

@app.errorhandler(413)
def request_entity_too_large(e):
return render_template('errors/413.html'), 413
def request_entity_too_large(error):
return render_template('errors/413.html', description=error.description), 413

@app.errorhandler(500)
def internal_server_error(e):
return render_template('errors/500.html'), 500
def internal_server_error(error):
return render_template('errors/500.html', description=error.description), 500

@app.errorhandler(CSRFError)
def handle_csrf_error(e):
return render_template('errors/400.html', description=e.description), 500
def handle_csrf_error(error):
description = 'Session expired, return last page and try again.'
return render_template('errors/400.html', description=description), 500
6 changes: 6 additions & 0 deletions moments/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ def _send_async_mail(app, message):


def send_mail(to, subject, template, **kwargs):
if current_app.debug:
current_app.logger.debug('Skip sending email in debug mode.')
current_app.logger.debug(f'To: {to}')
current_app.logger.debug(f'Subject: {subject}')
current_app.logger.debug(f'Template: {template}')
return
message = Message(current_app.config['MOMENTS_MAIL_SUBJECT_PREFIX'] + subject, recipients=[to])
message.body = render_template(template + '.txt', **kwargs)
message.html = render_template(template + '.html', **kwargs)
Expand Down
4 changes: 2 additions & 2 deletions moments/forms/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from flask_wtf.file import FileAllowed, FileField, FileRequired
from sqlalchemy import select
from wtforms import BooleanField, HiddenField, PasswordField, StringField, SubmitField, TextAreaField, ValidationError
from wtforms.validators import DataRequired, Email, EqualTo, Length, Optional, Regexp
from wtforms.validators import DataRequired, URL, Email, EqualTo, Length, Optional, Regexp

from moments.core.extensions import db
from moments.models import User
Expand All @@ -19,7 +19,7 @@ class EditProfileForm(FlaskForm):
Regexp('^[a-zA-Z0-9]*$', message='The username should contain only a-z, A-Z and 0-9.'),
],
)
website = StringField('Website', validators=[Optional(), Length(0, 255)])
website = StringField('Website', validators=[URL(), Optional(), Length(0, 255)])
location = StringField('City', validators=[Optional(), Length(0, 50)])
bio = TextAreaField('Bio', validators=[Optional(), Length(0, 120)])
submit = SubmitField()
Expand Down
16 changes: 12 additions & 4 deletions moments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ class Permission(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(30), unique=True)

roles: Mapped[List['Role']] = relationship(secondary=role_permission, back_populates='permissions')
roles: Mapped[List['Role']] = relationship(
secondary=role_permission,
back_populates='permissions',
passive_deletes=True
)

def __repr__(self):
return f'Permission {self.id}: {self.name}'
Expand All @@ -36,8 +40,12 @@ class Role(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(30), unique=True)

users: WriteOnlyMapped['User'] = relationship(back_populates='role')
permissions: Mapped[List['Permission']] = relationship(secondary=role_permission, back_populates='roles')
users: WriteOnlyMapped['User'] = relationship(back_populates='role', passive_deletes=True)
permissions: Mapped[List['Permission']] = relationship(
secondary=role_permission,
back_populates='roles',
passive_deletes=True
)

@staticmethod
def init_role():
Expand Down Expand Up @@ -293,7 +301,7 @@ class Photo(db.Model):
collections: WriteOnlyMapped['Collection'] = relationship(
back_populates='photo', cascade='all, delete-orphan', passive_deletes=True
)
tags: Mapped[List['Tag']] = relationship(secondary=photo_tag, back_populates='photos')
tags: Mapped[List['Tag']] = relationship(secondary=photo_tag, back_populates='photos', passive_deletes=True)

@property
def collectors_count(self):
Expand Down
2 changes: 1 addition & 1 deletion moments/templates/admin/manage_comment.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ <h1>Comments
<td><span class="dayjs" data-format="LL">{{ comment.created_at }}</span></td>
<td>
<form class="inline" method="post"
action="{{ url_for('main.delete_comment', comment_id=comment.id, next=request.full_path) }}">
action="{{ url_for('admin.delete_comment', comment_id=comment.id, next=request.full_path) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Are you sure?');">Delete
</button>
Expand Down
2 changes: 1 addition & 1 deletion moments/templates/admin/manage_photo.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ <h1>Photos
<td><span class="dayjs" data-format="LL">{{ photo.created_at }}</span></td>
<td>
<form class="inline" method="post"
action="{{ url_for('main.delete_photo', photo_id=photo.id, next=request.full_path) }}">
action="{{ url_for('admin.delete_photo', photo_id=photo.id, next=request.full_path) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Are you sure?');">Delete
</button>
Expand Down
2 changes: 2 additions & 0 deletions moments/templates/admin/manage_user.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ <h1>Users
<tr>
<th>Avatars</th>
<th>Name/username</th>
<th>Email</th>
<th>Role</th>
<th>Bio</th>
<th>City</th>
Expand All @@ -58,6 +59,7 @@ <h1>Users
<tr>
<td><img src="{{ url_for('main.get_avatar', filename=user.avatar_s) }}"></td>
<td>{{ user.name }}<br>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{{ user.role.name }}</td>
<td>{{ user.bio }}</td>
<td>{{ user.location }}</td>
Expand Down
2 changes: 1 addition & 1 deletion moments/templates/errors/403.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<img class="card-img-top" src="{{ url_for('static', filename='images/error.jpg') }}">
<div class="card-body">
<h5 class="card-title">403 Error</h5>
<p class="card-text">Forbidden</p>
<p class="card-text">{{ description|default('Forbidden') }}</p>
</div>
<div class="card-footer">
<div class="float-end">
Expand Down
2 changes: 1 addition & 1 deletion moments/templates/errors/404.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<img class="card-img-top" src="{{ url_for('static', filename='images/error.jpg') }}">
<div class="card-body">
<h5 class="card-title">404 Error</h5>
<p class="card-text">Page Not Found</p>
<p class="card-text">{{ description|default('Page Not Found') }}</p>
</div>
<div class="card-footer">
<div class="float-end">
Expand Down
2 changes: 1 addition & 1 deletion moments/templates/errors/413.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<img class="card-img-top" src="{{ url_for('static', filename='images/error.jpg') }}">
<div class="card-body">
<h5 class="card-title">413 Error</h5>
<p class="card-text">File Too Large</p>
<p class="card-text">{{ description|default('File Too Large') }}</p>
</div>
<div class="card-footer">
<div class="float-end">
Expand Down
2 changes: 1 addition & 1 deletion moments/templates/errors/500.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<img class="card-img-top" src="{{ url_for('static', filename='images/error.jpg') }}">
<div class="card-body">
<h5 class="card-title">500 Error</h5>
<p class="card-text">Internal Server Error</p>
<p class="card-text">{{ description|default('Internal Server Error') }}</p>
</div>
<div class="card-footer">
<div class="float-end">
Expand Down
4 changes: 2 additions & 2 deletions moments/templates/main/_comment.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ <h4>{{ photo.comments_count }} Comments
<a href="{{ url_for('user.index', username=comment.author.username) }}">
<img class="rounded img-fluid avatar-s profile-popover"
data-href="{{ url_for('ajax.get_profile', user_id=comment.author.id) }}"
src="{{ url_for('main.get_avatar', filename=comment.author.avatar_s) }}">
src="{{ url_for('main.get_avatar', filename=comment.author.avatar_m) }}">
</a>
</div>
<div class="comment-body">
Expand Down Expand Up @@ -96,7 +96,7 @@ <h6>
{% endif %}
<div class="comment-form-area">
<div class="comment-form-thumbnail">
<img class="rounded img-fluid avatar-s" src="{{ url_for('main.get_avatar', filename=current_user.avatar_s) }}">
<img class="rounded img-fluid avatar-s" src="{{ url_for('main.get_avatar', filename=current_user.avatar_m) }}">
</div>
<div class="comment-form" id="comment-form">
{{ render_form(comment_form, action=url_for('.new_comment', photo_id=photo.id, page=pagination.pages or 1,
Expand Down
2 changes: 1 addition & 1 deletion moments/templates/main/_sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div class="col">
<a href="{{ url_for('user.index', username=current_user.username) }}">
<img class="rounded avatar-s"
src="{{ url_for('main.get_avatar', filename=current_user.avatar_s) }}">
src="{{ url_for('main.get_avatar', filename=current_user.avatar_m) }}">
</a>
</div>
<div class="col">
Expand Down
2 changes: 1 addition & 1 deletion moments/templates/main/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<a class="dead-link" href="{{ url_for('user.index', username=photo.author.username) }}">
<img class="rounded img-fluid avatar-s profile-popover"
data-href="{{ url_for('ajax.get_profile', user_id=photo.author.id) }}"
src="{{ url_for('main.get_avatar', filename=photo.author.avatar_s) }}">
src="{{ url_for('main.get_avatar', filename=photo.author.avatar_m) }}">
</a>
<a class="profile-popover trend-card-avatar text-decoration-none"
data-href="{{ url_for('ajax.get_profile', user_id=photo.author.id) }}"
Expand Down
2 changes: 1 addition & 1 deletion moments/templates/main/profile_popup.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div class="popup-card">
<img class="rounded img-fluid avatar-s popup-avatar" src="{{ url_for('main.get_avatar', filename=user.avatar_s) }}">
<img class="rounded img-fluid avatar-s popup-avatar" src="{{ url_for('main.get_avatar', filename=user.avatar_m) }}">
<div class="popup-profile">
<h6>{{ user.name }}</h6>
<p class="text-muted">{{ user.username }}
Expand Down
10 changes: 6 additions & 4 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 19 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ cfgv==3.4.0 \
click==8.1.7 \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
colorama==0.4.6; platform_system == "Windows" \
colorama==0.4.6; sys_platform == "win32" or platform_system == "Windows" \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
distlib==0.3.8 \
Expand Down Expand Up @@ -86,8 +86,9 @@ flask-dropzone==2.0.0 \
flask-login==0.6.3 \
--hash=sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333 \
--hash=sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d
flask-mail==0.9.1 \
--hash=sha256:22e5eb9a940bf407bcf30410ecc3708f3c56cc44b29c34e1726fe85006935f41
flask-mail==0.10.0 \
--hash=sha256:44083e7b02bbcce792209c06252f8569dd5a325a7aaa76afe7330422bd97881d \
--hash=sha256:a451e490931bb3441d9b11ebab6812a16bfa81855792ae1bf9c1e1e22c4e51e7
flask-sqlalchemy==3.1.1 \
--hash=sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0 \
--hash=sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312
Expand Down Expand Up @@ -138,6 +139,9 @@ idna==3.6 \
importlib-metadata==8.0.0; python_version < "3.10" \
--hash=sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f \
--hash=sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812
iniconfig==2.0.0 \
--hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
--hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
itsdangerous==2.1.2 \
--hash=sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44 \
--hash=sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a
Expand Down Expand Up @@ -182,6 +186,9 @@ nodeenv==1.9.1 \
outcome==1.3.0.post0 \
--hash=sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8 \
--hash=sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b
packaging==23.2 \
--hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \
--hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7
pillow==10.2.0 \
--hash=sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac \
--hash=sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e \
Expand Down Expand Up @@ -233,6 +240,9 @@ pillow==10.2.0 \
platformdirs==4.2.2 \
--hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \
--hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3
pluggy==1.4.0 \
--hash=sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981 \
--hash=sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be
pre-commit==3.5.0 \
--hash=sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32 \
--hash=sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660
Expand All @@ -245,6 +255,9 @@ pyjwt==2.8.0 \
pysocks==1.7.1 \
--hash=sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5 \
--hash=sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0
pytest==8.1.1 \
--hash=sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7 \
--hash=sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044
python-dateutil==2.8.2 \
--hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
--hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
Expand Down Expand Up @@ -348,6 +361,9 @@ sqlalchemy==2.0.27 \
--hash=sha256:eb15ef40b833f5b2f19eeae65d65e191f039e71790dd565c2af2a3783f72262f \
--hash=sha256:f9374e270e2553653d710ece397df67db9d19c60d2647bcd35bfc616f1622dcd \
--hash=sha256:fa67d821c1fd268a5a87922ef4940442513b4e6c377553506b9db3b83beebbd8
tomli==2.0.1; python_version < "3.11" \
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
trio==0.25.0 \
--hash=sha256:9b41f5993ad2c0e5f62d0acca320ec657fdb6b2a2c22b8c7aed6caf154475c4e \
--hash=sha256:e6458efe29cc543e557a91e614e2b51710eba2961669329ce9c862d50c6e8e81
Expand Down
Loading