Electronic stock management. -- not updated anymore
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

models.py 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. from flask.ext.sqlalchemy import SQLAlchemy, BaseQuery
  2. from flask.ext.security import SQLAlchemyUserDatastore, \
  3. UserMixin, RoleMixin
  4. from sqlalchemy_mptt.mixins import BaseNestedSets
  5. from sqlalchemy import event
  6. from barcode_utils import generate_ean13
  7. from flask import current_app, url_for
  8. from sqlalchemy_utils.types import TSVectorType
  9. from sqlalchemy_searchable import make_searchable, SearchQueryMixin
  10. import os.path
  11. db = SQLAlchemy()
  12. make_searchable()
  13. roles_users = db.Table('roles_users',
  14. db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
  15. db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
  16. tags = db.Table('tags',
  17. db.Column('tag_id', db.Integer(), db.ForeignKey('tag.id')),
  18. db.Column('part_id', db.Integer(), db.ForeignKey('part.id'))
  19. )
  20. class Role(db.Model, RoleMixin):
  21. id = db.Column(db.Integer(), primary_key=True)
  22. name = db.Column(db.String(80), unique=True, nullable=False, info={'label': 'Name'})
  23. description = db.Column(db.String(255), info={'label': 'Description'})
  24. __mapper_args__ = {"order_by": name}
  25. class User(db.Model, UserMixin):
  26. BARCODE_TYPE = 10
  27. id = db.Column(db.Integer, primary_key=True)
  28. email = db.Column(db.String(255), unique=True, nullable=False, info={'label': 'Email'})
  29. name = db.Column(db.String(255), unique=True, nullable=False, info={'label': 'Name'})
  30. password = db.Column(db.String(255), nullable=False, info={'label': 'Password'})
  31. active = db.Column(db.Boolean())
  32. confirmed_at = db.Column(db.DateTime())
  33. barcode_str = db.Column(db.String(13), nullable=True, info={'label': 'Barcode string'}, default="0000000000000")
  34. roles = db.relationship('Role', secondary=roles_users, backref=db.backref('users', lazy='dynamic'))
  35. parts = db.relationship('Part', backref='user', lazy='dynamic')
  36. apitokens = db.relationship('Apitoken', backref='user', lazy='dynamic')
  37. __mapper_args__ = {"order_by": name}
  38. class Apitoken(db.Model):
  39. id = db.Column(db.Integer, primary_key=True)
  40. user_id = db.Column(db.Integer(), db.ForeignKey('user.id'), nullable=False)
  41. token = db.Column(db.String(), unique=True, nullable=False, info={'label': 'Token'})
  42. secret = db.Column(db.String(), unique=True, nullable=False, info={'label': 'Secret'})
  43. user_datastore = SQLAlchemyUserDatastore(db, User, Role)
  44. class Tag(db.Model):
  45. id = db.Column(db.Integer, primary_key=True)
  46. name = db.Column(db.String(100), unique=True)
  47. def __repr__(self):
  48. return self.name
  49. class Unit(db.Model):
  50. id = db.Column(db.Integer, primary_key=True)
  51. name = db.Column(db.String(255), unique=False, nullable=False)
  52. symbol = db.Column(db.String(255), unique=False, nullable=True)
  53. prefix = db.Column(db.String(255), unique=False, nullable=True)
  54. slug = db.Column(db.String(255), unique=False, nullable=True)
  55. description = db.Column(db.String(255), unique=False, nullable=True)
  56. parameters = db.relationship('PartParameter', backref='unit', lazy='dynamic')
  57. __mapper_args__ = {"order_by": name}
  58. def __repr__(self):
  59. return u"{1}{2} ({0})".format(self.name, self.prefix, self.symbol)
  60. class PartMeasurementUnit(db.Model):
  61. id = db.Column(db.Integer, primary_key=True)
  62. name = db.Column(db.String(255), unique=False, nullable=True)
  63. short_name = db.Column(db.String(255), unique=False, nullable=True)
  64. description = db.Column(db.String(255), unique=False, nullable=True)
  65. parts = db.relationship('Part', backref='part_measurement_unit', lazy='dynamic')
  66. __mapper_args__ = {"order_by": name}
  67. def __repr__(self):
  68. return u"{0} ({1})".format(self.name, self.short_name)
  69. class ManufacturerLogo(db.Model):
  70. BARCODE_TYPE = 45
  71. id = db.Column(db.Integer, primary_key=True)
  72. filename = db.Column(db.String(255), unique=False, nullable=True)
  73. orig_filename = db.Column(db.String(255), unique=False, nullable=True)
  74. hash = db.Column(db.String(255), unique=True, nullable=True)
  75. mimetype = db.Column(db.String(255), unique=False, nullable=True)
  76. filesize = db.Column(db.Integer, unique=False, nullable=True, default=0) # stored as bytes
  77. manufacturer_id = db.Column(db.Integer(), db.ForeignKey('manufacturer.id'), nullable=False)
  78. def get_uploads_url(self, fs=False):
  79. if fs:
  80. return os.path.join(current_app.config.get('UPLOADS_DEFAULT_DEST'),
  81. "attachments",
  82. str(self.BARCODE_TYPE),
  83. self.filename)
  84. else:
  85. return os.path.join('/uploads',
  86. "attachments",
  87. str(self.BARCODE_TYPE),
  88. self.filename)
  89. def get_thumbs_url(self, size, fs=False):
  90. if size not in ['mini', 'small']:
  91. return None
  92. filename = "thumb_{0}.{1}".format(size, self.filename)
  93. if fs:
  94. return os.path.join(current_app.config.get('UPLOADS_DEFAULT_DEST'),
  95. 'thumbs',
  96. str(self.BARCODE_TYPE),
  97. filename)
  98. else:
  99. return os.path.join('/uploads',
  100. 'thumbs',
  101. str(self.BARCODE_TYPE),
  102. filename)
  103. class Manufacturer(db.Model):
  104. BARCODE_TYPE = 15
  105. id = db.Column(db.Integer, primary_key=True)
  106. name = db.Column(db.String(255), unique=True, nullable=False, info={'label': 'Name'})
  107. address = db.Column(db.String(255), nullable=True, info={'label': 'Address'})
  108. url = db.Column(db.String(255), nullable=True, info={'label': 'URL'})
  109. email = db.Column(db.String(255), nullable=True, info={'label': 'Email'})
  110. comment = db.Column(db.String(255), nullable=True, info={'label': 'Comment'})
  111. phone = db.Column(db.String(255), nullable=True, info={'label': 'Phone'})
  112. fax = db.Column(db.String(255), nullable=True, info={'label': 'FAX'})
  113. barcode_str = db.Column(db.String(13), nullable=True, info={'label': 'Barcode string'}, default="0000000000000")
  114. part_manufacturers = db.relationship('PartManufacturer', backref='manufacturer', lazy='dynamic', cascade="all, delete-orphan")
  115. manufacturer_logos = db.relationship('ManufacturerLogo', backref='manufacturer', lazy='dynamic', cascade="all, delete-orphan")
  116. __mapper_args__ = {"order_by": name}
  117. def __repr__(self):
  118. return u"{0}".format(self.name)
  119. class Distributor(db.Model):
  120. BARCODE_TYPE = 20
  121. id = db.Column(db.Integer, primary_key=True)
  122. name = db.Column(db.String(255), unique=True, nullable=False, info={'label': 'Name'})
  123. address = db.Column(db.String(255), nullable=True, info={'label': 'Address'})
  124. url = db.Column(db.String(255), nullable=True, info={'label': 'URL'})
  125. email = db.Column(db.String(255), nullable=True, info={'label': 'Email'})
  126. comment = db.Column(db.String(255), nullable=True, info={'label': 'Comment'})
  127. phone = db.Column(db.String(255), nullable=True, info={'label': 'Phone'})
  128. fax = db.Column(db.String(255), nullable=True, info={'label': 'FAX'})
  129. skuurl = db.Column(db.String(255), nullable=True, info={'label': 'SKU Url'})
  130. barcode_str = db.Column(db.String(13), nullable=True, info={'label': 'Barcode string'}, default="0000000000000")
  131. part_distributors = db.relationship('PartDistributor', backref='distributor', lazy='dynamic')
  132. __mapper_args__ = {"order_by": name}
  133. class Project(db.Model):
  134. BARCODE_TYPE = 25
  135. id = db.Column(db.Integer, primary_key=True)
  136. name = db.Column(db.String(255), unique=True, nullable=False, info={'label': 'Name'})
  137. description = db.Column(db.String(255), nullable=True, info={'label': 'Description'})
  138. barcode_str = db.Column(db.String(13), nullable=True, info={'label': 'Barcode string'}, default="0000000000000")
  139. __mapper_args__ = {"order_by": name}
  140. class FootprintCategory(db.Model):
  141. id = db.Column(db.Integer, primary_key=True)
  142. name = db.Column(db.String(255), unique=False, nullable=True)
  143. description = db.Column(db.String(255), unique=False, nullable=True)
  144. footprints = db.relationship('Footprint', backref='footprint_category', cascade="all, delete-orphan")
  145. __mapper_args__ = {"order_by": name}
  146. class Footprint(db.Model):
  147. id = db.Column(db.Integer, primary_key=True)
  148. name = db.Column(db.String(64), unique=True)
  149. description = db.Column(db.String(255), nullable=True)
  150. footprint_category_id = db.Column(db.Integer, db.ForeignKey('footprint_category.id'), nullable=True)
  151. parts = db.relationship('Part', backref='footprint', lazy='dynamic')
  152. __mapper_args__ = {"order_by": name}
  153. def __repr__(self):
  154. return u"{0} - {1}".format(self.footprint_category.name, self.name)
  155. class StorageCategory(db.Model):
  156. BARCODE_TYPE = 30
  157. id = db.Column(db.Integer, primary_key=True)
  158. name = db.Column(db.String(255), unique=False, nullable=True)
  159. description = db.Column(db.String(255), unique=False, nullable=True)
  160. barcode_str = db.Column(db.String(13), nullable=True, info={'label': 'Barcode string'}, default="0000000000000")
  161. storages = db.relationship('Storage', backref='storage_category', cascade="all, delete-orphan")
  162. __mapper_args__ = {"order_by": name}
  163. class Storage(db.Model):
  164. BARCODE_TYPE = 35
  165. id = db.Column(db.Integer, primary_key=True)
  166. name = db.Column(db.String(64), unique=True)
  167. description = db.Column(db.String(255), nullable=True)
  168. barcode_str = db.Column(db.String(13), nullable=True, info={'label': 'Barcode string'}, default="0000000000000")
  169. storage_category_id = db.Column(db.Integer, db.ForeignKey('storage_category.id'), nullable=True)
  170. parts = db.relationship('Part', backref='storage', lazy='dynamic')
  171. __mapper_args__ = {"order_by": name}
  172. def __repr__(self):
  173. return u"{0} - {1}".format(self.storage_category.name, self.name)
  174. class PartParameter(db.Model):
  175. id = db.Column(db.Integer, primary_key=True)
  176. name = db.Column(db.String(64), unique=True)
  177. description = db.Column(db.String(255), nullable=True)
  178. unit_value = db.Column(db.String(255), nullable=True)
  179. unit_id = db.Column(db.Integer(), db.ForeignKey('unit.id'), nullable=True)
  180. part_id = db.Column(db.Integer(), db.ForeignKey('part.id'), nullable=False)
  181. __mapper_args__ = {"order_by": name}
  182. # links #
  183. class PartManufacturer(db.Model):
  184. id = db.Column(db.Integer, primary_key=True)
  185. sku = db.Column(db.String(255), nullable=True)
  186. part_id = db.Column(db.Integer(), db.ForeignKey('part.id'), nullable=False)
  187. manufacturer_id = db.Column(db.Integer(), db.ForeignKey('manufacturer.id'), nullable=False)
  188. class PartDistributor(db.Model):
  189. id = db.Column(db.Integer, primary_key=True)
  190. sku = db.Column(db.String(255), nullable=True)
  191. part_id = db.Column(db.Integer(), db.ForeignKey('part.id'), nullable=False)
  192. distributor_id = db.Column(db.Integer(), db.ForeignKey('distributor.id'), nullable=False)
  193. class PartProject(db.Model):
  194. id = db.Column(db.Integer, primary_key=True)
  195. part_id = db.Column(db.Integer(), db.ForeignKey('part.id'), nullable=False)
  196. project_id = db.Column(db.Integer(), db.ForeignKey('project.id'), nullable=False)
  197. class PartAttachment(db.Model):
  198. id = db.Column(db.Integer, primary_key=True)
  199. description = db.Column(db.String(255), unique=False, nullable=True)
  200. filename = db.Column(db.String(255), unique=False, nullable=True)
  201. orig_filename = db.Column(db.String(255), unique=False, nullable=True)
  202. hash = db.Column(db.String(255), unique=True, nullable=True)
  203. mimetype = db.Column(db.String(255), unique=False, nullable=True)
  204. simple_type = db.Column(db.String(255), unique=False, nullable=True)
  205. default = db.Column(db.Boolean, unique=False, nullable=False, default=False)
  206. remote_uri = db.Column(db.String(255), unique=False, nullable=True)
  207. is_remote = db.Column(db.Boolean, unique=False, nullable=False, default=False)
  208. remote_cached = db.Column(db.Boolean, unique=False, nullable=False, default=False)
  209. remote_processed = db.Column(db.Boolean, unique=False, nullable=False, default=False)
  210. cache_error = db.Column(db.String(255), unique=False, nullable=True, default=None)
  211. filesize = db.Column(db.Integer, unique=False, nullable=True, default=0) # stored as bytes
  212. part_id = db.Column(db.Integer(), db.ForeignKey('part.id'), nullable=False)
  213. def get_uploads_url(self, fs=False):
  214. if self.is_remote:
  215. if not self.remote_processed or not self.remote_cached:
  216. return None
  217. thing = "cache"
  218. else:
  219. thing = "attachments"
  220. if fs:
  221. return os.path.join(current_app.config.get('UPLOADS_DEFAULT_DEST'),
  222. thing,
  223. str(self.part.BARCODE_TYPE),
  224. self.filename)
  225. else:
  226. return os.path.join('/uploads',
  227. thing,
  228. str(self.part.BARCODE_TYPE),
  229. self.filename)
  230. def get_thumbs_url(self, size, fs=False):
  231. if size not in ['mini', 'small', 'medium', 'preview']:
  232. return None
  233. if self.is_remote:
  234. if not self.remote_processed or not self.remote_cached:
  235. return None
  236. filename = "thumb_{0}.{1}".format(size, self.filename)
  237. if fs:
  238. return os.path.join(current_app.config.get('UPLOADS_DEFAULT_DEST'),
  239. 'thumbs',
  240. str(self.part.BARCODE_TYPE),
  241. filename)
  242. else:
  243. return os.path.join('/uploads',
  244. 'thumbs',
  245. str(self.part.BARCODE_TYPE),
  246. filename)
  247. class PartQuery(BaseQuery, SearchQueryMixin):
  248. pass
  249. class Part(db.Model):
  250. query_class = PartQuery
  251. __tablename__ = "part"
  252. BARCODE_TYPE = 40
  253. id = db.Column(db.Integer, primary_key=True)
  254. name = db.Column(db.String(255), unique=False, info={'label': 'Name'})
  255. description = db.Column(db.String(255), nullable=True, info={'label': 'Description'})
  256. comment = db.Column(db.String(255), nullable=True, info={'label': 'Comment'})
  257. stock_level = db.Column(db.Integer(), default=1, info={'label': 'Stock level'})
  258. min_stock_level = db.Column(db.Integer(), default=0, info={'label': 'Minimal Stock'})
  259. needs_review = db.Column(db.Boolean(), default=0, info={'label': 'Needs Review'})
  260. status = db.Column(db.String(255), nullable=True, info={'label': 'Status'})
  261. condition = db.Column(db.String(255), nullable=True, info={'label': 'Condition'})
  262. barcode_str = db.Column(db.String(13), nullable=True, info={'label': 'Barcode string'}, default="0000000000000")
  263. can_be_sold = db.Column(db.Boolean(), default=False, info={'label': 'Can be sold'})
  264. private = db.Column(db.Boolean(), default=False, info={'label': 'Private part'})
  265. tags_slug = db.Column(db.Text(), default="untagged")
  266. footprint_id = db.Column(db.Integer(), db.ForeignKey('footprint.id'), nullable=True)
  267. part_measurement_unit_id = db.Column(db.Integer(), db.ForeignKey('part_measurement_unit.id'), nullable=False)
  268. storage_id = db.Column(db.Integer(), db.ForeignKey('storage.id'), nullable=True)
  269. tags = db.relationship('Tag', secondary=tags, backref=db.backref('parts', lazy='dynamic'))
  270. manufacturers = db.relationship('PartManufacturer', backref='part', cascade="all, delete-orphan")
  271. distributors = db.relationship('PartDistributor', backref='part', cascade="all, delete-orphan")
  272. projects = db.relationship('PartProject', backref='part', cascade="all, delete-orphan")
  273. parameters = db.relationship('PartParameter', backref='part', lazy='dynamic', cascade="all, delete-orphan")
  274. attachments = db.relationship('PartAttachment', backref='part', lazy='dynamic', cascade="all, delete-orphan")
  275. user_id = db.Column(db.Integer(), db.ForeignKey('user.id'), nullable=False)
  276. search_vector = db.Column(TSVectorType('name', 'description', 'comment', 'status', 'condition', 'barcode_str', 'tags_slug'))
  277. __mapper_args__ = {"order_by": name}
  278. BARCODE_TYPES = [
  279. {'model': 'user', 'id': User.BARCODE_TYPE},
  280. {'model': 'manufacturer', 'id': Manufacturer.BARCODE_TYPE},
  281. {'model': 'distributor', 'id': Distributor.BARCODE_TYPE},
  282. {'model': 'project', 'id': Project.BARCODE_TYPE},
  283. {'model': 'storage_category', 'id': StorageCategory.BARCODE_TYPE},
  284. {'model': 'storage', 'id': Storage.BARCODE_TYPE},
  285. {'model': 'part', 'id': Part.BARCODE_TYPE},
  286. {'model': 'manufacturer_logo', 'id': ManufacturerLogo.BARCODE_TYPE}
  287. ]
  288. # Events for barcodes generation
  289. @event.listens_for(Part, 'after_insert')
  290. def gen_part_barcode(mapper, connection, target):
  291. out_dir = current_app.config.get('UPLOADS_DEFAULT_DEST')
  292. barcode = generate_ean13(Part.BARCODE_TYPE, target.id, out_dir)
  293. part_table = Part.__table__
  294. if target.barcode_str is None or target.barcode_str == "0000000000000":
  295. connection.execute(
  296. part_table.update().where(part_table.c.id == target.id).values(barcode_str=barcode["Barcode"])
  297. )
  298. @event.listens_for(Storage, 'after_insert')
  299. def gen_storage_barcode(mapper, connection, target):
  300. out_dir = current_app.config.get('UPLOADS_DEFAULT_DEST')
  301. barcode = generate_ean13(Storage.BARCODE_TYPE, target.id, out_dir)
  302. _table = Part.__table__
  303. if target.barcode_str is None:
  304. connection.execute(
  305. _table.update().where(_table.c.id == target.id).values(barcode_str=barcode["Barcode"])
  306. )
  307. @event.listens_for(StorageCategory, 'after_insert')
  308. def gen_storage_category_barcode(mapper, connection, target):
  309. out_dir = current_app.config.get('UPLOADS_DEFAULT_DEST')
  310. barcode = generate_ean13(StorageCategory.BARCODE_TYPE, target.id, out_dir)
  311. _table = StorageCategory.__table__
  312. if target.barcode_str is None:
  313. connection.execute(
  314. _table.update().where(_table.c.id == target.id).values(barcode_str=barcode["Barcode"])
  315. )
  316. @event.listens_for(Project, 'after_insert')
  317. def gen_project_barcode(mapper, connection, target):
  318. out_dir = current_app.config.get('UPLOADS_DEFAULT_DEST')
  319. barcode = generate_ean13(Project.BARCODE_TYPE, target.id, out_dir)
  320. _table = Project.__table__
  321. if target.barcode_str is None:
  322. connection.execute(
  323. _table.update().where(_table.c.id == target.id).values(barcode_str=barcode["Barcode"])
  324. )
  325. @event.listens_for(Distributor, 'after_insert')
  326. def gen_distributor_barcode(mapper, connection, target):
  327. out_dir = current_app.config.get('UPLOADS_DEFAULT_DEST')
  328. barcode = generate_ean13(Distributor.BARCODE_TYPE, target.id, out_dir)
  329. _table = Distributor.__table__
  330. if target.barcode_str is None:
  331. connection.execute(
  332. _table.update().where(_table.c.id == target.id).values(barcode_str=barcode["Barcode"])
  333. )
  334. @event.listens_for(Manufacturer, 'after_insert')
  335. def gen_manufacturer_barcode(mapper, connection, target):
  336. out_dir = current_app.config.get('UPLOADS_DEFAULT_DEST')
  337. barcode = generate_ean13(Manufacturer.BARCODE_TYPE, target.id, out_dir)
  338. _table = Manufacturer.__table__
  339. if target.barcode_str is None:
  340. connection.execute(
  341. _table.update().where(_table.c.id == target.id).values(barcode_str=barcode["Barcode"])
  342. )
  343. @event.listens_for(User, 'after_insert')
  344. def gen_user_barcode(mapper, connection, target):
  345. out_dir = current_app.config.get('UPLOADS_DEFAULT_DEST')
  346. barcode = generate_ean13(User.BARCODE_TYPE, target.id, out_dir)
  347. _table = User.__table__
  348. if target.barcode_str is None:
  349. connection.execute(
  350. _table.update().where(_table.c.id == target.id).values(barcode_str=barcode["Barcode"])
  351. )
  352. def safe_delete(path):
  353. """ Not really """
  354. if not path: return
  355. if os.path.isfile(path):
  356. os.remove(path)
  357. @event.listens_for(ManufacturerLogo, 'after_delete')
  358. def del_manufacturer_files(mapper, connection, target):
  359. safe_delete(target.get_thumbs_url('mini', fs=True))
  360. safe_delete(target.get_thumbs_url('small', fs=True))
  361. safe_delete(target.get_uploads_url(fs=True))
  362. @event.listens_for(Part, 'after_delete')
  363. def del_part_files(mapper, connection, target):
  364. def urlfor(barcode, size):
  365. return os.path.join(
  366. current_app.config['UPLOADS_DEFAULT_DEST'],
  367. "barcodes",
  368. str(Part.BARCODE_TYPE),
  369. "{0}{1}.png".format(size, barcode)
  370. )
  371. safe_delete(urlfor(target.barcode_str, 'normal_'))
  372. safe_delete(urlfor(target.barcode_str, 'medium_'))
  373. safe_delete(urlfor(target.barcode_str, 'mini_'))
  374. safe_delete(urlfor(target.barcode_str, 'mini_notext_'))
  375. @event.listens_for(PartAttachment, 'after_delete')
  376. def del_part_attachment_files(mapper, connection, target):
  377. safe_delete(target.get_thumbs_url('mini', fs=True))
  378. safe_delete(target.get_thumbs_url('small', fs=True))
  379. safe_delete(target.get_thumbs_url('medium', fs=True))
  380. safe_delete(target.get_thumbs_url('preview', fs=True))
  381. safe_delete(target.get_uploads_url(fs=True))
  382. @event.listens_for(Part, 'after_update')
  383. @event.listens_for(Part, 'after_insert')
  384. def gen_tags_slug(mapper, connection, target):
  385. slugs = ""
  386. for i in target.tags:
  387. slugs += i.name
  388. slugs += " "
  389. if slugs == "":
  390. slugs = "untagged"
  391. connection.execute(
  392. Part.__table__.update().where(Part.__table__.c.id == target.id).values(tags_slug=slugs)
  393. )