diff --git a/.gitignore b/.gitignore index 5557511..3744093 100644 --- a/.gitignore +++ b/.gitignore @@ -199,3 +199,6 @@ dmypy.json .history # End of https://www.gitignore.io/api/flask,python,visualstudiocode + +migrations/* +.vscode/settings.json diff --git a/Pipfile b/Pipfile index 0516fc0..bfe4710 100644 --- a/Pipfile +++ b/Pipfile @@ -20,6 +20,10 @@ flask-assets = "*" pyscss = "*" libsass = "*" cssmin = "*" +python-magic = "*" +flask-mail = "*" +sqlalchemy-utc = "*" +flask-admin = "*" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index 63828d8..dfbfd3e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2b4208c857a995a746d67b2b523ebd5caface2c836881c0b549e9474d200c507" + "sha256": "4aa69f03ba5d5f31ea39beeefc97e9a9b61946c3275fb53e58897aa33c9ffc1c" }, "pipfile-spec": 6, "requires": { @@ -18,9 +18,15 @@ "default": { "alembic": { "hashes": [ - "sha256:49277bb7242192bbb9eac58fed4fe02ec6c3a2a4b4345d2171197459266482b2" + "sha256:3b0cb1948833e062f4048992fbc97ecfaaaac24aaa0d83a1202a99fb58af8c6d" ], - "version": "==1.3.1" + "version": "==1.3.2" + }, + "blinker": { + "hashes": [ + "sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6" + ], + "version": "==1.4" }, "click": { "hashes": [ @@ -44,12 +50,20 @@ "index": "pypi", "version": "==1.1.1" }, + "flask-admin": { + "hashes": [ + "sha256:ed7b256471dba0f3af74f1a315733c3b36244592f2002c3bbdc65fd7c2aa807a" + ], + "index": "pypi", + "version": "==1.5.4" + }, "flask-assets": { "hashes": [ - "sha256:6031527b89fb3509d1581d932affa5a79dd348cfffb58d0aef99a43461d47847" + "sha256:1dfdea35e40744d46aada72831f7613d67bf38e8b20ccaaa9e91fdc37aa3b8c2", + "sha256:2845bd3b479be9db8556801e7ebc2746ce2d9edb4e7b64a1c786ecbfc1e5867b" ], "index": "pypi", - "version": "==0.12" + "version": "==2.0" }, "flask-login": { "hashes": [ @@ -58,6 +72,13 @@ "index": "pypi", "version": "==0.4.1" }, + "flask-mail": { + "hashes": [ + "sha256:22e5eb9a940bf407bcf30410ecc3708f3c56cc44b29c34e1726fe85006935f41" + ], + "index": "pypi", + "version": "==0.9.1" + }, "flask-migrate": { "hashes": [ "sha256:6fb038be63d4c60727d5dfa5f581a6189af5b4e2925bc378697b4f0a40cfb4e1", @@ -159,39 +180,31 @@ }, "pillow": { "hashes": [ - "sha256:047d9473cf68af50ac85f8ee5d5f21a60f849bc17d348da7fc85711287a75031", - "sha256:0f66dc6c8a3cc319561a633b6aa82c44107f12594643efa37210d8c924fc1c71", - "sha256:12c9169c4e8fe0a7329e8658c7e488001f6b4c8e88740e76292c2b857af2e94c", - "sha256:248cffc168896982f125f5c13e9317c059f74fffdb4152893339f3be62a01340", - "sha256:27faf0552bf8c260a5cee21a76e031acaea68babb64daf7e8f2e2540745082aa", - "sha256:285edafad9bc60d96978ed24d77cdc0b91dace88e5da8c548ba5937c425bca8b", - "sha256:384b12c9aa8ef95558abdcb50aada56d74bc7cc131dd62d28c2d0e4d3aadd573", - "sha256:38950b3a707f6cef09cd3cbb142474357ad1a985ceb44d921bdf7b4647b3e13e", - "sha256:4aad1b88933fd6dc2846552b89ad0c74ddbba2f0884e2c162aa368374bf5abab", - "sha256:4ac6148008c169603070c092e81f88738f1a0c511e07bd2bb0f9ef542d375da9", - "sha256:4deb1d2a45861ae6f0b12ea0a786a03d19d29edcc7e05775b85ec2877cb54c5e", - "sha256:59aa2c124df72cc75ed72c8d6005c442d4685691a30c55321e00ed915ad1a291", - "sha256:5a47d2123a9ec86660fe0e8d0ebf0aa6bc6a17edc63f338b73ea20ba11713f12", - "sha256:5cc901c2ab9409b4b7ac7b5bcc3e86ac14548627062463da0af3b6b7c555a871", - "sha256:6c1db03e8dff7b9f955a0fb9907eb9ca5da75b5ce056c0c93d33100a35050281", - "sha256:7ce80c0a65a6ea90ef9c1f63c8593fcd2929448613fc8da0adf3e6bfad669d08", - "sha256:809c19241c14433c5d6135e1b6c72da4e3b56d5c865ad5736ab99af8896b8f41", - "sha256:83792cb4e0b5af480588601467c0764242b9a483caea71ef12d22a0d0d6bdce2", - "sha256:846fa202bd7ee0f6215c897a1d33238ef071b50766339186687bd9b7a6d26ac5", - "sha256:9f5529fc02009f96ba95bea48870173426879dc19eec49ca8e08cd63ecd82ddb", - "sha256:a423c2ea001c6265ed28700df056f75e26215fd28c001e93ef4380b0f05f9547", - "sha256:ac4428094b42907aba5879c7c000d01c8278d451a3b7cccd2103e21f6397ea75", - "sha256:b1ae48d87f10d1384e5beecd169c77502fcc04a2c00a4c02b85f0a94b419e5f9", - "sha256:bf4e972a88f8841d8fdc6db1a75e0f8d763e66e3754b03006cbc3854d89f1cb1", - "sha256:c6414f6aad598364aaf81068cabb077894eb88fed99c6a65e6e8217bab62ae7a", - "sha256:c710fcb7ee32f67baf25aa9ffede4795fd5d93b163ce95fdc724383e38c9df96", - "sha256:c7be4b8a09852291c3c48d3c25d1b876d2494a0a674980089ac9d5e0d78bd132", - "sha256:c9e5ffb910b14f090ac9c38599063e354887a5f6d7e6d26795e916b4514f2c1a", - "sha256:e0697b826da6c2472bb6488db4c0a7fa8af0d52fa08833ceb3681358914b14e5", - "sha256:e9a3edd5f714229d41057d56ac0f39ad9bdba6767e8c888c951869f0bdd129b0" + "sha256:0a628977ac2e01ca96aaae247ec2bd38e729631ddf2221b4b715446fd45505be", + "sha256:4d9ed9a64095e031435af120d3c910148067087541131e82b3e8db302f4c8946", + "sha256:54ebae163e8412aff0b9df1e88adab65788f5f5b58e625dc5c7f51eaf14a6837", + "sha256:5bfef0b1cdde9f33881c913af14e43db69815c7e8df429ceda4c70a5e529210f", + "sha256:5f3546ceb08089cedb9e8ff7e3f6a7042bb5b37c2a95d392fb027c3e53a2da00", + "sha256:5f7ae9126d16194f114435ebb79cc536b5682002a4fa57fa7bb2cbcde65f2f4d", + "sha256:62a889aeb0a79e50ecf5af272e9e3c164148f4bd9636cc6bcfa182a52c8b0533", + "sha256:7406f5a9b2fd966e79e6abdaf700585a4522e98d6559ce37fc52e5c955fade0a", + "sha256:8453f914f4e5a3d828281a6628cf517832abfa13ff50679a4848926dac7c0358", + "sha256:87269cc6ce1e3dee11f23fa515e4249ae678dbbe2704598a51cee76c52e19cda", + "sha256:875358310ed7abd5320f21dd97351d62de4929b0426cdb1eaa904b64ac36b435", + "sha256:8ac6ce7ff3892e5deaab7abaec763538ffd011f74dc1801d93d3c5fc541feee2", + "sha256:91b710e3353aea6fc758cdb7136d9bbdcb26b53cefe43e2cba953ac3ee1d3313", + "sha256:9d2ba4ed13af381233e2d810ff3bab84ef9f18430a9b336ab69eaf3cd24299ff", + "sha256:a62ec5e13e227399be73303ff301f2865bf68657d15ea50b038d25fc41097317", + "sha256:ab76e5580b0ed647a8d8d2d2daee170e8e9f8aad225ede314f684e297e3643c2", + "sha256:bf4003aa538af3f4205c5fac56eacaa67a6dd81e454ffd9e9f055fff9f1bc614", + "sha256:bf598d2e37cf8edb1a2f26ed3fb255191f5232badea4003c16301cb94ac5bdd0", + "sha256:c18f70dc27cc5d236f10e7834236aff60aadc71346a5bc1f4f83a4b3abee6386", + "sha256:c5ed816632204a2fc9486d784d8e0d0ae754347aba99c811458d69fcdfd2a2f9", + "sha256:dc058b7833184970d1248135b8b0ab702e6daa833be14035179f2acb78ff5636", + "sha256:ff3797f2f16bf9d17d53257612da84dd0758db33935777149b3334c01ff68865" ], "index": "pypi", - "version": "==6.2.1" + "version": "==7.0.0" }, "psycopg2": { "hashes": [ @@ -234,6 +247,14 @@ ], "version": "==1.0.4" }, + "python-magic": { + "hashes": [ + "sha256:f2674dcfad52ae6c49d4803fa027809540b130db1dec928cfbb9240316831375", + "sha256:f3765c0f582d2dfc72c15f3b5a82aecfae9498bd29ca840d72f37d7bd38bfcd5" + ], + "index": "pypi", + "version": "==0.4.15" + }, "six": { "hashes": [ "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", @@ -243,15 +264,24 @@ }, "sqlalchemy": { "hashes": [ - "sha256:afa5541e9dea8ad0014251bc9d56171ca3d8b130c9627c6cb3681cff30be3f8a" + "sha256:bfb8f464a5000b567ac1d350b9090cf081180ec1ab4aa87e7bca12dab25320ec" ], - "version": "==1.3.11" + "version": "==1.3.12" + }, + "sqlalchemy-utc": { + "hashes": [ + "sha256:ec5395dfa4d237239c162a1b83283d88c8e2a94219512708634c55329c900278", + "sha256:fed53af37d250168b99eba8f9908a50e34e10dab3c32d38df3e65601ac951baf" + ], + "index": "pypi", + "version": "==0.10.0" }, "webassets": { "hashes": [ - "sha256:e7d9c8887343123fd5b32309b33167428cb1318cdda97ece12d0907fd69d38db" + "sha256:167132337677c8cedc9705090f6d48da3fb262c8e0b2773b29f3352f050181cd", + "sha256:a31a55147752ba1b3dc07dee0ad8c8efff274464e08bbdb88c1fd59ffd552724" ], - "version": "==0.12.1" + "version": "==2.0" }, "werkzeug": { "hashes": [ @@ -420,9 +450,9 @@ }, "sqlalchemy": { "hashes": [ - "sha256:afa5541e9dea8ad0014251bc9d56171ca3d8b130c9627c6cb3681cff30be3f8a" + "sha256:bfb8f464a5000b567ac1d350b9090cf081180ec1ab4aa87e7bca12dab25320ec" ], - "version": "==1.3.11" + "version": "==1.3.12" }, "werkzeug": { "hashes": [ diff --git a/yadc/__init__.py b/yadc/__init__.py index ea1ef50..310b53e 100644 --- a/yadc/__init__.py +++ b/yadc/__init__.py @@ -6,11 +6,16 @@ from flask_migrate import Migrate from flask_login import LoginManager from flask_assets import Environment as AssetsEnvironment, Bundle as AssetsBundle +from flask_admin import Admin +from flask_admin.contrib.sqla import ModelView + db = SQLAlchemy() migrate = Migrate() login = LoginManager() assets = AssetsEnvironment() +admin = Admin() + def create_app(): app = Flask(__name__) @@ -18,6 +23,11 @@ def create_app(): SECRET_KEY='dev', #SQLALCHEMY_DATABASE_URI='sqlite:///{}'.format(os.path.join(app.instance_path, 'yadc.db')), SQLALCHEMY_DATABASE_URI='postgresql://{}:{}@{}:{}/{}'.format('yadc', 'omegalul', 'localhost', 5432, 'yadc'), + MAX_CONTENT_LENGTH=10*1024*1024, + POST_LIST_THUMB_HEIGHT=200, + POST_UPLOADS=os.path.join(app.instance_path, 'post'), + + SQLALCHEMY_ECHO=True, ) try: @@ -30,15 +40,35 @@ def create_app(): login.init_app(app) assets.init_app(app) - from yadc import main, auth + from yadc import models + + admin.init_app(app) + admin.add_view(ModelView(models.User, db.session)) + admin.add_view(ModelView(models.Post, db.session)) + admin.add_view(ModelView(models.Tag, db.session)) + admin.add_view(ModelView(models.Comment, db.session)) + + from yadc.bp import main, auth app.register_blueprint(main.bp) - app.register_blueprint(auth.bp) + app.register_blueprint(auth.bp, url_prefix='/user') login.login_view = 'auth.login' #assets.url = app.static_url_path scss = AssetsBundle('default.scss', filters='libsass', output='all.css') assets.register('scss_all', scss) - from yadc import models - return app + + +from yadc import models as m + +app = create_app() +@app.shell_context_processor +def make_shell_context(): + ctxt = {'db': db} + + for mdl in [m.User, m.Post, m.Tag, m.Comment]: + ctxt[mdl.__name__] = mdl + + return ctxt + # return {'db': db, 'User': models.User, 'Post': models.Post, 'Tag': models.Tag, 'Comment': models.Comment} \ No newline at end of file diff --git a/yadc/auth.py b/yadc/auth.py deleted file mode 100644 index 16875d5..0000000 --- a/yadc/auth.py +++ /dev/null @@ -1,40 +0,0 @@ -from flask import request -from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, BooleanField, SubmitField -from wtforms.validators import DataRequired -from flask_login import current_user, login_user, logout_user -from werkzeug.urls import url_parse - -from yadc.models import User - -class LoginForm(FlaskForm): - username = StringField('Username', validators=[DataRequired()]) - password = PasswordField('Password', validators=[DataRequired()]) - remember_me = BooleanField('Remember me') - submit = SubmitField('Log In') - -from flask import Blueprint, render_template, flash, redirect, url_for - -bp = Blueprint('auth', __name__) - -@bp.route('/login', methods=['GET', 'POST']) -def login(): - form = LoginForm() - if form.validate_on_submit(): - #flash('Login requested for user {}, remember_me={}'.format(form.username.data, form.remember_me.data)) - user = User.query.filter_by(username=form.username.data).first() - if user is None or not user.check_password(form.password.data): - flash('Invalid username or password.') - return redirect(url_for('auth.login')) - login_user(user, remember=form.remember_me.data) - nextpg = request.args.get('next') - if not nextpg and url_parse(nextpg).netloc != '': - nextpg = url_for('main.index') - #flash('Logged in as {}'.format(user.username)) - return redirect(nextpg) - return render_template('login.html', form=form) - -@bp.route('/logout') -def logout(): - logout_user() - return redirect(url_for('main.index')) diff --git a/yadc/bp/auth.py b/yadc/bp/auth.py new file mode 100644 index 0000000..90be874 --- /dev/null +++ b/yadc/bp/auth.py @@ -0,0 +1,82 @@ +import flask_login as fl +from flask import Blueprint, flash, redirect, render_template, request, url_for +from werkzeug.urls import url_parse +from wtforms import BooleanField, PasswordField, StringField, SubmitField +from wtforms.validators import DataRequired + +from yadc import db +from yadc.forms import LoginForm, RegisterForm, ResetPasswordForm +from yadc.models import User + +bp = Blueprint('auth', __name__) + +def nextpage(): + nextpg = request.args.get('next') + if not nextpg or url_parse(nextpg).netloc != '': + nextpg = url_for('main.index') + return nextpg + +@bp.route('/login/', methods=['GET', 'POST']) +def login(): + if fl.current_user.is_authenticated: + return redirect(url_for('main.index')) + + form = LoginForm(request.form) + if request.method == 'POST' and form.validate(): + user = User.query.filter_by(username=form.username.data).first() + if user is None or not user.check_password(form.password.data): + flash('Invalid username or password.') + return redirect(url_for('.login')) + + user.login(remember=form.remember_me.data) + db.session.commit() + + flash('Logged in as {}'.format(user.username)) + + return redirect(nextpage()) + + return render_template('login.html', form=form) + +@bp.route('/logout/') +def logout(): + fl.current_user.logout() + + return redirect(nextpage()) + +@bp.route('/reset_password/', methods=['GET', 'POST']) +def reset_password(): + if fl.current_user.is_authenticated: + return redirect(url_for('main.index')) + + form = ResetPasswordForm(request.form) + if request.method == 'POST' and form.validate(): + user = User.query.filter_by(email=form.email.data).first() + + if user: + user.create_password('kuxaman') + db.session.commit() + #do something to reset the password + + flash('Password successfully reset. Check your email.') + return redirect(url_for('.login')) + + return render_template('reset_password.html', form=form) + + +@bp.route('/register/', methods=['GET', 'POST']) +def register(): + if fl.current_user.is_authenticated: + return redirect(url_for('main.index')) + + form = RegisterForm(request.form) + if request.method == 'POST' and form.validate(): + user = User(username=form.username.data, email=form.email.data) + user.create_password(form.password.data) + + db.session.add(user) + db.session.commit() + + flash('Your account has been successfully registered. You can now login.') + return redirect(url_for('.login')) + + return render_template('register.html', form=form) diff --git a/yadc/bp/main.py b/yadc/bp/main.py new file mode 100644 index 0000000..eb2d401 --- /dev/null +++ b/yadc/bp/main.py @@ -0,0 +1,123 @@ +from flask import Blueprint, render_template, flash, redirect, url_for, request, abort +from flask_login import login_required + +bp = Blueprint('main', __name__) + +@bp.route('/') +def index(): + return posts() + +# @bp.route('/') +@bp.route('/post/') +def posts(): + posts = Post.query.order_by(Post.created).limit(20).all() + # flash(posts) + + tagset = set() + taglist = list() + for post in posts: + for tag in post.tags: + tagset.add(tag) + taglist.append(tag) + + for tag in tagset: + tag.count = taglist.count(tag) + + tags = list(tagset) + tags.sort(key=lambda x: (x.category.value, x.content) ) + # flash([tag.count for tag in tags]) + + return render_template('index.html', posts=posts, tags=tags) + +@bp.route('/post/show//') +def post_show(id): + post = Post.query.filter_by(id=id) + flash(post) + + + + return render_template('post.html', post=post, tags=tags) + +from flask_login import current_user +from yadc import db +from yadc.forms import UploadPostForm +from yadc.models import Post, RATING, FILETYPE + +from flask import current_app +from PIL import Image +import io +import os + +# @bp.route('/post/new/') +# @bp.route('/post/create/') +@bp.route('/post/upload/', methods=['GET', 'POST']) +@login_required +def post_upload(): + form = UploadPostForm(request.form) + if request.method == 'POST' and form.validate(): + file = request.files.get(form.post_img.name) + file.data = io.BytesIO(file.read()) + + # tagy + + post = Post(file, source=form.sauce.data, 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) + + 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('main.post_upload')) + + return render_template('upload.html', form=form) + +from flask import send_from_directory + +@bp.route('/i/') +def uploaded_img(path, store='img', exten=None): + ext = os.path.splitext(path)[1] + + if exten is not None and ext.split('.')[1] != exten: + abort(404) + + filename = path.split('/')[0].split('.')[0] + # print(os.path.join(current_app.config.get('POST_UPLOADS'), store, "{}{}".format(filename, ext))) + return send_from_directory(os.path.join(current_app.config.get('POST_UPLOADS'), store), "{}{}".format(filename, ext)) + +@bp.route('/jpeg/') +def uploaded_jpeg(*args, **kwargs): + return uploaded_img(*args, **kwargs, store='jpeg', exten='jpg') + +@bp.route('/thumb/') +def uploaded_thumb(*args, **kwargs): + return uploaded_img(*args, **kwargs, store='thumb', exten='jpg') + + +@bp.route('/threads/') +def threads(): + return render_template('index.html') + +@bp.route('/user/show//') +def user_profile(username): + pass + +@bp.route('/user/settings/') +@login_required +def user_settings(): + pass \ No newline at end of file diff --git a/yadc/forms.py b/yadc/forms.py new file mode 100644 index 0000000..6e4b9a6 --- /dev/null +++ b/yadc/forms.py @@ -0,0 +1,81 @@ +from wtforms import Form +from wtforms import StringField, PasswordField, BooleanField, SubmitField, FileField, MultipleFileField, ValidationError, RadioField +from wtforms.validators import DataRequired, InputRequired, Email, EqualTo, AnyOf + +from werkzeug.utils import cached_property + +from flask import current_app +from flask_wtf.csrf import _FlaskFormCSRF + +class CSRFForm(Form): + class Meta: + csrf = True + csrf_class = _FlaskFormCSRF + + @cached_property + def csrf_secret(self): + return current_app.secret_key + + csrf_time_limit = 3600 + csrf_field_name = 'csrf_token' + +class LoginForm(CSRFForm): + username = StringField('Username', validators=[DataRequired()]) + password = PasswordField('Password', validators=[DataRequired()]) + remember_me = BooleanField('Remember me') + submit = SubmitField('Log In') + +from yadc.models import User + +class ResetPasswordForm(CSRFForm): + email = StringField('E-mail', validators=[DataRequired(), Email()]) + submit = SubmitField('Reset password') + + # def validate_email(form, field): + # email = User.query.filter_by(email=field.data).first() + # if not email: + # raise ValidationError('This') + +class RegisterForm(CSRFForm): + username = StringField('Username', validators=[DataRequired()]) + email = StringField('E-mail', validators=[DataRequired(), Email()]) + password = PasswordField('Password', validators=[DataRequired()]) + password_again = PasswordField('Repeat password', validators=[DataRequired(), EqualTo('password')]) + submit = SubmitField('Register') + + def validate_username(form, field): + user = User.query.filter_by(username=field.data).first() + if user is not None: + raise ValidationError('Username already taken. Try different username.') + + def validate_email(form, field): + email = User.query.filter_by(email=field.data).first() + if email is not None: + raise ValidationError('This email address is already registered. Maybe try logging in instead?') + +from flask import request +# from magic import Magic + +def validate_file(form, field): + file = request.files.get(field.name) + if not file or file.filename == '': + raise ValidationError('Please select a file') + +class UploadPostForm(CSRFForm): + post_img = FileField('Image', validators=[validate_file], render_kw={'required':''}) + sauce = StringField('Sauce', validators=[DataRequired()]) + tags = StringField('Tags') + rating = RadioField('Rating', + choices=[('safe', 'S'), ('questionable', 'Q'), ('explicit', 'E')], + default='safe', + validators=[DataRequired()]) + submit = SubmitField('Upload') + + def validate_post_img(form, field): + file = request.files.get(field.name) + + client_mimetype = file.mimetype + # Not sure if safe + # real_mimetype = Magic(mime=True).from_buffer(file.stream.read()) + if client_mimetype not in ['image/png','image/jpeg']: + raise ValidationError('Please select an image file of PNG or JPEG format') \ No newline at end of file diff --git a/yadc/main.py b/yadc/main.py deleted file mode 100644 index a9110a4..0000000 --- a/yadc/main.py +++ /dev/null @@ -1,40 +0,0 @@ -from flask import Blueprint, render_template, flash, redirect -from flask_login import login_required - -bp = Blueprint('main', __name__) - -@bp.route('/index') -def index(): - return render_template('index.html') - -@bp.route('/humm') -@login_required -def humm(): - return render_template('index.html') - -@bp.route('/') -@bp.route('/post') -def post(): - return render_template('index.html') - pass - -@bp.route('/post/show/') -def post_show(id): - return render_template('post.html') - pass - -@bp.route('/post/new') -@bp.route('/post/create') -@bp.route('/post/upload') -@login_required -def post_upload(): - pass - -@bp.route('/user/show/') -def user_profile(username): - pass - -@bp.route('/user/settings') -@login_required -def user_settings(): - pass \ No newline at end of file diff --git a/yadc/models.py b/yadc/models.py index 63f4ea5..cd3d067 100644 --- a/yadc/models.py +++ b/yadc/models.py @@ -1,8 +1,7 @@ import enum from datetime import datetime - -from flask_login import UserMixin -from werkzeug.security import check_password_hash, generate_password_hash +from sqlalchemy_utc import UtcDateTime, utcnow +# from sqlalchemy.sql.functions import now as current_timestamp from yadc import db, login @@ -13,6 +12,10 @@ class OP_LEVEL(enum.Enum): moderator = 5 admin = 9 +class USER_STATUS(enum.Enum): + active = 0 + banned = 1 + class FILETYPE(enum.Enum): png = 0 jpeg = 1 @@ -35,15 +38,21 @@ class TAG_CATEGORY(enum.Enum): character = 4 copyright = 5 -class User(UserMixin, db.Model): +class TimestampMixin(object): + created = db.Column(UtcDateTime, nullable=False, default=utcnow()) + updated = db.Column(UtcDateTime, nullable=False, onupdate=utcnow(), default=utcnow()) + +from flask_login import UserMixin, login_user, logout_user +from werkzeug.security import check_password_hash, generate_password_hash + +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) - created = db.Column(db.DateTime, default=datetime.now()) - updated = db.Column(db.DateTime, default=datetime.now()) - last_login = db.Column(db.DateTime) + user_status = db.Column(db.Enum(USER_STATUS), default=USER_STATUS.active, nullable=False) + last_login = db.Column(UtcDateTime) #authored_posts = db.relationship('Post', back_populates='author') #approved_posts = db.relationship('Post', back_populates='approver') @@ -57,6 +66,21 @@ class User(UserMixin, db.Model): def check_password(self, password): return check_password_hash(self.pass_hash, password) + def login(self, remember): + login_user(self, remember=remember) + self.last_login = utcnow() + + def logout(self): + logout_user() + + @property + def is_moderator(self): + return self.op_level in [OP_LEVEL.moderator, OP_LEVEL.admin] + + @property + def is_admin(self): + return self.op_level is OP_LEVEL.admin + @login.user_loader def load_user(id): return User.query.get(int(id)) @@ -65,13 +89,18 @@ post_tags = db.Table('post_tags', db.metadata, db.Column('post_id', db.Integer, db.ForeignKey('post.id')), db.Column('tag_id', db.Integer, db.ForeignKey('tag.id')) ) + +from PIL import Image +import hashlib +import os + +from werkzeug.utils import cached_property +from flask import current_app, url_for -class Post(db.Model): +class Post(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) md5 = db.Column(db.String(32), unique=True, nullable=False) filetype = db.Column(db.Enum(FILETYPE), nullable=False) - created = db.Column(db.DateTime, default=datetime.now()) - updated = db.Column(db.DateTime, default=datetime.now()) rating = db.Column(db.Enum(RATING), nullable=False) status = db.Column(db.Enum(POST_STATUS), default=POST_STATUS.pending, nullable=False) @@ -79,6 +108,9 @@ class Post(db.Model): height = db.Column(db.Integer, default=0) filesize = db.Column(db.Integer, default=0) + source = db.Column(db.String(2048)) + origin_filename = db.Column(db.String(255)) + author_id = db.Column(db.Integer, db.ForeignKey('user.id')) author = db.relationship('User', backref=db.backref('authored_posts', lazy=True), foreign_keys=[author_id]) approver_id = db.Column(db.Integer, db.ForeignKey('user.id')) @@ -87,18 +119,70 @@ class Post(db.Model): #tags = db.relationship('Tag', secondary=post_tags, back_populates='posts') tags = db.relationship('Tag', secondary=post_tags, backref=db.backref('posts')) -class Tag(db.Model): + def __init__(self, file, **kwargs): + super().__init__(**kwargs) + + self.md5 = hashlib.md5(file.data.getbuffer()).hexdigest() + self.filetype = FILETYPE[file.mimetype.split('/')[1]] + + with Image.open(file.data) as im: + self.width, self.height = im.width, im.height + self.filesize = file.data.getbuffer().nbytes + + self.origin_filename = file.filename + + def __repr__(self): + return(''.format(self.id, self.author, self.filetype.name, self.filesize)) + + @cached_property + def flex(self): + 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): + # filename = "{}.{}".format('maybe_later_generated_cute_filename', 'jpg' if self.filetype is FILETYPE.jpeg else 'png') + filename = 'maybe_later_generated_cute_filename' + return os.path.join(self.md5, filename) + + def url(self, path, endpoint='img'): + if endpoint == 'img': + return url_for('main.uploaded_img', path="{}.{}".format(path,'jpg' if self.filetype is FILETYPE.jpeg else 'png')) + elif endpoint == 'jpeg': + return url_for('main.uploaded_jpeg', path="{}.{}".format(path,'jpg')) + 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.instance_path, 'post', 'img', filename) + + @property + def jpeg_path(self): + if self.filetype is FILETYPE.jpeg: + return None + + jpeg_filename = "{}.{}".format(self.md5, 'jpg') + return os.path.join(current_app.instance_path, 'post', 'jpeg', jpeg_filename) + + @property + def thumb_path(self): + jpeg_filename = "{}.{}".format(self.md5, 'jpg') + return os.path.join(current_app.instance_path, 'post', 'thumb', jpeg_filename) + + +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) - created = db.Column(db.DateTime, default=datetime.now()) - updated = db.Column(db.DateTime, default=datetime.now()) #posts = db.relationship('Post', secondary=post_tags, back_populates='tags') -class Comment(db.Model): + def __repr__(self): + return ''.format(self.content, self.category.name) + +class Comment(TimestampMixin, db.Model): id = db.Column(db.Integer, primary_key=True) - created = db.Column(db.DateTime, default=datetime.now()) content = db.Column(db.String(512)) post_id = db.Column(db.Integer, db.ForeignKey('post.id')) diff --git a/yadc/static/default.scss b/yadc/static/default.scss index b22daff..4d40a48 100644 --- a/yadc/static/default.scss +++ b/yadc/static/default.scss @@ -152,11 +152,11 @@ header { position: relative; .user_dropdown { - //display: none; - opacity: .1; // BOI DAT HACK - height: 0; - width: 0; - overflow: hidden; + display: none; + // opacity: .1; // BOI DAT HACK + // height: 0; + // width: 0; + // overflow: hidden; a { padding: 10px; @@ -174,16 +174,16 @@ header { position: absolute; margin: 0; top: 100%; - left: 0; + // left: 0; right: 0; z-index: 10; background-color: $nav-bg-color; - opacity: 1; // BOI DAT HACK - height: unset; - width: unset; - transition: .2s ease; + // opacity: 1; // BOI DAT HACK + // height: unset; + // width: unset; + // transition: .2s ease; } } } @@ -251,9 +251,10 @@ header { padding: 10px; article { - &:not(:last-child) { - margin-bottom: 10px; - } + // &:not(:last-child) { + // margin-bottom: 10px; + // } + margin-bottom: 10px; } article.tags { @@ -329,7 +330,7 @@ header { &::after { content: ""; - flex: /*10000*/350 0 350px; + flex: 10000/*350*/ 0 350px; } > figure { diff --git a/yadc/templates/_formhelpers.html b/yadc/templates/_formhelpers.html new file mode 100644 index 0000000..35ae620 --- /dev/null +++ b/yadc/templates/_formhelpers.html @@ -0,0 +1,9 @@ +{% macro errors(field) %} +{% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+{% endif %} +{% endmacro %} \ No newline at end of file diff --git a/yadc/templates/base.html b/yadc/templates/base.html index c41e357..8fafdd0 100644 --- a/yadc/templates/base.html +++ b/yadc/templates/base.html @@ -13,21 +13,31 @@
-
+ {% with msgs = get_flashed_messages() %} + {% if msgs %} +
    + {% for msg in msgs %} +
  • {{ msg }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} +
-{% block content %}{% endblock %} + {% block content %}{% endblock %}