schema change; added flask-login

This commit is contained in:
Yusur 2019-10-12 19:22:10 +02:00
parent 3f867b4027
commit a646c96b86
8 changed files with 100 additions and 74 deletions

View file

@ -1,6 +1,11 @@
# Changelog
## 0.4
## 0.5-dev
* Removed `type` and `info` fields from `Message` table and merged `privacy` field, previously into a separate table, into that table. In order to make the app work, when upgrading you should run the `migrate_0_4_to_0_5.py` script.
* Added flask-login dependency. Now, user logins can be persistent up to 365 days.
## 0.4.0
* Adding quick mention. You can now create a message mentioning another user in one click.
* Added mention notifications.

View file

@ -17,5 +17,5 @@ Based on Tweepee example of [peewee](https://github.com/coleifer/peewee/).
## Requirements
* **Python 3** only. We don't want to support Python 2.
* **Flask** web framework.
* **Flask** web framework (also required extension **Flask-Login**).
* **Peewee** ORM.

127
app.py
View file

@ -5,17 +5,27 @@ import hashlib
from peewee import *
import datetime, time, re, os, sys, string, json
from functools import wraps
import argparse
from flask_login import LoginManager, login_user, logout_user, login_required
__version__ = '0.4.0'
__version__ = '0.5-dev'
# we want to support Python 3 only.
# Python 2 has too many caveats.
if sys.version_info[0] < 3:
raise RuntimeError('Python 3 required')
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument('--norun', action='store_true',
help='Don\'t run the app. Useful for debugging.')
arg_parser.add_argument('-p', '--port', type=int, default=5000,
help='The port where to run the app. Defaults to 5000')
app = Flask(__name__)
app.config.from_pyfile('config.py')
login_manager = LoginManager(app)
### DATABASE ###
database = SqliteDatabase(app.config['DATABASE'])
@ -39,6 +49,19 @@ class User(BaseModel):
# A disabled flag. 0 = active, 1 = disabled by user, 2 = banned
is_disabled = IntegerField(default=0)
# Helpers for flask_login
def get_id(self):
return str(self.id)
@property
def is_active(self):
return not self.is_disabled
@property
def is_anonymous(self):
return False
@property
def is_authenticated(self):
return self == get_current_user()
# it often makes sense to put convenience methods on model instances, for
# example, "give me all the users this user is following":
def following(self):
@ -71,27 +94,24 @@ class User(BaseModel):
(Notification.target == self) & (Notification.seen == 0)
))
# The message privacy values.
MSGPRV_PUBLIC = 0 # everyone
MSGPRV_UNLISTED = 1 # everyone, doesn't show up in public timeline
MSGPRV_FRIENDS = 2 # only accounts which follow each other
MSGPRV_ONLYME = 3 # only the poster
# A single public message.
# New in v0.5: removed type and info fields; added privacy field.
class Message(BaseModel):
# The type of the message.
type = TextField()
# The user who posted the message.
user = ForeignKeyField(User, backref='messages')
# The text of the message.
text = TextField()
# Additional info (in JSON format)
# TODO: remove because it's dumb.
info = TextField(default='{}')
# The posted date.
pub_date = DateTimeField()
# Info about privacy of the message.
@property
def privacy(self):
try:
return MessagePrivacy.get(MessagePrivacy.message == self).value
except MessagePrivacy.DoesNotExist:
# default to public
return MSGPRV_PUBLIC
privacy = IntegerField(default=MSGPRV_PUBLIC)
def is_visible(self, is_public_timeline=False):
user = self.user
cur_user = get_current_user()
@ -112,20 +132,6 @@ class Message(BaseModel):
else:
return False
# The message privacy values.
MSGPRV_PUBLIC = 0 # everyone
MSGPRV_UNLISTED = 1 # everyone, doesn't show up in public timeline
MSGPRV_FRIENDS = 2 # only accounts which follow each other
MSGPRV_ONLYME = 3 # only the poster
# Doing it into a separate table to don't worry about schema change.
# Added in v0.4.
class MessagePrivacy(BaseModel):
# The message.
message = ForeignKeyField(Message, primary_key=True)
# The privacy value. Needs to be one of these above.
value = IntegerField()
# this model contains two foreign keys to user -- it essentially allows us to
# model a "many-to-many" relationship between users. by querying and joining
# on different columns we can expose who a user is "related to" and who is
@ -162,7 +168,7 @@ class Notification(BaseModel):
def create_tables():
with database:
database.create_tables([
User, Message, Relationship, Upload, Notification, MessagePrivacy])
User, Message, Relationship, Upload, Notification])
if not os.path.isdir(UPLOAD_DIRECTORY):
os.makedirs(UPLOAD_DIRECTORY)
@ -263,30 +269,14 @@ class Visibility(object):
yield i
counter += 1
# flask provides a "session" object, which allows us to store information across
# requests (stored by default in a secure cookie). this function allows us to
# mark a user as being logged-in by setting some values in the session data:
def auth_user(user):
session['logged_in'] = True
session['user_id'] = user.id
session['username'] = user.username
flash('You are logged in as %s' % (user.username))
# get the user from the session
# changed in 0.5 to comply with flask_login
def get_current_user():
if session.get('logged_in'):
return User.get(User.id == session['user_id'])
user_id = session.get('user_id')
if user_id:
return User[user_id]
# view decorator which indicates that the requesting user must be authenticated
# before they can access the view. it checks the session to see if they're
# logged in, and if not redirects them to the login view.
def login_required(f):
@wraps(f)
def inner(*args, **kwargs):
if not session.get('logged_in'):
return redirect(url_for('login'))
return f(*args, **kwargs)
return inner
login_manager.login_view = 'login'
def push_notification(type, target, **kwargs):
try:
@ -337,9 +327,9 @@ def after_request(response):
g.db.close()
return response
@app.context_processor
def _inject_user():
return {'current_user': get_current_user()}
@login_manager.user_loader
def _inject_user(userid):
return User[userid]
@app.errorhandler(404)
def error_404(body):
@ -347,7 +337,7 @@ def error_404(body):
@app.route('/')
def homepage():
if session.get('logged_in'):
if get_current_user():
return private_timeline()
else:
return render_template('homepage.html')
@ -395,7 +385,7 @@ def register():
join_date=datetime.datetime.now())
# mark the user as being 'authenticated' by setting the session vars
auth_user(user)
login_user(user)
return redirect(request.args.get('next','/'))
except IntegrityError:
@ -419,13 +409,18 @@ def login():
except User.DoesNotExist:
flash('A user with this username or email does not exist.')
else:
auth_user(user)
remember_for = int(request.form['remember'])
if remember_for > 0:
login_user(user, remember=True,
duration=datetime.timedelta(days=remember_for))
else:
login_user(user)
return redirect(request.args.get('next', '/'))
return render_template('login.html')
@app.route('/logout/')
def logout():
session.pop('logged_in', None)
logout_user()
flash('You were logged out')
return redirect(request.args.get('next','/'))
@ -437,6 +432,7 @@ def user_detail(username):
# the messages -- user.message_set. could also have written it as:
# Message.select().where(Message.user == user)
messages = Visibility(user.messages.order_by(Message.pub_date.desc()))
# TODO change to "profile.html"
return object_list('user_detail.html', messages, 'message_list', user=user)
@app.route('/+<username>/follow/', methods=['POST'])
@ -455,7 +451,6 @@ def user_follow(username):
flash('You are following %s' % user.username)
push_notification('follow', user, user=cur_user.id)
# TODO change to "profile.html"
return redirect(url_for('user_detail', username=user.username))
@app.route('/+<username>/unfollow/', methods=['POST'])
@ -485,11 +480,8 @@ def create():
type='text',
user=user,
text=text,
pub_date=datetime.datetime.now())
MessagePrivacy.create(
message=message,
value=privacy
)
pub_date=datetime.datetime.now(),
privacy=privacy)
file = request.files.get('file')
if file:
print('Uploading', file.filename)
@ -558,8 +550,9 @@ def uploads(id, type='jpg'):
@app.route('/ajax/username_availability/<username>')
def username_availability(username):
if session.get('logged_in'):
current = get_current_user().username
current = get_current_user()
if current:
current = current.username
else:
current = None
is_valid = is_username(username)
@ -585,5 +578,7 @@ def is_following(from_user, to_user):
# allow running from the command line
if __name__ == '__main__':
args = arg_parser.parse_args()
create_tables()
app.run()
if not args.norun:
app.run(port=args.port)

15
migrate_0_4_to_0_5.py Normal file
View file

@ -0,0 +1,15 @@
import config, sqlite3
conn = sqlite3.connect(config.DATABASE)
if __name__ == '__main__':
conn.executescript('''
BEGIN TRANSACTION;
CREATE TABLE new_message ("id" INTEGER NOT NULL PRIMARY KEY, "user_id" INTEGER NOT NULL, "text" TEXT NOT NULL, "pub_date" DATETIME NOT NULL, "privacy" INTEGER DEFAULT 0, FOREIGN KEY ("user_id") REFERENCES "user" ("id"));
INSERT INTO new_message (id, user_id, text, pub_date, privacy) SELECT t1.id, t1.user_id, t1.text, t1.pub_date, t2.value FROM message AS t1 LEFT JOIN messageprivacy AS t2 ON t2.message_id = t1.id;
UPDATE new_message SET privacy = 0 WHERE privacy IS NULL;
DROP TABLE message;
DROP TABLE messageprivacy;
ALTER TABLE new_message RENAME TO message;
COMMIT;
''')

View file

@ -1,2 +1,3 @@
flask
peewee
flask>=1.1.1
peewee>=3.11.1
flask-login>=0.4.1

View file

@ -12,14 +12,14 @@
<div class="header">
<h1><a href="{{ url_for('homepage') }}">{{ site_name }}</a></h1>
<div class="metanav">
{% if not session.logged_in %}
{% if current_user.is_anonymous %}
<a href="{{ url_for('login') }}">log in</a>
<a href="{{ url_for('register') }}">register</a>
{% else %}
<a href="{{ url_for('user_detail', username=current_user.username) }}">{{ current_user.username }}</a>
{% set notification_count = current_user.unseen_notification_count() %}
{% if notification_count > 0 %}
<a href="{{ url_for('notifications') }}">({{ notification_count }})</a>
<a href="{{ url_for('notifications') }}">(<strong>{{ notification_count }}</strong>)</a>
{% endif %}
-
<a href="{{ url_for('public_timeline') }}">explore</a>

View file

@ -2,12 +2,19 @@
{% block body %}
<h2>Login</h2>
{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
<form action="{{ url_for('login') }}" method="POST">
<form method="POST">
<dl>
<dt>Username or email:
<dd><input type="text" name="username">
<dt>Password:
<dd><input type="password" name="password">
<dt>Remember me for:
<dd><select name="remember">
<option value="0">This session only</option>
<option value="7">A week</option>
<option value="30">A month</option>
<option value="365">A year</option>
</select></dd>
<dd><input type="submit" value="Login">
</dl>
</form>

View file

@ -8,7 +8,7 @@
-
<strong>{{ user.following()|count }}</strong> following
</p>
{% if current_user %}
{% if not current_user.is_anonymous %}
{% if user.username != current_user.username %}
{% if current_user|is_following(user) %}
<form action="{{ url_for('user_unfollow', username=user.username) }}" method="post">
@ -20,6 +20,9 @@
</form>
{% endif %}
<p><a href="/create/?preload=%2B{{ user.username }}">Mention this user in a message</a></p>
{% else %}
<!-- here should go the "edit profile" button -->
<a href="/create/">Create a status</a>
{% endif %}
{% endif %}
<ul>