bpappbuilder 0.1.0__tar.gz

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.
Files changed (31) hide show
  1. bpappbuilder-0.1.0/.gitignore +8 -0
  2. bpappbuilder-0.1.0/.pypi_token +1 -0
  3. bpappbuilder-0.1.0/LICENSE +21 -0
  4. bpappbuilder-0.1.0/PKG-INFO +21 -0
  5. bpappbuilder-0.1.0/README.md +6 -0
  6. bpappbuilder-0.1.0/apps/development360/dev360_icon.ico +0 -0
  7. bpappbuilder-0.1.0/apps/development360/dev360_icon.svg +1 -0
  8. bpappbuilder-0.1.0/apps/development360/development360.py +504 -0
  9. bpappbuilder-0.1.0/apps/development360//320/227/320/260/320/264/320/260/321/207/320/260 1.doc +0 -0
  10. bpappbuilder-0.1.0/apps/technoholding/kriteriiZ2.xlsx +0 -0
  11. bpappbuilder-0.1.0/apps/technoholding/technoholding.py +233 -0
  12. bpappbuilder-0.1.0/apps/technoholding//320/227/320/260/320/264/320/260/321/207/320/260 2. /320/222/321/202/320/276/321/200/320/276/320/271 /321/202/321/203/321/200.doc +0 -0
  13. bpappbuilder-0.1.0/apps/technoholding//320/237/320/276/321/217/321/201/320/275/320/270/321/202/320/265/320/273/321/214/320/275/320/260/321/217 /320/267/320/260/320/277/320/270/321/201/320/272/320/260.md" +196 -0
  14. bpappbuilder-0.1.0/apps/vunderkind/vunderkind.py +93 -0
  15. bpappbuilder-0.1.0/pyproject.toml +30 -0
  16. bpappbuilder-0.1.0/src/bpappbuilder/__init__.py +7 -0
  17. bpappbuilder-0.1.0/src/bpappbuilder/app/__init__.py +5 -0
  18. bpappbuilder-0.1.0/src/bpappbuilder/app/app.py +134 -0
  19. bpappbuilder-0.1.0/src/bpappbuilder/app/db.py +47 -0
  20. bpappbuilder-0.1.0/src/bpappbuilder/app/styles.qss +243 -0
  21. bpappbuilder-0.1.0/src/bpappbuilder/app/themes.py +72 -0
  22. bpappbuilder-0.1.0/src/bpappbuilder/fields/__init__.py +29 -0
  23. bpappbuilder-0.1.0/src/bpappbuilder/fields/combo_box_items.py +93 -0
  24. bpappbuilder-0.1.0/src/bpappbuilder/fields/field.py +1194 -0
  25. bpappbuilder-0.1.0/src/bpappbuilder/reports/__init__.py +21 -0
  26. bpappbuilder-0.1.0/src/bpappbuilder/reports/report.py +1075 -0
  27. bpappbuilder-0.1.0/src/bpappbuilder/tabs/__init__.py +24 -0
  28. bpappbuilder-0.1.0/src/bpappbuilder/tabs/group.py +31 -0
  29. bpappbuilder-0.1.0/src/bpappbuilder/tabs/tab.py +80 -0
  30. bpappbuilder-0.1.0/src/bpappbuilder/tabs/table.py +544 -0
  31. bpappbuilder-0.1.0/src/bpappbuilder/tabs/table_tab.py +290 -0
@@ -0,0 +1,8 @@
1
+ test*
2
+ *.db
3
+ *.drawio
4
+ .venv*
5
+ __pycache__/
6
+ .vscode/
7
+ build/
8
+ dist/
@@ -0,0 +1 @@
1
+ pypi-AgEIcHlwaS5vcmcCJDRmYmVkMjYyLTRkYTItNDI2ZC1iNDRkLTc3M2YyZmFiZDg5ZAACKlszLCI5NzdjYjE5OC1mYmI5LTQ1MDUtOTgzZC02ZmUzNjQ2NzI0NTkiXQAABiC5pMP04lpAh8tMXjsoERVE-O5vn1H1Al8JLwEQes4RgQ
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AlexK-1
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: bpappbuilder
3
+ Version: 0.1.0
4
+ Summary: Оболочка над PyQt для создания приложений, как в 1С:Предприятии, но на Python
5
+ Author: AlexK-1
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.9
13
+ Requires-Dist: pyside6>=6.7
14
+ Description-Content-Type: text/markdown
15
+
16
+ # BPAppBuilder
17
+
18
+ Оболочка над PyQt для создания приложений, как в 1С:Предприятии, но на Python
19
+
20
+ **WORK IN PROGRESS, DO NOT USE**
21
+
@@ -0,0 +1,6 @@
1
+ # BPAppBuilder
2
+
3
+ Оболочка над PyQt для создания приложений, как в 1С:Предприятии, но на Python
4
+
5
+ **WORK IN PROGRESS, DO NOT USE**
6
+
@@ -0,0 +1 @@
1
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="120.22011" height="113.23706" viewBox="0,0,120.22011,113.23706"><g transform="translate(-179.88995,-123.38147)"><g data-paper-data="{&quot;isPaintingLayer&quot;:true}" fill-rule="nonzero" stroke="none" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" style="mix-blend-mode: normal"><g><path d="M186.873,180c0,-31.26955 25.34898,-56.61853 56.61853,-56.61853c31.26955,0 56.61852,25.34898 56.61852,56.61853c0,31.26955 -25.34898,56.61853 -56.61852,56.61853c-31.26955,0 -56.61853,-25.34898 -56.61853,-56.61853z" fill="#003eb7"/><path d="M198.29695,180c0,-24.59348 19.93695,-44.53043 44.53043,-44.53043c24.59348,0 44.53043,19.93695 44.53043,44.53043c0,24.59348 -19.93695,44.53043 -44.53043,44.53043c-24.59348,0 -44.53043,-19.93695 -44.53043,-44.53043z" fill="#0087fa"/><path d="M179.88995,180c0,-31.26955 25.34898,-56.61853 56.61852,-56.61853c31.26955,0 56.61853,25.34898 56.61853,56.61853c0,31.26955 -25.34898,56.61853 -56.61853,56.61853c-31.26955,0 -56.61852,-25.34898 -56.61852,-56.61853zM236.61571,224.71855c24.7094,0 44.74032,-20.03092 44.74032,-44.74032c0,-24.7094 -20.03092,-44.74032 -44.74032,-44.74032c-24.7094,0 -44.74032,20.03092 -44.74032,44.74032c0,24.7094 20.03092,44.74032 44.74032,44.74032z" fill="#0073eb"/></g><g><g fill="#b7b7b7"><path d="M227.96753,195.72873l36.83539,-39.66096l11.19266,10.39525l-36.83539,39.66096z"/><path d="M224.00639,171.18663l17.27564,16.04487l-10.39527,11.19265l-17.27564,-16.04485z"/></g><path d="M230.04947,206.09862v-5.8139h8.95805v5.8139z" fill="#b7b7b7"/><path d="M214.92938,176.97229v-5.8139h9.13359v5.8139z" fill="#b7b7b7"/><path d="M255.76353,161.84985v-5.8139h8.95805v5.8139z" fill="#b7b7b7"/><g fill="#ffffff"><path d="M218.80938,195.72873l36.83539,-39.66096l11.19266,10.39525l-36.83538,39.66096z"/><path d="M214.84823,171.18663l17.27564,16.04487l-10.39525,11.19265l-17.27564,-16.04485z"/></g></g></g></g></svg>
@@ -0,0 +1,504 @@
1
+ # pyinstaller -D -n "Развитие 360" --windowed --icon "dev360_icon.ico" .\development360.py
2
+
3
+ from bpappbuilder.app import App, DataBase, SQLiteDB
4
+ from bpappbuilder.tabs import TableTab, TableColumn, FormFieldWidget, FormRow
5
+ from bpappbuilder.fields import (Field, DateField, TimeField, TextLineField,
6
+ TextAreaField, ComboBoxField, IntField, FloatField, TableField,
7
+ TableFieldItems, LabelField, StaticItems, FileField)
8
+ from bpappbuilder.reports import ReportTab, LineChartReport, ListReport
9
+
10
+ from PySide6.QtWidgets import QWidget, QCalendarWidget, QToolTip, QPushButton
11
+ from PySide6.QtGui import QColor, QPalette, QPen, QPainter
12
+ from PySide6.QtCore import QDate, QTime, Qt, QEvent, QObject, QPoint, QRect
13
+
14
+ # import win11toast
15
+ import notifypy # notify_py
16
+ import threading
17
+ import time
18
+ import random
19
+ from math import log10
20
+ from typing import Optional
21
+
22
+
23
+ app_name = "Развитие 360"
24
+
25
+ def set_class_field(widget: QWidget, new_class: str):
26
+ widget.setProperty("class", new_class)
27
+ old = widget.styleSheet()
28
+ if not old:
29
+ widget.setStyleSheet('* {}')
30
+ widget.setStyleSheet(old)
31
+
32
+
33
+ class HabitMarkButton(QPushButton):
34
+ def __init__(self, habit_type: int):
35
+ super().__init__()
36
+ self.marked = False
37
+ self.habit_type = habit_type
38
+
39
+ class HabitsCalendar(QCalendarWidget):
40
+ def __init__(self, habit_id: int, habit_type: int, db: DataBase):
41
+ super().__init__()
42
+
43
+ self.habit_id = habit_id
44
+ self.habit_type = habit_type
45
+ self.db = db
46
+
47
+ self.notes = self.db.cur.execute("SELECT id, text, date FROM Notes WHERE habit = ?", (self.habit_id,)).fetchall()
48
+ self.files = self.db.cur.execute("SELECT NotesFiles.name, NotesFiles.parent, Notes.date FROM NotesFiles LEFT JOIN Notes ON NotesFiles.parent = Notes.id WHERE Notes.habit = ?", (self.habit_id,)).fetchall()
49
+
50
+ self.setMouseTracking(True)
51
+ self.installEventFilter(self)
52
+
53
+ self.enabled_dates = set()
54
+
55
+ self.get_days(QDate.currentDate().year(), QDate.currentDate().month())
56
+ self.updateCells()
57
+
58
+ self.clicked.connect(self.toggle_day)
59
+ self.currentPageChanged.connect(self.get_days)
60
+
61
+ def toggle_day(self, date: QDate):
62
+ if date > QDate.currentDate().addDays(30) or date < QDate.currentDate().addDays(-30):
63
+ return
64
+
65
+ date_str = date.toString(DateField.date_format)
66
+
67
+ if date in self.enabled_dates:
68
+ self.enabled_dates.remove(date)
69
+ self.db.cur.execute("DELETE FROM HabitMarks WHERE habit = ? AND date = ?", (self.habit_id, date_str))
70
+ self.db.conn.commit()
71
+ else:
72
+ score = calculate_score(self.habit_id, self.habit_type, date_str)
73
+ self.enabled_dates.add(date)
74
+ self.db.cur.execute("INSERT INTO HabitMarks (habit, score, date) VALUES (?, ?, ?)",
75
+ (self.habit_id, score, date_str))
76
+ app.db.conn.commit()
77
+ if date == QDate.currentDate():
78
+ for table in habits_table.tables_widgets:
79
+ find = False
80
+ for row in range(table.rowCount()):
81
+ if int(table.item(row, 0).text()) == self.habit_id:
82
+ find = True
83
+ break
84
+
85
+ if find:
86
+ mark_btn_clicked(table.cellWidget(row, len(habits_table.fields)+1), self.habit_id)
87
+
88
+ if date in self.enabled_dates:
89
+ data = self.db.cur.execute(
90
+ "SELECT * FROM (SELECT name FROM Habits WHERE id = ?), (SELECT SUM(score) AS score FROM HabitMarks WHERE date = ?)",
91
+ (self.habit_id, date_str)).fetchone()
92
+
93
+ threading.Thread(target=lambda:notifypy.Notify(
94
+ app_name,
95
+ f"{score:+} {score_word(score)} за привычку \"{data['name']}\"! Теперь у вас за сегодня {data['score']} {score_word(data['score'])}",
96
+ app_name,
97
+ default_notification_icon="dev360_icon.ico").send(), daemon=True).start()
98
+
99
+ self.updateCells()
100
+
101
+ def get_days(self, year: int, month: int):
102
+ days_data = self.db.cur.execute(
103
+ "SELECT date FROM HabitMarks WHERE habit = ? AND date BETWEEN ? AND ?", (self.habit_id, f"{year}-{month:02}-00", f"{year}-{month:02}-31")
104
+ ).fetchall()
105
+ self.enabled_dates = {QDate.fromString(x["date"], DateField.date_format) for x in days_data}
106
+
107
+ def paintCell(self, painter: QPainter, rect: QRect, date: QDate):
108
+ super().paintCell(painter, rect, date)
109
+
110
+ if date in self.enabled_dates or date == self.selectedDate():
111
+ if date != self.selectedDate():
112
+ painter.fillRect(rect, QColor(65, 243, 71) if self.habit_type == 0 else QColor(243, 65, 65))
113
+ elif date in self.enabled_dates:
114
+ painter.fillRect(rect, QColor(63, 235, 66) if self.habit_type == 0 else QColor(228, 62, 62))
115
+ else:
116
+ painter.fillRect(rect, self.palette().color(QPalette.ColorRole.Midlight))
117
+ painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, str(date.day()))
118
+
119
+ date_str = date.toString(DateField.date_format)
120
+ if date_str in map(lambda x: x["date"], self.notes):
121
+ painter.setPen(QPen(Qt.GlobalColor.black, 2, Qt.PenStyle.SolidLine))
122
+ painter.drawEllipse(QPoint(rect.x()+6, rect.y()+6), 3, 3)
123
+
124
+ def eventFilter(self, obj: QObject, event: QEvent) -> bool:
125
+ if obj is self and event.type() == QEvent.Type.ToolTip:
126
+ pos: QPoint = event.pos()
127
+ date = self.get_date_at_position(pos)
128
+
129
+ QToolTip.showText(self.mapToGlobal(pos), self.create_tooltip(date), self)
130
+ return super().eventFilter(obj, event)
131
+
132
+ def create_tooltip(self, date: QDate) -> str:
133
+ date_str = date.toString(DateField.date_format)
134
+
135
+ # notes = self.db.cur.execute("SELECT id, text FROM Notes WHERE date = ? AND habit = ?", (date.toString(DateField.date_format), self.habit_id)).fetchall()
136
+ notes = [x for x in self.notes if x["date"] == date_str]
137
+
138
+ text = ""
139
+ for note_data in notes:
140
+ text += note_data["text"]
141
+ # files = self.db.cur.execute("SELECT name FROM NotesFiles WHERE parent = ?", (note_data["id"],)).fetchall()
142
+ files = [x for x in self.files if x["parent"] == note_data["id"]]
143
+ for file in files:
144
+ text += f"<img src = '{file['name']}'/><br>"
145
+ if len(notes) > 1:
146
+ text += "<hr>"
147
+
148
+ return text
149
+
150
+ def get_date_at_position(self, pos: QPoint) -> QDate:
151
+ table_view: QTableView = self.findChild(QTableView)
152
+ if not table_view:
153
+ return QDate()
154
+
155
+ table_pos = table_view.mapFrom(self, pos)
156
+
157
+ index = table_view.indexAt(table_pos)
158
+ if not index.isValid():
159
+ return QDate()
160
+
161
+ return self.get_date_from_index(index.row()-1, index.column()-1)
162
+
163
+ def get_date_from_index(self, row: int, column: int) -> QDate:
164
+ year = self.yearShown()
165
+ month = self.monthShown()
166
+
167
+ first_day = QDate(year, month, 1)
168
+ offset = (first_day.dayOfWeek() - self.firstDayOfWeek().value) % 7
169
+
170
+ start_date = first_day.addDays(-offset)
171
+
172
+ if offset == 0:
173
+ start_date = start_date.addDays(-7)
174
+
175
+ return start_date.addDays(row * 7 + column)
176
+
177
+ def calculate_score(habit_id: int, habit_type: int, date: str):
178
+ data = app.db.cur.execute("""
179
+ WITH RECURSIVE
180
+ Marks AS (
181
+ SELECT date FROM HabitMarks WHERE habit = :habit_id
182
+ ),
183
+ MonthMarksCount AS (
184
+ SELECT COUNT(*) AS month_count FROM Marks WHERE date BETWEEN date(:date, '-30 days') AND :date
185
+ ),
186
+ Streak AS (
187
+ SELECT :date AS day
188
+ WHERE day IS NOT NULL
189
+ UNION ALL
190
+ SELECT date(day, '-1 day')
191
+ FROM Streak
192
+ WHERE date(day, '-1 day') IN Marks
193
+ LIMIT 100
194
+ ),
195
+ Complexity AS (
196
+ SELECT complexity FROM Habits WHERE id = :habit_id
197
+ )
198
+ SELECT COUNT(day) AS streak, month_count, complexity FROM Streak, MonthMarksCount, Complexity
199
+ """, {"habit_id": habit_id, "date": date}).fetchone()
200
+
201
+ complexity = (0.5, 1, 2)
202
+ score = (10 + log10(1 + data["streak"]) * 15) * ((data["month_count"]+1) / 30) * complexity[data["complexity"]]
203
+ score = max(1, int(score)) * (1 if habit_type == 0 else -1)
204
+
205
+ return score
206
+
207
+ def create_form(form_widgets: dict[str, FormRow], fields: dict[str, Field], db: DataBase):
208
+ if "id" in form_widgets.keys():
209
+ calendar = HabitsCalendar(int(form_widgets["id"].widget.field_widget.text()), int(form_widgets["type"].widget.field_widget.currentData()), db)
210
+ form_widgets["calendar"] = FormRow("Календарь", FormFieldWidget(calendar))
211
+
212
+ def update_habits_type(data: None, form_widgets: dict[str, FormFieldWidget], fields: dict[str, Field], db: DataBase):
213
+ habit_type = int(form_widgets["type"].field_widget.currentData())
214
+
215
+ if habit_type == 1:
216
+ form_widgets["remind_every"].set_activated(False)
217
+ form_widgets["remind_at"].set_activated(False)
218
+
219
+ if "id" in form_widgets.keys():
220
+ calendar: HabitsCalendar = form_widgets["calendar"].field_widget
221
+ calendar.habit_type = habit_type
222
+ calendar.updateCells()
223
+
224
+ habit_id = int(form_widgets["id"].field_widget.text())
225
+ app.db.cur.execute("UPDATE HabitMarks SET score = ABS(score) * IIF(? = 0, 1, -1) WHERE habit = ?", (habit_type, habit_id))
226
+ app.db.conn.commit()
227
+
228
+ form_widgets["remind_every"].whole_widget.setEnabled(not habit_type)
229
+ form_widgets["remind_at"].whole_widget.setEnabled(not habit_type)
230
+
231
+ def score_word(score: int) -> str:
232
+ if 5 <= abs(score) <= 20:
233
+ return "баллов"
234
+ elif abs(score) % 10 == 1:
235
+ return "балл"
236
+ elif abs(score) in (2,3,4):
237
+ return "балла"
238
+ else:
239
+ return "баллов"
240
+
241
+ def mark_btn_clicked(button: HabitMarkButton, habit_id: int, db: Optional[DataBase] = None):
242
+ current_date = QDate.currentDate().toString(DateField.date_format)
243
+
244
+ if button.marked: # Выключение
245
+ button.setText("Пометить выполненной за сегодня")
246
+ set_class_field(button, "")
247
+ if db is not None:
248
+ db.cur.execute("DELETE FROM HabitMarks WHERE habit = ? AND date = ?", (habit_id, current_date))
249
+ db.conn.commit()
250
+ else: # Включение
251
+ button.setText("Помечена выполненной за сегодня")
252
+ set_class_field(button, "green-button" if button.habit_type == 0 else "red-button")
253
+ if db is not None:
254
+ score = calculate_score(habit_id, button.habit_type, current_date)
255
+ db.cur.execute("INSERT INTO HabitMarks (habit, score, date) VALUES (?, ?, ?)", (habit_id, score, current_date))
256
+ db.conn.commit()
257
+
258
+ data = db.cur.execute(
259
+ "SELECT * FROM (SELECT name FROM Habits WHERE id = ?), (SELECT SUM(score) AS score FROM HabitMarks WHERE date = ?)",
260
+ (habit_id, current_date)).fetchone()
261
+
262
+ threading.Thread(target=lambda:notifypy.Notify(
263
+ app_name,
264
+ f"{score:+} {score_word(score)} за привычку \"{data['name']}\"! Теперь у вас за сегодня {data['score']} {score_word(data['score'])}",
265
+ app_name,
266
+ default_notification_icon="dev360_icon.ico").send(), daemon=True).start()
267
+
268
+ button.marked = not button.marked
269
+ if habit_id in remind_times.keys():
270
+ remind_times[habit_id]["completed"] = button.marked
271
+
272
+ def mark_btn_generator(row_widgets: dict[str, QWidget], fields: dict[str, Field], db: DataBase):
273
+ habit = int(row_widgets["id"].text())
274
+ habit_type = int(row_widgets["type"].field_data)
275
+
276
+ button = HabitMarkButton(habit_type)
277
+ button.clicked.connect(lambda: mark_btn_clicked(button, habit, db))
278
+
279
+ marked = db.cur.execute("SELECT COUNT(*) FROM HabitMarks WHERE habit = ? AND date = ?", (habit, QDate.currentDate().toString(DateField.date_format))).fetchall()[0][0] > 0
280
+ if marked:
281
+ button.setText("Помечена выполненной за сегодня")
282
+ button.setProperty("class", "green-button" if habit_type == 0 else "red-button")
283
+ else:
284
+ button.setText("Пометить выполненной за сегодня")
285
+ button.marked = marked
286
+
287
+ if habit in remind_times.keys():
288
+ remind_times[habit]["completed"] = marked
289
+
290
+ return button
291
+
292
+ def habit_row_changed(row_widgets: dict[str, QWidget], fields: dict[str, Field], additional_columns: dict[str, TableColumn], db: DataBase):
293
+ habit_type: int = int(row_widgets["type"].field_data)
294
+ button: HabitMarkButton = row_widgets["mark_btn"]
295
+
296
+ button.habit_type = habit_type
297
+ if button.marked:
298
+ set_class_field(button, "green-button" if habit_type == 0 else "red-button")
299
+
300
+ def habit_after_save(form_widgets: dict[str, FormFieldWidget], fields: dict[str, Field], db: DataBase):
301
+ habit_id = int(form_widgets["id"].field_widget.text())
302
+ remind_every = form_widgets["remind_every"]
303
+ remind_at = form_widgets["remind_at"]
304
+
305
+ remind_every_activated = remind_every.is_activated()
306
+ remind_at_activated = remind_at.is_activated()
307
+
308
+ with remind_times_mutex:
309
+ if form_widgets["type"].field_widget.currentData() == 1:
310
+ if habit_id in remind_times.keys():
311
+ del remind_times[habit_id]
312
+ return True, ""
313
+
314
+ if habit_id in remind_times.keys():
315
+ if not remind_every_activated and not remind_at_activated:
316
+ del remind_times[habit_id]
317
+ return True, ""
318
+ else:
319
+ remind_times[habit_id] = {}
320
+
321
+ remind_times[habit_id]["remind_every"] = remind_every.field_widget.value() if remind_every_activated else None
322
+ remind_times[habit_id]["remind_at"] = QTime.toString(remind_at.field_widget.time(), TimeField.time_format) if remind_at_activated else None
323
+ if remind_every_activated:
324
+ if "last_time_reminder" not in remind_times[habit_id].keys() or remind_times[habit_id]["last_time_reminder"] is None:
325
+ remind_times[habit_id]["last_time_reminder"] = QTime.currentTime().addSecs(60*random.randint(30, int(remind_every.field_widget.value())*60))
326
+
327
+ app = App(app_name, SQLiteDB("dev360.db"), 1280, 720, "dev360_icon.ico")
328
+
329
+ app.db.cur.execute("""
330
+ CREATE TABLE IF NOT EXISTS HabitMarks
331
+ (
332
+ habit INTEGER REFERENCES Habits (id) ON DELETE CASCADE NOT NULL,
333
+ score INTEGER DEFAULT (0) NOT NULL,
334
+ date TEXT UNIQUE NOT NULL,
335
+ FOREIGN KEY (
336
+ habit
337
+ )
338
+ REFERENCES Habits (id) ON DELETE CASCADE
339
+ )
340
+ """)
341
+ app.db.conn.commit()
342
+
343
+ habits_table = TableTab("Привычки", "Habits", create_form_handler=create_form, table_row_change_handler=habit_row_changed, after_save_handler=habit_after_save,
344
+ additional_columns=[TableColumn(" "*31, "mark_btn", mark_btn_generator)])
345
+ habits_table.add_field(TextLineField("Название", "name", placeholder="Короткое название привычки", not_null=True, column_width=200))
346
+ habits_table.add_field(TextAreaField("Описание", "description", placeholder="Описание привычки или комментарий", column_width=300))
347
+ habits_table.add_field(ComboBoxField("Тип", "type", StaticItems(["Нужно развивать", "Нужно избавляться"]), column_width=113, on_change_handler=update_habits_type))
348
+ habits_table.add_field(ComboBoxField("Сложность привычки", "complexity", StaticItems(["Лёгкая", "Средняя", "Сложная"])))
349
+ habits_table.add_field(IntField("Напоминать раз в (часов)", "remind_every", min_value=1, max_value=24, tooltip="Регулярная отправка напоминаний раз в некоторое количество часов", is_optional=True))
350
+ habits_table.add_field(TimeField("Напоминать в", "remind_at", column_width=100, is_optional=True))
351
+ app.add_tab(habits_table)
352
+
353
+ notes_table = TableTab("Заметки", "Notes", "Здесь вы можете записывать свои мысли, успехи и трудности, связанные с выполнением привычек. Каждая заметка относится к определённой привычке.")
354
+ notes_table.add_field(DateField("Дата", "date", max_date="now", default="now", column_width=100))
355
+ notes_table.add_field(ComboBoxField("Привычка", "habit", TableFieldItems("Habits", "name", app.db), column_width=200))
356
+ notes_table.add_field(TextAreaField("Текст", "text", placeholder="Текст заметки", column_width=250))
357
+ notes_table.add_field(TableField("Прикреплённые изображения", "files", "NotesFiles", [FileField("Имя файла", "name", column_width=200)]))
358
+ app.add_tab(notes_table)
359
+
360
+ habits_report = ReportTab("Отчёт по выполненным привычкам", "", [
361
+ LineChartReport("График выполненных привычек", """
362
+ WITH RECURSIVE
363
+ HM AS (
364
+ SELECT date AS mark_date, type
365
+ FROM HabitMarks
366
+ LEFT JOIN Habits ON Habits.id = HabitMarks.habit
367
+ WHERE 1 {AND habit = :habit} {AND date >= :from} {AND date <= :to}
368
+ ),
369
+ HMType0 AS (
370
+ SELECT mark_date
371
+ FROM HM
372
+ WHERE type = 0
373
+ ),
374
+ HMType1 AS (
375
+ SELECT mark_date
376
+ FROM HM
377
+ WHERE type = 1
378
+ ),
379
+ MinMax AS (
380
+ SELECT
381
+ strftime(IIF(:ctype = '0', '%Y-01-01', IIF(:ctype = '1', '%Y-%m-01', '%Y-%m-%d')), date({MAX(MIN(mark_date), :from)}[MIN(mark_date)], IIF(:ctype = '2', 'weekday 0', '+0 days'))) AS min_date,
382
+ strftime(IIF(:ctype = '0', '%Y-01-01', IIF(:ctype = '1', '%Y-%m-01', '%Y-%m-%d')), date({MIN(MAX(mark_date), :to)}[MAX(mark_date)], IIF(:ctype = '2', 'weekday 0', '+0 days'))) AS max_date
383
+ FROM HM
384
+ ),
385
+ Months AS (
386
+ SELECT min_date AS month_date
387
+ FROM MinMax
388
+ WHERE min_date IS NOT NULL
389
+ UNION ALL
390
+ SELECT date(month_date, IIF(:ctype = '0', '+1 year', IIF(:ctype = '1', '+1 month', IIF(:ctype = '2', '+7 days', '+1 days'))))
391
+ FROM Months
392
+ WHERE month_date < (SELECT max_date FROM MinMax)
393
+ ),
394
+ Type0 AS (
395
+ SELECT
396
+ strftime(IIF(:ctype = '1', '%Y-%m', IIF(:ctype = '3', '%Y-%m-%d', '%Y ')), Months.month_date) || IIF(:ctype = '2', strftime('%W', Months.month_date)+1, '') AS month,
397
+ COUNT(HMType0.mark_date) AS count,
398
+ "Нужно развивать" AS type
399
+ FROM Months
400
+ LEFT JOIN HMType0 ON strftime(IIF(:ctype = '1', '%Y-%m', IIF(:ctype = '3', '%Y-%m-%d', '%Y ')), HMType0.mark_date) || IIF(:ctype = '2', strftime('%W', HMType0.mark_date)+1, '') = month
401
+ GROUP BY Months.month_date
402
+ ORDER BY Months.month_date
403
+ ),
404
+ Type1 AS (
405
+ SELECT
406
+ strftime(IIF(:ctype = '1', '%Y-%m', IIF(:ctype = '3', '%Y-%m-%d', '%Y ')), Months.month_date) || IIF(:ctype = '2', strftime('%W', Months.month_date)+1, '') AS month,
407
+ COUNT(HMType1.mark_date) AS count,
408
+ "Нужно избавляться" AS type
409
+ FROM Months
410
+ LEFT JOIN HMType1 ON strftime(IIF(:ctype = '1', '%Y-%m', IIF(:ctype = '3', '%Y-%m-%d', '%Y ')), HMType1.mark_date) || IIF(:ctype = '2', strftime('%W', HMType1.mark_date)+1, '') = month
411
+ GROUP BY Months.month_date
412
+ ORDER BY Months.month_date
413
+ )
414
+ SELECT * FROM Type0
415
+ UNION
416
+ SELECT * FROM Type1
417
+ ORDER BY month
418
+ """, x="month", values="count", lines="type", int_y_labels=True, stretch=10)],
419
+ [ComboBoxField("Привычка", "habit", TableFieldItems("Habits", "name", app.db), is_optional=True, not_null=True),
420
+ ComboBoxField("Считать по", "ctype", StaticItems(["Годам", "Месяцам", "Неделям", "Дням"]), default=1),
421
+ DateField("С", "from", default="now", tooltip="Начальная дата периода", is_optional=True),
422
+ DateField("До", "to", default="now", tooltip="Конечная дата периода", is_optional=True)])
423
+ app.add_tab(habits_report)
424
+
425
+ score_report = ReportTab("Отчёт по накопленным баллам", "", [
426
+ ListReport("""
427
+ SELECT COALESCE(SUM(HabitMarks.score), 0) AS score
428
+ FROM HabitMarks
429
+ WHERE 1 {AND habit = :habit} {AND date >= :from} {AND date <= :to}
430
+ """, [LabelField("Всего баллов за период", "score")], stretch=1),
431
+ LineChartReport("График распределения баллов по времени", """
432
+ WITH RECURSIVE
433
+ HM AS (
434
+ SELECT * FROM HabitMarks
435
+ WHERE 1 {AND habit = :habit} {AND date >= :from} {AND date <= :to}
436
+ ),
437
+ MinMax AS (
438
+ SELECT
439
+ strftime(IIF(:ctype = '0', '%Y-01-01', IIF(:ctype = '1', '%Y-%m-01', '%Y-%m-%d')), date(MIN(date), IIF(:ctype = '2', 'weekday 0', '+0 days'))) AS min_date,
440
+ strftime(IIF(:ctype = '0', '%Y-01-01', IIF(:ctype = '1', '%Y-%m-01', '%Y-%m-%d')), date(MAX(date), IIF(:ctype = '2', 'weekday 0', '+0 days'))) AS max_date
441
+ FROM HM
442
+ ),
443
+ Months AS (
444
+ SELECT min_date AS month_date
445
+ FROM MinMax
446
+ WHERE min_date IS NOT NULL
447
+ UNION ALL
448
+ SELECT date(month_date, IIF(:ctype = '0', '+1 year', IIF(:ctype = '1', '+1 month', IIF(:ctype = '2', '+7 days', '+1 days'))))
449
+ FROM Months
450
+ WHERE month_date < (SELECT max_date FROM MinMax)
451
+ )
452
+ SELECT
453
+ strftime(IIF(:ctype = '1', '%Y-%m', IIF(:ctype = '3', '%Y-%m-%d', '%Y ')), Months.month_date) || IIF(:ctype = '2', strftime('%W', Months.month_date)+1, '') AS month,
454
+ COALESCE(SUM(HM.score), 0) AS score
455
+ FROM Months
456
+ LEFT JOIN HM ON strftime(IIF(:ctype = '1', '%Y-%m', IIF(:ctype = '3', '%Y-%m-%d', '%Y ')), HM.date) || IIF(:ctype = '2', strftime('%W', HM.date)+1, '') = month
457
+ GROUP BY Months.month_date
458
+ ORDER BY Months.month_date
459
+ """, x="month", values="score", int_y_labels=True, stretch=10)],
460
+ [ComboBoxField("Привычка", "habit", TableFieldItems("Habits", "name", app.db), is_optional=True, not_null=True),
461
+ ComboBoxField("Считать по", "ctype", StaticItems(["Годам", "Месяцам", "Неделям", "Дням"]), default=1),
462
+ DateField("С", "from", default="now", tooltip="Начальная дата периода", is_optional=True),
463
+ DateField("До", "to", default="now", tooltip="Конечная дата периода", is_optional=True)])
464
+ app.add_tab(score_report)
465
+
466
+ remind_times: dict[int, dict] = {int(x["id"]): {"name": x["name"], "completed": False, "remind_every": x["remind_every"], "remind_at": x["remind_at"], "last_time_reminder": QTime.currentTime().addSecs(60*random.randint(30, int(x["remind_every"])*60)) if x["remind_every"] is not None else None} for x in
467
+ app.db.cur.execute("SELECT id, name, remind_every, remind_at FROM Habits WHERE type = 0 AND (remind_every IS NOT NULL OR remind_at IS NOT NULL)").fetchall()
468
+ }
469
+ remind_times_mutex = threading.Lock()
470
+
471
+ def notifications():
472
+ while True:
473
+ time.sleep(1)
474
+
475
+ with remind_times_mutex:
476
+ current_time = QTime.currentTime()
477
+ current_time_str = current_time.toString(TimeField.time_format)
478
+
479
+ find = False
480
+ habit_name = ""
481
+ for remind in remind_times.values():
482
+ if remind["completed"]:
483
+ continue
484
+
485
+ if remind["remind_at"] == current_time_str:
486
+ find = True
487
+ habit_name = remind["name"]
488
+ break
489
+ if remind["remind_every"] is not None and current_time_str == remind["last_time_reminder"].addSecs(60*60*int(remind["remind_every"])).toString(TimeField.time_format):
490
+ remind["last_time_reminder"] = current_time
491
+ find = True
492
+ habit_name = remind["name"]
493
+ break
494
+
495
+ # if find:
496
+ # threading.Thread(target=lambda:notifypy.Notify(
497
+ # app_name,
498
+ # f"Напоминание, что вы должны выполнить привычку \"{habit_name}\"",
499
+ # app_name,
500
+ # default_notification_icon="dev360_icon.ico").send(), daemon=True).start()
501
+
502
+ # threading.Thread(target=notifications, daemon=True).start()
503
+
504
+ app.run()