diff --git a/.gitignore b/.gitignore index 6edd26b..3f804b3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ CodeGarden.egg-info/ *.env *.pyc .DS_Store -todos.txt .coverage htmlcov/ *.db diff --git a/code_garden/models.py b/code_garden/models.py index 470c70a..7774f3a 100644 --- a/code_garden/models.py +++ b/code_garden/models.py @@ -7,6 +7,7 @@ import requests from code_garden.readme import Readme +from code_garden.todos import Todo from . import config @@ -21,7 +22,7 @@ class Repository(object): branches: All local branches of the Repository. current_branch: The currently checked-out branch. log: List of (5 default) commits, sorted by most recent. - todos: List of tasks found in the todos.txt file. + todos: List of tasks found in the database file. diffs: List of all changed file in the current Repository. readme: Dict object of text in the README.md file 'txt' is the plaintext content, 'md' is the Markdown-formatted text. ignored: List of items in the .gitignore file. @@ -60,41 +61,36 @@ def current_branch(self): @property def log(self): _ = [] - for i in self.run_command( - ["git", "log", "--oneline", "-5", "--pretty=format:%s\t%at\t%h"] - ).split("\n"): - if len(i.strip().split("\t")) == 2: - _.append( - LogItem( - self.name, - "[No Commit Message]", - datetime.datetime.min, - i.strip().split("\t")[0], + try: + for i in self.run_command( + ["git", "log", "--oneline", "-5", "--pretty=format:%s\t%at\t%h"] + ).split("\n"): + if len(i.strip().split("\t")) == 2: + _.append( + LogItem( + self.name, + "[No Commit Message]", + datetime.datetime.min, + i.strip().split("\t")[0], + ) ) - ) - else: - _.append( - LogItem( - self.name, - i.strip().split("\t")[0], - datetime.datetime.fromtimestamp(int(i.split("\t")[1])), - i.strip().split("\t")[2], + else: + _.append( + LogItem( + self.name, + i.strip().split("\t")[0], + datetime.datetime.fromtimestamp(int(i.split("\t")[1])), + i.strip().split("\t")[2], + ) ) - ) - return _ + return _ + except: + return [] @property def todos(self): - return ( - [ - Todo(self.name, i.strip()) - for i in open(self.path / "todos.txt").readlines() - if i.strip() - ] - if (self.path / "todos.txt").exists() - else [] - ) + return Todo.see_list(self.name) @property def diffs(self): @@ -106,16 +102,22 @@ def diffs(self): @property def readme(self): - raw = open(self.path / "README.md").read() - return dict(txt=raw, md=markdown.markdown(raw)) + try: + raw = open(self.path / "README.md").read() + return dict(txt=raw, md=markdown.markdown(raw)) + except: + return {} @property def ignored(self): - return [ - IgnoreItem(self.name, i.strip()) - for i in open(self.path / ".gitignore").readlines() - if i.strip() - ] + try: + return [ + IgnoreItem(self.name, i.strip()) + for i in open(self.path / ".gitignore").readlines() + if i.strip() + ] + except: + return [] @property def remote_url(self): @@ -150,13 +152,11 @@ def init(self, brief_descrip: str): Args: brief_descrip (str): Short description of what the Repository contains. """ - files = ["LICENSE.md", ".gitignore", "todos.txt"] + files = ["LICENSE.md", ".gitignore"] self.path.mkdir() Readme(self.name, brief_descrip).write(self.path) for i in files: (self.path / i).touch() - if i == ".gitignore": - open(self.path / i, "w").write("todos.txt\n") self.run_command(["git", "init"]) self.commit("Initial commit") @@ -288,79 +288,6 @@ def to_dict(self): ) -class Todo(object): - """Todo item found in todos.txt. - - Attributes: - repository (str): name of the containing Repository - name (str): description of this Todo item. - """ - - def __init__(self, repository, name): - self.repository = repository - self.name = name.replace("[x] ", "") - self.done = name.startswith("[x] ") - - def create(self): - """Create a new Todo.""" - todos_ = Repository(self.repository).todos - todos_.append(self) - - with open((Repository(self.repository).path / "todos.txt"), "w") as f: - for i in todos_: - f.write(f"{'[x] ' if i.done else ''} {i.name}\n") - - @classmethod - def edit(cls, repository, id, new_name): - """Edit a Todo item. - - Args: - repository (str): name of the Repository that contains this Todo. - id (int): index, or location, of the Todo in the list. - new_name (str): new description of the Todo item. - """ - todos_ = Repository(repository).todos - todos_[id].name = new_name - - with open((Repository(repository).path / "todos.txt"), "w") as f: - for i in todos_: - f.write(f"{'[x] ' if i.done else ''} {i.name}\n") - - @classmethod - def delete(cls, repository, id): - """Delete a Todo item. - - Args: - repository (str): name of the Repository that contains this Todo. - id (int): index, or location, of the Todo in the list. - """ - todos_ = Repository(repository).todos - del todos_[id] - - with open((Repository(repository).path / "todos.txt"), "w") as f: - for i in todos_: - f.write(f"{'[x] ' if i.done else ''} {i.name}\n") - - @classmethod - def toggle(cls, repository, id): - """Delete a Todo item. - - Args: - repository (str): name of the Repository that contains this Todo. - id (int): index, or location, of the Todo in the list. - """ - todos_ = Repository(repository).todos - todos_[id].done = not todos_[id].done - - with open((Repository(repository).path / "todos.txt"), "w") as f: - for i in todos_: - f.write(f"{'[x] ' if i.done else ''} {i.name}\n") - - def to_dict(self): - """Get a dict representation of this object (for API use).""" - return dict(repository=self.repository, name=self.name, done=self.done) - - class DiffItem(object): """Changed item in the Repository. diff --git a/code_garden/repos.py b/code_garden/repos.py index 1f0deed..623acd8 100644 --- a/code_garden/repos.py +++ b/code_garden/repos.py @@ -1,12 +1,12 @@ import datetime +import subprocess from pathlib import Path from pprint import pformat -import subprocess import click from code_garden.models import Repository -from code_garden.todos import Task +from code_garden.todos import Todo @click.group() @@ -17,6 +17,7 @@ def repo_cli(): @repo_cli.command() @click.option("--name") def add_repo(name): + """Create a new repo.""" repo_ = Repository(name or Repository.generate_name()) repo_.init(f"Created {datetime.date.today().strftime('%d/%m/%Y')}") @@ -30,7 +31,7 @@ def add_repo(name): @click.option("--fixup", "-f", is_flag=True, help="Capitalize input (convenience).") def commit(title: str, desc, tag, fixup): """Commit changes to git using task info as the commit message.""" - task_ = Task( + todo_ = Todo( title.capitalize() if fixup else title, desc, tag, @@ -38,9 +39,9 @@ def commit(title: str, desc, tag, fixup): "open", ) - if click.confirm(f"Commit {task_.title}?", default=True): - task_.status = "completed" - task_.add() + if click.confirm(f"Commit {todo_.title}?", default=True): + todo_.status = "completed" + todo_.add() click.secho( subprocess.run( @@ -48,7 +49,7 @@ def commit(title: str, desc, tag, fixup): "git", "commit", "-am", - f"({task_.tag or datetime.date.today().strftime('%d/%m/%Y')}) {task_.title}", + f"({todo_.tag or datetime.date.today().strftime('%d/%m/%Y')}) {todo_.title}", ], cwd=Path.cwd(), text=True, @@ -62,12 +63,14 @@ def commit(title: str, desc, tag, fixup): @repo_cli.command() def generate_name(): + """Generate a random placeholder name for a new repo.""" click.secho(Repository.generate_name(), fg="green") @repo_cli.command() @click.argument("name") def view_repo(name): + """View all attributes of a repo for exporting.""" repo_ = Repository(name) click.secho(pformat(repo_.to_dict()), fg="green") @@ -75,12 +78,14 @@ def view_repo(name): @repo_cli.command() def view_repos(): + """See a list of all repos found in the home directory.""" click.secho("\n".join([str(i) for i in Repository.all()]), fg="green") @repo_cli.command() @click.argument("name") def delete_repo(name): + """Delete a repo.""" repo_ = Repository(name) if click.confirm(f"Delete {repo_.name}?", default=True): repo_.delete() diff --git a/code_garden/todos.py b/code_garden/todos.py index bea54db..81a018e 100644 --- a/code_garden/todos.py +++ b/code_garden/todos.py @@ -5,14 +5,16 @@ import click -db = sqlite3.connect("todos.db") +from code_garden import config + +db = sqlite3.connect(config.HOME_DIR / "todos.db", check_same_thread=False) cursor = db.cursor() cursor.execute( - "CREATE TABLE IF NOT EXISTS tasks (title TEXT, description TEXT, tag TEXT, date_added DATETIME, status TEXT, id INTEGER PRIMARY KEY AUTOINCREMENT)" + "CREATE TABLE IF NOT EXISTS todos (title TEXT, description TEXT, tag TEXT, date_added DATETIME, status TEXT, repo TEXT, id INTEGER PRIMARY KEY AUTOINCREMENT)" ) -class Task: +class Todo: status_options = {"open": "cyan", "active": "yellow", "completed": "blue"} def __init__( @@ -22,6 +24,7 @@ def __init__( tag: str, date_added: datetime.datetime, status: str, + repo: str, id: int = None, ): self.title = title @@ -29,44 +32,55 @@ def __init__( self.tag = tag self.date_added = date_added self.status = status + self.repo = repo self.id = id def add(self): cursor.execute( - "INSERT INTO tasks (title, description, tag, date_added, status) VALUES (?,?,?,?,?)", - (self.title, self.description, self.tag, self.date_added, self.status), + "INSERT INTO todos (title, description, tag, date_added, status, repo) VALUES (?,?,?,?,?,?)", + ( + self.title, + self.description, + self.tag, + self.date_added, + self.status, + self.repo, + ), ) db.commit() @classmethod def get(cls, id): cursor.execute( - "SELECT title, description, tag, date_added, status, id FROM tasks WHERE id=?", + "SELECT title, description, tag, date_added, status, repo, id FROM todos WHERE id=?", (str(id),), ) result = cursor.fetchone() - return Task(result[0], result[1], result[2], result[3], result[4], result[5]) + return Todo( + result[0], result[1], result[2], result[3], result[4], result[5], result[6] + ) @classmethod - def see_list(cls): + def see_list(cls, repo): cursor.execute( - "SELECT title, description, tag, date_added, status, id FROM tasks" + "SELECT title, description, tag, date_added, status, repo, id FROM todos WHERE repo=?", + (repo,), ) results = cursor.fetchall() return sorted( - [Task(i[0], i[1], i[2], i[3], i[4], i[5]) for i in results], + [Todo(i[0], i[1], i[2], i[3], i[4], i[5], i[6]) for i in results], key=lambda x: (x.status == "completed", x.status != "active", x.id), ) def edit(self): cursor.execute( - "UPDATE tasks SET title=?, description=?, tag=?, status=? WHERE id=?", + "UPDATE todos SET title=?, description=?, tag=?, status=? WHERE id=?", (self.title, self.description, self.tag, self.status, str(self.id)), ) db.commit() def delete(self): - cursor.execute("DELETE FROM tasks WHERE id=?", (str(self.id),)) + cursor.execute("DELETE FROM todos WHERE id=?", (str(self.id),)) db.commit() def __str__(self): @@ -76,6 +90,15 @@ def __str__(self): self.tag or datetime.date.today().strftime("%d/%m/%Y"), ) + def to_dict(self): + return { + "id": self.id, + "name": self.title, + "tag": self.tag, + "repository": self.repo, + "done": self.status == "completed", + } + @click.group() def todos_cli(): @@ -89,47 +112,48 @@ def todos_cli(): @click.option("--fixup", "-f", is_flag=True, help="Capitalize input (convenience).") def add_todo(title: str, desc, tag, fixup): """Add a task.""" - task_ = Task( + todo_ = Todo( title.capitalize() if fixup else title, desc, tag, datetime.datetime.now(), "open", + Path.cwd().name, ) - task_.add() - click.secho(f"{task_.title} added.", fg="green") + todo_.add() + click.secho(f"{todo_.title} added.", fg="green") @todos_cli.command() @click.option( - "-a", "--all", is_flag=True, default=False, help="Include completed tasks." + "-a", "--all", is_flag=True, default=False, help="Include completed todos." ) def view_todos(all): - """See list of all undone tasks.""" + """See list of all undone todos.""" _ = ( - Task.see_list() + Todo.see_list(Path.cwd().name) if all - else [i for i in Task.see_list() if i.status != "completed"] + else [i for i in Todo.see_list(Path.cwd().name) if i.status != "completed"] ) for i in _: - click.secho(str(i), fg=Task.status_options.get(i.status)) + click.secho(str(i), fg=Todo.status_options.get(i.status)) @todos_cli.command() @click.argument("id") def view_todo(id): """Get a task and see detailed info.""" - task_ = Task.get(int(id)) + todo_ = Todo.get(int(id)) display = "\n\n".join( [ - task_.title, - task_.status, - task_.tag or "(No Tag)", - task_.description or "(No Description)", - task_.date_added, + todo_.title, + todo_.status, + todo_.tag or "(No Tag)", + todo_.description or "(No Description)", + todo_.date_added, ] ) - click.secho(display, fg=Task.status_options.get(task_.status)) + click.secho(display, fg=Todo.status_options.get(todo_.status)) @todos_cli.command() @@ -145,24 +169,24 @@ def view_todo(id): ) def edit_todo(name, desc, tag, status, id): """Edit a task.""" - task_ = Task.get(int(id)) - task_.title = name or task_.title - task_.description = desc or task_.description - task_.tag = tag or task_.tag - task_.status = status or task_.status + todo_ = Todo.get(int(id)) + todo_.title = name or todo_.title + todo_.description = desc or todo_.description + todo_.tag = tag or todo_.tag + todo_.status = status or todo_.status - task_.edit() - click.secho(f"{task_.title} edited.", fg="green") + todo_.edit() + click.secho(f"{todo_.title} edited.", fg="green") @todos_cli.command() @click.argument("id") def delete_todo(id): """Delete a task.""" - task_ = Task.get(int(id)) - if click.confirm(f"Delete {task_.title}?", default=True): - task_.delete() - click.secho(f"{task_.title} deleted.", fg="green") + todo_ = Todo.get(int(id)) + if click.confirm(f"Delete {todo_.title}?", default=True): + todo_.delete() + click.secho(f"{todo_.title} deleted.", fg="green") else: click.secho("Nevermind.", fg="red") @@ -171,22 +195,22 @@ def delete_todo(id): @click.argument("id") def todo_done(id): """Mark a task as complete.""" - task_ = Task.get(int(id)) - task_.status = "completed" - task_.edit() + todo_ = Todo.get(int(id)) + todo_.status = "completed" + todo_.edit() - click.secho(f"{task_.title} completed.", fg="blue") + click.secho(f"{todo_.title} completed.", fg="blue") @todos_cli.command() @click.argument("id") def commit_todo(id): """Commit changes to git using task info as the commit message.""" - task_ = Task.get(int(id)) + todo_ = Todo.get(int(id)) - if click.confirm(f"Commit {task_.title}?", default=True): - task_.status = "completed" - task_.edit() + if click.confirm(f"Commit {todo_.title}?", default=True): + todo_.status = "completed" + todo_.edit() click.secho( subprocess.run( @@ -194,7 +218,7 @@ def commit_todo(id): "git", "commit", "-am", - f"({task_.tag or datetime.date.today().strftime('%d/%m/%Y')}) {task_.title}", + f"({todo_.tag or datetime.date.today().strftime('%d/%m/%Y')}) {todo_.title}", ], cwd=Path.cwd(), text=True, @@ -210,8 +234,8 @@ def commit_todo(id): @click.argument("id") def pick_todo(id): """Mark a task as 'active' (currently being worked on).""" - task_ = Task.get(int(id)) - task_.status = "active" - task_.edit() + todo_ = Todo.get(int(id)) + todo_.status = "active" + todo_.edit() - click.secho(f"{task_.title} active.", fg="yellow") + click.secho(f"{todo_.title} active.", fg="yellow") diff --git a/code_garden/web/routes.py b/code_garden/web/routes.py index b2af52b..94932c7 100644 --- a/code_garden/web/routes.py +++ b/code_garden/web/routes.py @@ -1,7 +1,11 @@ +import datetime + from flask import current_app, render_template, request +from code_garden.todos import Todo + from .. import config -from ..models import Branch, DiffItem, IgnoreItem, Repository, Todo +from ..models import Branch, DiffItem, IgnoreItem, Repository @current_app.get("/") @@ -11,7 +15,10 @@ def index(): @current_app.post("/settings") def settings(): - return config.get() + config_ = config.get() + config_.update({"debug": current_app.config.get("ENV") == "development"}) + + return config_ @current_app.post("/repositories") @@ -107,33 +114,67 @@ def merge_branch(): @current_app.post("/create_todo") def create_todo(): - todo_ = Todo(request.json.get("repository"), request.json.get("name")) - todo_.create() + todo_ = Todo( + request.json.get("name"), + "", + request.json.get("tag"), + datetime.datetime.now(), + "open", + request.json.get("repository"), + ) + todo_.add() return {"status": "done"} @current_app.post("/edit_todo") def edit_todo(): - Todo.edit( - request.json.get("repository"), - int(request.json.get("id")), - request.json.get("new_name"), - ) + todo_ = Todo.get(request.json.get("id")) + + todo_.title = request.json.get("new_name") + todo_.tag = request.json.get("new_tag") + todo_.edit() return {"status": "done"} @current_app.post("/delete_todo") def delete_todo(): - Todo.delete(request.json.get("repository"), int(request.json.get("id"))) + todo_ = Todo.get(request.json.get("id")) + todo_.delete() + + return {"status": "done"} + + +@current_app.post("/clear_completed") +def clear_completed(): + repo_ = Repository(request.json.get("repo")) + for i in repo_.todos: + if i.status == "completed": + i.delete() return {"status": "done"} @current_app.post("/toggle_todo") def toggle_todo(): - Todo.toggle(request.json.get("repository"), int(request.json.get("id"))) + todo_ = Todo.get(request.json.get("id")) + + todo_.status = "completed" if todo_.status in ["open", "active"] else "open" + todo_.edit() + + return {"status": "done"} + + +@current_app.post("/commit_todo") +def commit_todo(): + todo_ = Todo.get(request.json.get("id")) + + todo_.status = "completed" if todo_.status in ["open", "active"] else "open" + todo_.edit() + Repository(todo_.repo).commit( + f"({todo_.tag or datetime.date.today().strftime('%d/%m/%Y')}) {todo_.title}" + ) return {"status": "done"} diff --git a/code_garden/web/static/App.css b/code_garden/web/static/App.css index 6b44351..6a9ac07 100644 --- a/code_garden/web/static/App.css +++ b/code_garden/web/static/App.css @@ -1,3 +1,7 @@ +* { + font-family: "Play"; +} + :root, html[data-theme="light"] { --primary-bg: #cccccc; @@ -76,7 +80,7 @@ a:hover { border-color: var(--btn-color); background-color: transparent; color: var(--btn-color); - font-size: small; + /* font-size: small; */ letter-spacing: 1px; } @@ -91,7 +95,7 @@ a:hover { background-color: transparent; color: var(--primary-txt); border-color: var(--primary-txt); - font-size: small; + /* font-size: small; */ } ::placeholder { @@ -104,3 +108,10 @@ a:hover { overflow-y: scroll; height: 700px; } + +.badge { + border: 0; + border-left: 1px dotted var(--btn-color); + background-color: var(--primary-bg); + color: var(--btn-color); +} diff --git a/code_garden/web/static/App.jsx b/code_garden/web/static/App.jsx index 5716f2a..7a64f6d 100644 --- a/code_garden/web/static/App.jsx +++ b/code_garden/web/static/App.jsx @@ -1,6 +1,16 @@ const LoadingContext = React.createContext(); const CurrentRepoContext = React.createContext(); +const tags = [ + "misc", + "bugfix", + "refactor", + "documentation", + "feature", + "tweak", + "ui", +]; + const apiCall = (url, args, callback) => { fetch(url, { method: "POST", @@ -142,6 +152,7 @@ function CreateTodoForm() { const [currentRepository, , getRepository] = React.useContext(CurrentRepoContext); const [name, setName] = React.useState(""); + const [tag, setTag] = React.useState(""); const createTodo = (e) => { e.preventDefault(); @@ -151,18 +162,18 @@ function CreateTodoForm() { { repository: currentRepository.name, name: name, + tag: tag, }, function (data) { getRepository(currentRepository.name); setLoading(false); setName(""); + setTag(""); } ); }; - const onChangeName = (e) => { - setName(e.target.value); - }; + const onChangeName = (e) => setName(e.target.value); return (
); } -function TodoItem({ item, id }) { +function TodoItem({ item }) { const [, setLoading] = React.useContext(LoadingContext); const [currentRepository, , getRepository] = React.useContext(CurrentRepoContext); const [name, setName] = React.useState(item.name); + const [tag, setTag] = React.useState(item.tag); + const [deleting, setDeleting] = React.useState(false); - const onChangeName = (e) => { - setName(e.target.value); - }; + const onChangeName = (e) => setName(e.target.value); + const onChangeTag = (e) => setTag(e.target.value); const editTodo = (e) => { e.preventDefault(); @@ -197,9 +231,9 @@ function TodoItem({ item, id }) { apiCall( "/edit_todo", { - repository: currentRepository.name, - id: id, + id: item.id, new_name: name, + new_tag: tag, }, function (data) { getRepository(currentRepository.name); @@ -213,8 +247,7 @@ function TodoItem({ item, id }) { apiCall( "/delete_todo", { - repository: currentRepository.name, - id: id, + id: item.id, }, function (data) { getRepository(currentRepository.name); @@ -228,8 +261,7 @@ function TodoItem({ item, id }) { apiCall( "/toggle_todo", { - repository: currentRepository.name, - id: id, + id: item.id, }, function (data) { getRepository(currentRepository.name); @@ -241,13 +273,11 @@ function TodoItem({ item, id }) { const commitTodo = () => { setLoading(true); apiCall( - "/commit", + "/commit_todo", { - name: currentRepository.name, - msg: item.name, + id: item.id, }, function (data) { - toggleTodo(id); getRepository(currentRepository.name); setLoading(false); } @@ -258,7 +288,7 @@ function TodoItem({ item, id }) { <> > ); @@ -700,6 +745,20 @@ function App() { currentRepository.length !== 0 && setPage("repo"); }, [currentRepository]); + const clearCompleted = () => { + setLoading(true); + apiCall( + "/clear_completed", + { + repo: currentRepository.name, + }, + function (data) { + getRepository(currentRepository.name); + setLoading(false); + } + ); + }; + const themes = [ "light", "dark", @@ -981,9 +1040,17 @@ function App() {