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.
- bpappbuilder-0.1.0/.gitignore +8 -0
- bpappbuilder-0.1.0/.pypi_token +1 -0
- bpappbuilder-0.1.0/LICENSE +21 -0
- bpappbuilder-0.1.0/PKG-INFO +21 -0
- bpappbuilder-0.1.0/README.md +6 -0
- bpappbuilder-0.1.0/apps/development360/dev360_icon.ico +0 -0
- bpappbuilder-0.1.0/apps/development360/dev360_icon.svg +1 -0
- bpappbuilder-0.1.0/apps/development360/development360.py +504 -0
- bpappbuilder-0.1.0/apps/development360//320/227/320/260/320/264/320/260/321/207/320/260 1.doc +0 -0
- bpappbuilder-0.1.0/apps/technoholding/kriteriiZ2.xlsx +0 -0
- bpappbuilder-0.1.0/apps/technoholding/technoholding.py +233 -0
- 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
- 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
- bpappbuilder-0.1.0/apps/vunderkind/vunderkind.py +93 -0
- bpappbuilder-0.1.0/pyproject.toml +30 -0
- bpappbuilder-0.1.0/src/bpappbuilder/__init__.py +7 -0
- bpappbuilder-0.1.0/src/bpappbuilder/app/__init__.py +5 -0
- bpappbuilder-0.1.0/src/bpappbuilder/app/app.py +134 -0
- bpappbuilder-0.1.0/src/bpappbuilder/app/db.py +47 -0
- bpappbuilder-0.1.0/src/bpappbuilder/app/styles.qss +243 -0
- bpappbuilder-0.1.0/src/bpappbuilder/app/themes.py +72 -0
- bpappbuilder-0.1.0/src/bpappbuilder/fields/__init__.py +29 -0
- bpappbuilder-0.1.0/src/bpappbuilder/fields/combo_box_items.py +93 -0
- bpappbuilder-0.1.0/src/bpappbuilder/fields/field.py +1194 -0
- bpappbuilder-0.1.0/src/bpappbuilder/reports/__init__.py +21 -0
- bpappbuilder-0.1.0/src/bpappbuilder/reports/report.py +1075 -0
- bpappbuilder-0.1.0/src/bpappbuilder/tabs/__init__.py +24 -0
- bpappbuilder-0.1.0/src/bpappbuilder/tabs/group.py +31 -0
- bpappbuilder-0.1.0/src/bpappbuilder/tabs/tab.py +80 -0
- bpappbuilder-0.1.0/src/bpappbuilder/tabs/table.py +544 -0
- bpappbuilder-0.1.0/src/bpappbuilder/tabs/table_tab.py +290 -0
|
@@ -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
|
+
|
|
Binary file
|
|
@@ -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="{"isPaintingLayer":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()
|
|
Binary file
|
|
Binary file
|