PageRenderTime 33ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/website/notifications/utils.py

https://gitlab.com/doublebits/osf.io
Python | 382 lines | 322 code | 27 blank | 33 comment | 34 complexity | fa17bd7eb37f79387199bf697f8768db MD5 | raw file
  1. import collections
  2. from modularodm import Q
  3. from modularodm.exceptions import NoResultsFound
  4. from framework.auth import signals
  5. from website.models import Node, User
  6. from website.notifications import constants
  7. from website.notifications import model
  8. from website.notifications.model import NotificationSubscription
  9. class NotificationsDict(dict):
  10. def __init__(self):
  11. super(NotificationsDict, self).__init__()
  12. self.update(messages=[], children=collections.defaultdict(NotificationsDict))
  13. def add_message(self, keys, messages):
  14. """
  15. :param keys: ordered list of project ids from parent to node (e.g. ['parent._id', 'node._id'])
  16. :param messages: built email message for an event that occurred on the node
  17. :return: nested dict with project/component ids as the keys with the message at the appropriate level
  18. """
  19. d_to_use = self
  20. for key in keys:
  21. d_to_use = d_to_use['children'][key]
  22. if not isinstance(messages, list):
  23. messages = [messages]
  24. d_to_use['messages'].extend(messages)
  25. def find_subscription_type(subscription):
  26. """Find subscription type string within specific subscription.
  27. Essentially removes extraneous parts of the string to get the type.
  28. """
  29. subs_available = list(constants.USER_SUBSCRIPTIONS_AVAILABLE.keys())
  30. subs_available.extend(list(constants.NODE_SUBSCRIPTIONS_AVAILABLE.keys()))
  31. for available in subs_available:
  32. if available in subscription:
  33. return available
  34. def to_subscription_key(uid, event):
  35. """Build the Subscription primary key for the given guid and event"""
  36. return u'{}_{}'.format(uid, event)
  37. def from_subscription_key(key):
  38. parsed_key = key.split("_", 1)
  39. return {
  40. 'uid': parsed_key[0],
  41. 'event': parsed_key[1]
  42. }
  43. @signals.contributor_removed.connect
  44. def remove_contributor_from_subscriptions(contributor, node):
  45. """ Remove contributor from node subscriptions unless the user is an
  46. admin on any of node's parent projects.
  47. """
  48. if contributor._id not in node.admin_contributor_ids:
  49. node_subscriptions = get_all_node_subscriptions(contributor, node)
  50. for subscription in node_subscriptions:
  51. subscription.remove_user_from_subscription(contributor)
  52. @signals.node_deleted.connect
  53. def remove_subscription(node):
  54. model.NotificationSubscription.remove(Q('owner', 'eq', node))
  55. parent = node.parent_node
  56. if parent and parent.child_node_subscriptions:
  57. for user_id in parent.child_node_subscriptions:
  58. if node._id in parent.child_node_subscriptions[user_id]:
  59. parent.child_node_subscriptions[user_id].remove(node._id)
  60. parent.save()
  61. def separate_users(node, user_ids):
  62. """Separates users into ones with permissions and ones without given a list.
  63. :param node: Node to separate based on permissions
  64. :param user_ids: List of ids, will also take and return User instances
  65. :return: list of subbed, list of removed user ids
  66. """
  67. removed = []
  68. subbed = []
  69. for user_id in user_ids:
  70. try:
  71. user = User.load(user_id)
  72. except TypeError:
  73. user = user_id
  74. if node.has_permission(user, 'read'):
  75. subbed.append(user_id)
  76. else:
  77. removed.append(user_id)
  78. return subbed, removed
  79. def users_to_remove(source_event, source_node, new_node):
  80. """Find users that do not have permissions on new_node.
  81. :param source_event: such as _file_updated
  82. :param source_node: Node instance where a subscription currently resides
  83. :param new_node: Node instance where a sub or new sub will be.
  84. :return: Dict of notification type lists with user_ids
  85. """
  86. removed_users = {key: [] for key in constants.NOTIFICATION_TYPES}
  87. if source_node == new_node:
  88. return removed_users
  89. old_sub = NotificationSubscription.load(to_subscription_key(source_node._id, source_event))
  90. old_node_sub = NotificationSubscription.load(to_subscription_key(source_node._id,
  91. '_'.join(source_event.split('_')[-2:])))
  92. if not old_sub and not old_node_sub:
  93. return removed_users
  94. for notification_type in constants.NOTIFICATION_TYPES:
  95. users = getattr(old_sub, notification_type, []) + getattr(old_node_sub, notification_type, [])
  96. subbed, removed_users[notification_type] = separate_users(new_node, users)
  97. return removed_users
  98. def move_subscription(remove_users, source_event, source_node, new_event, new_node):
  99. """Moves subscription from old_node to new_node
  100. :param remove_users: dictionary of lists of users to remove from the subscription
  101. :param source_event: A specific guid event <guid>_file_updated
  102. :param source_node: Instance of Node
  103. :param new_event: A specific guid event
  104. :param new_node: Instance of Node
  105. :return: Returns a NOTIFICATION_TYPES list of removed users without permissions
  106. """
  107. if source_node == new_node:
  108. return
  109. old_sub = NotificationSubscription.load(to_subscription_key(source_node._id, source_event))
  110. if not old_sub:
  111. return
  112. elif old_sub:
  113. old_sub.update_fields(_id=to_subscription_key(new_node._id, new_event), event_name=new_event,
  114. owner=new_node)
  115. new_sub = old_sub
  116. # Remove users that don't have permission on the new node.
  117. for notification_type in constants.NOTIFICATION_TYPES:
  118. if new_sub:
  119. for user_id in remove_users[notification_type]:
  120. if user_id in getattr(new_sub, notification_type, []):
  121. user = User.load(user_id)
  122. new_sub.remove_user_from_subscription(user)
  123. def get_configured_projects(user):
  124. """Filter all user subscriptions for ones that are on parent projects
  125. and return the project ids.
  126. :param user: modular odm User object
  127. :return: list of project ids for projects with no parent
  128. """
  129. configured_project_ids = set()
  130. user_subscriptions = get_all_user_subscriptions(user)
  131. for subscription in user_subscriptions:
  132. if subscription is None:
  133. continue
  134. # If the user has opted out of emails skip
  135. node = subscription.owner
  136. if not isinstance(node, Node) or (user in subscription.none and not node.parent_id):
  137. continue
  138. while node.parent_id and not node.is_deleted:
  139. node = Node.load(node.parent_id)
  140. if not node.is_deleted:
  141. configured_project_ids.add(node._id)
  142. return list(configured_project_ids)
  143. def check_project_subscriptions_are_all_none(user, node):
  144. node_subscriptions = get_all_node_subscriptions(user, node)
  145. for s in node_subscriptions:
  146. if user not in s.none:
  147. return False
  148. return True
  149. def get_all_user_subscriptions(user):
  150. """ Get all Subscription objects that the user is subscribed to"""
  151. for notification_type in constants.NOTIFICATION_TYPES:
  152. for subscription in getattr(user, notification_type, []):
  153. yield subscription
  154. def get_all_node_subscriptions(user, node, user_subscriptions=None):
  155. """ Get all Subscription objects for a node that the user is subscribed to
  156. :param user: modular odm User object
  157. :param node: modular odm Node object
  158. :param user_subscriptions: all Subscription objects that the user is subscribed to
  159. :return: list of Subscription objects for a node that the user is subscribed to
  160. """
  161. if not user_subscriptions:
  162. user_subscriptions = get_all_user_subscriptions(user)
  163. for subscription in user_subscriptions:
  164. if subscription and subscription.owner == node:
  165. yield subscription
  166. def format_data(user, node_ids):
  167. """ Format subscriptions data for project settings page
  168. :param user: modular odm User object
  169. :param node_ids: list of parent project ids
  170. :return: treebeard-formatted data
  171. """
  172. items = []
  173. for node_id in node_ids:
  174. node = Node.load(node_id)
  175. assert node, '{} is not a valid Node.'.format(node_id)
  176. can_read = node.has_permission(user, 'read')
  177. can_read_children = node.has_permission_on_children(user, 'read')
  178. if not can_read and not can_read_children:
  179. continue
  180. children = []
  181. # List project/node if user has at least 'read' permissions (contributor or admin viewer) or if
  182. # user is contributor on a component of the project/node
  183. if can_read:
  184. node_sub_available = list(constants.NODE_SUBSCRIPTIONS_AVAILABLE.keys())
  185. subscriptions = [subscription for subscription in get_all_node_subscriptions(user, node)
  186. if getattr(subscription, 'event_name') in node_sub_available]
  187. for subscription in subscriptions:
  188. index = node_sub_available.index(getattr(subscription, 'event_name'))
  189. children.append(serialize_event(user, subscription=subscription,
  190. node=node, event_description=node_sub_available.pop(index)))
  191. for node_sub in node_sub_available:
  192. children.append(serialize_event(user, node=node, event_description=node_sub))
  193. children.sort(key=lambda s: s['event']['title'])
  194. children.extend(format_data(
  195. user,
  196. [
  197. n._id
  198. for n in node.nodes
  199. if n.primary and
  200. not n.is_deleted
  201. ]
  202. ))
  203. item = {
  204. 'node': {
  205. 'id': node_id,
  206. 'url': node.url if can_read else '',
  207. 'title': node.title if can_read else 'Private Project',
  208. },
  209. 'children': children,
  210. 'kind': 'folder' if not node.node__parent or not node.parent_node.has_permission(user, 'read') else 'node',
  211. 'nodeType': node.project_or_component,
  212. 'category': node.category,
  213. 'permissions': {
  214. 'view': can_read,
  215. },
  216. }
  217. items.append(item)
  218. return items
  219. def format_user_subscriptions(user):
  220. """ Format user-level subscriptions (e.g. comment replies across the OSF) for user settings page"""
  221. user_subs_available = list(constants.USER_SUBSCRIPTIONS_AVAILABLE.keys())
  222. subscriptions = [
  223. serialize_event(
  224. user, subscription,
  225. event_description=user_subs_available.pop(user_subs_available.index(getattr(subscription, 'event_name')))
  226. )
  227. for subscription in get_all_user_subscriptions(user)
  228. if subscription is not None and getattr(subscription, 'event_name') in user_subs_available
  229. ]
  230. subscriptions.extend([serialize_event(user, event_description=sub) for sub in user_subs_available])
  231. return subscriptions
  232. def format_file_subscription(user, node_id, path, provider):
  233. """Format a single file event"""
  234. node = Node.load(node_id)
  235. wb_path = path.lstrip('/')
  236. for subscription in get_all_node_subscriptions(user, node):
  237. if wb_path in getattr(subscription, 'event_name'):
  238. return serialize_event(user, subscription, node)
  239. return serialize_event(user, node=node, event_description='file_updated')
  240. def serialize_event(user, subscription=None, node=None, event_description=None):
  241. """
  242. :param user: modular odm User object
  243. :param subscription: modular odm Subscription object, use if parsing particular subscription
  244. :param node: modular odm Node object, use if node is known
  245. :param event_description: use if specific subscription is known
  246. :return: treebeard-formatted subscription event
  247. """
  248. all_subs = constants.NODE_SUBSCRIPTIONS_AVAILABLE.copy()
  249. all_subs.update(constants.USER_SUBSCRIPTIONS_AVAILABLE)
  250. if not event_description:
  251. event_description = getattr(subscription, 'event_name')
  252. # Looks at only the types available. Deals with pre-pending file names.
  253. for sub_type in all_subs:
  254. if sub_type in event_description:
  255. event_type = sub_type
  256. else:
  257. event_type = event_description
  258. if node and node.node__parent:
  259. notification_type = 'adopt_parent'
  260. else:
  261. notification_type = 'none'
  262. if subscription:
  263. for n_type in constants.NOTIFICATION_TYPES:
  264. if user in getattr(subscription, n_type):
  265. notification_type = n_type
  266. return {
  267. 'event': {
  268. 'title': event_description,
  269. 'description': all_subs[event_type],
  270. 'notificationType': notification_type,
  271. 'parent_notification_type': get_parent_notification_type(node, event_type, user)
  272. },
  273. 'kind': 'event',
  274. 'children': []
  275. }
  276. def get_parent_notification_type(node, event, user):
  277. """
  278. Given an event on a node (e.g. comment on node 'xyz'), find the user's notification
  279. type on the parent project for the same event.
  280. :param obj node: event owner (Node or User object)
  281. :param str event: notification event (e.g. 'comment_replies')
  282. :param obj user: modular odm User object
  283. :return: str notification type (e.g. 'email_transactional')
  284. """
  285. if node and isinstance(node, Node) and node.node__parent and node.parent_node.has_permission(user, 'read'):
  286. for parent in node.node__parent:
  287. key = to_subscription_key(parent._id, event)
  288. try:
  289. subscription = model.NotificationSubscription.find_one(Q('_id', 'eq', key))
  290. except NoResultsFound:
  291. return get_parent_notification_type(parent, event, user)
  292. for notification_type in constants.NOTIFICATION_TYPES:
  293. if user in getattr(subscription, notification_type):
  294. return notification_type
  295. else:
  296. return get_parent_notification_type(parent, event, user)
  297. else:
  298. return None
  299. def format_user_and_project_subscriptions(user):
  300. """ Format subscriptions data for user settings page. """
  301. return [
  302. {
  303. 'node': {
  304. 'id': user._id,
  305. 'title': 'User Notifications',
  306. },
  307. 'kind': 'heading',
  308. 'children': format_user_subscriptions(user)
  309. },
  310. {
  311. 'node': {
  312. 'id': '',
  313. 'title': 'Project Notifications',
  314. },
  315. 'kind': 'heading',
  316. 'children': format_data(user, get_configured_projects(user))
  317. }]