ddgbasic 1.0.0__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.
- ddgbasic-1.0.0.dist-info/METADATA +473 -0
- ddgbasic-1.0.0.dist-info/RECORD +11 -0
- ddgbasic-1.0.0.dist-info/WHEEL +5 -0
- ddgbasic-1.0.0.dist-info/entry_points.txt +2 -0
- ddgbasic-1.0.0.dist-info/licenses/LICENSE +21 -0
- ddgbasic-1.0.0.dist-info/top_level.txt +5 -0
- ddgbasic.py +14883 -0
- ddgformgen.py +295 -0
- ddgide.py +1277 -0
- ddgreport.py +175 -0
- ddgscreen.py +2205 -0
ddgformgen.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The program MAKE FORM writes: a data-entry application for one table, in plain DDGBasic,
|
|
3
|
+
with a menu bar (Browse, Add, Edit, Delete), an entry form with one field per column,
|
|
4
|
+
F1 help, and messages for changes the database refuses.
|
|
5
|
+
|
|
6
|
+
build_form_program() is pure: it takes a description of the table and returns the
|
|
7
|
+
program's text, so it can be tested without a database.
|
|
8
|
+
"""
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
_INT_TYPES = ('INT',) # INTEGER, BIGINT, SMALLINT, ...
|
|
12
|
+
_REAL_TYPES = ('REAL', 'FLOA', 'DOUB', 'DEC', 'NUM')
|
|
13
|
+
MAX_FIELDS = 26 # two columns of 13 fields
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FormColumn:
|
|
17
|
+
"""One column of the table, as the form needs it.
|
|
18
|
+
|
|
19
|
+
kind is 'text', 'int', 'real', 'date', 'bool' or 'list'; width is the text width;
|
|
20
|
+
items_sql is the query of a 'list' field; auto marks a key the database numbers."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, name, kind='text', width=20, required=False, items_sql=None, hint_ref='',
|
|
23
|
+
decimals=2, digits=9, auto=False):
|
|
24
|
+
self.name, self.kind, self.width, self.required = name, kind, width, required
|
|
25
|
+
self.items_sql, self.hint_ref, self.auto = items_sql, hint_ref, auto
|
|
26
|
+
self.decimals, self.digits = decimals, digits
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def column_kind(declared_type):
|
|
30
|
+
"""'int', 'real', 'date', 'bool' or 'text' for a declared SQL type."""
|
|
31
|
+
t = (declared_type or '').upper()
|
|
32
|
+
if 'BOOL' in t:
|
|
33
|
+
return 'bool'
|
|
34
|
+
if t == 'DATE':
|
|
35
|
+
return 'date'
|
|
36
|
+
if 'TIME' in t:
|
|
37
|
+
return 'text' # DATETIME, TIMESTAMP: typed as text
|
|
38
|
+
if any(k in t for k in _INT_TYPES):
|
|
39
|
+
return 'int'
|
|
40
|
+
if any(k in t for k in _REAL_TYPES):
|
|
41
|
+
return 'real'
|
|
42
|
+
return 'text'
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def decimal_places(declared_type):
|
|
46
|
+
"""(digits before the point, decimals) from DECIMAL(p, s), or (9, 2)."""
|
|
47
|
+
m = re.search(r'\(\s*(\d+)\s*,\s*(\d+)\s*\)', declared_type or '')
|
|
48
|
+
if m:
|
|
49
|
+
p, s = int(m.group(1)), int(m.group(2))
|
|
50
|
+
return max(1, min(p - s, 12)), min(s, 6)
|
|
51
|
+
return 9, 2
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def label_of(name):
|
|
55
|
+
return name.replace('_', ' ').title()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _mask(col):
|
|
59
|
+
"""The PICTURE of a numeric field: digits grouped with commas, like a calculator."""
|
|
60
|
+
digits = '9' * (col.digits if col.kind == 'real' else 10)
|
|
61
|
+
if col.kind == 'int':
|
|
62
|
+
return digits
|
|
63
|
+
groups = []
|
|
64
|
+
while digits:
|
|
65
|
+
groups.insert(0, digits[-3:])
|
|
66
|
+
digits = digits[:-3]
|
|
67
|
+
return ','.join(groups) + ('.' + '9' * col.decimals if col.decimals else '')
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _hint(col):
|
|
71
|
+
if col.kind == 'int':
|
|
72
|
+
text = 'A whole number'
|
|
73
|
+
elif col.kind == 'real':
|
|
74
|
+
text = 'A number: digits, then . for the decimals'
|
|
75
|
+
elif col.kind == 'date':
|
|
76
|
+
text = 'A date: YYYY-MM-DD'
|
|
77
|
+
elif col.kind == 'bool':
|
|
78
|
+
text = 'Space ticks or clears the box'
|
|
79
|
+
elif col.kind == 'list':
|
|
80
|
+
text = f'Space or F2 opens the list of {col.hint_ref}; a letter jumps'
|
|
81
|
+
else:
|
|
82
|
+
text = f'Text, up to {col.width} characters'
|
|
83
|
+
return text + (' (required)' if col.required else '')
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _q(text):
|
|
87
|
+
"""A DDGBasic string literal."""
|
|
88
|
+
return '"' + str(text).replace('"', '""') + '"'
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def build_form_program(table, columns, key, engine, database, title="", made_on="",
|
|
92
|
+
quote=lambda name: name):
|
|
93
|
+
"""The program's text.
|
|
94
|
+
|
|
95
|
+
table the table's name columns FormColumn list, in table order
|
|
96
|
+
key the column that identifies a row, or 'rowid' (SQLite tables without one)
|
|
97
|
+
engine 'sqlite' or 'duckdb' database the file the program opens
|
|
98
|
+
quote quote(name) gives a column or table name as SQL needs it"""
|
|
99
|
+
title = title or label_of(table)
|
|
100
|
+
singular = title[:-1] if title.endswith('s') and len(title) > 3 else title
|
|
101
|
+
t_sql = quote(table)
|
|
102
|
+
fields = [c for c in columns if not c.auto]
|
|
103
|
+
if not fields:
|
|
104
|
+
raise ValueError(f"MAKE FORM: the table {table} has no columns to edit.")
|
|
105
|
+
if len(fields) > MAX_FIELDS:
|
|
106
|
+
raise ValueError(f"MAKE FORM: {table} has {len(fields)} columns; a form holds at most {MAX_FIELDS}. "
|
|
107
|
+
"Make a view or a narrower table, or build the form by hand.")
|
|
108
|
+
key_name = 'row_id' if key == 'rowid' else key
|
|
109
|
+
key_sql = 'rowid' if key == 'rowid' else quote(key)
|
|
110
|
+
shown = [c for c in columns if c.name != key and c.kind not in ('bool',)][:3]
|
|
111
|
+
if not shown:
|
|
112
|
+
shown = [c for c in columns if c.name != key][:1]
|
|
113
|
+
first = shown[0].name if shown else key_name
|
|
114
|
+
lines = []
|
|
115
|
+
add = lines.append
|
|
116
|
+
|
|
117
|
+
# --- header ------------------------------------------------------
|
|
118
|
+
add('REM ' + '=' * 67)
|
|
119
|
+
add(f'REM {title}: a data-entry program for the table {table}.')
|
|
120
|
+
add(f'REM Made by MAKE FORM{" on " + made_on if made_on else ""}. It is plain DDGBasic: change it freely.')
|
|
121
|
+
add('REM F1 shows the keys. Browse edits the table as a grid; Add, Edit and Delete use the form.')
|
|
122
|
+
add('REM ' + '=' * 67)
|
|
123
|
+
add(f'DATABASE ENGINE {engine.upper()}')
|
|
124
|
+
add(f'DATABASE OPEN {_q(database)}')
|
|
125
|
+
add('SET SQL_FATAL_ERRORS OFF')
|
|
126
|
+
add('REM (a change the database refuses sets ERR and ERRMSG$, and is shown with ALERT)')
|
|
127
|
+
add('')
|
|
128
|
+
help_lines = ['F1 This help, from anywhere',
|
|
129
|
+
'Menu bar Left/Right move, Enter or a letter opens a menu',
|
|
130
|
+
'Grid Enter edits a cell, Ins adds a row, Del deletes,',
|
|
131
|
+
' click a heading or press F3 to sort',
|
|
132
|
+
'Pick list Type to jump to a row, Enter chooses',
|
|
133
|
+
'Form Tab/Enter next field, Space or F2 opens a list or',
|
|
134
|
+
' ticks a box, Ctrl-Y clears a field, Esc cancels']
|
|
135
|
+
add(f'keys$ = {_q(help_lines[0])} + CHR$(10)')
|
|
136
|
+
for n, text in enumerate(help_lines[1:], 1):
|
|
137
|
+
add(f'keys$ = keys$ + {_q(text)}' + (' + CHR$(10)' if n < len(help_lines) - 1 else ''))
|
|
138
|
+
add('FUNCTION show_help()')
|
|
139
|
+
add(' VIEW keys$ AT 6, 8, 16, 74 TITLE "Keys (Esc closes)"')
|
|
140
|
+
add('END FUNCTION')
|
|
141
|
+
add('ON KEY "F1" CALL show_help')
|
|
142
|
+
add('')
|
|
143
|
+
|
|
144
|
+
# --- main loop ---------------------------------------------------
|
|
145
|
+
add('SET THEME "classic" REM or default, norton, green, amber, paper, mono')
|
|
146
|
+
add('DO')
|
|
147
|
+
add(' GOSUB desktop')
|
|
148
|
+
menu = re.sub(r'[^A-Za-z0-9 ]', '', title).strip() or 'Records'
|
|
149
|
+
add(f' choice$ = MENUBAR$("File:Quit", "{menu}:Browse|Add|Edit|Delete", "Help:Keys")')
|
|
150
|
+
add(f' IF choice$ = "{menu}.Browse" THEN GOSUB browse_records')
|
|
151
|
+
add(f' IF choice$ = "{menu}.Add" THEN GOSUB add_record')
|
|
152
|
+
add(f' IF choice$ = "{menu}.Edit" THEN GOSUB edit_record')
|
|
153
|
+
add(f' IF choice$ = "{menu}.Delete" THEN GOSUB delete_record')
|
|
154
|
+
add(' IF choice$ = "Help.Keys" THEN x = show_help()')
|
|
155
|
+
add('LOOP UNTIL choice$ = "File.Quit" OR choice$ = ""')
|
|
156
|
+
add('SET THEME "default"')
|
|
157
|
+
add('CLS')
|
|
158
|
+
add('END')
|
|
159
|
+
add('')
|
|
160
|
+
|
|
161
|
+
add('desktop:')
|
|
162
|
+
add(' CLS')
|
|
163
|
+
add(f' BOX 2, 1, 23, 80, {_q(title.upper())} DOUBLE')
|
|
164
|
+
add(f' @ 4, 4 SAY LTRIM$(STR$((SELECT COUNT(*) FROM {t_sql}))) & " records in {table}."')
|
|
165
|
+
add(f' @ 6, 4 SAY "{menu}: Browse, Add, Edit or Delete. F1: keys."')
|
|
166
|
+
add(' @ 24, 2 SAY "F1 help Arrows move Enter chooses Esc goes back Letters are hot keys"')
|
|
167
|
+
add(' RETURN')
|
|
168
|
+
add('')
|
|
169
|
+
|
|
170
|
+
# --- browse --------------------------------------------------------
|
|
171
|
+
add('browse_records:')
|
|
172
|
+
add(' REM The whole table as a grid: changes are saved as they are made')
|
|
173
|
+
if key == 'rowid':
|
|
174
|
+
add(f' BROWSE {_q(table)} EDIT AT 3, 3, 21, 78 TITLE "{title} (Enter edits, Ins adds, Del deletes, F3 sorts)"')
|
|
175
|
+
else:
|
|
176
|
+
add(f' BROWSE (SELECT * FROM {t_sql} ORDER BY {quote(first) if first != key_name else key_sql}) '
|
|
177
|
+
f'EDIT KEY {_q(key)} _')
|
|
178
|
+
add(f' AT 3, 3, 21, 78 TITLE "{title} (Enter edits, Ins adds, Del deletes, F3 sorts)"')
|
|
179
|
+
add(' RETURN')
|
|
180
|
+
add('')
|
|
181
|
+
|
|
182
|
+
# --- pick ------------------------------------------------------------
|
|
183
|
+
pick_cols = [quote(c.name) for c in shown if c.name != key]
|
|
184
|
+
key_col = f'rowid AS row_id' if key == 'rowid' else quote(key)
|
|
185
|
+
add('pick_record:')
|
|
186
|
+
add(' REM Choose a row: p gets it, or an empty hash after Esc. Typing jumps to a row.')
|
|
187
|
+
add(f' BROWSE (SELECT {", ".join(pick_cols + [key_col])} FROM {t_sql} ORDER BY {quote(first) if pick_cols else key_sql}) _')
|
|
188
|
+
add(f' AT 3, 3, 21, 78 TITLE "Pick a {singular.lower()} (type to find)" INTO p')
|
|
189
|
+
add(' RETURN')
|
|
190
|
+
add('')
|
|
191
|
+
|
|
192
|
+
# --- the form --------------------------------------------------------
|
|
193
|
+
per_column = 13
|
|
194
|
+
two = len(fields) > per_column
|
|
195
|
+
label_w = max(len(label_of(c.name)) for c in fields)
|
|
196
|
+
rows_used = min(len(fields), per_column)
|
|
197
|
+
top, bottom = 3, 3 + rows_used + 6
|
|
198
|
+
left, right = (2, 79) if two else (6, 75)
|
|
199
|
+
field_room = (right - left - 6) // (2 if two else 1) - label_w - 5
|
|
200
|
+
add('form:')
|
|
201
|
+
add(' REM The entry form for the hash r. BUTTON$ says how it ended ("Save" or "").')
|
|
202
|
+
add(' saved$ = SAVESCREEN()')
|
|
203
|
+
add(f' BOX {top}, {left}, {bottom}, {right}, form_title$ SHADOW')
|
|
204
|
+
for n, c in enumerate(fields):
|
|
205
|
+
row = top + 2 + (n % per_column)
|
|
206
|
+
col = left + 3 + (n // per_column) * ((right - left) // 2)
|
|
207
|
+
say = _q(label_of(c.name).ljust(label_w) + ' : ')
|
|
208
|
+
target = f'r.{c.name}'
|
|
209
|
+
parts = [f' @ {row}, {col} SAY {say} GET {target}']
|
|
210
|
+
if c.kind == 'text':
|
|
211
|
+
parts.append(f'PICTURE "@S{max(1, min(c.width, field_room))}"')
|
|
212
|
+
elif c.kind in ('int', 'real'):
|
|
213
|
+
parts.append(f'PICTURE "{_mask(c)}"')
|
|
214
|
+
elif c.kind == 'date':
|
|
215
|
+
parts.append('PICTURE "@D"')
|
|
216
|
+
elif c.kind == 'bool':
|
|
217
|
+
parts.append('CHECKBOX')
|
|
218
|
+
elif c.kind == 'list':
|
|
219
|
+
parts.append(f'LIST ({c.items_sql})')
|
|
220
|
+
if c.required and c.kind in ('text', 'date', 'list'):
|
|
221
|
+
parts.append(f'VALID LEN(TRIM$({target})) > 0 MESSAGE {_q(label_of(c.name) + " is required")}')
|
|
222
|
+
line = ' '.join(parts)
|
|
223
|
+
hint = f'HINT {_q(_hint(c))}'
|
|
224
|
+
if len(line) + len(hint) + 1 > 118:
|
|
225
|
+
add(line + ' _')
|
|
226
|
+
add(' ' + hint)
|
|
227
|
+
else:
|
|
228
|
+
add(line + ' ' + hint)
|
|
229
|
+
add(f' @ {bottom - 2}, {left + 3} BUTTON "Save"')
|
|
230
|
+
add(f' @ {bottom - 2}, {left + 13} BUTTON "Cancel" CANCEL')
|
|
231
|
+
add(' READ')
|
|
232
|
+
add(' RESTSCREEN saved$')
|
|
233
|
+
add(' RETURN')
|
|
234
|
+
add('')
|
|
235
|
+
|
|
236
|
+
def value_sql(c):
|
|
237
|
+
bind = f':r.{c.name}'
|
|
238
|
+
return f'({bind} <> 0)' if c.kind == 'bool' else bind
|
|
239
|
+
|
|
240
|
+
# --- add -----------------------------------------------------------------
|
|
241
|
+
add('add_record:')
|
|
242
|
+
blanks = []
|
|
243
|
+
for c in fields:
|
|
244
|
+
blanks.append(f'r.{c.name} = ' + ('0' if c.kind in ('int', 'real', 'bool') else '""'))
|
|
245
|
+
for n in range(0, len(blanks), 4):
|
|
246
|
+
add(' ' + ' : '.join(blanks[n:n + 4]))
|
|
247
|
+
add(f' form_title$ = {_q("New " + singular.lower())}')
|
|
248
|
+
add(' DO')
|
|
249
|
+
add(' GOSUB form')
|
|
250
|
+
add(' IF BUTTON$ <> "Save" THEN EXIT DO')
|
|
251
|
+
add(f' INSERT INTO {t_sql} ({", ".join(quote(c.name) for c in fields)})')
|
|
252
|
+
add(f' VALUES ({", ".join(value_sql(c) for c in fields)});')
|
|
253
|
+
add(' IF ERR = 0 THEN EXIT DO')
|
|
254
|
+
add(' x = ALERT("Not saved;" & ERRMSG$)')
|
|
255
|
+
add(' LOOP')
|
|
256
|
+
add(' RETURN')
|
|
257
|
+
add('')
|
|
258
|
+
|
|
259
|
+
# --- edit -----------------------------------------------------------------
|
|
260
|
+
def select_expr(c):
|
|
261
|
+
if c.kind in ('int', 'real', 'bool'):
|
|
262
|
+
zero = 'false' if c.kind == 'bool' and engine == 'duckdb' else '0'
|
|
263
|
+
return f'COALESCE({quote(c.name)}, {zero}) AS {quote(c.name)}'
|
|
264
|
+
return quote(c.name)
|
|
265
|
+
|
|
266
|
+
add('edit_record:')
|
|
267
|
+
add(' GOSUB pick_record')
|
|
268
|
+
add(' IF LASTKEY$ = CHR$(27) THEN RETURN')
|
|
269
|
+
add(f' FOR r IN (SELECT {", ".join(select_expr(c) for c in fields)} FROM {t_sql} WHERE {key_sql} = :p.{key_name})')
|
|
270
|
+
add(f' form_title$ = {_q("Edit " + singular.lower())}')
|
|
271
|
+
add(' DO')
|
|
272
|
+
add(' GOSUB form')
|
|
273
|
+
add(' IF BUTTON$ <> "Save" THEN EXIT DO')
|
|
274
|
+
sets = [f'{quote(c.name)} = {value_sql(c)}' for c in fields]
|
|
275
|
+
add(f' UPDATE {t_sql} SET {sets[0]}' + (',' if len(sets) > 1 else ''))
|
|
276
|
+
for n, s in enumerate(sets[1:], 1):
|
|
277
|
+
add(f' {s}' + (',' if n < len(sets) - 1 else ''))
|
|
278
|
+
add(f' WHERE {key_sql} = :p.{key_name};')
|
|
279
|
+
add(' IF ERR = 0 THEN EXIT DO')
|
|
280
|
+
add(' x = ALERT("Not saved;" & ERRMSG$)')
|
|
281
|
+
add(' LOOP')
|
|
282
|
+
add(' NEXT')
|
|
283
|
+
add(' RETURN')
|
|
284
|
+
add('')
|
|
285
|
+
|
|
286
|
+
# --- delete --------------------------------------------------------------
|
|
287
|
+
add('delete_record:')
|
|
288
|
+
add(' GOSUB pick_record')
|
|
289
|
+
add(' IF LASTKEY$ = CHR$(27) THEN RETURN')
|
|
290
|
+
add(f' IF ALERT("Delete " & p.{first} & "?", "No", "Yes") = 2 THEN')
|
|
291
|
+
add(f' DELETE FROM {t_sql} WHERE {key_sql} = :p.{key_name};')
|
|
292
|
+
add(' IF ERR THEN x = ALERT("Not deleted;" & ERRMSG$)')
|
|
293
|
+
add(' END IF')
|
|
294
|
+
add(' RETURN')
|
|
295
|
+
return '\n'.join(lines) + '\n'
|