models.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. # -*- coding: utf-8 -*-
  2. from decimal import Decimal
  3. import json
  4. import os
  5. from datetime import datetime
  6. from . import db, app
  7. import flask_sqlalchemy
  8. from sqlalchemy.types import TypeDecorator, VARCHAR
  9. from sqlalchemy.ext.mutable import MutableDict
  10. import whoosh
  11. from whoosh import fields, index, qparser
  12. class fakefloat(float):
  13. def __init__(self, value):
  14. self._value = value
  15. def __repr__(self):
  16. return str(self._value)
  17. def defaultencode(o):
  18. if isinstance(o, Decimal):
  19. # Subclass float with custom repr?
  20. return fakefloat(o)
  21. raise TypeError(repr(o) + " is not JSON serializable")
  22. class JSONEncodedDict(TypeDecorator):
  23. "Represents an immutable structure as a json-encoded string."
  24. impl = VARCHAR
  25. def process_bind_param(self, value, dialect):
  26. if value is not None:
  27. value = json.dumps(value, default=defaultencode)
  28. return value
  29. def process_result_value(self, value, dialect):
  30. if value is not None:
  31. value = json.loads(value)
  32. return value
  33. class ISP(db.Model):
  34. id = db.Column(db.Integer, primary_key=True)
  35. name = db.Column(db.String, nullable=False, index=True, unique=True)
  36. shortname = db.Column(db.String(12), index=True, unique=True)
  37. is_ffdn_member = db.Column(db.Boolean, default=False)
  38. is_disabled = db.Column(db.Boolean, default=False) # True = ISP will not appear
  39. url = db.Column(db.String)
  40. last_update_success = db.Column(db.DateTime)
  41. last_update_attempt = db.Column(db.DateTime)
  42. is_updatable = db.Column(db.Boolean, default=True) # set to False to disable JSON-URL updates
  43. tech_email = db.Column(db.String)
  44. cache_info = db.Column(db.Text)
  45. json = db.Column(MutableDict.as_mutable(JSONEncodedDict))
  46. def __init__(self, *args, **kwargs):
  47. super(ISP, self).__init__(*args, **kwargs)
  48. self.json={}
  49. def covered_areas_names(self):
  50. return [c['name'] for c in self.json.get('coveredAreas', [])]
  51. @staticmethod
  52. def str2date(_str):
  53. d=None
  54. try:
  55. d=datetime.strptime(_str, '%Y-%m-%d')
  56. except ValueError:
  57. pass
  58. if d is None:
  59. try:
  60. d=datetime.strptime(_str, '%Y-%m')
  61. except ValueError:
  62. pass
  63. return d
  64. def __repr__(self):
  65. return '<ISP %r>' % (self.shortname if self.shortname else self.name,)
  66. class ISPWhoosh(object):
  67. """
  68. Helper class to index the ISP model with Whoosh to allow full-text search
  69. """
  70. schema = fields.Schema(
  71. id=fields.ID(unique=True, stored=True),
  72. is_ffdn_member=fields.BOOLEAN(),
  73. is_disabled=fields.BOOLEAN(),
  74. name=fields.TEXT(),
  75. shortname=fields.TEXT(),
  76. description=fields.TEXT(),
  77. covered_areas=fields.KEYWORD(scorable=True, commas=True, lowercase=True),
  78. step=fields.NUMERIC(signed=False),
  79. )
  80. primary_key=schema._fields['id']
  81. @staticmethod
  82. def get_index_dir():
  83. return app.config.get('WHOOSH_INDEX_DIR', 'whoosh')
  84. @classmethod
  85. def get_index(cls):
  86. idxdir=cls.get_index_dir()
  87. if index.exists_in(idxdir):
  88. idx = index.open_dir(idxdir)
  89. else:
  90. if not os.path.exists(idxdir):
  91. os.makedirs(idxdir)
  92. idx = index.create_in(idxdir, cls.schema)
  93. return idx
  94. @classmethod
  95. def _search(cls, s, terms):
  96. return s.search(qparser.MultifieldParser([
  97. 'name', 'shortname', 'description', 'covered_areas'
  98. ], schema=cls.schema).parse(terms),
  99. mask=whoosh.query.Term('is_disabled', True))
  100. @classmethod
  101. def search(cls, terms):
  102. with ISPWhoosh.get_index().searcher() as s:
  103. sres=cls._search(s, terms)
  104. ranks={}
  105. for rank, r in enumerate(sres):
  106. ranks[r['id']]=rank
  107. if not len(ranks):
  108. return []
  109. _res=ISP.query.filter(ISP.id.in_(ranks.keys()))
  110. res=[None]*_res.count()
  111. for isp in _res:
  112. res[ranks[isp.id]]=isp
  113. return res
  114. @classmethod
  115. def update_document(cls, writer, model):
  116. kw={
  117. 'id': unicode(model.id),
  118. '_stored_id': model.id,
  119. 'is_ffdn_member': model.is_ffdn_member,
  120. 'is_disabled': model.is_disabled,
  121. 'name': model.name,
  122. 'shortname': model.shortname,
  123. 'description': model.json.get('description'),
  124. 'covered_areas': model.covered_areas_names(),
  125. 'step': model.json.get('progressStatus')
  126. }
  127. writer.update_document(**kw)
  128. @classmethod
  129. def _after_flush(cls, app, changes):
  130. isp_changes = []
  131. for change in changes:
  132. if change[0].__class__ == ISP:
  133. update = change[1] in ('update', 'insert')
  134. isp_changes.append((update, change[0]))
  135. if not len(changes):
  136. return
  137. idx=cls.get_index()
  138. with idx.writer() as writer:
  139. for update, model in isp_changes:
  140. if update:
  141. cls.update_document(writer, model)
  142. else:
  143. writer.delete_by_term(cls.primary_key, model.id)
  144. flask_sqlalchemy.models_committed.connect(ISPWhoosh._after_flush)