diff --git a/frontend/api.py b/frontend/api.py new file mode 100644 index 0000000..e180c72 --- /dev/null +++ b/frontend/api.py @@ -0,0 +1,218 @@ +from types import SimpleNamespace +import urllib +import requests + +DEFAULT_URL = "http://127.0.0.1:8000" + +API_TODO_ITEMS_LIST = "api/todo_items/" +API_TODO_ITEMS_CREATE = "api/todo_items/" +API_TODO_ITEMS_READ = "api/todo_items/{0}/" +API_TODO_ITEMS_UPDATE = "api/todo_items/{0}/" +API_TODO_ITEMS_PARTIAL_UPDATE = "api/todo_items/{0}/" +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_TOKEN = "api/token/" + + +class UserApi(object): + def __init__(self, url=DEFAULT_URL, token=None): + """ + Constructor + + Parameters + ---------- + url : str, optional + Server url. The default is DEFAULT_URL. + token : dict, optional + Existing user tokens to bypass authorization. + The default is None. + + Returns + ------- + None. + + """ + self.token = token + self.get_api = lambda x: urllib.parse.urljoin(url, x) + + # ToDo - store tokens in config + def auth(self, user, passwd): + """ + Authosization + + Parameters + ---------- + user : str + Login. + passwd : str + Password. + + Returns + ------- + dict + Generated auth token. + + """ + token = UserApi._raise_or_return_( + requests.post(url=self.get_api(API_TOKEN), json={"username": user, "password": passwd}) + ) + self.token = SimpleNamespace(**token) + return self.token + + def lists_list(self, **argv): + """ + List all the exsiting to-do lists. + Auth required + + Returns + ------- + list + to-do lists. + + """ + return UserApi._raise_or_return_( + requests.get( + url=self.get_api(API_LISTS_LIST), headers=self._access_token_(), params=argv + ) + ) + + def lists_create(self, title="Untitled"): + """ + Create a new to-do list + Auth required + + Parameters + ---------- + title : str, optional + New list name. The default is "Untitled". + + """ + return UserApi._raise_or_return_( + requests.post( + url=self.get_api(API_LISTS_CREATE), + json={"title": title}, + headers=self._access_token_(), + ) + ) + + def todo_items_list(self, **argv): + """ + List all the exsiting to-do items. + Auth required + + Returns + ------- + list + to-do items. + + """ + return UserApi._raise_or_return_( + requests.get( + url=self.get_api(API_TODO_ITEMS_LIST), headers=self._access_token_(), params=argv + ) + ) + + # def create(self, title="Untitled"): + # """ + # Create a new to-do list + # Auth required + + # Parameters + # ---------- + # title : str, optional + # New list name. The default is "Untitled". + + # """ + # response = requests.post( + # url=self.get_api(API_LISTS_CREATE), json={"title": title}, headers=self._access_token_() + # url=self.get_api(API_TODO_ITEMS_CREATE), json={"title": title}, headers=self._access_token_() + # ) + # response.raise_for_status() + # return response.json() + + # def read(self, id): + # """ + # Read a to-do list contents + + # Parameters + # ---------- + # id : int + # List id. + + # Returns + # ------- + # list + # Requested contents + + # """ + # response = requests.post( + # url=self.get_api(API_LISTS_READ).format(id), headers=self._access_token_() + # ) + # response.raise_for_status() + # return response.json() + + # def update(self, id, title="Untitled"): + # """ + # Add a to-do item to the list + + # Parameters + # ---------- + # id : int + # List id. + # title : str, optional + # To-do item title. The default is "Untitled". + + # """ + # response = requests.put( + # json={"title": title}, + # url=self.get_api(API_LISTS_UPDATE).format(id), + # headers=self._access_token_(), + # ) + # response.raise_for_status() + # return response.json() + + # def partial_update(self, id, title="Untitled"): + # """ + # Update list item - untrusted + + # """ + # response = requests.patch( + # json={"title": title}, + # url=self.get_api(API_LISTS_PARTIAL_UPDATE).format(id), + # headers=self._access_token_(), + # ) + # response.raise_for_status() + # return response.json() + + # def delete(self, id): + # """ + # Delete list + + # Parameters + # ---------- + # id : int + # List id to delete. + + # """ + # response = requests.delete( + # url=self.get_api(API_LISTS_DELETE).format(id), headers=self._access_token_() + # ) + # response.raise_for_status() + # return response.json() + + def _access_token_(self): + if self.token is None: + raise RuntimeError("Authosization required for requested operation!") + return {"Authorization": f"Bearer {self.token.access}"} + + @staticmethod + def _raise_or_return_(response): + response.raise_for_status() + return response.json() diff --git a/frontend/api_demo.py b/frontend/api_demo.py new file mode 100644 index 0000000..f79bbc3 --- /dev/null +++ b/frontend/api_demo.py @@ -0,0 +1,48 @@ +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])) + + +DEFAULT_URL = "http://127.0.0.1:8000" + +user = User(url=DEFAULT_URL) +user.auth("root", "root") + +# 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) + +# Append a new list to user: +print("Appending list...") +scroll = user.appendUserList(title="a new list!") +print_lists(lists) + +# Modify list 0: +print("Modifyng list...") +lists[0].modify(title="A new title") +print_lists(lists) + +# Append item to list: +print("Appending item to last list...") +item = lists[-1].append(text="this is an item") +print_lists(lists) + +# Modifying item +print("Modifyng appended item...") +item.modify(finished=True, text="this is an updated item") +print_lists(lists) + +# Removing item at 0 +print("Removing item 0 from list 0...") +lists[0].remove(0) +print_lists(lists) diff --git a/frontend/login.py b/frontend/login.py new file mode 100644 index 0000000..64ba0e3 --- /dev/null +++ b/frontend/login.py @@ -0,0 +1,68 @@ +import tkinter as tk +from user import User +import message + + +class LoginFrame(tk.Frame): + + loggedIn = False + + def __init__(self, master=None, url=None) -> None: + """ + Функция инициаизации класса + """ + super().__init__(master) + + # Иницализируем параметры окна + self.master = master + self.pack(fill=tk.BOTH, expand=1) + + # self.grid(row=0, column=0, sticky=tk.N + tk.S + tk.E + tk.W) + # tk.Grid.rowconfigure(master, 0, weight=1) + # tk.Grid.columnconfigure(master, 0, weight=1) + + # Иницализируем параметры пользователя + self.user = User(url=url) + + # Настраиваем размеры и включаем иницализацию + self.initAUTH() + + def login_clicked(self) -> None: + """ + Функция авторизации + """ + try: + self.user.auth(self.login.get(), self.password.get()) + self.loggedIn = True + except Exception as ex: + print(ex) + message.invalid_login() + + def initAUTH(self) -> None: + """ + Создает окно авторизации программы + """ + # Конфигурируем сетку + for rows in range(25): + tk.Grid.rowconfigure(self, rows, weight=1) + + for columns in range(25): + tk.Grid.columnconfigure(self, columns, weight=1) + + # Подпись и поле ввода для логина + login_label = tk.Label(self, text="Введите логин") + login_label.grid(row=9, column=12, columnspan=3, rowspan=1, sticky="nsew") + + self.login = tk.Entry(self) + self.login.grid(row=10, column=12, columnspan=3, rowspan=1, sticky="nsew") + + # Подпись и поле ввода для пароля + password_label = tk.Label(self, text="Введите пароль") + password_label.grid(row=11, column=12, columnspan=3, rowspan=1, sticky="nsew") + + self.password = tk.Entry(self, show="*") + self.password.grid(row=12, column=12, columnspan=3, rowspan=1, sticky="nsew") + + # Кнопка авториазции + btn = tk.Button(self, text="Войти", command=self.login_clicked) + btn.grid(row=14, column=12, columnspan=3, rowspan=1, sticky="nsew") diff --git a/frontend/message.py b/frontend/message.py new file mode 100644 index 0000000..824d07b --- /dev/null +++ b/frontend/message.py @@ -0,0 +1,20 @@ +from tkinter import messagebox as mb + +TITLE_INFO_BOX = "Сообщение!" +MESSAGE_INVALID_LOGIN = "Неправильный логин или пароль" +MESSAGE_EMPTY = "Сдесь могло быть ваше сообщение" + + +def infobox(msg: str = None) -> None: + """ + Показывает передаваемое сообщение в messagebox + + :param msg: передаваемое сообщение + """ + if msg is None: + msg = MESSAGE_EMPTY + mb.showinfo(TITLE_INFO_BOX, msg) + + +def invalid_login(): + infobox(MESSAGE_INVALID_LOGIN) diff --git a/frontend/todo_tk.py b/frontend/todo_tk.py index 41e0e99..831ac6c 100644 --- a/frontend/todo_tk.py +++ b/frontend/todo_tk.py @@ -1,14 +1,55 @@ #!/usr/bin/env python3 +import sys import tkinter as tk +from login import LoginFrame +from workspace import WorkSpaceFrame + +if "win" in sys.platform.lower(): + DEFAULT_URL = "http://localhost:8000" +else: + DEFAULT_URL = "http://0.0.0.0:8000" + +BASE_W = 580 +BASE_H = 400 + +TITLE_APP = "ToDo Application" -class Application(tk.Frame): - def __init__(self, master=None): - super().__init__(master) +class Application(tk.Tk): + def __init__(self): + super().__init__() + self.center_window() + self.title(TITLE_APP) + + def login(self): + """Возвращает пользователя - его можно потом сериализовать""" + self.frame = LoginFrame(master=self, url=DEFAULT_URL) + while not self.frame.loggedIn: + self.update_idletasks() + self.update() + self.frame.destroy() + return self.frame.user + + def main(self, user): + self.frame = WorkSpaceFrame(master=self, user=user) + self.mainloop() + + def center_window(self, width: str = BASE_W, heigh: str = BASE_H) -> None: + """ + Центрирует приложение по центру экрана + + :param width: ширина окна + :param heigh: высота окна + """ + sw = self.winfo_screenwidth() + sh = self.winfo_screenheight() + + x = (sw - width) / 2 + y = (sh - heigh) / 2 + self.geometry("%dx%d+%d+%d" % (width, heigh, x, y)) if __name__ == "__main__": app = Application() - app.master.title("ToDo") - app.mainloop() + app.main(app.login()) diff --git a/frontend/user.py b/frontend/user.py new file mode 100644 index 0000000..5cdff8e --- /dev/null +++ b/frontend/user.py @@ -0,0 +1,108 @@ +import os + +from datetime import datetime +from api import UserApi + + +class ToDoList(object): + def __init__(self, id, title, created_at=None, items=[], parent=None): + self.id = id + self.title = title + self.items = items + self.created_at = created_at + + def __iter__(self): + for item in self.items: + yield item + + def __getitem__(self, index): + return self.items[index] + + def __len__(self): + return len(self.items) + + def __str__(self): + return f"[{self.id}] {self.title}" + + def index(self, value): + return self.items.index(value) + + # ToDo + def remove(self, index): + self.items.remove(self.items[index]) + self.sync() + + # 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 + + def modify(self, **argv): + 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 ToDoItem(object): + def __init__(self, id, text, finished=False, created_at=None, parent=None): + self.id = id + self.text = text + self.finished = finished + self.created_at = created_at + + def __str__(self): + return f"[{self.id}] {self.text}" + + def modify(self, **argv): + 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): + if "DEBUG" in os.environ: + return + UserApi.auth(self, user, passwd) + + # ToDo + items = [ + ToDoList( + id=i, + title=f"List {i}", + created_at=datetime.now(), + items=[ + ToDoItem(id=i * 10 + j, text=f"Item {i*10+j}", created_at=datetime.now()) + for j in range(10) + ], + ) + for i in range(10) + ] + + # ToDo + def fetchUserLists(self): + return self.items + + # ToDo + def removeUserList(self, id): + self.items = [item for item in self.items if item.id != id] + + # ToDo + def appendUserList(self, title): + item = ToDoList(id=None, title=title, created_at=datetime.now()) + self.items.append(item) + item.sync() + return item diff --git a/frontend/workspace.py b/frontend/workspace.py new file mode 100644 index 0000000..34830ec --- /dev/null +++ b/frontend/workspace.py @@ -0,0 +1,163 @@ +import tkinter as tk + + +def str_time(time): + return time.strftime("%Y-%m-%d %H:%M:%S") + + +TODO_ITEM_TABLE_TEXT_WIDTH = 15 +TODO_ITEM_TABLE_FINISHED_WIDTH = 8 +TODO_ITEM_TABLE_CREATED_AT_WIDTH = 15 + + +def placeholder(): + print("Не реализовано") + + +class ToDoItemWidget(tk.Frame): + @staticmethod + def header(parent): + body = tk.Frame(parent) + + text = tk.Label(body, text="Текст", width=TODO_ITEM_TABLE_TEXT_WIDTH) + text.pack(side="left") + + text = tk.Label(body, text="Выполнено", width=TODO_ITEM_TABLE_FINISHED_WIDTH) + text.pack(side="left") + + text = tk.Label(body, text="Создано", width=TODO_ITEM_TABLE_CREATED_AT_WIDTH) + text.pack(side="left") + + return body + + def __init__(self, *args, item, **argv): + super().__init__(*args, **argv) + + self.parent = self.master + self.item = item + + self.noteLabel = tk.Label(self, text=item.text, width=TODO_ITEM_TABLE_TEXT_WIDTH) + self.noteLabel.pack(side="left") + + self.finished = tk.IntVar(value=int(item.finished)) + self.finishedButton = tk.Checkbutton( + self, + variable=self.finished, + command=self.finishedButton_command, + width=TODO_ITEM_TABLE_FINISHED_WIDTH, + ) + self.finishedButton.pack(side="left") + + self.createdAt = tk.Label( + self, text=str_time(item.created_at), width=TODO_ITEM_TABLE_CREATED_AT_WIDTH + ) + self.createdAt.pack(side="left") + + self.remove = tk.Button(self, text="Удалить", command=lambda: self.parent.remove(self.item)) + self.remove.pack(side="left") + + def finishedButton_command(self): + self.item.modify(finished=self.finished.get() > 0) + + +class ToDoListWidget(tk.Frame): + def __init__(self, *args, **argv): + super().__init__(*args, **argv) + + def fill(self, itemList): + + header = ToDoItemWidget.header(self) + header.pack(side="left") + header.pack(side="top", fill="y") + + self.itemList = itemList + + for item in itemList: + item = ToDoItemWidget(self, item=item) + item.pack(side="top", fill="y") + + self.itemToAdd = tk.Text(self, width=15, height=1) + self.itemToAdd.pack(side="top") + + add = tk.Button(self, text="Добавить заметку", command=self.add_command) + add.pack(side="top") + + delete = tk.Button(self, text="Удалить лист", command=placeholder) + delete.pack(side="top") + + def update(self, itemList=None): + self.clear() + if itemList is None: + self.fill(self.itemList) + else: + self.fill(itemList) + + def add_command(self): + self.itemList.append(self.itemToAdd.get(1.0, "end")) + self.update() + + def remove(self, item): + self.itemList.remove(self.itemList.index(item)) + self.update() + + def clear(self): + for widget in self.winfo_children(): + widget.destroy() + + +class WorkSpaceFrame(tk.Frame): + def __init__(self, user, master=None, url=None) -> None: + """ + Функция инициаизации класса + """ + super().__init__(master) + + self.master = master + self.user = user + + self.pack(fill=tk.BOTH, expand=1) + self.initLayout(user) + + def initLayout(self, user): + + # data + self.lists = user.fetchUserLists() + + text = tk.Text(self, width=15, height=1) + text.pack(anchor="sw") + + add = tk.Button(self, text="Добавить лист", command=placeholder) + add.pack(anchor="sw") + + # select list box + self.listBox = tk.Listbox(self, width=30, selectmode=tk.SINGLE) + self.listBox.pack(side="left", fill="y") + self.listBox.bind("<>", self.listBox_selected) + + # scroll bar + scrollbar = tk.Scrollbar(self, orient="vertical") + scrollbar.config(command=self.listBox.yview) + scrollbar.pack(side="left", fill="y") + + # add scroll bar to list box + self.listBox.config(yscrollcommand=scrollbar.set) + + # fill list box + 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) + + # todo lists + self.toToList = ToDoListWidget(self) + self.toToList.pack(side="left", fill="both", expand=1) + + def listBox_selected(self, *args): + + self.toToList.clear() + + selection = self.listBox.curselection() + cur = selection[0] + + self.toToList.fill(self.lists[cur])