diff --git a/backend/backend/urls.py b/backend/backend/urls.py index d123db5..cdcbc7f 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -13,13 +13,13 @@ from drf_yasg import openapi from .api import router schema_view = get_schema_view( - openapi.Info( - title="ToDo List", - default_version='v1', - description="Swagger Interface for ToDo List", - ), - public=True, - permission_classes=(permissions.AllowAny,), + openapi.Info( + title="ToDo List", + default_version="v1", + description="Swagger Interface for ToDo List", + ), + public=True, + permission_classes=(permissions.AllowAny,), ) urlpatterns = [ @@ -28,5 +28,5 @@ urlpatterns = [ path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), path("api/", include(router.urls)), path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), - path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + path("swagger/", schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui"), ] diff --git a/frontend/api.py b/frontend/api.py index e180c72..458c0d8 100644 --- a/frontend/api.py +++ b/frontend/api.py @@ -13,12 +13,13 @@ API_TODO_ITEMS_DELETE = "api/todo_items/{0}/" API_LISTS_LIST = "api/lists/" API_LISTS_CREATE = "api/lists/" -API_LISTS_READ = "lists/{0}/" -API_LISTS_UPDATE = "lists/{0}/" -API_LISTS_PARTIAL_UPDATE = "lists/{0}/" -API_LISTS_DELETE = "lists/{0}/" +API_LISTS_READ = "api/lists/{0}/" +API_LISTS_UPDATE = "api/lists/{0}/" +API_LISTS_PARTIAL_UPDATE = "api/lists/{0}/" +API_LISTS_DELETE = "api/lists/{0}/" -API_TOKEN = "api/token/" +API_TOKEN_CREATE = "api/token/" +API_TOKEN_REFRESH = "api/token/refresh/" class UserApi(object): @@ -61,11 +62,23 @@ class UserApi(object): """ token = UserApi._raise_or_return_( - requests.post(url=self.get_api(API_TOKEN), json={"username": user, "password": passwd}) + requests.post( + url=self.get_api(API_TOKEN_CREATE), json={"username": user, "password": passwd} + ) ) self.token = SimpleNamespace(**token) return self.token + def refresh(self): + """ + Refresh existing token + """ + token = UserApi._raise_or_return_( + requests.post(url=self.get_api(API_TOKEN_REFRESH), json={"refresh": self.token.refresh}) + ) + self.token.access = token["access"] + return self.token + def lists_list(self, **argv): """ List all the exsiting to-do lists. @@ -102,6 +115,43 @@ class UserApi(object): ) ) + def lists_delete(self, id): + """ + Auth required + + Deletes a to-do list by id + + Parameters + ---------- + id: to-do list id to delete + """ + return UserApi._raise_or_return_( + requests.delete( + url=self.get_api(API_LISTS_DELETE.format(id)), + headers=self._access_token_(), + ) + ) + + def lists_update(self, title, id): + """ + Rename a new to-do list + Auth required + + Parameters + ---------- + title : str + New name for a list. + id : int + + """ + return UserApi._raise_or_return_( + requests.put( + url=self.get_api(API_LISTS_UPDATE.format(id)), + json={"title": title}, + headers=self._access_token_(), + ) + ) + def todo_items_list(self, **argv): """ List all the exsiting to-do items. @@ -119,6 +169,67 @@ class UserApi(object): ) ) + def todo_items_create(self, parent, text="Note"): + """ + Create a new to-do item + Auth required + + Parameters + ---------- + parent : id of parent list + text : str, optional + New note. The default is "Note". + + """ + return UserApi._raise_or_return_( + requests.post( + url=self.get_api(API_TODO_ITEMS_CREATE), + json={"text": text, "parent": parent, "finished": False}, + headers=self._access_token_(), + ) + ) + + def todo_items_delete(self, id): + """ + Auth required + + Deletes a to-do item by id + + Parameters + ---------- + id: to-do item id to delete + """ + return UserApi._raise_or_return_( + requests.delete( + url=self.get_api(API_TODO_ITEMS_DELETE.format(id)), + headers=self._access_token_(), + ) + ) + + def todo_items_update(self, id, text, finished, parent): + """ + Rename a new to-do list + Auth required + + Parameters + ---------- + id : int + Note id + text : str + New note for the item. + finished : bool + New state for the item + parent : int + Parent list id + """ + return UserApi._raise_or_return_( + requests.put( + url=self.get_api(API_TODO_ITEMS_UPDATE.format(id)), + json={"text": text, "finished": finished, "parent": parent}, + headers=self._access_token_(), + ) + ) + # def create(self, title="Untitled"): # """ # Create a new to-do list @@ -215,4 +326,8 @@ class UserApi(object): @staticmethod def _raise_or_return_(response): response.raise_for_status() - return response.json() + try: + return response.json() + except Exception as e: + print(e) + return response.content diff --git a/frontend/api_demo.py b/frontend/api_demo.py index f79bbc3..329297c 100644 --- a/frontend/api_demo.py +++ b/frontend/api_demo.py @@ -1,48 +1,50 @@ +import random from user import User def print_lists(lists): for item in lists: - print(f"List: '{item}'", f"Id: {item.id}", "|", "|".join([str(x) for x in item.items])) + print( + f"List: '{item.title}'", f"Id: {item.id}", "|", "|".join([str(x) for x in item.items_]) + ) DEFAULT_URL = "http://127.0.0.1:8000" user = User(url=DEFAULT_URL) user.auth("root", "root") +user.refresh() # Fetch existing lists: -lists = user.fetchUserLists() -print("Fecthing...") -print_lists(lists) - -# Remove user list by id: -user.removeUserList(5) -lists = user.fetchUserLists() -print(f"Removing {5}...") -print_lists(lists) +print_lists(user.fetchUserLists()) # Append a new list to user: print("Appending list...") scroll = user.appendUserList(title="a new list!") -print_lists(lists) +print_lists(user.fetchUserLists()) # Modify list 0: print("Modifyng list...") -lists[0].modify(title="A new title") -print_lists(lists) +user.lists_[0].modify(title=f"A new title №{random.random()}") +print_lists(user.fetchUserLists()) # Append item to list: print("Appending item to last list...") -item = lists[-1].append(text="this is an item") -print_lists(lists) +item = user.lists_[-1].append(text="this is an item") +print_lists(user.fetchUserLists()) # Modifying item print("Modifyng appended item...") item.modify(finished=True, text="this is an updated item") -print_lists(lists) +print_lists(user.fetchUserLists()) -# Removing item at 0 -print("Removing item 0 from list 0...") -lists[0].remove(0) -print_lists(lists) +# Removing item at from last list +print("Removing the last item from the last list...") +user.lists_[-1].remove(-1) +print_lists(user.fetchUserLists()) + +# Remove user list by id: +i = user.lists_[0].id +print(f"Removing {i}...") +user.removeUserList(i) +print_lists(user.fetchUserLists()) diff --git a/frontend/login.py b/frontend/login.py index 64ba0e3..0efd7f9 100644 --- a/frontend/login.py +++ b/frontend/login.py @@ -38,6 +38,13 @@ class LoginFrame(tk.Frame): print(ex) message.invalid_login() + # Если захочется реализовать в логине + """ + @property + def remember(self): + return self.rbtn_var.get() + """ + def initAUTH(self) -> None: """ Создает окно авторизации программы @@ -66,3 +73,16 @@ class LoginFrame(tk.Frame): # Кнопка авториазции btn = tk.Button(self, text="Войти", command=self.login_clicked) btn.grid(row=14, column=12, columnspan=3, rowspan=1, sticky="nsew") + + # Если захочется реализовать в логине + """ + # Запомнить пользователя + self.rbtn_var = tk.IntVar(value=0) + rbtn = tk.Checkbutton( + self, + text="Запомнить меня", + variable=self.rbtn_var, + command=None + ) + rbtn.grid(row=15, column=12, columnspan=3, rowspan=1, sticky="nsew") + """ diff --git a/frontend/todo_tk.py b/frontend/todo_tk.py index 831ac6c..50a43e5 100644 --- a/frontend/todo_tk.py +++ b/frontend/todo_tk.py @@ -4,13 +4,14 @@ import sys import tkinter as tk from login import LoginFrame from workspace import WorkSpaceFrame +from user import User if "win" in sys.platform.lower(): DEFAULT_URL = "http://localhost:8000" else: DEFAULT_URL = "http://0.0.0.0:8000" -BASE_W = 580 +BASE_W = 600 BASE_H = 400 TITLE_APP = "ToDo Application" @@ -24,11 +25,22 @@ class Application(tk.Tk): def login(self): """Возвращает пользователя - его можно потом сериализовать""" + # Пользователь сохранен! Авторизация не нужна! + try: + user = User.load() + if user is not None: + return user + except Exception as e: + print("Failed to restore login:", e) + # Не удалось - нужен логин self.frame = LoginFrame(master=self, url=DEFAULT_URL) while not self.frame.loggedIn: self.update_idletasks() self.update() self.frame.destroy() + # Нужно запомнить пользователя + # if self.frame.remember: + # self.frame.user.save() return self.frame.user def main(self, user): diff --git a/frontend/user.py b/frontend/user.py index 5cdff8e..588dac8 100644 --- a/frontend/user.py +++ b/frontend/user.py @@ -1,85 +1,140 @@ import os - +import random from datetime import datetime +from types import SimpleNamespace + +from pathlib import Path + +import json + from api import UserApi +LIST_UPDATEBLE = ["title"] +TODO_ITEM_UPDATEBLE = ["text", "finished"] +UPDATE_ERROR = "Failed to update property: {0}" + +DATETIME_STR = "%Y-%m-%dT%H:%M:%S.%fZ" + +USER_TOKEN_PATH = os.path.join(Path.home(), ".todo_config.json") + + +def bad_arguments(x, d): + return list((set(x) - set(d))) + + +def date_or_str(inpt): + if type(inpt) is datetime: + return inpt + elif type(inpt) is str: + return datetime.strptime(inpt, DATETIME_STR) + else: + return datetime.now() + class ToDoList(object): - def __init__(self, id, title, created_at=None, items=[], parent=None): + def __init__(self, id, title, created_at=None, items=None, parent=None, user=None): self.id = id self.title = title - self.items = items - self.created_at = created_at + self.items_ = [] if items is None else items + self.created_at = date_or_str(created_at) + self.user = user def __iter__(self): - for item in self.items: + for item in self.items_: yield item def __getitem__(self, index): - return self.items[index] + return self.items_[index] def __len__(self): - return len(self.items) + return len(self.items_) def __str__(self): return f"[{self.id}] {self.title}" def index(self, value): - return self.items.index(value) + return self.items_.index(value) - # ToDo def remove(self, index): - self.items.remove(self.items[index]) - self.sync() + """ + Remove item AT INDEX from db + """ + item = self.items_[index] + self.items_.remove(item) + item.dispose() - # ToDo def append(self, text): - item = ToDoItem(id=None, text=text, created_at=datetime.now()) - self.items.append(item) - item.sync() - self.sync() - return item + """ + Add a new item to db + """ + if "DEBUG" in os.environ: + created_item = ToDoItem(id=random.randint(100, 1000), text=text, user=self.user) + else: + created_item = self.user.todo_items_create(parent=self.id, text=text) + created_item = ToDoItem(**created_item, user=self.user) + self.items_.append(created_item) + return created_item def modify(self, **argv): + bad = bad_arguments(argv.keys(), LIST_UPDATEBLE) + if len(bad) > 0: + raise RuntimeError(UPDATE_ERROR.format(bad[0])) for key, value in argv.items(): setattr(self, key, value) self.sync() - # ToDo + def dispose(self): + print(f"To-do list id '{self.id}' is being disposed of...") + for item in self.items_: + item.dispose() + if "DEBUG" in os.environ: + return + self.user.lists_delete(self.id) + def sync(self): - # ToDo send request or store in form print(f"Item '{self}' is being synchronized...") + if "DEBUG" in os.environ: + return + self.user.lists_update(title=self.title, id=self.id) class ToDoItem(object): - def __init__(self, id, text, finished=False, created_at=None, parent=None): + def __init__(self, id, text, finished=False, created_at=None, parent=None, user=None): self.id = id self.text = text self.finished = finished - self.created_at = created_at + self.created_at = date_or_str(created_at) + self.parent = parent + self.user = user def __str__(self): return f"[{self.id}] {self.text}" def modify(self, **argv): + bad = bad_arguments(argv.keys(), TODO_ITEM_UPDATEBLE) + if len(bad) > 0: + raise RuntimeError(UPDATE_ERROR.format(bad[0])) for key, value in argv.items(): setattr(self, key, value) self.sync() - # ToDo - def sync(self): - # ToDo send request or store in form - print(f"Item '{self}' is being synchronized...") - - -class User(UserApi): - def auth(self, user, passwd): + def dispose(self): + print(f"To-do item id '{self.id}' is being disposed of...") if "DEBUG" in os.environ: return - UserApi.auth(self, user, passwd) + self.user.todo_items_delete(self.id) - # ToDo - items = [ + def sync(self): + print(f"Item '{self}' is being synchronized...") + if "DEBUG" in os.environ: + return + self.user.todo_items_update( + id=self.id, text=self.text, finished=self.finished, parent=self.parent + ) + + +def make_debug_lists(): + return [ ToDoList( id=i, title=f"List {i}", @@ -92,17 +147,106 @@ class User(UserApi): for i in range(10) ] - # ToDo + +class User(UserApi): + def auth(self, user, passwd): + """ + Basic authentification + """ + if "DEBUG" in os.environ: + return + return UserApi.auth(self, user, passwd) + + def remove(self): + """ + Remove the login file from homedir + """ + if "DEBUG" in os.environ: + print("Debug mode is on - no login storing") + return + if not os.path.exists(USER_TOKEN_PATH): + return + try: + os.remove(USER_TOKEN_PATH) + except Exception as e: + raise RuntimeError("Failed to remove tokens:", e) + + def save(self): + """ + Store user token in homedir + """ + if "DEBUG" in os.environ: + print("Debug mode is on - no login storing") + return + try: + with open(USER_TOKEN_PATH, "w") as handler: + json.dump(self.token.__dict__, handler) + except Exception as e: + raise RuntimeError("Failed to store tokens:", e) + + @staticmethod + def load(): + """ + Restore user token from the file in homedir + """ + if "DEBUG" in os.environ: + raise RuntimeError("Debug mode is on - no login storing") + if os.path.exists(USER_TOKEN_PATH): + try: + with open(USER_TOKEN_PATH, "r") as handler: + user = User(token=SimpleNamespace(**json.load(handler))) + user.refresh() + return user + except Exception as e: + raise RuntimeError("Failed to restore tokens:", e) + return None + + # Storing lists - mostly for debug purposes + lists_ = make_debug_lists() + def fetchUserLists(self): - return self.items + """ + Fetch existing user lists from the server + returns: fetched list of ToDoList sorted by creation datetime + """ + print("Fetching lists...") + if "DEBUG" in os.environ: + return self.lists_ + user_lists = self.lists_list()["results"] + user_items = self.todo_items_list()["results"] + todo_lists = {x["id"]: ToDoList(**x, user=self) for x in user_lists} + todo_items = [ToDoItem(**x, user=self) for x in user_items] + for todo_item in todo_items: + # Catching stray items + # if not hasattr(toDoItem, "parent"): + # toDoItem.dispose() + # continue + todo_lists[todo_item.parent].items_.append(todo_item) + for todo_list in todo_lists.values(): + todo_list.items_ = sorted(todo_list.items_, key=lambda x: x.created_at) + self.lists_ = sorted(todo_lists.values(), key=lambda x: x.created_at) + return self.lists_ - # ToDo def removeUserList(self, id): - self.items = [item for item in self.items if item.id != id] + """ + Remove existing user to-do list BY ID from the serverreturns: + """ + to_remove = [item for item in self.lists_ if item.id == id][0] + self.lists_.remove(to_remove) + # if not ("DEBUG" in os.environ): + to_remove.dispose() + return self.lists_ - # ToDo def appendUserList(self, title): - item = ToDoList(id=None, title=title, created_at=datetime.now()) - self.items.append(item) - item.sync() - return item + """ + Create a new user list + title: title of list to create + returns: created item + """ + if "DEBUG" in os.environ: + item = ToDoList(id=random.randint(100, 1000), title=title, created_at=datetime.now()) + self.lists_.append(item) + return item + created_list = self.lists_create(title=title) + created_list = ToDoList(**created_list) + return created_list diff --git a/frontend/workspace.py b/frontend/workspace.py index 34830ec..73875e7 100644 --- a/frontend/workspace.py +++ b/frontend/workspace.py @@ -61,8 +61,9 @@ class ToDoItemWidget(tk.Frame): class ToDoListWidget(tk.Frame): - def __init__(self, *args, **argv): + def __init__(self, *args, delete_list, **argv): super().__init__(*args, **argv) + self.delete_list = delete_list def fill(self, itemList): @@ -82,7 +83,7 @@ class ToDoListWidget(tk.Frame): add = tk.Button(self, text="Добавить заметку", command=self.add_command) add.pack(side="top") - delete = tk.Button(self, text="Удалить лист", command=placeholder) + delete = tk.Button(self, text="Удалить лист", command=self.delete_list) delete.pack(side="top") def update(self, itemList=None): @@ -106,6 +107,17 @@ class ToDoListWidget(tk.Frame): class WorkSpaceFrame(tk.Frame): + def delete_list(self, *args): + selection = self.listBox.curselection() + cur = selection[0] + self.user.removeUserList(self.lists[cur].id) + self.lists = self.user.fetchUserLists() + self.update_lists() + if len(self.lists) > 1: + self.listBox.selection_set(first=cur - 1) + elif len(self.lists) > 0: + self.listBox.selection_set(first=0) + def __init__(self, user, master=None, url=None) -> None: """ Функция инициаизации класса @@ -118,15 +130,27 @@ class WorkSpaceFrame(tk.Frame): self.pack(fill=tk.BOTH, expand=1) self.initLayout(user) + def destroy(self): + tk.Tk.destroy(self) + if self.rbtn_var.get() > 0: + self.user.save() + else: + self.user.remove() + def initLayout(self, user): + # Запомнить пользователя + self.rbtn_var = tk.IntVar(value=1) + rbtn = tk.Checkbutton(self, text="Запомнить меня", variable=self.rbtn_var, command=None) + rbtn.pack(anchor="n") + # data self.lists = user.fetchUserLists() - text = tk.Text(self, width=15, height=1) - text.pack(anchor="sw") + self.add_list_text = tk.Text(self, width=15, height=1) + self.add_list_text.pack(anchor="sw") - add = tk.Button(self, text="Добавить лист", command=placeholder) + add = tk.Button(self, text="Добавить лист", command=self.add_list) add.pack(anchor="sw") # select list box @@ -142,22 +166,54 @@ class WorkSpaceFrame(tk.Frame): # add scroll bar to list box self.listBox.config(yscrollcommand=scrollbar.set) - # fill list box + # init list view + self.update_lists() + + # canvas for todo lists + canvas = tk.Canvas(self) + canvas.pack(side="left", fill="both", expand=1) + scrollbar = tk.Scrollbar(self, orient="vertical") + scrollbar.config(command=canvas.yview) + scrollbar.pack(side="left", fill="y") + canvas.configure(yscrollcommand=scrollbar.set) + + # todo lists + self.toDoList = ToDoListWidget(self, delete_list=self.delete_list) + self.toDoList.grid_propagate(True) + # self.toDoList = ToDoListWidget(canvas) + self.toDoList.pack(side="left", fill="y") + canvas.bind("", lambda *argv: canvas.configure(scrollregion=canvas.bbox("all"))) + canvas.create_window((0, 0), window=self.toDoList, anchor="nw") + + # select list! + if len(self.lists) > 0: + self.listBox.selection_set(first=0) + self.listBox_selected() + + def update_lists(self): + self.listBox.delete(0, "end") for item in self.lists: s = f"{str(item)}: {item.created_at.strftime('%Y-%m-%d %H:%M:%S')}" self.listBox.insert(tk.END, s) self.listBox.pack() - len(self.lists) > 0 and self.listBox.selection_set(first=0) + return len(self.lists) - # todo lists - self.toToList = ToDoListWidget(self) - self.toToList.pack(side="left", fill="both", expand=1) + def add_list(self, *args): + text = self.add_list_text.get(1.0, "end").strip() + if len(text) == 0: + print("empty name! Not adding!") + return + self.user.appendUserList(title=text) + self.lists = self.user.fetchUserLists() + + # fill list box + self.update_lists() + len(self.lists) > 0 and self.listBox.selection_set(first=len(self.lists) - 1) def listBox_selected(self, *args): - - self.toToList.clear() + self.toDoList.clear() selection = self.listBox.curselection() cur = selection[0] - self.toToList.fill(self.lists[cur]) + self.toDoList.fill(self.lists[cur])