PaferaPy Async 0.1
ASGI framework focused on simplicity and efficiency
Loading...
Searching...
No Matches
db.py
Go to the documentation of this file.
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4import datetime
5import logging
6import re
7import json
8import threading
9import random
10import sqlite3
11import traceback
12
13from pprint import pprint
14
15from pafera.types import *
16from pafera.utils import *
17from pafera.validators import *
18
20
21# Security access constants
22DB_CAN_CREATE = 0x1
23DB_CANNOT_CREATE = 0x2
24DB_CAN_CHANGE = 0x4
25DB_CANNOT_CHANGE = 0x8
26DB_CAN_DELETE = 0x10
27DB_CANNOT_DELETE = 0x20
28DB_CAN_VIEW = 0x40
29DB_CANNOT_VIEW = 0x80
30DB_CAN_LINK = 0x100
31DB_CANNOT_LINK = 0x200
32DB_CAN_CHANGE_PERMISSIONS = 0x400
33DB_CANNOT_CHANGE_PERMISSIONS = 0x800
34DB_CAN_VIEW_PROTECTED = 0x1000
35DB_CANNOT_VIEW_PROTECTED = 0x2000
36
37DB_CAN_ALL = 0x555
38DB_CANNOT_ALL = 0xaaa
39
40# dbflag constants
41DB_DEBUG = 0x01
42DB_TRACK_CHANGES = 0x02
43DB_TRACK_VALUES = 0x04
44DB_TRACK_VIEW = 0x08
45DB_SECURE = 0x10
46DB_PRODUCTION = 0x20
47
48# *********************************************************************
50 """Bootstrap class for the database.
51
52 This class keep tracks of every database model sent to
53 db.RegisterModel() and enables them to show up in the web database
54 manager.
55 """
56
57 _dbfields = {
58 'typename': ('VARCHAR(255)', 'NOT NULL PRIMARY KEY',),
59 'access': ('INT', 'NOT NULL DEFAULT 0',),
60 'aclid': ('INT', 'NOT NULL DEFAULT 0',),
61 'flags': ('INT', 'NOT NULL DEFAULT 0',),
62 }
63 _dbdisplay = ['typename']
64 _dblinks = []
65 _dbflags = 0
66
67# *********************************************************************
69 """Bootstrap class for the database.
70
71 This class keeps track of ACL rules and exists to save space so that
72 rules do not need to be stored in every object with security enabled.
73 """
74
75 _dbfields = {
76 'id': ('INTEGER PRIMARY KEY AUTOINCREMENT', 'NOT NULL',),
77 'rules': ('DICT', "NOT NULL",),
78 }
79 _dbdisplay = ['rules']
80 _dblinks = []
81 _dbflags = 0
82
83# *********************************************************************
85 """Bootstrap class for the database.
86
87 This class allows the database object itself to be used like a DB2
88 key-value dict. Try
89
90 g.db['John'] = 'Cleese'
91
92 print(g.db['John'])
93 """
94
95 _dbfields = {
96 'dbkey': ('VARCHAR(255)', 'NOT NULL PRIMARY KEY',),
97 'dbvalue': ('SINGLETEXT', 'NOT NULL',),
98 }
99 _dbdisplay = ['dbkey', 'dbvalue']
100 _dblinks = []
101 _dbflags = 0
102
103# *********************************************************************
105 """Bootstrap class for the database.
106
107 This class keeps track of all changed values if you have enabled
108 tracking in dbflags. Future plans involve using this class to enabled
109 replication and rollbacks like the PHP version.
110 """
111
112 CREATED = 1;
113 MODIFIED = 2;
114 DELETED = 3;
115 RESTORED = 4;
116 ACCESSED = 5;
117
118 _dbfields = {
119 'id': ('INT PRIMARY KEY', '',),
120 'objtype': ('INT', 'NOT NULL',),
121 'eventtype': (
122 'ENUM',
123 'NOT NULL',
124 0,
125 {
126 'CREATED': 1,
127 'MODIFIED': 2,
128 'DELETED': 3,
129 'RESTORED': 4,
130 'ACCESSED': 5,
131 }
132 ),
133 'userid': ('INT', 'NOT NULL',),
134 'eventtime': ('BIGINT', 'NOT NULL',),
135 'changed': ('DICT', 'NOT NULL',),
136 'ids': ('NEWLINELIST', 'NOT NULL',),
137 }
138 _dbdisplay = ['objtype', 'eventtype', 'eventtime', 'changed']
139 _dblinks = []
140 _dbflags = 0
141
142# *********************************************************************
144 """Bootstrap class for the database.
145
146 Tagging system for every object in the system. Useful if you enjoy
147 creating blogs or galleries.
148 """
149
150 _dbfields = {
151 'id': ('INT PRIMARY KEY', '',),
152 'languageid': ('INT', 'NOT NULL',),
153 'tagname': ('SINGLETEXT', 'NOT NULL',),
154 }
155 _dbindexes = (
156 ('unique', ('languageid', 'tagname',)),
157 )
158 _dbdisplay = ['tagname']
159 _dblinks = []
160 _dbflags = 0
161
162# *********************************************************************
163class DB(object):
164 """The main database object. Note that Pafera allows you to create as
165 many database objects as you want, so you can keep one part of your
166 logic in a MySQL database, one part in an Sqlite database, and so
167 forth.
168
169 The current access is saved in db.userid and db.groups. Root access
170 can be enabled at any time by calling db.Sudo(), while debugging
171 is enabled through db.Debug().
172 """
173
174 LANGUAGES = {
175 1: ('en', 'English', 'English'),
176 2: ('zh', 'Chinese', '中文'),
177 }
178
179 DBTYPES = {
180 'sqlite': 1,
181 'mysql': 2,
182 'postgresql': 3,
183 }
184
185 CONNECTIONS = {}
186 DEFAULTCONN = None
187 DEFAULTFILE = "default.db"
188
189 # ----------------------------------------------------------------------------------------
190 def __init__(self,
191 connectionname = None,
192 dbname = None,
193 dbhost = 'localhost',
194 dbtype = 'sqlite',
195 dbuser = None,
196 dbpassword = None,
197 dbflags = DB_SECURE | DB_TRACK_CHANGES | DB_TRACK_VALUES,
198 closeondelete = 1
199 ):
200 """Basic database constructor.
201
202 Note that security must be enabled on *both* the database and the model to take effect.
203
204 Using closeondelete means that the object will automatically close the database
205 connection when it goes out of scope.
206 """
207
208 self.debug = 0
209 self.language = 1
210 self.objtypes = {}
211 self.userid = 0
212 self.groups = []
213
214 self.numsudos = 0
215 self.numdebugs = 0
216 self.dbtype = dbtype
217 self.dbflags = dbflags
218 self.closeondelete = closeondelete
219 self.cursor = 0
220 self.closed = 0
221 self.acls = {}
222
223 if not connectionname:
224 if not dbname:
225 self.connectionname = 'default'
226 self.dbname = DB.DEFAULTFILE
227 else:
228 self.connectionname = dbname
229 self.dbname = dbname
230
231 if dbtype == 'sqlite':
232 import sqlite3
233 self.dbconn = sqlite3.connect(dbname, 30)
234 self.dbconn.row_factory = sqlite3.Row
235 self.dbconn.execute("PRAGMA busy_timeout = 15000")
236 elif dbtype == 'mysql':
237 import mysql.connector as mysql
238
239 self.dbconn = mysql.connect(
240 host = dbhost,
241 user = dbuser,
242 passwd = dbpassword,
243 database = dbname,
244 )
245
246 self.dbconn.set_charset_collation('utf8mb4', 'utf8mb4_unicode_520_ci')
247 elif dbtype == 'postgresql':
248 import psycopg2 as postgresql
249
250 self.dbconn = postgresql.connect(
251 host = dbhost,
252 user = dbuser,
253 password = dbpassword,
254 database = dbname,
255 )
256 else:
257 raise Exception(f'Database type {dbtype} not supported')
258
259 self.connectionname = connectionname
260
261 DB.CONNECTIONS[connectionname] = (dbname, dbtype, self)
262
263 if not DB.DEFAULTCONN:
264 DB.DEFAULTCONN = self
265
266 # Initialize system tables on first run
267 try:
268 for r in self.Execute("SELECT typename, access, aclid, flags FROM system_objtype"):
269 self.objtypes[ r[0] ] = (r[1], r[2], r[3])
270
271 failed = 0
272 except Exception as e:
273 self.RegisterModel(system_objtype)
274 self.RegisterModel(system_acl)
275 self.RegisterModel(system_config)
276 self.RegisterModel(system_changelog)
277 self.RegisterModel(system_tag)
278
279 self.Commit()
280
281 for r in self.Execute("SELECT typename, access, aclid, flags FROM system_objtype"):
282 self.objtypes[ r[0] ] = (r[1], r[2], r[3])
283
284 # ----------------------------------------------------------------------------------------
285 def __del__(self):
286 """Standard destructor to automatically close the database connection
287 """
288 if self.closeondelete:
289 self.Close()
290
291 # ----------------------------------------------------------------------------------------
292 def Close(self):
293 """Close the database connection. You should normally never need to call this since the
294 object will automatically close the connectino when it goes out of scope
295 """
296 if self.closed:
297 return
298
299 try:
300 self.closed = 1
301 self.dbconn.close()
302 except Exception as e:
303 traceback.print_exc()
304
305 # ----------------------------------------------------------------------------------------
306 def Begin(self):
307 """Begins a transaction. Useful if you don't use auto-commit mode.
308 """
309 if self.cursor:
310 raise Exception("DB.Begin: Already in a transaction!")
311
312 self.cursor = self.dbconn.cursor()
313
314 if self.dbtype == 'mysql':
315 self.dbconn.start_transaction()
316 else:
317 self.cursor.execute("BEGIN TRANSACTION")
318
319 # ----------------------------------------------------------------------------------------
320 def Commit(self):
321 """Commits the current transaction. Be sure to call this at the end of your script or none
322 of your changes will take effect.
323 """
324 self.dbconn.commit()
325
326 if self.cursor:
327 self.cursor.close()
328 self.cursor = 0
329
330 # ----------------------------------------------------------------------------------------
331 def Rollback(self):
332 """Rolls back the current transaction. This will also automatically be done if you end
333 the script without calling db.Commit()
334 """
335 self.dbconn.rollback()
336
337 if self.cursor:
338 self.cursor.close()
339 self.cursor = 0
340
341 # ----------------------------------------------------------------------------------------
342 def Execute(self, query, params = [], many = 0, returndict = 0):
343 """Executes a query upon the database. Use db.Query() if you need a version which
344 automatically wraps the returned values into a dict. Use db.Debug() to see what is
345 being done behind the scenes.
346 """
347
348 if not (isinstance(params, list) or isinstance(params, tuple)):
349 params = (params,)
350
351 query = query.strip()
352
353 if self.dbtype == 'mysql':
354 query = query.replace('?', '%s')
355
356 if self.numdebugs:
357 print('DB: Executing query')
358 pprint(query)
359 print(f'with {len(params)} params')
360 pprint(params)
361
362 if self.cursor:
363 c = self.cursor
364 else:
365 c = self.dbconn.cursor()
366
367 if many:
368 c.executemany(query, params)
369 else:
370 c.execute(query, params)
371
372 if query.startswith('SELECT'):
373 results = c.fetchall()
374
375 if returndict:
376 ls = []
377
378 if self.dbtype == 'mysql':
379 columns = tuple([d[0] for d in c.description])
380
381 for row in results:
382 ls.append(dict(zip(columns, row)))
383 else:
384 for r in results:
385 ls.append(dict(zip(r.keys(), r)))
386
387 results = ls
388
389 else:
390 results = c.lastrowid
391
392 if self.cursor:
393 pass
394 else:
395 c.close()
396
397 if self.numdebugs:
398 if isinstance(results, list):
399 Print(f'DB: Query results with length {len(results)}')
400 pprint(results)
401 else:
402 Print(f'DB: Query results returned {results}')
403
404 return results
405
406 # ----------------------------------------------------------------------------------------
407 def ExecuteMany(self, query, params = []):
408 """Convenience function to execute many statements at once. Separated into its own function
409 for more readable code.
410 """
411 return self.Execute(query, params, many = 1)
412
413 # ----------------------------------------------------------------------------------------
414 def Query(self, query, params = []):
415 """Convenience function to execute many statements at once. Separated into its own function
416 for more readable code.
417 """
418 return self.Execute(query, params, many = 0, returndict = 1)
419
420 # ----------------------------------------------------------------------------------------
421 def Sudo(self, state):
422 """Enable or disable administrator mode, bypassing security for the current user.
423 """
424 if state:
425 self.numsudos += 1
426 elif self.numsudos > 0:
427 self.numsudos -= 1
428
429 # ----------------------------------------------------------------------------------------
430 def Debug(self, state):
431 """Enable or disable query debugging
432 """
433 if state:
434 self.numdebugs += 1
435 elif self.numdebugs > 0:
436 self.numdebugs -= 1
437
438 # ----------------------------------------------------------------------------------------
439 def RequireGroup(self, groupname, errmsg):
440 """Throws an exception if the current user does not belong to one of the groups listed
441 in groupname. It's a less expensive check than going through object security.
442 """
443 if isinstance(groupname, list):
444 found = 0
445
446 for g in groupname:
447 if g in self.groups:
448 found = 1
449 break
450
451 if not found:
452 raise Exception(errmsg)
453 else:
454 if groupname not in self.groups:
455 raise Exception(errmsg)
456
457 # ----------------------------------------------------------------------------------------
458 def HasGroup(self, groupname):
459 """Returns true if the current user belongs to the given group
460 """
461 return groupname in self.groups
462
463 # ----------------------------------------------------------------------------------------
464 def GetAccess(self, obj):
465 """Utility function to calculate the current user's access to the given database object.
466 It's only effective if security is enabled on both the database and the object's model.
467 """
468 if self.numsudos or 'admins' in self.groups or (hasattr(obj, 'dbowner') and obj.dbowner == self.userid):
469 return DB_CAN_ALL
470
471 typename, access, aclid = self.objtypes[obj._dbtable]
472
473 access = self.ApplyACL(access, aclid)
474
475 if hasattr(obj, 'dbaccess'):
476 access = self.ApplyAccess(access, obj.dbaccess)
477
478 if hasattr(obj, 'dbgroupaccess') and getattr(obj, 'dbgroup') in self.groupids:
479 access = self.ApplyAccess(access, obj.groupaccess)
480
481 if hasattr(obj, 'dbaclid'):
482 access = self.ApplyACL(access, obj.dbaclid)
483
484 return access
485
486 # ----------------------------------------------------------------------------------------
487 def GetACL(self, aclid):
488 """Get the rules for the given ACL. You should not need to call this yourself since it's
489 automatically handled by db.GetAccess()
490 """
491 acl = {}
492
493 if aclid not in self.acls:
494 acl = self.Execute('SELECT rules FROM system_acl WHERE id = ?', aclid)[0]
495 self.acls[aclid] = json.loads(acl) if acl else {}
496
497 return acl
498
499 # ----------------------------------------------------------------------------------------
500 def ApplyACL(self, access, aclid):
501 """Get the rules for the given ACL. You should not need to call this yourself since it's
502 automatically handled by db.GetAccess()
503 """
504 if not aclid:
505 return access
506
507 acl = self.GetACL(aclid)
508
509 if 'groups' in acl:
510 for k, v in acl['groups'].items():
511 if k in self.groups:
512 access = self.ApplyAccess(access, v)
513
514 if 'users' in acl:
515 for k, v in acl['users'].items():
516 if k == self.userid:
517 access = self.ApplyAccess(access, v)
518
519 return access
520
521 # ----------------------------------------------------------------------------------------
522 def ApplyAccess(self, origaccess, newaccess):
523 """Utility function to calculate access control. You should not need to call this yourself
524 since it's automatically handled by db.GetAccess()
525 """
526 if newaccess & DB_CAN_CREATE:
527 origaccess = origaccess & (~DB_CANNOT_CREATE) | DB_CAN_CREATE
528 elif newaccess & DB_CANNOT_CREATE:
529 origaccess = origaccess & (~DB_CAN_CREATE) | DB_CANNOT_CREATE
530
531 if newaccess & DB_CAN_CHANGE:
532 origaccess = origaccess & (~DB_CANNOT_CHANGE) | DB_CAN_CHANGE
533 elif newaccess & DB_CANNOT_CHANGE:
534 origaccess = origaccess & (~DB_CAN_CHANGE) | DB_CANNOT_CHANGE
535
536 if newaccess & DB_CAN_VIEW:
537 origaccess = origaccess & (~DB_CANNOT_VIEW) | DB_CAN_VIEW
538 elif newaccess & DB_CANNOT_VIEW:
539 origaccess = origaccess & (~DB_CAN_VIEW) | DB_CANNOT_VIEW
540
541 if newaccess & DB_CAN_DELETE:
542 origaccess = origaccess & (~DB_CANNOT_DELETE) | DB_CAN_DELETE
543 elif newaccess & DB_CANNOT_DELETE:
544 origaccess = origaccess & (~DB_CAN_DELETE) | DB_CANNOT_DELETE
545
546 if newaccess & DB_CAN_LINK:
547 origaccess = origaccess & (~DB_CANNOT_LINK) | DB_CAN_LINK
548 elif newaccess & DB_CANNOT_LINK:
549 origaccess = origaccess & (~DB_CAN_LINK) | DB_CANNOT_LINK
550
551 if newaccess & DB_CAN_CHANGE_PERMISSIONS:
552 origaccess = origaccess & (~DB_CANNOT_CHANGE_PERMISSIONS) | DB_CAN_CHANGE_PERMISSIONS
553 elif newaccess & DB_CANNOT_CHANGE_PERMISSIONS:
554 origaccess = origaccess & (~DB_CAN_CHANGE_PERMISSIONS) | DB_CANNOT_CHANGE_PERMISSIONS
555
556 if newaccess & DB_CAN_VIEW_PROTECTED:
557 origaccess = origaccess & (~DB_CANNOT_VIEW_PROTECTED) | DB_CAN_VIEW_PROTECTED
558 elif newaccess & DB_CANNOT_VIEW_PROTECTED:
559 origaccess = origaccess & (~DB_CAN_VIEW_PROTECTED) | DB_CANNOT_VIEW_PROTECTED
560
561 return origaccess
562
563 # ------------------------------------------------------------------
564 def RegisterModel(self, model):
565 """All models must be registered before use, which means creating the
566 table, indexes, and storing a record of the model for use in the
567 web database management interface.
568
569 You normally do not need to call this function itself since any
570 database function like db.Find(), db.Load(), and db.Save() will
571 automatically do it for you.
572 """
573
574 # If model is an instance, then work on the model itself
575 if isinstance(model, pafera.modelbase.ModelBase):
576 model = model.__class__
577
578 # Model has already been registered
579 if V(model, '_dbinit'):
580 return
581
582 # Automatically create the table name from the apppath and model names
583 if not V(model, '_dbtable'):
584 if V(model, '_apppath'):
585 model._dbtable = model._apppath.replace('/', '_') + '_' + model.__name__.lower()
586 else:
587 model._dbtable = model.__name__.lower()
588
589 # Find the primary key field for quick searches
590 if 'id' in model._dbfields:
591 model._dbid = 'id'
592 elif 'rid' in model._dbfields:
593 model._dbid = 'rid'
594 elif 'rbid' in model._dbfields:
595 model._dbid = 'rbid'
596 else:
597 model._dbid = 0
598
599 for k, v in model._dbfields.items():
600 if v[1].find('PRIMARY KEY') != -1:
601 model._dbid = k
602 break
603
604 if not hasattr(model, '_dbflags'):
605 model._dbflags = 0
606
607 model._dbinit = 1
608
609 if model._dbtable in self.objtypes.keys():
610 return
611
612 # Create table if necessary
613 fields = []
614
615 for k, v in model._dbfields.items():
616 fields.append(k + ' ' + self.MapFieldType(v[0]) + ' ' + v[1])
617
618 if model._dbflags & DB_SECURE:
619 fields.append('dbowner INT NOT NULL DEFAULT 0')
620 fields.append('dbgroup INT NOT NULL DEFAULT 0')
621 fields.append('dbaccess INT NOT NULL DEFAULT 0')
622 fields.append('dbgroupaccess INT NOT NULL DEFAULT 0')
623 fields.append('dbaclid INT NOT NULL DEFAULT 0')
624
625 fields = ','.join(fields)
626
627 try:
628 if self.dbtype == 'mysql':
629 self.Execute(f"CREATE TABLE {model._dbtable}({fields}) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;")
630 else:
631 self.Execute(f"CREATE TABLE {model._dbtable}({fields})")
632 except Exception as e:
633 # Skip exception if table already exists but was somehow not properly
634 # stored in objtypes
635 if 'exists' not in str(e):
636 raise Exception from e
637
638 # Create indexes
639 if V(model, '_dbindexes'):
640 indexnum = 1
641
642 for r in model._dbindexes:
643 indextype = r[0]
644 indexcolumns = r[1]
645
646 if not isinstance(indexcolumns, tuple):
647 indexcolumns = [indexcolumns]
648
649 if self.dbtype == 'mysql':
650 columns = []
651
652 for c in indexcolumns:
653 fielddef = model._dbfields[c][0]
654
655 if (fielddef.find('TEXT') != -1
656 or fielddef.find('DICT') != -1
657 or fielddef.find('LIST') != -1
658 or fielddef.find('TRANSLATION') != -1
659 or fielddef.find('FILE') != -1
660 ):
661 columns.append(c + '(255)')
662 else:
663 columns.append(c)
664
665 columns = ', '.join(columns)
666 else:
667 columns = ', '.join(indexcolumns)
668
669 self.Execute(f"CREATE {indextype} INDEX {model._dbtable}_index{indexnum} ON {model._dbtable}({columns})")
670 indexnum += 1
671
672 # Register the model into the system for use in the web database manager
673 if model._dbtable != 'system_objtype':
674 self.Sudo(1)
675
676 newtype = system_objtype()
677 newtype.Set(
678 typename = model._dbtable,
679 access = DB_CANNOT_DELETE
680 | DB_CANNOT_CHANGE
681 | DB_CANNOT_LINK
682 | DB_CANNOT_CHANGE_PERMISSIONS
683 | DB_CANNOT_VIEW_PROTECTED,
684 aclid = 0,
685 flags = 0,
686 )
687
688 self.Insert(newtype)
689 self.Commit()
690
691 self.Sudo(0)
692
693 self.objtypes[model._dbtable] = newtype.ToJSON()
694
695 return model
696
697 # ------------------------------------------------------------------
698 def UnregisterModel(self, model):
699 """Removes the model from the system registry.
700
701 Useful if you're uninstalling an app.
702 """
703 model = self.RegisterModel(model)
704
705 self.Execute('DELETE FROM system_objtype WHERE typename = ?', model)
706 self.Commit();
707
708 # ------------------------------------------------------------------
709 def MapFieldType(self, fieldtype):
710 """This function is responsible for mapping python types to their
711 database representations. If you are using a database outside of
712 MySQL or SQLite, be sure to add the conversions here.
713 """
714 replacements = []
715
716 if self.dbtype == 'mysql':
717 replacements = (
718 ('AUTOINCREMENT', 'AUTO_INCREMENT'),
719 ('BIGINT PRIMARY KEY', 'BIGINT PRIMARY KEY'),
720 ('INT PRIMARY KEY', 'INT PRIMARY KEY'),
721 ('INT64', 'BIGINT'),
722 ('INT32', 'INT'),
723 ('INT16', 'SMALLINT'),
724 ('INT8', 'TINYINT'),
725 ('ENUM', 'INT'),
726 ('BITFLAGS', 'INT'),
727 ('DATETIME', 'BIGINT'),
728 ('DICT', 'TEXT'),
729 ('LIST', 'TEXT'),
730 ('SINGLETEXT', 'TEXT'),
731 ('MULTITEXT', 'TEXT'),
732 ('PASSWORD', 'TEXT'),
733 ('IMAGEFILE', 'TEXT'),
734 ('SOUNDFILE', 'TEXT'),
735 ('VIDEOFILE', 'TEXT'),
736 ('SYSTEMFILE', 'TEXT'),
737 ('NEWLINELIST', 'TEXT'),
738 ('PROTECTED', ''),
739 ('PRIVATE', ''),
740 ('TRANSLATION', 'TEXT'),
741 )
742 elif self.dbtype == 'postgresql':
743 replacements = (
744 ('AUTOINCREMENT', 'AUTO_INCREMENT'),
745 ('BIGINT PRIMARY KEY', 'BIGINT PRIMARY KEY'),
746 ('INT PRIMARY KEY', 'INT PRIMARY KEY'),
747 ('INT64', 'BIGINT'),
748 ('INT32', 'INT'),
749 ('INT16', 'SMALLINT'),
750 ('INT8', 'TINYINT'),
751 ('ENUM', 'INT'),
752 ('BITFLAGS', 'INT'),
753 ('DATETIME', 'BIGINT'),
754 ('DICT', 'TEXT'),
755 ('LIST', 'TEXT'),
756 ('SINGLETEXT', 'TEXT'),
757 ('MULTITEXT', 'TEXT'),
758 ('PASSWORD', 'TEXT'),
759 ('IMAGEFILE', 'TEXT'),
760 ('SOUNDFILE', 'TEXT'),
761 ('VIDEOFILE', 'TEXT'),
762 ('SYSTEMFILE', 'TEXT'),
763 ('NEWLINELIST', 'TEXT'),
764 ('PROTECTED', ''),
765 ('PRIVATE', ''),
766 ('TRANSLATION', 'TEXT'),
767 )
768 elif self.dbtype == 'sqlite':
769 replacements = (
770 ('INT64', 'INT'),
771 ('INT32', 'INT'),
772 ('INT16', 'INT'),
773 ('INT8', 'INT'),
774 ('ENUM', 'INT'),
775 ('BITFLAGS', 'INT'),
776 ('DATETIME', 'INT'),
777 ('DICT', 'TEXT'),
778 ('LIST', 'TEXT'),
779 ('SINGLETEXT', 'TEXT'),
780 ('MULTITEXT', 'TEXT'),
781 ('PASSWORD', 'TEXT'),
782 ('IMAGEFILE', 'TEXT'),
783 ('SOUNDFILE', 'TEXT'),
784 ('VIDEOFILE', 'TEXT'),
785 ('SYSTEMFILE', 'TEXT'),
786 ('NEWLINELIST', 'TEXT'),
787 ('PROTECTED', ''),
788 ('PRIVATE', ''),
789 ('TRANSLATION', 'TEXT'),
790 )
791
792 for r in replacements:
793 fieldtype = fieldtype.replace(r[0], r[1])
794
795 return fieldtype
796
797 # ---------------------------------------------------------------------------------------
798 def HasTable(self, table):
799 """Quick utility function to see if a table exists.
800
801 For model tables, the same functionality could be achieved by checking db._objtypes,
802 but there is always the possibility that another script has created a table between
803 the initialization of the database object and the current running code, so it's
804 always best to be sure by directly querying the database itself.
805 """
806 try:
807 self.Execute(f"SELECT * FROM {table} LIMIT 1")
808 return True
809 except Exception as e:
810 return False
811
812 # ------------------------------------------------------------------
813 def __getitem__(self, key, value = None):
814 """Utility function to allow the database object itself to work like
815 a big DB2 store.
816 """
817 r = self.Execute("SELECT value FROM system_config WHERE key = ?", key)
818
819 if r != None:
820 return r[0]
821
822 return None
823
824 # ------------------------------------------------------------------
825 def __setitem__(self, key, value):
826 """Utility function to allow the database object itself to work like
827 a big DB2 store.
828 """
829 self.Query("REPLACE INTO system_config(key, value) VALUES(?, ?)", [key, value])
830
831 # ------------------------------------------------------------------
832 def Insert(self, obj):
833 """db.Insert(), db.Update(), and db.Replace() are all convenient
834 wrappers to the underlying db.Save() function for easier
835 readability of code.
836 """
837 return self.Save(obj, 'insert')
838
839 # ------------------------------------------------------------------
840 def Update(self, obj):
841 """db.Insert(), db.Update(), and db.Replace() are all convenient
842 wrappers to the underlying db.Save() function for easier
843 readability of code.
844 """
845 return self.Save(obj, 'update')
846
847 # ------------------------------------------------------------------
848 def Replace(self, obj):
849 """db.Insert(), db.Update(), and db.Replace() are all convenient
850 wrappers to the underlying db.Save() function for easier
851 readability of code.
852 """
853 return self.Save(obj, 'replace')
854
855 # ------------------------------------------------------------------
856 def Save(self, obj, method = ''):
857 """This is the function where we convert python types
858 and store them into the database.
859
860 Any fields named "rid" or "rbid" will be automatically given a
861 randomized non-existing value. You can check its value on the
862 object after this function returns.
863
864 Security fields will be automatically added if security is enabled
865 for both the database and the object's model.
866 """
867
868 # No need to update objects which haven't changed
869 if not obj._changed:
870 return
871
872 self.RegisterModel(obj.__class__)
873
874 dbid = obj._dbid
875 dbidvalue = getattr(obj, dbid, None)
876 autoincrement = 0
877
878 # Select method if not specified.
879 # Note that REPLACE is not used since some databases will set all
880 # non-specified fields to defaults, wiping out existing data.
881 if not method:
882 if dbidvalue:
883 method = 'update'
884 else:
885 method = 'insert'
886
887 # Check if the current user has permission to save
888 enablesecurity = obj.__class__._dbflags & DB_SECURE
889
890 if enablesecurity:
891 access = self.GetAccess(obj)
892 cancreate = access & DB_CAN_CREATE
893 canchange = access & DB_CAN_CHANGE
894
895 if (
896 ((method == 'insert') and not cancreate)
897 or ((method == 'update') and not canchange)
898 or ((method == 'replace') and not canchange and not cancreate)
899 ):
900 raise Exception("DB.Save: Permission denied")
901
902
903 if obj._dbfields[dbid][0].find('AUTOINCREMENT') != -1:
904 autoincrement = 1
905
906 # Find a new RID or RBID if needed
907 if (
908 (method == 'insert' or method == 'replace')
909 and not dbidvalue
910 and (dbid == 'rid' or dbid == 'rbid')
911 ):
912 newid = 0
913
914 for i in range(0, 100):
915 if dbid == 'rid':
916 newid = MakeRID()
917 else:
918 newid = MakeRBID()
919
920 r = self.Execute(f"SELECT {dbid} FROM {obj._dbtable} WHERE {dbid} = ?", newid)
921
922 if r:
923 newid = 0
924 continue
925 else:
926 setattr(obj, dbid, newid)
927 break
928
929 if not newid:
930 raise Exception(f"DB.Insert(): Could not find new random ID for table {obj._dbtable}")
931
932 keys = []
933 values = []
934
935 changedkeys = obj._changed.keys()
936
937 # Convert python types to database types
938 for k, v in obj._dbfields.items():
939 if k not in changedkeys and k != dbid:
940 continue
941
942 if k == dbid:
943 # Let the database handle autoincrement IDs
944 if (
945 (autoincrement and not dbidvalue)
946 or (method == 'update')
947 ):
948 continue
949
950 keys.append(k)
951
952 value = getattr(obj, k, None)
953
954 if v[0].find('PRIMARY KEY') != -1:
955 values.append(value)
956 elif v[0].find('INT') != -1:
957 values.append(int(value if value else 0))
958 elif v[0].find('FLOAT') != -1:
959 values.append(float(value if value else 0))
960 elif v[0] == 'NEWLINELIST':
961 values.append('\n'.join(value) if value else '')
962 elif v[0] == 'DICT' or v[0] == 'TRANSLATION':
963 values.append(json.dumps(value if value else {}))
964 elif v[0] == 'LIST':
965 values.append(json.dumps(value if value else []))
966 elif v[0] == 'DATETIME':
967 values.append(value.timestamp() if value else 0)
968 else:
969 values.append(value)
970
971 parameters = ', '.join('?' * len(keys))
972
973 # Add security fields if enabled
974 if enablesecurity:
975 keys.append('dbowner')
976 values.append(getattr(obj, 'dbowner', self.userid))
977
978 keys.append('dbgroup')
979 values.append(getattr(obj, 'dbgroup', 0))
980
981 keys.append('dbaccess')
982 values.append(getattr(obj, 'dbaccess', 0))
983
984 keys.append('dbgroupaccess')
985 values.append(getattr(obj, 'dbgroupaccess', 0))
986
987 keys.append('dbaclid')
988 values.append(getattr(obj, 'dbaclid', 0))
989
990 # Execute queries
991 if method == 'insert' or method == 'replace':
992 needcursor = not self.cursor
993
994 if needcursor:
995 self.cursor = self.dbconn.cursor()
996
997 if method == 'insert':
998 self.Execute(f"INSERT INTO {obj._dbtable}({', '.join(keys)}) VALUES({parameters})", values)
999 else:
1000 self.Execute(f"REPLACE INTO {obj._dbtable}({', '.join(keys)}) VALUES({parameters})", values)
1001
1002 if dbid != 'rid' and dbid != 'rbid':
1003 if self.cursor.lastrowid:
1004 setattr(obj, dbid, self.cursor.lastrowid)
1005
1006 if needcursor:
1007 self.cursor.close()
1008 self.cursor = 0
1009 elif method == 'update':
1010 if not dbidvalue:
1011 raise Exception(f"DB.Update(): Cannot update {obj._dbtable} since no ID was provided")
1012
1013 ls = []
1014
1015 ls.append("UPDATE " + obj._dbtable + " SET ")
1016
1017 for i in range(0, len(keys) - 1):
1018 if keys[i] == dbid:
1019 continue
1020
1021 ls.append(keys[i] + ' = ?, ')
1022
1023 ls.append(f" {keys[len(keys) - 1]} = ? WHERE {dbid} = ?")
1024
1025 values.append(dbidvalue)
1026
1027 self.Execute('\n'.join(ls), values)
1028 else:
1029 raise Exception(f"Unknown save method {method}")
1030
1031 return self
1032
1033 # ------------------------------------------------------------------
1034 def Load(self, model, objid, fields = '*'):
1035 """Loads an object given its ID. Like many other database designers,
1036 I consider giving every row its own ID value to be convenient
1037 despite the added space requirements.
1038 """
1039 self.RegisterModel(model)
1040
1041 enablesecurity = model._dbflags & DB_SECURE
1042
1043 if enablesecurity and fields != '*':
1044 fields += ', dbowner, dbgroup, dbaccess, dbgroupaccess, dbaclid'
1045
1046 r = self.Query(f"SELECT {fields} FROM {model._dbtable} WHERE {model._dbid} = ?", objid)
1047
1048 if not r:
1049 raise Exception(f"No object with id {getattr(model, model._dbid, '[blank]')} found in {model._dbtable}")
1050
1051 o = model()
1052
1053 r = r[0]
1054 r['_updatechanged'] = 0
1055 o.Set(**r)
1056
1057 if enablesecurity:
1058 access = self.GetAccess(o)
1059
1060 if not (access & DB_CAN_VIEW):
1061 raise Exception("DB.Load: Permission denied")
1062
1063 return o
1064
1065 # ------------------------------------------------------------------
1066 def LoadMany(self, model, ids, fields = '*'):
1067 """Does the same thing as db.Load(), only you can load a number of
1068 objects at once. This is separated into its own function since
1069 it actually involves different syntax in returning an array versus
1070 returning a single object.
1071 """
1072 self.RegisterModel(model)
1073
1074 enablesecurity = model._dbflags & DB_SECURE
1075
1076 if enablesecurity and fields != '*':
1077 fields += ', dbowner, dbgroup, dbaccess, dbgroupaccess, dbaclid'
1078
1079 questionmarks = ['?' for x in ids]
1080
1081 return self.Find(
1082 model,
1083 f'WHERE {model._dbid} IN (' + ', '.join(questionmarks) + ')',
1084 ids,
1085 fields = fields,
1086 limit = 99999999,
1087 )
1088
1089 # ------------------------------------------------------------------
1090 def Find(self,
1091 model,
1092 cond = '',
1093 params = [],
1094 **kwargs
1095 ):
1096 """The Pafera equivalent to SQL SELECT. All it does is to pass the
1097 parameters to DBList()'s constructor.
1098
1099 cond is the filter such as WHERE or HAVING
1100
1101 params is a list of parameters to be used in the query. SQL
1102 injections are still a thing in 2016, so if you're still
1103 manually constructing queries, be sure that you know what you're
1104 doing.
1105
1106 Other available parameters are fields, start, limit, and orderby.
1107 They do pretty much what you think they do in constructing the
1108 underlying SQL query.
1109 """
1110 self.RegisterModel(model)
1111
1112 kwargs['cond'] = cond
1113 kwargs['params'] = params
1114
1115 return pafera.modelbase.DBList(self, model, **kwargs)
1116
1117 # ------------------------------------------------------------------
1118 def Delete(self,
1119 model,
1120 cond = '',
1121 params = [],
1122 ):
1123 """Deletes the given object or objects of the same type matching cond and params.
1124
1125 As with any deletion function, make sure that you're *really* sure before
1126 using this. If in doubt, enable DB_TRACK_VALUES on this model so that you can
1127 get the object back if you've made a mistake.
1128 """
1129 self.RegisterModel(model)
1130
1131 enablesecurity = model._dbflags & DB_SECURE
1132
1133 if enablesecurity:
1134 if cond:
1135 for r in self.Find(model, cond, params):
1136 access = self.GetAccess(r)
1137
1138 if access & DB_CAN_DELETE:
1139 self.Execute(f"DELETE FROM {model._dbtable} WHERE {model._dbid} = ?", r[model._dbid])
1140 elif getattr(model, model._dbid, None):
1141 access = self.GetAccess(model)
1142
1143 if access & DB_CAN_DELETE:
1144 self.Execute(f"DELETE FROM {model._dbtable} WHERE {model._dbid} = ?", getattr(model, model._dbid))
1145 else:
1146 for r in self.Find(model):
1147 access = self.GetAccess(r)
1148
1149 if access & DB_CAN_DELETE:
1150 self.Execute(f"DELETE FROM {model._dbtable} WHERE {model._dbid} = ?", r[model._dbid])
1151 else:
1152 if cond:
1153 self.Execute(f"DELETE FROM {model._dbtable} " + cond, params)
1154 elif getattr(model, model._dbid, None):
1155 self.Execute(f"DELETE FROM {model._dbtable} WHERE {model._dbid} = ?", getattr(model, model._dbid))
1156 else:
1157 self.Execute(f"DELETE FROM {model._dbtable}")
1158
1159 # ------------------------------------------------------------------
1160 def GetLinkTable(self, obj1, obj2):
1161 """Utility function to get the name of the database table between
1162 two objects.
1163
1164 Use this if you need to do any manual operations on link tables.
1165 """
1166 self.RegisterModel(obj1)
1167 self.RegisterModel(obj2)
1168
1169 # Ensure that the table name is always the same between two objects,
1170 # thus removing the need for bidirectional links
1171 if obj1._dbtable > obj2._dbtable:
1172 temp = obj1
1173 obj1 = obj2
1174 obj2 = temp
1175
1176 return obj1._dbtable + '__' + obj2._dbtable
1177
1178 # ------------------------------------------------------------------
1179 def CreateLinkTable(self, obj1, obj2):
1180 """Creates the link table if necessary.
1181 """
1182
1183 linktable = self.GetLinkTable(obj1, obj2)
1184
1185 if not self.HasTable(linktable):
1186 aidtype = self.MapFieldType(obj1._dbfields[obj1._dbid][0]).replace('PRIMARY KEY', '').replace('AUTOINCREMENT', '').replace('AUTO_INCREMENT', '')
1187
1188 bidtype = self.MapFieldType(obj2._dbfields[obj2._dbid][0]).replace('PRIMARY KEY', '').replace('AUTOINCREMENT', '').replace('AUTO_INCREMENT', '')
1189
1190 self.Execute(f"""CREATE TABLE {linktable}(
1191 linkaid {aidtype} NOT NULL,
1192 linkbid {bidtype} NOT NULL,
1193 linktype INT NOT NULL,
1194 linknum INT NOT NULL,
1195 linkcomment TEXT NOT NULL,
1196 linkflags INT NOT NULL
1197 )""")
1198 self.Execute(f"CREATE UNIQUE INDEX {linktable}_index1 ON {linktable}(linkaid, linkbid)")
1199 self.Commit()
1200
1201 return linktable
1202
1203 # ------------------------------------------------------------------
1204 def Link(self,
1205 obj1,
1206 obj2,
1207 linktype = 0,
1208 linknum = 0,
1209 comment = '',
1210 flags = 0,
1211 ):
1212 """Linking is one of the fundamental concepts in Pafera. It's essentially
1213 a foreign key mechanism that can link two arbitary objects via their
1214 database IDs, thus you can do things like
1215
1216 u = new user
1217 g = new group
1218
1219 # Link the user and group
1220 db.Link(u, g)
1221
1222 # prints g's information
1223 print(db.Linked(u, group))
1224
1225 Linking is a bidirectional operation, meaning that both
1226 db.Linked(u, group) and db.Linked(g, user) will work. For unidirectional
1227 links, it's suggested to store the linked IDs in a NEWLINELIST field
1228 in an object itself.
1229
1230 Behind the scenes, linking works by create a separate table that connects
1231 IDs between one table and other. Any attempt to find links are returned
1232 in a DBList() object which performs a SQL JOIN for one query results.
1233
1234 Linking supports different types of links via the linktype parameter.
1235
1236 The linknum parameter is used to create results that are returned in
1237 order of linknum, guaranteeing that the same sequence will be returned
1238 every time.
1239
1240 flags can be used to store application-specific meanings outside of
1241 linktype.
1242 """
1243
1244 if obj1._dbtable > obj2._dbtable:
1245 temp = obj1
1246 obj1 = obj2
1247 obj2 = temp
1248
1249 linktable = self.CreateLinkTable(obj1, obj2)
1250
1251 enablesecurity = (obj1.__class__._dbflags & DB_SECURE) or (obj2.__class__._dbflags & DB_SECURE)
1252
1253 if enablesecurity:
1254 access1 = self.GetAccess(obj1)
1255 access2 = self.GetAccess(obj2)
1256
1257 if (access1 & DB_CANNOT_LINK) or (access2 & DB_CANNOT_LINK):
1258 raise Exception("DB.Link: Permission denied")
1259
1260 obj1id = getattr(obj1, obj1._dbid)
1261 obj2id = getattr(obj2, obj2._dbid)
1262
1263 if not obj1id or not obj2id:
1264 raise Exception("DB.Link: Missing object IDs")
1265
1266 self.Execute(f"""REPLACE INTO {linktable}(
1267 linkaid,
1268 linkbid,
1269 linktype,
1270 linknum,
1271 linkcomment,
1272 linkflags
1273 ) VALUES(?, ?, ?, ?, ?, ?)
1274 """,
1275 (
1276 obj1id,
1277 obj2id,
1278 linktype,
1279 linknum,
1280 comment,
1281 flags,
1282 )
1283 )
1284
1285 # ------------------------------------------------------------------
1286 def LinkArray(self,
1287 obj1,
1288 obj2list,
1289 linktype = 0,
1290 comment = '',
1291 flags = 0,
1292 ):
1293 """A version of db.Link() that will erase all previous links of the
1294 same type and store the given links as an array. This is useful for
1295 representing tree diagrams from the normally flat SQL architecture
1296 outside of advanced usage in PostgreSQL and the sort.
1297 """
1298
1299 obj2 = obj2list[0]
1300
1301 self.RegisterModel(obj1)
1302 self.RegisterModel(obj2)
1303
1304 enablesecurity = (obj1.__class__._dbflags & DB_SECURE) or (obj2.__class__._dbflags & DB_SECURE)
1305
1306 if enablesecurity:
1307 access1 = self.GetAccess(obj1)
1308 access2 = self.GetAccess(obj2)
1309
1310 if (access1 & DB_CANNOT_LINK) or (access2 & DB_CANNOT_LINK):
1311 raise Exception("DB.Link: Permission denied")
1312
1313 linktable = self.CreateLinkTable(obj1, obj2)
1314
1315 params = []
1316
1317 obj1id = getattr(obj1, obj1._dbid)
1318 obj2dbid = obj2._dbid
1319 linknum = 0
1320
1321 if not obj1id or not obj2dbid:
1322 raise Exception("DB.LinkArray: Missing object IDs")
1323
1324 for obj2 in obj2list:
1325 params.append(
1326 (
1327 obj1id,
1328 getattr(obj2, obj2dbid),
1329 linktype,
1330 linknum,
1331 comment,
1332 flags,
1333 )
1334 )
1335 linknum += 1
1336
1337 if obj1._dbtable > obj2._dbtable:
1338 self.Execute(f"""DELETE FROM {linktable}
1339 WHERE linkbid = ? and linktype = ?
1340 """,
1341 (obj1id, linktype)
1342 )
1343 self.ExecuteMany(f"""REPLACE INTO {linktable}(
1344 linkbid,
1345 linkaid,
1346 linktype,
1347 linknum,
1348 linkcomment,
1349 linkflags
1350 ) VALUES(?, ?, ?, ?, ?, ?)
1351 """,
1352 params
1353 )
1354 else:
1355 self.Execute(f"""DELETE FROM {linktable}
1356 WHERE linkaid = ? and linktype = ?
1357 """,
1358 (obj1id, linktype)
1359 )
1360 self.ExecuteMany(f"""REPLACE INTO {linktable}(
1361 linkaid,
1362 linkbid,
1363 linktype,
1364 linknum,
1365 linkcomment,
1366 linkflags
1367 ) VALUES(?, ?, ?, ?, ?, ?)
1368 """,
1369 params
1370 )
1371
1372 # ------------------------------------------------------------------
1373 def Unlink(self,
1374 obj1,
1375 obj2,
1376 linktype = 0,
1377 ):
1378 """Unlink two objects, or if obj2 is a model, unlink *every*
1379 object of the given linktype. Use with caution.
1380 """
1381
1382 self.RegisterModel(obj1)
1383 self.RegisterModel(obj2)
1384
1385 # If model is an instance, then work on the model itself
1386 if isinstance(obj2, pafera.modelbase.ModelBase):
1387 model2 = obj2.__class__
1388 else:
1389 model2 = obj2
1390
1391 enablesecurity = (obj1.__class__._dbflags & DB_SECURE) or (obj2._dbflags & DB_SECURE)
1392
1393 if enablesecurity:
1394 access1 = self.GetAccess(obj1)
1395 access2 = self.GetAccess(obj2)
1396
1397 if (access1 & DB_CANNOT_LINK) or (access2 & DB_CANNOT_LINK):
1398 raise Exception("DB.Link: Permission denied")
1399
1400 linktable = self.CreateLinkTable(obj1, obj2)
1401
1402 if isinstance(obj2, pafera.modelbase.ModelBase):
1403 self.Execute(f"""DELETE FROM {linktable}
1404 WHERE linkaid = ? AND linkbid = ? AND linktype = ?
1405 """,
1406 (
1407 getattr(obj1, obj1._dbid),
1408 getattr(obj2, obj2._dbid),
1409 linktype,
1410 )
1411 )
1412 else:
1413 self.Execute(f"""DELETE FROM {linktable}
1414 WHERE linkaid = ? AND linktype = ?
1415 """,
1416 (
1417 getattr(obj1, obj1._dbid),
1418 linktype,
1419 )
1420 )
1421
1422 # ------------------------------------------------------------------
1423 def HasLink(self,
1424 model1,
1425 model1id,
1426 model2,
1427 model2id,
1428 linktype = 0,
1429 ):
1430 """Convenience function to check if two objects are linked
1431 without actually loading the objects themselves. Commonly used
1432 to check for membership in a group.
1433 """
1434
1435 linktable = self.CreateLinkTable(model1, model2)
1436
1437 if model1._dbtable > model2._dbtable:
1438 r = self.Execute(f"""SELECT linkaid FROM {linktable}
1439 WHERE linkaid = ? AND linkbid = ? AND linktype = ?""",
1440 (model2id, model1id, linktype)
1441 )
1442 else:
1443 r = self.Execute(f"""SELECT linkaid FROM {linktable}
1444 WHERE linkaid = ? AND linkbid = ? AND linktype = ?""",
1445 (model1id, model2id, linktype)
1446 )
1447
1448 return 1 if r else 0
1449
1450 # ------------------------------------------------------------------
1451 def Linked(self,
1452 obj1,
1453 model2,
1454 linktype = 0,
1455 start = 0,
1456 limit = 1000,
1457 fields = '*',
1458 orderby = '',
1459 cond = '',
1460 params = [],
1461 ):
1462 """Return the list of objects linked to this object with the given
1463 linktype. Note that this function returns a DBList(), so if you have
1464 many objects linked, it will automatically separate them into chunks
1465 while iterating through them.
1466 """
1467
1468 self.RegisterModel(obj1.__class__)
1469 self.RegisterModel(model2)
1470
1471 if obj1.__class__._dbflags & DB_SECURE:
1472 access1 = self.GetAccess(obj1)
1473
1474 if not access1 & DB_CAN_LINK:
1475 raise Exception('DB.Linked: Permission denied')
1476
1477 linktable = self.CreateLinkTable(obj1, model2)
1478
1479 questionmarks = []
1480 model2ids = []
1481
1482 obj1id = getattr(obj1, obj1._dbid)
1483 obj2dbid = model2._dbid
1484
1485 totalcount = 0
1486
1487 if obj1._dbtable > model2._dbtable:
1488 joinclause = f'JOIN {linktable} ON linkaid = ' + obj2dbid
1489
1490 if not orderby:
1491 orderby = 'linknum, linkaid'
1492
1493 newparams = [obj1id, linktype]
1494
1495 if cond:
1496 cond = 'WHERE linkbid = ? AND linktype = ? AND (' + cond + ')'
1497 newparams += params
1498 else:
1499 cond = 'WHERE linkbid = ? AND linktype = ?'
1500 else:
1501 joinclause = f'JOIN {linktable} ON linkbid = ' + obj2dbid
1502
1503 if not orderby:
1504 orderby = 'linknum, linkbid'
1505
1506 newparams = [obj1id, linktype]
1507
1508 if cond:
1509 cond = 'WHERE linkaid = ? AND linktype = ? AND (' + cond + ')'
1510 newparams += params
1511 else:
1512 cond = 'WHERE linkaid = ? AND linktype = ?'
1513
1514 cond = joinclause + ' ' + cond
1515
1517 self,
1518 model2,
1519 cond = cond,
1520 params = newparams,
1521 start = start,
1522 limit = limit,
1523 orderby = orderby,
1524 )
1525
1526 # ------------------------------------------------------------------
1527 def LinkedToID(self,
1528 model1,
1529 model1id,
1530 model2,
1531 linktype = 0,
1532 start = 0,
1533 limit = 1000,
1534 fields = '*',
1535 orderby = '',
1536 cond = '',
1537 params = [],
1538 ):
1539 """Convenience function for checking what objects are linked to
1540 model1id without having to load the object itself.
1541 """
1542
1543 self.RegisterModel(model1)
1544
1545 obj1 = model1()
1546
1547 setattr(obj1, obj1._dbid, model1id)
1548
1549 return self.Linked(obj1, model2, linktype, start, limit, fields, orderby, cond, params)
The main database object.
Definition: db.py:163
def LoadMany(self, model, ids, fields=' *')
Does the same thing as db.Load(), only you can load a number of objects at once.
Definition: db.py:1066
closeondelete
Definition: db.py:218
def Debug(self, state)
Enable or disable query debugging.
Definition: db.py:430
def GetACL(self, aclid)
Get the rules for the given ACL.
Definition: db.py:487
def Update(self, obj)
db.Insert(), db.Update(), and db.Replace() are all convenient wrappers to the underlying db....
Definition: db.py:840
def HasGroup(self, groupname)
Returns true if the current user belongs to the given group.
Definition: db.py:458
def MapFieldType(self, fieldtype)
This function is responsible for mapping python types to their database representations.
Definition: db.py:709
def Close(self)
Close the database connection.
Definition: db.py:292
def Unlink(self, obj1, obj2, linktype=0)
Unlink two objects, or if obj2 is a model, unlink every object of the given linktype.
Definition: db.py:1377
def Link(self, obj1, obj2, linktype=0, linknum=0, comment='', flags=0)
Linking is one of the fundamental concepts in Pafera.
Definition: db.py:1211
def GetAccess(self, obj)
Utility function to calculate the current user's access to the given database object.
Definition: db.py:464
def RequireGroup(self, groupname, errmsg)
Throws an exception if the current user does not belong to one of the groups listed in groupname.
Definition: db.py:439
def RegisterModel(self, model)
All models must be registered before use, which means creating the table, indexes,...
Definition: db.py:564
def ApplyAccess(self, origaccess, newaccess)
Utility function to calculate access control.
Definition: db.py:522
def __getitem__(self, key, value=None)
Utility function to allow the database object itself to work like a big DB2 store.
Definition: db.py:813
def CreateLinkTable(self, obj1, obj2)
Creates the link table if necessary.
Definition: db.py:1179
def Delete(self, model, cond='', params=[])
Deletes the given object or objects of the same type matching cond and params.
Definition: db.py:1122
def Sudo(self, state)
Enable or disable administrator mode, bypassing security for the current user.
Definition: db.py:421
def HasLink(self, model1, model1id, model2, model2id, linktype=0)
Convenience function to check if two objects are linked without actually loading the objects themselv...
Definition: db.py:1429
def Query(self, query, params=[])
Convenience function to execute many statements at once.
Definition: db.py:414
def ExecuteMany(self, query, params=[])
Convenience function to execute many statements at once.
Definition: db.py:407
def LinkedToID(self, model1, model1id, model2, linktype=0, start=0, limit=1000, fields=' *', orderby='', cond='', params=[])
Convenience function for checking what objects are linked to model1id without having to load the obje...
Definition: db.py:1538
numsudos
Definition: db.py:214
def Execute(self, query, params=[], many=0, returndict=0)
Executes a query upon the database.
Definition: db.py:342
connectionname
Definition: db.py:225
def __del__(self)
Standard destructor to automatically close the database connection.
Definition: db.py:285
def LinkArray(self, obj1, obj2list, linktype=0, comment='', flags=0)
A version of db.Link() that will erase all previous links of the same type and store the given links ...
Definition: db.py:1292
objtypes
Definition: db.py:210
def Find(self, model, cond='', params=[], **kwargs)
The Pafera equivalent to SQL SELECT.
Definition: db.py:1095
language
Definition: db.py:209
def Load(self, model, objid, fields=' *')
Loads an object given its ID.
Definition: db.py:1034
def HasTable(self, table)
Quick utility function to see if a table exists.
Definition: db.py:798
def Replace(self, obj)
db.Insert(), db.Update(), and db.Replace() are all convenient wrappers to the underlying db....
Definition: db.py:848
numdebugs
Definition: db.py:215
def __init__(self, connectionname=None, dbname=None, dbhost='localhost', dbtype='sqlite', dbuser=None, dbpassword=None, dbflags=DB_SECURE|DB_TRACK_CHANGES|DB_TRACK_VALUES, closeondelete=1)
Basic database constructor.
Definition: db.py:199
def Begin(self)
Begins a transaction.
Definition: db.py:306
def Rollback(self)
Rolls back the current transaction.
Definition: db.py:331
def GetLinkTable(self, obj1, obj2)
Utility function to get the name of the database table between two objects.
Definition: db.py:1160
def Save(self, obj, method='')
This is the function where we convert python types and store them into the database.
Definition: db.py:856
def Commit(self)
Commits the current transaction.
Definition: db.py:320
def ApplyACL(self, access, aclid)
Get the rules for the given ACL.
Definition: db.py:500
def Insert(self, obj)
db.Insert(), db.Update(), and db.Replace() are all convenient wrappers to the underlying db....
Definition: db.py:832
def UnregisterModel(self, model)
Removes the model from the system registry.
Definition: db.py:698
def Linked(self, obj1, model2, linktype=0, start=0, limit=1000, fields=' *', orderby='', cond='', params=[])
Return the list of objects linked to this object with the given linktype.
Definition: db.py:1461
def __setitem__(self, key, value)
Utility function to allow the database object itself to work like a big DB2 store.
Definition: db.py:825
Bootstrap class for the database.
Definition: db.py:68
Bootstrap class for the database.
Definition: db.py:104
Bootstrap class for the database.
Definition: db.py:84
Bootstrap class for the database.
Definition: db.py:49
Bootstrap class for the database.
Definition: db.py:143
Iterator class for database access that supports automatic chunking.
Definition: modelbase.py:332
Base class for all database models.
Definition: modelbase.py:20
def Print(v, *args)
Print a unicode string with arguments.
Definition: types.py:32
def V(d, k, default=None)
Utility function to get a value from a dict or object given its name.
Definition: types.py:151
def MakeRID()
Returns a random 32-bit int ID.
Definition: utils.py:9
def MakeRBID()
Returns a random 64-bit int ID.
Definition: utils.py:15