diff --git a/Pipfile b/Pipfile index 1792fe8..c4be5f0 100644 --- a/Pipfile +++ b/Pipfile @@ -26,6 +26,7 @@ sqlalchemy-utc = "*" flask-admin = "*" sqlalchemy-utils = "*" werkzeug = "==0.16.1" +humanize = "*" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index d1e5771..00aacc6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "90470384a8160ec4176abf18d39b07271e755ae437ead34f7b70d914c514d709" + "sha256": "4fcb2df52789ae242fc5ab588bcbaad8232386d4388b1c72d16dc7ade9a7ba30" }, "pipfile-spec": 6, "requires": { @@ -52,10 +52,11 @@ }, "flask-admin": { "hashes": [ - "sha256:ed7b256471dba0f3af74f1a315733c3b36244592f2002c3bbdc65fd7c2aa807a" + "sha256:aeebf87b5d5fd18525c876e0ecd71e0d0837614610122be235375c411b2d59dc", + "sha256:ff8270de5e8916541d19a0b03e469e2f8bbd22e4952c17aebc605112976f2fc4" ], "index": "pypi", - "version": "==1.5.4" + "version": "==1.5.5" }, "flask-assets": { "hashes": [ @@ -67,10 +68,11 @@ }, "flask-login": { "hashes": [ - "sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec" + "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b", + "sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0" ], "index": "pypi", - "version": "==0.4.1" + "version": "==0.5.0" }, "flask-mail": { "hashes": [ @@ -103,6 +105,14 @@ "index": "pypi", "version": "==0.14.3" }, + "humanize": { + "hashes": [ + "sha256:3478104dcb9e111991ad141b15c9bf9522aa00ccfc5144561d639b3372e1d064", + "sha256:38ace9b66bcaeb7f8186b9dbf0b3448e00148e5b4fbaf726f96c789e52c3e741" + ], + "index": "pypi", + "version": "==1.0.0" + }, "itsdangerous": { "hashes": [ "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", diff --git a/yadc/__init__.py b/yadc/__init__.py index dfeaf58..631b919 100644 --- a/yadc/__init__.py +++ b/yadc/__init__.py @@ -30,6 +30,7 @@ def create_app(): INSTANCE_NAME='YaDc', POSTS_PER_PAGE=8, MANAGE_USERS_PER_PAGE=2, + MANAGE_POSTS_PER_PAGE=2, SQLALCHEMY_ECHO=True, ) diff --git a/yadc/bp/post.py b/yadc/bp/post.py index 47eeafa..d6b822e 100644 --- a/yadc/bp/post.py +++ b/yadc/bp/post.py @@ -65,7 +65,7 @@ def posts(page): return render_template('post/index.html', posts=posts.items, tags=tags, pagination=posts) -@bp.route('/show/') +@bp.route('/show/') def post_show(id): post = Post.query.filter_by(id=id).first() # flash(post) diff --git a/yadc/bp/user.py b/yadc/bp/user.py index 7f9dc08..bcca5d5 100644 --- a/yadc/bp/user.py +++ b/yadc/bp/user.py @@ -1,14 +1,19 @@ 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 ChangePassForm, EditUserForm, EditPostForm -from yadc.models import User +from yadc import db +from yadc.models import User, USER_STATUS, Post bp = Blueprint('user', __name__) -@bp.route('/') +@bp.route('/@') def profile(username): + + + + return "OH HELLO, HERETIC!" @bp.route('/settings') @@ -34,6 +39,82 @@ def settings(): @bp.route('/manage_users/') @login_required def manage_users(page): - users = User.query.order_by(User.created).paginate(page, current_app.config.get('MANAGE_USERS_PER_PAGE')) + users = User.query.order_by(User.created.desc()).paginate(page, current_app.config.get('MANAGE_USERS_PER_PAGE')) + + for user in users.items: + user.editform = EditUserForm( + user_id=user.id, + username=user.username, + email=user.email, + op_level=user.op_level.name, + user_status=user.user_status.name) return render_template('manage/users.html', users=users.items, pagination=users) + +@bp.route('user_modify', methods=['POST']) +@login_required +def modify_user(): + form = EditUserForm(request.form) + flash(str(request.form)) + if request.method == 'POST' and form.validate(): + user = User.query.filter_by(id=form.user_id.data).first() + if form.delete.data: + # user.user_status = USER_STATUS.inactive + #db.session.delete(user) + #db.session.commit() + flash('User {} deleted.'.format(str(user))) + elif form.edit.data: + if form.username.data: + user.username = form.username.data + if form.email.data: + user.email = form.email.data + if form.user_status.data: + user.user_status = form.user_status.data + if form.op_level.data: + user.op_level = form.op_level.data + + db.session.commit() + flash('User {}\'s data modified.'.format(str(user))) + + return redirect(url_for('.manage_users')) + + return redirect(url_for('main.index')) + +@bp.route('/manage_posts', defaults={'page': 1}) +@bp.route('/manage_posts/') +@login_required +def manage_posts(page): + posts = Post.query.order_by(Post.created.desc()).paginate(page, current_app.config.get('MANAGE_POSTS_PER_PAGE')) + + for post in posts.items: + post.editform = EditPostForm( + post_id=post.id, + rating=post.rating.name, + status=post.status.name, + source=post.source) + + return render_template('manage/posts.html', posts=posts.items, pagination=posts) + +@bp.route('post_modify', methods=['POST']) +@login_required +def modify_post(): + form = EditPostForm(request.form) + flash(str(request.form)) + if request.method == 'POST' and form.validate(): + post = Post.query.filter_by(id=form.post_id.data).first() + if form.delete.data: + # user.user_status = USER_STATUS.inactive + #db.session.delete(post) + #db.session.commit() + flash('User {} deleted.'.format(str(user))) + elif form.edit.data: + if form.rating.data: + post.rating = form.rating.data + if form.status.data: + post.status = form.status.data + if form.source.data: + post.source = form.source.data + + return redirect(url_for('.manage_posts')) + + return redirect(url_for('main.index')) diff --git a/yadc/forms.py b/yadc/forms.py index 8fde217..7fb10cb 100644 --- a/yadc/forms.py +++ b/yadc/forms.py @@ -1,6 +1,6 @@ from wtforms import Form -from wtforms import StringField, PasswordField, BooleanField, SubmitField, FileField, MultipleFileField, ValidationError, RadioField, TextAreaField, HiddenField -from wtforms.validators import DataRequired, InputRequired, Email, EqualTo, AnyOf +from wtforms import StringField, PasswordField, BooleanField, SubmitField, FileField, MultipleFileField, ValidationError, RadioField, TextAreaField, HiddenField, SelectField +from wtforms.validators import DataRequired, InputRequired, Email, EqualTo, AnyOf, optional from werkzeug.utils import cached_property @@ -66,7 +66,7 @@ class UploadForm(CSRFForm): sauce = StringField('Sauce', validators=[DataRequired()]) tags = StringField('Tags') rating = RadioField('Rating', - choices=[('safe', 'S'), ('questionable', 'Q'), ('explicit', 'E')], + choices=[('safe', 'Safe'), ('questionable', 'Questionable'), ('explicit', 'Explicit')], default='safe', validators=[DataRequired()]) submit = SubmitField('Upload') @@ -89,4 +89,34 @@ 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') \ No newline at end of file + submit = SubmitField('Change password') + + +class EditUserForm(CSRFForm): + user_id = HiddenField(validators=[DataRequired()]) + + username = StringField('Username') + email = StringField('E-mail', validators=[optional(), Email()]) + user_status = SelectField('User status', + choices=[('active', 'Active'), ('inactive', 'Inactive'), ('banned', 'Banned')], + validators=[optional()]) + op_level = SelectField('Permission level', + choices=[('user', 'User'), ('creator', 'Creator'), ('moderator', 'Moderator'), ('admin', 'Admin')], + validators=[optional()]) + + edit = SubmitField('Modify') + delete = SubmitField('Delete') + +class EditPostForm(CSRFForm): + post_id = HiddenField(validators=[DataRequired()]) + + rating = SelectField('Rating', + choices=[('safe', 'Safe'), ('questionable', 'Questionable'), ('explicit', 'Explicit')], + validators=[optional()]) + status = SelectField('Status', + choices=[('pending', 'Pending'), ('active', 'Active'), ('deleted', 'Deleted')], + validators=[optional()]) + source = StringField('Source') + + edit = SubmitField('Modify') + delete = SubmitField('Delete') \ No newline at end of file diff --git a/yadc/models.py b/yadc/models.py index 2842b84..14f0a9e 100644 --- a/yadc/models.py +++ b/yadc/models.py @@ -1,7 +1,8 @@ import enum import hashlib +import humanize import os -from datetime import datetime +from datetime import datetime, timezone from flask import current_app, url_for from flask_login import UserMixin, login_user, logout_user, current_user @@ -20,7 +21,8 @@ class OP_LEVEL(enum.Enum): class USER_STATUS(enum.Enum): active = 0 - banned = 1 + inactive = 1 + banned = 5 class FILETYPE(enum.Enum): png = 0 @@ -52,6 +54,12 @@ class TimestampMixin(object): created = db.Column(UtcDateTime, nullable=False, default=utcnow()) updated = db.Column(UtcDateTime, nullable=False, onupdate=utcnow(), default=utcnow()) + def natural_created(self): + return humanize.naturaltime(datetime.now().astimezone(self.created.tzinfo) - self.created) + + def natural_updated(self): + return humanize.naturaltime(datetime.now().astimezone(self.updated.tzinfo) - self.updated) + class User(UserMixin, TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(128), unique=True, nullable=False) @@ -97,12 +105,12 @@ class User(UserMixin, TimestampMixin, db.Model): def load_user(id): return User.query.get(int(id)) -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)) +# 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 +# rating = db.Column(db.Enum(RATING), default=RATING.safe, nullable=False) +# tag_blacklist = None post_tags = db.Table('post_tags', db.metadata, diff --git a/yadc/static/default.scss b/yadc/static/default.scss index 3b22363..c0f4544 100644 --- a/yadc/static/default.scss +++ b/yadc/static/default.scss @@ -1,6 +1,13 @@ -@import "_include-media"; +//@import "_include-media"; +@import "include-media/dist/_include-media"; $breakpoints: (tablet: 560px, desktop: 900px); +// $formbase__prefix: "fb-"; +// $formbase__padding: .2rem; +// @import "formbase/src/styles/main"; + +// @import "boilerform/assets/scss/boilerform"; + * { box-sizing: border-box; } @@ -197,7 +204,7 @@ header { .flash_msgs { display: flex; flex-flow: column-reverse nowrap; - position: absolute; + position: fixed; z-index: 20; // top: 0; //left: 0; @@ -257,6 +264,7 @@ header { font-size: 1.3em; } + $side-panel-width: 14rem; .important_subwrap { display: flex; @@ -267,78 +275,86 @@ header { flex-flow: row nowrap; } overflow: visible; // for negative margin of .post_single - } - - $side-panel-width: 14rem; - section.side_panel { - flex-shrink: 0; - @include media($bp-desktop) { - // width: 14em; - width: $side-panel-width; + > section.side_panel { + flex-shrink: 0; - height: 0; // for overflow - } + @include media($bp-desktop) { + // width: 14em; + width: $side-panel-width; - padding: 10px; + height: 0; // for overflow + } - article { - // &:not(:last-child) { - // margin-bottom: 10px; - // } - margin-bottom: 10px; - } + padding: 10px; - article.tags { - .tag_container { - display: flex; + article { + // &:not(:last-child) { + // margin-bottom: 10px; + // } + margin-bottom: 10px; + } - > a { - margin: 2px 2px; - // padding: .4em .75em; - padding: .35em .6em; + article.tags { + .tag_container { + display: flex; - //text-align: center; - border-radius: 4px; - background-color: #0005; + > a { + margin: 2px 2px; + // padding: .4em .75em; + padding: .35em .6em; + + //text-align: center; + border-radius: 4px; + background-color: #0005; + + > .fa-tag { + font-size: .9em; + margin-right: 2px; + } + + > .count { + // display: none; + font-size: .8em; + } + // > a { + // display: block; + // } + } + } + @include media($bp-tablet) { + .tag_container { + flex-flow: row wrap; - > .fa-tag { - font-size: .9em; - margin-right: 2px; } - > .count { - // display: none; - font-size: .8em; + } + @include media($bp-desktop) { + .tag_container { + flex-flow: column nowrap; + align-items: start; } - // > a { - // display: block; - // } } } - @include media($bp-tablet) { - .tag_container { - flex-flow: row wrap; - - } - } - @include media($bp-desktop) { - .tag_container { - flex-flow: column nowrap; - align-items: start; + article.post_desc { + display: flex; + flex-flow: column nowrap; + font-size: .9em; + + > img { + display: block; + max-width: 128px; } } } - article.post_desc { - display: flex; - flex-flow: column nowrap; - font-size: .9em; - - > img { - display: block; - max-width: 128px; + > section:not(.side_panel) { + // fucking just for desktop because son of a bitch negative margin does shit when in mobile layout + // it's a hack and I know it + // pls, destiny, have a mercy + @include media($bp-desktop) { + width: 100%; } } } @@ -444,24 +460,137 @@ header { } } + section.management_table { + table { + margin: 0 auto; + // border-collapse: collapse + } + + tr { + + + // &:nth-child(even), th { + // background-color: #404040; + // } + th { + background-color: #606060; + } + &:nth-child(even) { + background-color: #303030; + } + + th, td { + padding: 6px 10px; + } + + input { + + } + + &:not(.edit) { + td > .edit { + display: none; + } + } + &.edit { + td > .show { + display: none; + } + } + input[type=submit] { + display: none; + } + + .fa { + padding: 4px 4px; + align-self: center; + font-size: 1.5em; + cursor: pointer; + } + } + } + form { margin: 0 auto; width: 300px; //text-align: center; padding: 5px; + // input[type=text] { + // @extend .fb-input; + // margin: 0; + // // background: none; + // // border: none; + // // color: inherit; + // // font: inherit; + + // //line-height: 2em; + // // padding: 2px 6px; + // } + // input[type=submit] { + // @extend .fb-input; + // // padding: 0; + // width: unset; + // margin: .2rem; + // } + // select { + // @extend .fb-select; + // -moz-appearance: none; + // -webkit-appearance: none; + // margin: 0; + // // width: 100%; + // } + // input[type=file] { + // @extend .fb-input; + // padding: 0; + // background: none; + // border: none; + // color: inherit; + // width: unset; + // } + input { margin: 5px 0; //font-size: 1em; &:not([type=checkbox]):not([type=submit]):not([type=radio]):not([type=file]) { - width: 100%; + // width: 100%; } - &[type=file] { - //display: none; - } + // &[type=file] { + // //display: none; + + // @extend .fb-input; + // padding: 0; + // background: none; + // border: none; + // } + + // &[type=text] { + // @extend .fb-input; + // // margin: 0; + // // background: none; + // // border: none; + // // color: inherit; + // // font: inherit; + + // //line-height: 2em; + // // padding: 2px 6px; + // } + // &[type=submit] { + // @extend .fb-input; + // // padding: 0; + // width: unset; + // margin: .2rem; + // } } + // select { + // @extend .fb-select; + // -moz-appearance: none; + // -webkit-appearance: none; + // margin: 0; + // // width: 100%; + // } } diff --git a/yadc/templates/layout/base.html b/yadc/templates/layout/base.html index 356a5a1..c5ebfea 100644 --- a/yadc/templates/layout/base.html +++ b/yadc/templates/layout/base.html @@ -90,5 +90,24 @@ } }) + \ No newline at end of file diff --git a/yadc/templates/layout/settings.html b/yadc/templates/layout/settings.html index 0567f56..a0bb66b 100644 --- a/yadc/templates/layout/settings.html +++ b/yadc/templates/layout/settings.html @@ -6,8 +6,6 @@
{{ render_sidenav({"speedtest": ("Speedtest", "http://speedtest.cesnet.cz")}) }}
-
- {% block setting_content %}{% endblock %} -
+ {% block setting_content %}{% endblock %} {% endblock content %} \ No newline at end of file diff --git a/yadc/templates/manage/posts.html b/yadc/templates/manage/posts.html new file mode 100644 index 0000000..c2451ab --- /dev/null +++ b/yadc/templates/manage/posts.html @@ -0,0 +1,53 @@ +{% extends 'layout/settings.html' %} +{% from '_includes.html' import render_pagination with context %} +{% from "_formhelpers.html" import errors %} + +{% block setting_content %} +
+ + + + + + + + + + + + + + {% for post in posts %} + + + {{ post.editform.csrf_token }} + {{ post.editform.post_id() }} + + + + + + + + + + {% endfor %} + +
IDMD5RatingStatusSourceOriginal FilenameManage
{{ post.id }}
{{ post.md5[:7] }}
+ {{ post.rating.name.capitalize() }} + {{ post.editform.rating() }}{{ errors(post.editform.rating) }} + + {{ post.status.name.capitalize() }} + {{ post.editform.status() }}{{ errors(post.editform.status) }} + + {{ post.source }} + {{ post.editform.source() }}{{ errors(post.editform.source) }} + {{ post.origin_filename }} + + + + +
+ {{ render_pagination('user.manage_posts') }} +
+{% endblock %} \ No newline at end of file diff --git a/yadc/templates/manage/profile.html b/yadc/templates/manage/profile.html index 78319ba..61610a7 100644 --- a/yadc/templates/manage/profile.html +++ b/yadc/templates/manage/profile.html @@ -2,23 +2,25 @@ {% from "_formhelpers.html" import errors %} {% block setting_content %} -
-

Change password

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

Change password

+ {{ form.csrf_token }} +
+ {{ form.password_current(placeholder="Current password") }} + {{ errors(form.password_current) }} +
+
+ {{ form.password(placeholder="Password") }} + {{ errors(form.password) }} +
+
+ {{ form.password_again(placeholder="Repeat password") }} + {{ errors(form.password_again) }} +
+
+ {{ form.submit() }} +
+
+
{% endblock %} \ No newline at end of file diff --git a/yadc/templates/manage/users.html b/yadc/templates/manage/users.html index 81e7888..f053e6f 100644 --- a/yadc/templates/manage/users.html +++ b/yadc/templates/manage/users.html @@ -3,18 +3,47 @@ {% from "_formhelpers.html" import errors %} {% block setting_content %} - - - - {% for user in users %} +
+
+ - - - - + + + + + - {% endfor %} - -
{{ user.username }}{{ user.user_status.name }}{{ user.op_level.name }}{{ user.last_login.strftime('%I:%M %p %d %b, %y') }}UsernameUser statusPerm levelLast loginManage
-{{ render_pagination('user.manage_users') }} + + + {% for user in users %} + +
+ {{ user.editform.csrf_token }} + {{ user.editform.user_id() }} + + {{ user.username }} + {{ user.editform.username() }}{{ errors(user.editform.username) }} + + + {{ user.user_status.name.capitalize() }} + {{ user.editform.user_status() }}{{ errors(user.editform.user_status) }} + + + {{ user.op_level.name.capitalize() }} + {{ user.editform.op_level() }}{{ errors(user.editform.user_status) }} + + {{ user.last_login.strftime('%I:%M %p %d %b, %Y') }} + + + + + + +
+ + {% endfor %} + + + {{ render_pagination('user.manage_users') }} + {% endblock %} \ No newline at end of file diff --git a/yadc/templates/post/post.html b/yadc/templates/post/post.html index 7d4ea91..6756fe2 100644 --- a/yadc/templates/post/post.html +++ b/yadc/templates/post/post.html @@ -9,14 +9,14 @@
Id: {{ post.id }}
-
Author: {{ post.author.username }}
+
Author: {{ post.author.username or "Deleted Account" }}
{% if post.is_approved %}
Approved by: {{ post.approver.username }}
{% endif %} {% if not post.is_approved %}
Status: {{ post.status.name.capitalize() }}
{% endif %} -
Posted: 10 hours ago
+
Posted: {{ post.natural_created() }}
File size: {{ post.filesize_human }}
Image res: {{ post.image_resolution }}
diff --git a/yadc/templates/post/upload.html b/yadc/templates/post/upload.html index 3a7ea01..eddd0ae 100644 --- a/yadc/templates/post/upload.html +++ b/yadc/templates/post/upload.html @@ -18,7 +18,7 @@ {{ errors(form.tags) }}
- {{ form.rating.label }} {% for r in form.rating %}{{ r.label }}{{ r() }}{% endfor %} + {{ form.rating.label }} {% for r in form.rating %}
{{ r() }} {{ r.label }}{% endfor %} {{ errors(form.rating) }}
{{ form.submit() }}