From dddff20bfabbb007ea39993fef802d5ce599d7d9 Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Thu, 30 Sep 2021 12:59:06 +0900 Subject: [PATCH 01/18] Use SQLAlchemy --- .gitignore | 2 + db.py | 24 ++-- follower.py | 74 +++++++---- login.py | 38 +++--- main.py | 254 +++++++++++++++++------------------- static/lion.jpg | Bin 8946 -> 0 bytes static/lion_200x200.jpeg | Bin 29345 -> 0 bytes static/panda.jpg | Bin 10901 -> 0 bytes templates/chosing_link.html | 9 -- templates/profile.html | 2 +- templates/tweet.html | 13 +- tweet.py | 178 +++++++++++++++++-------- tweet_like.py | 61 ++++++--- user.py | 49 ++++++- user_info.py | 118 +++++++++-------- 15 files changed, 482 insertions(+), 340 deletions(-) delete mode 100644 static/lion.jpg delete mode 100644 static/lion_200x200.jpeg delete mode 100644 static/panda.jpg delete mode 100644 templates/chosing_link.html diff --git a/.gitignore b/.gitignore index 49974f9..d360a47 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ __pycache__ volumes/ .secrets/ +.static/* +!.static/150x150.png diff --git a/db.py b/db.py index d1757dd..88f0f57 100644 --- a/db.py +++ b/db.py @@ -1,12 +1,18 @@ -import mysql.connector +from flask_sqlalchemy import SQLAlchemy class Database: - def __init__(self): - self.db = mysql.connector.connect( - host = 'localhost', - user = 'root', - password = '', - database = 'twitter_development' - ) + _instance = None + app = None + database = None - self.cursor = self.db.cursor() \ No newline at end of file + def __new__(klass, flask_app): + if klass._instance is None: + klass.app = flask_app + klass._instance = super(Database, klass).__new__(klass) + klass.__initialize(klass) + return klass._instance + + def __initialize(klass): + klass.app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+mysqlconnector://root@localhost:3306/twitter_development' + klass.app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + klass.database = SQLAlchemy(klass.app) diff --git a/follower.py b/follower.py index 5f81926..66015e6 100644 --- a/follower.py +++ b/follower.py @@ -1,27 +1,51 @@ +from flask import json +from user import Users from db import Database -class Follower: - def __init__(self): - db = Database() - self.db = db.db - self.cursor = db.cursor - - def follow(self, user_id, follow_id): - sql = 'SELECT COUNT(id) FROM followers WHERE user_id = %s AND follow_id = %s' % (user_id, follow_id) - self.cursor.execute(sql) - total = dict(zip(self.cursor.column_names, self.cursor.fetchone())) - if total['COUNT(id)'] > 0: - return False - else: - next_sql = 'INSERT INTO followers (user_id, follow_id) VALUES (%s, %s)' % (user_id, follow_id) - - self.cursor.execute(next_sql) - self.db.commit() - return True - - def unfollow(self, user_id, follow_id): - sql = 'DELETE FROM followers WHERE user_id = %s AND follow_id = %s' % (user_id, follow_id) - self.cursor.execute(sql) - self.db.commit() - - return True \ No newline at end of file +db = Database.database + +class Followers(db.Model): + id = db.Column(db.Integer, primary_key = True) + user_id = db.Column(db.Integer, db.ForeignKey(Users.id), nullable = True) + follow_id = db.Column(db.Integer, db.ForeignKey(Users.id), nullable = True) + + def __init__(self, data): + self.user_id = data['user_id'] + self.follow_id = data['follow_id'] + + @classmethod + def search(self, data): + return Followers.query.filter_by(user_id = data['user_id'], follow_id = data['follow_id']).first() + + def create(self): + db.session.add(self) + db.session.commit() + return self + + def delete(self): + db.session.delete(self) + db.session.commit() + return self + + def to_json(self): + return json.dumps(self, default = lambda o: '', sort_keys = True, indent = 4) + + # def follow(self, user_id, follow_id): + # sql = 'SELECT COUNT(id) FROM followers WHERE user_id = %s AND follow_id = %s' % (user_id, follow_id) + # self.cursor.execute(sql) + # total = dict(zip(self.cursor.column_names, self.cursor.fetchone())) + # if total['COUNT(id)'] > 0: + # return False + # else: + # next_sql = 'INSERT INTO followers (user_id, follow_id) VALUES (%s, %s)' % (user_id, follow_id) + + # self.cursor.execute(next_sql) + # self.db.commit() + # return True + + # def unfollow(self, user_id, follow_id): + # sql = 'DELETE FROM followers WHERE user_id = %s AND follow_id = %s' % (user_id, follow_id) + # self.cursor.execute(sql) + # self.db.commit() + + # return True \ No newline at end of file diff --git a/login.py b/login.py index 3f4fb3b..b08c8e3 100644 --- a/login.py +++ b/login.py @@ -1,31 +1,23 @@ -from db import Database -import hashlib +from user_info import UserInfos +from user import Users class Login: def __init__(self): - db = Database() - self.db = db.db - self.cursor = db.cursor + pass def signup(self, email, password): - self.cursor.execute('SELECT COUNT(*) FROM users WHERE email = "%s" AND password = "%s"' % (email, self.__to_sha256(password))) - (total,) = self.cursor.fetchone() - if total <= 0: - self.cursor.execute('INSERT INTO users (email, password) VALUES ("%s", "%s")' % (email, self.__to_sha256(password))) - self.db.commit() - self.cursor.execute('SELECT * FROM users WHERE email = "%s"' % email) - (id, email, _) = self.cursor.fetchone() - return (id, email) + exists_user = Users.search({ 'email': email, 'password': password}) + if exists_user: + return exists_user else: - return (None, None) - + u = Users({ 'email': email, 'password': password }) + created_user = u.create() + UserInfos.initial_to_create(created_user.id) + return created_user + def login(self, email, password): - self.cursor.execute('SELECT * FROM users LEFT JOIN user_infos ON users.id = user_infos.user_id WHERE email = "%s" AND password = "%s"' % (email, self.__to_sha256(password))) - (id, email, _, _, _, display_name, user_name, interests, _, _) = self.cursor.fetchone() - if id: - return (id, email, display_name, user_name, interests) + exists_user = Users.search({ 'email': email, 'password': password}) + if exists_user: + return exists_user else: - return False - - def __to_sha256(self, password): - return hashlib.sha256(f'{password}'.encode()).hexdigest() \ No newline at end of file + return None diff --git a/main.py b/main.py index c830219..b9a22ca 100644 --- a/main.py +++ b/main.py @@ -1,18 +1,21 @@ -from operator import itemgetter +from db import Database +from flask import Flask, render_template, redirect, url_for, request, jsonify + +app = Flask(__name__) +database = Database(app) + +import sqlalchemy import os -from re import U -from follower import Follower -from tweet_like import TweetLike +import json +from flask.globals import session from flask.helpers import flash from werkzeug.utils import secure_filename -from tweet import Tweet -from user_info import UserInfo -from flask import Flask, render_template, redirect, url_for, request, jsonify -from flask.globals import session +from tweet import Tweets +from user_info import UserInfos from login import Login -import json +from follower import Followers +from tweet_like import TweetLikes -app = Flask(__name__) UPLOAD_FOLDER = './static/' ALLOWED_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif'] @@ -22,7 +25,6 @@ @app.route('/') def home(): __remove_session() - session = None return render_template('sign_up.html') @app.route('/sign_up', methods = ['POST']) @@ -32,12 +34,17 @@ def sign_up(): if (email is None or email == '') or (password is None or password == ''): flash('email or password are not filled') return redirect(url_for('home')) + l = Login() - db_id, db_email = l.signup(email, password) - if db_id: - session['user_id'] = db_id - session['email'] = db_email - return redirect(url_for('tweets')) + try: + user = l.signup(email, password) + except sqlalchemy.exc.IntegrityError: + flash('Duplicate the email, please use another email') + return redirect(url_for('home')) + + if user: + __add_session({ 'user_id': user.id, 'email': user.email }) + return redirect(url_for('profile')) else: flash('The email is exists already') return redirect(url_for('login')) @@ -47,20 +54,24 @@ def login(): if request.method == 'POST': email = request.form['email'] password = request.form['password'] - l = Login() - (id, db_email, display_name, user_name, _) = l.login(email, password) if email == None: - return redirect(url_for('sign_up')) - elif db_email: - session['user_id'] = id - session['email'] = db_email - if display_name is not None: - session['name'] = display_name - else: - session['name'] = user_name + return redirect(url_for('home')) + + l = Login() + user = l.login(email, password) + if user: + __add_session({ + 'user_id': user.id, + 'email': user.email, + 'display_name': user.user_info.display_name, + 'user_name': user.user_info.user_name, + 'age': user.user_info.age, + 'interests': user.user_info.interests, + 'profile_image': user.user_info.profile_image + }) return redirect(url_for('tweets')) else: - return redirect(url_for('sign_up')) + return redirect(url_for('home')) elif request.method == 'GET': return render_template('sign_up.html') @@ -69,168 +80,139 @@ def logout(): __remove_session() return redirect(url_for('login')) -@app.route('/chosing_link') -def chosing_link(): - if session['name'] is not None: - name = session['name'] - else: - name = session['email'] - return render_template('chosing_link.html', name = name) - @app.route('/profile') def profile(): if session['user_id'] is None: return redirect(url_for('home')) - u = UserInfo() - id = session['user_id'] - user = u.get_user_info(id) - __add_session(user) + + user_info = UserInfos.search({ 'user_id': session['user_id'] }) + __add_session({ + 'user_id': user_info.user_id, + 'display_name': user_info.display_name, + 'user_name': user_info.user_name, + 'age': user_info.age, + 'interests': user_info.interests, + 'profile_image': user_info.profile_image + }) return render_template('profile.html') -@app.route('/profile/new', methods = ['POST']) -def profile_new(): - u = UserInfo() - id = session['user_id'] - - display_name = request.form['display_name'] or '' - user_name = request.form['user_name'] or '' - age = request.form['age'] or 0 - interests = request.form['interests'] or '' - u.insert_user_info({ 'user_id': id, 'display_name': display_name, 'user_name': user_name, 'age': age, 'interests': interests}) - __add_session(u.get_user_info(id)) - return redirect(url_for('profile')) +# @app.route('/profile/new', methods = ['POST']) +# def profile_new(): +# u = UserInfo() +# id = session['user_id'] + +# display_name = request.form['display_name'] or '' +# user_name = request.form['user_name'] or '' +# age = request.form['age'] or 0 +# interests = request.form['interests'] or '' +# u.insert_user_info({ 'user_id': id, 'display_name': display_name, 'user_name': user_name, 'age': age, 'interests': interests}) +# __add_session(u.get_user_info(id)) +# return redirect(url_for('profile')) @app.route('/profile//edit', methods = ['POST']) def profile_edit(user_id): - u = UserInfo() - id = user_id - display_name = request.form['display_name'] user_name = request.form['user_name'] - age = request.form['age'] or 0 + age = request.form['age'] email = request.form['email'] interests = request.form['interests'] - u.update_user_info({ 'id': id, 'display_name': display_name, 'email': email, 'user_name': user_name, 'age': age, 'interests': interests}) - __add_session(u.get_user_info(id)) + u = UserInfos.search({ 'user_id': user_id }) + updated = u.update({ 'display_name': display_name, 'user_name': user_name, 'age': age, 'email': email, 'interests': interests}) + __add_session({ + 'user_id': updated.user_id, + 'display_name': updated.display_name, + 'user_name': updated.user_name, + 'age': updated.age, + 'interests': updated.interests, + 'profile_image': updated.profile_image, + 'email': updated.user.email + }) return redirect(url_for('profile')) @app.route('/profile//upload_image', methods = ['POST']) def upload_image(user_id): - u = UserInfo() filename = __profile_image(request.files['profile_image']) - u.update_profile_image(user_id, filename) - __add_session_only_profile_image(filename) + u = UserInfos.search({ 'user_id': user_id }) + u.upload_image(filename) + __add_session({ 'profile_image': filename }) return redirect(url_for('profile')) @app.route('/profile//get_image') def get_image(user_id): - u = UserInfo() + u = UserInfos.search({ 'user_id': user_id }) result = { 'data': { - 'profile_image': u.get_profile_image(user_id) + 'profile_image': u.get_image() } } return json.dumps(result) @app.route('/tweets') def tweets(): - t = Tweet() - session['tweets'] = t.get_tweets({ 'user_id': session['user_id'] }) + session['tweets'] = Tweets.default_tweets(session['user_id']) return render_template('tweet.html') @app.route('/tweets/new', methods = ['POST']) def tweets_new(): - id = session['user_id'] - - message = request.form['message'] - t = Tweet() - t.add_tweet({ 'user_id': id, 'message': message }) + t = Tweets({ 'user_id': session['user_id'], 'message': request.form['message'] }) + t.create() return redirect(url_for('tweets')) @app.route('/tweets/', methods = ['POST']) def tweets_edit(message_id): - t = Tweet() - if t.invisible_tweet(message_id): - result = { - 'code': 200, - 'message': 'OK' - } - else: - result = { - 'code': 503, - 'message': 'It happened something' - } - return jsonify(values = json.dumps(result)) + t = Tweets.search(message_id) + tweet = t.invisible() + return tweet.to_json() @app.route('/tweets/', methods = ['DELETE']) def tweets_delete(message_id): - t = Tweet() - if t.delete_tweet(message_id): - result = { - 'code': 200, - 'message': 'OK' - } - else: - result = { - 'code': 503, - 'message': 'It happened something' - } - return jsonify(values = json.dumps(result)) + t = Tweets.search(message_id) + tweet = t.delete() + return tweet.to_json() @app.route('/tweets//likes', methods = ['POST']) def tweet_likes(message_id): - tl = TweetLike() - if tl.favorite(message_id, session['user_id']): - result = { - 'code': 200, - 'message': 'OK' - } + data = { 'user_id': session['user_id'], 'tweet_id': message_id } + tl = TweetLikes.search(data) + if tl: + v = tl.delete() else: - result = { - 'code': 503, - 'message': 'It happened something' - } - return json.dumps(result) + new = TweetLikes(data) + v = new.create() + return v.to_json() @app.route('/followers/', methods = ['POST']) def follow(follow_id): - user_id = session['user_id'] - f = Follower() - if f.follow(user_id, follow_id): - result = { - 'code': 200, - 'message': 'OK' - } - elif f.unfollow(user_id, follow_id): - result = { - 'code': 200, - 'message': 'OK' - } + data = { 'user_id': session['user_id'], 'follow_id': follow_id } + f = Followers.search(data) + if f: + v = f.delete() else: - result = { - 'code': 503, - 'message': 'It happened something' - } - return json.dumps(result) - -def __add_session_only_profile_image(filename): - session['profile_image'] = filename + new = Followers(data) + v = new.create() + return v.to_json() def __add_session(data): - id, email, user_info_id, display_name, user_name, age, interests, profile_image = data - session['user_id'] = id - session['email'] = email - session['user_info_id'] = user_info_id - session['display_name'] = display_name - session['user_name'] = user_name - session['interests'] = interests - session['profile_image'] = profile_image - session['age'] = age + if 'user_id' in data: + session['user_id'] = data['user_id'] + if 'email' in data: + session['email'] = data['email'] + if 'user_info_id' in data: + session['user_info_id'] = data['user_info_id'] + if 'display_name' in data: + session['display_name'] = data['display_name'] + if 'user_name' in data: + session['user_name'] = data['user_name'] + if 'interests' in data: + session['interests'] = data['interests'] + if 'profile_image' in data: + session['profile_image'] = data['profile_image'] + if 'age' in data: + session['age'] = data['age'] def __remove_session(): - for key in session: - session[key] = None + session = None def __allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @@ -245,4 +227,4 @@ def __profile_image(file): if __name__ == '__main__': app.secret_key = 'test' app.config['SESSION_TYPE'] = 'filesystem' - app.run(debug=True) + app.run(debug=True) \ No newline at end of file diff --git a/static/lion.jpg b/static/lion.jpg deleted file mode 100644 index 1d5b7a7ed1abb3e4a66e4797901c8bf8e416c7e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8946 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�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 diff --git a/static/lion_200x200.jpeg b/static/lion_200x200.jpeg deleted file mode 100644 index 9ac21c5b8a493073481df7d898bc31bbde6733b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29345 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

fL^_7xavi7Lv{qc0xcp!k*lu2aVf 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 diff --git a/static/panda.jpg b/static/panda.jpg deleted file mode 100644 index c5acc660af424b28586a4e4690ddd73b1c2d4e7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10901 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 diff --git a/templates/chosing_link.html b/templates/chosing_link.html deleted file mode 100644 index 283f6cb..0000000 --- a/templates/chosing_link.html +++ /dev/null @@ -1,9 +0,0 @@ -

diff --git a/templates/profile.html b/templates/profile.html index 945da71..832f360 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -62,7 +62,7 @@ fetch(`/profile/${userId}/get_image`) .then((res) => res.json()) .then((json) => { - const src = `/static/${json.data.profile_image[0]}` + const src = `/static/${json.data.profile_image}` $profileImage.src = src; }) } diff --git a/templates/tweet.html b/templates/tweet.html index 5485c39..5aeded9 100644 --- a/templates/tweet.html +++ b/templates/tweet.html @@ -14,6 +14,7 @@
    + {% set login_user_id = session['user_id'] %} {% for t in session['tweets'] %} {% set id = t['id'] %} {% set tweet_user_id = t['user_id'] %} @@ -24,10 +25,10 @@ {% set user_name = t['user_name'] %} {% set age = t['age'] %} {% set interests = t['interests'] %} - {% set image = url_for('static', filename = t['profile_image'] or '150x150.png') %} + {% set image = url_for('static', filename = t['profile_image']) %} {% set pushed_user_id = t['pushed_user_id'] %} {% set following = t['following'] %} - {% set is_current_user = session['user_id'] == tweet_user_id %} + {% set is_current_user = login_user_id == tweet_user_id %} {% if is_visible: %}
  • @@ -36,7 +37,7 @@ {% if not is_current_user %} {% if following %}
    - +
    {% else %}
    @@ -56,11 +57,11 @@
  • - {{ display_name or user_name }} + {{ display_name or user_name }} {% if not is_current_user %} {% if following %}
    - +
    {% else %}
    @@ -116,6 +117,8 @@ document.querySelectorAll(`button[data-follow-id='${followId}']`).forEach((el) => { el.classList.toggle('btn-primary') el.classList.toggle('btn-outline-primary') + if (el.innerHTML === 'Following') el.innerHTML = 'Add Follow' + else el.innerHTML = 'Following' }) } }); diff --git a/tweet.py b/tweet.py index 157c4e2..926649d 100644 --- a/tweet.py +++ b/tweet.py @@ -1,74 +1,148 @@ +from flask import json +from follower import Followers +from tweet_like import TweetLikes +from user_info import UserInfos +from user import Users from db import Database -from operator import itemgetter -class Tweet: - def __init__(self): - db = Database() - self.db = db.db - self.cursor = db.cursor +db = Database.database - def get_tweets(self, data): - current_user_id = itemgetter('user_id')(data) - sql = 'SELECT * FROM tweets LEFT JOIN user_infos ON tweets.user_id = user_infos.user_id \ - LEFT JOIN tweet_likes ON tweets.id = tweet_likes.tweet_id ORDER BY tweets.id DESC' - self.cursor.execute(sql) - tweets = self.cursor.fetchall() +class Tweets(db.Model): + id = db.Column(db.Integer, primary_key = True) + user_id = db.Column(db.Integer, db.ForeignKey(Users.id), unique = True) + message = db.Column(db.String(140), nullable = True) + is_visible = db.Column(db.Boolean, nullable = True, default = True) + following = False - second_sql = 'SELECT * FROM followers WHERE user_id = %s' % current_user_id - self.cursor.execute(second_sql) - followers = self.cursor.fetchall() + user = db.relationship('Users', backref = 'users') + tweet_likes = db.relationship('TweetLikes', back_populates = 'tweet') + tweet_like = db.relationship('TweetLikes', uselist = False, back_populates = 'tweet') + + def __repr__(self): + return '' \ + % (self.id, self.user_id, self.message, self.is_visible) + + def __init__(self, data): + self.user_id = data['user_id'] + self.message = data['message'] + + @classmethod + def default_tweets(self, user_id): + tweets = Tweets.query \ + .join(UserInfos, UserInfos.user_id == Tweets.user_id) \ + .join(TweetLikes, isouter = True) \ + .order_by(db.desc(Tweets.id)) \ + .all() + followers = Followers.query.filter_by(user_id = user_id).all() result = [] for t in tweets: dic = {} - id, user_id, message, is_visible, user_info_id, _, display_name, user_name, age, interests, profile_image, _, pushed_user_id, _ = t for f in followers: - _, user_id_on_followers, follow_id = f - if current_user_id == user_id_on_followers and follow_id == user_id: + if user_id == f.user_id and f.follow_id == t.user_id: dic['following'] = True else: dic['following'] = False - dic['id'] = id - dic['user_id'] = user_id - dic['message'] = message - dic['is_visible'] = is_visible - dic['user_info_id'] = user_info_id - dic['display_name'] = display_name - dic['user_name'] = user_name - dic['age'] = age - dic['interests'] = interests - dic['profile_image'] = profile_image - dic['pushed_user_id'] = pushed_user_id + dic['id'] = t.id + dic['user_id'] = t.user_id + dic['message'] = t.message + dic['is_visible'] = t.is_visible + user_info = t.user.user_info + dic['user_info_id'] = user_info.id + dic['display_name'] = user_info.display_name + dic['user_name'] = user_info.user_name + dic['age'] = user_info.age + dic['interests'] = user_info.interests + dic['profile_image'] = user_info.profile_image + dic['pushed_user_id'] = t.tweet_likes[0].user_id if t.tweet_likes else 0 result.append(dic) return result + + @classmethod + def search(self, id): + return Tweets.query.filter_by(id = id).first() + + def create(self): + db.session.add(self) + db.session.commit() + return self + + def delete(self): + db.session.delete(self) + db.session.commit() + return self + + def invisible(self): + self.is_visible = not self.is_visible + db.session.commit() + return self + + def to_json(self): + return json.dumps(self, default = lambda o: '', sort_keys = True, indent = 4) + # def get_tweets(self, data): + # current_user_id = itemgetter('user_id')(data) + # sql = 'SELECT * FROM tweets LEFT JOIN user_infos ON tweets.user_id = user_infos.user_id \ + # LEFT JOIN tweet_likes ON tweets.id = tweet_likes.tweet_id ORDER BY tweets.id DESC' + # self.cursor.execute(sql) + # tweets = self.cursor.fetchall() + + # second_sql = 'SELECT * FROM followers WHERE user_id = %s' % current_user_id + # self.cursor.execute(second_sql) + # followers = self.cursor.fetchall() + + # result = [] + # for t in tweets: + # dic = {} + # id, user_id, message, is_visible, user_info_id, _, display_name, user_name, age, interests, profile_image, _, pushed_user_id, _ = t + # for f in followers: + # _, user_id_on_followers, follow_id = f + # if current_user_id == user_id_on_followers and follow_id == user_id: + # dic['following'] = True + # else: + # dic['following'] = False + + # dic['id'] = id + # dic['user_id'] = user_id + # dic['message'] = message + # dic['is_visible'] = is_visible + # dic['user_info_id'] = user_info_id + # dic['display_name'] = display_name + # dic['user_name'] = user_name + # dic['age'] = age + # dic['interests'] = interests + # dic['profile_image'] = profile_image + # dic['pushed_user_id'] = pushed_user_id + # result.append(dic) + + # return result - def add_tweet(self, data): - user_id, message = itemgetter('user_id', 'message')(data) - sql = 'INSERT INTO tweets (user_id, message) VALUES (%s, "%s")' % (user_id, message) - self.cursor.execute(sql) - self.db.commit() + # def add_tweet(self, data): + # user_id, message = itemgetter('user_id', 'message')(data) + # sql = 'INSERT INTO tweets (user_id, message) VALUES (%s, "%s")' % (user_id, message) + # self.cursor.execute(sql) + # self.db.commit() - return True + # return True - def invisible_tweet(self, id): - sql = 'SELECT COUNT(id) FROM tweets WHERE is_visible = 1 AND id = %s' % id - self.cursor.execute(sql) - total = self.cursor.fetchone() - if total[0] > 0: - next_sql = 'UPDATE tweets SET is_visible = 0 WHERE id = %s' % id - else: - next_sql = 'UPDATE tweets SET is_visible = 1 WHERE id = %s' % id - - self.cursor.execute(next_sql) - self.db.commit() + # def invisible_tweet(self, id): + # sql = 'SELECT COUNT(id) FROM tweets WHERE is_visible = 1 AND id = %s' % id + # self.cursor.execute(sql) + # total = self.cursor.fetchone() + # if total[0] > 0: + # next_sql = 'UPDATE tweets SET is_visible = 0 WHERE id = %s' % id + # else: + # next_sql = 'UPDATE tweets SET is_visible = 1 WHERE id = %s' % id + + # self.cursor.execute(next_sql) + # self.db.commit() - return True + # return True - def delete_tweet(self, id): - sql = 'DELETE FROM tweets WHERE id = %s' % id - self.cursor.execute(sql) - self.db.commit() + # def delete_tweet(self, id): + # sql = 'DELETE FROM tweets WHERE id = %s' % id + # self.cursor.execute(sql) + # self.db.commit() - return True \ No newline at end of file + # return True \ No newline at end of file diff --git a/tweet_like.py b/tweet_like.py index bb817e5..b43f4fc 100644 --- a/tweet_like.py +++ b/tweet_like.py @@ -1,22 +1,51 @@ +from flask import json +from user import Users from db import Database -class TweetLike: - def __init__(self): - db = Database() - self.db = db.db - self.cursor = db.cursor +db = Database.database +class TweetLikes(db.Model): + id = db.Column(db.Integer, primary_key = True) + user_id = db.Column(db.Integer, db.ForeignKey(Users.id), nullable = True) + tweet_id = db.Column(db.Integer, db.ForeignKey('tweets.id'), nullable = True) - def favorite(self, id, pushed_user_id): - sql = 'SELECT COUNT(id) FROM tweet_likes WHERE user_id = %s AND tweet_id = %s' % (pushed_user_id, id) - self.cursor.execute(sql) - total = self.cursor.fetchone() - if (total[0] > 0): - next_sql = 'DELETE FROM tweet_likes WHERE user_id = %s AND tweet_id = %s' % (pushed_user_id, id) - else: - next_sql = 'INSERT INTO tweet_likes (user_id, tweet_id) VALUES (%s, %s)' % (pushed_user_id, id) + user = db.relationship('Users', backref = 'user') + tweet = db.relationship('Tweets', uselist = False, back_populates = 'tweet_likes') - self.cursor.execute(next_sql) - self.db.commit() + def __repr__(self): + return '' % (self.id, self.user_id, self.tweet_id) - return True \ No newline at end of file + def __init__(self, data): + self.user_id = data['user_id'] + self.tweet_id = data['tweet_id'] + + @classmethod + def search(self, data): + return TweetLikes.query.filter_by(user_id = data['user_id'], tweet_id = data['tweet_id']).first() + + def create(self): + db.session.add(self) + db.session.commit() + return self + + def delete(self): + db.session.delete(self) + db.session.commit() + return self + + def to_json(self): + return json.dumps(self, default = lambda o: '', sort_keys = True, indent = 4) + + # def favorite(self, id, pushed_user_id): + # sql = 'SELECT COUNT(id) FROM tweet_likes WHERE user_id = %s AND tweet_id = %s' % (pushed_user_id, id) + # self.cursor.execute(sql) + # total = self.cursor.fetchone() + # if (total[0] > 0): + # next_sql = 'DELETE FROM tweet_likes WHERE user_id = %s AND tweet_id = %s' % (pushed_user_id, id) + # else: + # next_sql = 'INSERT INTO tweet_likes (user_id, tweet_id) VALUES (%s, %s)' % (pushed_user_id, id) + + # self.cursor.execute(next_sql) + # self.db.commit() + + # return True \ No newline at end of file diff --git a/user.py b/user.py index fcf175f..5871145 100644 --- a/user.py +++ b/user.py @@ -1,13 +1,48 @@ from db import Database +import hashlib -class User: - def __init__(self): - db = Database() - self.db = db.db - self.cursor = db.cursor +db = Database.database + +class Users(db.Model): + id = db.Column(db.Integer, primary_key = True) + email = db.Column(db.String(256), unique = True, nullable = True) + password = db.Column(db.String(256), unique = True, nullable = True) + + user_info = db.relationship('UserInfos', uselist = False, back_populates = 'user') + + def __repr__(self): + return '' % (self.id, self.email, self.password) + + def __init__(self, data): + self.email = data['email'] + self.password = self.__to_sha256(data['password']) + + @classmethod + def search(self, data): + return Users.query.filter_by(email = data['email'], password = self.__to_sha256(self, data['password'])).limit(1).first() + + @classmethod + def search_and_user_infos(self, data): + return Users.query.filter_by(email = data['email'], password = self.__to_sha256(self, data['password'])).limit(1).first() + + def create(self): + db.session.add(self) + db.session.commit() + return self + + def update(self, data): + if 'email' in data and data['email'] != '': + self.email = data['email'] + if 'password' in data: + self.password = self.__to_sha256(data['password']) + db.session.commit() + return self def update_user(self, id, email): self.cursor.execute('UPDATE users SET email = "%s" WHERE id = %s' % (email, id)) self.db.commit() - - return True \ No newline at end of file + + return True + + def __to_sha256(self, password): + return hashlib.sha256(f'{password}'.encode()).hexdigest() \ No newline at end of file diff --git a/user_info.py b/user_info.py index 9eec858..0302d79 100644 --- a/user_info.py +++ b/user_info.py @@ -1,67 +1,71 @@ -from operator import itemgetter -from user import User +from user import Users from db import Database -class UserInfo: - def __init__(self): - db = Database() - self.db = db.db - self.cursor = db.cursor +db = Database.database - def get_user_info(self, user_id): - self.cursor.execute('SELECT * FROM users LEFT JOIN user_infos ON users.id = user_infos.user_id WHERE users.id = %s' % user_id) - (id, email, _, user_info_id, _, display_name, user_name, interests, profile_image, age) = self.cursor.fetchone() - return (id, email, user_info_id, display_name, user_name, interests, profile_image, age) +class UserInfos(db.Model): + id = db.Column(db.Integer, primary_key = True) + user_id = db.Column(db.Integer, db.ForeignKey(Users.id), unique = True) + display_name = db.Column(db.String(80)) + user_name = db.Column(db.String(80)) + age = db.Column(db.Integer, default = 0) + interests = db.Column(db.String(256)) + profile_image = db.Column(db.String(256), nullable = False, default = '150x150.png') - def get_profile_image(self, user_id): - sql = 'SELECT profile_image FROM user_infos WHERE user_id = %s' % user_id - self.cursor.execute(sql) - profile_image = self.cursor.fetchone() - return profile_image + user = db.relationship('Users', uselist = False, foreign_keys = 'UserInfos.user_id') - def insert_user_info(self, data): - user_id, display_name, user_name, age, interests = itemgetter('user_id', 'display_name', 'user_name', 'age', 'interests')(data) - sql = 'INSERT INTO user_infos (user_id, display_name, user_name, interests, age) VALUES (%s, "%s", "%s", "%s", %s);' \ - % (user_id, self.__xstr(display_name), self.__xstr(user_name), self.__xstr(interests), age) - self.cursor.execute(sql) - self.db.commit() - return True + def __repr__(self): + return '' \ + % (self.id, self.user_id, self.display_name, self.user_name, self.age, self.interests, self.profile_image) - def update_profile_image(self, user_id, filename): - sql = 'UPDATE user_infos SET profile_image = "%s" WHERE user_id = %s' % (filename, user_id) - self.cursor.execute(sql) - self.db.commit() - return True + def __init__(self, data): + self.user_id = data['user_id'] + if 'display_name' in data: + self.display_name = data['display_name'] + if 'user_name' in data: + self.user_name = data['user_name'] + if 'age' in data: + self.age = data['age'] + if 'interests' in data: + self.interests = data['interests'] + if 'profile_image' in data: + self.profile_image = data['profile_image'] - def update_user_info(self, data): - sqls = ['UPDATE user_infos SET '] - user_id, display_name, user_name, age, email, interests = itemgetter('id', 'display_name', 'user_name', 'age', 'email', 'interests')(data) - display_name = display_name or '' - user_name = user_name or '' - interests = interests or '' - # if display_name is not None: - sqls.append('display_name = "%s"' % self.__xstr(display_name)) - # if user_name is not None: - sqls.append('user_name = "%s"' % self.__xstr(user_name)) - sqls.append('age = %s' % age) - # if interests is not None: - sqls.append('interests = "%s"' % self.__xstr(interests)) + @classmethod + def initial_to_create(self, user_id): + u = UserInfos({ 'user_id': user_id }) + return u.create() - sql = '' - for x in range(len(sqls)): - if x == 0 or x == (len(sqls) - 1): - sql += sqls[x] + ' ' - else: - sql += sqls[x] + ', ' + @classmethod + def search(self, data): + return UserInfos.query.filter_by(user_id = data['user_id']).limit(1).first() - sql += 'WHERE user_id = %s' % user_id - self.cursor.execute(sql) - self.db.commit() - if email is not None and email != '': - u = User() - u.update_user(user_id, email) - - return True + def create(self): + db.session.add(self) + db.session.commit() + return self + + def update(self, data): + if 'display_name' in data: + self.display_name = data['display_name'] + if 'user_name' in data: + self.user_name = data['user_name'] + if 'age' in data: + self.age = data['age'] + if 'interests' in data: + self.interests = data['interests'] + + if 'email' in data: + self.user.update({ 'email': data['email'] }) + + db.session.commit() + return self - def __xstr(self, s): - return '' if s is None else str(s) \ No newline at end of file + def upload_image(self, filename): + self.profile_image = filename + + db.session.commit() + return self + + def get_image(self): + return self.profile_image \ No newline at end of file From 28c8c694ca4fa6ae642e61ee00dbbde397870a5d Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Thu, 30 Sep 2021 13:36:32 +0900 Subject: [PATCH 02/18] Modified & Adding heroku config --- Dockerfile | 9 ++++++ follower.py | 20 ------------- heroku.yml | 6 ++++ main.py | 5 ++-- templates/sign_up.html | 2 +- tweet.py | 65 ------------------------------------------ 6 files changed, 19 insertions(+), 88 deletions(-) create mode 100644 Dockerfile create mode 100644 heroku.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..efb367e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.9.7-slim + +WORKDIR /python + +RUN pip install \ + flask \ + flask-sqlalchemy + +ADD ./ ./ diff --git a/follower.py b/follower.py index 66015e6..f0477dd 100644 --- a/follower.py +++ b/follower.py @@ -29,23 +29,3 @@ def delete(self): def to_json(self): return json.dumps(self, default = lambda o: '', sort_keys = True, indent = 4) - - # def follow(self, user_id, follow_id): - # sql = 'SELECT COUNT(id) FROM followers WHERE user_id = %s AND follow_id = %s' % (user_id, follow_id) - # self.cursor.execute(sql) - # total = dict(zip(self.cursor.column_names, self.cursor.fetchone())) - # if total['COUNT(id)'] > 0: - # return False - # else: - # next_sql = 'INSERT INTO followers (user_id, follow_id) VALUES (%s, %s)' % (user_id, follow_id) - - # self.cursor.execute(next_sql) - # self.db.commit() - # return True - - # def unfollow(self, user_id, follow_id): - # sql = 'DELETE FROM followers WHERE user_id = %s AND follow_id = %s' % (user_id, follow_id) - # self.cursor.execute(sql) - # self.db.commit() - - # return True \ No newline at end of file diff --git a/heroku.yml b/heroku.yml new file mode 100644 index 0000000..c1338bf --- /dev/null +++ b/heroku.yml @@ -0,0 +1,6 @@ +build: + docker: + web: Dockerfile + +run: + web: python3 main.py diff --git a/main.py b/main.py index b9a22ca..e7e1588 100644 --- a/main.py +++ b/main.py @@ -54,8 +54,9 @@ def login(): if request.method == 'POST': email = request.form['email'] password = request.form['password'] - if email == None: - return redirect(url_for('home')) + if email is None: + print(email) + return redirect(url_for('login')) l = Login() user = l.login(email, password) diff --git a/templates/sign_up.html b/templates/sign_up.html index 24b8dce..00aecda 100644 --- a/templates/sign_up.html +++ b/templates/sign_up.html @@ -18,7 +18,7 @@

    {{ title }}

    - +
    diff --git a/tweet.py b/tweet.py index 926649d..602546d 100644 --- a/tweet.py +++ b/tweet.py @@ -81,68 +81,3 @@ def invisible(self): def to_json(self): return json.dumps(self, default = lambda o: '', sort_keys = True, indent = 4) - # def get_tweets(self, data): - # current_user_id = itemgetter('user_id')(data) - # sql = 'SELECT * FROM tweets LEFT JOIN user_infos ON tweets.user_id = user_infos.user_id \ - # LEFT JOIN tweet_likes ON tweets.id = tweet_likes.tweet_id ORDER BY tweets.id DESC' - # self.cursor.execute(sql) - # tweets = self.cursor.fetchall() - - # second_sql = 'SELECT * FROM followers WHERE user_id = %s' % current_user_id - # self.cursor.execute(second_sql) - # followers = self.cursor.fetchall() - - # result = [] - # for t in tweets: - # dic = {} - # id, user_id, message, is_visible, user_info_id, _, display_name, user_name, age, interests, profile_image, _, pushed_user_id, _ = t - # for f in followers: - # _, user_id_on_followers, follow_id = f - # if current_user_id == user_id_on_followers and follow_id == user_id: - # dic['following'] = True - # else: - # dic['following'] = False - - # dic['id'] = id - # dic['user_id'] = user_id - # dic['message'] = message - # dic['is_visible'] = is_visible - # dic['user_info_id'] = user_info_id - # dic['display_name'] = display_name - # dic['user_name'] = user_name - # dic['age'] = age - # dic['interests'] = interests - # dic['profile_image'] = profile_image - # dic['pushed_user_id'] = pushed_user_id - # result.append(dic) - - # return result - - # def add_tweet(self, data): - # user_id, message = itemgetter('user_id', 'message')(data) - # sql = 'INSERT INTO tweets (user_id, message) VALUES (%s, "%s")' % (user_id, message) - # self.cursor.execute(sql) - # self.db.commit() - - # return True - - # def invisible_tweet(self, id): - # sql = 'SELECT COUNT(id) FROM tweets WHERE is_visible = 1 AND id = %s' % id - # self.cursor.execute(sql) - # total = self.cursor.fetchone() - # if total[0] > 0: - # next_sql = 'UPDATE tweets SET is_visible = 0 WHERE id = %s' % id - # else: - # next_sql = 'UPDATE tweets SET is_visible = 1 WHERE id = %s' % id - - # self.cursor.execute(next_sql) - # self.db.commit() - - # return True - - # def delete_tweet(self, id): - # sql = 'DELETE FROM tweets WHERE id = %s' % id - # self.cursor.execute(sql) - # self.db.commit() - - # return True \ No newline at end of file From 001412f775cc6aa105b9566d9f82a3e472eac834 Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Thu, 30 Sep 2021 13:50:17 +0900 Subject: [PATCH 03/18] Modified running port --- main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index e7e1588..b7867b1 100644 --- a/main.py +++ b/main.py @@ -228,4 +228,5 @@ def __profile_image(file): if __name__ == '__main__': app.secret_key = 'test' app.config['SESSION_TYPE'] = 'filesystem' - app.run(debug=True) \ No newline at end of file + port = int(os.environ.get('PORT', 5000)) + app.run(debug = True, host='0.0.0.0', port = port) \ No newline at end of file From 398a684248d714f8dd2dd68fe7c0418acc1d1f80 Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Fri, 1 Oct 2021 09:52:45 +0900 Subject: [PATCH 04/18] Updated Dockerfile --- Dockerfile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Dockerfile b/Dockerfile index efb367e..35668a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,18 @@ +#FROM mysql/mysql-server:latest FROM python:3.9.7-slim WORKDIR /python + +RUN apt-get update -y && \ + apt-get install -y \ + python \ + default-mysql-client RUN pip install \ flask \ flask-sqlalchemy +#RUN apt update && \ +# apt install mysql-server -y + ADD ./ ./ From db9bb29cf5fe1b0c92e0f374b36d0193c96bb86f Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Fri, 1 Oct 2021 10:10:34 +0900 Subject: [PATCH 05/18] Using os.environ --- .gitignore | 2 +- Dockerfile | 3 --- db.py | 3 ++- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index d360a47..97c3cf2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ __pycache__ volumes/ .secrets/ -.static/* +static/ !.static/150x150.png diff --git a/Dockerfile b/Dockerfile index 35668a3..343ed3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,4 @@ RUN pip install \ flask \ flask-sqlalchemy -#RUN apt update && \ -# apt install mysql-server -y - ADD ./ ./ diff --git a/db.py b/db.py index 88f0f57..d92a5c0 100644 --- a/db.py +++ b/db.py @@ -1,3 +1,4 @@ +import os from flask_sqlalchemy import SQLAlchemy class Database: @@ -13,6 +14,6 @@ def __new__(klass, flask_app): return klass._instance def __initialize(klass): - klass.app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+mysqlconnector://root@localhost:3306/twitter_development' + klass.app.config['SQLALCHEMY_DATABASE_URI'] = os.environ['SQLALCHEMY_DATABASE_URI'] klass.app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False klass.database = SQLAlchemy(klass.app) From c942f48977967f6ea8992d8f662580b3f397f8ed Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Fri, 1 Oct 2021 10:23:54 +0900 Subject: [PATCH 06/18] Fixed mysql environment --- Dockerfile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 343ed3d..a3d57d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,9 +7,13 @@ WORKDIR /python RUN apt-get update -y && \ apt-get install -y \ python \ - default-mysql-client + default-mysql-client \ + python3-dev \ + default-libmysqlclient-dev \ + build-essential RUN pip install \ flask \ - flask-sqlalchemy + flask-sqlalchemy \ + mysqlclient ADD ./ ./ From 91b7798be415997901fde67d3f883d359dc650d2 Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Fri, 1 Oct 2021 10:56:29 +0900 Subject: [PATCH 07/18] Add heroku simple-file-upload --- templates/profile.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/profile.html b/templates/profile.html index 832f360..4bc2ed9 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -23,7 +23,7 @@
@@ -86,4 +86,5 @@ document.querySelector('input[type="file"]').addEventListener('change', uploadImage) })() + {% endblock %} \ No newline at end of file From 4e1c51d6b6535e461a1395240167419f71f517b4 Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Fri, 1 Oct 2021 11:33:01 +0900 Subject: [PATCH 08/18] Using s3 --- .gitignore | 1 + main.py | 2 +- templates/profile.html | 2 +- templates/tweet.html | 2 +- tweet.py | 3 ++- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 97c3cf2..85e884c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ volumes/ .secrets/ static/ !.static/150x150.png +.DS_Store diff --git a/main.py b/main.py index b7867b1..c67e0f3 100644 --- a/main.py +++ b/main.py @@ -208,7 +208,7 @@ def __add_session(data): if 'interests' in data: session['interests'] = data['interests'] if 'profile_image' in data: - session['profile_image'] = data['profile_image'] + session['profile_image'] = os.environ['S3_BUCKET'] + data['profile_image'] if 'age' in data: session['age'] = data['age'] diff --git a/templates/profile.html b/templates/profile.html index 4bc2ed9..79fc9ae 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -9,7 +9,7 @@ {% set email = session['email'] %} {% set user_info_id = session['user_info_id'] %} {% set interests = session['interests'] or '' %} -{% set profile_image = url_for('static', filename = session['profile_image'] or '150x150.png') %} +{% set profile_image = session['profile_image'] %} {% if user_info_id is none: %} {% set action = '/profile/new' %} {% else %} diff --git a/templates/tweet.html b/templates/tweet.html index 5aeded9..e15bb83 100644 --- a/templates/tweet.html +++ b/templates/tweet.html @@ -25,7 +25,7 @@ {% set user_name = t['user_name'] %} {% set age = t['age'] %} {% set interests = t['interests'] %} - {% set image = url_for('static', filename = t['profile_image']) %} + {% set image = t['profile_image'] %} {% set pushed_user_id = t['pushed_user_id'] %} {% set following = t['following'] %} {% set is_current_user = login_user_id == tweet_user_id %} diff --git a/tweet.py b/tweet.py index 602546d..6c9130d 100644 --- a/tweet.py +++ b/tweet.py @@ -1,3 +1,4 @@ +import os from flask import json from follower import Followers from tweet_like import TweetLikes @@ -54,7 +55,7 @@ def default_tweets(self, user_id): dic['user_name'] = user_info.user_name dic['age'] = user_info.age dic['interests'] = user_info.interests - dic['profile_image'] = user_info.profile_image + dic['profile_image'] = os.environ['S3_BUCKET'] + user_info.profile_image dic['pushed_user_id'] = t.tweet_likes[0].user_id if t.tweet_likes else 0 result.append(dic) From 7dd74215a55b4ee6933217a5d0df1b3bf52c875c Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Fri, 1 Oct 2021 11:38:28 +0900 Subject: [PATCH 09/18] Fixed broken filepath --- main.py | 2 +- templates/profile.html | 3 +-- tweet.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index c67e0f3..c225040 100644 --- a/main.py +++ b/main.py @@ -208,7 +208,7 @@ def __add_session(data): if 'interests' in data: session['interests'] = data['interests'] if 'profile_image' in data: - session['profile_image'] = os.environ['S3_BUCKET'] + data['profile_image'] + session['profile_image'] = os.environ['S3_BUCKET'] + '/' + data['profile_image'] if 'age' in data: session['age'] = data['age'] diff --git a/templates/profile.html b/templates/profile.html index 79fc9ae..13bab78 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -23,7 +23,7 @@ @@ -86,5 +86,4 @@ document.querySelector('input[type="file"]').addEventListener('change', uploadImage) })() - {% endblock %} \ No newline at end of file diff --git a/tweet.py b/tweet.py index 6c9130d..c54a803 100644 --- a/tweet.py +++ b/tweet.py @@ -55,7 +55,7 @@ def default_tweets(self, user_id): dic['user_name'] = user_info.user_name dic['age'] = user_info.age dic['interests'] = user_info.interests - dic['profile_image'] = os.environ['S3_BUCKET'] + user_info.profile_image + dic['profile_image'] = os.environ['S3_BUCKET'] + '/' + user_info.profile_image dic['pushed_user_id'] = t.tweet_likes[0].user_id if t.tweet_likes else 0 result.append(dic) From 60d2b10f101b74579593932ddae1490051b36107 Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Fri, 1 Oct 2021 12:21:20 +0900 Subject: [PATCH 10/18] Using signed url --- templates/profile.html | 45 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/templates/profile.html b/templates/profile.html index 13bab78..4311618 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -66,6 +66,41 @@ $profileImage.src = src; }) } + const getSignedReqeust = (file) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', '/sign_s3?file_name=' + file.name + '&file_type=' + file.type) + xhr.onreadystatechange = function() { + if (xhr.status === 4) { + if (xhr.status === 200) { + const res = JSON.parse(xhr.responseText); + uploadImage(file, res.data, res.url) + } else { + alert('Could not get signed URL') + } + } + } + xhr.send() + } + const uploadFile = (file, s3Data, url) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', s3Data.url) + + const formData = new FormData(); + for(key in s3Data.fields) { + formData.append(key, s3Data.fields[key]) + } + formData.append('file', file) + + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + if (xhr.status === 200 || xhr.status === 204) { + $profileImage.src = url + } + } else { + alert('Could not upload file') + } + } + } const uploadImage = (e) => { const file = e.target.files[0] const userId = e.target.dataset.userId @@ -81,9 +116,11 @@ }); } $profileImage.addEventListener('click', () => { - document.querySelector('input[type="file"]').click() - }) - document.querySelector('input[type="file"]').addEventListener('change', uploadImage) - })() + document.querySelector('input[type="file"]').click(); + }); + document.querySelector('input[type="file"]').addEventListener('change', (e) => { + getSignedReqeust(e.target.files[0]); + }); + })(); {% endblock %} \ No newline at end of file From 9fe074d0ed4970b8fa7ffd1c5e1b5142c107e4b5 Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Fri, 1 Oct 2021 12:34:39 +0900 Subject: [PATCH 11/18] Update sign_s3 --- Dockerfile | 3 ++- main.py | 28 +++++++++++++++++++++++++--- tweet.py | 2 +- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index a3d57d9..144cc76 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ RUN apt-get update -y && \ RUN pip install \ flask \ flask-sqlalchemy \ - mysqlclient + mysqlclient \ + boto3 ADD ./ ./ diff --git a/main.py b/main.py index c225040..36f9f56 100644 --- a/main.py +++ b/main.py @@ -5,8 +5,7 @@ database = Database(app) import sqlalchemy -import os -import json +import os, json, boto3 from flask.globals import session from flask.helpers import flash from werkzeug.utils import secure_filename @@ -131,6 +130,29 @@ def profile_edit(user_id): }) return redirect(url_for('profile')) +@app.route('sign_s3/') +def sign3(): + BUCKET = os.environ.get('S3_BUCKET') + filename = request.args.get('file_name') + filetype = request.args.get('file_type') + + s3 = boto3.client('s3') + presigned_post = s3.generate_presigned_post( + Bucket = BUCKET, + Key = filename, + Fields = { 'acl': 'public-read', 'Content-Type': filetype }, + Conditions = [ + { 'acl': 'pubilc-read' }, + { 'Content-Type': filetype } + ], + ExpiresIn = 3600 + ) + + return json.dumps({ + 'data': presigned_post, + 'url': 'https://%s.s3.amazonaws.com/%s' % (BUCKET, filename) + }) + @app.route('/profile//upload_image', methods = ['POST']) def upload_image(user_id): filename = __profile_image(request.files['profile_image']) @@ -208,7 +230,7 @@ def __add_session(data): if 'interests' in data: session['interests'] = data['interests'] if 'profile_image' in data: - session['profile_image'] = os.environ['S3_BUCKET'] + '/' + data['profile_image'] + session['profile_image'] = 'https://%s.s3.amazonaws.com/%s' % (os.environ.get('S3_BUCKET'), data['profile_image']) if 'age' in data: session['age'] = data['age'] diff --git a/tweet.py b/tweet.py index c54a803..5e5ab4f 100644 --- a/tweet.py +++ b/tweet.py @@ -55,7 +55,7 @@ def default_tweets(self, user_id): dic['user_name'] = user_info.user_name dic['age'] = user_info.age dic['interests'] = user_info.interests - dic['profile_image'] = os.environ['S3_BUCKET'] + '/' + user_info.profile_image + dic['profile_image'] = 'https://%s.s3.amazonaws.com/%s' % (os.environ.get('S3_BUCKET'), user_info.profile_image) dic['pushed_user_id'] = t.tweet_likes[0].user_id if t.tweet_likes else 0 result.append(dic) From 551498408dea15fa979a6848c3bf21b515d68779 Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Fri, 1 Oct 2021 12:38:03 +0900 Subject: [PATCH 12/18] Typo slash --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 36f9f56..194b6ff 100644 --- a/main.py +++ b/main.py @@ -130,7 +130,7 @@ def profile_edit(user_id): }) return redirect(url_for('profile')) -@app.route('sign_s3/') +@app.route('/sign_s3/') def sign3(): BUCKET = os.environ.get('S3_BUCKET') filename = request.args.get('file_name') From 67184cfb21c72d8706d98b88a7db8e80857a65ff Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Fri, 1 Oct 2021 12:50:24 +0900 Subject: [PATCH 13/18] Debug --- main.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 194b6ff..a25cbb6 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ +from werkzeug.wrappers import response from db import Database from flask import Flask, render_template, redirect, url_for, request, jsonify @@ -135,7 +136,8 @@ def sign3(): BUCKET = os.environ.get('S3_BUCKET') filename = request.args.get('file_name') filetype = request.args.get('file_type') - + + print('respooooooooooooooooooooonse', response) s3 = boto3.client('s3') presigned_post = s3.generate_presigned_post( Bucket = BUCKET, @@ -147,7 +149,9 @@ def sign3(): ], ExpiresIn = 3600 ) - + print('s33333333333333333333333333333333', s3) + print('requeeeeeeeeeeeeeeeeeeeeeeeeest', request) + return json.dumps({ 'data': presigned_post, 'url': 'https://%s.s3.amazonaws.com/%s' % (BUCKET, filename) From b78335053b21a3cac907ebc7676c79cc06f6ca9d Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Sat, 2 Oct 2021 10:16:33 +0900 Subject: [PATCH 14/18] Using ProxyFit --- main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index a25cbb6..f98ff96 100644 --- a/main.py +++ b/main.py @@ -5,8 +5,8 @@ app = Flask(__name__) database = Database(app) -import sqlalchemy -import os, json, boto3 +import os, json, boto3, sqlalchemy +from werkzeug.middleware.proxy_fix import ProxyFix from flask.globals import session from flask.helpers import flash from werkzeug.utils import secure_filename @@ -254,5 +254,6 @@ def __profile_image(file): if __name__ == '__main__': app.secret_key = 'test' app.config['SESSION_TYPE'] = 'filesystem' + app.wsgi_app = ProxyFix(app.wsgi_app, x_proto = 1) port = int(os.environ.get('PORT', 5000)) app.run(debug = True, host='0.0.0.0', port = port) \ No newline at end of file From 47b1d0c328479f965ea524015ec7f93fb88011ae Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Sat, 2 Oct 2021 11:47:14 +0900 Subject: [PATCH 15/18] Add well-known url --- main.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index f98ff96..fba423a 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ +from flask.wrappers import Response from werkzeug.wrappers import response from db import Database from flask import Flask, render_template, redirect, url_for, request, jsonify @@ -220,6 +221,13 @@ def follow(follow_id): v = new.create() return v.to_json() +@app.route('/.well-known/acme-challenge/') +def achme(achme_id): + challenge = { + '': '', + } + return Response(challenge[achme_id], mimetype = 'text/plain') + def __add_session(data): if 'user_id' in data: session['user_id'] = data['user_id'] @@ -254,6 +262,6 @@ def __profile_image(file): if __name__ == '__main__': app.secret_key = 'test' app.config['SESSION_TYPE'] = 'filesystem' - app.wsgi_app = ProxyFix(app.wsgi_app, x_proto = 1) + app.wsgi_app = ProxyFix(app.wsgi_app, x_proto = 'https') port = int(os.environ.get('PORT', 5000)) app.run(debug = True, host='0.0.0.0', port = port) \ No newline at end of file From 6f3d533e53c7219e29f0ef30580c6fbb2da2e0aa Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Sat, 2 Oct 2021 11:51:01 +0900 Subject: [PATCH 16/18] Fixed x_proto problem --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index fba423a..9585eb1 100644 --- a/main.py +++ b/main.py @@ -262,6 +262,6 @@ def __profile_image(file): if __name__ == '__main__': app.secret_key = 'test' app.config['SESSION_TYPE'] = 'filesystem' - app.wsgi_app = ProxyFix(app.wsgi_app, x_proto = 'https') + app.wsgi_app = ProxyFix(app.wsgi_app, x_proto = 1) port = int(os.environ.get('PORT', 5000)) app.run(debug = True, host='0.0.0.0', port = port) \ No newline at end of file From 9ef1d3d0d285bc4176d775350a27e0bee5959a83 Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Sat, 2 Oct 2021 12:37:46 +0900 Subject: [PATCH 17/18] Create the acme_id --- main.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 9585eb1..9fac19e 100644 --- a/main.py +++ b/main.py @@ -222,11 +222,13 @@ def follow(follow_id): return v.to_json() @app.route('/.well-known/acme-challenge/') -def achme(achme_id): - challenge = { - '': '', +def acme(acme_id): + # dirname = '.well-known/acme-challenge' + r = { + '5AhuECaKSeiCsAfKIyThyOEzDENQ5uz5wDIfqOm2i14': 'k0g5SPH2IEAHNDbJw-rS0SbBUv09vYEAUec_pyVSgV8' } - return Response(challenge[achme_id], mimetype = 'text/plain') + return Response(r[acme_id], mimetype='text/plain') + def __add_session(data): if 'user_id' in data: From ba81194efb54fcb5cf0072dcb56e399ecbf58090 Mon Sep 17 00:00:00 2001 From: lion-man44 Date: Sat, 2 Oct 2021 12:41:41 +0900 Subject: [PATCH 18/18] Modified --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 9fac19e..4a94a81 100644 --- a/main.py +++ b/main.py @@ -225,7 +225,7 @@ def follow(follow_id): def acme(acme_id): # dirname = '.well-known/acme-challenge' r = { - '5AhuECaKSeiCsAfKIyThyOEzDENQ5uz5wDIfqOm2i14': 'k0g5SPH2IEAHNDbJw-rS0SbBUv09vYEAUec_pyVSgV8' + 'a6cWkYIVEtIhQjSajd69Yr89EQ0CmRJess0vEhvQzUg': 'a6cWkYIVEtIhQjSajd69Yr89EQ0CmRJess0vEhvQzUg.k0g5SPH2IEAHNDbJw-rS0SbBUv09vYEAUec_pyVSgV8' } return Response(r[acme_id], mimetype='text/plain')