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 = "*"
sqlalchemy-utils = "*"
werkzeug = "==0.16.1"
humanize = "*"
[requires]
python_version = "3.8"

20
Pipfile.lock generated

@ -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",

@ -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,
)

@ -65,7 +65,7 @@ def posts(page):
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):
post = Post.query.filter_by(id=id).first()
# flash(post)

@ -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('/<username>')
@bp.route('/@<username>')
def profile(username):
return "OH HELLO, HERETIC!"
@bp.route('/settings')
@ -34,6 +39,82 @@ def settings():
@bp.route('/manage_users/<int:page>')
@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/<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 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')
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 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,

@ -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%;
// }
}

@ -90,5 +90,24 @@
}
})
</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>
</html>

@ -6,8 +6,6 @@
<section class="side_panel">
{{ render_sidenav({"speedtest": ("Speedtest", "http://speedtest.cesnet.cz")}) }}
</section>
<section class="{{ setting_class }}">
{% block setting_content %}{% endblock %}
</section>
{% block setting_content %}{% endblock %}
</div>
{% 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,23 +2,25 @@
{% from "_formhelpers.html" import errors %}
{% block setting_content %}
<form action="" method="post">
<h3>Change password</h3>
{{ form.csrf_token }}
<div>
{{ form.password_current(placeholder="Current password") }}
{{ errors(form.password_current) }}
</div>
<div>
{{ form.password(placeholder="Password") }}
{{ errors(form.password) }}
</div>
<div>
{{ form.password_again(placeholder="Repeat password") }}
{{ errors(form.password_again) }}
</div>
<div>
{{ form.submit() }}
</div>
</form>
<section class="manage_profile">
<form action="" method="post">
<h3>Change password</h3>
{{ form.csrf_token }}
<div>
{{ form.password_current(placeholder="Current password") }}
{{ errors(form.password_current) }}
</div>
<div>
{{ form.password(placeholder="Password") }}
{{ errors(form.password) }}
</div>
<div>
{{ form.password_again(placeholder="Repeat password") }}
{{ errors(form.password_again) }}
</div>
<div>
{{ form.submit() }}
</div>
</form>
</section>
{% endblock %}

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

@ -9,14 +9,14 @@
<!-- <h4>Description</h4> -->
<!-- <img src="/static/profile.jpg" alt=""> -->
<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 %}
<div class="approver">Approved by: {{ post.approver.username }}</div>
{% endif %}
{% if not post.is_approved %}
<div class="status">Status: {{ post.status.name.capitalize() }}</div>
{% 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="size">File size: {{ post.filesize_human }}</div>
<div class="resolution">Image res: {{ post.image_resolution }}</div>

@ -18,7 +18,7 @@
{{ errors(form.tags) }}
</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) }}
</div>
<div>{{ form.submit() }}</div>

Loading…
Cancel
Save