From a646c96b865a8ef8b82abe8b539abda700207627 Mon Sep 17 00:00:00 2001 From: Mattia Succurro Date: Sat, 12 Oct 2019 19:22:10 +0200 Subject: [PATCH] schema change; added flask-login --- CHANGELOG.md | 7 +- README.md | 2 +- app.py | 127 ++++++++++++++++++------------------- migrate_0_4_to_0_5.py | 15 +++++ requirements.txt | 5 +- templates/base.html | 4 +- templates/login.html | 9 ++- templates/user_detail.html | 5 +- 8 files changed, 100 insertions(+), 74 deletions(-) create mode 100644 migrate_0_4_to_0_5.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ba9a021..d30ccf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 53f8595..5602072 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/app.py b/app.py index e0a484d..855254c 100644 --- a/app.py +++ b/app.py @@ -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('/+/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('/+/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/') 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) diff --git a/migrate_0_4_to_0_5.py b/migrate_0_4_to_0_5.py new file mode 100644 index 0000000..d64d23c --- /dev/null +++ b/migrate_0_4_to_0_5.py @@ -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; +''') diff --git a/requirements.txt b/requirements.txt index 1efada2..e21ebfc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ -flask -peewee +flask>=1.1.1 +peewee>=3.11.1 +flask-login>=0.4.1 diff --git a/templates/base.html b/templates/base.html index 99e540a..15f48c0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -12,14 +12,14 @@

{{ site_name }}

- {% if not session.logged_in %} + {% if current_user.is_anonymous %} log in register {% else %} {{ current_user.username }} {% set notification_count = current_user.unseen_notification_count() %} {% if notification_count > 0 %} - ({{ notification_count }}) + ({{ notification_count }}) {% endif %} - explore diff --git a/templates/login.html b/templates/login.html index ef76e39..d1dbaae 100644 --- a/templates/login.html +++ b/templates/login.html @@ -2,12 +2,19 @@ {% block body %}

Login

{% if error %}

Error: {{ error }}{% endif %} -

+
Username or email:
Password:
+
Remember me for: +
diff --git a/templates/user_detail.html b/templates/user_detail.html index ac24f11..a21dd17 100644 --- a/templates/user_detail.html +++ b/templates/user_detail.html @@ -8,7 +8,7 @@ - {{ user.following()|count }} following

- {% if current_user %} + {% if not current_user.is_anonymous %} {% if user.username != current_user.username %} {% if current_user|is_following(user) %}
@@ -20,6 +20,9 @@
{% endif %}

Mention this user in a message

+ {% else %} + + Create a status {% endif %} {% endif %}