38 Commits

Author SHA1 Message Date
moscap
d27066b15b Merge pull request #27 from AlekseyLobanov/fix
Исправлен баг потери локализации
2021-06-08 10:31:49 +03:00
Derinhelm
cac9d14d52 Исправлен баг потери локализации 2021-06-08 10:29:22 +03:00
Derinhelm
57f2f72856 Merge pull request #25 from AlekseyLobanov/add_wheels_to_project
Add wheels to project
2021-06-08 09:48:20 +03:00
moscap
f8bebe8115 add some automatization and localizatipon into wheels 2021-06-08 07:55:11 +03:00
moscap
f714f5ad1d Merge branch 'master' of https://github.com/AlekseyLobanov/django-todo into add_wheels_to_project 2021-06-08 06:57:43 +03:00
moscap
437b97b005 Merge pull request #26 from AlekseyLobanov/feat_11
Добавлена локализация
2021-06-08 06:51:03 +03:00
Derinhelm
16c151a1f3 Изменен интейрфейс 2021-06-07 20:47:42 +03:00
Derinhelm
bb8e1365bb Изменен путь к директории po 2021-06-07 16:36:01 +03:00
Derinhelm
fbd033e6ef Добавлена локализация
Перед запуском программы сгенерировать mo-файл
pybabel compile -D todo -i po/eng/LC_MESSAGES/todo.po -o po/eng/LC_MESSAGES/todo.mo
2021-06-07 00:38:53 +03:00
moscap
4fdd63b234 lint improvements 2021-06-02 14:16:35 +03:00
moscap
75e1d5c13e add deployment configs and tools 2021-06-02 14:11:10 +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
f43c652671 hotfix: Исправлено добавление списков и их элементов из API 2021-04-18 12:24:12 +03:00
Aleksey Lobanov
4886e2e9be Merge pull request #20 from AlekseyLobanov/feat_4.base_api
Добавлено базовое API + finished #4 #17
2021-04-17 15:01:03 +03:00
3f5083f8e4 feat: Реализовано базовое простое API
Минимальное, но самодостаточное
2021-04-17 15:00:04 +03:00
0f2f291c18 feat: Добавлена пагинация для API 2021-04-17 14:59:26 +03:00
8a26910651 db: Больше индексов для важных полей 2021-04-17 14:59:08 +03:00
64607efaf3 feat: Новая модель в админке и добавления поля finished 2021-04-17 14:58:49 +03:00
6352deda3c feat: Поддержка фильтрации на стороне Django 2021-04-17 14:57:38 +03:00
28 changed files with 1064 additions and 164 deletions

6
.gitignore vendored
View File

@@ -65,6 +65,10 @@ target/
*.jsl
*.db
tmp*
test_*
.env*
venv*
# Doit
*.bak
*.dat
*.dir

View File

@@ -15,4 +15,4 @@ repos:
rev: 3.9.0
hooks:
- id: flake8
args: ["--ignore=E203,W503,FI10,FI11,FI12,FI13,FI14,FI15,FI16,FI17,FI58,E501"]
args: ["--ignore=E203,W503,FI10,FI11,FI12,FI13,FI14,FI15,FI16,FI17,FI58,E501", "--builtins=_"]

View File

@@ -32,8 +32,14 @@
## Как запустить проект
### Frontend
Интерфейс на русском языке
```bash
python3 todo_tk.py
python3 -m frontend
```
Интерфейс на английском языке
```bash
LANG=eng python3 -m frontend
```
### backend
@@ -53,3 +59,8 @@ docker-compose up
```bash
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

@@ -1,6 +1,6 @@
from django.contrib import admin
from .models import ToDoList
from .models import ToDoList, ToDoItem
class ToDoListAdmin(admin.ModelAdmin):
@@ -9,4 +9,11 @@ class ToDoListAdmin(admin.ModelAdmin):
list_editable = ["title"]
class ToDoItemAdmin(admin.ModelAdmin):
model = ToDoItem
list_display = ["parent", "finished", "text", "created_at"]
list_editable = ["finished", "text"]
admin.site.register(ToDoList, ToDoListAdmin)
admin.site.register(ToDoItem, ToDoItemAdmin)

View File

@@ -1,20 +1,62 @@
from rest_framework import viewsets, serializers, permissions
from rest_framework import routers
from django_filters.rest_framework import DjangoFilterBackend
from .models import ToDoList
from .models import ToDoList, ToDoItem
class ToDoListField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
user = self.context["request"].user
return ToDoList.objects.filter(user=user)
class ToDoItemSerializer(serializers.HyperlinkedModelSerializer):
parent = ToDoListField(many=False, read_only=False, help_text="ID родительского списка")
class Meta:
model = ToDoItem
fields = ["id", "text", "finished", "created_at", "parent"]
class ToDoItemViewSet(viewsets.ModelViewSet):
serializer_class = ToDoItemSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend]
filterset_fields = ["parent", "finished"]
def get_queryset(self):
user = self.request.user
if not user.is_authenticated:
# ветка только для генерации схемы
return ToDoItem.objects.all()
return ToDoItem.objects.filter(parent__user=user)
class ToDoListSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ToDoList
fields = ["title", "created_at"]
fields = ["id", "title", "created_at"]
def create(self, validated_data):
todo_list = ToDoList.objects.create(
user=self.context["request"].user, title=validated_data["title"]
)
return todo_list
class ToDoListViewSet(viewsets.ModelViewSet):
queryset = ToDoList.objects.all()
serializer_class = ToDoListSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
user = self.request.user
if not user.is_authenticated:
# ветка только для генерации схемы
return ToDoList.objects.all()
return ToDoList.objects.filter(user=user)
router = routers.DefaultRouter()
router.register(r"lists", ToDoListViewSet)
router.register(r"lists", ToDoListViewSet, basename="ToDoLists")
router.register(r"todo_items", ToDoItemViewSet, basename="ToDoItems")

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2 on 2021-04-17 11:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("backend", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="todoitem",
name="finished",
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.2 on 2021-04-17 11:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("backend", "0002_todoitem_finished"),
]
operations = [
migrations.AlterField(
model_name="todoitem",
name="created_at",
field=models.DateTimeField(auto_now_add=True, db_index=True),
),
migrations.AlterField(
model_name="todoitem",
name="finished",
field=models.BooleanField(db_index=True, default=False),
),
migrations.AlterField(
model_name="todolist",
name="created_at",
field=models.DateTimeField(auto_now_add=True, db_index=True),
),
]

View File

@@ -5,10 +5,11 @@ from django.contrib.auth.models import User
class ToDoList(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, default=None)
title = models.CharField(max_length=250)
created_at = models.DateTimeField(auto_now_add=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
class ToDoItem(models.Model):
parent = models.ForeignKey(ToDoList, on_delete=models.CASCADE, null=False, default=None)
text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
finished = models.BooleanField(default=False, null=False, db_index=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)

View File

@@ -28,7 +28,7 @@ DEBUG = True
ALLOWED_HOSTS = []
if DEBUG:
ALLOWED_HOSTS = ["0.0.0.0"]
ALLOWED_HOSTS = ["0.0.0.0", "localhost", "127.0.0.1", "ALLOWED_HOSTS", "testserver"]
# Application definition
@@ -45,13 +45,7 @@ INSTALLED_APPS = [
]
SWAGGER_SETTINGS = {
'SECURITY_DEFINITIONS': {
'Bearer': {
'type': 'apiKey',
'name': 'Authorization',
'in': 'header'
}
}
"SECURITY_DEFINITIONS": {"Bearer": {"type": "apiKey", "name": "Authorization", "in": "header"}}
}
MIDDLEWARE = [
@@ -130,7 +124,12 @@ SIMPLE_JWT = {
}
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework_simplejwt.authentication.JWTAuthentication",)
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
"PAGE_SIZE": 100,
}

View File

@@ -15,7 +15,7 @@ from .api import router
schema_view = get_schema_view(
openapi.Info(
title="ToDo List",
default_version='v1',
default_version="v1",
description="Swagger Interface for ToDo List",
),
public=True,
@@ -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"),
]

View File

@@ -1,4 +1,7 @@
pytest==6.2.3
pytest-cov==2.11.1
djangorestframework==3.12.4
django-filter==2.4.0
markdown==3.3.4
appdirs==1.4.4
asgiref==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])

19
dodo.py Normal file
View File

@@ -0,0 +1,19 @@
#!usr/bin/env python3
"""
"""
def task_mo():
"""Create bynary wheel distribution"""
return {
"actions": [
"""pybabel compile -D todo -i frontend/po/eng/LC_MESSAGES/todo.po -o frontend/po/eng/LC_MESSAGES/todo.mo"""
],
"file_dep": ["frontend/po/eng/LC_MESSAGES/todo.po"],
"targets": ["frontend/po/eng/LC_MESSAGES/todo.mo"],
}
def task_wheel():
"""Create bynary wheel distribution"""
return {"actions": ["python3 -m build -w"], "task_dep": ["mo"]}

0
frontend/__init__.py Normal file
View File

8
frontend/__main__.py Normal file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env python3
"""django-todo application launcher"""
from .todo_tk import Application
if __name__ == "__main__":
app = Application()
app.main(app.login())

View File

@@ -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()
try:
return response.json()
except Exception as e:
print(e)
return response.content

View File

@@ -1,48 +1,50 @@
from user import User
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())

View File

@@ -1,6 +1,10 @@
import gettext
import os
import tkinter as tk
from user import User
import message
from .user import User
from . import message
gettext.install("todo", os.path.join(os.path.dirname(__file__), "po"))
class LoginFrame(tk.Frame):
@@ -38,6 +42,13 @@ class LoginFrame(tk.Frame):
print(ex)
message.invalid_login()
# Если захочется реализовать в логине
"""
@property
def remember(self):
return self.rbtn_var.get()
"""
def initAUTH(self) -> None:
"""
Создает окно авторизации программы
@@ -50,19 +61,33 @@ class LoginFrame(tk.Frame):
tk.Grid.columnconfigure(self, columns, weight=1)
# Подпись и поле ввода для логина
login_label = tk.Label(self, text="Введите логин")
t = _("Введите логин")
login_label = tk.Label(self, text=t)
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 = 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 = 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")
"""

View File

@@ -1,8 +1,12 @@
import gettext
import os
from tkinter import messagebox as mb
TITLE_INFO_BOX = "Сообщение!"
MESSAGE_INVALID_LOGIN = "Неправильный логин или пароль"
MESSAGE_EMPTY = "Сдесь могло быть ваше сообщение"
gettext.install("todo", os.path.join(os.path.dirname(__file__), "po"))
TITLE_INFO_BOX = _("Сообщение!")
MESSAGE_INVALID_LOGIN = _("Неправильный логин или пароль")
MESSAGE_EMPTY = _("Сдесь могло быть ваше сообщение")
def infobox(msg: str = None) -> None:

View File

@@ -0,0 +1,83 @@
# English (United States) translations for PROJECT.
# Copyright (C) 2021 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-06-06 23:42+0300\n"
"PO-Revision-Date: 2021-06-07 00:24+0300\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en_US\n"
"Language-Team: en_US <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.8.1\n"
#: todo_tk.py:20
msgid "Приложение для планирования"
msgstr "ToDo Application"
#: message.py:6
msgid "Сообщение!"
msgstr "Message!"
#: message.py:7
msgid "Неправильный логин или пароль"
msgstr "Wrong login or password"
#: message.py:8
msgid "Сдесь могло быть ваше сообщение"
msgstr "This could have been your message"
#: login.py:63
msgid "Введите логин"
msgstr "Enter your username"
#: login.py:71
msgid "Введите пароль"
msgstr "Enter your password"
#: login.py:78
msgid "Войти"
msgstr "Enter"
#: workspace.py:17
msgid "Не реализовано"
msgstr "Not implemented"
#: workspace.py:25
msgid "Текст"
msgstr "Text"
#: workspace.py:28
msgid "Выполнено"
msgstr "Done"
#: workspace.py:31
msgid "Создано"
msgstr "Created"
#: workspace.py:60
msgid "Удалить"
msgstr "Delete"
#: workspace.py:88
msgid "Добавить заметку"
msgstr "Add note"
#: workspace.py:91
msgid "Удалить лист"
msgstr "Delete list"
#: workspace.py:149
msgid "Запомнить меня"
msgstr "Remember me"
#: workspace.py:158
msgid "Добавить лист"
msgstr "Add list"

View File

@@ -1,19 +1,24 @@
#!/usr/bin/env python3
import gettext
import os
import sys
import tkinter as tk
from login import LoginFrame
from workspace import WorkSpaceFrame
from .login import LoginFrame
from .workspace import WorkSpaceFrame
from .user import User
gettext.install("todo", os.path.join(os.path.dirname(__file__), "po"))
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 = 900
BASE_H = 400
TITLE_APP = "ToDo Application"
TITLE_APP = _("Приложение для планирования")
class Application(tk.Tk):
@@ -24,11 +29,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):
@@ -38,7 +54,6 @@ class Application(tk.Tk):
def center_window(self, width: str = BASE_W, heigh: str = BASE_H) -> None:
"""
Центрирует приложение по центру экрана
:param width: ширина окна
:param heigh: высота окна
"""

View File

@@ -1,85 +1,140 @@
import os
import random
from datetime import datetime
from api import UserApi
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()
"""
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

View File

@@ -1,89 +1,138 @@
import gettext
import os
import tkinter as tk
gettext.install("todo", os.path.join(os.path.dirname(__file__), "po"))
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
TODO_ITEM_TABLE_TEXT_WIDTH = 25
TODO_ITEM_TABLE_FINISHED_WIDTH = 20
TODO_ITEM_TABLE_CREATED_AT_WIDTH = 25
def placeholder():
print("Не реализовано")
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):
def __init__(self, *args, row_number, item, table, **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.noteLabel = tk.Label(
table,
text=item.text,
width=TODO_ITEM_TABLE_TEXT_WIDTH,
justify="center",
font=("Arial", 8),
)
self.noteLabel.grid(row=row_number, column=0)
self.finished = tk.IntVar(value=int(item.finished))
self.finishedButton = tk.Checkbutton(
self,
table,
variable=self.finished,
command=self.finishedButton_command,
width=TODO_ITEM_TABLE_FINISHED_WIDTH,
justify="center",
)
self.finishedButton.pack(side="left")
self.finishedButton.grid(row=row_number, column=1)
self.createdAt = tk.Label(
self, text=str_time(item.created_at), width=TODO_ITEM_TABLE_CREATED_AT_WIDTH
table,
text=str_time(item.created_at),
width=TODO_ITEM_TABLE_CREATED_AT_WIDTH,
justify="center",
)
self.createdAt.pack(side="left")
self.createdAt.grid(row=row_number, column=2)
self.remove = tk.Button(self, text="Удалить", command=lambda: self.parent.remove(self.item))
self.remove.pack(side="left")
self.remove = tk.Button(
table,
text=_("Удалить"),
command=lambda: self.parent.remove(self.item),
justify="center",
)
self.remove.grid(row=row_number, column=3)
def finishedButton_command(self):
self.item.modify(finished=self.finished.get() > 0)
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):
def create_table_header(self, body):
header = ToDoItemWidget.header(self)
header.pack(side="left")
header.pack(side="top", fill="y")
header_font = ("Arial", "10", "bold")
text = tk.Label(
body,
text=_("Текст"),
width=TODO_ITEM_TABLE_TEXT_WIDTH,
justify="center",
font=header_font,
)
text.grid(row=0, column=0)
done = tk.Label(
body,
text=_("Выполнено"),
width=TODO_ITEM_TABLE_FINISHED_WIDTH,
justify="center",
font=header_font,
)
done.grid(row=0, column=1)
created = tk.Label(
body,
text=_("Создано"),
width=TODO_ITEM_TABLE_CREATED_AT_WIDTH,
justify="center",
font=header_font,
)
created.grid(row=0, column=2)
def create_table(self, itemList):
table = tk.LabelFrame(self, relief=tk.GROOVE)
table.grid()
self.create_table_header(table)
self.itemList = itemList
row_number = 1
for item in itemList:
item = ToDoItemWidget(self, item=item)
item.pack(side="top", fill="y")
item = ToDoItemWidget(self, row_number=row_number, item=item, table=table)
row_number += 1
return table
self.itemToAdd = tk.Text(self, width=15, height=1)
self.itemToAdd.pack(side="top")
def create_new_item(self):
table = tk.LabelFrame(self, relief=tk.GROOVE)
table.grid()
self.itemToAdd = tk.Text(table, width=15, height=1)
self.itemToAdd.grid(row=0, column=0)
add = tk.Button(self, text="Добавить заметку", command=self.add_command)
add.pack(side="top")
add = tk.Button(table, text=_("Добавить заметку"), command=self.add_command)
add.grid(row=0, column=1)
return table
delete = tk.Button(self, text="Удалить лист", command=placeholder)
delete.pack(side="top")
def fill(self, itemList):
self.frame = tk.LabelFrame(self, relief=tk.GROOVE)
self.frame.grid(sticky="NEWS")
table = self.create_table(itemList)
table.grid(row=0, column=0)
new = self.create_new_item()
new.grid(row=2, column=0)
delete = tk.Button(self, text=_("Удалить лист"), command=self.delete_list)
delete.grid(row=4, column=0)
def update(self, itemList=None):
self.clear()
@@ -106,6 +155,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 +178,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 +214,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("<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()
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])

View File

@@ -21,3 +21,15 @@ exclude = '''
| profiling
)/
'''
[build-system]
requires = [
"setuptools",
"wheel",
"requests",
"build",
"coverage",
"doit"
]
build-backend = "setuptools.build_meta"

11
setup.cfg Normal file
View File

@@ -0,0 +1,11 @@
[metadata]
name = django-todo
version = 0.0.1
[options]
packages = frontend
install_requires =
requests; python_version >= "3.6"
[options.package_data]
frontend = */*/*/todo.mo