Merge pull request #23 from AlekseyLobanov/feat_9.functional_api

Implemented most of the api placeholders
This commit was merged in pull request #23.
This commit is contained in:
Aleksey Lobanov
2021-04-28 23:02:43 +03:00
committed by GitHub
6 changed files with 431 additions and 82 deletions

View File

@@ -13,12 +13,13 @@ API_TODO_ITEMS_DELETE = "api/todo_items/{0}/"
API_LISTS_LIST = "api/lists/" API_LISTS_LIST = "api/lists/"
API_LISTS_CREATE = "api/lists/" API_LISTS_CREATE = "api/lists/"
API_LISTS_READ = "lists/{0}/" API_LISTS_READ = "api/lists/{0}/"
API_LISTS_UPDATE = "lists/{0}/" API_LISTS_UPDATE = "api/lists/{0}/"
API_LISTS_PARTIAL_UPDATE = "lists/{0}/" API_LISTS_PARTIAL_UPDATE = "api/lists/{0}/"
API_LISTS_DELETE = "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): class UserApi(object):
@@ -61,11 +62,23 @@ class UserApi(object):
""" """
token = UserApi._raise_or_return_( 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) self.token = SimpleNamespace(**token)
return self.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): def lists_list(self, **argv):
""" """
List all the exsiting to-do lists. 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): def todo_items_list(self, **argv):
""" """
List all the exsiting to-do items. 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"): # def create(self, title="Untitled"):
# """ # """
# Create a new to-do list # Create a new to-do list
@@ -215,4 +326,8 @@ class UserApi(object):
@staticmethod @staticmethod
def _raise_or_return_(response): def _raise_or_return_(response):
response.raise_for_status() response.raise_for_status()
return response.json() try:
return response.json()
except Exception as e:
print(e)
return response.content

View File

@@ -1,48 +1,50 @@
import random
from user import User from user import User
def print_lists(lists): def print_lists(lists):
for item in 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" DEFAULT_URL = "http://127.0.0.1:8000"
user = User(url=DEFAULT_URL) user = User(url=DEFAULT_URL)
user.auth("root", "root") user.auth("root", "root")
user.refresh()
# Fetch existing lists: # Fetch existing lists:
lists = user.fetchUserLists() print_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: # Append a new list to user:
print("Appending list...") print("Appending list...")
scroll = user.appendUserList(title="a new list!") scroll = user.appendUserList(title="a new list!")
print_lists(lists) print_lists(user.fetchUserLists())
# Modify list 0: # Modify list 0:
print("Modifyng list...") print("Modifyng list...")
lists[0].modify(title="A new title") user.lists_[0].modify(title=f"A new title{random.random()}")
print_lists(lists) print_lists(user.fetchUserLists())
# Append item to list: # Append item to list:
print("Appending item to last list...") print("Appending item to last list...")
item = lists[-1].append(text="this is an item") item = user.lists_[-1].append(text="this is an item")
print_lists(lists) print_lists(user.fetchUserLists())
# Modifying item # Modifying item
print("Modifyng appended item...") print("Modifyng appended item...")
item.modify(finished=True, text="this is an updated item") item.modify(finished=True, text="this is an updated item")
print_lists(lists) print_lists(user.fetchUserLists())
# Removing item at 0 # Removing item at from last list
print("Removing item 0 from list 0...") print("Removing the last item from the last list...")
lists[0].remove(0) user.lists_[-1].remove(-1)
print_lists(lists) 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())

View File

@@ -38,6 +38,13 @@ class LoginFrame(tk.Frame):
print(ex) print(ex)
message.invalid_login() message.invalid_login()
# Если захочется реализовать в логине
"""
@property
def remember(self):
return self.rbtn_var.get()
"""
def initAUTH(self) -> None: def initAUTH(self) -> None:
""" """
Создает окно авторизации программы Создает окно авторизации программы
@@ -66,3 +73,16 @@ class LoginFrame(tk.Frame):
# Кнопка авториазции # Кнопка авториазции
btn = tk.Button(self, text="Войти", command=self.login_clicked) btn = tk.Button(self, text="Войти", command=self.login_clicked)
btn.grid(row=14, column=12, columnspan=3, rowspan=1, sticky="nsew") 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")
"""

View File

@@ -4,13 +4,14 @@ import sys
import tkinter as tk import tkinter as tk
from login import LoginFrame from login import LoginFrame
from workspace import WorkSpaceFrame from workspace import WorkSpaceFrame
from user import User
if "win" in sys.platform.lower(): if "win" in sys.platform.lower():
DEFAULT_URL = "http://localhost:8000" DEFAULT_URL = "http://localhost:8000"
else: else:
DEFAULT_URL = "http://0.0.0.0:8000" DEFAULT_URL = "http://0.0.0.0:8000"
BASE_W = 580 BASE_W = 600
BASE_H = 400 BASE_H = 400
TITLE_APP = "ToDo Application" TITLE_APP = "ToDo Application"
@@ -24,11 +25,22 @@ class Application(tk.Tk):
def login(self): 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) self.frame = LoginFrame(master=self, url=DEFAULT_URL)
while not self.frame.loggedIn: while not self.frame.loggedIn:
self.update_idletasks() self.update_idletasks()
self.update() self.update()
self.frame.destroy() self.frame.destroy()
# Нужно запомнить пользователя
# if self.frame.remember:
# self.frame.user.save()
return self.frame.user return self.frame.user
def main(self, user): def main(self, user):

View File

@@ -1,85 +1,140 @@
import os import os
import random
from datetime import datetime from datetime import datetime
from types import SimpleNamespace
from pathlib import Path
import json
from api import UserApi 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): 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.id = id
self.title = title self.title = title
self.items = items self.items_ = [] if items is None else items
self.created_at = created_at self.created_at = date_or_str(created_at)
self.user = user
def __iter__(self): def __iter__(self):
for item in self.items: for item in self.items_:
yield item yield item
def __getitem__(self, index): def __getitem__(self, index):
return self.items[index] return self.items_[index]
def __len__(self): def __len__(self):
return len(self.items) return len(self.items_)
def __str__(self): def __str__(self):
return f"[{self.id}] {self.title}" return f"[{self.id}] {self.title}"
def index(self, value): def index(self, value):
return self.items.index(value) return self.items_.index(value)
# ToDo
def remove(self, index): 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): def append(self, text):
item = ToDoItem(id=None, text=text, created_at=datetime.now()) """
self.items.append(item) Add a new item to db
item.sync() """
self.sync() if "DEBUG" in os.environ:
return item 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): 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(): for key, value in argv.items():
setattr(self, key, value) setattr(self, key, value)
self.sync() 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): def sync(self):
# ToDo send request or store in form
print(f"Item '{self}' is being synchronized...") 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): 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.id = id
self.text = text self.text = text
self.finished = finished 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): def __str__(self):
return f"[{self.id}] {self.text}" return f"[{self.id}] {self.text}"
def modify(self, **argv): 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(): for key, value in argv.items():
setattr(self, key, value) setattr(self, key, value)
self.sync() self.sync()
# ToDo def dispose(self):
def sync(self): print(f"To-do item id '{self.id}' is being disposed of...")
# 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: if "DEBUG" in os.environ:
return return
UserApi.auth(self, user, passwd) self.user.todo_items_delete(self.id)
# ToDo def sync(self):
items = [ 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( ToDoList(
id=i, id=i,
title=f"List {i}", title=f"List {i}",
@@ -92,17 +147,106 @@ class User(UserApi):
for i in range(10) 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): 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): 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): def appendUserList(self, title):
item = ToDoList(id=None, title=title, created_at=datetime.now()) """
self.items.append(item) Create a new user list
item.sync() title: title of list to create
return item 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

View File

@@ -61,8 +61,9 @@ class ToDoItemWidget(tk.Frame):
class ToDoListWidget(tk.Frame): class ToDoListWidget(tk.Frame):
def __init__(self, *args, **argv): def __init__(self, *args, delete_list, **argv):
super().__init__(*args, **argv) super().__init__(*args, **argv)
self.delete_list = delete_list
def fill(self, itemList): def fill(self, itemList):
@@ -82,7 +83,7 @@ class ToDoListWidget(tk.Frame):
add = tk.Button(self, text="Добавить заметку", command=self.add_command) add = tk.Button(self, text="Добавить заметку", command=self.add_command)
add.pack(side="top") add.pack(side="top")
delete = tk.Button(self, text="Удалить лист", command=placeholder) delete = tk.Button(self, text="Удалить лист", command=self.delete_list)
delete.pack(side="top") delete.pack(side="top")
def update(self, itemList=None): def update(self, itemList=None):
@@ -106,6 +107,17 @@ class ToDoListWidget(tk.Frame):
class WorkSpaceFrame(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: 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.pack(fill=tk.BOTH, expand=1)
self.initLayout(user) 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): 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 # data
self.lists = user.fetchUserLists() self.lists = user.fetchUserLists()
text = tk.Text(self, width=15, height=1) self.add_list_text = tk.Text(self, width=15, height=1)
text.pack(anchor="sw") 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") add.pack(anchor="sw")
# select list box # select list box
@@ -142,22 +166,54 @@ class WorkSpaceFrame(tk.Frame):
# add scroll bar to list box # add scroll bar to list box
self.listBox.config(yscrollcommand=scrollbar.set) 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("<Configure>", 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: for item in self.lists:
s = f"{str(item)}: {item.created_at.strftime('%Y-%m-%d %H:%M:%S')}" s = f"{str(item)}: {item.created_at.strftime('%Y-%m-%d %H:%M:%S')}"
self.listBox.insert(tk.END, s) self.listBox.insert(tk.END, s)
self.listBox.pack() self.listBox.pack()
len(self.lists) > 0 and self.listBox.selection_set(first=0) return len(self.lists)
# todo lists def add_list(self, *args):
self.toToList = ToDoListWidget(self) text = self.add_list_text.get(1.0, "end").strip()
self.toToList.pack(side="left", fill="both", expand=1) 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): def listBox_selected(self, *args):
self.toDoList.clear()
self.toToList.clear()
selection = self.listBox.curselection() selection = self.listBox.curselection()
cur = selection[0] cur = selection[0]
self.toToList.fill(self.lists[cur]) self.toDoList.fill(self.lists[cur])