schema change; added flask-login
This commit is contained in:
parent
3f867b4027
commit
a646c96b86
8 changed files with 100 additions and 74 deletions
|
|
@ -1,6 +1,11 @@
|
||||||
# Changelog
|
# 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.
|
* Adding quick mention. You can now create a message mentioning another user in one click.
|
||||||
* Added mention notifications.
|
* Added mention notifications.
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@ Based on Tweepee example of [peewee](https://github.com/coleifer/peewee/).
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
* **Python 3** only. We don't want to support Python 2.
|
* **Python 3** only. We don't want to support Python 2.
|
||||||
* **Flask** web framework.
|
* **Flask** web framework (also required extension **Flask-Login**).
|
||||||
* **Peewee** ORM.
|
* **Peewee** ORM.
|
||||||
|
|
|
||||||
125
app.py
125
app.py
|
|
@ -5,17 +5,27 @@ import hashlib
|
||||||
from peewee import *
|
from peewee import *
|
||||||
import datetime, time, re, os, sys, string, json
|
import datetime, time, re, os, sys, string, json
|
||||||
from functools import wraps
|
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.
|
# we want to support Python 3 only.
|
||||||
# Python 2 has too many caveats.
|
# Python 2 has too many caveats.
|
||||||
if sys.version_info[0] < 3:
|
if sys.version_info[0] < 3:
|
||||||
raise RuntimeError('Python 3 required')
|
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 = Flask(__name__)
|
||||||
app.config.from_pyfile('config.py')
|
app.config.from_pyfile('config.py')
|
||||||
|
|
||||||
|
login_manager = LoginManager(app)
|
||||||
|
|
||||||
### DATABASE ###
|
### DATABASE ###
|
||||||
|
|
||||||
database = SqliteDatabase(app.config['DATABASE'])
|
database = SqliteDatabase(app.config['DATABASE'])
|
||||||
|
|
@ -39,6 +49,19 @@ class User(BaseModel):
|
||||||
# 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)
|
||||||
|
|
||||||
|
# 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
|
# it often makes sense to put convenience methods on model instances, for
|
||||||
# example, "give me all the users this user is following":
|
# example, "give me all the users this user is following":
|
||||||
def following(self):
|
def following(self):
|
||||||
|
|
@ -71,27 +94,24 @@ class User(BaseModel):
|
||||||
(Notification.target == self) & (Notification.seen == 0)
|
(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.
|
# A single public message.
|
||||||
|
# New in v0.5: removed type and info fields; added privacy field.
|
||||||
class Message(BaseModel):
|
class Message(BaseModel):
|
||||||
# The type of the message.
|
|
||||||
type = TextField()
|
|
||||||
# The user who posted the message.
|
# The user who posted the message.
|
||||||
user = ForeignKeyField(User, backref='messages')
|
user = ForeignKeyField(User, backref='messages')
|
||||||
# The text of the message.
|
# The text of the message.
|
||||||
text = TextField()
|
text = TextField()
|
||||||
# Additional info (in JSON format)
|
|
||||||
# TODO: remove because it's dumb.
|
|
||||||
info = TextField(default='{}')
|
|
||||||
# The posted date.
|
# The posted date.
|
||||||
pub_date = DateTimeField()
|
pub_date = DateTimeField()
|
||||||
# Info about privacy of the message.
|
# Info about privacy of the message.
|
||||||
@property
|
privacy = IntegerField(default=MSGPRV_PUBLIC)
|
||||||
def privacy(self):
|
|
||||||
try:
|
|
||||||
return MessagePrivacy.get(MessagePrivacy.message == self).value
|
|
||||||
except MessagePrivacy.DoesNotExist:
|
|
||||||
# default to public
|
|
||||||
return MSGPRV_PUBLIC
|
|
||||||
def is_visible(self, is_public_timeline=False):
|
def is_visible(self, is_public_timeline=False):
|
||||||
user = self.user
|
user = self.user
|
||||||
cur_user = get_current_user()
|
cur_user = get_current_user()
|
||||||
|
|
@ -112,20 +132,6 @@ class Message(BaseModel):
|
||||||
else:
|
else:
|
||||||
return False
|
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
|
# 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
|
# 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
|
# 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():
|
def create_tables():
|
||||||
with database:
|
with database:
|
||||||
database.create_tables([
|
database.create_tables([
|
||||||
User, Message, Relationship, Upload, Notification, MessagePrivacy])
|
User, Message, Relationship, Upload, Notification])
|
||||||
if not os.path.isdir(UPLOAD_DIRECTORY):
|
if not os.path.isdir(UPLOAD_DIRECTORY):
|
||||||
os.makedirs(UPLOAD_DIRECTORY)
|
os.makedirs(UPLOAD_DIRECTORY)
|
||||||
|
|
||||||
|
|
@ -263,30 +269,14 @@ class Visibility(object):
|
||||||
yield i
|
yield i
|
||||||
counter += 1
|
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
|
# get the user from the session
|
||||||
|
# changed in 0.5 to comply with flask_login
|
||||||
def get_current_user():
|
def get_current_user():
|
||||||
if session.get('logged_in'):
|
user_id = session.get('user_id')
|
||||||
return User.get(User.id == session['user_id'])
|
if user_id:
|
||||||
|
return User[user_id]
|
||||||
|
|
||||||
# view decorator which indicates that the requesting user must be authenticated
|
login_manager.login_view = 'login'
|
||||||
# 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
|
|
||||||
|
|
||||||
def push_notification(type, target, **kwargs):
|
def push_notification(type, target, **kwargs):
|
||||||
try:
|
try:
|
||||||
|
|
@ -337,9 +327,9 @@ def after_request(response):
|
||||||
g.db.close()
|
g.db.close()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@app.context_processor
|
@login_manager.user_loader
|
||||||
def _inject_user():
|
def _inject_user(userid):
|
||||||
return {'current_user': get_current_user()}
|
return User[userid]
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def error_404(body):
|
def error_404(body):
|
||||||
|
|
@ -347,7 +337,7 @@ def error_404(body):
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def homepage():
|
def homepage():
|
||||||
if session.get('logged_in'):
|
if get_current_user():
|
||||||
return private_timeline()
|
return private_timeline()
|
||||||
else:
|
else:
|
||||||
return render_template('homepage.html')
|
return render_template('homepage.html')
|
||||||
|
|
@ -395,7 +385,7 @@ def register():
|
||||||
join_date=datetime.datetime.now())
|
join_date=datetime.datetime.now())
|
||||||
|
|
||||||
# mark the user as being 'authenticated' by setting the session vars
|
# mark the user as being 'authenticated' by setting the session vars
|
||||||
auth_user(user)
|
login_user(user)
|
||||||
return redirect(request.args.get('next','/'))
|
return redirect(request.args.get('next','/'))
|
||||||
|
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
|
|
@ -419,13 +409,18 @@ def login():
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
flash('A user with this username or email does not exist.')
|
flash('A user with this username or email does not exist.')
|
||||||
else:
|
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 redirect(request.args.get('next', '/'))
|
||||||
return render_template('login.html')
|
return render_template('login.html')
|
||||||
|
|
||||||
@app.route('/logout/')
|
@app.route('/logout/')
|
||||||
def logout():
|
def logout():
|
||||||
session.pop('logged_in', None)
|
logout_user()
|
||||||
flash('You were logged out')
|
flash('You were logged out')
|
||||||
return redirect(request.args.get('next','/'))
|
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:
|
# the messages -- user.message_set. could also have written it as:
|
||||||
# Message.select().where(Message.user == user)
|
# Message.select().where(Message.user == user)
|
||||||
messages = Visibility(user.messages.order_by(Message.pub_date.desc()))
|
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)
|
return object_list('user_detail.html', messages, 'message_list', user=user)
|
||||||
|
|
||||||
@app.route('/+<username>/follow/', methods=['POST'])
|
@app.route('/+<username>/follow/', methods=['POST'])
|
||||||
|
|
@ -455,7 +451,6 @@ def user_follow(username):
|
||||||
|
|
||||||
flash('You are following %s' % user.username)
|
flash('You are following %s' % user.username)
|
||||||
push_notification('follow', user, user=cur_user.id)
|
push_notification('follow', user, user=cur_user.id)
|
||||||
# TODO change to "profile.html"
|
|
||||||
return redirect(url_for('user_detail', username=user.username))
|
return redirect(url_for('user_detail', username=user.username))
|
||||||
|
|
||||||
@app.route('/+<username>/unfollow/', methods=['POST'])
|
@app.route('/+<username>/unfollow/', methods=['POST'])
|
||||||
|
|
@ -485,11 +480,8 @@ def create():
|
||||||
type='text',
|
type='text',
|
||||||
user=user,
|
user=user,
|
||||||
text=text,
|
text=text,
|
||||||
pub_date=datetime.datetime.now())
|
pub_date=datetime.datetime.now(),
|
||||||
MessagePrivacy.create(
|
privacy=privacy)
|
||||||
message=message,
|
|
||||||
value=privacy
|
|
||||||
)
|
|
||||||
file = request.files.get('file')
|
file = request.files.get('file')
|
||||||
if file:
|
if file:
|
||||||
print('Uploading', file.filename)
|
print('Uploading', file.filename)
|
||||||
|
|
@ -558,8 +550,9 @@ def uploads(id, type='jpg'):
|
||||||
|
|
||||||
@app.route('/ajax/username_availability/<username>')
|
@app.route('/ajax/username_availability/<username>')
|
||||||
def username_availability(username):
|
def username_availability(username):
|
||||||
if session.get('logged_in'):
|
current = get_current_user()
|
||||||
current = get_current_user().username
|
if current:
|
||||||
|
current = current.username
|
||||||
else:
|
else:
|
||||||
current = None
|
current = None
|
||||||
is_valid = is_username(username)
|
is_valid = is_username(username)
|
||||||
|
|
@ -585,5 +578,7 @@ def is_following(from_user, to_user):
|
||||||
|
|
||||||
# allow running from the command line
|
# allow running from the command line
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
args = arg_parser.parse_args()
|
||||||
create_tables()
|
create_tables()
|
||||||
app.run()
|
if not args.norun:
|
||||||
|
app.run(port=args.port)
|
||||||
|
|
|
||||||
15
migrate_0_4_to_0_5.py
Normal file
15
migrate_0_4_to_0_5.py
Normal 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;
|
||||||
|
''')
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
flask
|
flask>=1.1.1
|
||||||
peewee
|
peewee>=3.11.1
|
||||||
|
flask-login>=0.4.1
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,14 @@
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1><a href="{{ url_for('homepage') }}">{{ site_name }}</a></h1>
|
<h1><a href="{{ url_for('homepage') }}">{{ site_name }}</a></h1>
|
||||||
<div class="metanav">
|
<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('login') }}">log in</a>
|
||||||
<a href="{{ url_for('register') }}">register</a>
|
<a href="{{ url_for('register') }}">register</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('user_detail', username=current_user.username) }}">{{ current_user.username }}</a>
|
<a href="{{ url_for('user_detail', username=current_user.username) }}">{{ current_user.username }}</a>
|
||||||
{% set notification_count = current_user.unseen_notification_count() %}
|
{% set notification_count = current_user.unseen_notification_count() %}
|
||||||
{% if notification_count > 0 %}
|
{% if notification_count > 0 %}
|
||||||
<a href="{{ url_for('notifications') }}">({{ notification_count }})</a>
|
<a href="{{ url_for('notifications') }}">(<strong>{{ notification_count }}</strong>)</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
-
|
-
|
||||||
<a href="{{ url_for('public_timeline') }}">explore</a>
|
<a href="{{ url_for('public_timeline') }}">explore</a>
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,19 @@
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2>Login</h2>
|
<h2>Login</h2>
|
||||||
{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
|
{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
|
||||||
<form action="{{ url_for('login') }}" method="POST">
|
<form method="POST">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Username or email:
|
<dt>Username or email:
|
||||||
<dd><input type="text" name="username">
|
<dd><input type="text" name="username">
|
||||||
<dt>Password:
|
<dt>Password:
|
||||||
<dd><input type="password" name="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">
|
<dd><input type="submit" value="Login">
|
||||||
</dl>
|
</dl>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
-
|
-
|
||||||
<strong>{{ user.following()|count }}</strong> following
|
<strong>{{ user.following()|count }}</strong> following
|
||||||
</p>
|
</p>
|
||||||
{% if current_user %}
|
{% if not current_user.is_anonymous %}
|
||||||
{% if user.username != current_user.username %}
|
{% if user.username != current_user.username %}
|
||||||
{% if current_user|is_following(user) %}
|
{% if current_user|is_following(user) %}
|
||||||
<form action="{{ url_for('user_unfollow', username=user.username) }}" method="post">
|
<form action="{{ url_for('user_unfollow', username=user.username) }}" method="post">
|
||||||
|
|
@ -20,6 +20,9 @@
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p><a href="/create/?preload=%2B{{ user.username }}">Mention this user in a message</a></p>
|
<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 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue