diff --git a/data/boards.csv b/data/boards.csv index b8d2ca77..0d5badd0 100644 --- a/data/boards.csv +++ b/data/boards.csv @@ -1,3 +1,9 @@ -id,title -1,"Board 1" -2,"Board 2" +id,title,userid +1,"Board public 1",0 +2,"Board user 1",1 +3,"Board user 2",2 +4,"Board user 2_2",2 +5,"Board public 2",0 +6,"Board user 3",3 +7,"Board user 2_3",2 +8,"Board public 3",0 diff --git a/data/cards.csv b/data/cards.csv index 02d8e81f..7aacb3b0 100644 --- a/data/cards.csv +++ b/data/cards.csv @@ -11,3 +11,4 @@ id,board_id,title,status_id,order 10,2,"planning",2,0 11,2,"done card 1",3,0 12,2,"done card 1",3,1 +13,3,"planning",2,1 diff --git a/data/statuses.csv b/data/statuses.csv index d027decd..2fe62fa3 100644 --- a/data/statuses.csv +++ b/data/statuses.csv @@ -1,5 +1,5 @@ id,title 0,new -1,"in progress" +1,"in_progress" 2,testing 3,done diff --git a/data/users.csv b/data/users.csv new file mode 100644 index 00000000..4023e3c3 --- /dev/null +++ b/data/users.csv @@ -0,0 +1,3 @@ +id,username,password +1,gabor,$2b$12$cyufJCWyed8lP57ATXq.9.SdfRu5S7jHOzloY1AI8kuBjpbe4.MvG +2,alma,$2b$12$bmNkkgmQEYhw4WDdpAOYze5PqXBKwj.E2IkX7zC60E.NBFNuce/I2 diff --git a/data_handler.py b/data_handler.py index 0d8d9086..a6af53a6 100644 --- a/data_handler.py +++ b/data_handler.py @@ -28,3 +28,11 @@ def get_cards_for_board(board_id): card['status_id'] = get_card_status(card['status_id']) # Set textual status for the card matching_cards.append(card) return matching_cards + + +def get_users(): + return persistence.get_users(force=True) + + +def write_users(user_data): + return persistence.write_users(user_data) diff --git a/main.py b/main.py index 9e25a1ac..d9775537 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,14 @@ -from flask import Flask, render_template, url_for -from util import json_response +from flask import Flask, render_template, url_for, request +# from util import json_response +import util +import json import data_handler app = Flask(__name__) -@app.route("/") +@app.route("/", methods=["GET", "POST"]) def index(): """ This is a one-pager which shows all the boards and cards @@ -15,7 +17,7 @@ def index(): @app.route("/get-boards") -@json_response +@util.json_response def get_boards(): """ All the boards @@ -24,7 +26,7 @@ def get_boards(): @app.route("/get-cards/") -@json_response +@util.json_response def get_cards_for_board(board_id: int): """ All cards that belongs to a board @@ -33,6 +35,43 @@ def get_cards_for_board(board_id: int): return data_handler.get_cards_for_board(board_id) +@app.route('/login', methods=["GET", "POST"]) +def login(): + users = data_handler.get_users() + user_data = util.get_user_data_from_form(request.form) + + if util.check_user_login(user_data, users): + userid = util.get_usr_id(user_data["username"], users) + dic = {"userid": userid, + "success": True} + + return json.dumps(dic), 200, {'ContentType': 'application/json'} + return json.dumps({'success': False}), 500, {'ContentType': 'application/json'} + + +@app.route('/register', methods=["POST"]) +def register(): + user_data = util.get_user_data_from_form(request.form) + users = data_handler.get_users() + if not util.check_user_exists(user_data["username"], users): + user_data["password"] = util.hash_password(user_data["password"]) + user_data["id"] = util.get_max_id(data_handler.get_users()) + 1 + if users: + users.append(user_data) + else: + users = [user_data] + print(users) + data_handler.write_users(users) + + userid = util.get_usr_id(user_data["username"], users) + dic = {"userid": userid, + "success": True} + + return json.dumps(dic), 200, {'ContentType': 'application/json'} + + return json.dumps({'success': False}), 500, {'ContentType': 'application/json'} + + def main(): app.run(debug=True) diff --git a/persistence.py b/persistence.py index 1e1f1e3d..568800ee 100644 --- a/persistence.py +++ b/persistence.py @@ -1,12 +1,24 @@ import csv STATUSES_FILE = './data/statuses.csv' +STATUSES_HEADER = ['id', 'title'] BOARDS_FILE = './data/boards.csv' +BOARDS_HEADER = ['id', 'title', 'userid'] CARDS_FILE = './data/cards.csv' +CARDS_HEADER = ['id', 'board_id', 'title', 'status_id', 'order'] +USERS_FILE = './data/users.csv' +USERS_HEADER = ['id', 'username', 'password'] _cache = {} # We store cached data in this dict to avoid multiple file readings +def _write_csv(file_name, data, header): + with open(file_name, 'w') as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=header, delimiter=',', quotechar='"') + writer.writeheader() + writer.writerows(data) + + def _read_csv(file_name): """ Reads content of a .csv file @@ -49,3 +61,23 @@ def get_boards(force=False): def get_cards(force=False): return _get_data('cards', CARDS_FILE, force) + + +def get_users(force=False): + return _get_data('users', USERS_FILE, force) + + +def write_statuses(data): + return _write_csv(STATUSES_FILE, data, STATUSES_HEADER) + + +def write_boards(data): + return _write_csv(BOARDS_FILE, data, BOARDS_HEADER) + + +def write_cards(data): + return _write_csv(CARDS_FILE, data, CARDS_HEADER) + + +def write_users(data): + return _write_csv(USERS_FILE, data, USERS_HEADER) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..55e871c9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +bcrypt==3.1.6 +cffi==1.12.3 +Click==7.0 +Flask==1.0.2 +itsdangerous==1.1.0 +Jinja2==2.10.1 +MarkupSafe==1.1.1 +pycparser==2.19 +six==1.12.0 +Werkzeug==0.15.2 diff --git a/static/css/main.css b/static/css/main.css index d8f3b84f..a8b8f2f2 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -1,3 +1,129 @@ -#boards { - background-color: green; -} \ No newline at end of file +:root{ + --border-radius: 3px; + --status-1: #590000; + --status-2: #594300; + --status-3: #525900; + --status-4: #085900; +} + +body{ + min-width: 620px; + background: #ddd url(http://cdn.backgroundhost.com/backgrounds/subtlepatterns/diagonal-noise.png); + font-family: sans-serif; +} + +h1, .board-title, .board-column-title{ + font-weight: 100; +} + +h1{ + text-align: center; + font-size: 4em; + letter-spacing: 5px; + transform: scale(1.2, 1); +} + +button{ + background: #222; + color: #fff; + border: none; + font-size: 14px; + font-family: sans-serif; + padding: 4px 10px; +} + +.board-container{ + max-width: 960px; + margin: 0 auto; +} + +section.board{ + margin: 20px; + border: aliceblue; + background: #ffffff90; + border-radius: 3px; +} + +.board-header{ + height: 50px; + background: #fff; + border-radius: var(--border-radius) var(--border-radius) 0 0; +} + +.board-title{ + margin: 13px; + display: inline-block; + font-size: 20px; +} +.board-title, .board-add, .board-toggle{ + display: inline-block; +} + +.board-toggle{ + float: right; + margin: 13px; +} + +.board-columns{ + display: flex; + flex-wrap: nowrap; +} + +.board-column{ + padding: 10px; + flex: 1; +} + +.board-column-content{ + min-height: 49px; +} + +.board-column-content:empty{ + /*This only works if the tag is really empty and there is not even whitespace inside*/ + border: 4px solid #cdcdcd; + margin-top: 4px; + border-radius: 10px; + background: #eee; +} + +.board-column-title{ + text-align: center; +} + +.card{ + position: relative; + background: #222; + color: #fff; + border-radius: var(--border-radius); + margin: 4px 0; + padding: 4px; +} + +.board-column:nth-of-type(1) .card{ + background: var(--status-1); +} + +.board-column:nth-of-type(2) .card{ + background: var(--status-2); +} + +.board-column:nth-of-type(3) .card{ + background: var(--status-3); +} + +.board-column:nth-of-type(4) .card{ + background: var(--status-4); +} + +.card-remove{ + display: block; + position: absolute; + top: 4px; + right: 4px; + font-size: 12px; + cursor: pointer; +} + +.card-title{ + padding-right: 16px; +} diff --git a/static/js/data_handler.js b/static/js/data_handler.js index 66df0ba8..bd3daa54 100644 --- a/static/js/data_handler.js +++ b/static/js/data_handler.js @@ -24,6 +24,9 @@ export let dataHandler = { }, getBoards: function (callback) { // the boards are retrieved and then the callback function is called with the boards + if(this._data.hasOwnProperty('boards')){ + callback(this._data.boards); + } // Here we use an arrow function to keep the value of 'this' on dataHandler. // if we would use function(){...} here, the value of 'this' would change. @@ -43,6 +46,13 @@ export let dataHandler = { }, getCardsByBoardId: function (boardId, callback) { // the cards are retrieved and then the callback function is called with the cards + if (this._data.hasOwnProperty('cards')) { + callback(this._data.cards); + } + this._api_get(`/get-cards/${boardId}`, (response) => { + this._data = response; + callback(response) + }); }, getCard: function (cardId, callback) { // the card is retrieved and then the callback function is called with the card @@ -55,3 +65,5 @@ export let dataHandler = { } // here comes more features }; + +sessionStorage.get \ No newline at end of file diff --git a/static/js/dom.js b/static/js/dom.js index 0092afb7..e7a36dae 100644 --- a/static/js/dom.js +++ b/static/js/dom.js @@ -1,5 +1,5 @@ // It uses data_handler.js to visualize elements -import { dataHandler } from "./data_handler.js"; +import {dataHandler} from "./data_handler.js"; export let dom = { _appendToElement: function (elementToExtend, textToAppend, prepend = false) { @@ -19,39 +19,170 @@ export let dom = { }, init: function () { // This function should run once, when the page is loaded. + document.getElementById('logout').addEventListener('click', dom.logout); + document.getElementById('login').addEventListener('click', dom.login); + document.getElementById('register').addEventListener('click', dom.register); + dom.showLoggedIn(); }, loadBoards: function () { // retrieves boards and makes showBoards called - dataHandler.getBoards(function(boards){ + dataHandler.getBoards(function (boards) { dom.showBoards(boards); }); }, + showBoards: function (boards) { // shows boards appending them to #boards div // it adds necessary event listeners also + const boardContainer = document.querySelector('.board-container'); + boardContainer.innerHTML = ''; - let boardList = ''; + let userid = dom.getUserIdFromSession(); - for(let board of boards){ - boardList += ` -
  • ${board.title}
  • - `; + for (let board of boards) { + if ((board['userid'] === '0' && !board['userid']) || board['userid'] === userid) { + let template = document.querySelector('#board_header'); + let clone = document.importNode(template.content, true); + let section = document.createElement(`section`); + section.id = `board${board.id}`; + section.classList.add(`board`); + clone.querySelector('.board-toggle').setAttribute('target', board.id); + clone.querySelector('.board-title').innerHTML = board.title; + section.appendChild(clone); + boardContainer.appendChild(section); + dom.loadCards(board.id); + } } - - const outerHtml = ` - - `; - - this._appendToElement(document.querySelector('#boards'), outerHtml); }, + loadCards: function (boardId) { - // retrieves cards and makes showCards called + dataHandler.getCardsByBoardId(boardId, function (cards) { + dom.showCards(boardId, cards) + }) }, - showCards: function (cards) { + showCards: function (boardId, cards) { // shows the cards of a board // it adds necessary event listeners also + const board = document.querySelector(`#board${boardId}`); + let template_column = document.querySelector('#board_columns'); + let clone_columns = document.importNode(template_column.content, true); + clone_columns.querySelector('.board-columns').id = `box${boardId}`; + for (let card of cards) { + let card_template = document.querySelector('#card_sample'); + let clone_card = document.importNode(card_template.content, true); + clone_card.querySelector('.card-title').innerHTML = card.title; + clone_card.querySelector('.card').setAttribute('data-order', card.order); + clone_card.querySelector('.card').setAttribute('data-cardId', card.id); + let column = clone_columns.querySelector(`.${card.status_id}`); + column.querySelector('.board-column-content').appendChild(clone_card); + } + board.appendChild(clone_columns); + }, + slide: function () { + $(document).ready(function () { + $('.board-toggle').click(function () { + $('#box' + $(this).attr('target')).slideToggle(400); + }); + }); }, // here comes more features + + login: function () { + + let data = { + "title": "Login", + "button_text": "Login", + "url": "/login", + "message_success": "Logged in ", + "message_fail": "Wrong user name or password" + }; + + dom.getAjax(data); + }, + + + register: function () { + + let data = { + "title": "Register new user", + "button_text": "Register", + "url": "/register", + "message_success": "Register succesfull", + "message_fail": "Register failed" + }; + + dom.getAjax(data); + }, + + getAjax: function (data) { + event.preventDefault(); + let form_values = {}; + + + $('#inputLabel').text(data["title"]); + $('#submit-button').text(data["button_text"]); + + + $('#inputModal').modal({show: true}); + + $('#submit-button').click(function () { + let $inputs = $('#inputForm :input'); + $inputs.each(function () { + form_values[this.name] = $(this).val(); + }); + + $.ajax({ + type: 'POST', + url: data['url'], + dataType: 'json', + data: form_values + }) + .then( + function success(data) { + + sessionStorage.setItem('username', form_values["user-name"]); + sessionStorage.setItem('userid', data["userid"]); + dom.showLoggedIn(form_values["user-name"], true); + location.reload() + }, + function fail() { + alert(data["message_fail"]); + location.reload(); + } + ); + + + }); + }, + showLoggedIn: function () { + let username = sessionStorage.getItem("username"); + let register = document.getElementById("register"); + let login = document.getElementById("login"); + let logout = document.getElementById("logout"); + let navbar = document.getElementById("navbar-text"); + if (username) { + navbar.style.display = 'block'; + navbar.innerText = `Signed in as ${username}`; + register.style.display = 'none'; + login.style.display = 'none'; + logout.style.display = 'block'; + } else { + navbar.style.display = 'none'; + register.style.display = 'block'; + login.style.display = 'block'; + logout.style.display = 'none'; + } + }, + + logout: function () { + if (sessionStorage.getItem("username")) + sessionStorage.removeItem("username"); + sessionStorage.removeItem("userid"); + location.reload(); + }, + + getUserIdFromSession: function () { + return sessionStorage.getItem("userid") ? sessionStorage.getItem("userid") : '0' + } }; + diff --git a/static/js/main.js b/static/js/main.js index f9298ae2..b201bc80 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -7,6 +7,7 @@ function init() { // loads the boards to the screen dom.loadBoards(); + dom.slide(); } init(); diff --git a/templates/index.html b/templates/index.html index 6f42f26e..b938b48b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,21 +1,39 @@ - - - - - ProMan + + + + + ProMan + + + + + + + + - - - - - - - - - -

    ProMan

    -
    Boards are loading...
    - + + + + + + + + +{% include 'navbar.html' %} +

    ProMan

    +
    Boards are loading...
    +{% include 'templates.html' %} +{% include 'modal.html' %} + \ No newline at end of file diff --git a/templates/modal.html b/templates/modal.html new file mode 100644 index 00000000..f448678f --- /dev/null +++ b/templates/modal.html @@ -0,0 +1,32 @@ + + + diff --git a/templates/navbar.html b/templates/navbar.html new file mode 100644 index 00000000..d17c8e27 --- /dev/null +++ b/templates/navbar.html @@ -0,0 +1,31 @@ + \ No newline at end of file diff --git a/templates/templates.html b/templates/templates.html new file mode 100644 index 00000000..4f7739a4 --- /dev/null +++ b/templates/templates.html @@ -0,0 +1,47 @@ + + + +{# Board header template parent section class="board" #} + + +{# Board coulumn template parent: div class="board-columns" #} + + +{# Card sample parent:board-column-content #} + + diff --git a/util.py b/util.py index 352cbd88..d716c797 100644 --- a/util.py +++ b/util.py @@ -1,5 +1,6 @@ from functools import wraps from flask import jsonify +import bcrypt def json_response(func): @@ -8,8 +9,56 @@ def json_response(func): :param func: :return: """ + @wraps(func) def decorated_function(*args, **kwargs): return jsonify(func(*args, **kwargs)) return decorated_function + + +def hash_password(plain_text_password): + hashed_bytes = bcrypt.hashpw(plain_text_password.encode('utf-8'), bcrypt.gensalt()) + return hashed_bytes.decode('utf-8') + + +def verify_password(plain_text_password, hashed_password): + hashed_bytes_password = hashed_password.encode('utf-8') + return bcrypt.checkpw(plain_text_password.encode('utf-8'), hashed_bytes_password) + + +def check_user_exists(username, users): + if len(users)<1: + return False + for user in users: + if user["username"] == username: + return user["password"] + return False + + +def check_user_login(user_data, users): + user_password = check_user_exists(user_data["username"], users) + if not user_password: + return False + + if not verify_password(user_data["password"], user_password): + return False + return True + + +def get_user_data_from_form(request): + data = { + "username": request.get('user-name'), + "password": request.get('password') + } + return data + + +def get_max_id(data): + print(len(data)) + return int(max(row["id"] for row in data)) if len(data) > 0 else 0 + +def get_usr_id(username, users): + for user in users: + if user["username"] == username: + return user["id"]