Compare commits
No commits in common. "536e49d1b9314604e2846d0740a377c3979c3512" and "c834424836eeaa9865014ec27e5a2c7af98dbc83" have entirely different histories.
536e49d1b9
...
c834424836
9 changed files with 51 additions and 128 deletions
|
|
@ -6,5 +6,4 @@ source .env && \
|
||||||
case "$1" in
|
case "$1" in
|
||||||
("+") pw_migrate create --auto --auto-source=coriplus.models --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;;
|
("+") pw_migrate create --auto --auto-source=coriplus.models --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;;
|
||||||
("@") pw_migrate migrate --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;;
|
("@") pw_migrate migrate --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;;
|
||||||
(\\) pw_migrate rollback --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;;
|
esac
|
||||||
esac
|
|
||||||
|
|
@ -24,7 +24,7 @@ from flask_wtf import CSRFProtect
|
||||||
import dotenv
|
import dotenv
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
__version__ = '0.10.0-dev45'
|
__version__ = '0.10.0-dev44'
|
||||||
|
|
||||||
# we want to support Python 3.10+ only.
|
# we want to support Python 3.10+ only.
|
||||||
# Python 2 has too many caveats.
|
# Python 2 has too many caveats.
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,17 @@ from functools import wraps
|
||||||
|
|
||||||
bp = Blueprint('admin', __name__, url_prefix='/admin')
|
bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||||
|
|
||||||
def _check_auth(username) -> bool:
|
def _check_auth(username, password) -> bool:
|
||||||
try:
|
try:
|
||||||
return User.get((User.username == username)).is_admin
|
return User.select().where((User.username == username) & (User.password == pwdhash(password)) & (User.is_admin)
|
||||||
|
).exists()
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def admin_required(f):
|
def admin_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapped_view(**kwargs):
|
def wrapped_view(**kwargs):
|
||||||
if not _check_auth(current_user.username):
|
if not _check_auth(current_user.username, current_user.password):
|
||||||
abort(403)
|
abort(403)
|
||||||
return f(**kwargs)
|
return f(**kwargs)
|
||||||
return wrapped_view
|
return wrapped_view
|
||||||
|
|
|
||||||
|
|
@ -29,25 +29,19 @@ class BaseModel(Model):
|
||||||
# A user. The user is separated from its page.
|
# A user. The user is separated from its page.
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
# The unique username.
|
# The unique username.
|
||||||
username = CharField(30, unique=True)
|
username = CharField(unique=True)
|
||||||
# The user's full name (here for better search since 0.8)
|
# The user's full name (here for better search since 0.8)
|
||||||
full_name = CharField(80)
|
full_name = TextField()
|
||||||
# The password hash.
|
# The password hash.
|
||||||
password = CharField(256)
|
password = CharField()
|
||||||
# An email address.
|
# An email address.
|
||||||
email = CharField(256)
|
email = CharField()
|
||||||
# The date of birth (required because of Terms of Service)
|
# The date of birth (required because of Terms of Service)
|
||||||
birthday = DateField()
|
birthday = DateField()
|
||||||
# The date joined
|
# The date joined
|
||||||
join_date = DateTimeField()
|
join_date = DateTimeField()
|
||||||
# A disabled flag. 0 = active, 1 = disabled by user, 2 = banned
|
# A disabled flag. 0 = active, 1 = disabled by user, 2 = banned
|
||||||
is_disabled = IntegerField(default=0)
|
is_disabled = IntegerField(default=0)
|
||||||
# Short description of user.
|
|
||||||
biography = CharField(256, default='')
|
|
||||||
# Personal website.
|
|
||||||
website = TextField(null=True)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Helpers for flask_login
|
# Helpers for flask_login
|
||||||
def get_id(self):
|
def get_id(self):
|
||||||
|
|
@ -116,12 +110,15 @@ class UserAdminship(BaseModel):
|
||||||
# User profile.
|
# User profile.
|
||||||
# Additional info for identifying users.
|
# Additional info for identifying users.
|
||||||
# New in 0.6
|
# New in 0.6
|
||||||
# Deprecated in 0.10 and merged with User
|
|
||||||
class UserProfile(BaseModel):
|
class UserProfile(BaseModel):
|
||||||
user = ForeignKeyField(User, primary_key=True)
|
user = ForeignKeyField(User, primary_key=True)
|
||||||
biography = TextField(default='')
|
biography = TextField(default='')
|
||||||
location = IntegerField(null=True)
|
location = IntegerField(null=True)
|
||||||
|
year = IntegerField(null=True)
|
||||||
website = TextField(null=True)
|
website = TextField(null=True)
|
||||||
|
instagram = TextField(null=True)
|
||||||
|
facebook = TextField(null=True)
|
||||||
|
telegram = TextField(null=True)
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self):
|
||||||
'''
|
'''
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@
|
||||||
--accent: #f0372e;
|
--accent: #f0372e;
|
||||||
--link: #3399ff;
|
--link: #3399ff;
|
||||||
}
|
}
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
body, button, input, select, textarea {
|
body, button, input, select, textarea {
|
||||||
font-family: Inter, Roboto, sans-serif;
|
font-family: Inter, Roboto, sans-serif;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|
@ -41,13 +38,13 @@ a:hover{text-decoration:underline}
|
||||||
#site-name {text-align: center;flex: 1}
|
#site-name {text-align: center;flex: 1}
|
||||||
.header h1{margin:0;display:inline-block}
|
.header h1{margin:0;display:inline-block}
|
||||||
.flash{background-color:#ff9;border:yellow 1px solid}
|
.flash{background-color:#ff9;border:yellow 1px solid}
|
||||||
.infobox{width: 50%; float: right;}
|
.infobox{padding:12px;border:#ccc 1px solid}
|
||||||
@media (max-width:639px) {
|
@media (min-width:640px) {
|
||||||
.infobox{width: 100%;}
|
.infobox{float:right;width:320px}
|
||||||
}
|
}
|
||||||
.weak{opacity:.5}
|
.weak{opacity:.5}
|
||||||
.field_desc{display:block}
|
.field_desc{display:block}
|
||||||
ul.timeline{padding:0;margin:auto;max-width:960px;clear: both}
|
ul.timeline{padding:0;margin:auto;max-width:960px}
|
||||||
ul.timeline > li{list-style:none;}
|
ul.timeline > li{list-style:none;}
|
||||||
.message-visual img{max-width:100%;margin:auto}
|
.message-visual img{max-width:100%;margin:auto}
|
||||||
.message-options-showhide::before{content:'\2026'}
|
.message-options-showhide::before{content:'\2026'}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,26 @@
|
||||||
|
{% set profile = user.profile %}
|
||||||
<div class="card infobox">
|
<div class="infobox">
|
||||||
<h3>{{ user.full_name }}</h3>
|
<h3>{{ profile.full_name }}</h3>
|
||||||
<p>{{ user.biography|enrich }}</p>
|
<p>{{ profile.biography|enrich }}</p>
|
||||||
{% if user.website %}
|
{% if profile.location %}
|
||||||
{% set website = user.website %}
|
<p><span class="weak">Location:</span> {{ profile.location|locationdata }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if profile.year %}
|
||||||
|
<p><span class="weak">Year:</span> {{ profile.year }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if profile.website %}
|
||||||
|
{% set website = profile.website %}
|
||||||
{% set website = website if website.startswith(('http://', 'https://')) else 'http://' + website %}
|
{% set website = website if website.startswith(('http://', 'https://')) else 'http://' + website %}
|
||||||
<p><span class="weak">Website:</span> {{ website|urlize }}</p>
|
<p><span class="weak">Website:</span> {{ profile.website|urlize }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if profile.instagram %}
|
||||||
|
<p><span class="weak">Instagram:</span> <a href="https://www.instagram.com/{{ profile.instagram }}">{{ profile.instagram }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if profile.facebook %}
|
||||||
|
<p><span class="weak">Facebook:</span> <a href="https://facebook.com/{{ profile.facebook }}">{{ profile.facebook }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if profile.telegram %}
|
||||||
|
<p><span class="weak">Telegram:</span> <a href="https://t.me/{{ profile.facebook }}">{{ profile.telegram }}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ user.messages|count }}</strong> messages
|
<strong>{{ user.messages|count }}</strong> messages
|
||||||
|
|
@ -15,6 +30,6 @@
|
||||||
<a href="{{ url_for('website.user_following', username=user.username) }}"><strong>{{ user.following()|count }}</strong></a> following
|
<a href="{{ url_for('website.user_following', username=user.username) }}"><strong>{{ user.following()|count }}</strong></a> following
|
||||||
</p>
|
</p>
|
||||||
{% if user == current_user %}
|
{% if user == current_user %}
|
||||||
<p><a href="/edit_profile/">{{ inline_svg('edit') }} Edit profile</a></p>
|
<p><a href="/edit_profile/">{{ inline_svg('edit', 18) }} Edit profile</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ A list of utilities used across modules.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import datetime, re, base64, hashlib, string, sys, json
|
import datetime, re, base64, hashlib, string, sys, json
|
||||||
|
|
||||||
from flask_login import current_user
|
|
||||||
from .models import User, Message, Notification, MSGPRV_PUBLIC, MSGPRV_UNLISTED, \
|
from .models import User, Message, Notification, MSGPRV_PUBLIC, MSGPRV_UNLISTED, \
|
||||||
MSGPRV_FRIENDS, MSGPRV_ONLYME
|
MSGPRV_FRIENDS, MSGPRV_ONLYME
|
||||||
from flask import abort, render_template, request, session
|
from flask import abort, render_template, request, session
|
||||||
|
|
@ -104,14 +102,15 @@ except OSError:
|
||||||
|
|
||||||
# get the user from the session
|
# get the user from the session
|
||||||
# changed in 0.5 to comply with flask_login
|
# changed in 0.5 to comply with flask_login
|
||||||
# DEPRECATED in 0.10; use current_user instead
|
|
||||||
def get_current_user():
|
def get_current_user():
|
||||||
# new in 0.7; need a different method to get current user id
|
# new in 0.7; need a different method to get current user id
|
||||||
if request.path.startswith('/api/'):
|
if request.path.startswith('/api/'):
|
||||||
# assume token validation is already done
|
# assume token validation is already done
|
||||||
return User[request.args['access_token'].split(':')[0]]
|
return User[request.args['access_token'].split(':')[0]]
|
||||||
elif current_user.is_authenticated:
|
else:
|
||||||
return current_user
|
user_id = session.get('user_id')
|
||||||
|
if user_id:
|
||||||
|
return User[user_id]
|
||||||
|
|
||||||
def push_notification(type, target, **kwargs):
|
def push_notification(type, target, **kwargs):
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from .models import *
|
||||||
from . import __version__ as app_version
|
from . import __version__ as app_version
|
||||||
from sys import version as python_version
|
from sys import version as python_version
|
||||||
from flask import Blueprint, abort, flash, redirect, render_template, request, url_for, __version__ as flask_version
|
from flask import Blueprint, abort, flash, redirect, render_template, request, url_for, __version__ as flask_version
|
||||||
from flask_login import current_user, login_required, login_user, logout_user
|
from flask_login import login_required, login_user, logout_user
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ bp = Blueprint('website', __name__)
|
||||||
|
|
||||||
@bp.route('/')
|
@bp.route('/')
|
||||||
def homepage():
|
def homepage():
|
||||||
if current_user and current_user.is_authenticated:
|
if get_current_user():
|
||||||
return private_timeline()
|
return private_timeline()
|
||||||
else:
|
else:
|
||||||
return render_template('homepage.html')
|
return render_template('homepage.html')
|
||||||
|
|
@ -26,7 +26,7 @@ def private_timeline():
|
||||||
# the private timeline (aka feed) exemplifies the use of a subquery -- we are asking for
|
# the private timeline (aka feed) exemplifies the use of a subquery -- we are asking for
|
||||||
# messages where the person who created the message is someone the current
|
# messages where the person who created the message is someone the current
|
||||||
# user is following. these messages are then ordered newest-first.
|
# user is following. these messages are then ordered newest-first.
|
||||||
user = current_user
|
user = get_current_user()
|
||||||
messages = Visibility(Message
|
messages = Visibility(Message
|
||||||
.select()
|
.select()
|
||||||
.where((Message.user << user.following())
|
.where((Message.user << user.following())
|
||||||
|
|
@ -83,9 +83,6 @@ def register():
|
||||||
|
|
||||||
@bp.route('/login/', methods=['GET', 'POST'])
|
@bp.route('/login/', methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
if current_user and current_user.is_authenticated:
|
|
||||||
flash('You are already logged in')
|
|
||||||
return redirect(request.args.get('next', '/'))
|
|
||||||
if request.method == 'POST' and request.form['username']:
|
if request.method == 'POST' and request.form['username']:
|
||||||
try:
|
try:
|
||||||
username = request.form['username']
|
username = request.form['username']
|
||||||
|
|
@ -137,12 +134,11 @@ def user_follow(username):
|
||||||
from_user=cur_user,
|
from_user=cur_user,
|
||||||
to_user=user,
|
to_user=user,
|
||||||
created_date=datetime.datetime.now())
|
created_date=datetime.datetime.now())
|
||||||
push_notification('follow', user, user=cur_user.id)
|
|
||||||
flash('You are now following %s' % user.username)
|
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
flash(f'Error following {user.username}')
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
flash('You are following %s' % user.username)
|
||||||
|
push_notification('follow', user, user=cur_user.id)
|
||||||
return redirect(url_for('website.user_detail', username=user.username))
|
return redirect(url_for('website.user_detail', username=user.username))
|
||||||
|
|
||||||
@bp.route('/+<username>/unfollow/', methods=['POST'])
|
@bp.route('/+<username>/unfollow/', methods=['POST'])
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
"""Peewee migrations -- 002_move_columns_from_userprofile.py.
|
|
||||||
|
|
||||||
Some examples (model - class or model name)::
|
|
||||||
|
|
||||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
|
||||||
> Model = migrator.ModelClass # Return model in current state by name
|
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
|
||||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
|
||||||
> migrator.change_fields(model, **fields) # Change fields
|
|
||||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
|
||||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
|
||||||
> migrator.rename_table(model, new_table_name)
|
|
||||||
> migrator.add_index(model, *col_names, unique=False)
|
|
||||||
> migrator.add_not_null(model, *field_names)
|
|
||||||
> migrator.add_default(model, field_name, default)
|
|
||||||
> migrator.add_constraint(model, name, sql)
|
|
||||||
> migrator.drop_index(model, *col_names)
|
|
||||||
> migrator.drop_not_null(model, *field_names)
|
|
||||||
> migrator.drop_constraints(model, *constraints)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from contextlib import suppress
|
|
||||||
|
|
||||||
import peewee as pw
|
|
||||||
from peewee_migrate import Migrator
|
|
||||||
|
|
||||||
|
|
||||||
with suppress(ImportError):
|
|
||||||
import playhouse.postgres_ext as pw_pext
|
|
||||||
|
|
||||||
|
|
||||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
|
||||||
"""Write your migrations here."""
|
|
||||||
|
|
||||||
migrator.add_fields(
|
|
||||||
'user',
|
|
||||||
|
|
||||||
biography=pw.CharField(max_length=256, default=""),
|
|
||||||
website=pw.TextField(null=True))
|
|
||||||
|
|
||||||
migrator.change_fields('user', username=pw.CharField(max_length=30, unique=True))
|
|
||||||
|
|
||||||
migrator.change_fields('user', full_name=pw.CharField(max_length=80))
|
|
||||||
|
|
||||||
migrator.change_fields('user', password=pw.CharField(max_length=256))
|
|
||||||
|
|
||||||
migrator.change_fields('user', email=pw.CharField(max_length=256))
|
|
||||||
|
|
||||||
migrator.sql("""
|
|
||||||
UPDATE "user" SET biography = (SELECT p.biography FROM userprofile p WHERE p.user_id = id LIMIT 1),
|
|
||||||
website = (SELECT p.website FROM userprofile p WHERE p.user_id = id LIMIT 1);
|
|
||||||
""")
|
|
||||||
|
|
||||||
migrator.remove_fields('userprofile', 'year', 'instagram', 'facebook', 'telegram')
|
|
||||||
|
|
||||||
|
|
||||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
|
||||||
"""Write your rollback migrations here."""
|
|
||||||
|
|
||||||
migrator.add_fields(
|
|
||||||
'userprofile',
|
|
||||||
|
|
||||||
year=pw.IntegerField(null=True),
|
|
||||||
instagram=pw.TextField(null=True),
|
|
||||||
facebook=pw.TextField(null=True),
|
|
||||||
telegram=pw.TextField(null=True))
|
|
||||||
|
|
||||||
migrator.remove_fields('user', 'biography', 'website')
|
|
||||||
|
|
||||||
migrator.change_fields('user', username=pw.CharField(max_length=255, unique=True))
|
|
||||||
|
|
||||||
migrator.change_fields('user', full_name=pw.TextField())
|
|
||||||
|
|
||||||
migrator.change_fields('user', password=pw.CharField(max_length=255))
|
|
||||||
|
|
||||||
migrator.change_fields('user', email=pw.CharField(max_length=255))
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue