From 2d8320571de6397271a93f11d0a2f4bde8b92a50 Mon Sep 17 00:00:00 2001 From: Konstantin Moskalenko Date: Wed, 8 Nov 2023 20:42:08 +0700 Subject: [PATCH 01/10] Initial commit --- projects/task_manager/.gitignore | 2 ++ projects/task_manager/README.MD | 1 + projects/task_manager/app.py | 12 ++++++++++++ 3 files changed, 15 insertions(+) create mode 100644 projects/task_manager/.gitignore create mode 100644 projects/task_manager/README.MD create mode 100644 projects/task_manager/app.py diff --git a/projects/task_manager/.gitignore b/projects/task_manager/.gitignore new file mode 100644 index 0000000..f32e31a --- /dev/null +++ b/projects/task_manager/.gitignore @@ -0,0 +1,2 @@ +.idea/ +.DS_Store diff --git a/projects/task_manager/README.MD b/projects/task_manager/README.MD new file mode 100644 index 0000000..2b3ca97 --- /dev/null +++ b/projects/task_manager/README.MD @@ -0,0 +1 @@ +# Personal Task Manager diff --git a/projects/task_manager/app.py b/projects/task_manager/app.py new file mode 100644 index 0000000..4b059ec --- /dev/null +++ b/projects/task_manager/app.py @@ -0,0 +1,12 @@ +from flask import Flask + +app = Flask(__name__) + + +@app.route('/') +def hello_world(): + return 'Hello World!' + + +if __name__ == '__main__': + app.run() From 3666475414e3c0d8518b8a33c2b3a0a529079aff Mon Sep 17 00:00:00 2001 From: Konstantin Moskalenko Date: Wed, 22 Nov 2023 20:12:27 +0700 Subject: [PATCH 02/10] Implement the database manager class --- projects/task_manager/.gitignore | 2 + projects/task_manager/app.py | 6 +++ projects/task_manager/db.py | 75 ++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 projects/task_manager/db.py diff --git a/projects/task_manager/.gitignore b/projects/task_manager/.gitignore index f32e31a..135ebce 100644 --- a/projects/task_manager/.gitignore +++ b/projects/task_manager/.gitignore @@ -1,2 +1,4 @@ .idea/ .DS_Store + +instance/ \ No newline at end of file diff --git a/projects/task_manager/app.py b/projects/task_manager/app.py index 4b059ec..e743395 100644 --- a/projects/task_manager/app.py +++ b/projects/task_manager/app.py @@ -1,6 +1,12 @@ +import os from flask import Flask app = Flask(__name__) +app.config.from_mapping( + DATABASE=os.path.join(app.instance_path, 'tasks.sqlite') +) + +os.makedirs(app.instance_path, exist_ok=True) @app.route('/') diff --git a/projects/task_manager/db.py b/projects/task_manager/db.py new file mode 100644 index 0000000..5a6e325 --- /dev/null +++ b/projects/task_manager/db.py @@ -0,0 +1,75 @@ +import sqlite3 +from enum import Enum, auto +from flask import current_app + + +class TaskPriority(Enum): + LOW = auto() + MEDIUM = auto() + HIGH = auto() + + +class TaskManager: + def __init__(self): + db_name = current_app.config['DATABASE'] + self.conn = sqlite3.connect(db_name) + self.create_table() + + def __del__(self): + self.conn.close() + + def create_table(self): + cursor = self.conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + deadline DATE, + priority TEXT CHECK(priority IN ('LOW', 'MEDIUM', 'HIGH')) + ) + ''') + self.conn.commit() + + def add_task(self, title, description, deadline, priority): + TaskManager.__validate_priority(priority) + + cursor = self.conn.cursor() + cursor.execute(''' + INSERT INTO tasks (title, description, deadline, priority) + VALUES (?, ?, ?, ?) + ''', (title, description, deadline, priority.name)) + self.conn.commit() + + def get_tasks(self): + cursor = self.conn.cursor() + cursor.execute('SELECT * FROM tasks') + return cursor.fetchall() + + def update_task(self, task_id, title=None, description=None, deadline=None, priority=None): + update_query = 'UPDATE tasks SET ' + updates = [] + if title: + updates.append(f'title = "{title}"') + if description: + updates.append(f'description = "{description}"') + if deadline: + updates.append(f'deadline = "{deadline}"') + if priority: + TaskManager.__validate_priority(priority) + updates.append(f'priority = "{priority.name}"') + + update_query += ', '.join(updates) + f' WHERE id = {task_id}' + cursor = self.conn.cursor() + cursor.execute(update_query) + self.conn.commit() + + def delete_task(self, task_id): + cursor = self.conn.cursor() + cursor.execute('DELETE FROM tasks WHERE id = ?', (task_id,)) + self.conn.commit() + + @staticmethod + def __validate_priority(priority): + if not isinstance(priority, TaskPriority): + raise ValueError('Priority must be of type "TaskPriority"') From ba7b26b7a5dd15775d876654196ba1ee1a314dcd Mon Sep 17 00:00:00 2001 From: Konstantin Moskalenko Date: Thu, 23 Nov 2023 10:29:51 +0700 Subject: [PATCH 03/10] Implement the basic task manager pages --- projects/task_manager/app.py | 8 +-- projects/task_manager/db.py | 13 ++++- projects/task_manager/routes.py | 52 +++++++++++++++++++ projects/task_manager/static/list_style.css | 14 +++++ .../task_manager/templates/task_form.html | 37 +++++++++++++ .../task_manager/templates/task_list.html | 45 ++++++++++++++++ 6 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 projects/task_manager/routes.py create mode 100644 projects/task_manager/static/list_style.css create mode 100644 projects/task_manager/templates/task_form.html create mode 100644 projects/task_manager/templates/task_list.html diff --git a/projects/task_manager/app.py b/projects/task_manager/app.py index e743395..85f4c73 100644 --- a/projects/task_manager/app.py +++ b/projects/task_manager/app.py @@ -1,18 +1,14 @@ import os from flask import Flask +from routes import tasks_bp app = Flask(__name__) +app.register_blueprint(tasks_bp) app.config.from_mapping( DATABASE=os.path.join(app.instance_path, 'tasks.sqlite') ) os.makedirs(app.instance_path, exist_ok=True) - -@app.route('/') -def hello_world(): - return 'Hello World!' - - if __name__ == '__main__': app.run() diff --git a/projects/task_manager/db.py b/projects/task_manager/db.py index 5a6e325..b07b688 100644 --- a/projects/task_manager/db.py +++ b/projects/task_manager/db.py @@ -1,6 +1,6 @@ import sqlite3 from enum import Enum, auto -from flask import current_app +from flask import current_app, g class TaskPriority(Enum): @@ -46,6 +46,11 @@ def get_tasks(self): cursor.execute('SELECT * FROM tasks') return cursor.fetchall() + def get_task_by_id(self, task_id): + cursor = self.conn.cursor() + cursor.execute('SELECT * FROM tasks WHERE id = ?', (task_id,)) + return cursor.fetchone() + def update_task(self, task_id, title=None, description=None, deadline=None, priority=None): update_query = 'UPDATE tasks SET ' updates = [] @@ -69,6 +74,12 @@ def delete_task(self, task_id): cursor.execute('DELETE FROM tasks WHERE id = ?', (task_id,)) self.conn.commit() + @staticmethod + def get_instance(): + if 'task_manager' not in g: + g.task_manager = TaskManager() + return g.task_manager + @staticmethod def __validate_priority(priority): if not isinstance(priority, TaskPriority): diff --git a/projects/task_manager/routes.py b/projects/task_manager/routes.py new file mode 100644 index 0000000..71bc267 --- /dev/null +++ b/projects/task_manager/routes.py @@ -0,0 +1,52 @@ +from db import TaskManager, TaskPriority +from flask import Blueprint, render_template, request, redirect, url_for + +tasks_bp = Blueprint('tasks', __name__, template_folder='templates') + + +@tasks_bp.route('/') +@tasks_bp.route('/tasks') +def show_tasks(): + task_manager = TaskManager.get_instance() + tasks = task_manager.get_tasks() + return render_template('task_list.html', tasks=tasks) + + +@tasks_bp.route('/add_task', methods=['GET', 'POST']) +def add_task(): + if request.method == 'POST': + title = request.form['title'] + description = request.form['description'] + deadline = request.form['deadline'] + priority = TaskPriority[request.form['priority']] + + task_manager = TaskManager.get_instance() + task_manager.add_task(title, description, deadline, priority) + return redirect(url_for('.show_tasks')) + return render_template('task_form.html') + + +@tasks_bp.route('/update_task/', methods=['GET', 'POST']) +def update_task(task_id): + task_manager = TaskManager.get_instance() + task = task_manager.get_task_by_id(task_id) + if not task: + return "Task not found", 404 + + if request.method == 'POST': + title = request.form['title'] + description = request.form['description'] + deadline = request.form['deadline'] + priority = TaskPriority[request.form['priority']] + + task_manager.update_task(task_id, title, description, deadline, priority) + return redirect(url_for('.show_tasks')) + + return render_template('task_form.html', task=task) + + +@tasks_bp.route('/delete_task/', methods=['POST']) +def delete_task(task_id): + task_manager = TaskManager.get_instance() + task_manager.delete_task(task_id) + return redirect(url_for('.show_tasks')) diff --git a/projects/task_manager/static/list_style.css b/projects/task_manager/static/list_style.css new file mode 100644 index 0000000..fbd5fdd --- /dev/null +++ b/projects/task_manager/static/list_style.css @@ -0,0 +1,14 @@ +table { + width: 100%; + border-collapse: collapse; +} + +th, td { + border: 1px solid #dddddd; + padding: 8px; + text-align: left; +} + +th { + background-color: #f2f2f2; +} \ No newline at end of file diff --git a/projects/task_manager/templates/task_form.html b/projects/task_manager/templates/task_form.html new file mode 100644 index 0000000..cbd842f --- /dev/null +++ b/projects/task_manager/templates/task_form.html @@ -0,0 +1,37 @@ + + + + + Task Form + + +{% if task %} +

Update Task

+
+{% else %} +

Add Task

+ +{% endif %} +
+

+ +
+

+ +
+

+ +
+

+ + +
+
+Back to Task List + + diff --git a/projects/task_manager/templates/task_list.html b/projects/task_manager/templates/task_list.html new file mode 100644 index 0000000..7968cb0 --- /dev/null +++ b/projects/task_manager/templates/task_list.html @@ -0,0 +1,45 @@ + + + + + Task List + + + + +

Task List

+ + + + + + + + + + + + + {% for task in tasks %} + + + + + + + + + {% endfor %} + +
IDTitleDescriptionDeadlinePriorityActions
{{ task[0] }}{{ task[1] }}{{ task[2] }}{{ task[3] }}{{ task[4] }} +
+ +
+
+ +
+
+Add Task + + From 89683966737e3781caef5a75eb5b74b1d96fc8e2 Mon Sep 17 00:00:00 2001 From: Konstantin Moskalenko Date: Thu, 30 Nov 2023 11:55:02 +0700 Subject: [PATCH 04/10] Add some styling with Bootstrap --- projects/task_manager/static/list_style.css | 14 +-- .../task_manager/templates/task_form.html | 69 +++++++---- .../task_manager/templates/task_list.html | 113 +++++++++++++----- 3 files changed, 129 insertions(+), 67 deletions(-) diff --git a/projects/task_manager/static/list_style.css b/projects/task_manager/static/list_style.css index fbd5fdd..db8f1cb 100644 --- a/projects/task_manager/static/list_style.css +++ b/projects/task_manager/static/list_style.css @@ -1,14 +1,8 @@ table { - width: 100%; - border-collapse: collapse; + table-layout: fixed; } -th, td { - border: 1px solid #dddddd; - padding: 8px; - text-align: left; +td { + white-space: normal !important; + word-wrap: break-word; } - -th { - background-color: #f2f2f2; -} \ No newline at end of file diff --git a/projects/task_manager/templates/task_form.html b/projects/task_manager/templates/task_form.html index cbd842f..87603ad 100644 --- a/projects/task_manager/templates/task_form.html +++ b/projects/task_manager/templates/task_form.html @@ -2,36 +2,57 @@ + Task Form + -{% if task %} -

Update Task

-
-{% else %} -

Add Task

- -{% endif %} -
-

+
+ {% if task %} +

Update Task

+ + {% else %} +

Add Task

+ + {% endif %} -
-

+
+ + +
-
-

+
+ + +
-
-

+
+ + +
- - -
-Back to Task List +
+ + +
+ + + +
+ + diff --git a/projects/task_manager/templates/task_list.html b/projects/task_manager/templates/task_list.html index 7968cb0..a2200e4 100644 --- a/projects/task_manager/templates/task_list.html +++ b/projects/task_manager/templates/task_list.html @@ -2,44 +2,91 @@ + Task List - + -

Task List

- - - - - - - - - - - - - {% for task in tasks %} +
+

Task List

+ +
+ +
+ +
+ +
+
+ + + + + +
+
+ + +
IDTitleDescriptionDeadlinePriorityActions
+ - - - - - - + + + + + + - {% endfor %} - -
{{ task[0] }}{{ task[1] }}{{ task[2] }}{{ task[3] }}{{ task[4] }} -
- -
-
- -
-
IDTitleDescriptionDeadlinePriorityActions
-Add Task + + + {% for task in tasks %} + + {{ task[0] }} + {{ task[1] }} + {{ task[2] }} + {{ task[3] }} + {{ task[4] }} + +
+
+ +
+
+ +
+
+ + + {% endfor %} + + + Add Task + + + From 4b33f85303357a8bcd3bbeb47f8b7f1a5b48f98a Mon Sep 17 00:00:00 2001 From: Konstantin Moskalenko Date: Thu, 30 Nov 2023 11:03:56 +0700 Subject: [PATCH 05/10] Implement table sorting --- projects/task_manager/db.py | 9 +++++++-- projects/task_manager/routes.py | 5 ++++- projects/task_manager/templates/task_list.html | 12 ++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/projects/task_manager/db.py b/projects/task_manager/db.py index b07b688..e43bb19 100644 --- a/projects/task_manager/db.py +++ b/projects/task_manager/db.py @@ -41,9 +41,14 @@ def add_task(self, title, description, deadline, priority): ''', (title, description, deadline, priority.name)) self.conn.commit() - def get_tasks(self): + def get_tasks(self, sort_by, order): + if sort_by not in ('id', 'title', 'deadline', 'priority'): + sort_by = 'id' + if order not in ('ASC', 'DESC'): + order = 'ASC' + cursor = self.conn.cursor() - cursor.execute('SELECT * FROM tasks') + cursor.execute(f'SELECT * FROM tasks ORDER BY {sort_by} {order}') return cursor.fetchall() def get_task_by_id(self, task_id): diff --git a/projects/task_manager/routes.py b/projects/task_manager/routes.py index 71bc267..9c064c2 100644 --- a/projects/task_manager/routes.py +++ b/projects/task_manager/routes.py @@ -7,8 +7,11 @@ @tasks_bp.route('/') @tasks_bp.route('/tasks') def show_tasks(): + sort_by = request.args.get('sort_by', 'id') + order = request.args.get('order', 'ASC') + task_manager = TaskManager.get_instance() - tasks = task_manager.get_tasks() + tasks = task_manager.get_tasks(sort_by, order) return render_template('task_list.html', tasks=tasks) diff --git a/projects/task_manager/templates/task_list.html b/projects/task_manager/templates/task_list.html index a2200e4..7ede003 100644 --- a/projects/task_manager/templates/task_list.html +++ b/projects/task_manager/templates/task_list.html @@ -85,6 +85,18 @@

Task List

Add Task + From d5de009f520454d4adcd10a2fc49354a453f6e69 Mon Sep 17 00:00:00 2001 From: Konstantin Moskalenko Date: Thu, 30 Nov 2023 11:32:54 +0700 Subject: [PATCH 06/10] Eliminate SQL injections in update_task request --- projects/task_manager/db.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/projects/task_manager/db.py b/projects/task_manager/db.py index e43bb19..98ccb74 100644 --- a/projects/task_manager/db.py +++ b/projects/task_manager/db.py @@ -59,19 +59,27 @@ def get_task_by_id(self, task_id): def update_task(self, task_id, title=None, description=None, deadline=None, priority=None): update_query = 'UPDATE tasks SET ' updates = [] + values = [] + if title: - updates.append(f'title = "{title}"') + updates.append('title = ?') + values.append(title) if description: - updates.append(f'description = "{description}"') + updates.append('description = ?') + values.append(description) if deadline: - updates.append(f'deadline = "{deadline}"') + updates.append('deadline = ?') + values.append(deadline) if priority: TaskManager.__validate_priority(priority) - updates.append(f'priority = "{priority.name}"') + updates.append('priority = ?') + values.append(priority.name) + + update_query += ', '.join(updates) + ' WHERE id = ?' + values.append(task_id) - update_query += ', '.join(updates) + f' WHERE id = {task_id}' cursor = self.conn.cursor() - cursor.execute(update_query) + cursor.execute(update_query, values) self.conn.commit() def delete_task(self, task_id): From 3ba9f74c7c65c4c8d4f802b565cbf49366f6e060 Mon Sep 17 00:00:00 2001 From: Konstantin Moskalenko Date: Thu, 30 Nov 2023 18:06:28 +0700 Subject: [PATCH 07/10] Add support for projects and implement filtering --- projects/task_manager/db.py | 34 ++++++-- projects/task_manager/routes.py | 13 +++- .../task_manager/templates/task_form.html | 6 ++ .../task_manager/templates/task_list.html | 78 +++++++++++-------- 4 files changed, 88 insertions(+), 43 deletions(-) diff --git a/projects/task_manager/db.py b/projects/task_manager/db.py index 98ccb74..0694ac6 100644 --- a/projects/task_manager/db.py +++ b/projects/task_manager/db.py @@ -26,29 +26,39 @@ def create_table(self): title TEXT NOT NULL, description TEXT, deadline DATE, - priority TEXT CHECK(priority IN ('LOW', 'MEDIUM', 'HIGH')) + priority TEXT CHECK(priority IN ('LOW', 'MEDIUM', 'HIGH')), + project TEXT ) ''') self.conn.commit() - def add_task(self, title, description, deadline, priority): + def add_task(self, title, description, deadline, priority, project): TaskManager.__validate_priority(priority) cursor = self.conn.cursor() cursor.execute(''' - INSERT INTO tasks (title, description, deadline, priority) - VALUES (?, ?, ?, ?) - ''', (title, description, deadline, priority.name)) + INSERT INTO tasks (title, description, deadline, priority, project) + VALUES (?, ?, ?, ?, ?) + ''', (title, description, deadline, priority.name, project)) self.conn.commit() - def get_tasks(self, sort_by, order): + def get_tasks(self, sort_by, order, project): if sort_by not in ('id', 'title', 'deadline', 'priority'): sort_by = 'id' if order not in ('ASC', 'DESC'): order = 'ASC' + query = ['SELECT * FROM tasks'] + values = [] + + if project: + query.append('WHERE project = ?') + values.append(project) + + query.append(f'ORDER BY {sort_by} {order}') + cursor = self.conn.cursor() - cursor.execute(f'SELECT * FROM tasks ORDER BY {sort_by} {order}') + cursor.execute(' '.join(query), values) return cursor.fetchall() def get_task_by_id(self, task_id): @@ -56,7 +66,7 @@ def get_task_by_id(self, task_id): cursor.execute('SELECT * FROM tasks WHERE id = ?', (task_id,)) return cursor.fetchone() - def update_task(self, task_id, title=None, description=None, deadline=None, priority=None): + def update_task(self, task_id, title=None, description=None, deadline=None, priority=None, project=None): update_query = 'UPDATE tasks SET ' updates = [] values = [] @@ -74,6 +84,9 @@ def update_task(self, task_id, title=None, description=None, deadline=None, prio TaskManager.__validate_priority(priority) updates.append('priority = ?') values.append(priority.name) + if project: + updates.append('project = ?') + values.append(project) update_query += ', '.join(updates) + ' WHERE id = ?' values.append(task_id) @@ -87,6 +100,11 @@ def delete_task(self, task_id): cursor.execute('DELETE FROM tasks WHERE id = ?', (task_id,)) self.conn.commit() + def get_projects(self): + cursor = self.conn.cursor() + cursor.execute('SELECT DISTINCT project FROM tasks ORDER BY project ASC') + return [project[0] for project in cursor.fetchall()] + @staticmethod def get_instance(): if 'task_manager' not in g: diff --git a/projects/task_manager/routes.py b/projects/task_manager/routes.py index 9c064c2..9cffb7e 100644 --- a/projects/task_manager/routes.py +++ b/projects/task_manager/routes.py @@ -9,10 +9,13 @@ def show_tasks(): sort_by = request.args.get('sort_by', 'id') order = request.args.get('order', 'ASC') + project = request.args.get('project') task_manager = TaskManager.get_instance() - tasks = task_manager.get_tasks(sort_by, order) - return render_template('task_list.html', tasks=tasks) + tasks = task_manager.get_tasks(sort_by, order, project) + projects = task_manager.get_projects() + + return render_template('task_list.html', tasks=tasks, projects=projects) @tasks_bp.route('/add_task', methods=['GET', 'POST']) @@ -22,9 +25,10 @@ def add_task(): description = request.form['description'] deadline = request.form['deadline'] priority = TaskPriority[request.form['priority']] + project = request.form['project'] task_manager = TaskManager.get_instance() - task_manager.add_task(title, description, deadline, priority) + task_manager.add_task(title, description, deadline, priority, project) return redirect(url_for('.show_tasks')) return render_template('task_form.html') @@ -41,8 +45,9 @@ def update_task(task_id): description = request.form['description'] deadline = request.form['deadline'] priority = TaskPriority[request.form['priority']] + project = request.form['project'] - task_manager.update_task(task_id, title, description, deadline, priority) + task_manager.update_task(task_id, title, description, deadline, priority, project) return redirect(url_for('.show_tasks')) return render_template('task_form.html', task=task) diff --git a/projects/task_manager/templates/task_form.html b/projects/task_manager/templates/task_form.html index 87603ad..c6a1982 100644 --- a/projects/task_manager/templates/task_form.html +++ b/projects/task_manager/templates/task_form.html @@ -44,6 +44,12 @@

Add Task

+
+ + +
+
Back to Task List diff --git a/projects/task_manager/templates/task_list.html b/projects/task_manager/templates/task_list.html index 7ede003..91c3413 100644 --- a/projects/task_manager/templates/task_list.html +++ b/projects/task_manager/templates/task_list.html @@ -12,38 +12,54 @@

Task List

-
- -
- -
+ +
+ +
+ +
+ +
+
+ + -
-
- - + + +
+
+
- - +
+ +
+
@@ -66,7 +82,7 @@

Task List

{{ task[1] }} {{ task[2] }} {{ task[3] }} - {{ task[4] }} + {{ task[4].capitalize() }}
From 638b9017e0e88d1d8a3be87e2ad1be29e6674acf Mon Sep 17 00:00:00 2001 From: Konstantin Moskalenko Date: Thu, 30 Nov 2023 18:34:58 +0700 Subject: [PATCH 08/10] Implement error handling --- projects/task_manager/app.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/projects/task_manager/app.py b/projects/task_manager/app.py index 85f4c73..0f9be8f 100644 --- a/projects/task_manager/app.py +++ b/projects/task_manager/app.py @@ -1,4 +1,6 @@ import os +import sys +import sqlite3 from flask import Flask from routes import tasks_bp @@ -8,7 +10,16 @@ DATABASE=os.path.join(app.instance_path, 'tasks.sqlite') ) -os.makedirs(app.instance_path, exist_ok=True) + +@app.errorhandler(sqlite3.Error) +def handle_error(err): + return f'Database operation failed: {err}', 500 + + +try: + os.makedirs(app.instance_path, exist_ok=True) +except OSError as e: + sys.exit(f'Failed to create the instance directory at {e.filename}') if __name__ == '__main__': app.run() From b139abfe3a52494344a06a8ff4f11a71b47a4846 Mon Sep 17 00:00:00 2001 From: Konstantin Moskalenko Date: Thu, 30 Nov 2023 18:58:52 +0700 Subject: [PATCH 09/10] Update README.md --- projects/task_manager/.gitignore | 2 ++ projects/task_manager/README.MD | 22 ++++++++++++++++++++++ projects/task_manager/requirements.txt | 1 + 3 files changed, 25 insertions(+) create mode 100644 projects/task_manager/requirements.txt diff --git a/projects/task_manager/.gitignore b/projects/task_manager/.gitignore index 135ebce..85d6e9e 100644 --- a/projects/task_manager/.gitignore +++ b/projects/task_manager/.gitignore @@ -1,4 +1,6 @@ .idea/ +.venv/ +__pycache__/ .DS_Store instance/ \ No newline at end of file diff --git a/projects/task_manager/README.MD b/projects/task_manager/README.MD index 2b3ca97..e36ff0d 100644 --- a/projects/task_manager/README.MD +++ b/projects/task_manager/README.MD @@ -1 +1,23 @@ # Personal Task Manager + +В данном проекте реализован проект менеджера задач согласно [описанию](./task_description.md). В качестве базы данных +используется встроенный в стандартную библиотеку модуль `sqlite3`, а для пользовательского интерфейса реализовано +веб-приложение на [Flask](https://github.com/pallets/flask). + +## Запуск + +Для первого запуска приложения рекомендуется создать виртуальное окружение и установить зависимости: + +```shell +python3 -m venv .venv +source .venv/bin/activate +python3 -m pip install -r requirements.txt +``` + +Далее для запуска веб-приложение необходимо вызвать: + +```shell +python3 -m flask run +``` + +Актуальный адрес для подключения к приложению будет выведен в стандартный вывод. По умолчанию это http://127.0.0.1:5000. \ No newline at end of file diff --git a/projects/task_manager/requirements.txt b/projects/task_manager/requirements.txt new file mode 100644 index 0000000..047e950 --- /dev/null +++ b/projects/task_manager/requirements.txt @@ -0,0 +1 @@ +Flask==3.0.0 From a384ccaf18764944be2599db9c66fe75dad78645 Mon Sep 17 00:00:00 2001 From: Konstantin Moskalenko Date: Fri, 1 Dec 2023 00:44:31 +0700 Subject: [PATCH 10/10] Implement importing and exporting tasks --- projects/task_manager/managers/__init__.py | 0 projects/task_manager/managers/csv_manager.py | 36 ++++++++++++ .../{db.py => managers/task_manager.py} | 2 +- projects/task_manager/routes.py | 55 ++++++++++++++++++- .../task_manager/templates/task_list.html | 31 ++++++++++- 5 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 projects/task_manager/managers/__init__.py create mode 100644 projects/task_manager/managers/csv_manager.py rename projects/task_manager/{db.py => managers/task_manager.py} (97%) diff --git a/projects/task_manager/managers/__init__.py b/projects/task_manager/managers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projects/task_manager/managers/csv_manager.py b/projects/task_manager/managers/csv_manager.py new file mode 100644 index 0000000..f2db131 --- /dev/null +++ b/projects/task_manager/managers/csv_manager.py @@ -0,0 +1,36 @@ +import csv +from .task_manager import TaskManager, TaskPriority + + +class CSVManager: + def __init__(self): + self.task_manager = TaskManager.get_instance() + + def import_tasks(self, file_path): + with open(file_path, 'r') as file: + reader = csv.DictReader(file) + for row in reader: + title = row['Title'] + description = row['Description'] + deadline = row['Deadline'] + priority = TaskPriority[row['Priority']] + project = row['Project'] + + self.task_manager.add_task(title, description, deadline, priority, project) + + def export_tasks(self, file_path): + tasks = self.task_manager.get_tasks(sort_by=None, order=None, project=None) + + with open(file_path, 'w') as file: + fieldnames = ['Title', 'Description', 'Deadline', 'Priority', 'Project'] + writer = csv.DictWriter(file, fieldnames=fieldnames) + writer.writeheader() + + for task in tasks: + writer.writerow({ + 'Title': task[1], + 'Description': task[2], + 'Deadline': task[3], + 'Priority': task[4], + 'Project': task[5] + }) diff --git a/projects/task_manager/db.py b/projects/task_manager/managers/task_manager.py similarity index 97% rename from projects/task_manager/db.py rename to projects/task_manager/managers/task_manager.py index 0694ac6..928ffef 100644 --- a/projects/task_manager/db.py +++ b/projects/task_manager/managers/task_manager.py @@ -43,7 +43,7 @@ def add_task(self, title, description, deadline, priority, project): self.conn.commit() def get_tasks(self, sort_by, order, project): - if sort_by not in ('id', 'title', 'deadline', 'priority'): + if sort_by not in ('id', 'title', 'description', 'deadline', 'priority'): sort_by = 'id' if order not in ('ASC', 'DESC'): order = 'ASC' diff --git a/projects/task_manager/routes.py b/projects/task_manager/routes.py index 9cffb7e..2b5a335 100644 --- a/projects/task_manager/routes.py +++ b/projects/task_manager/routes.py @@ -1,5 +1,10 @@ -from db import TaskManager, TaskPriority -from flask import Blueprint, render_template, request, redirect, url_for +import os +import sys +from tempfile import gettempdir + +from managers.csv_manager import CSVManager +from managers.task_manager import TaskManager, TaskPriority +from flask import Blueprint, render_template, request, redirect, url_for, send_file, after_this_request tasks_bp = Blueprint('tasks', __name__, template_folder='templates') @@ -58,3 +63,49 @@ def delete_task(task_id): task_manager = TaskManager.get_instance() task_manager.delete_task(task_id) return redirect(url_for('.show_tasks')) + + +@tasks_bp.route('/upload_csv', methods=['POST']) +def upload_csv(): + if 'csv_file' not in request.files: + return 'No file part', 400 + + csv_file = request.files['csv_file'] + + if csv_file.filename == '': + return 'No file selected', 400 + + if csv_file and csv_file.filename.endswith('.csv'): + try: + file_path = os.path.join(gettempdir(), csv_file.filename) + csv_file.save(file_path) + + csv_manager = CSVManager() + csv_manager.import_tasks(file_path) + os.remove(file_path) + except Exception: + return 'Failed to import the CSV file, try uploading another one', 400 + + return redirect(url_for('.show_tasks')) + + return 'Invalid file format, please upload a CSV file', 400 + + +@tasks_bp.route('/download_csv') +def download_csv(): + try: + csv_manager = CSVManager() + file_path = os.path.join(gettempdir(), 'tasks_export.csv') + csv_manager.export_tasks(file_path) + except Exception: + return 'Failed to export tasks to a CSV file', 400 + + @after_this_request + def remove_file(response): + try: + os.remove(file_path) + except OSError as err: + print(f'Failed to remove the exported CSV file: {err}', file=sys.stderr) + return response + + return send_file(file_path, as_attachment=True, mimetype='text/csv') diff --git a/projects/task_manager/templates/task_list.html b/projects/task_manager/templates/task_list.html index 91c3413..ae827cf 100644 --- a/projects/task_manager/templates/task_list.html +++ b/projects/task_manager/templates/task_list.html @@ -23,6 +23,9 @@

Task List

+ @@ -98,7 +101,27 @@

Task List

{% endfor %} - Add Task + +
+
+ Add Task +
+ +
+ + +
+ +
+ +
+
+ +
+
+