/src/appengine/libs/issue_management/monorail/issue.py

https://github.com/google/clusterfuzz · Python · 288 lines · 235 code · 33 blank · 20 comment · 20 complexity · 425248c13f652eb28e9e7bc0e3ef10aa MD5 · raw file

  1. # Copyright 2019 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Helper classes for managing issues."""
  15. from builtins import object
  16. import re
  17. def get_values_containing(target, expression):
  18. regex = re.compile(expression, re.DOTALL | re.IGNORECASE)
  19. return [value for value in target if regex.search(value)]
  20. def get_values_matching(target, expression):
  21. regex = re.compile(expression + r'\Z', re.DOTALL | re.IGNORECASE)
  22. return [value for value in target if regex.match(value)]
  23. def has_values_containing(target, expression):
  24. return any(get_values_containing(target, expression))
  25. def has_values_matching(target, expression):
  26. return any(get_values_matching(target, expression))
  27. def has_value(target, value):
  28. for target_value in target:
  29. if target_value.lower() == value.lower():
  30. return True
  31. return False
  32. class ChangeList(list):
  33. """List that tracks changes for incremental updates."""
  34. def __init__(self, seq=()):
  35. super(ChangeList, self).__init__(seq)
  36. self.added = set()
  37. self.removed = set()
  38. def append(self, p_object):
  39. list.append(self, p_object)
  40. if p_object in self.removed:
  41. self.removed.remove(p_object)
  42. else:
  43. self.added.add(p_object)
  44. def remove(self, value):
  45. list.remove(self, value)
  46. if value in self.added:
  47. self.added.remove(value)
  48. else:
  49. self.removed.add(value)
  50. def is_changed(self):
  51. return (len(self.added) + len(self.removed)) > 0
  52. def reset(self):
  53. self.added.clear()
  54. self.removed.clear()
  55. class Issue(object):
  56. """Class representing a single issue."""
  57. def __init__(self):
  58. self.blocking = None
  59. self.blocked_on = None
  60. self.body = None
  61. self.depends_on = None
  62. self.cc = ChangeList()
  63. self.closed = None
  64. self.comment = ''
  65. self.components = ChangeList()
  66. self.created = None
  67. self.id = 0
  68. self.labels = ChangeList()
  69. self.merged_into = None
  70. self.merged_into_project = None
  71. self.open = False
  72. self.owner = None
  73. self.reporter = None
  74. self.status = None
  75. self.stars = 0
  76. self.summary = None
  77. self.updated = None
  78. self.dirty = False
  79. self.send_email = True
  80. self.new = True
  81. self.itm = None
  82. self.project_name = None
  83. self.comments = None
  84. self.comment_count = 0
  85. self.first_comment = None
  86. self.last_comment = None
  87. self.changed = set()
  88. def __getattribute__(self, item):
  89. if item in ['body'] and not object.__getattribute__(self, item):
  90. comment = self.get_first_comment()
  91. self.__setattr__(item, comment.comment)
  92. return object.__getattribute__(self, item)
  93. def __setattr__(self, name, value):
  94. self.__dict__[name] = value
  95. if 'changed' in self.__dict__:
  96. self.__dict__['changed'].add(name)
  97. # Automatically set the project name if the itm is set.
  98. if name == 'itm' and value and hasattr(value, 'project_name'):
  99. self.__dict__['project_name'] = value.project_name
  100. # Treat comments and dirty flag specially.
  101. if name not in ('dirty', 'body', 'comments', 'itm', 'new', 'comment_count',
  102. 'first_comment', 'last_comment', 'project_name', 'changed',
  103. 'send_email'):
  104. self.__dict__['dirty'] = True
  105. if name in ('dirty') and not value:
  106. self.labels.reset()
  107. self.cc.reset()
  108. if 'changed' in self.__dict__:
  109. self.changed.clear()
  110. def __getstate__(self):
  111. """Ensure that we don't pickle the itm.
  112. This would raise an exception due to the way the apiary folks did their
  113. information (i.e. OAuth kicking us once again).
  114. """
  115. result_dict = self.__dict__.copy()
  116. del result_dict['itm']
  117. return result_dict
  118. def __setstate__(self, new_dict):
  119. self.__dict__.update(new_dict)
  120. self.itm = None
  121. def _remove_tracked_value(self, target, value):
  122. for existing_value in target:
  123. if existing_value.lower() == value.lower():
  124. target.remove(existing_value)
  125. self.dirty = True
  126. return
  127. def add_component(self, component):
  128. if not self.has_component(component):
  129. self.components.append(component)
  130. self.dirty = True
  131. def remove_component(self, component):
  132. if self.has_component(component):
  133. self._remove_tracked_value(self.components, component)
  134. self.add_component('-%s' % component)
  135. def remove_components_by_prefix(self, prefix):
  136. components = self.get_components_by_prefix(prefix)
  137. for component in components:
  138. self.remove_label(component)
  139. def add_label(self, label):
  140. if not self.has_label(label):
  141. self.labels.append(label)
  142. self.dirty = True
  143. def remove_label(self, label):
  144. if self.has_label(label):
  145. self._remove_tracked_value(self.labels, label)
  146. self.add_label('-%s' % label)
  147. def remove_label_by_prefix(self, prefix):
  148. labels = self.get_labels_by_prefix(prefix)
  149. for label in labels:
  150. self.remove_label(label)
  151. def add_cc(self, cc):
  152. if not self.has_cc(cc):
  153. self.cc.append(cc)
  154. self.dirty = True
  155. def remove_cc(self, cc):
  156. if self.has_cc(cc):
  157. self.cc.remove(cc)
  158. self.dirty = True
  159. def get_components_by_prefix(self, prefix):
  160. return get_values_matching(self.components, '%s.*' % prefix)
  161. def get_components_containing(self, expression):
  162. return get_values_containing(self.components, expression)
  163. def get_components_matching(self, expression):
  164. return get_values_matching(self.components, expression)
  165. def has_components_containing(self, expression):
  166. return has_values_containing(self.components, expression)
  167. def has_components_matching(self, expression):
  168. return has_values_matching(self.components, expression)
  169. def has_component(self, value):
  170. return has_value(self.components, value)
  171. def get_labels_by_prefix(self, prefix):
  172. return get_values_matching(self.labels, '%s.*' % prefix)
  173. def get_labels_containing(self, expression):
  174. return get_values_containing(self.labels, expression)
  175. def get_labels_matching(self, expression):
  176. return get_values_matching(self.labels, expression)
  177. def has_label_by_prefix(self, prefix):
  178. return has_values_containing(self.labels, '%s.*' % prefix)
  179. def has_label_containing(self, expression):
  180. return has_values_containing(self.labels, expression)
  181. def has_label_matching(self, expression):
  182. return has_values_matching(self.labels, expression)
  183. def has_label(self, value):
  184. return has_value(self.labels, value)
  185. def has_cc(self, value):
  186. return has_value(self.cc, value)
  187. def has_comment_with_label(self, label):
  188. for comment in self.get_comments():
  189. if comment.has_label(label):
  190. return True
  191. return False
  192. def has_comment_with_label_by_prefix(self, prefix):
  193. for comment in self.get_comments():
  194. if comment.get_labels_by_prefix(prefix):
  195. return True
  196. return False
  197. def get_comments(self):
  198. if not self.comments and self.itm:
  199. self.comments = self.itm.get_comments(self.id)
  200. self.comment_count = len(self.comments)
  201. return self.comments
  202. def get_first_comment(self):
  203. if not self.first_comment and self.itm:
  204. self.first_comment = self.itm.get_first_comment(self.id)
  205. return self.first_comment
  206. def get_last_comment(self):
  207. if not self.last_comment and self.itm:
  208. self.last_comment = self.itm.get_last_comment(self.id)
  209. return self.last_comment
  210. def get_comment_count(self):
  211. if not self.comment_count and self.itm:
  212. self.comment_count = self.itm.get_comment_count(self.id)
  213. return self.comment_count
  214. def save(self, send_email=None):
  215. if self.itm:
  216. self.itm.save(self, send_email)
  217. def refresh(self):
  218. if self.itm:
  219. self.comments = None
  220. self.last_comment = None
  221. self.comment_count = 0
  222. self.itm.refresh(self)
  223. return self