Compare commits
2 Commits
master
...
feature/ma
Author | SHA1 | Date | |
---|---|---|---|
78931145ac | |||
8a67085d5b |
@ -1,6 +1,5 @@
|
||||
# PyKanban
|
||||
|
||||
**Note this project has been abandoned. I ended up not liking Textual as a TUI library. I plan on creating another project to accomplish a similar goal using rust.**
|
||||
A python implementation of a simple kanban board for managing personal projects.
|
||||
|
||||
## License
|
||||
|
@ -15,13 +15,11 @@ requires-python = ">=3.8"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
|
||||
"Operating System :: OS Independent",
|
||||
"Operating System :: Unix",
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Environment :: Console"
|
||||
]
|
||||
dependencies = [
|
||||
"textual"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://alexselimov.com/git/aselimov/pykanban"
|
||||
Issues = "https://github.com/aselimov/PyKanban/issues"
|
||||
|
@ -1,19 +0,0 @@
|
||||
columns:
|
||||
- To Do
|
||||
- In Progress
|
||||
- Review
|
||||
- Done
|
||||
tasks:
|
||||
- column: To Do
|
||||
description: "We want to be able to retire some done tickets \nwithout having to\
|
||||
\ delete them."
|
||||
score: '5'
|
||||
summary: Work out some way to handle sprints
|
||||
- column: To Do
|
||||
description: 'I want to add footers which describe the key shortcuts,
|
||||
|
||||
Additionally this should be disabledable via a command line argument
|
||||
|
||||
'
|
||||
score: '5'
|
||||
summary: 'Add some footers for the key shortcuts '
|
@ -1,28 +1,29 @@
|
||||
""" This module contains classes and functions to contain the kanban board information """
|
||||
|
||||
import sys
|
||||
import numpy as np
|
||||
import yaml
|
||||
import os
|
||||
import glob
|
||||
|
||||
|
||||
class Task:
|
||||
""" This class represents each task,
|
||||
"""
|
||||
"""This class represents each task,"""
|
||||
|
||||
def __init__(self, summary, score, description):
|
||||
""" Initialize the task class
|
||||
"""
|
||||
"""Initialize the task class"""
|
||||
# Each task has the following properties
|
||||
self.summary = summary # Summary of the task
|
||||
self.score = score # Score for ticket
|
||||
self.description = description # Description of ticket
|
||||
self.summary = summary # Summary of the task
|
||||
self.score = score # Score for ticket
|
||||
self.description = description # Description of ticket
|
||||
|
||||
|
||||
class Board:
|
||||
def __init__(self, file = None):
|
||||
""" Initialize the Board class, this class has three important class variables.
|
||||
These are:
|
||||
self.sprint | str - name of the current sprint
|
||||
def __init__(self, file=None):
|
||||
"""Initialize the Board class, this class has three important class variables.
|
||||
These are: self.sprint | str - name of the current sprint
|
||||
self.columns | list(str) - columns in kanban board
|
||||
self.tasks | list(list()) - tasks in each column
|
||||
|
||||
"""
|
||||
self.sprint = None
|
||||
self.columns = list()
|
||||
@ -33,27 +34,27 @@ class Board:
|
||||
if file:
|
||||
self.read_yaml(file)
|
||||
|
||||
|
||||
def read_yaml(self, file):
|
||||
""" Read the yaml file in and set up the data
|
||||
"""Read the yaml file in and set up the data
|
||||
|
||||
Arguments:
|
||||
file - yaml file to read in
|
||||
"""
|
||||
|
||||
# Read in the data
|
||||
with open(file, 'r') as f:
|
||||
with open(file, "r") as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
# Assign the data to board variables
|
||||
self.columns = data['columns']
|
||||
self.columns = data["columns"]
|
||||
self.tasks = [[] for col in self.columns]
|
||||
for task in data['tasks']:
|
||||
self.tasks[self.columns.index(task['column'])].append(
|
||||
Task(task['summary'], task['score'], task['description']))
|
||||
for task in data["tasks"]:
|
||||
self.tasks[self.columns.index(task["column"])].append(
|
||||
Task(task["summary"], task["score"], task["description"])
|
||||
)
|
||||
|
||||
def write_yaml(self, file):
|
||||
""" Write the yaml file
|
||||
"""Write the yaml file
|
||||
|
||||
Arguments:
|
||||
file - yaml file to write to
|
||||
@ -61,19 +62,24 @@ class Board:
|
||||
|
||||
# Set up data to write out
|
||||
data = dict()
|
||||
data['columns'] = self.columns
|
||||
data['tasks'] = list()
|
||||
for col,task_list in zip(self.columns, self.tasks):
|
||||
data["columns"] = self.columns
|
||||
data["tasks"] = list()
|
||||
for col, task_list in zip(self.columns, self.tasks):
|
||||
for task in task_list:
|
||||
data['tasks'].append({'column':col, 'summary':task.summary, 'score':task.score,
|
||||
'description':task.description})
|
||||
data["tasks"].append(
|
||||
{
|
||||
"column": col,
|
||||
"summary": task.summary,
|
||||
"score": task.score,
|
||||
"description": task.description,
|
||||
}
|
||||
)
|
||||
|
||||
with open(file, 'w') as f:
|
||||
with open(file, "w") as f:
|
||||
yaml.dump(data, f)
|
||||
|
||||
|
||||
def move_task(self, col_index, task_index, direction):
|
||||
""" This class method moves tasks between columns by incrementing/decrementing the column
|
||||
"""This class method moves tasks between columns by incrementing/decrementing the column
|
||||
index
|
||||
|
||||
Arguments:
|
||||
@ -85,41 +91,66 @@ class Board:
|
||||
moved - True if a task was moved else false
|
||||
"""
|
||||
task = self.tasks[col_index][task_index]
|
||||
if col_index+direction >= 0 and col_index+direction < len(self.columns):
|
||||
self.tasks[col_index+direction].append(task)
|
||||
if col_index + direction >= 0 and col_index + direction < len(self.columns):
|
||||
self.tasks[col_index + direction].append(task)
|
||||
del self.tasks[col_index][task_index]
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
def print_board_items(self):
|
||||
for i, col in enumerate(self.columns):
|
||||
print(col)
|
||||
print(self.tasks[i])
|
||||
|
||||
def get_columns(self):
|
||||
""" Return columns"""
|
||||
"""Return columns"""
|
||||
return self.columns
|
||||
|
||||
def get_tasks(self):
|
||||
""" Return tasks"""
|
||||
"""Return tasks"""
|
||||
return self.tasks
|
||||
|
||||
def get_task(self, icol, itask):
|
||||
""" Return a task based on column and task index"""
|
||||
"""Return a task based on column and task index"""
|
||||
return self.tasks[icol][itask]
|
||||
|
||||
def update_task( self, icol, itask, task):
|
||||
""" Update the task based on text """
|
||||
def update_task(self, icol, itask, task):
|
||||
"""Update the task based on text"""
|
||||
self.tasks[icol][itask] = task
|
||||
|
||||
def add_task( self, icol, task):
|
||||
def add_task(self, icol, task):
|
||||
"""Add a task to icol"""
|
||||
self.tasks[icol].append(task)
|
||||
|
||||
def del_task(self, icol, itask):
|
||||
del self.tasks[icol][itask]
|
||||
|
||||
|
||||
class BoardList:
|
||||
"""This class is used to process the full list of boards"""
|
||||
|
||||
def __init__(self):
|
||||
self.boards = self.get_boards()
|
||||
|
||||
def get_boards(self):
|
||||
"""This function returns the boards that have been created"""
|
||||
configpath = os.path.join(
|
||||
os.environ.get("APPDATA")
|
||||
or os.environ.get("XDG_CONFIG_HOME")
|
||||
or os.path.join(os.environ["HOME"], ".config"),
|
||||
"pykban",
|
||||
)
|
||||
|
||||
boards = list()
|
||||
for board in glob.glob(os.path.join(configpath, "(*.yaml)")):
|
||||
with open(board, "r") as f:
|
||||
data = yaml.safe_load(f)
|
||||
try:
|
||||
boards.append((data["name"], board))
|
||||
except KeyError:
|
||||
print("Board yaml file is missing the name attribute")
|
||||
sys.exit()
|
||||
|
||||
return boards
|
||||
|
@ -19,6 +19,14 @@ EditColScreen {
|
||||
layout: vertical;
|
||||
background: #000000 25%;
|
||||
}
|
||||
|
||||
SelectBoardScreen{
|
||||
align: center middle;
|
||||
overflow-x: hidden;
|
||||
layout: vertical;
|
||||
background: #000000 25%;
|
||||
}
|
||||
|
||||
.column {
|
||||
width: 1fr;
|
||||
height: 100%;
|
||||
|
@ -3,12 +3,7 @@ from textual.widgets import Static, Label, ListItem, ListView, TextArea, Input
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.screen import Screen
|
||||
from textual.binding import Binding
|
||||
from .board import Board, Task
|
||||
|
||||
|
||||
def run_tui():
|
||||
kb = KanbanForm()
|
||||
kb.run()
|
||||
from .board import Board, Task, BoardList
|
||||
|
||||
|
||||
class TaskList(ListView):
|
||||
@ -137,7 +132,72 @@ class EditColScreen(Screen):
|
||||
self.dismiss(query.nodes[0].value)
|
||||
|
||||
|
||||
class KanbanForm(App):
|
||||
class SelectBoardScreen(Screen):
|
||||
"""This is a screen used to select a board"""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("enter", "pick_option", "Save Changes", priority=True),
|
||||
Binding("q", "exit", "Exit"),
|
||||
]
|
||||
CSS = """
|
||||
$bg: #282828;
|
||||
Label{
|
||||
width:50%;
|
||||
background: #282828;
|
||||
padding: 0;
|
||||
}
|
||||
TaskList{
|
||||
width:50%;
|
||||
background: #282828;
|
||||
padding: 0 0;
|
||||
border: #ebdbb9;
|
||||
}
|
||||
ListView{
|
||||
width:50%;
|
||||
background: #282828;
|
||||
}
|
||||
ListItem{
|
||||
border: solid #ebdbb2 100%;
|
||||
background: $bg;
|
||||
}
|
||||
|
||||
ListView > ListItem.--highlight {
|
||||
background: $bg;
|
||||
}
|
||||
|
||||
ListView:focus > ListItem.--highlight {
|
||||
background: #458588;
|
||||
}
|
||||
|
||||
Label:focus{
|
||||
background: #458588;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, logger):
|
||||
"""
|
||||
Initialize the screen
|
||||
"""
|
||||
super().__init__()
|
||||
self.board_list = BoardList()
|
||||
self.logger = logger
|
||||
|
||||
def compose(self):
|
||||
"""
|
||||
Compose the widgets on the screen, this screen doesn't need dynamic layout changes
|
||||
"""
|
||||
yield Label("Select a board:")
|
||||
yield TaskList(
|
||||
*[ListItem(Label(board)) for board in self.board_list.get_boards()],
|
||||
ListItem(Label("Add a new board")),
|
||||
)
|
||||
|
||||
def action_pick_option(self):
|
||||
"""Pick a board from the ListItem"""
|
||||
self.focused.highlighted_child
|
||||
|
||||
|
||||
class MainBoardScreen(Screen):
|
||||
CSS_PATH = "layout.tcss"
|
||||
BINDINGS = [
|
||||
Binding(
|
||||
@ -173,7 +233,7 @@ class KanbanForm(App):
|
||||
show=False,
|
||||
),
|
||||
Binding(
|
||||
"x",
|
||||
"d",
|
||||
"delete_task",
|
||||
"Delete Task",
|
||||
show=False,
|
||||
@ -181,6 +241,10 @@ class KanbanForm(App):
|
||||
Binding("q", "exit", "Exit"),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Kanban Form App"""
|
||||
super().__init__()
|
||||
|
||||
def compose(self):
|
||||
"""
|
||||
Initialization function for form
|
||||
@ -203,7 +267,10 @@ class KanbanForm(App):
|
||||
else:
|
||||
yield Static(col, classes="header")
|
||||
yield TaskList(
|
||||
*[ListItem(Label(task.summary)) for task in self.board.get_tasks()[i]]
|
||||
*[
|
||||
ListItem(Label(task.summary))
|
||||
for task in self.board.get_tasks()[i]
|
||||
]
|
||||
)
|
||||
|
||||
def action_fnext(self):
|
||||
@ -324,3 +391,27 @@ class KanbanForm(App):
|
||||
self.focused.mount(ListItem(Label(task.summary)))
|
||||
self.board.add_task(icol, task)
|
||||
self.focused.action_cursor_down()
|
||||
|
||||
|
||||
class KanbanForm(App):
|
||||
"""Main Kanban app"""
|
||||
|
||||
CSS_PATH = "layout.tcss"
|
||||
SCREENS = {"main": SelectBoardScreen()}
|
||||
|
||||
def on_mount(self):
|
||||
self.push_screen("main")
|
||||
|
||||
|
||||
# def on_key(self):
|
||||
# with open('log','a') as f:
|
||||
# f.write("{}".format(self.children[0].focus_next))
|
||||
|
||||
|
||||
def run_tui():
|
||||
kb = KanbanForm()
|
||||
kb.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_tui()
|
||||
|
Reference in New Issue
Block a user