Add early support for user accounts
This commit is contained in:
		
							parent
							
								
									785cf9f687
								
							
						
					
					
						commit
						1c7b736b1a
					
				
					 6 changed files with 222 additions and 34 deletions
				
			
		
							
								
								
									
										10
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								README.md
									
									
									
									
									
								
							|  | @ -3,6 +3,16 @@ Etiquette | ||||||
| 
 | 
 | ||||||
| This is the readme file. | This is the readme file. | ||||||
| 
 | 
 | ||||||
|  | ### To do list | ||||||
|  | - User account system, permission levels, private pages. | ||||||
|  | - Bookmark system. Maybe the ability to submit URLs as photo objects. | ||||||
|  | - Generalize the filename expression filter so it can work with any strings. | ||||||
|  | - Improve the "tags on this page" list. Maybe add separate buttons for must/may/forbid on each. | ||||||
|  | - Some way for the database to re-identify a file that was moved / renamed (lost & found). Maybe file hash of the first few mb is good enough. | ||||||
|  | - Move out more helpers | ||||||
|  | - Create objects.py | ||||||
|  | - Debate whether the `UserMixin.login` method should accept usernames or I should standardize the usage of IDs only internally. | ||||||
|  | 
 | ||||||
| ### Changelog | ### Changelog | ||||||
| 
 | 
 | ||||||
| - **[addition]** A new feature was added. | - **[addition]** A new feature was added. | ||||||
|  |  | ||||||
|  | @ -32,6 +32,11 @@ MIN_TAG_NAME_LENGTH = 1 | ||||||
| MAX_TAG_NAME_LENGTH = 32 | MAX_TAG_NAME_LENGTH = 32 | ||||||
| VALID_TAG_CHARS = string.ascii_lowercase + string.digits + '_' | VALID_TAG_CHARS = string.ascii_lowercase + string.digits + '_' | ||||||
| 
 | 
 | ||||||
|  | MIN_USERNAME_LENGTH = 2 | ||||||
|  | MAX_USERNAME_LENGTH = 24 | ||||||
|  | MIN_PASSWORD_LENGTH = 6 | ||||||
|  | VALID_USERNAME_CHARS = string.ascii_letters + string.digits + '~!@#$%^&*()[]{}:;,.<>/\\-_+=' | ||||||
|  | 
 | ||||||
| DEFAULT_ID_LENGTH = 12 | DEFAULT_ID_LENGTH = 12 | ||||||
| DEFAULT_DATADIR = '.\\_etiquette' | DEFAULT_DATADIR = '.\\_etiquette' | ||||||
| DEFAULT_DIGEST_EXCLUDE_FILES = [ | DEFAULT_DIGEST_EXCLUDE_FILES = [ | ||||||
|  |  | ||||||
|  | @ -13,6 +13,24 @@ def upgrade_1_to_2(sql): | ||||||
|     cur = sql.cursor() |     cur = sql.cursor() | ||||||
|     cur.execute('ALTER TABLE photos ADD COLUMN tagged_at INT') |     cur.execute('ALTER TABLE photos ADD COLUMN tagged_at INT') | ||||||
| 
 | 
 | ||||||
|  | def upgrade_2_to_3(sql): | ||||||
|  |     ''' | ||||||
|  |     Preliminary support for user account management was added. This includes a `user` table | ||||||
|  |     with id, username, password hash, and a timestamp. | ||||||
|  |     Plus some indices. | ||||||
|  |     ''' | ||||||
|  |     cur = sql.cursor() | ||||||
|  |     cur.execute(''' | ||||||
|  |     CREATE TABLE IF NOT EXISTS users( | ||||||
|  |         id TEXT, | ||||||
|  |         username TEXT COLLATE NOCASE, | ||||||
|  |         password BLOB, | ||||||
|  |         created INT | ||||||
|  |     ) | ||||||
|  |     ''') | ||||||
|  |     cur.execute('CREATE INDEX IF NOT EXISTS index_user_id ON users(id)') | ||||||
|  |     cur.execute('CREATE INDEX IF NOT EXISTS index_user_username ON users(username COLLATE NOCASE)') | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| def upgrade_all(database_filename): | def upgrade_all(database_filename): | ||||||
|     ''' |     ''' | ||||||
|  | @ -38,7 +56,7 @@ def upgrade_all(database_filename): | ||||||
|         upgrade_function = 'upgrade_%d_to_%d' % (current_version, version_number) |         upgrade_function = 'upgrade_%d_to_%d' % (current_version, version_number) | ||||||
|         upgrade_function = eval(upgrade_function) |         upgrade_function = eval(upgrade_function) | ||||||
|         upgrade_function(sql) |         upgrade_function(sql) | ||||||
|         sql.cursor().execute('PRAGMA user_version = 2') |         sql.cursor().execute('PRAGMA user_version = %d' % version_number) | ||||||
|         sql.commit() |         sql.commit() | ||||||
|         current_version = version_number |         current_version = version_number | ||||||
|     print('Upgrades finished.') |     print('Upgrades finished.') | ||||||
|  |  | ||||||
|  | @ -1,6 +1,4 @@ | ||||||
| class CantSynonymSelf(Exception): | # NO SUCH | ||||||
|     pass |  | ||||||
| 
 |  | ||||||
| class NoSuchAlbum(Exception): | class NoSuchAlbum(Exception): | ||||||
|     pass |     pass | ||||||
| 
 | 
 | ||||||
|  | @ -16,6 +14,13 @@ class NoSuchSynonym(Exception): | ||||||
| class NoSuchTag(Exception): | class NoSuchTag(Exception): | ||||||
|     pass |     pass | ||||||
| 
 | 
 | ||||||
|  | class NoSuchUser(Exception): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # EXISTS | ||||||
|  | class GroupExists(Exception): | ||||||
|  |     pass | ||||||
| 
 | 
 | ||||||
| class PhotoExists(Exception): | class PhotoExists(Exception): | ||||||
|     pass |     pass | ||||||
|  | @ -23,16 +28,39 @@ class PhotoExists(Exception): | ||||||
| class TagExists(Exception): | class TagExists(Exception): | ||||||
|     pass |     pass | ||||||
| 
 | 
 | ||||||
| class GroupExists(Exception): | class UserExists(Exception): | ||||||
|     pass |     pass | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | # TAG ERRORS | ||||||
| class TagTooLong(Exception): | class TagTooLong(Exception): | ||||||
|     pass |     pass | ||||||
| 
 | 
 | ||||||
| class TagTooShort(Exception): | class TagTooShort(Exception): | ||||||
|     pass |     pass | ||||||
| 
 | 
 | ||||||
|  | class CantSynonymSelf(Exception): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # USER ERRORS | ||||||
|  | class InvalidUsernameChars(Exception): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | class PasswordTooShort(Exception): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | class UsernameTooLong(Exception): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | class UsernameTooShort(Exception): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | class WrongLogin(Exception): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # GENERAL ERRORS | ||||||
| class NotExclusive(Exception): | class NotExclusive(Exception): | ||||||
|     ''' |     ''' | ||||||
|     For when two or more mutually exclusive actions have been requested. |     For when two or more mutually exclusive actions have been requested. | ||||||
|  |  | ||||||
							
								
								
									
										184
									
								
								phototagger.py
									
									
									
									
									
								
							
							
						
						
									
										184
									
								
								phototagger.py
									
									
									
									
									
								
							|  | @ -1,3 +1,4 @@ | ||||||
|  | import bcrypt | ||||||
| import collections | import collections | ||||||
| import converter | import converter | ||||||
| import datetime | import datetime | ||||||
|  | @ -85,20 +86,28 @@ SQL_TAGGROUP_COLUMNS = [ | ||||||
|     'parentid', |     'parentid', | ||||||
|     'memberid', |     'memberid', | ||||||
| ] | ] | ||||||
|  | SQL_USER_COLUMNS = [ | ||||||
|  |     'id', | ||||||
|  |     'username', | ||||||
|  |     'password', | ||||||
|  |     'created', | ||||||
|  | ] | ||||||
| 
 | 
 | ||||||
| SQL_ALBUM = {key:index for (index, key) in enumerate(SQL_ALBUM_COLUMNS)} | _sql_dictify = lambda columns: {key:index for (index, key) in enumerate(columns)} | ||||||
| SQL_ALBUMPHOTO = {key:index for (index, key) in enumerate(SQL_ALBUMPHOTO_COLUMNS)} | SQL_ALBUM = _sql_dictify(SQL_ALBUM_COLUMNS) | ||||||
| SQL_LASTID = {key:index for (index, key) in enumerate(SQL_LASTID_COLUMNS)} | SQL_ALBUMPHOTO = _sql_dictify(SQL_ALBUMPHOTO_COLUMNS) | ||||||
| SQL_PHOTO = {key:index for (index, key) in enumerate(SQL_PHOTO_COLUMNS)} | SQL_LASTID = _sql_dictify(SQL_LASTID_COLUMNS) | ||||||
| SQL_PHOTOTAG = {key:index for (index, key) in enumerate(SQL_PHOTOTAG_COLUMNS)} | SQL_PHOTO = _sql_dictify(SQL_PHOTO_COLUMNS) | ||||||
| SQL_SYN = {key:index for (index, key) in enumerate(SQL_SYN_COLUMNS)} | SQL_PHOTOTAG = _sql_dictify(SQL_PHOTOTAG_COLUMNS) | ||||||
| SQL_TAG = {key:index for (index, key) in enumerate(SQL_TAG_COLUMNS)} | SQL_SYN = _sql_dictify(SQL_SYN_COLUMNS) | ||||||
| SQL_TAGGROUP = {key:index for (index, key) in enumerate(SQL_TAGGROUP_COLUMNS)} | SQL_TAG = _sql_dictify(SQL_TAG_COLUMNS) | ||||||
|  | SQL_TAGGROUP = _sql_dictify(SQL_TAGGROUP_COLUMNS) | ||||||
|  | SQL_USER = _sql_dictify(SQL_USER_COLUMNS) | ||||||
| 
 | 
 | ||||||
| # Note: Setting user_version pragma in init sequence is safe because it only | # Note: Setting user_version pragma in init sequence is safe because it only | ||||||
| # happens after the out-of-date check occurs, so no chance of accidentally | # happens after the out-of-date check occurs, so no chance of accidentally | ||||||
| # overwriting it. | # overwriting it. | ||||||
| DATABASE_VERSION = 2 | DATABASE_VERSION = 3 | ||||||
| DB_INIT = ''' | DB_INIT = ''' | ||||||
| PRAGMA count_changes = OFF; | PRAGMA count_changes = OFF; | ||||||
| PRAGMA cache_size = 10000; | PRAGMA cache_size = 10000; | ||||||
|  | @ -144,11 +153,16 @@ CREATE TABLE IF NOT EXISTS tag_synonyms( | ||||||
|     name TEXT, |     name TEXT, | ||||||
|     mastername TEXT |     mastername TEXT | ||||||
| ); | ); | ||||||
| 
 |  | ||||||
| CREATE TABLE IF NOT EXISTS id_numbers( | CREATE TABLE IF NOT EXISTS id_numbers( | ||||||
|     tab TEXT, |     tab TEXT, | ||||||
|     last_id TEXT |     last_id TEXT | ||||||
| ); | ); | ||||||
|  | CREATE TABLE IF NOT EXISTS users( | ||||||
|  |     id TEXT, | ||||||
|  |     username TEXT COLLATE NOCASE, | ||||||
|  |     password BLOB, | ||||||
|  |     created INT | ||||||
|  | ); | ||||||
| 
 | 
 | ||||||
| -- Album | -- Album | ||||||
| CREATE INDEX IF NOT EXISTS index_album_id on albums(id); | CREATE INDEX IF NOT EXISTS index_album_id on albums(id); | ||||||
|  | @ -176,6 +190,10 @@ CREATE INDEX IF NOT EXISTS index_tagsyn_name on tag_synonyms(name); | ||||||
| -- Tag-group relation | -- Tag-group relation | ||||||
| CREATE INDEX IF NOT EXISTS index_grouprel_parentid on tag_group_rel(parentid); | CREATE INDEX IF NOT EXISTS index_grouprel_parentid on tag_group_rel(parentid); | ||||||
| CREATE INDEX IF NOT EXISTS index_grouprel_memberid on tag_group_rel(memberid); | CREATE INDEX IF NOT EXISTS index_grouprel_memberid on tag_group_rel(memberid); | ||||||
|  | 
 | ||||||
|  | -- User | ||||||
|  | CREATE INDEX IF NOT EXISTS index_user_id on users(id); | ||||||
|  | CREATE INDEX IF NOT EXISTS index_user_username on users(username COLLATE NOCASE); | ||||||
| '''.format(user_version=DATABASE_VERSION) | '''.format(user_version=DATABASE_VERSION) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -852,7 +870,7 @@ class PDBTagMixin: | ||||||
|         Redirect to get_tag_by_id or get_tag_by_name after xor-checking the parameters. |         Redirect to get_tag_by_id or get_tag_by_name after xor-checking the parameters. | ||||||
|         ''' |         ''' | ||||||
|         if not helpers.is_xor(id, name): |         if not helpers.is_xor(id, name): | ||||||
|             raise exceptions.NotExclusive('One and only one of `id`, `name` can be passed.') |             raise exceptions.NotExclusive('One and only one of `id`, `name` must be passed.') | ||||||
| 
 | 
 | ||||||
|         if id is not None: |         if id is not None: | ||||||
|             return self.get_tag_by_id(id) |             return self.get_tag_by_id(id) | ||||||
|  | @ -910,7 +928,101 @@ class PDBTagMixin: | ||||||
|         return tag |         return tag | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | class PDBUserMixin: | ||||||
|  |     def generate_user_id(self): | ||||||
|  |         ''' | ||||||
|  |         User IDs are randomized instead of integers like the other objects, | ||||||
|  |         so they get their own method. | ||||||
|  |         ''' | ||||||
|  |         possible = string.digits + string.ascii_uppercase | ||||||
|  |         for retry in range(20): | ||||||
|  |             user_id = [random.choice(possible) for x in range(self.id_length)] | ||||||
|  |             user_id = ''.join(user_id) | ||||||
|  | 
 | ||||||
|  |             self.cur.execute('SELECT * FROM users WHERE id == ?', [user_id]) | ||||||
|  |             if self.cur.fetchone() is None: | ||||||
|  |                 break | ||||||
|  |         else: | ||||||
|  |             raise Exception('Failed to create user id after 20 tries.') | ||||||
|  | 
 | ||||||
|  |         return user_id | ||||||
|  | 
 | ||||||
|  |     def get_user(self, username=None, id=None): | ||||||
|  |         if not helpers.is_xor(id, username): | ||||||
|  |             raise exceptions.NotExclusive('One and only one of `id`, `username` must be passed.') | ||||||
|  | 
 | ||||||
|  |         if username is not None: | ||||||
|  |             self.cur.execute('SELECT * FROM users WHERE username == ?', [username]) | ||||||
|  |         else: | ||||||
|  |             self.cur.execute('SELECT * FROM users WHERE id == ?', [id]) | ||||||
|  | 
 | ||||||
|  |         fetch = self.cur.fetchone() | ||||||
|  |         if fetch is not None: | ||||||
|  |             return User(self, fetch) | ||||||
|  |         else: | ||||||
|  |             raise exceptions.NoSuchUser(username) | ||||||
|  | 
 | ||||||
|  |     def login(self, user_id, password): | ||||||
|  |         self.cur.execute('SELECT * FROM users WHERE id == ?', [user_id]) | ||||||
|  |         fetch = self.cur.fetchone() | ||||||
|  | 
 | ||||||
|  |         if fetch is None: | ||||||
|  |             raise exceptions.WrongLogin() | ||||||
|  | 
 | ||||||
|  |         stored_password = fetch[SQL_USER['password']] | ||||||
|  | 
 | ||||||
|  |         if not isinstance(password, bytes): | ||||||
|  |             password = password.encode('utf-8') | ||||||
|  | 
 | ||||||
|  |         success = bcrypt.checkpw(password, stored_password) | ||||||
|  |         if not success: | ||||||
|  |             raise exceptions.WrongLogin() | ||||||
|  | 
 | ||||||
|  |         return User(self, fetch) | ||||||
|  | 
 | ||||||
|  |     def register_user(self, username, password, commit=True): | ||||||
|  |         if len(username) < constants.MIN_USERNAME_LENGTH: | ||||||
|  |             raise exceptions.UsernameTooShort(username) | ||||||
|  | 
 | ||||||
|  |         if len(username) > constants.MAX_USERNAME_LENGTH: | ||||||
|  |             raise exceptions.UsernameTooLong(username) | ||||||
|  | 
 | ||||||
|  |         badchars = [c for c in username if c not in constants.VALID_USERNAME_CHARS] | ||||||
|  |         if badchars: | ||||||
|  |             raise exceptions.InvalidUsernameChars(badchars) | ||||||
|  | 
 | ||||||
|  |         if not isinstance(password, bytes): | ||||||
|  |             password = password.encode('utf-8') | ||||||
|  | 
 | ||||||
|  |         if len(password) < constants.MIN_PASSWORD_LENGTH: | ||||||
|  |             raise exceptions.PasswordTooShort | ||||||
|  | 
 | ||||||
|  |         self.cur.execute('SELECT * FROM users WHERE username == ?', [username]) | ||||||
|  |         if self.cur.fetchone() is not None: | ||||||
|  |             raise exceptions.UserExists(username) | ||||||
|  | 
 | ||||||
|  |         user_id = self.generate_user_id() | ||||||
|  |         hashed_password = bcrypt.hashpw(password, bcrypt.gensalt()) | ||||||
|  |         created = int(getnow()) | ||||||
|  | 
 | ||||||
|  |         data = {} | ||||||
|  |         data['id'] = user_id | ||||||
|  |         data['username'] = username | ||||||
|  |         data['password'] = hashed_password | ||||||
|  |         data['created'] = created | ||||||
|  | 
 | ||||||
|  |         (qmarks, bindings) = binding_filler(SQL_USER_COLUMNS, data) | ||||||
|  |         query = 'INSERT INTO users VALUES(%s)' % qmarks | ||||||
|  |         self.cur.execute(query, bindings) | ||||||
|  | 
 | ||||||
|  |         if commit: | ||||||
|  |             log.debug('Committing - register user') | ||||||
|  |             self.commit() | ||||||
|  | 
 | ||||||
|  |         return User(self, data) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin, PDBUserMixin): | ||||||
|     ''' |     ''' | ||||||
|     This class represents an SQLite3 database containing the following tables: |     This class represents an SQLite3 database containing the following tables: | ||||||
| 
 | 
 | ||||||
|  | @ -973,8 +1085,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | ||||||
|             if existing_version != DATABASE_VERSION: |             if existing_version != DATABASE_VERSION: | ||||||
|                 message = constants.ERROR_DATABASE_OUTOFDATE |                 message = constants.ERROR_DATABASE_OUTOFDATE | ||||||
|                 message = message.format(current=existing_version, new=DATABASE_VERSION) |                 message = message.format(current=existing_version, new=DATABASE_VERSION) | ||||||
|                 log.critical(message) |                 raise SystemExit(message) | ||||||
|                 raise SystemExit |  | ||||||
| 
 | 
 | ||||||
|         statements = DB_INIT.split(';') |         statements = DB_INIT.split(';') | ||||||
|         for statement in statements: |         for statement in statements: | ||||||
|  | @ -1192,15 +1303,14 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | ||||||
|     def generate_id(self, table): |     def generate_id(self, table): | ||||||
|         ''' |         ''' | ||||||
|         Create a new ID number that is unique to the given table. |         Create a new ID number that is unique to the given table. | ||||||
|         Note that this method does not commit the database. We'll wait for that |         Note that while this method may INSERT / UPDATE, it does not commit. | ||||||
|         to happen in whoever is calling us, so we know the ID is actually used. |         We'll wait for that to happen in whoever is calling us, so we know the | ||||||
|  |         ID is actually used. | ||||||
|         ''' |         ''' | ||||||
|         table = table.lower() |         table = table.lower() | ||||||
|         if table not in ['photos', 'tags', 'groups']: |         if table not in ['photos', 'tags', 'groups']: | ||||||
|             raise ValueError('Invalid table requested: %s.', table) |             raise ValueError('Invalid table requested: %s.', table) | ||||||
| 
 | 
 | ||||||
|         do_insert = False |  | ||||||
| 
 |  | ||||||
|         self.cur.execute('SELECT * FROM id_numbers WHERE tab == ?', [table]) |         self.cur.execute('SELECT * FROM id_numbers WHERE tab == ?', [table]) | ||||||
|         fetch = self.cur.fetchone() |         fetch = self.cur.fetchone() | ||||||
|         if fetch is None: |         if fetch is None: | ||||||
|  | @ -1210,6 +1320,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | ||||||
|         else: |         else: | ||||||
|             # Use database value |             # Use database value | ||||||
|             new_id_int = int(fetch[SQL_LASTID['last_id']]) + 1 |             new_id_int = int(fetch[SQL_LASTID['last_id']]) + 1 | ||||||
|  |             do_insert = False | ||||||
| 
 | 
 | ||||||
|         new_id = str(new_id_int).rjust(self.id_length, '0') |         new_id = str(new_id_int).rjust(self.id_length, '0') | ||||||
|         if do_insert: |         if do_insert: | ||||||
|  | @ -1273,9 +1384,11 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | ||||||
| 
 | 
 | ||||||
| class ObjectBase: | class ObjectBase: | ||||||
|     def __eq__(self, other): |     def __eq__(self, other): | ||||||
|         if not isinstance(other, type(self)): |         return ( | ||||||
|             return False |             isinstance(other, type(self)) and | ||||||
|         return self.id == other.id |             self.photodb == other.photodb and | ||||||
|  |             self.id == other.id | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|     def __format__(self, formcode): |     def __format__(self, formcode): | ||||||
|         if formcode == 'r': |         if formcode == 'r': | ||||||
|  | @ -1878,14 +1991,6 @@ class Tag(ObjectBase, GroupableMixin): | ||||||
|         self.group_getter = self.photodb.get_tag |         self.group_getter = self.photodb.get_tag | ||||||
|         self._cached_qualified_name = None |         self._cached_qualified_name = None | ||||||
| 
 | 
 | ||||||
|     def __eq__(self, other): |  | ||||||
|         if isinstance(other, str): |  | ||||||
|             return self.name == other |  | ||||||
|         elif isinstance(other, Tag): |  | ||||||
|             return self.id == other.id and self.name == other.name |  | ||||||
|         else: |  | ||||||
|             return False |  | ||||||
| 
 |  | ||||||
|     def __hash__(self): |     def __hash__(self): | ||||||
|         return hash(self.name) |         return hash(self.name) | ||||||
| 
 | 
 | ||||||
|  | @ -2032,6 +2137,27 @@ class Tag(ObjectBase, GroupableMixin): | ||||||
|         return fetch |         return fetch | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class User(ObjectBase): | ||||||
|  |     ''' | ||||||
|  |     A dear friend of ours. | ||||||
|  |     ''' | ||||||
|  |     def __init__(self, photodb, row_tuple): | ||||||
|  |         self.photodb = photodb | ||||||
|  |         if isinstance(row_tuple, (list, tuple)): | ||||||
|  |             row_tuple = {SQL_USER_COLUMNS[index]: value for (index, value) in enumerate(row_tuple)} | ||||||
|  |         self.id = row_tuple['id'] | ||||||
|  |         self.username = row_tuple['username'] | ||||||
|  |         self.created = row_tuple['created'] | ||||||
|  | 
 | ||||||
|  |     def __repr__(self): | ||||||
|  |         rep = 'User:{id}:{username}'.format(id=self.id, username=self.username) | ||||||
|  |         return rep | ||||||
|  | 
 | ||||||
|  |     def __str__(self): | ||||||
|  |         rep = 'User:{username}'.format(username=self.username) | ||||||
|  |         return rep | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|     p = PhotoDB() |     p = PhotoDB() | ||||||
|     print(p) |     print(p) | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | bcrypt | ||||||
| flask | flask | ||||||
| gevent | gevent | ||||||
| pillow | pillow | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue