Skip to content
4 changes: 4 additions & 0 deletions data_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ def get_boards():
return persistence.get_boards(force=True)


def add_new_board(title):
return persistence.add_new_board(title)


def get_cards_for_board(board_id):
persistence.clear_cache()
all_cards = persistence.get_cards()
Expand Down
105 changes: 105 additions & 0 deletions database_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import os
import psycopg2
import psycopg2.extras


def establish_connection(connection_data=None):
"""
Create a database connection based on the :connection_data: parameter

:connection_data: Connection string attributes

:returns: psycopg2.connection
"""
if connection_data is None:
connection_data = get_connection_data()
try:
connect_str = "dbname={} user={} host={} password={}".format(connection_data['dbname'],
connection_data['user'],
connection_data['host'],
connection_data['password'])
conn = psycopg2.connect(connect_str)
conn.autocommit = True
except psycopg2.DatabaseError as e:
print("Cannot connect to database.")
print(e)
else:
return conn


def get_connection_data(db_name=None):
"""
Give back a properly formatted dictionary based on the environment variables values which are started
with :MY__PSQL_: prefix

:db_name: optional parameter. By default it uses the environment variable value.
"""
if db_name is None:
db_name = os.environ.get('MY_PSQL_DBNAME')

return {
'dbname': db_name,
'user': os.environ.get('MY_PSQL_USER'),
'host': os.environ.get('MY_PSQL_HOST'),
'password': os.environ.get('MY_PSQL_PASSWORD')
}


def execute_script_file(file_path):
"""
Execute script file based on the given file path.
Print the result of the execution to console.

Example:
> execute_script_file('db_schema/01_create_schema.sql')

:file_path: Relative path of the file to be executed.
"""
package_directory = os.path.dirname(os.path.abspath(__file__))
full_path = os.path.join(package_directory, file_path)
with open(full_path) as script_file:
with establish_connection() as conn, \
conn.cursor() as cursor:
try:
sql_to_run = script_file.read()
cursor.execute(sql_to_run)
print("{} script executed successfully.".format(file_path))
except Exception as ex:
print("Execution of {} failed".format(file_path))
print(ex.args)


def execute_select(statement, variables=None):
"""
Execute SELECT statement optionally parameterized

Example:
> execute_select('SELECT %(title)s; FROM shows', variables={'title': 'Codecool'})

:statement: SELECT statement

:variables: optional parameter dict"""
result_set = []
with establish_connection() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor:
cursor.execute(statement, variables)
result_set = cursor.fetchall()
return result_set


def execute_dml_statement(statement, variables=None):
"""
Execute data manipulation query statement (optionally parameterized)

:statment: SQL statement

:variables: optional parameter dict"""
result = None
with establish_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(statement, variables)
try:
result = cursor.fetchone()
except psycopg2.ProgrammingError as pe:
pass
return result
4 changes: 2 additions & 2 deletions design-materials/design.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/solid.css" integrity="sha384-rdyFrfAIC05c5ph7BKz3l5NG5yEottvO/DQ0dCrwD8gzeQDjYBHNr1ucUpQuljos" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/fontawesome.css" integrity="sha384-u5J7JghGz0qUrmEsWzBQkfvc8nK3fUT7DCaQzNQ+q4oEXhGSx+P2OqjWsfIRB8QT" crossorigin="anonymous">

<link rel="stylesheet" href="design.css">
<link rel="stylesheet" href="../static/css/design.css">
</head>
<body>
<h1>ProMan</h1>
Expand All @@ -18,7 +18,7 @@ <h1>ProMan</h1>
<section class="board">
<div class="board-header"><span class="board-title">Board 1</span>
<button class="board-add">Add Card</button>
<button class="board-toggle"><i class="fas fa-chevron-down"></i></button>
<button class="board-toggle"><i class=""></i></button>
</div>
<div class="board-columns">
<div class="board-column">
Expand Down
17 changes: 14 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from flask import Flask, render_template, url_for
from flask import Flask, render_template, url_for, request
from util import json_response

import persistence
import data_handler
import json


app = Flask(__name__)

Expand All @@ -20,7 +22,8 @@ def get_boards():
"""
All the boards
"""
return data_handler.get_boards()

return persistence.get_sql_boards()


@app.route("/get-cards/<int:board_id>")
Expand All @@ -33,6 +36,14 @@ def get_cards_for_board(board_id: int):
return data_handler.get_cards_for_board(board_id)


@app.route('/add-new-board', methods=["GET", "POST"])
@json_response
def add_new_board():
title = request.get_json()['title']
return data_handler.add_new_board(title)



def main():
app.run(debug=True)

Expand Down
54 changes: 6 additions & 48 deletions persistence.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,9 @@
import csv
import database_connection

STATUSES_FILE = './data/statuses.csv'
BOARDS_FILE = './data/boards.csv'
CARDS_FILE = './data/cards.csv'
def get_sql_boards():
return database_connection.execute_select('SELECT * FROM boards;')

_cache = {} # We store cached data in this dict to avoid multiple file readings


def _read_csv(file_name):
"""
Reads content of a .csv file
:param file_name: relative path to data file
:return: OrderedDict
"""
with open(file_name) as boards:
rows = csv.DictReader(boards, delimiter=',', quotechar='"')
formatted_data = []
for row in rows:
formatted_data.append(dict(row))
return formatted_data


def _get_data(data_type, file, force):
"""
Reads defined type of data from file or cache
:param data_type: key where the data is stored in cache
:param file: relative path to data file
:param force: if set to True, cache will be ignored
:return: OrderedDict
"""
if force or data_type not in _cache:
_cache[data_type] = _read_csv(file)
return _cache[data_type]


def clear_cache():
for k in list(_cache.keys()):
_cache.pop(k)


def get_statuses(force=False):
return _get_data('statuses', STATUSES_FILE, force)


def get_boards(force=False):
return _get_data('boards', BOARDS_FILE, force)


def get_cards(force=False):
return _get_data('cards', CARDS_FILE, force)
def add_new_board(title):
return database_connection.execute_dml_statement("""INSERT INTO boards (title)
VALUES (%(title)s) RETURNING *""", dict(title=title))
9 changes: 9 additions & 0 deletions design-materials/design.css → static/css/design.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ button{
margin: 0 auto;
}

.board-template {
background: #ffffff90;
}

.add-new-board-button {
text-align: center;
margin-bottom: 5vh;
}

section.board{
margin: 20px;
border: aliceblue;
Expand Down
24 changes: 23 additions & 1 deletion static/js/data_handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export let dataHandler = {
_api_get: function (url, callback) {
// it is not called from outside
// loads data from API, parses it and calls the callback with it

const json = JSON.parse('[{"id": "1", "title": "Board 1"}, {"id": "2", "title": "Board 2"}]');
fetch(url, {
method: 'GET',
credentials: 'same-origin'
Expand All @@ -19,6 +19,17 @@ export let dataHandler = {
_api_post: function (url, data, callback) {
// it is not called from outside
// sends the data to the API, and calls callback function
fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: {
"Content-Type": "application/json",
// "Content-Type": "application/x-www-form-urlencoded",
},
body: JSON.stringify(data)
})
.then(response => response.json()) // parse the response as JSON
.then(json_response => callback(json_response));
},
init: function () {
},
Expand All @@ -32,6 +43,17 @@ export let dataHandler = {
callback(response);
});
},

addNewBoard: function (callback) {
// the boards are retrieved and then the callback function is called with the 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.
this._api_post('/add-new-board', {title: 'New Board'}, (response) => {
this._data = response;
callback(response);
});
},
getBoard: function (boardId, callback) {
// the board is retrieved and then the callback function is called with the board
},
Expand Down
63 changes: 51 additions & 12 deletions static/js/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,65 @@ export let dom = {
dom.showBoards(boards);
});
},


getBoardTitle: function (title) {
const template = document.querySelector('#board');
const clone = document.importNode(template.content, true);

if (title[1] == 'New Board') {
clone.querySelector('.board-title').textContent = title[1];
console.log("yes")
} else {
clone.querySelector('.board-title').textContent = title;
}


return clone;
},


showBoards: function (boards) {
// shows boards appending them to #boards div
// it adds necessary event listeners also
//console.log(clone);
let boardList = document.createElement("section");
boardList.id ="board";
for (let board of boards) {
boardList.appendChild(this.getBoardTitle(board.title))
}

let boardList = '';
let container = document.querySelector('.board-container');
container.appendChild(boardList);

for(let board of boards){
boardList += `
<li>${board.title}</li>
`;
}
},

const outerHtml = `
<ul class="board-container">
${boardList}
</ul>
`;

this._appendToElement(document.querySelector('#boards'), outerHtml);
addNewBoardEventListener: function () {
let addNewBoardButton = document.getElementsByClassName("add-new-board-button");
addNewBoardButton[0].addEventListener("click", this.addNewBoardClickHandler)
},


addNewBoardClickHandler: function () {
dataHandler.addNewBoard(function (newCardTitle) {
dom.showBoard(newCardTitle);
})

},

showBoard: function (newCardTitle) {
let boardList = document.createElement("section");
boardList.id ="board";

boardList.appendChild(this.getBoardTitle(newCardTitle));



let container = document.querySelector('.board-container');
container.appendChild(boardList);
},

loadCards: function (boardId) {
// retrieves cards and makes showCards called
},
Expand Down
2 changes: 2 additions & 0 deletions static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ function init() {
dom.init();
// loads the boards to the screen
dom.loadBoards();
// add event listener on 'add new board' button
dom.addNewBoardEventListener()

}

Expand Down
Loading