1
1
Fork 0

I don't have time for this,

admin management,
post management,
some random quirks
dev
Jan Kužílek 5 years ago
parent b2ffd74286
commit 65d0e96e3e

@ -26,6 +26,7 @@ sqlalchemy-utc = "*"
flask-admin = "*" flask-admin = "*"
sqlalchemy-utils = "*" sqlalchemy-utils = "*"
werkzeug = "==0.16.1" werkzeug = "==0.16.1"
humanize = "*"
[requires] [requires]
python_version = "3.8" python_version = "3.8"

20
Pipfile.lock generated

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "90470384a8160ec4176abf18d39b07271e755ae437ead34f7b70d914c514d709" "sha256": "4fcb2df52789ae242fc5ab588bcbaad8232386d4388b1c72d16dc7ade9a7ba30"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -52,10 +52,11 @@
}, },
"flask-admin": { "flask-admin": {
"hashes": [ "hashes": [
"sha256:ed7b256471dba0f3af74f1a315733c3b36244592f2002c3bbdc65fd7c2aa807a" "sha256:aeebf87b5d5fd18525c876e0ecd71e0d0837614610122be235375c411b2d59dc",
"sha256:ff8270de5e8916541d19a0b03e469e2f8bbd22e4952c17aebc605112976f2fc4"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.5.4" "version": "==1.5.5"
}, },
"flask-assets": { "flask-assets": {
"hashes": [ "hashes": [
@ -67,10 +68,11 @@
}, },
"flask-login": { "flask-login": {
"hashes": [ "hashes": [
"sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec" "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b",
"sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.4.1" "version": "==0.5.0"
}, },
"flask-mail": { "flask-mail": {
"hashes": [ "hashes": [
@ -103,6 +105,14 @@
"index": "pypi", "index": "pypi",
"version": "==0.14.3" "version": "==0.14.3"
}, },
"humanize": {
"hashes": [
"sha256:3478104dcb9e111991ad141b15c9bf9522aa00ccfc5144561d639b3372e1d064",
"sha256:38ace9b66bcaeb7f8186b9dbf0b3448e00148e5b4fbaf726f96c789e52c3e741"
],
"index": "pypi",
"version": "==1.0.0"
},
"itsdangerous": { "itsdangerous": {
"hashes": [ "hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",

@ -30,6 +30,7 @@ def create_app():
INSTANCE_NAME='YaDc', INSTANCE_NAME='YaDc',
POSTS_PER_PAGE=8, POSTS_PER_PAGE=8,
MANAGE_USERS_PER_PAGE=2, MANAGE_USERS_PER_PAGE=2,
MANAGE_POSTS_PER_PAGE=2,
SQLALCHEMY_ECHO=True, SQLALCHEMY_ECHO=True,
) )

@ -65,7 +65,7 @@ def posts(page):
return render_template('post/index.html', posts=posts.items, tags=tags, pagination=posts) return render_template('post/index.html', posts=posts.items, tags=tags, pagination=posts)
@bp.route('/show/<id>') @bp.route('/show/<int:id>')
def post_show(id): def post_show(id):
post = Post.query.filter_by(id=id).first() post = Post.query.filter_by(id=id).first()
# flash(post) # flash(post)

@ -1,14 +1,19 @@
from flask import (Blueprint, abort, current_app, flash, redirect, from flask import (Blueprint, abort, current_app, flash, redirect,
render_template, request, send_from_directory, url_for) render_template, request, send_from_directory, url_for)
from flask_login import current_user, login_required 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 = Blueprint('user', __name__)
@bp.route('/<username>') @bp.route('/@<username>')
def profile(username): def profile(username):
return "OH HELLO, HERETIC!" return "OH HELLO, HERETIC!"
@bp.route('/settings') @bp.route('/settings')
@ -34,6 +39,82 @@ def settings():
@bp.route('/manage_users/<int:page>') @bp.route('/manage_users/<int:page>')
@login_required @login_required
def manage_users(page): 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) 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/<int:page>')
@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'))

@ -1,6 +1,6 @@
from wtforms import Form from wtforms import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField, FileField, MultipleFileField, ValidationError, RadioField, TextAreaField, HiddenField from wtforms import StringField, PasswordField, BooleanField, SubmitField, FileField, MultipleFileField, ValidationError, RadioField, TextAreaField, HiddenField, SelectField
from wtforms.validators import DataRequired, InputRequired, Email, EqualTo, AnyOf from wtforms.validators import DataRequired, InputRequired, Email, EqualTo, AnyOf, optional
from werkzeug.utils import cached_property from werkzeug.utils import cached_property
@ -66,7 +66,7 @@ class UploadForm(CSRFForm):
sauce = StringField('Sauce', validators=[DataRequired()]) sauce = StringField('Sauce', validators=[DataRequired()])
tags = StringField('Tags') tags = StringField('Tags')
rating = RadioField('Rating', rating = RadioField('Rating',
choices=[('safe', 'S'), ('questionable', 'Q'), ('explicit', 'E')], choices=[('safe', 'Safe'), ('questionable', 'Questionable'), ('explicit', 'Explicit')],
default='safe', default='safe',
validators=[DataRequired()]) validators=[DataRequired()])
submit = SubmitField('Upload') submit = SubmitField('Upload')
@ -90,3 +90,33 @@ class ChangePassForm(CSRFForm):
password = PasswordField('Password', validators=[DataRequired()]) password = PasswordField('Password', validators=[DataRequired()])
password_again = PasswordField('Repeat password', validators=[DataRequired(), EqualTo('password')]) password_again = PasswordField('Repeat password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Change password') 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')

@ -1,7 +1,8 @@
import enum import enum
import hashlib import hashlib
import humanize
import os import os
from datetime import datetime from datetime import datetime, timezone
from flask import current_app, url_for from flask import current_app, url_for
from flask_login import UserMixin, login_user, logout_user, current_user 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): class USER_STATUS(enum.Enum):
active = 0 active = 0
banned = 1 inactive = 1
banned = 5
class FILETYPE(enum.Enum): class FILETYPE(enum.Enum):
png = 0 png = 0
@ -52,6 +54,12 @@ class TimestampMixin(object):
created = db.Column(UtcDateTime, nullable=False, default=utcnow()) created = db.Column(UtcDateTime, nullable=False, default=utcnow())
updated = db.Column(UtcDateTime, nullable=False, onupdate=utcnow(), 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): class User(UserMixin, TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(128), unique=True, nullable=False) username = db.Column(db.String(128), unique=True, nullable=False)
@ -97,12 +105,12 @@ class User(UserMixin, TimestampMixin, db.Model):
def load_user(id): def load_user(id):
return User.query.get(int(id)) return User.query.get(int(id))
class UserPreferences(db.Model): # class UserPreferences(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) # user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
user = db.relationship('User', backref=db.backref('profile', uselist=False)) # user = db.relationship('User', backref=db.backref('profile', uselist=False))
rating = db.Column(db.Enum(RATING), default=RATING.safe, nullable=False) # rating = db.Column(db.Enum(RATING), default=RATING.safe, nullable=False)
tag_blacklist = None # tag_blacklist = None
post_tags = db.Table('post_tags', db.metadata, post_tags = db.Table('post_tags', db.metadata,

@ -1,6 +1,13 @@
@import "_include-media"; //@import "_include-media";
@import "include-media/dist/_include-media";
$breakpoints: (tablet: 560px, desktop: 900px); $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; box-sizing: border-box;
} }
@ -197,7 +204,7 @@ header {
.flash_msgs { .flash_msgs {
display: flex; display: flex;
flex-flow: column-reverse nowrap; flex-flow: column-reverse nowrap;
position: absolute; position: fixed;
z-index: 20; z-index: 20;
// top: 0; // top: 0;
//left: 0; //left: 0;
@ -257,6 +264,7 @@ header {
font-size: 1.3em; font-size: 1.3em;
} }
$side-panel-width: 14rem;
.important_subwrap { .important_subwrap {
display: flex; display: flex;
@ -267,10 +275,8 @@ header {
flex-flow: row nowrap; flex-flow: row nowrap;
} }
overflow: visible; // for negative margin of .post_single overflow: visible; // for negative margin of .post_single
}
$side-panel-width: 14rem; > section.side_panel {
section.side_panel {
flex-shrink: 0; flex-shrink: 0;
@include media($bp-desktop) { @include media($bp-desktop) {
@ -343,6 +349,16 @@ header {
} }
} }
> 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%;
}
}
}
section.post_list { section.post_list {
overflow: hidden; overflow: hidden;
@include media("<tablet") { @include media("<tablet") {
@ -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 { form {
margin: 0 auto; margin: 0 auto;
width: 300px; width: 300px;
//text-align: center; //text-align: center;
padding: 5px; 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 { input {
margin: 5px 0; margin: 5px 0;
//font-size: 1em; //font-size: 1em;
&:not([type=checkbox]):not([type=submit]):not([type=radio]):not([type=file]) { &:not([type=checkbox]):not([type=submit]):not([type=radio]):not([type=file]) {
width: 100%; // width: 100%;
} }
&[type=file] { // &[type=file] {
//display: none; // //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%;
// }
} }

@ -90,5 +90,24 @@
} }
}) })
</script> </script>
<script>
let rows = document.querySelectorAll("section.management_table tbody > tr")
let show_edit_controls = function(ev) {
let row = ev.target.closest("tr")
console.log(row)
if (!row.classList.contains("edit")) {
row.classList.add("edit")
} else {
row.classList.remove("edit")
}
}
rows.forEach(element => {
element.querySelectorAll("label.to-edit, label.to-close").forEach(el => {
el.addEventListener('click', show_edit_controls)
})
});
</script>
</body> </body>
</html> </html>

@ -6,8 +6,6 @@
<section class="side_panel"> <section class="side_panel">
{{ render_sidenav({"speedtest": ("Speedtest", "http://speedtest.cesnet.cz")}) }} {{ render_sidenav({"speedtest": ("Speedtest", "http://speedtest.cesnet.cz")}) }}
</section> </section>
<section class="{{ setting_class }}">
{% block setting_content %}{% endblock %} {% block setting_content %}{% endblock %}
</section>
</div> </div>
{% endblock content %} {% endblock content %}

@ -0,0 +1,53 @@
{% extends 'layout/settings.html' %}
{% from '_includes.html' import render_pagination with context %}
{% from "_formhelpers.html" import errors %}
{% block setting_content %}
<section class="management_table manage_posts">
<table>
<thead>
<tr>
<th>ID</th>
<th>MD5</th>
<th>Rating</th>
<th>Status</th>
<th>Source</th>
<th>Original Filename</th>
<th>Manage</th>
</tr>
</thead>
<tbody>
{% for post in posts %}
<tr>
<form action="{{ url_for('user.modify_post') }}" method="post">
{{ post.editform.csrf_token }}
{{ post.editform.post_id() }}
<td>{{ post.id }}</td>
<td><pre style="margin: 0;">{{ post.md5[:7] }}</pre></td>
<td>
<span class="show">{{ post.rating.name.capitalize() }}</span>
<span class="edit">{{ post.editform.rating() }}{{ errors(post.editform.rating) }}</span>
</td>
<td>
<span class="show">{{ post.status.name.capitalize() }}</span>
<span class="edit">{{ post.editform.status() }}{{ errors(post.editform.status) }}</span>
</td>
<td>
<span class="show">{{ post.source }}</span>
<span class="edit">{{ post.editform.source() }}{{ errors(post.editform.source) }}</span>
</td>
<td>{{ post.origin_filename }}</td>
<td>
<label class="show to-edit"><span class="fa fa-edit"></span></label>
<label class="edit to-close"><span class="fa fa-close"></span></label>
<label class="edit"><span class="fa fa-check"></span>{{ post.editform.edit() }}</label>
<label><span class="fa fa-trash-o"></span>{{ post.editform.delete() }}</label>
</td>
</form>
</tr>
{% endfor %}
</tbody>
</table>
{{ render_pagination('user.manage_posts') }}
</section>
{% endblock %}

@ -2,6 +2,7 @@
{% from "_formhelpers.html" import errors %} {% from "_formhelpers.html" import errors %}
{% block setting_content %} {% block setting_content %}
<section class="manage_profile">
<form action="" method="post"> <form action="" method="post">
<h3>Change password</h3> <h3>Change password</h3>
{{ form.csrf_token }} {{ form.csrf_token }}
@ -21,4 +22,5 @@
{{ form.submit() }} {{ form.submit() }}
</div> </div>
</form> </form>
</section>
{% endblock %} {% endblock %}

@ -3,18 +3,47 @@
{% from "_formhelpers.html" import errors %} {% from "_formhelpers.html" import errors %}
{% block setting_content %} {% block setting_content %}
<section class="management_table manage_users">
<table> <table>
<!-- <thead></thead> --> <thead>
<tr>
<th>Username</th>
<th>User status</th>
<th>Perm level</th>
<th>Last login</th>
<th>Manage</th>
</tr>
</thead>
<tbody> <tbody>
{% for user in users %} {% for user in users %}
<tr> <tr>
<td>{{ user.username }}</td> <form action="{{ url_for('user.modify_user') }}" method="post">
<td>{{ user.user_status.name }}</td> {{ user.editform.csrf_token }}
<td>{{ user.op_level.name }}</td> {{ user.editform.user_id() }}
<td>{{ user.last_login.strftime('%I:%M %p %d %b, %y') }}</td> <td>
<span class="show">{{ user.username }}</span>
<span class="edit">{{ user.editform.username() }}{{ errors(user.editform.username) }}</span>
</td>
<td>
<span class="show">{{ user.user_status.name.capitalize() }}</span>
<span class="edit">{{ user.editform.user_status() }}{{ errors(user.editform.user_status) }}</span>
</td>
<td>
<span class="show">{{ user.op_level.name.capitalize() }}</span>
<span class="edit">{{ user.editform.op_level() }}{{ errors(user.editform.user_status) }}</span>
</td>
<td>{{ user.last_login.strftime('%I:%M %p %d %b, %Y') }}</td>
<td>
<label class="show to-edit"><span class="fa fa-edit"></span></label>
<label class="edit to-close"><span class="fa fa-close"></span></label>
<label class="edit"><span class="fa fa-check"></span>{{ user.editform.edit() }}</label>
<label><span class="fa fa-trash-o"></span>{{ user.editform.delete() }}</label>
</td>
</form>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{{ render_pagination('user.manage_users') }} {{ render_pagination('user.manage_users') }}
</section>
{% endblock %} {% endblock %}

@ -9,14 +9,14 @@
<!-- <h4>Description</h4> --> <!-- <h4>Description</h4> -->
<!-- <img src="/static/profile.jpg" alt=""> --> <!-- <img src="/static/profile.jpg" alt=""> -->
<div class="id">Id: {{ post.id }}</div> <div class="id">Id: {{ post.id }}</div>
<div class="author">Author: {{ post.author.username }}</div> <div class="author">Author: {{ post.author.username or "Deleted Account" }}</div>
{% if post.is_approved %} {% if post.is_approved %}
<div class="approver">Approved by: {{ post.approver.username }}</div> <div class="approver">Approved by: {{ post.approver.username }}</div>
{% endif %} {% endif %}
{% if not post.is_approved %} {% if not post.is_approved %}
<div class="status">Status: {{ post.status.name.capitalize() }}</div> <div class="status">Status: {{ post.status.name.capitalize() }}</div>
{% endif %} {% endif %}
<div class="time">Posted: 10 hours ago</div> <div class="time">Posted: {{ post.natural_created() }}</div>
<div class="source">Source: <a href="{{ post.source }}">{{ post.source }}</a></div> <div class="source">Source: <a href="{{ post.source }}">{{ post.source }}</a></div>
<div class="size">File size: {{ post.filesize_human }}</div> <div class="size">File size: {{ post.filesize_human }}</div>
<div class="resolution">Image res: {{ post.image_resolution }}</div> <div class="resolution">Image res: {{ post.image_resolution }}</div>

@ -18,7 +18,7 @@
{{ errors(form.tags) }} {{ errors(form.tags) }}
</div> </div>
<div> <div>
{{ form.rating.label }} {% for r in form.rating %}{{ r.label }}{{ r() }}{% endfor %} {{ form.rating.label }} {% for r in form.rating %}<br>{{ r() }} {{ r.label }}{% endfor %}
{{ errors(form.rating) }} {{ errors(form.rating) }}
</div> </div>
<div>{{ form.submit() }}</div> <div>{{ form.submit() }}</div>

Loading…
Cancel
Save