From afe26e10e3b0ee5bc860f4bd7277b38f8433535e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ku=C5=BE=C3=ADlek?= Date: Tue, 10 Mar 2020 13:36:43 +0100 Subject: [PATCH] User settings, moving endpoints, restructuring forms and some other BS --- migrations/versions/b1a3eed8e38f_.py | 47 ++++++++++++ yadc/bp/manage.py | 15 +++- yadc/bp/post.py | 41 ++++------- yadc/bp/user.py | 103 +++++++++++++++++++++++---- yadc/forms.py | 40 +++++++++-- yadc/models.py | 82 ++++++++++++--------- yadc/templates/manage/users.html | 4 +- yadc/templates/post/index.html | 2 +- yadc/templates/post/post.html | 12 ++-- yadc/templates/user/profile.html | 2 +- yadc/templates/user/settings.html | 70 +++++++++++++++--- 11 files changed, 323 insertions(+), 95 deletions(-) create mode 100644 migrations/versions/b1a3eed8e38f_.py diff --git a/migrations/versions/b1a3eed8e38f_.py b/migrations/versions/b1a3eed8e38f_.py new file mode 100644 index 0000000..4fb947e --- /dev/null +++ b/migrations/versions/b1a3eed8e38f_.py @@ -0,0 +1,47 @@ +"""empty message + +Revision ID: b1a3eed8e38f +Revises: f4e1b4727000 +Create Date: 2020-03-09 23:58:53.716242 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utc + + +# revision identifiers, used by Alembic. +revision = 'b1a3eed8e38f' +down_revision = 'f4e1b4727000' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user_tags_blacklist', + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('tag_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ) + ) + op.add_column('user', sa.Column('ban_reason', sa.String(length=512), nullable=True)) + op.add_column('user', sa.Column('ban_until', sqlalchemy_utc.sqltypes.UtcDateTime(timezone=True), nullable=True)) + op.add_column('user', sa.Column('biography', sa.String(length=512), nullable=True)) + op.add_column('user', sa.Column('rating', sa.Enum('safe', 'questionable', 'explicit', name='rating'), server_default='safe', nullable=False)) + + op.alter_column('user', 'op_level', server_default='user') + op.alter_column('user', 'user_status', server_default='active') + op.alter_column('post', 'status', server_default='pending') + op.alter_column('tag', 'category', server_default='general') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'rating') + op.drop_column('user', 'biography') + op.drop_column('user', 'ban_until') + op.drop_column('user', 'ban_reason') + op.drop_table('user_tags_blacklist') + # ### end Alembic commands ### diff --git a/yadc/bp/manage.py b/yadc/bp/manage.py index b1322f8..1e80955 100644 --- a/yadc/bp/manage.py +++ b/yadc/bp/manage.py @@ -114,10 +114,11 @@ def modify_post(): pass else: el = Post.query.filter_by(id=form.id.data).first() - if not current_user.is_moderator or not el.author.is_current: + if not current_user.is_moderator and not (el.author.is_current if el.author is not None else None): flash("You don't have sufficient rights to do this.") return redirect(url_for('main.index')) if form.delete.data: + el.remove_image_files() db.session.delete(el) db.session.commit() flash('{} deleted.'.format(str(el))) @@ -128,6 +129,16 @@ def modify_post(): db.session.commit() flash('Changes to {} have been applied.'.format(str(el))) + elif form.approve.data: + if not current_user.is_moderator: + flash("You don't have sufficient rights to do this.") + return redirect(url_for('main.index')) + post.status = POST_STATUS.active + post.approver = current_user + + db.session.commit() + flash('Approved post {}'.format(str(post))) + redirect(url_for('post.post_show', id=post.id)) return redirect(url_for('.manage_posts')) @@ -179,7 +190,7 @@ def modify_comment(): return redirect(url_for('post.post_show', id=form.post_id.data)) else: el = Comment.query.filter_by(id=form.id.data).first() - if not current_user.is_moderator or not el.user.is_current: + if not current_user.is_moderator and not (el.user.is_current if el.user is not None else None): flash("You don't have sufficient rights to do this.") return redirect(url_for('main.index')) if form.delete.data: diff --git a/yadc/bp/post.py b/yadc/bp/post.py index 01e32fc..1fabad1 100644 --- a/yadc/bp/post.py +++ b/yadc/bp/post.py @@ -4,13 +4,12 @@ import os from flask import (Blueprint, abort, current_app, flash, redirect, render_template, request, send_from_directory, url_for, session, jsonify) from flask_login import current_user, login_required -from PIL import Image from sqlalchemy import func from sqlalchemy.orm import aliased from yadc import db from yadc.forms import UploadForm, CommentForm, PostForm -from yadc.models import FILETYPE, RATING, Post, Tag, Comment +from yadc.models import FILETYPE, RATING, POST_STATUS, Post, Tag, Comment, moderator_required from yadc.utils import query_replace bp = Blueprint('post', __name__) @@ -35,6 +34,10 @@ def posts(page): f_rating = {r.name : r for r in RATING}.get(request.args.get('rating'), RATING.safe) m_ratings = f_rating.matched + # filter user's blacklist + if current_user.is_authenticated: + f_tags = list(t for t in f_tags if t not in [tb.content for tb in current_user.tag_blacklist]) + posts_query = Post.query if f_tags: posts_query = posts_query.join(Post.tags).group_by(Post.id).filter(Tag.content.in_(f_tags)).having(func.count(Post.id)==len(f_tags)) @@ -66,6 +69,11 @@ from yadc.bp import manage def comment(): return manage.modify_comment() +@bp.route('/editpost', methods=['POST']) +@login_required +def editpost(): + return manage.modify_post() + @bp.route('/upload', methods=['GET', 'POST']) @login_required def upload(): @@ -78,33 +86,12 @@ def upload(): tags = Tag.query.filter(Tag.content.in_(f_tags)).all() post = Post(file, source=form.sauce.data, tags=tags, rating=RATING[form.rating.data], author=current_user) - - - with open(post.image_path, "wb") as f: - f.write(file.data.getbuffer()) - # file.seek(0) - # file.save(post.image_path) - - with Image.open(file.data) as im: - im = im.convert('RGB') - - if post.jpeg_path is not None: - im.save(post.jpeg_path, 'JPEG', quality=80) - - sim = im.copy() - sim.thumbnail([800,800]) - sim.save(post.sample_path, 'JPEG', quality=80) - - im.thumbnail([512,512]) - im.save(post.thumb_path, 'JPEG', quality=80) - db.session.add(post) db.session.commit() flash('Successfully submitted {}'.format(str(post))) - #return redirect(url_for('.upload')) return redirect(url_for('.posts')) return render_template('post/upload.html', form=form) @@ -151,21 +138,21 @@ def posts_api(): source=p.source, md5=p.md5, file_size=p.filesize, - file_url=p.url(path=p.image_url, endpoint='img'), - preview_url=p.url(path=p.image_url, endpoint='thumb'), + file_url=p.url(path=p.file_uri, endpoint='img'), + preview_url=p.url(path=p.file_uri, endpoint='thumb'), preview_width=0, preview_height=0, actual_preview_width=0, actual_preview_height=0, - sample_url=p.url(path=p.image_url, endpoint='sample'), + sample_url=p.url(path=p.file_uri, endpoint='sample'), sample_width=0, sample_height=0, sample_file_size=0, - jpeg_url=p.url(path=p.image_url, endpoint='img') if p.filetype is FILETYPE.jpeg else p.url(path=p.image_url, endpoint='jpeg'), + jpeg_url=p.url(path=p.file_uri, endpoint='img') if p.filetype is FILETYPE.jpeg else p.url(path=p.file_uri, endpoint='jpeg'), jpeg_width=0, jpeg_height=0, diff --git a/yadc/bp/user.py b/yadc/bp/user.py index dbfc87a..66d37ac 100644 --- a/yadc/bp/user.py +++ b/yadc/bp/user.py @@ -1,10 +1,10 @@ from flask import (Blueprint, abort, current_app, flash, redirect, render_template, request, send_from_directory, url_for) from flask_login import current_user, login_required -from yadc.forms import ChangePassForm +from yadc.forms import ChangeUserInfoForm, ChangePassForm, ChangeMailForm, ChangeUserRatingForm, ChangeTagBlacklistForm, DeleteUserDataForm from yadc import db -from yadc.models import User +from yadc.models import User, Post, Comment bp = Blueprint('user', __name__) @@ -15,23 +15,102 @@ def profile(username): if user is not None: return render_template('user/profile.html', user=user) - return "FUCK YOU, THIS USER DOES NOT EXIST" + return redirect(url_for('main.index')) -@bp.route('/settings', methods=['GET','POST']) +@bp.route('/settings') @login_required def settings(): - form = ChangePassForm(request.form) - if request.method == 'POST' and form.validate(): - user = current_user + return render_template( + 'user/settings.html', + userinfo_form=ChangeUserInfoForm(bio=current_user.biography), + pass_form=ChangePassForm(), + mail_form=ChangeMailForm(), + rating_form=ChangeUserRatingForm(rating=current_user.rating.name), + tags_form=ChangeTagBlacklistForm(), + delete_form=DeleteUserDataForm() + ) + +@bp.route('/change_info', methods=['POST']) +@login_required +def change_info(): + form = ChangeUserInfoForm(request.form) + if form.validate(): + current_user.biography = form.bio.data + + db.session.commit() + flash('Your biography was updated.') + + return redirect(url_for('.settings')) - if not user.check_password(form.password_current.data): +@bp.route('/change_pass', methods=['POST']) +@login_required +def change_pass(): + form = ChangePassForm(request.form) + if form.validate(): + if not current_user.check_password(form.password_current.data): flash('Incorrect password') return redirect(url_for('.settings')) - user.create_password(form.password.data) - db.session.commit() + current_user.create_password(form.password.data) + db.session.commit() flash('Password changed successfully.') - return redirect(url_for('.settings')) - return render_template('user/settings.html', form=form) + return redirect(url_for('.settings')) + +@bp.route('/change_mail', methods=['POST']) +@login_required +def change_mail(): + form = ChangeMailForm(request.form) + if form.validate(): + current_user.email = form.email.data + + db.session.commit() + flash('Your email was updated.') + + return redirect(url_for('.settings')) + +@bp.route('/change_rating', methods=['POST']) +@login_required +def change_rating(): + form = ChangeUserRatingForm(request.form) + if form.validate(): + current_user.rating = form.rating.data + + db.session.commit() + flash('Your rating preference was updated.') + + return redirect(url_for('.settings')) + +@bp.route('/change_tagblacklist', methods=['POST']) +@login_required +def change_tagblacklist(): + form = ChangeTagBlacklistForm(request.form) + if form.validate(): + f_tags = form.tags.data.split() + tags = Tag.query.filter(Tag.content.in_(f_tags)).all() + current_user.tag_blacklist = tags + + db.session.commit() + flash('Your tag blacklist was updated.') + + return redirect(url_for('.settings')) + +@bp.route('/delete_data', methods=['POST']) +@login_required +def delete_data(): + form = DeleteUserDataForm(request.form) + if form.validate(): + if form.all_posts: + Post.query.filter_by(author_id=current_user.id).delete() + + if form.all_comments: + Comment.query.filter_by(user_id=current_user.id).delete() + + db.session.delete(current_user) + db.session.commit() + flash('Thank you for using our service.') + + return redirect(url_for('main.index')) + + return redirect(url_for('.settings')) \ No newline at end of file diff --git a/yadc/forms.py b/yadc/forms.py index 9430d44..fa40844 100644 --- a/yadc/forms.py +++ b/yadc/forms.py @@ -7,6 +7,8 @@ from werkzeug.utils import cached_property from flask import current_app from flask_wtf.csrf import _FlaskFormCSRF +from yadc.models import USER_STATUS, OP_LEVEL, RATING, POST_STATUS, TAG_CATEGORY + class CSRFForm(Form): class Meta: csrf = True @@ -66,8 +68,8 @@ class UploadForm(CSRFForm): sauce = StringField('Sauce', validators=[DataRequired()], render_kw={'placeholder':'Source URL','autocomplete':'off'}) tags = StringField('Tags', validators=[DataRequired()], render_kw={'placeholder':'Tags','autocomplete':'off'}) # CUSTOM VALIDATOR (also for Post edits) rating = RadioField('Rating', - choices=[('safe', 'Safe'), ('questionable', 'Questionable'), ('explicit', 'Explicit')], - default='safe', + choices=[(e.name, e.name.capitalize()) for e in RATING], + default=RATING.safe.name, validators=[DataRequired()]) submit = SubmitField('Upload') @@ -80,15 +82,43 @@ class UploadForm(CSRFForm): if client_mimetype not in ['image/png','image/jpeg']: raise ValidationError('Please select an image file of PNG or JPEG format') +# Change user section +class ChangeUserInfoForm(CSRFForm): + bio = TextAreaField('Biography') + userinfo_submit = SubmitField('Change your info') class ChangePassForm(CSRFForm): password_current = PasswordField('Current password', validators=[DataRequired()]) password = PasswordField('Password', validators=[DataRequired()]) password_again = PasswordField('Repeat password', validators=[DataRequired(), EqualTo('password')]) - submit = SubmitField('Change password') + pass_submit = SubmitField('Change password') +class ChangeMailForm(CSRFForm): + email = StringField('E-mail', validators=[DataRequired(), Email()], render_kw=dict(placeholder="E-mail")) + email_again = StringField('Repeat e-mail', validators=[DataRequired(), EqualTo('email')], render_kw=dict(placeholder="Repeat e-mail")) + mail_submit = SubmitField('Change email') + +class ChangeUserRatingForm(CSRFForm): + rating = SelectField('Rating', + choices=[(e.name, e.name.capitalize()) for e in RATING], + validators=[DataRequired()], + render_kw=dict(onchange="submit()")) + rating_submit = SubmitField('Change default rating') + +class ChangeTagBlacklistForm(CSRFForm): + tags = StringField('Tags', render_kw={'placeholder':'Tags','autocomplete':'off'}) + tags_submit = SubmitField('Change blacklisted tags', validators=[DataRequired()]) + +class DeleteUserDataForm(CSRFForm): + all_posts = BooleanField('Delete all posts') + all_comments = BooleanField('Delete all comments') + + delete_submit = SubmitField( + 'Delete your data', + validators=[DataRequired()], + render_kw=dict(onclick="return confirm('Do you really want to delete all your data?')") + ) -from yadc.models import USER_STATUS, OP_LEVEL, RATING, POST_STATUS, TAG_CATEGORY class EditForm(CSRFForm): id = HiddenField('ID') @@ -125,6 +155,8 @@ class PostForm(EditForm): validators=[optional()]) source = StringField('Source', render_kw={'autocomplete':'off'}) + approve = SubmitField('Approve') + class TagForm(EditForm): content = StringField('Content', validators=[validate_create_required], render_kw={'autocomplete':'off'}) category = SelectField('Category', diff --git a/yadc/models.py b/yadc/models.py index e5a58d1..f3af26e 100644 --- a/yadc/models.py +++ b/yadc/models.py @@ -70,21 +70,27 @@ class TimestampMixin(object): def natural_updated(self): return humanize.naturaltime(datetime.now().astimezone(self.updated.tzinfo) - self.updated) +user_tags_blacklist = db.Table('user_tags_blacklist', db.metadata, + db.Column('user_id', db.Integer, db.ForeignKey('user.id')), + db.Column('tag_id', db.Integer, db.ForeignKey('tag.id')) +) + class User(UserMixin, TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(128), unique=True, nullable=False) email = db.Column(db.String(256), unique=True) pass_hash = db.Column(db.String(128)) - op_level = db.Column(db.Enum(OP_LEVEL), default=OP_LEVEL.user, nullable=False) - user_status = db.Column(db.Enum(USER_STATUS), default=USER_STATUS.active, nullable=False) + op_level = db.Column(db.Enum(OP_LEVEL), server_default=OP_LEVEL.user.name, nullable=False) + user_status = db.Column(db.Enum(USER_STATUS), server_default=USER_STATUS.active.name, nullable=False) last_login = db.Column(UtcDateTime) - ban_status = None - ban_until = None - ban_reason = None + ban_until = db.Column(UtcDateTime) + ban_reason = db.Column(db.String(512)) + + biography = db.Column(db.String(512)) - #authored_posts = db.relationship('Post', back_populates='author') - #approved_posts = db.relationship('Post', back_populates='approver') + rating = db.Column(db.Enum(RATING), server_default=RATING.safe.name, nullable=False) + tag_blacklist = db.relationship('Tag', secondary=user_tags_blacklist, backref=db.backref('user_blacklisted')) def __repr__(self): return ''.format(self.username) @@ -93,8 +99,10 @@ class User(UserMixin, TimestampMixin, db.Model): self.pass_hash = generate_password_hash(password, salt_length=16) def check_password(self, password): + # if self.pass_hash is None: + # return True if self.pass_hash is None: - return True + return False return check_password_hash(self.pass_hash, password) def login(self, remember): @@ -145,9 +153,6 @@ def admin_required(func): # class UserPreferences(db.Model): # user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) # user = db.relationship('User', backref=db.backref('profile', uselist=False)) - -# rating = db.Column(db.Enum(RATING), default=RATING.safe, nullable=False) -# tag_blacklist = None post_tags = db.Table('post_tags', db.metadata, @@ -160,7 +165,7 @@ class Post(TimestampMixin, db.Model): md5 = db.Column(db.String(32), unique=True, nullable=False) filetype = db.Column(db.Enum(FILETYPE), nullable=False) rating = db.Column(db.Enum(RATING), nullable=False) - status = db.Column(db.Enum(POST_STATUS), default=POST_STATUS.pending, nullable=False) + status = db.Column(db.Enum(POST_STATUS), server_default=POST_STATUS.pending.name, nullable=False) width = db.Column(db.Integer, default=0) height = db.Column(db.Integer, default=0) @@ -189,6 +194,8 @@ class Post(TimestampMixin, db.Model): self.origin_filename = file.filename + self.generate_image_files(file) + def __repr__(self): return(''.format(self.id, self.author, self.filetype.name, self.filesize)) @@ -197,7 +204,7 @@ class Post(TimestampMixin, db.Model): return "flex: {0:.2f} 1 {0:.2f}px;".format(self.width/self.height*current_app.config.get('POST_LIST_THUMB_HEIGHT', 240)) @property - def image_url(self): + def file_uri(self): # filename = "{}.{}".format('maybe_later_generated_cute_filename', 'jpg' if self.filetype is FILETYPE.jpeg else 'png') # filename = 'maybe_later_generated_cute_filename' filename = "{} - {} {}".format(current_app.config.get('INSTANCE_NAME'), self.id, " ".join(tag.content.replace(' ', '_') for tag in self.tags)) @@ -213,29 +220,40 @@ class Post(TimestampMixin, db.Model): elif endpoint == 'thumb': return url_for('main.uploaded_thumb', path="{}.{}".format(path,'jpg')) - @property - def image_path(self): - filename = "{}.{}".format(self.md5, 'jpg' if self.filetype is FILETYPE.jpeg else 'png') - return os.path.join(current_app.config.get('POST_UPLOADS'), 'img', filename) - @property - def jpeg_path(self): - if self.filetype is FILETYPE.jpeg: - return None + def generate_image_files(self, file): + with open(self.image_files['image'], "wb") as f: + f.write(file.data.getbuffer()) + # file.seek(0) + # file.save(post.image_path) + + with Image.open(file.data) as im: + im = im.convert('RGB') - jpeg_filename = "{}.{}".format(self.md5, 'jpg') - return os.path.join(current_app.config.get('POST_UPLOADS'), 'jpeg', jpeg_filename) + if self.image_files['jpeg'] is not None: + im.save(self.image_files['jpeg'], 'JPEG', quality=80) + + sim = im.copy() + sim.thumbnail([800,800]) + sim.save(self.image_files['sample'], 'JPEG', quality=80) - @property - def sample_path(self): - jpeg_filename = "{}.{}".format(self.md5, 'jpg') - return os.path.join(current_app.config.get('POST_UPLOADS'), 'sample', jpeg_filename) + sim = im.copy() + sim.thumbnail([512,512]) + sim.save(self.image_files['thumb'], 'JPEG', quality=80) + + def remove_image_files(self): + for im in self.image_files.values(): + os.remove(im) @property - def thumb_path(self): - jpeg_filename = "{}.{}".format(self.md5, 'jpg') - return os.path.join(current_app.config.get('POST_UPLOADS'), 'thumb', jpeg_filename) - + def image_files(self): + return dict( + image=os.path.join(current_app.config.get('POST_UPLOADS'), 'img', "{}.{}".format(self.md5, 'jpg' if self.filetype is FILETYPE.jpeg else 'png')), + jpeg=(None if self.filetype is FILETYPE.jpeg else os.path.join(current_app.config.get('POST_UPLOADS'), 'jpeg', "{}.{}".format(self.md5, 'jpg'))), + sample=os.path.join(current_app.config.get('POST_UPLOADS'), 'sample', "{}.{}".format(self.md5, 'jpg')), + thumb=os.path.join(current_app.config.get('POST_UPLOADS'), 'thumb', "{}.{}".format(self.md5, 'jpg')) + ) + @cached_property def image_resolution(self): return "{}x{}".format(self.width, self.height) @@ -257,7 +275,7 @@ class Post(TimestampMixin, db.Model): class Tag(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) content = db.Column(db.String(128), unique=True, nullable=False) - category = db.Column(db.Enum(TAG_CATEGORY), default=TAG_CATEGORY.general, nullable=False) + category = db.Column(db.Enum(TAG_CATEGORY), server_default=TAG_CATEGORY.general.name, nullable=False) @property def content_deser(self): diff --git a/yadc/templates/manage/users.html b/yadc/templates/manage/users.html index be01190..8b442b6 100644 --- a/yadc/templates/manage/users.html +++ b/yadc/templates/manage/users.html @@ -30,7 +30,9 @@ - + diff --git a/yadc/templates/post/index.html b/yadc/templates/post/index.html index 0ec5253..b5cc266 100644 --- a/yadc/templates/post/index.html +++ b/yadc/templates/post/index.html @@ -9,7 +9,7 @@ {% for post in posts.items %}
- +
{% endfor %} diff --git a/yadc/templates/post/post.html b/yadc/templates/post/post.html index 2290560..2fd335b 100644 --- a/yadc/templates/post/post.html +++ b/yadc/templates/post/post.html @@ -21,10 +21,10 @@
File size: {{ post.filesize_human }}
Image res: {{ post.image_resolution }}
- + {% if post.filetype.name == 'png' %} - + {% endif %} @@ -33,7 +33,7 @@ {% if post.can_edit %}

Edit

-
+ {{ editform.csrf_token }} {{ editform.id() }}
@@ -54,7 +54,7 @@ {% block main_content %}
- +
{% endblock %} @@ -77,7 +77,7 @@ {% if not comment.deleted %} - + {{ comment.editform.csrf_token }} {{ comment.editform.id() }}

{{ comment.content }}

@@ -103,7 +103,7 @@ {% if current_user.is_authenticated %}

Reply

- + {{ comment_form.csrf_token }} {{ comment_form.post_id() }}
diff --git a/yadc/templates/user/profile.html b/yadc/templates/user/profile.html index 85d8cf9..8fb2e64 100644 --- a/yadc/templates/user/profile.html +++ b/yadc/templates/user/profile.html @@ -6,5 +6,5 @@ {% endblock %} {% block main_content %} - +

{{ user.biography }}

{% endblock %} \ No newline at end of file diff --git a/yadc/templates/user/settings.html b/yadc/templates/user/settings.html index ffd834a..8d74db3 100644 --- a/yadc/templates/user/settings.html +++ b/yadc/templates/user/settings.html @@ -3,23 +3,75 @@ {% block main_content %}
- + +

Change user info

+ {{ userinfo_form.csrf_token }} +
+ {{ userinfo_form.bio() }} + {{ errors(userinfo_form.bio) }} +
+
+ {{ userinfo_form.userinfo_submit() }} +
+ +

Change password

- {{ form.csrf_token }} + {{ pass_form.csrf_token }} +
+ {{ pass_form.password_current(placeholder="Current password") }} + {{ errors(pass_form.password_current) }} +
+
+ {{ pass_form.password(placeholder="Password") }} + {{ errors(pass_form.password) }} +
+
+ {{ pass_form.password_again(placeholder="Repeat password") }} + {{ errors(pass_form.password_again) }} +
+
+ {{ pass_form.pass_submit() }} +
+
+
+

Change e-mail address

+ {{ mail_form.csrf_token }} +
+ {{ mail_form.email() }} + {{ errors(mail_form.email) }} +
+
+ {{ mail_form.email_again() }} + {{ errors(mail_form.email_again) }} +
- {{ form.password_current(placeholder="Current password") }} - {{ errors(form.password_current) }} + {{ mail_form.mail_submit() }}
+
+
+

Preferred rating

+ {{ rating_form.csrf_token }} +
+ {{ rating_form.rating() }} + {{ errors(rating_form.rating) }} +
+
+ {{ rating_form.rating_submit() }} +
+
+
+

Delete user data

+ {{ delete_form.csrf_token }}
- {{ form.password(placeholder="Password") }} - {{ errors(form.password) }} + {{ delete_form.all_comments() }}{{ delete_form.all_comments.label }} + {{ errors(delete_form.all_posts) }}
- {{ form.password_again(placeholder="Repeat password") }} - {{ errors(form.password_again) }} + {{ delete_form.all_posts() }}{{ delete_form.all_posts.label }} + {{ errors(delete_form.all_posts) }}
- {{ form.submit() }} + {{ delete_form.delete_submit() }}