28 Commits

Author SHA1 Message Date
4830651886 fix: GUI увеличили 2021-04-29 00:11:16 +03:00
Aleksey Lobanov
bd1d2c65b9 Merge pull request #23 from AlekseyLobanov/feat_9.functional_api
Implemented most of the api placeholders
2021-04-28 23:02:43 +03:00
Aleksey Lobanov
07e4f56061 Merge pull request #24 from AlekseyLobanov/tests
Tests
2021-04-28 23:02:35 +03:00
Ivan
7afb99d3d0 Добавил скроллбар в представление айтемов 2021-04-28 18:08:07 +03:00
Ivan
3d779590e9 Функциональная кнопка 2021-04-28 17:53:38 +03:00
Derinhelm
9152cc9772 Исправлены недочеты в оформлении 2021-04-28 15:47:08 +03:00
Ivan
5ddadac47c Пофиксил апи сохранения токенов и режим отладки 2021-04-28 14:46:28 +03:00
fdd40d4592 feat: Добавлено вычисления покрытия для тестов бекенда 2021-04-27 23:13:16 +03:00
0ee81d4b42 feat: Используем pytest для бекенда 2021-04-27 23:12:51 +03:00
Derinhelm
bba4be6b27 В README добавлена команда для запуска тестов 2021-04-27 20:38:49 +03:00
Ivan
277f1e1aff Добавлено сохранение/восстановление токенов 2021-04-27 19:59:31 +03:00
Derinhelm
4ab5f11dd2 Перегруппированы тесты 2021-04-27 17:52:16 +03:00
Ivan
3e307506e9 Косметические правки 2021-04-27 17:05:55 +03:00
Derinhelm
e3b93f3524 Добавлены тесты для Item 2021-04-26 23:35:45 +03:00
Ivan
c9610f7765 Косметические изменения в коде 2021-04-26 22:50:40 +03:00
Ivan
b2b447e392 Implemented most of the api placeholders 2021-04-26 22:42:56 +03:00
Aleksey Lobanov
7ba9f228b7 Merge pull request #22 from AlekseyLobanov/feat_8.frontend_development
Feat 8.frontend development
2021-04-26 15:37:09 +03:00
Derinhelm
a6a31c8c10 Добавлены простейшие тесты на создание Item 2021-04-26 12:57:47 +03:00
Derinhelm
889acf6c45 Добавлены тесты для ToDoList 2021-04-26 11:46:01 +03:00
Derinhelm
7e02dff184 Добавлен тест на простейшее создание ToDoList
Для запуска docker-compose exec web python manage.py test
2021-04-25 21:58:27 +03:00
Derinhelm
61bb90540e Добавлены тесты 2021-04-25 21:58:27 +03:00
LazIvanS
8b52f3bc74 Merge pull request #21 from AlekseyLobanov/feat8
Небольшие исправления и добавление кнопок
2021-04-22 21:28:04 +03:00
Ivan
b6eb0b6d30 Небольшие исправления и добавление кнопок 2021-04-22 21:25:40 +03:00
Ivan
66beba6b0d Добавил базовый графический интерфейс с частью функций. Для запуска необходима переменная среды DEBUG, пока что работаем на игровых данных - без бэкэнда. 2021-04-22 20:35:48 +03:00
Ivan
5836783e69 Небольшое исправление 2021-04-18 16:16:19 +03:00
Ivan
0d1e321b7e Добавлено api для работы со списками и задачами 2021-04-18 16:07:59 +03:00
unknown
6bf6f9d3f1 Исправления стиля, поддержка базовой документации, соответствующей текущим запросам 2021-04-15 20:31:24 +03:00
unknown
633326ac89 Добавлен базовый api для frontend 2021-04-14 20:26:06 +03:00
16 changed files with 1264 additions and 97 deletions

1
.gitignore vendored
View File

@@ -65,6 +65,5 @@ target/
*.jsl *.jsl
*.db *.db
tmp* tmp*
test_*
.env* .env*
venv* venv*

View File

@@ -53,3 +53,8 @@ docker-compose up
```bash ```bash
docker-compose exec web python manage.py makemigrations backend docker-compose exec web python manage.py makemigrations backend
``` ```
Для запуска тестов использовать
```bash
docker-compose run -e DJANGO_SETTINGS_MODULE=backend.settings web pytest --cov=backend
```

3
backend/.coveragerc Normal file
View File

@@ -0,0 +1,3 @@
[run]
omit =
backend/migrations/*

View File

@@ -28,7 +28,7 @@ DEBUG = True
ALLOWED_HOSTS = [] ALLOWED_HOSTS = []
if DEBUG: if DEBUG:
ALLOWED_HOSTS = ["0.0.0.0", "localhost", "127.0.0.1"] ALLOWED_HOSTS = ["0.0.0.0", "localhost", "127.0.0.1", "ALLOWED_HOSTS", "testserver"]
# Application definition # Application definition

View File

@@ -13,13 +13,13 @@ from drf_yasg import openapi
from .api import router from .api import router
schema_view = get_schema_view( schema_view = get_schema_view(
openapi.Info( openapi.Info(
title="ToDo List", title="ToDo List",
default_version='v1', default_version="v1",
description="Swagger Interface for ToDo List", description="Swagger Interface for ToDo List",
), ),
public=True, public=True,
permission_classes=(permissions.AllowAny,), permission_classes=(permissions.AllowAny,),
) )
urlpatterns = [ urlpatterns = [
@@ -28,5 +28,5 @@ urlpatterns = [
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("api/", include(router.urls)), path("api/", include(router.urls)),
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), 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"),
] ]

View File

@@ -1,3 +1,5 @@
pytest==6.2.3
pytest-cov==2.11.1
djangorestframework==3.12.4 djangorestframework==3.12.4
django-filter==2.4.0 django-filter==2.4.0
markdown==3.3.4 markdown==3.3.4

View File

@@ -0,0 +1,3 @@
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

154
backend/tests/test_item.py Normal file
View File

@@ -0,0 +1,154 @@
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User
from .test_todo import create_todo
class ItemTest(APITestCase):
"""Tests API for items."""
def prepare(self):
user = User.objects.create_user("test_user4", "test@test.com", "test_password")
self.client.force_authenticate(user=user)
to_do_id_1 = create_todo(self.client, "ToDoList1").data["id"]
to_do_id_2 = create_todo(self.client, "ToDoList2").data["id"]
return to_do_id_1, to_do_id_2
def get(self, expected_titles, todo_id=None, finished=None):
url = reverse("ToDoItems-list")
data = {}
if finished is not None:
data["finished"] = finished
if todo_id is not None:
data["parent"] = todo_id
response = self.client.get(url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
real_titles = [(d["text"], d["parent"]) for d in response.data["results"]]
self.assertEqual(real_titles, expected_titles)
if finished is not None:
item_status = [data["finished"] for data in response.data["results"]]
self.assertEqual(finished, all(item_status))
def post(self, item_text, todo_id, finished=None):
url = reverse("ToDoItems-list")
if finished is not None:
data = {"text": item_text, "parent": todo_id, "finished": finished}
else:
data = {"text": item_text, "parent": todo_id}
response = self.client.post(url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
check_finished = False if (finished is None) else finished
self.assertEqual(response.data["text"], item_text)
self.assertEqual(response.data["parent"], todo_id)
self.assertEqual(response.data["finished"], check_finished)
return response.data["id"], response.data["finished"]
def get_by_id(self, id, text, finished, parent):
url_with_id = reverse("ToDoItems-detail", args=(id,))
response = self.client.get(url_with_id, {id: id}, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["text"], text)
self.assertEqual(response.data["finished"], finished)
self.assertEqual(response.data["parent"], parent)
def put(self, id, text, parent, finished=None):
url_with_id = reverse("ToDoItems-detail", args=(id,))
data = {"text": text, "parent": parent}
if finished is not None:
data["finished"] = finished
response = self.client.put(url_with_id, data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["text"], text)
self.assertEqual(response.data["parent"], parent)
if finished is not None:
self.assertEqual(response.data["finished"], finished)
def patch(self, id, text=None, finished=None, parent=None):
url_with_id = reverse("ToDoItems-detail", args=(id,))
data = {}
if text is not None:
data["text"] = text
if finished is not None:
data["finished"] = finished
if parent is not None:
data["parent"] = parent
response = self.client.patch(url_with_id, data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
if text is not None:
self.assertEqual(response.data["text"], text)
if finished is not None:
self.assertEqual(response.data["finished"], finished)
if parent is not None:
self.assertEqual(response.data["parent"], parent)
def delete(self, id, title, finished, to_do_id):
self.get_by_id(id, title, finished, to_do_id)
url_with_id = reverse("ToDoItems-detail", args=(id,))
response = self.client.delete(url_with_id, {}, format="json")
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
def test_create_delete(self):
"""
/todo_items/: get, post (create)
/todo_items/{id}/: get (read), delete
"""
to_do_id_1, to_do_id_2 = self.prepare()
self.get([], to_do_id_1)
item_text_1, item_text_2, item_text_3, item_text_4 = "Item1", "Item2", "Item3", "Item4"
item_id_1, item_finished_1 = self.post(item_text_1, to_do_id_1)
self.get([(item_text_1, to_do_id_1)], to_do_id_1)
item_id_2, item_finished_2 = self.post(item_text_2, to_do_id_1, finished=False)
self.get([(item_text_1, to_do_id_1), (item_text_2, to_do_id_1)], to_do_id_1)
item_id_3, item_finished_3 = self.post(item_text_3, to_do_id_1, finished=True)
self.get(
[(item_text_1, to_do_id_1), (item_text_2, to_do_id_1), (item_text_3, to_do_id_1)],
to_do_id_1,
)
item_id_4, item_finished_4 = self.post(item_text_4, to_do_id_2, finished=False)
self.get(
[
(item_text_1, to_do_id_1),
(item_text_2, to_do_id_1),
(item_text_3, to_do_id_1),
(item_text_4, to_do_id_2),
]
)
self.get(
[(item_text_1, to_do_id_1), (item_text_2, to_do_id_1), (item_text_3, to_do_id_1)],
to_do_id_1,
)
self.get([(item_text_1, to_do_id_1), (item_text_2, to_do_id_1)], to_do_id_1, finished=False)
self.get([(item_text_3, to_do_id_1)], to_do_id_1, finished=True)
self.get_by_id(item_id_1, item_text_1, item_finished_1, to_do_id_1)
self.get_by_id(item_id_2, item_text_2, item_finished_2, to_do_id_1)
self.get_by_id(item_id_3, item_text_3, item_finished_3, to_do_id_1)
self.delete(item_id_3, item_text_3, item_finished_3, to_do_id_1)
self.get([(item_text_1, to_do_id_1), (item_text_2, to_do_id_1)], to_do_id_1)
def test_update(self):
"""
/todo_items/{id}/: put (update), patch (partial_update)
"""
to_do_id_1, to_do_id_2 = self.prepare()
item_text_1 = "Item1"
item_id_1, item_finished_1 = self.post(item_text_1, to_do_id_1)
item_text_1_2 = "Item5"
self.put(item_id_1, item_text_1_2, to_do_id_2)
self.put(item_id_1, item_text_1_2, to_do_id_2, finished=False)
self.put(item_id_1, item_text_1_2, to_do_id_2, finished=True)
item_text_1_3 = "Item6"
self.patch(item_id_1, parent=to_do_id_1)
self.patch(item_id_1, finished=True)
self.patch(item_id_1, text=item_text_1_3)

View File

@@ -0,0 +1,85 @@
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User
def create_todo(client, title):
url = reverse("ToDoLists-list")
response = client.post(url, {"title": title}, format="json")
return response
class ToDoTest(APITestCase):
"""Tests API for todo."""
def get(self, expected_titles):
url = reverse("ToDoLists-list")
response = self.client.get(url, {}, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
real_titles = [data["title"] for data in response.data["results"]]
self.assertEqual(real_titles, expected_titles)
def post(self, to_do_title):
response = create_todo(self.client, to_do_title)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["title"], to_do_title)
return response.data["id"]
def get_by_id(self, id, expected_title):
url_with_id = reverse("ToDoLists-detail", args=(id,))
response = self.client.get(url_with_id, {id: id}, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["title"], expected_title)
def put(self, id, new_title):
url_with_id = reverse("ToDoLists-detail", args=(id,))
response = self.client.put(url_with_id, {"title": new_title}, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["title"], new_title)
def patch(self, id, new_title):
url_with_id = reverse("ToDoLists-detail", args=(id,))
response = self.client.patch(url_with_id, {"title": new_title}, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["title"], new_title)
def delete(self, id, title):
self.get_by_id(id, title)
url_with_id = reverse("ToDoLists-detail", args=(id,))
response = self.client.delete(url_with_id, {}, format="json")
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
def prepare(self):
user = User.objects.create_user("test_user", "test@test.com", "test_password")
self.client.force_authenticate(user=user)
def test_create_delete(self):
"""
lists/{id}/: put (update), patch (partial_update)
"""
self.prepare()
to_do_title_1 = "ToDoList1"
to_do_id1 = self.post(to_do_title_1)
self.put(to_do_id1, "ToDoList11")
self.patch(to_do_id1, "ToDoList12")
def test_todo(self):
"""
lists/: get, post
lists/{id}/: get, delete
"""
self.prepare()
self.get([])
to_do_title_1, to_do_title_2 = "ToDoList1", "ToDoList2"
to_do_id1 = self.post(to_do_title_1)
self.get([to_do_title_1])
to_do_id2 = self.post(to_do_title_2)
self.get([to_do_title_1, to_do_title_2])
self.get_by_id(to_do_id1, to_do_title_1)
self.get_by_id(to_do_id2, to_do_title_2)
self.delete(to_do_id2, to_do_title_2)
self.get([to_do_title_1])

333
frontend/api.py Normal file
View File

@@ -0,0 +1,333 @@
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 = "api/lists/{0}/"
API_LISTS_UPDATE = "api/lists/{0}/"
API_LISTS_PARTIAL_UPDATE = "api/lists/{0}/"
API_LISTS_DELETE = "api/lists/{0}/"
API_TOKEN_CREATE = "api/token/"
API_TOKEN_REFRESH = "api/token/refresh/"
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_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.
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 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.
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 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
# 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()
try:
return response.json()
except Exception as e:
print(e)
return response.content

50
frontend/api_demo.py Normal file
View File

@@ -0,0 +1,50 @@
import random
from user import User
def print_lists(lists):
for item in lists:
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:
print_lists(user.fetchUserLists())
# Append a new list to user:
print("Appending list...")
scroll = user.appendUserList(title="a new list!")
print_lists(user.fetchUserLists())
# Modify list 0:
print("Modifyng list...")
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 = 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(user.fetchUserLists())
# 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())

88
frontend/login.py Normal file
View File

@@ -0,0 +1,88 @@
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()
# Если захочется реализовать в логине
"""
@property
def remember(self):
return self.rbtn_var.get()
"""
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")
# Если захочется реализовать в логине
"""
# Запомнить пользователя
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")
"""

20
frontend/message.py Normal file
View File

@@ -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)

View File

@@ -1,113 +1,67 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys import sys
import tkinter as tk import tkinter as tk
from tkinter import messagebox as mb from login import LoginFrame
from workspace import WorkSpaceFrame
from user import User 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 = 1600
BASE_H = 400 BASE_H = 600
TITLE_APP = "ToDo Application"
class Application(tk.Frame): class Application(tk.Tk):
def __init__(self, def __init__(self):
master=None super().__init__()
) -> None: self.center_window()
""" self.title(TITLE_APP)
Функция инициаизации класса
"""
super().__init__(master)
#Иницализируем параметры окна def login(self):
self.master = master """Возвращает пользователя - его можно потом сериализовать"""
self.pack(fill=tk.BOTH, expand=1) # Пользователь сохранен! Авторизация не нужна!
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
self.grid(row=0, column=0, sticky=tk.N + tk.S + tk.E + tk.W) def main(self, user):
tk.Grid.rowconfigure(master, 0, weight=1) self.frame = WorkSpaceFrame(master=self, user=user)
tk.Grid.columnconfigure(master, 0, weight=1) self.mainloop()
# Иницализируем параметры пользователя def center_window(self, width: str = BASE_W, heigh: str = BASE_H) -> None:
self.user = User(url=DEFAULT_URL)
# Настраиваем размеры и включаем иницализацию
self.centerWindow()
self.initAUTH()
def centerWindow(self,
width: str = BASE_W,
heigh: str = BASE_H
) -> None:
""" """
Центрирует приложение по центру экрана Центрирует приложение по центру экрана
:param width: ширина окна :param width: ширина окна
:param heigh: высота окна :param heigh: высота окна
""" """
sw = self.master.winfo_screenwidth() sw = self.winfo_screenwidth()
sh = self.master.winfo_screenheight() sh = self.winfo_screenheight()
x = (sw - width) / 2 x = (sw - width) / 2
y = (sh - heigh) / 2 y = (sh - heigh) / 2
self.master.geometry('%dx%d+%d+%d' % (width, heigh, x, y)) self.geometry("%dx%d+%d+%d" % (width, heigh, x, y))
def login_clicked(self) -> None:
"""
Функция авторизации
"""
try:
self.user.auth(self.login.get(), self.password.get())
except Exception as ex:
print(ex)
self.show_info()
def show_info(selfб,
msg: str = None
) -> None:
"""
Показывает передаваемое сообщение в messagebox
:param msg: передаваемое сообщение
"""
if msg is None:
msg = "Неправильный логин или пароль"
mb.showinfo("Информация", msg)
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")
if __name__ == "__main__": if __name__ == "__main__":
master = tk.Tk() app = Application()
app = Application(master) app.main(app.login())
app.master.title("ToDo")
app.mainloop()

252
frontend/user.py Normal file
View File

@@ -0,0 +1,252 @@
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=None, parent=None, user=None):
self.id = id
self.title = title
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_:
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)
def remove(self, index):
"""
Remove item AT INDEX from db
"""
item = self.items_[index]
self.items_.remove(item)
item.dispose()
def append(self, text):
"""
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()
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):
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, user=None):
self.id = id
self.text = text
self.finished = finished
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()
def dispose(self):
print(f"To-do item id '{self.id}' is being disposed of...")
if "DEBUG" in os.environ:
return
self.user.todo_items_delete(self.id)
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}",
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)
]
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):
"""
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_
def removeUserList(self, 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_
def appendUserList(self, title):
"""
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

219
frontend/workspace.py Normal file
View File

@@ -0,0 +1,219 @@
import tkinter as tk
def str_time(time):
return time.strftime("%Y-%m-%d %H:%M:%S")
TODO_ITEM_TABLE_TEXT_WIDTH = 24
TODO_ITEM_TABLE_FINISHED_WIDTH = 12
TODO_ITEM_TABLE_CREATED_AT_WIDTH = 24
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, delete_list, **argv):
super().__init__(*args, **argv)
self.delete_list = delete_list
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=self.delete_list)
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 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:
"""
Функция инициаизации класса
"""
super().__init__(master)
self.master = master
self.user = user
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()
self.add_list_text = tk.Text(self, width=25, height=1)
self.add_list_text.pack(anchor="sw")
add = tk.Button(self, text="Добавить лист", command=self.add_list)
add.pack(anchor="sw")
# select list box
self.listBox = tk.Listbox(self, width=40, selectmode=tk.SINGLE)
self.listBox.pack(side="left", fill="y")
self.listBox.bind("<<ListboxSelect>>", 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)
# 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:
s = f"{str(item)}: {item.created_at.strftime('%Y-%m-%d %H:%M:%S')}"
self.listBox.insert(tk.END, s)
self.listBox.pack()
return len(self.lists)
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.toDoList.clear()
selection = self.listBox.curselection()
cur = selection[0]
self.toDoList.fill(self.lists[cur])