fletable 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fletable/__init__.py +3 -0
- fletable/editable_table.py +237 -0
- fletable/sql_table.py +128 -0
- fletable/utils.py +117 -0
- fletable-0.0.1.dist-info/METADATA +383 -0
- fletable-0.0.1.dist-info/RECORD +8 -0
- fletable-0.0.1.dist-info/WHEEL +5 -0
- fletable-0.0.1.dist-info/top_level.txt +1 -0
fletable/__init__.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import flet as ft
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class ForeignKeyConfig:
|
|
8
|
+
table: str
|
|
9
|
+
id_column: str
|
|
10
|
+
label_column: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class FieldConfig:
|
|
15
|
+
label: str
|
|
16
|
+
foreign_key: ForeignKeyConfig | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EditableTable:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
cursor,
|
|
23
|
+
table_name: str,
|
|
24
|
+
field_mapping: dict[str, FieldConfig | str],
|
|
25
|
+
width: int = 800,
|
|
26
|
+
height: int = 400,
|
|
27
|
+
):
|
|
28
|
+
self.cursor = cursor
|
|
29
|
+
self.table_name = table_name
|
|
30
|
+
# Приводим значения словаря к FieldConfig для типобезопасных подсказок
|
|
31
|
+
self.field_configs: dict[str, FieldConfig] = {
|
|
32
|
+
name: cfg if isinstance(cfg, FieldConfig) else FieldConfig(label=str(cfg))
|
|
33
|
+
for name, cfg in field_mapping.items()
|
|
34
|
+
}
|
|
35
|
+
self.width = width
|
|
36
|
+
self.height = height
|
|
37
|
+
self.dropdown_options = self._generate_dropdown_options()
|
|
38
|
+
self.row_checkboxes: list[tuple[ft.Checkbox, dict]] = [] # (checkbox, row_data)
|
|
39
|
+
self.header_checkbox: ft.Checkbox = None
|
|
40
|
+
|
|
41
|
+
def _generate_dropdown_options(self):
|
|
42
|
+
options = {}
|
|
43
|
+
for field, cfg in self.field_configs.items():
|
|
44
|
+
# Настройки FK: явно через FieldConfig.foreign_key или по шаблону *_id
|
|
45
|
+
ref_cfg = cfg.foreign_key
|
|
46
|
+
if ref_cfg or (field.endswith("_id") and field != f"{self.table_name}_id"):
|
|
47
|
+
ref_table = ref_cfg.table if ref_cfg else field.replace("_id", "")
|
|
48
|
+
id_column = ref_cfg.id_column if ref_cfg else field
|
|
49
|
+
label_column = ref_cfg.label_column if ref_cfg else ref_table
|
|
50
|
+
try:
|
|
51
|
+
self.cursor.execute(
|
|
52
|
+
f"SELECT {id_column}, {label_column} FROM {ref_table}"
|
|
53
|
+
)
|
|
54
|
+
results = self.cursor.fetchall()
|
|
55
|
+
options[field] = [(str(row[0]), str(row[1])) for row in results]
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"[WARN] Не удалось загрузить dropdown для {field}: {e}")
|
|
58
|
+
return options
|
|
59
|
+
|
|
60
|
+
def create_add_form(self):
|
|
61
|
+
new_fields = {}
|
|
62
|
+
input_controls = []
|
|
63
|
+
|
|
64
|
+
for field in list(self.field_configs.keys())[1:]: # Пропускаем ID
|
|
65
|
+
if field in self.dropdown_options:
|
|
66
|
+
ctrl = ft.Dropdown(
|
|
67
|
+
options=[
|
|
68
|
+
ft.dropdown.Option(key=str(k), text=str(v))
|
|
69
|
+
for k, v in self.dropdown_options[field]
|
|
70
|
+
],
|
|
71
|
+
value=None,
|
|
72
|
+
expand=True,
|
|
73
|
+
label=self.field_configs[field].label,
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
ctrl = ft.TextField(label=self.field_configs[field].label, expand=True)
|
|
77
|
+
|
|
78
|
+
new_fields[field] = ctrl
|
|
79
|
+
input_controls.append(ctrl)
|
|
80
|
+
|
|
81
|
+
def handle_add():
|
|
82
|
+
try:
|
|
83
|
+
fields = ", ".join(new_fields.keys())
|
|
84
|
+
placeholders = ", ".join(["%s"] * len(new_fields))
|
|
85
|
+
values = [ctrl.value for ctrl in new_fields.values()]
|
|
86
|
+
insert_query = (
|
|
87
|
+
f"INSERT INTO {self.table_name} ({fields}) VALUES ({placeholders})"
|
|
88
|
+
)
|
|
89
|
+
self.cursor.execute(insert_query, values)
|
|
90
|
+
self.cursor.connection.commit()
|
|
91
|
+
for ctrl in input_controls:
|
|
92
|
+
ctrl.value = ""
|
|
93
|
+
print("[INFO] Запись добавлена:", values)
|
|
94
|
+
return True, "Успешно добавлено"
|
|
95
|
+
except Exception as ex:
|
|
96
|
+
print("[ERROR] Ошибка добавления:", str(ex))
|
|
97
|
+
return False, f"Ошибка: {str(ex)}"
|
|
98
|
+
|
|
99
|
+
form_row = ft.Row(input_controls)
|
|
100
|
+
return form_row, handle_add
|
|
101
|
+
|
|
102
|
+
def create_table(self):
|
|
103
|
+
db_fields = list(self.field_configs.keys())
|
|
104
|
+
query = f"SELECT {', '.join(db_fields)} FROM {self.table_name}"
|
|
105
|
+
self.cursor.execute(query)
|
|
106
|
+
data = self.cursor.fetchall()
|
|
107
|
+
|
|
108
|
+
# Очищаем список чекбоксов перед созданием новой таблицы
|
|
109
|
+
self.row_checkboxes = []
|
|
110
|
+
|
|
111
|
+
# Создаём чекбокс для заголовка (выбрать все)
|
|
112
|
+
def on_header_checkbox_change(e):
|
|
113
|
+
for checkbox, _ in self.row_checkboxes:
|
|
114
|
+
checkbox.value = self.header_checkbox.value
|
|
115
|
+
e.page.update()
|
|
116
|
+
|
|
117
|
+
self.header_checkbox = ft.Checkbox(
|
|
118
|
+
value=False, on_change=on_header_checkbox_change
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
rows = []
|
|
122
|
+
for row in data:
|
|
123
|
+
record_id = row[0]
|
|
124
|
+
cells = []
|
|
125
|
+
field_controls = {}
|
|
126
|
+
|
|
127
|
+
# Собираем данные строки в словарь
|
|
128
|
+
row_data = {field: value for field, value in zip(db_fields, row)}
|
|
129
|
+
|
|
130
|
+
# Создаём чекбокс для строки
|
|
131
|
+
row_checkbox = ft.Checkbox(value=False)
|
|
132
|
+
self.row_checkboxes.append((row_checkbox, row_data))
|
|
133
|
+
cells.append(ft.DataCell(row_checkbox))
|
|
134
|
+
|
|
135
|
+
for field, value in zip(db_fields, row):
|
|
136
|
+
if field == db_fields[0]:
|
|
137
|
+
cells.append(ft.DataCell(ft.Text(str(value))))
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
if field in self.dropdown_options:
|
|
141
|
+
ctrl = ft.Container(
|
|
142
|
+
content=ft.Dropdown(
|
|
143
|
+
options=[
|
|
144
|
+
ft.dropdown.Option(key=str(k), text=v)
|
|
145
|
+
for k, v in self.dropdown_options[field]
|
|
146
|
+
],
|
|
147
|
+
value=str(value),
|
|
148
|
+
expand=True,
|
|
149
|
+
),
|
|
150
|
+
padding=5,
|
|
151
|
+
expand=True,
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
ctrl = ft.Container(
|
|
155
|
+
content=ft.TextField(
|
|
156
|
+
value=str(value), border=ft.InputBorder.NONE, expand=True
|
|
157
|
+
),
|
|
158
|
+
padding=5,
|
|
159
|
+
expand=True,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
field_controls[field] = ctrl.content
|
|
163
|
+
cells.append(ft.DataCell(ctrl))
|
|
164
|
+
|
|
165
|
+
def make_save_callback(record_id, controls):
|
|
166
|
+
def save(e):
|
|
167
|
+
try:
|
|
168
|
+
update_fields = ", ".join(
|
|
169
|
+
f"{field} = %s" for field in controls.keys()
|
|
170
|
+
)
|
|
171
|
+
values = [c.value for c in controls.values()]
|
|
172
|
+
update_query = f"UPDATE {self.table_name} SET {update_fields} WHERE {db_fields[0]} = %s"
|
|
173
|
+
self.cursor.execute(update_query, (*values, record_id))
|
|
174
|
+
self.cursor.connection.commit()
|
|
175
|
+
e.page.open(ft.SnackBar(ft.Text("Изменения сохранены")))
|
|
176
|
+
print(f"[LOG] Updated record {record_id} with values {values}")
|
|
177
|
+
except Exception as ex:
|
|
178
|
+
e.page.open(ft.SnackBar(ft.Text(f"Ошибка: {str(ex)}")))
|
|
179
|
+
e.page.update()
|
|
180
|
+
|
|
181
|
+
return save
|
|
182
|
+
|
|
183
|
+
save_button = ft.IconButton(
|
|
184
|
+
icon=ft.Icons.SAVE,
|
|
185
|
+
tooltip="Сохранить",
|
|
186
|
+
on_click=make_save_callback(record_id, field_controls),
|
|
187
|
+
)
|
|
188
|
+
delete_button = ft.IconButton(
|
|
189
|
+
icon=ft.Icons.DELETE,
|
|
190
|
+
tooltip="Удалить",
|
|
191
|
+
on_click=self._handle_delete(record_id),
|
|
192
|
+
)
|
|
193
|
+
cells.append(ft.DataCell(ft.Row([save_button, delete_button], spacing=0)))
|
|
194
|
+
|
|
195
|
+
rows.append(ft.DataRow(cells=cells))
|
|
196
|
+
|
|
197
|
+
# Колонки: чекбокс + поля из mapping + действия
|
|
198
|
+
columns = (
|
|
199
|
+
[ft.DataColumn(self.header_checkbox)]
|
|
200
|
+
+ [
|
|
201
|
+
ft.DataColumn(ft.Text(self.field_configs[field].label))
|
|
202
|
+
for field in db_fields
|
|
203
|
+
]
|
|
204
|
+
+ [ft.DataColumn(ft.Text("Действия"))]
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return ft.DataTable(
|
|
208
|
+
columns=columns,
|
|
209
|
+
rows=rows,
|
|
210
|
+
# width=self.width - 20
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def get_selected_rows(self) -> list[dict]:
|
|
214
|
+
"""
|
|
215
|
+
Возвращает список словарей с данными выделенных строк.
|
|
216
|
+
Ключи словаря соответствуют полям из field_mapping.
|
|
217
|
+
"""
|
|
218
|
+
selected = []
|
|
219
|
+
for checkbox, row_data in self.row_checkboxes:
|
|
220
|
+
if checkbox.value:
|
|
221
|
+
selected.append(row_data.copy())
|
|
222
|
+
return selected
|
|
223
|
+
|
|
224
|
+
def _handle_delete(self, record_id: int):
|
|
225
|
+
def callback(e):
|
|
226
|
+
try:
|
|
227
|
+
delete_query = f"DELETE FROM {self.table_name} WHERE {list(self.field_configs.keys())[0]} = %s"
|
|
228
|
+
self.cursor.execute(delete_query, (record_id,))
|
|
229
|
+
self.cursor.connection.commit()
|
|
230
|
+
e.page.open(ft.SnackBar(ft.Text("Запись удалена!")))
|
|
231
|
+
e.page.update()
|
|
232
|
+
except Exception as ex:
|
|
233
|
+
print(ex)
|
|
234
|
+
e.page.open(ft.SnackBar(ft.Text(f"Ошибка: {str(ex)}")))
|
|
235
|
+
e.page.update()
|
|
236
|
+
|
|
237
|
+
return callback
|
fletable/sql_table.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import flet as ft
|
|
4
|
+
|
|
5
|
+
from .editable_table import FieldConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class DisplayValue:
|
|
10
|
+
id: str
|
|
11
|
+
label: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SqlTable:
|
|
15
|
+
"""
|
|
16
|
+
Read-only таблица: только отображение и выбор строк.
|
|
17
|
+
|
|
18
|
+
Пример:
|
|
19
|
+
table = SqlTable(cursor, "users", {"user_id": "ID", "name": "Имя"})
|
|
20
|
+
data_table = table.create_table()
|
|
21
|
+
selected = table.get_selected_rows()
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
cursor,
|
|
27
|
+
table_name: str,
|
|
28
|
+
field_mapping: dict[str, FieldConfig | str],
|
|
29
|
+
width: int = 800,
|
|
30
|
+
height: int = 400,
|
|
31
|
+
):
|
|
32
|
+
self.cursor = cursor
|
|
33
|
+
self.table_name = table_name
|
|
34
|
+
self.field_configs: dict[str, FieldConfig] = {
|
|
35
|
+
name: cfg if isinstance(cfg, FieldConfig) else FieldConfig(label=str(cfg))
|
|
36
|
+
for name, cfg in field_mapping.items()
|
|
37
|
+
}
|
|
38
|
+
self.width = width
|
|
39
|
+
self.height = height
|
|
40
|
+
self.dropdown_options = self._generate_dropdown_options()
|
|
41
|
+
self.row_checkboxes: list[tuple[ft.Checkbox, dict]] = []
|
|
42
|
+
self.header_checkbox: ft.Checkbox = None
|
|
43
|
+
|
|
44
|
+
def _generate_dropdown_options(self) -> dict[str, list[DisplayValue]]:
|
|
45
|
+
"""
|
|
46
|
+
Подтягиваем значения для внешних ключей, чтобы показывать подписанные лейблы.
|
|
47
|
+
"""
|
|
48
|
+
options: dict[str, list[DisplayValue]] = {}
|
|
49
|
+
for field, cfg in self.field_configs.items():
|
|
50
|
+
ref_cfg = cfg.foreign_key
|
|
51
|
+
if ref_cfg or (field.endswith("_id") and field != f"{self.table_name}_id"):
|
|
52
|
+
ref_table = ref_cfg.table if ref_cfg else field.replace("_id", "")
|
|
53
|
+
id_column = ref_cfg.id_column if ref_cfg else field
|
|
54
|
+
label_column = ref_cfg.label_column if ref_cfg else ref_table
|
|
55
|
+
try:
|
|
56
|
+
self.cursor.execute(
|
|
57
|
+
f"SELECT {id_column}, {label_column} FROM {ref_table}"
|
|
58
|
+
)
|
|
59
|
+
results = self.cursor.fetchall()
|
|
60
|
+
options[field] = [
|
|
61
|
+
DisplayValue(id=str(row[0]), label=str(row[1]))
|
|
62
|
+
for row in results
|
|
63
|
+
]
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(f"[WARN] Не удалось загрузить dropdown для {field}: {e}")
|
|
66
|
+
return options
|
|
67
|
+
|
|
68
|
+
def _label_for_fk(self, field: str, value) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Возвращает человеко-читаемое значение для внешнего ключа.
|
|
71
|
+
"""
|
|
72
|
+
if field not in self.dropdown_options:
|
|
73
|
+
return str(value)
|
|
74
|
+
for option in self.dropdown_options[field]:
|
|
75
|
+
if option.id == str(value):
|
|
76
|
+
return option.label
|
|
77
|
+
return str(value)
|
|
78
|
+
|
|
79
|
+
def create_table(self) -> ft.DataTable:
|
|
80
|
+
db_fields = list(self.field_configs.keys())
|
|
81
|
+
query = f"SELECT {', '.join(db_fields)} FROM {self.table_name}"
|
|
82
|
+
self.cursor.execute(query)
|
|
83
|
+
data = self.cursor.fetchall()
|
|
84
|
+
|
|
85
|
+
self.row_checkboxes = []
|
|
86
|
+
|
|
87
|
+
def on_header_checkbox_change(e):
|
|
88
|
+
for checkbox, _ in self.row_checkboxes:
|
|
89
|
+
checkbox.value = self.header_checkbox.value
|
|
90
|
+
e.page.update()
|
|
91
|
+
|
|
92
|
+
self.header_checkbox = ft.Checkbox(
|
|
93
|
+
value=False, on_change=on_header_checkbox_change
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
rows = []
|
|
97
|
+
for row in data:
|
|
98
|
+
row_data = {field: value for field, value in zip(db_fields, row)}
|
|
99
|
+
row_checkbox = ft.Checkbox(value=False)
|
|
100
|
+
self.row_checkboxes.append((row_checkbox, row_data))
|
|
101
|
+
|
|
102
|
+
cells = [ft.DataCell(row_checkbox)]
|
|
103
|
+
for field, value in zip(db_fields, row):
|
|
104
|
+
display_value = self._label_for_fk(field, value)
|
|
105
|
+
cells.append(ft.DataCell(ft.Text(display_value)))
|
|
106
|
+
|
|
107
|
+
rows.append(ft.DataRow(cells=cells))
|
|
108
|
+
|
|
109
|
+
columns = [ft.DataColumn(self.header_checkbox)] + [
|
|
110
|
+
ft.DataColumn(ft.Text(self.field_configs[field].label))
|
|
111
|
+
for field in db_fields
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
return ft.DataTable(
|
|
115
|
+
columns=columns,
|
|
116
|
+
rows=rows,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def get_selected_rows(self) -> list[dict]:
|
|
120
|
+
"""
|
|
121
|
+
Возвращает список словарей с данными выделенных строк.
|
|
122
|
+
Ключи словаря соответствуют полям из field_mapping.
|
|
123
|
+
"""
|
|
124
|
+
selected = []
|
|
125
|
+
for checkbox, row_data in self.row_checkboxes:
|
|
126
|
+
if checkbox.value:
|
|
127
|
+
selected.append(row_data.copy())
|
|
128
|
+
return selected
|
fletable/utils.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import flet as ft
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LoginView(ft.View):
|
|
5
|
+
"""Login form view for user authentication.
|
|
6
|
+
|
|
7
|
+
Creates an interface for user authentication with login and password fields.
|
|
8
|
+
Validates user credentials against the database and displays appropriate
|
|
9
|
+
error messages.
|
|
10
|
+
|
|
11
|
+
:param user_table: Name of the database table containing user data
|
|
12
|
+
:type user_table: str
|
|
13
|
+
:param user_login_col: Name of the column in the table containing user logins
|
|
14
|
+
:type user_login_col: str
|
|
15
|
+
:param user_password_col: Name of the column in the table containing user passwords
|
|
16
|
+
:type user_password_col: str
|
|
17
|
+
:param dbapi_cursor: Database cursor for executing SQL queries
|
|
18
|
+
:type dbapi_cursor: dbapi.cursor
|
|
19
|
+
|
|
20
|
+
Example::
|
|
21
|
+
|
|
22
|
+
login_view = LoginView(
|
|
23
|
+
user_table="employees",
|
|
24
|
+
user_login_col="login",
|
|
25
|
+
user_password_col="password",
|
|
26
|
+
dbapi_cursor=connection.cursor(),
|
|
27
|
+
)
|
|
28
|
+
page.views.append(login_view)
|
|
29
|
+
page.update()
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
user_table,
|
|
35
|
+
user_login_col,
|
|
36
|
+
user_password_col,
|
|
37
|
+
dbapi_cursor,
|
|
38
|
+
next,
|
|
39
|
+
user_role_col=None,
|
|
40
|
+
user_role_key=None,
|
|
41
|
+
user_id_col=None,
|
|
42
|
+
user_id_key=None,
|
|
43
|
+
):
|
|
44
|
+
super().__init__()
|
|
45
|
+
self.user_table = user_table
|
|
46
|
+
self.user_login_col = user_login_col
|
|
47
|
+
self.user_password_col = user_password_col
|
|
48
|
+
self.user_role_col = user_role_col
|
|
49
|
+
self.user_role_key = user_role_key
|
|
50
|
+
self.cursor = dbapi_cursor
|
|
51
|
+
self.next_func = next
|
|
52
|
+
self.user_id_col = user_id_col
|
|
53
|
+
self.user_id_key = user_id_key
|
|
54
|
+
|
|
55
|
+
title = ft.Text("Вход", size=20, weight="bold")
|
|
56
|
+
self.login = ft.TextField(label="Логин")
|
|
57
|
+
self.password = ft.TextField(
|
|
58
|
+
label="Пароль", password=True, can_reveal_password=True
|
|
59
|
+
)
|
|
60
|
+
self.submit = ft.FilledButton("Войти", on_click=self.check_credentials)
|
|
61
|
+
|
|
62
|
+
self.route = "/login"
|
|
63
|
+
self.form = ft.Column(
|
|
64
|
+
[
|
|
65
|
+
ft.Row(controls=[title], alignment=ft.MainAxisAlignment.CENTER),
|
|
66
|
+
self.login,
|
|
67
|
+
self.password,
|
|
68
|
+
ft.Row(controls=[self.submit], alignment=ft.MainAxisAlignment.END),
|
|
69
|
+
],
|
|
70
|
+
width=300,
|
|
71
|
+
)
|
|
72
|
+
self.horizontal_alignment = ft.CrossAxisAlignment.CENTER
|
|
73
|
+
self.vertical_alignment = ft.MainAxisAlignment.CENTER
|
|
74
|
+
self.controls = [self.form]
|
|
75
|
+
|
|
76
|
+
def check_credentials(self, e):
|
|
77
|
+
login = self.login.value
|
|
78
|
+
password = self.password.value
|
|
79
|
+
self.password.error_text = None
|
|
80
|
+
self.login.error_text = None
|
|
81
|
+
if not (login and password):
|
|
82
|
+
if not login:
|
|
83
|
+
self.login.error_text = "введите логин"
|
|
84
|
+
else:
|
|
85
|
+
self.login.error_text = None
|
|
86
|
+
if not password:
|
|
87
|
+
self.password.error_text = "Введите пароль"
|
|
88
|
+
else:
|
|
89
|
+
self.password.error_text = None
|
|
90
|
+
self.page.update()
|
|
91
|
+
return
|
|
92
|
+
self.page.update()
|
|
93
|
+
|
|
94
|
+
self.cursor.execute(
|
|
95
|
+
f"""
|
|
96
|
+
SELECT {self.user_role_col if self.user_role_col else -1}, {self.user_id_col if self.user_id_col else -1}
|
|
97
|
+
FROM {self.user_table}
|
|
98
|
+
WHERE {self.user_login_col}='{login}' AND {self.user_password_col}='{password}'
|
|
99
|
+
"""
|
|
100
|
+
)
|
|
101
|
+
result = self.cursor.fetchone()
|
|
102
|
+
if not result:
|
|
103
|
+
dlg = ft.AlertDialog(
|
|
104
|
+
title=ft.Text("Предупреждение"),
|
|
105
|
+
content=ft.Text("Неверный логин и пароль"),
|
|
106
|
+
alignment=ft.alignment.center,
|
|
107
|
+
title_padding=ft.padding.all(25),
|
|
108
|
+
actions=[ft.TextButton("Ok", on_click=lambda e: self.page.close(dlg))],
|
|
109
|
+
)
|
|
110
|
+
self.page.open(dlg)
|
|
111
|
+
return
|
|
112
|
+
if self.user_role_col and self.user_role_key and result[0] != -1:
|
|
113
|
+
self.page.client_storage.set(self.user_role_key, result[0])
|
|
114
|
+
self.page.client_storage.set(self.user_id_key, result[1])
|
|
115
|
+
|
|
116
|
+
self.page.views.pop()
|
|
117
|
+
self.next_func(self.page)
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fletable
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Tables that take data from SQL database
|
|
5
|
+
Home-page: https://github.com/RichCake/fletable
|
|
6
|
+
Author: RichCake
|
|
7
|
+
Author-email: abs2016123@gmail.com
|
|
8
|
+
Project-URL: GitHub, https://github.com/RichCake/fletable
|
|
9
|
+
Keywords: flet sql table
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: flet==0.28.3
|
|
16
|
+
Requires-Dist: flet-cli==0.28.3
|
|
17
|
+
Requires-Dist: flet-desktop==0.28.3
|
|
18
|
+
Requires-Dist: flet-web==0.28.3
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: keywords
|
|
26
|
+
Dynamic: project-url
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
30
|
+
|
|
31
|
+
# Fletable
|
|
32
|
+
|
|
33
|
+
**Fletable** — Python-библиотека для создания интерактивных таблиц с данными из SQL-баз в приложениях на Flet. Поддерживает редактирование данных, автоматическую обработку внешних ключей и удобную работу с формами.
|
|
34
|
+
|
|
35
|
+
## ✨ Возможности
|
|
36
|
+
|
|
37
|
+
- 📝 **Редактируемые таблицы** — изменение, добавление и удаление записей прямо в интерфейсе
|
|
38
|
+
- 👀 **Таблицы только для чтения** — отображение данных с возможностью выбора строк
|
|
39
|
+
- 🔗 **Автоматическая обработка внешних ключей** — dropdown-списки для связанных таблиц
|
|
40
|
+
- ✅ **Множественный выбор** — чекбоксы для выделения строк
|
|
41
|
+
- 🔐 **Встроенная форма авторизации** — готовый компонент для входа пользователей
|
|
42
|
+
- 🎨 **Настраиваемые подписи полей** — удобное отображение имен колонок
|
|
43
|
+
|
|
44
|
+
## 📦 Установка
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install fletable
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Или установите из исходного кода:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone <repository-url>
|
|
54
|
+
cd fletable
|
|
55
|
+
pip install -e .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 🚀 Быстрый старт
|
|
59
|
+
|
|
60
|
+
### 1. Редактируемая таблица
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import flet as ft
|
|
64
|
+
import psycopg2
|
|
65
|
+
from fletable import EditableTable, FieldConfig
|
|
66
|
+
|
|
67
|
+
def main(page: ft.Page):
|
|
68
|
+
# Подключение к базе данных
|
|
69
|
+
conn = psycopg2.connect(
|
|
70
|
+
host="localhost",
|
|
71
|
+
database="mydb",
|
|
72
|
+
user="user",
|
|
73
|
+
password="password"
|
|
74
|
+
)
|
|
75
|
+
cursor = conn.cursor()
|
|
76
|
+
|
|
77
|
+
# Создание таблицы
|
|
78
|
+
table = EditableTable(
|
|
79
|
+
cursor=cursor,
|
|
80
|
+
table_name="employees",
|
|
81
|
+
field_mapping={
|
|
82
|
+
"employee_id": "ID",
|
|
83
|
+
"name": "Имя",
|
|
84
|
+
"email": "Email",
|
|
85
|
+
"department_id": FieldConfig(
|
|
86
|
+
label="Отдел",
|
|
87
|
+
foreign_key=ForeignKeyConfig(
|
|
88
|
+
table="departments",
|
|
89
|
+
id_column="department_id",
|
|
90
|
+
label_column="department_name"
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Форма добавления
|
|
97
|
+
add_form, handle_add = table.create_add_form()
|
|
98
|
+
|
|
99
|
+
def add_record(e):
|
|
100
|
+
success, message = handle_add()
|
|
101
|
+
if success:
|
|
102
|
+
# Обновление таблицы после добавления
|
|
103
|
+
container.content = table.create_table()
|
|
104
|
+
page.update()
|
|
105
|
+
|
|
106
|
+
add_button = ft.ElevatedButton("Добавить", on_click=add_record)
|
|
107
|
+
|
|
108
|
+
# Контейнер с таблицей
|
|
109
|
+
container = ft.Container(
|
|
110
|
+
content=table.create_table(),
|
|
111
|
+
padding=10
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
page.add(
|
|
115
|
+
ft.Column([
|
|
116
|
+
ft.Text("Управление сотрудниками", size=24, weight="bold"),
|
|
117
|
+
add_form,
|
|
118
|
+
add_button,
|
|
119
|
+
container
|
|
120
|
+
])
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
ft.app(target=main)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 2. Таблица только для чтения
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import flet as ft
|
|
130
|
+
from fletable import SqlTable
|
|
131
|
+
|
|
132
|
+
def main(page: ft.Page):
|
|
133
|
+
# Подключение к БД
|
|
134
|
+
cursor = conn.cursor()
|
|
135
|
+
|
|
136
|
+
# Создание read-only таблицы
|
|
137
|
+
table = SqlTable(
|
|
138
|
+
cursor=cursor,
|
|
139
|
+
table_name="products",
|
|
140
|
+
field_mapping={
|
|
141
|
+
"product_id": "ID",
|
|
142
|
+
"product_name": "Название",
|
|
143
|
+
"price": "Цена",
|
|
144
|
+
"category_id": "Категория"
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Кнопка для получения выделенных строк
|
|
149
|
+
def show_selected(e):
|
|
150
|
+
selected = table.get_selected_rows()
|
|
151
|
+
print("Выбрано записей:", len(selected))
|
|
152
|
+
for row in selected:
|
|
153
|
+
print(row)
|
|
154
|
+
|
|
155
|
+
page.add(
|
|
156
|
+
ft.Column([
|
|
157
|
+
ft.Container(content=table.create_table(), padding=10),
|
|
158
|
+
ft.ElevatedButton("Показать выбранные", on_click=show_selected)
|
|
159
|
+
])
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
ft.app(target=main)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 3. Форма авторизации
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
import flet as ft
|
|
169
|
+
from fletable import LoginView
|
|
170
|
+
|
|
171
|
+
def main(page: ft.Page):
|
|
172
|
+
def after_login(page):
|
|
173
|
+
# Переход на главную страницу после успешного входа
|
|
174
|
+
page.views.append(
|
|
175
|
+
ft.View("/home", [
|
|
176
|
+
ft.Text("Добро пожаловать!")
|
|
177
|
+
])
|
|
178
|
+
)
|
|
179
|
+
page.update()
|
|
180
|
+
|
|
181
|
+
login_view = LoginView(
|
|
182
|
+
user_table="users",
|
|
183
|
+
user_login_col="login",
|
|
184
|
+
user_password_col="password",
|
|
185
|
+
dbapi_cursor=cursor,
|
|
186
|
+
next=after_login,
|
|
187
|
+
user_role_col="role",
|
|
188
|
+
user_role_key="user_role",
|
|
189
|
+
user_id_col="user_id",
|
|
190
|
+
user_id_key="current_user_id"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
page.views.append(login_view)
|
|
194
|
+
page.update()
|
|
195
|
+
|
|
196
|
+
ft.app(target=main)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## 📚 Документация API
|
|
200
|
+
|
|
201
|
+
### EditableTable
|
|
202
|
+
|
|
203
|
+
Класс для создания редактируемых таблиц с поддержкой CRUD-операций.
|
|
204
|
+
|
|
205
|
+
#### Конструктор
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
EditableTable(
|
|
209
|
+
cursor, # Курсор базы данных (DB-API 2.0)
|
|
210
|
+
table_name: str, # Имя таблицы в БД
|
|
211
|
+
field_mapping: dict, # Маппинг полей {column: label или FieldConfig}
|
|
212
|
+
width: int = 800, # Ширина таблицы (пикселей)
|
|
213
|
+
height: int = 400 # Высота таблицы (пикселей)
|
|
214
|
+
)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### Методы
|
|
218
|
+
|
|
219
|
+
- **`create_table()`** — создает и возвращает `ft.DataTable` с данными
|
|
220
|
+
- **`create_add_form()`** — создает форму для добавления новых записей, возвращает `(form_row, handle_add)`
|
|
221
|
+
- **`get_selected_rows()`** — возвращает список словарей с данными выделенных строк
|
|
222
|
+
|
|
223
|
+
### SqlTable
|
|
224
|
+
|
|
225
|
+
Класс для создания таблиц только для чтения с возможностью выбора строк.
|
|
226
|
+
|
|
227
|
+
#### Конструктор
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
SqlTable(
|
|
231
|
+
cursor, # Курсор базы данных
|
|
232
|
+
table_name: str, # Имя таблицы
|
|
233
|
+
field_mapping: dict, # Маппинг полей
|
|
234
|
+
width: int = 800,
|
|
235
|
+
height: int = 400
|
|
236
|
+
)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### Методы
|
|
240
|
+
|
|
241
|
+
- **`create_table()`** — создает и возвращает `ft.DataTable`
|
|
242
|
+
- **`get_selected_rows()`** — возвращает выделенные строки
|
|
243
|
+
|
|
244
|
+
### FieldConfig
|
|
245
|
+
|
|
246
|
+
Конфигурация для настройки отображения полей.
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
@dataclass
|
|
250
|
+
class FieldConfig:
|
|
251
|
+
label: str # Отображаемое название поля
|
|
252
|
+
foreign_key: ForeignKeyConfig | None = None # Конфигурация внешнего ключа
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### ForeignKeyConfig
|
|
256
|
+
|
|
257
|
+
Настройка внешнего ключа для автоматического создания dropdown-списков.
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
@dataclass
|
|
261
|
+
class ForeignKeyConfig:
|
|
262
|
+
table: str # Имя связанной таблицы
|
|
263
|
+
id_column: str # Колонка с ID
|
|
264
|
+
label_column: str # Колонка с отображаемым значением
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### LoginView
|
|
268
|
+
|
|
269
|
+
Готовая форма авторизации пользователей.
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
LoginView(
|
|
273
|
+
user_table: str, # Таблица с пользователями
|
|
274
|
+
user_login_col: str, # Колонка с логином
|
|
275
|
+
user_password_col: str, # Колонка с паролем
|
|
276
|
+
dbapi_cursor, # Курсор БД
|
|
277
|
+
next: callable, # Функция после успешного входа
|
|
278
|
+
user_role_col: str = None, # Колонка с ролью (опционально)
|
|
279
|
+
user_role_key: str = None, # Ключ для хранения роли
|
|
280
|
+
user_id_col: str = None, # Колонка с ID пользователя
|
|
281
|
+
user_id_key: str = None # Ключ для хранения ID
|
|
282
|
+
)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## 🔧 Автоматическая обработка внешних ключей
|
|
286
|
+
|
|
287
|
+
Fletable автоматически создает dropdown-списки для полей с именами, заканчивающимися на `_id` (кроме первичного ключа таблицы):
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
field_mapping = {
|
|
291
|
+
"order_id": "ID заказа",
|
|
292
|
+
"customer_id": "Клиент", # Автоматически создаст dropdown из таблицы "customer"
|
|
293
|
+
"product_id": "Товар" # Автоматически создаст dropdown из таблицы "product"
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Для более точной настройки используйте `ForeignKeyConfig`:
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
field_mapping = {
|
|
301
|
+
"order_id": "ID заказа",
|
|
302
|
+
"customer_id": FieldConfig(
|
|
303
|
+
label="Клиент",
|
|
304
|
+
foreign_key=ForeignKeyConfig(
|
|
305
|
+
table="customers",
|
|
306
|
+
id_column="customer_id",
|
|
307
|
+
label_column="full_name"
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## 💡 Примеры использования
|
|
314
|
+
|
|
315
|
+
### Обновление таблицы после изменений
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
def refresh_table(e):
|
|
319
|
+
container.content = table.create_table()
|
|
320
|
+
page.update()
|
|
321
|
+
|
|
322
|
+
refresh_button = ft.IconButton(
|
|
323
|
+
icon=ft.Icons.REFRESH,
|
|
324
|
+
on_click=refresh_table
|
|
325
|
+
)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Работа с выделенными строками
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
def process_selected(e):
|
|
332
|
+
selected = table.get_selected_rows()
|
|
333
|
+
for row in selected:
|
|
334
|
+
print(f"ID: {row['employee_id']}, Name: {row['name']}")
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Массовое удаление
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
def delete_selected(e):
|
|
341
|
+
selected = table.get_selected_rows()
|
|
342
|
+
for row in selected:
|
|
343
|
+
cursor.execute(
|
|
344
|
+
"DELETE FROM employees WHERE employee_id = %s",
|
|
345
|
+
(row['employee_id'],)
|
|
346
|
+
)
|
|
347
|
+
conn.commit()
|
|
348
|
+
refresh_table(e)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## 🗄️ Поддерживаемые базы данных
|
|
352
|
+
|
|
353
|
+
Fletable работает с любыми базами данных, поддерживающими DB-API 2.0:
|
|
354
|
+
|
|
355
|
+
- PostgreSQL (psycopg2)
|
|
356
|
+
- MySQL (mysql-connector-python)
|
|
357
|
+
- SQLite (sqlite3)
|
|
358
|
+
- Oracle
|
|
359
|
+
- Microsoft SQL Server
|
|
360
|
+
|
|
361
|
+
## 📋 Требования
|
|
362
|
+
|
|
363
|
+
- Python >= 3.6
|
|
364
|
+
- flet >= 0.28.3
|
|
365
|
+
- Драйвер базы данных (psycopg2, mysql-connector, и т.д.)
|
|
366
|
+
|
|
367
|
+
## 🤝 Участие в разработке
|
|
368
|
+
|
|
369
|
+
Мы приветствуем ваши предложения и pull request'ы!
|
|
370
|
+
|
|
371
|
+
## 📄 Лицензия
|
|
372
|
+
|
|
373
|
+
MIT License
|
|
374
|
+
|
|
375
|
+
## 👨💻 Автор
|
|
376
|
+
|
|
377
|
+
**RichCake**
|
|
378
|
+
Email: abs2016123@gmail.com
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
⭐ Если вам понравился проект, поставьте звезду на GitHub!
|
|
383
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
fletable/__init__.py,sha256=bAuv-OvRBPuBJWlqU2Gq3b0ppj2-L5qS0SkuPk6RBTM,134
|
|
2
|
+
fletable/editable_table.py,sha256=PcUPxmfIsZzBgHCAPFgD-8nG1fe-kq8Tf9A9Hq69Y1s,9399
|
|
3
|
+
fletable/sql_table.py,sha256=Q-XzZR8mhMnHffaT63UuBP0B1lmHCsR5m8DgTRigPx8,4675
|
|
4
|
+
fletable/utils.py,sha256=o7gaTyBw5VL0cYs-6IqnTuU7PtmQwsdqXKTGzBxEVI4,4186
|
|
5
|
+
fletable-0.0.1.dist-info/METADATA,sha256=m0U4g78ikOf1x4_P4jXB5oFDwMfShJpYF43U1G9THjc,11959
|
|
6
|
+
fletable-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
fletable-0.0.1.dist-info/top_level.txt,sha256=YofKKSe3S3HQ1YwJwI7CVrF_1KEHcYwzLEBezSPppGk,9
|
|
8
|
+
fletable-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fletable
|