diff --git a/.gitignore b/.gitignore index dbed2ff1f..9bd77adbd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ __pycache__/ *.py[cod] *$py.class +# VS Code +.vscode + # C extensions *.so diff --git a/connection.py b/connection.py new file mode 100644 index 000000000..c9bda939a --- /dev/null +++ b/connection.py @@ -0,0 +1,51 @@ +# Creates a decorator to handle the database connection/cursor opening/closing. +# Creates the cursor with RealDictCursor, thus it returns real dictionaries, where the column names are the keys. +import os +import psycopg2 +import psycopg2.extras + + +def get_connection_string(): + # setup connection string + # to do this, please define these environment variables first + user_name = os.environ.get('PSQL_USER_NAME') + password = os.environ.get('PSQL_PASSWORD') + host = os.environ.get('PSQL_HOST') + database_name = os.environ.get('PSQL_DB_NAME') + + env_variables_defined = user_name and password and host and database_name + + if env_variables_defined: + # this string describes all info for psycopg2 to connect to the database + return 'postgresql://{user_name}:{password}@{host}/{database_name}'.format( + user_name=user_name, + password=password, + host=host, + database_name=database_name + ) + else: + raise KeyError('Some necessary environment variable(s) are not defined') + + +def open_database(): + try: + connection_string = get_connection_string() + connection = psycopg2.connect(connection_string) + connection.autocommit = True + except psycopg2.DatabaseError as exception: + print('Database connection problem') + raise exception + return connection + + +def connection_handler(function): + def wrapper(*args, **kwargs): + connection = open_database() + # we set the cursor_factory parameter to return with a RealDictCursor cursor (cursor which provide dictionaries) + dict_cur = connection.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + ret_value = function(dict_cur, *args, **kwargs) + dict_cur.close() + connection.close() + return ret_value + + return wrapper diff --git a/data_manager.py b/data_manager.py new file mode 100644 index 000000000..718bbed10 --- /dev/null +++ b/data_manager.py @@ -0,0 +1,329 @@ +import connection +from psycopg2.extensions import AsIs +from datetime import datetime + + +@connection.connection_handler +def get_questions_fix(cursor): + cursor.execute("""SELECT * FROM question ORDER BY submission_time DESC LIMIT 5;""") + questions = cursor.fetchall() + return questions + + +@connection.connection_handler +def get_questions(cursor): + cursor.execute("""SELECT * FROM question ORDER BY submission_time DESC;""") + questions = cursor.fetchall() + return questions + + +@connection.connection_handler +def delete_question(cursor, question_id): + cursor.execute("""DELETE FROM question WHERE id=%(id)s;""", {'id': question_id}) + + +@connection.connection_handler +def get_latest5_questions(cursor, order, direction): + + cursor.execute("""SELECT * FROM question ORDER BY %(order)s %(direction)s;""", {"order": AsIs(order), "direction":AsIs(direction.upper())}) + + questions = cursor.fetchall() + return questions + + +@connection.connection_handler +def get_answers(cursor): + cursor.execute("""SELECT * FROM answer ORDER BY submission_time DESC;""") + answers = cursor.fetchall() + return answers + + +@connection.connection_handler +def delete_answer(cursor, answer_id): + cursor.execute("""DELETE FROM comment WHERE answer_id=%(answer_id)s;""", {'answer_id': answer_id}) + cursor.execute("""DELETE FROM answer WHERE id=%(answer_id)s;""", {'answer_id': answer_id}) + + +@connection.connection_handler +def get_comments(cursor): + cursor.execute("""SELECT * FROM comment ORDER BY submission_time DESC;""") + comments = cursor.fetchall() + return comments + + +@connection.connection_handler +def add_question(cursor, title, message,user_id): + user_story = { + 'submission_time': datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + 'view_number': 0, + 'vote_number': 0, + 'title': title, + 'message': message, + 'user_id':user_id, + 'image': "" + } + + cursor.execute("""INSERT INTO question(submission_time, view_number, vote_number, title, message, image,user_id) + VALUES(%(submission_time)s, %(view_number)s, %(vote_number)s, + %(title)s, %(message)s, %(image)s,%(user_id)s);""", + user_story) + + cursor.execute("""SELECT id FROM question + ORDER BY id DESC + LIMIT 1;""") + return cursor.fetchone()['id'] + + +@connection.connection_handler +def add_answer(cursor, question_id, message,user_id): + + user_story = { + 'submission_time': datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + 'vote_number': 0, + 'question_id': question_id, + 'message': message, + 'user_id': user_id, + 'image': "" + } + + cursor.execute("""INSERT INTO answer(submission_time, vote_number, question_id, message, image,user_id) + VALUES(%(submission_time)s,%(vote_number)s,%(question_id)s, %(message)s,%(image)s,%(user_id)s);""", + user_story) + + +@connection.connection_handler +def add_comment(cursor, question_id, answer_id, message,user_id): + user_story = { + 'submission_time': datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + 'message': message, + 'answer_id': answer_id, + 'question_id': question_id, + 'user_id': user_id + } + + cursor.execute("""INSERT INTO comment(submission_time, question_id, answer_id, message,user_id) + VALUES(%(submission_time)s,%(question_id)s, %(answer_id)s, %(message)s,%(user_id)s);""", user_story) + + + +@connection.connection_handler +def delete_comments(cursor, comment_id): + cursor.execute("""DELETE FROM comment WHERE id=%(comment_id)s;""", {'comment_id': comment_id}) + + +@connection.connection_handler +def get_update(cursor, answer_id, message): + time = datetime.now() + cursor.execute("""UPDATE answer SET message = %(message)s,submission_time = %(time)s WHERE id=%(answer_id)s;""", + {"message": message, 'answer_id': answer_id, 'time': time}) + + +@connection.connection_handler +def get_update_question(cursor, question_id, message, title): + time = datetime.now() + cursor.execute("""UPDATE question SET message = %(message)s,submission_time = %(time)s,title = %(title)s + WHERE id=%(question_id)s;""", + {"message": message, 'question_id': question_id, 'time': time, 'title': title}) + + +@connection.connection_handler +def get_update_for_comment(cursor, comment_id, message): + time = datetime.now() + cursor.execute("""UPDATE comment SET + message = %(message)s, + submission_time = %(time)s, + edited_count = %(count)s + WHERE id=%(comment_id)s;""", + {"message": message, 'comment_id': comment_id, 'time': time, 'count': 1}) + + +@connection.connection_handler +def get_new_update_for_comment(cursor, comment_id, message): + time = datetime.now() + cursor.execute("""UPDATE comment SET + message = %(message)s, + submission_time = %(time)s, + edited_count = edited_count + 1 + WHERE id=%(comment_id)s;""", {"message": message, 'comment_id': comment_id, 'time': time}) + + +@connection.connection_handler +def get_question_id(cursor, answer_id): + cursor.execute(""" + SELECT * FROM answer + WHERE id=%(id)s LIMIT 1 + """, + {'id': answer_id}) + return cursor.fetchone()['question_id'] + + +@connection.connection_handler +def get_question_id_for_comment(cursor, comment_id): + cursor.execute(""" + SELECT * FROM comment + WHERE id=%(id)s LIMIT 1 + """, + {'id': comment_id}) + return cursor.fetchone()['question_id'] + + +@connection.connection_handler +def search_in(cursor, searched_word): + cursor.execute("""SELECT question.* FROM question LEFT JOIN answer ON question.id = answer.question_id + WHERE (LOWER(title) LIKE %(searched_word)s OR LOWER(answer.message) LIKE %(searched_word)s + OR LOWER(question.message) LIKE %(searched_word)s);""", + {'searched_word': '%' + searched_word + '%'}) + searched_data = cursor.fetchall() + return searched_data + + +@connection.connection_handler +def vote_up_question(cursor, question_id): + + variables = { + 'question_id': question_id + } + + cursor.execute("""UPDATE question + SET vote_number = vote_number+1 + WHERE id = %(question_id)s;""", variables) + + +@connection.connection_handler +def vote_down_question(cursor, question_id): + + variables = { + 'question_id': question_id + } + + cursor.execute("""UPDATE question + SET vote_number = vote_number-1 + WHERE id = %(question_id)s;""", variables) + + +@connection.connection_handler +def vote_up_answer(cursor, question_id, answer_id): + + variables = { + 'question_id': question_id, + 'answer_id': answer_id + } + + cursor.execute("""UPDATE answer + SET vote_number = vote_number+1 + WHERE question_id = %(question_id)s AND id = %(answer_id)s;""", variables) + + +@connection.connection_handler +def vote_down_answer(cursor, question_id, answer_id): + + variables = { + 'question_id': question_id, + 'answer_id': answer_id + } + + cursor.execute("""UPDATE answer + SET vote_number = vote_number-1 + WHERE question_id = %(question_id)s AND id = %(answer_id)s;""", variables) + + +@connection.connection_handler +def registration(cursor, username, hashed_password): + user_details = { + 'username': username, + 'password': hashed_password + } + cursor.execute("""INSERT INTO users(username, password) + VALUES(%(username)s, %(password)s);""", user_details) + + +@connection.connection_handler +def accept_answer(cursor, question_id, answer_id): + + variables = { + 'question_id': question_id, + 'answer_id': answer_id + } + + cursor.execute("""UPDATE answer + SET acception = TRUE + WHERE question_id = %(question_id)s AND id = %(answer_id)s;""", variables) + +@connection.connection_handler +def get_user_name(cursor, user_id): + cursor.execute(""" + SELECT * FROM users + WHERE id=%(id)s LIMIT 1 + """, + {'id': user_id}) + return cursor.fetchone()['username'] + + +@connection.connection_handler +def get_q_and_a_by_user(cursor): + cursor.execute("""SELECT question.title,question.message AS question_message,answer.message AS answer_message, + question.user_id + FROM (question INNER JOIN answer ON question.id=answer.question_id); +""") + datas = cursor.fetchall() + return datas + +@connection.connection_handler +def get_answers_for_user(cursor): + cursor.execute("""SELECT submission_time,message AS title,user_id,question_id AS id FROM answer + ORDER BY submission_time DESC;""") + answers = cursor.fetchall() + return answers\ + +@connection.connection_handler +def get_comment_for_user(cursor): + cursor.execute("""SELECT submission_time,message AS title,user_id,question_id AS id FROM comment + ORDER BY submission_time DESC;""") + answers = cursor.fetchall() + return answers + + +@connection.connection_handler +def get_password_by_username(cursor, username): + + cursor.execute("""SELECT password FROM users + WHERE username = %(username)s;""", {'username': username}) + password = cursor.fetchone() + if password is not None: + return password['password'] + else: + return None + + +@connection.connection_handler +def check_username(cursor, username): + + cursor.execute("""SELECT username FROM users + WHERE username = %(username)s;""", {'username': username}) + user = cursor.fetchone() + if user is not None: + return user['username'] + else: + return None + + +@connection.connection_handler +def get_user_id_by_username(cursor, username): + cursor.execute("""SELECT id FROM users WHERE username = %(username)s;""", {'username': username}) + + user_id = cursor.fetchone() + return user_id['id'] + + +@connection.connection_handler +def get_users(cursor): + cursor.execute("""SELECT * FROM users ORDER BY registration_date;""") + users = cursor.fetchall() + return users + + +@connection.connection_handler +def get_user_id_by_question_id(cursor, question_id): + cursor.execute("""SELECT user_id FROM question WHERE id = %(question_id)s;""", {'question_id': question_id}) + user_id = cursor.fetchone() + return user_id['user_id'] diff --git a/requiremnts.txt b/requiremnts.txt new file mode 100644 index 000000000..d88bff86c --- /dev/null +++ b/requiremnts.txt @@ -0,0 +1,6 @@ +Click==7.0 +Flask==1.0.2 +itsdangerous==1.1.0 +Jinja2==2.10 +MarkupSafe==1.1.0 +Werkzeug==0.14.1 \ No newline at end of file diff --git a/sample_data/answer.csv b/sample_data/answer.csv index 3ddfa1ffb..4211f55d6 100644 --- a/sample_data/answer.csv +++ b/sample_data/answer.csv @@ -1,3 +1,11 @@ id,submission_time,vote_number,question_id,message,image 0,1493398154,4,0,"You need to use brackets: my_list = []", 1,1493088154,35,0,"Look it up in the Python docs", + + +2,1550496481,0,3,"sikerrrrrrrrrrrrr +", +4,1549610956,0,1,this is an other answer to Q 2, +5,1549613796,0,3,asdélkfjéldsakfj, +6,1549617230,0,0,this my answer, +7,1550496321,0,4,asdfadsfsa, diff --git a/sample_data/askmatepart2-sample-data.sql b/sample_data/askmatepart2-sample-data.sql new file mode 100644 index 000000000..2d29f1ce3 --- /dev/null +++ b/sample_data/askmatepart2-sample-data.sql @@ -0,0 +1,160 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 9.5.6 +-- Dumped by pg_dump version 9.5.6 + +ALTER TABLE IF EXISTS ONLY public.question DROP CONSTRAINT IF EXISTS pk_question_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.question DROP CONSTRAINT IF EXISTS fk_user_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.answer DROP CONSTRAINT IF EXISTS pk_answer_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.answer DROP CONSTRAINT IF EXISTS fk_question_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.answer DROP CONSTRAINT IF EXISTS fk_user_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.comment DROP CONSTRAINT IF EXISTS pk_comment_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.comment DROP CONSTRAINT IF EXISTS fk_question_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.comment DROP CONSTRAINT IF EXISTS fk_answer_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.comment DROP CONSTRAINT IF EXISTS fk_user_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.question_tag DROP CONSTRAINT IF EXISTS pk_question_tag_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.question_tag DROP CONSTRAINT IF EXISTS fk_question_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.tag DROP CONSTRAINT IF EXISTS pk_tag_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.question_tag DROP CONSTRAINT IF EXISTS fk_tag_id CASCADE; +ALTER TABLE IF EXISTS ONLY public.users DROP CONSTRAINT IF EXISTS pk_user_id CASCADE; + +DROP TABLE IF EXISTS public.question CASCADE; +DROP SEQUENCE IF EXISTS public.question_id_seq; +CREATE TABLE question ( + id serial NOT NULL, + submission_time timestamp without time zone, + view_number integer, + vote_number integer, + title text, + message text, + image text, + user_id integer, +PRIMARY KEY (id) +); + +DROP TABLE IF EXISTS public.answer; +DROP SEQUENCE IF EXISTS public.answer_id_seq; +CREATE TABLE answer ( + id serial NOT NULL, + submission_time timestamp without time zone, + vote_number integer, + acception BOOLEAN NOT NULL DEFAULT FALSE, + question_id integer, + message text, + image text, + user_id integer, +PRIMARY KEY (id), +FOREIGN KEY (question_id) REFERENCES question(id) ON DELETE CASCADE +); + +DROP TABLE IF EXISTS public.comment; +DROP SEQUENCE IF EXISTS public.comment_id_seq; +CREATE TABLE comment ( + id serial NOT NULL, + question_id integer, + answer_id integer, + message text, + submission_time timestamp without time zone, + edited_count integer, + user_id integer +); + + +DROP TABLE IF EXISTS public.question_tag; +CREATE TABLE question_tag ( + question_id integer NOT NULL, + tag_id integer NOT NULL +); + +DROP TABLE IF EXISTS public.tag; +DROP SEQUENCE IF EXISTS public.tag_id_seq; +CREATE TABLE tag ( + id serial NOT NULL, + name text +); + + +DROP TABLE IF EXISTS public.users; +DROP SEQUENCE IF EXISTS public.users_id_seq; +CREATE TABLE users ( + id serial NOT NULL, + username varchar(255) NOT NULL, + password varchar(255) NOT NULL, + registration_date date NOT NULL DEFAULT CURRENT_DATE +); + + +ALTER TABLE ONLY users + ADD CONSTRAINT pk_user_id PRIMARY KEY (id); + +--ALTER TABLE ONLY answer +-- ADD CONSTRAINT pk_answer_id PRIMARY KEY (id); + +ALTER TABLE ONLY comment + ADD CONSTRAINT pk_comment_id PRIMARY KEY (id); + +--ALTER TABLE ONLY question +-- ADD CONSTRAINT pk_question_id PRIMARY KEY (id); + +ALTER TABLE ONLY question_tag + ADD CONSTRAINT pk_question_tag_id PRIMARY KEY (question_id, tag_id); + +ALTER TABLE ONLY tag + ADD CONSTRAINT pk_tag_id PRIMARY KEY (id); + +ALTER TABLE ONLY comment + ADD CONSTRAINT fk_answer_id FOREIGN KEY (answer_id) REFERENCES answer(id) ON DELETE CASCADE; + +ALTER TABLE ONLY comment + ADD CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + +ALTER TABLE ONLY answer + ADD CONSTRAINT fk_question_id FOREIGN KEY (question_id) REFERENCES question(id) ON DELETE CASCADE; + +ALTER TABLE ONLY answer + ADD CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + +ALTER TABLE ONLY question + ADD CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + +ALTER TABLE ONLY question_tag + ADD CONSTRAINT fk_question_id FOREIGN KEY (question_id) REFERENCES question(id) ON DELETE CASCADE; + +ALTER TABLE ONLY comment + ADD CONSTRAINT fk_question_id FOREIGN KEY (question_id) REFERENCES question(id) ON DELETE CASCADE; + +ALTER TABLE ONLY question_tag + ADD CONSTRAINT fk_tag_id FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE; + +INSERT INTO question VALUES (1, '2017-04-28 08:29:00', 29, 7, 'How to make lists in Python?', 'I am totally new to this, any hints?', NULL); +INSERT INTO question VALUES (2, '2017-04-29 09:19:00', 15, 9, 'Wordpress loading multiple jQuery Versions', 'I developed a plugin that uses the jquery booklet plugin (http://builtbywill.com/booklet/#/) this plugin binds a function to $ so I cann call $(".myBook").booklet(); + +I could easy managing the loading order with wp_enqueue_script so first I load jquery then I load booklet so everything is fine. + +BUT in my theme i also using jquery via webpack so the loading order is now following: + +jquery +booklet +app.js (bundled file with webpack, including jquery)', 'images/image1.png'); +INSERT INTO question VALUES (3, '2017-05-01 10:41:00', 1364, 57, 'Drawing canvas with an image picked with Cordova Camera Plugin', 'I''m getting an image from device and drawing a canvas with filters using Pixi JS. It works all well using computer to get an image. But when I''m on IOS, it throws errors such as cross origin issue, or that I''m trying to use an unknown format. +', NULL); +SELECT pg_catalog.setval('question_id_seq', 3, true); + +INSERT INTO answer VALUES (1, '2017-04-28 16:49:00', 4, FALSE, 1,'You need to use brackets: my_list = []', NULL); +INSERT INTO answer VALUES (2, '2017-04-25 14:42:00', 35, FALSE, 1,'Look it up in the Python docs', 'images/image2.jpg'); +SELECT pg_catalog.setval('answer_id_seq', 2, true); + +INSERT INTO comment VALUES (1, 1, NULL, 'Please clarify the question as it is too vague!', '2017-05-01 05:49:00'); +INSERT INTO comment VALUES (2, NULL, 2, 'I think you could use my_list = list() as well.', '2017-05-02 16:55:00'); +SELECT pg_catalog.setval('comment_id_seq', 2, true); + +INSERT INTO tag VALUES (1, 'python'); +INSERT INTO tag VALUES (2, 'sql'); +INSERT INTO tag VALUES (3, 'css'); +SELECT pg_catalog.setval('tag_id_seq', 3, true); + +INSERT INTO question_tag VALUES (1, 2); +INSERT INTO question_tag VALUES (1, 3); +INSERT INTO question_tag VALUES (2, 3); diff --git a/server.py b/server.py new file mode 100644 index 000000000..e87c7d5dd --- /dev/null +++ b/server.py @@ -0,0 +1,361 @@ +from flask import Flask, render_template, redirect, url_for, request, session +import bcrypt +import data_manager +from datetime import timedelta + + +app = Flask(__name__) + + +app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' +app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(minutes=1) + + +@app.route('/') +def route_index(): + return render_template('index.html', title="Welcome!") + + +@app.route('/list') +def route_main(): + stored_questions = data_manager.get_questions_fix() + return render_template('list.html', questions=stored_questions, title="Welcome!") + + +@app.route('/list', methods=['GET']) +def route_list(): + stored_questions = data_manager.get_questions_fix() + if request.method == "GET": + order = request.args.get('order_by') + direction = request.args.get('direction') + if order is None: + order = 'submission_time' + direction = 'desc' + stored_questions = data_manager.get_latest5_questions(order, direction) + return render_template('list.html', questions=stored_questions, title="Welcome!") + + +@app.route('/question/') +def route_question_id(question_id): + stored_questions = data_manager.get_questions() + stored_answers = data_manager.get_answers() + stored_comments = data_manager.get_comments() + user_id = data_manager.get_user_id_by_question_id(question_id) + + return render_template('questiondetails.html', + questions=stored_questions, + answers=stored_answers, + id=question_id, + comments=stored_comments, + question_id=question_id, + user_id=user_id) + + +@app.route('/question//delete', methods=['GET', 'POST']) +def delete_question(question_id): + try: + get_user_id = data_manager.get_user_id_by_username(session['username']) + except KeyError: + return redirect(url_for('route_main')) + questions = data_manager.get_questions() + + for question in questions: + if int(question['id']) == int(question_id) and int(question['user_id']) == int(get_user_id): + if request.method == "POST": + data_manager.delete_question(question_id) + return redirect(url_for('route_list')) + + return redirect(url_for('route_main')) + + +@app.route('/question//new-answer', methods=['GET', 'POST']) +def route_new_answer(question_id): + if 'username' in session: + if request.method == "POST": + user_id = data_manager.get_user_id_by_username(session['username']) + data_manager.add_answer(question_id, request.form["answer"],user_id) + return redirect(url_for('route_question_id', question_id=question_id)) + + return render_template('answer.html', title="Add New Answer!", question_id=question_id) + else: + return redirect(url_for('login')) + + +@app.route('/question//edit', methods=['GET', 'POST']) +def edit_question(question_id): + try: + get_user_id = data_manager.get_user_id_by_username(session['username']) + except KeyError: + return redirect(url_for('route_main')) + questions = data_manager.get_questions() + + for question in questions: + if int(question['id']) == int(question_id) and int(question['user_id']) == int(get_user_id): + if request.method == "POST": + new_message = request.form['message'] + new_title = request.form['title'] + data_manager.get_update_question(question_id, new_message, new_title) + return redirect(f'/question/{question_id}') + + return render_template('edit_question.html', questions=questions, question_id=question_id) + + return redirect(url_for('route_main')) + + +@app.route('/answer//edit', methods=['GET', 'POST']) +def edit_answer(answer_id): + try: + get_user_id = data_manager.get_user_id_by_username(session['username']) + except KeyError: + return redirect(url_for('route_main')) + answers = data_manager.get_answers() + question_id = data_manager.get_question_id(answer_id) + + for answer in answers: + if int(answer['id']) == int(answer_id) and int(answer['user_id']) == int(get_user_id): + if request.method == "POST": + new_message = request.form['answer'] + data_manager.get_update(answer_id, new_message) + return redirect(f'/question/{question_id}') + + return render_template('edit_answer.html', answer_id=answer_id, answers=answers) + + return redirect(url_for('route_main')) + + +@app.route('/answer//delete', methods=['GET', 'POST']) +def delete_answer(answer_id): + try: + get_user_id = data_manager.get_user_id_by_username(session['username']) + except KeyError: + return redirect(url_for('route_main')) + answers = data_manager.get_answers() + + for answer in answers: + if int(answer['id']) == int(answer_id) and int(answer['user_id']) == int(get_user_id): + if request.method == "POST": + data_manager.delete_answer(answer_id) + return redirect(url_for('route_list')) + + return redirect(url_for('route_main')) + + +@app.route("/add-question", methods=["GET", "POST"]) +def add_question(): + if 'username' in session: + if request.method == "POST": + user_id=data_manager.get_user_id_by_username(session['username']) + user_story_id = data_manager.add_question(request.form["question-title"], request.form["new-question"],user_id) + return redirect(url_for('route_question_id', question_id=user_story_id)) + else: + return redirect(url_for('login')) + return render_template("newquestion.html") + + +@app.route('/question//new-comment', methods=['GET', 'POST']) +@app.route('/answer//new-comment', methods=['GET', 'POST']) +def route_new_comment(question_id='', answer_id=''): + if 'username' in session: + if request.method == "POST": + user_id = data_manager.get_user_id_by_username(session['username']) + if question_id == '': + data_manager.add_comment(str(data_manager.get_question_id(answer_id)), answer_id, request.form["comment"],user_id) + return redirect(url_for('route_list')) + elif answer_id == '': + data_manager.add_comment(question_id,None, request.form["comment"],user_id) + return redirect(url_for('route_list')) + + return render_template('newcomment.html', title="Add New Comment!", answer_id=answer_id, question_id=question_id) + else: + return redirect(url_for('route_main')) + + +@app.route('/comment//edit', methods=['GET', 'POST']) +def edit_comment(comment_id): + try: + get_user_id = data_manager.get_user_id_by_username(session['username']) + except KeyError: + return redirect(url_for('route_main')) + comments = data_manager.get_comments() + + for comment in comments: + if int(comment['id']) == int(comment_id) and int(comment['user_id']) == int(get_user_id): + if request.method == "POST": + edit_counter = '' + for comment in comments: + if comment['id'] == comment_id: + edit_counter = comment['edited_count'] + if edit_counter is None: + new_message = request.form['comment'] + data_manager.get_update_for_comment(comment_id, new_message) + return redirect('/') + elif edit_counter is not None: + new_message = request.form['comment'] + data_manager.get_new_update_for_comment(comment_id, new_message) + return redirect('/') + + return render_template('edit_comment.html', comment_id=comment_id, comments=comments) + + return redirect(url_for('route_main')) + + +@app.route('/comments//delete', methods=['GET', 'POST']) +def delete_comment(comment_id): + try: + get_user_id = data_manager.get_user_id_by_username(session['username']) + except KeyError: + return redirect(url_for('route_main')) + comments = data_manager.get_comments() + + for comment in comments: + if int(comment['id']) == int(comment_id) and int(comment['user_id']) == int(get_user_id): + if request.method == "POST": + data_manager.delete_comments(comment_id) + return redirect(url_for('route_list')) + + return redirect(url_for('route_main')) + + +@app.route("/search") +def search(): + searched_word = request.args.get('q').lower() + if searched_word is not None: + questions = data_manager.search_in(searched_word) + updated_questions = [] + for question in questions: + if question not in updated_questions: + updated_questions.append(question) + print(updated_questions) + + return render_template('search.html', searched_word=searched_word, questions=updated_questions) + return redirect(url_for('route_list')) + + +@app.route("/question//vote-up", methods=['GET', 'POST']) +def vote_up_question(question_id): + if request.method == 'POST': + data_manager.vote_up_question(question_id) + return redirect(url_for('route_question_id', question_id=question_id)) + + +@app.route("/question//vote-down", methods=['GET', 'POST']) +def vote_down_question(question_id): + if request.method == 'POST': + data_manager.vote_down_question(question_id) + return redirect(url_for('route_question_id', question_id=question_id)) + + +@app.route("/question///vote-up", methods=['GET', 'POST']) +def vote_up_answer(question_id, answer_id): + if request.method == 'POST': + data_manager.vote_up_answer(question_id, answer_id) + return redirect(url_for('route_question_id', question_id=question_id)) + + +@app.route("/question///vote-down", methods=['GET', 'POST']) +def vote_down_answer(question_id, answer_id): + if request.method == 'POST': + data_manager.vote_down_answer(question_id, answer_id) + return redirect(url_for('route_question_id', question_id=question_id)) + + +@app.route("/login", methods=['GET', 'POST']) +def login(): + if request.method == 'GET': + if session.get('username') is not None: + return redirect(url_for('loginerror')) + else: + return render_template('login.html', error=None) + elif request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + hashed_password = data_manager.get_password_by_username(username) + if hashed_password is not None: + hashed_password = hashed_password.encode('utf-8') + if bcrypt.checkpw(password.encode('utf-8'), hashed_password) is True: + session['username'] = username + session['user_id'] = int(data_manager.get_user_id_by_username(username)) + session.permanent = True + return redirect(url_for('route_main')) + else: + return render_template('login.html', error="not valid") + else: + return render_template('login.html', error="not valid") + + +@app.route("/registration", methods=['GET', 'POST']) +def register(): + if request.method == 'GET': + if session.get('username') is not None: + return redirect(url_for('loginerror')) + else: + return render_template('register.html', error=None) + elif request.method == 'POST': + username = request.form['username'] + if data_manager.check_username(username) == username: + return render_template('register.html', error="taken") + else: + password = request.form['password'] + hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) + hashed_password = hashed_password.decode('utf-8') + data_manager.registration(username, hashed_password) + return redirect(url_for('route_main')) + + +@app.route("/user//") +def profile(user_id): + stored_questions = data_manager.get_questions() + user_name = data_manager.get_user_name(user_id) + return render_template('user_profile.html', questions=stored_questions, user_id=user_id,user_name=user_name) + + +@app.route("/user//") +def profile_question(user_id,type): + try: + get_user_id = data_manager.get_user_id_by_username(session['username']) + except KeyError: + return redirect(url_for('route_main')) + + if get_user_id == user_id: + if type == "question": + some_data = data_manager.get_questions() + elif type == "answer": + some_data = data_manager.get_answers_for_user() + elif type == "comment": + some_data = data_manager.get_comment_for_user() + user_name = data_manager.get_user_name(user_id) + return render_template('user_profile.html', datas=some_data, user_id=user_id, user_name=user_name) + else: + return redirect(url_for('route_main')) + + +@app.route("/question///accept-answer", methods=['GET', 'POST']) +def accept_answer(question_id, answer_id): + if request.method == 'POST': + data_manager.accept_answer(question_id, answer_id) + return redirect(url_for('route_question_id', question_id=question_id)) + + +@app.route('/logout') +def logout(): + session.pop('username', None) + session.pop('user_id', None) + return redirect(url_for('route_main')) + + +@app.route('/users') +def route_users(): + stored_users = data_manager.get_users() + return render_template('users_list.html', users=stored_users, title="Welcome!") + + +@app.route('/loginerror') +def loginerror(): + return render_template('loginerror.html') + + +if __name__ == "__main__": + app.run( + debug=True, + port=5000 + ) diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 000000000..1c2b485b3 --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,86 @@ +/********** Register and login css **********/ + +.wrapper { + margin-top: 40px; +} + +.form-login { + max-width: 420px; + padding: 15px 35px 45px; + margin: 0 auto; + background-color: #fff; + border: 1px solid rgba(0,0,0,0.1); + border-radius: 5px; +} + +.form-login h2 { + font-family: 'Fredericka the Great', Tahoma, sans-serif; + padding: 10px 0; +} + +.form-button { + font-family: Shojumaru, Tahoma, sans-serif; +} + +.login-or { + position: relative; + color: #aaa; + margin-top: 20px; + margin-bottom: 20px; + padding-top: 10px; + padding-bottom: 10px; +} + +.span-or { + display: block; + position: absolute; + left: 50%; + top: -2px; + margin-left: -25px; + background-color: #fff; + width: 50px; + text-align: center; +} + +.hr-or { + height: 1px; + margin-top: 0px !important; + margin-bottom: 0px !important; +} + +/*********** Profile css **********/ + +.user_name { + font-family: 'Fredericka the Great', Tahoma, sans-serif; + margin-top: 8px; + margin-bottom: 40px; +} + +.profile-img img { + width: 80%; + height: 100%; + margin-top: 8px; + border-radius: 50%; +} + +.profile-head .nav-tabs { + margin-bottom:5%; +} + +.profile-head .nav-tabs .nav-link { + font-weight: 600; + border: none; +} + +.nav-item-profile { + font-family: 'Fredericka the Great', Tahoma, sans-serif; + font-size: 18px; +} + +.row-header { + margin-bottom: 28px; +} + +.row-questions { + margin-bottom: 16px; +} diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 000000000..8b3274bff --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,225 @@ +/******** Index page css ********/ + +html { + font-size: 62.5%; + width: 100%; + height: 100%; +} + +body { + margin: 0; + padding: 0; + height: 100%; + font-size: 1.4rem; +} + +h1 { + font-size: 2.4rem; +} + +.container__item { + margin: 0 auto 0; +} + +.landing-page-container { + width: 100%; + min-height: 100%; + height: 100vh; + background-image: url("/static/img/bg6.jpeg"); + background-position: bottom; + background-repeat: no-repeat; + background-size: cover; + overflow: hidden; + display: table; + font-family: "Montserrat", sans-serif; + color: #09383E; +} + +.content-wrapper { + max-width: 1200px; + width: 90%; + height: 100%; + margin: 0 auto; + position: relative; +} + +.header { + width: 100%; + height: 2rem; + padding: 3rem 0; + display: block; +} + +.ellipse-container { + width: 50rem; + height: 50rem; + border-radius: 50%; + margin: 0 auto; + position: relative; + top: 2rem; +} + +.ellipse-container .greeting { + font-family: Monoton, Tahoma, sans-serif; + color: #09383E; + position: relative; + top: 14rem; + left: -8rem; + right: 0; + margin: 0 auto; + letter-spacing: 4rem; + font-size: 8rem; + font-weight: 400; + opacity: .5; +} + +.ellipse-container .team { + font-size: 18px; + text-align: center; + padding: 10px; +} + +.ellipse-container .greeting:after { + content: ''; + width: .3rem; + height: .3rem; + border-radius: 50%; + display: inline-block; + background-color: #09383E; + position: relative; + top: -0.65rem; + left: -5.05rem; +} + +.ellipse { + border-radius: 50%; + position: absolute; + top: 0; + border-style: solid; +} + +.ellipse__outer--thin { + width: 100%; + height: 100%; + border-width: 1px; + border-color: rgba(9, 56, 62, .3); + animation: ellipseOrbit 15s ease-in-out infinite; +} + +.ellipse__outer--thin:after { + content: ''; + background-image: url("/static/img/dial2.png"); + background-repeat: no-repeat; + background-position: center; + top: 0; + left: 0; + bottom: 0; + right: 0; + color: #000; + position: absolute; + opacity: .3; +} + +.ellipse__outer--thick { + width: 99.5%; + height: 99.5%; + border-color: #09383E transparent; + border-width: 2px; + transform: rotate(-45deg); + animation: ellipseRotate 15s ease-in-out infinite; +} + +.ellipse__orbit { + width: 2.5rem; + height: 2.5rem; + border-width: 2px; + border-color: #09383E; + top: 5rem; + right: 6.75rem; +} + +.ellipse__orbit:before { + content: ''; + width: 0.7rem; + height: 0.7rem; + border-radius: 50%; + display: inline-block; + background-color: #09383E; + margin: 0 auto; + left: 0; + right: 0; + position: absolute; + top: 50%; + transform: translateY(-50%) +} + +@keyframes ellipseRotate { + 0% { + transform: rotate(-45deg); + } + 100% { + transform: rotate(-405deg); + } +} + +@keyframes ellipseOrbit { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/*************** Overlay menu ****************/ + +.overlay { + height: 100%; + width: 0; + position: fixed; + z-index: 1; + top: 0; + left: 0; + background-color: rgb(0,0,0); + background-color: rgba(0,0,0, 0.9); + overflow-x: hidden; + transition: 0.5s; + } + +.overlay-content { + font-family: Shojumaru, Tahoma, sans-serif; + position: relative; + top: 25%; + width: 100%; + text-align: center; + margin-top: 30px; +} + +.overlay a { + padding: 8px; + text-decoration: none; + font-size: 36px; + color: #818181; + display: block; + transition: 0.3s; +} + +.overlay a:hover, .overlay a:focus { + color: #f1f1f1; +} + +.overlay .closebtn { + position: absolute; + top: 20px; + right: 45px; + font-size: 60px; +} + +@media screen and (max-height: 450px) { + .overlay a {font-size: 20px} + .overlay .closebtn { + font-size: 40px; + top: 15px; + right: 35px; + } +} \ No newline at end of file diff --git a/static/img/bg6.jpeg b/static/img/bg6.jpeg new file mode 100644 index 000000000..40d6ec418 Binary files /dev/null and b/static/img/bg6.jpeg differ diff --git a/static/img/dial2.png b/static/img/dial2.png new file mode 100644 index 000000000..1c2ab63b1 Binary files /dev/null and b/static/img/dial2.png differ diff --git a/static/img/user.png b/static/img/user.png new file mode 100644 index 000000000..c42279162 Binary files /dev/null and b/static/img/user.png differ diff --git a/templates/answer.html b/templates/answer.html new file mode 100644 index 000000000..daf46d02d --- /dev/null +++ b/templates/answer.html @@ -0,0 +1,16 @@ +{% extends 'layout.html' %} + +{% block content %} + {% include 'navbar.html' %} + +

Post a new answer

+ +
+ +
+ +
+ + +
+{% endblock %} \ No newline at end of file diff --git a/templates/delete.html b/templates/delete.html new file mode 100644 index 000000000..566549bdf --- /dev/null +++ b/templates/delete.html @@ -0,0 +1,10 @@ + + + + + Title + + + + + \ No newline at end of file diff --git a/templates/edit_answer.html b/templates/edit_answer.html new file mode 100644 index 000000000..be8fa4c7d --- /dev/null +++ b/templates/edit_answer.html @@ -0,0 +1,20 @@ +{% extends 'layout.html' %} + +{% block content %} + {% include 'navbar.html' %} + +

Edit answer

+ +
+ +
+ {% for answer in answers %} + {% if answer_id == answer['id'] %} + + {% endif %} + {% endfor %} +
+ + +
+{% endblock %} \ No newline at end of file diff --git a/templates/edit_comment.html b/templates/edit_comment.html new file mode 100644 index 000000000..899b62fd8 --- /dev/null +++ b/templates/edit_comment.html @@ -0,0 +1,20 @@ +{% extends 'layout.html' %} + +{% block content %} + {% include 'navbar.html' %} + +

Edit comment

+ +
+ +
+ {% for comment in comments %} + {% if comment_id == comment['id'] %} + + {% endif %} + {% endfor %} +
+ + +
+{% endblock %} \ No newline at end of file diff --git a/templates/edit_question.html b/templates/edit_question.html new file mode 100644 index 000000000..8df231c6d --- /dev/null +++ b/templates/edit_question.html @@ -0,0 +1,31 @@ +{% extends 'layout.html' %} + +{% block content %} + {% include 'navbar.html' %} + +

Edit question

+ +
+ {% for question in questions %} + {% if question_id == question['id'] %} +
+ + + + +
+ + + +
+
+ +
+ + + + {% endif %} + {% endfor %} + +
+{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 000000000..66d839c4d --- /dev/null +++ b/templates/index.html @@ -0,0 +1,63 @@ + + + + + + + {{ title }} | Ask Mate + + + + + + + + + +
+
+
+ +
+ +
+ +
+ × +
+ Home + Register + Login +
+
+ +
+ +

AskMate

+

By Astro Toddlers

+ +
+
+
+ +
+ +
+ +
+ +
+
+
+ + + + \ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 000000000..def8726ff --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + {{ title }} | Ask Mate + + +
+ {% block content %}{% endblock %} +
+ + + + + + + + \ No newline at end of file diff --git a/templates/list.html b/templates/list.html new file mode 100644 index 000000000..43a12b10c --- /dev/null +++ b/templates/list.html @@ -0,0 +1,59 @@ +{% extends 'layout.html' %} + +{% block content %} + {% include 'navbar.html' %} + + +

Questions

+
+ + + + + + {% for question in questions %} + + + + + + {% endfor %} + +
+ + +
{{ question['submission_time'] }}
+
+
+ +
{{ question['view_number'] }}
+
Views
+
+
+
+
+ +
{{ question['vote_number'] }}
+
Votes
+
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 000000000..d178cd9d5 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,32 @@ +{% extends 'layout.html' %} + +{% block content %} + {% include 'navbar.html' %} + +
+ + +
+ + + +{% endblock %} \ No newline at end of file diff --git a/templates/loginerror.html b/templates/loginerror.html new file mode 100644 index 000000000..3aba9f73a --- /dev/null +++ b/templates/loginerror.html @@ -0,0 +1,10 @@ +{% extends 'layout.html' %} + +{% block content %} + {% include 'navbar.html' %} + +

Login error!

+

You are already logged in.

+ + +{% endblock %} \ No newline at end of file diff --git a/templates/navbar.html b/templates/navbar.html new file mode 100644 index 000000000..67ad3db5d --- /dev/null +++ b/templates/navbar.html @@ -0,0 +1,47 @@ + diff --git a/templates/newcomment.html b/templates/newcomment.html new file mode 100644 index 000000000..821461624 --- /dev/null +++ b/templates/newcomment.html @@ -0,0 +1,18 @@ +{% extends 'layout.html' %} + +{% block content %} +

Post a new comment

+ {% if answer_id == '' %} +
+ {% else %} + + {% endif %} + +
+ +
+ + + +
+{% endblock %} \ No newline at end of file diff --git a/templates/newquestion.html b/templates/newquestion.html new file mode 100644 index 000000000..63c4297be --- /dev/null +++ b/templates/newquestion.html @@ -0,0 +1,24 @@ +{% extends 'layout.html' %} + +{% block content %} + {% include 'navbar.html' %} + +

Add new question

+ +
+ +
+ + +
+ +
+
+ +
+ + + + +
+{% endblock %} \ No newline at end of file diff --git a/templates/questiondetails.html b/templates/questiondetails.html new file mode 100644 index 000000000..97e19f8bf --- /dev/null +++ b/templates/questiondetails.html @@ -0,0 +1,178 @@ +{% extends 'layout.html' %} + +{% block content %} + {% include 'navbar.html' %} + + + + {% for question in questions %} + {% if id == question['id'] %} + +

{{ question['title'] }}

+ +
+ + + + + + + + + + + +
+
+

{{ question['message'] }}

+
+ +
+ Comment + +
+ +
+
+ +
{{ question['submission_time'] }}
+
+
+ +
+ +
+
{{ question['vote_number'] }}
+ +
+ +
+
Votes
+
+
+
+ {% endif %} + {% endfor %} + + + + {% for question in questions %} + {% if id == question['id'] %} + +

Question Comments

+
+ +
+ {% for comment in comments %} + + {% if comment['question_id'] == question['id'] and comment['answer_id'] == None %} + +
+

{{ comment['message'] }}

+
+ +
+ +
+
+
{{ comment['submission_time'] }}
+
+
+ {% endif %} + {% endfor %} +
+ + {% endif %} + {% endfor %} + + +
+

Answers

+ Answer +
+ + + {% for answer in answers %} + {% if id == answer['question_id'] %} + + + + + {% endif %} + {% endfor %} + +
+
+

{{ answer['message'] }}

+
+ + + {% if answer['acception'] == False %} + {% if session["user_id"] == user_id %} +
+ +
+ {% endif %} + {% endif %} +
+ + {% if answer['acception'] == True %} + ✔Accepted by user + {% endif %} + + Comment + +
+ +
+
+
+
{{ answer['submission_time'] }}
+ +
+ +
+
+ +
+ +
+
{{ answer['vote_number'] }}
+
+ +
+
Votes
+
+
+
+ + + +

Answer Comments

+
+ + {% for answer in answers %} + {% if id == answer['question_id'] %} +
+ + {% for comment in comments %} + {% if comment['answer_id'] == answer['id'] %} +
+

Answer: {{ answer['message'] }}

+

Comment: {{ comment['message'] }}

+
+ +
+ +
+
+
{{ comment['submission_time'] }}
+
+
+ {% endif %} + {% endfor %} +
+ {% endif %} + {% endfor %} +
+{% endblock %} diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 000000000..17be7342c --- /dev/null +++ b/templates/register.html @@ -0,0 +1,27 @@ +{% extends 'layout.html' %} + +{% block content %} + {% include 'navbar.html' %} + +
+ + +
+ + + +{% endblock %} \ No newline at end of file diff --git a/templates/search.html b/templates/search.html new file mode 100644 index 000000000..0bacf374a --- /dev/null +++ b/templates/search.html @@ -0,0 +1,47 @@ +{% extends 'layout.html' %} + +{% block content %} + {% include 'navbar.html' %} + + +

Questions

+
+ + + + + + {% for question in questions %} + + + + + + {% endfor %} + + + +
+
+ +
{{ question['view_number'] }}
+
Views
+
+
+
+ +
{{ question['vote_number'] }}
+
Votes
+
+
+ + +
{{ question['submission_time'] }}
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/user_profile.html b/templates/user_profile.html new file mode 100644 index 000000000..9f672db1b --- /dev/null +++ b/templates/user_profile.html @@ -0,0 +1,69 @@ +{% extends 'layout.html' %} + +{% block content %} + {% include 'navbar.html' %} + +
+ +
+
+ profile picture +
+
+ +
+
+

+ {{ user_name }} +

+ +
+
+
+
+ {% for data in datas %} + {% if user_id == data['user_id'] %} +
+
+ +
{{ data['submission_time'] }}
+ +
+
+
+ +
{{ data['view_number'] }}
+
Views
+
+
+
+
+
+ +
{{ data['vote_number'] }}
+
Votes
+
+
+
+

+ +
+ {% endif %} + {% endfor %} + +
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/users_list.html b/templates/users_list.html new file mode 100644 index 000000000..56ad218e3 --- /dev/null +++ b/templates/users_list.html @@ -0,0 +1,31 @@ +{% extends 'layout.html' %} + +{% block content %} + {% include 'navbar.html' %} + + +

Users

+
+ + + + + + + {% for user in users %} + + + + + + {% endfor %} + +
+ +
{{ user['registration_date'] }}
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/util.py b/util.py new file mode 100644 index 000000000..14e412ec2 --- /dev/null +++ b/util.py @@ -0,0 +1,5 @@ +import time + + +def get_current_time(): + return time.time()