##// END OF EJS Templates
channelstream: push events with comments on single commits....
marcink -
r1970:ef3d81a8 default
parent child Browse files
Show More
@@ -1,253 +1,254 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import hashlib
22 import hashlib
23 import itsdangerous
23 import itsdangerous
24 import logging
24 import logging
25 import requests
25 import requests
26 import datetime
26 import datetime
27
27
28 from dogpile.core import ReadWriteMutex
28 from dogpile.core import ReadWriteMutex
29 from pyramid.threadlocal import get_current_registry
29 from pyramid.threadlocal import get_current_registry
30
30
31 import rhodecode.lib.helpers as h
31 import rhodecode.lib.helpers as h
32 from rhodecode.lib.auth import HasRepoPermissionAny
32 from rhodecode.lib.auth import HasRepoPermissionAny
33 from rhodecode.lib.ext_json import json
33 from rhodecode.lib.ext_json import json
34 from rhodecode.model.db import User
34 from rhodecode.model.db import User
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38 LOCK = ReadWriteMutex()
38 LOCK = ReadWriteMutex()
39
39
40 STATE_PUBLIC_KEYS = ['id', 'username', 'first_name', 'last_name',
40 STATE_PUBLIC_KEYS = ['id', 'username', 'first_name', 'last_name',
41 'icon_link', 'display_name', 'display_link']
41 'icon_link', 'display_name', 'display_link']
42
42
43
43
44 class ChannelstreamException(Exception):
44 class ChannelstreamException(Exception):
45 pass
45 pass
46
46
47
47
48 class ChannelstreamConnectionException(ChannelstreamException):
48 class ChannelstreamConnectionException(ChannelstreamException):
49 pass
49 pass
50
50
51
51
52 class ChannelstreamPermissionException(ChannelstreamException):
52 class ChannelstreamPermissionException(ChannelstreamException):
53 pass
53 pass
54
54
55
55
56 def channelstream_request(config, payload, endpoint, raise_exc=True):
56 def channelstream_request(config, payload, endpoint, raise_exc=True):
57 signer = itsdangerous.TimestampSigner(config['secret'])
57 signer = itsdangerous.TimestampSigner(config['secret'])
58 sig_for_server = signer.sign(endpoint)
58 sig_for_server = signer.sign(endpoint)
59 secret_headers = {'x-channelstream-secret': sig_for_server,
59 secret_headers = {'x-channelstream-secret': sig_for_server,
60 'x-channelstream-endpoint': endpoint,
60 'x-channelstream-endpoint': endpoint,
61 'Content-Type': 'application/json'}
61 'Content-Type': 'application/json'}
62 req_url = 'http://{}{}'.format(config['server'], endpoint)
62 req_url = 'http://{}{}'.format(config['server'], endpoint)
63 response = None
63 response = None
64 try:
64 try:
65 response = requests.post(req_url, data=json.dumps(payload),
65 response = requests.post(req_url, data=json.dumps(payload),
66 headers=secret_headers).json()
66 headers=secret_headers).json()
67 except requests.ConnectionError:
67 except requests.ConnectionError:
68 log.exception('ConnectionError happened')
68 log.exception('ConnectionError happened')
69 if raise_exc:
69 if raise_exc:
70 raise ChannelstreamConnectionException()
70 raise ChannelstreamConnectionException()
71 except Exception:
71 except Exception:
72 log.exception('Exception related to channelstream happened')
72 log.exception('Exception related to channelstream happened')
73 if raise_exc:
73 if raise_exc:
74 raise ChannelstreamConnectionException()
74 raise ChannelstreamConnectionException()
75 return response
75 return response
76
76
77
77
78 def get_user_data(user_id):
78 def get_user_data(user_id):
79 user = User.get(user_id)
79 user = User.get(user_id)
80 return {
80 return {
81 'id': user.user_id,
81 'id': user.user_id,
82 'username': user.username,
82 'username': user.username,
83 'first_name': user.first_name,
83 'first_name': user.first_name,
84 'last_name': user.last_name,
84 'last_name': user.last_name,
85 'icon_link': h.gravatar_url(user.email, 60),
85 'icon_link': h.gravatar_url(user.email, 60),
86 'display_name': h.person(user, 'username_or_name_or_email'),
86 'display_name': h.person(user, 'username_or_name_or_email'),
87 'display_link': h.link_to_user(user),
87 'display_link': h.link_to_user(user),
88 'notifications': user.user_data.get('notification_status', True)
88 'notifications': user.user_data.get('notification_status', True)
89 }
89 }
90
90
91
91
92 def broadcast_validator(channel_name):
92 def broadcast_validator(channel_name):
93 """ checks if user can access the broadcast channel """
93 """ checks if user can access the broadcast channel """
94 if channel_name == 'broadcast':
94 if channel_name == 'broadcast':
95 return True
95 return True
96
96
97
97
98 def repo_validator(channel_name):
98 def repo_validator(channel_name):
99 """ checks if user can access the broadcast channel """
99 """ checks if user can access the broadcast channel """
100 channel_prefix = '/repo$'
100 channel_prefix = '/repo$'
101 if channel_name.startswith(channel_prefix):
101 if channel_name.startswith(channel_prefix):
102 elements = channel_name[len(channel_prefix):].split('$')
102 elements = channel_name[len(channel_prefix):].split('$')
103 repo_name = elements[0]
103 repo_name = elements[0]
104 can_access = HasRepoPermissionAny(
104 can_access = HasRepoPermissionAny(
105 'repository.read',
105 'repository.read',
106 'repository.write',
106 'repository.write',
107 'repository.admin')(repo_name)
107 'repository.admin')(repo_name)
108 log.debug('permission check for {} channel '
108 log.debug('permission check for {} channel '
109 'resulted in {}'.format(repo_name, can_access))
109 'resulted in {}'.format(repo_name, can_access))
110 if can_access:
110 if can_access:
111 return True
111 return True
112 return False
112 return False
113
113
114
114
115 def check_channel_permissions(channels, plugin_validators, should_raise=True):
115 def check_channel_permissions(channels, plugin_validators, should_raise=True):
116 valid_channels = []
116 valid_channels = []
117
117
118 validators = [broadcast_validator, repo_validator]
118 validators = [broadcast_validator, repo_validator]
119 if plugin_validators:
119 if plugin_validators:
120 validators.extend(plugin_validators)
120 validators.extend(plugin_validators)
121 for channel_name in channels:
121 for channel_name in channels:
122 is_valid = False
122 is_valid = False
123 for validator in validators:
123 for validator in validators:
124 if validator(channel_name):
124 if validator(channel_name):
125 is_valid = True
125 is_valid = True
126 break
126 break
127 if is_valid:
127 if is_valid:
128 valid_channels.append(channel_name)
128 valid_channels.append(channel_name)
129 else:
129 else:
130 if should_raise:
130 if should_raise:
131 raise ChannelstreamPermissionException()
131 raise ChannelstreamPermissionException()
132 return valid_channels
132 return valid_channels
133
133
134
134
135 def get_channels_info(self, channels):
135 def get_channels_info(self, channels):
136 payload = {'channels': channels}
136 payload = {'channels': channels}
137 # gather persistence info
137 # gather persistence info
138 return channelstream_request(self._config(), payload, '/info')
138 return channelstream_request(self._config(), payload, '/info')
139
139
140
140
141 def parse_channels_info(info_result, include_channel_info=None):
141 def parse_channels_info(info_result, include_channel_info=None):
142 """
142 """
143 Returns data that contains only secure information that can be
143 Returns data that contains only secure information that can be
144 presented to clients
144 presented to clients
145 """
145 """
146 include_channel_info = include_channel_info or []
146 include_channel_info = include_channel_info or []
147
147
148 user_state_dict = {}
148 user_state_dict = {}
149 for userinfo in info_result['users']:
149 for userinfo in info_result['users']:
150 user_state_dict[userinfo['user']] = {
150 user_state_dict[userinfo['user']] = {
151 k: v for k, v in userinfo['state'].items()
151 k: v for k, v in userinfo['state'].items()
152 if k in STATE_PUBLIC_KEYS
152 if k in STATE_PUBLIC_KEYS
153 }
153 }
154
154
155 channels_info = {}
155 channels_info = {}
156
156
157 for c_name, c_info in info_result['channels'].items():
157 for c_name, c_info in info_result['channels'].items():
158 if c_name not in include_channel_info:
158 if c_name not in include_channel_info:
159 continue
159 continue
160 connected_list = []
160 connected_list = []
161 for userinfo in c_info['users']:
161 for userinfo in c_info['users']:
162 connected_list.append({
162 connected_list.append({
163 'user': userinfo['user'],
163 'user': userinfo['user'],
164 'state': user_state_dict[userinfo['user']]
164 'state': user_state_dict[userinfo['user']]
165 })
165 })
166 channels_info[c_name] = {'users': connected_list,
166 channels_info[c_name] = {'users': connected_list,
167 'history': c_info['history']}
167 'history': c_info['history']}
168
168
169 return channels_info
169 return channels_info
170
170
171
171
172 def log_filepath(history_location, channel_name):
172 def log_filepath(history_location, channel_name):
173 hasher = hashlib.sha256()
173 hasher = hashlib.sha256()
174 hasher.update(channel_name.encode('utf8'))
174 hasher.update(channel_name.encode('utf8'))
175 filename = '{}.log'.format(hasher.hexdigest())
175 filename = '{}.log'.format(hasher.hexdigest())
176 filepath = os.path.join(history_location, filename)
176 filepath = os.path.join(history_location, filename)
177 return filepath
177 return filepath
178
178
179
179
180 def read_history(history_location, channel_name):
180 def read_history(history_location, channel_name):
181 filepath = log_filepath(history_location, channel_name)
181 filepath = log_filepath(history_location, channel_name)
182 if not os.path.exists(filepath):
182 if not os.path.exists(filepath):
183 return []
183 return []
184 history_lines_limit = -100
184 history_lines_limit = -100
185 history = []
185 history = []
186 with open(filepath, 'rb') as f:
186 with open(filepath, 'rb') as f:
187 for line in f.readlines()[history_lines_limit:]:
187 for line in f.readlines()[history_lines_limit:]:
188 try:
188 try:
189 history.append(json.loads(line))
189 history.append(json.loads(line))
190 except Exception:
190 except Exception:
191 log.exception('Failed to load history')
191 log.exception('Failed to load history')
192 return history
192 return history
193
193
194
194
195 def update_history_from_logs(config, channels, payload):
195 def update_history_from_logs(config, channels, payload):
196 history_location = config.get('history.location')
196 history_location = config.get('history.location')
197 for channel in channels:
197 for channel in channels:
198 history = read_history(history_location, channel)
198 history = read_history(history_location, channel)
199 payload['channels_info'][channel]['history'] = history
199 payload['channels_info'][channel]['history'] = history
200
200
201
201
202 def write_history(config, message):
202 def write_history(config, message):
203 """ writes a messge to a base64encoded filename """
203 """ writes a messge to a base64encoded filename """
204 history_location = config.get('history.location')
204 history_location = config.get('history.location')
205 if not os.path.exists(history_location):
205 if not os.path.exists(history_location):
206 return
206 return
207 try:
207 try:
208 LOCK.acquire_write_lock()
208 LOCK.acquire_write_lock()
209 filepath = log_filepath(history_location, message['channel'])
209 filepath = log_filepath(history_location, message['channel'])
210 with open(filepath, 'ab') as f:
210 with open(filepath, 'ab') as f:
211 json.dump(message, f)
211 json.dump(message, f)
212 f.write('\n')
212 f.write('\n')
213 finally:
213 finally:
214 LOCK.release_write_lock()
214 LOCK.release_write_lock()
215
215
216
216
217 def get_connection_validators(registry):
217 def get_connection_validators(registry):
218 validators = []
218 validators = []
219 for k, config in registry.rhodecode_plugins.iteritems():
219 for k, config in registry.rhodecode_plugins.iteritems():
220 validator = config.get('channelstream', {}).get('connect_validator')
220 validator = config.get('channelstream', {}).get('connect_validator')
221 if validator:
221 if validator:
222 validators.append(validator)
222 validators.append(validator)
223 return validators
223 return validators
224
224
225
225
226 def post_message(channel, message, username, registry=None):
226 def post_message(channel, message, username, registry=None):
227
227
228 if not registry:
228 if not registry:
229 registry = get_current_registry()
229 registry = get_current_registry()
230
230
231 log.debug('Channelstream: sending notification to channel %s', channel)
231 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
232 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
232 channelstream_config = rhodecode_plugins.get('channelstream', {})
233 channelstream_config = rhodecode_plugins.get('channelstream', {})
233 if channelstream_config.get('enabled'):
234 if channelstream_config.get('enabled'):
234 payload = {
235 payload = {
235 'type': 'message',
236 'type': 'message',
236 'timestamp': datetime.datetime.utcnow(),
237 'timestamp': datetime.datetime.utcnow(),
237 'user': 'system',
238 'user': 'system',
238 'exclude_users': [username],
239 'exclude_users': [username],
239 'channel': channel,
240 'channel': channel,
240 'message': {
241 'message': {
241 'message': message,
242 'message': message,
242 'level': 'success',
243 'level': 'success',
243 'topic': '/notifications'
244 'topic': '/notifications'
244 }
245 }
245 }
246 }
246
247
247 try:
248 try:
248 return channelstream_request(
249 return channelstream_request(
249 channelstream_config, [payload], '/message',
250 channelstream_config, [payload], '/message',
250 raise_exc=False)
251 raise_exc=False)
251 except ChannelstreamException:
252 except ChannelstreamException:
252 log.exception('Failed to send channelstream data')
253 log.exception('Failed to send channelstream data')
253 raise
254 raise
@@ -1,665 +1,656 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import collections
27 import collections
28
28
29 from datetime import datetime
29 from datetime import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pyramid.threadlocal import get_current_registry, get_current_request
32 from pyramid.threadlocal import get_current_registry, get_current_request
33 from sqlalchemy.sql.expression import null
33 from sqlalchemy.sql.expression import null
34 from sqlalchemy.sql.functions import coalesce
34 from sqlalchemy.sql.functions import coalesce
35
35
36 from rhodecode.lib import helpers as h, diffs
36 from rhodecode.lib import helpers as h, diffs, channelstream
37 from rhodecode.lib import audit_logger
37 from rhodecode.lib import audit_logger
38 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.channelstream import channelstream_request
39 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
39 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
40 from rhodecode.model import BaseModel
40 from rhodecode.model import BaseModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
43 from rhodecode.model.notification import NotificationModel
43 from rhodecode.model.notification import NotificationModel
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.settings import VcsSettingsModel
45 from rhodecode.model.settings import VcsSettingsModel
46 from rhodecode.model.notification import EmailNotificationModel
46 from rhodecode.model.notification import EmailNotificationModel
47 from rhodecode.model.validation_schema.schemas import comment_schema
47 from rhodecode.model.validation_schema.schemas import comment_schema
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class CommentsModel(BaseModel):
53 class CommentsModel(BaseModel):
54
54
55 cls = ChangesetComment
55 cls = ChangesetComment
56
56
57 DIFF_CONTEXT_BEFORE = 3
57 DIFF_CONTEXT_BEFORE = 3
58 DIFF_CONTEXT_AFTER = 3
58 DIFF_CONTEXT_AFTER = 3
59
59
60 def __get_commit_comment(self, changeset_comment):
60 def __get_commit_comment(self, changeset_comment):
61 return self._get_instance(ChangesetComment, changeset_comment)
61 return self._get_instance(ChangesetComment, changeset_comment)
62
62
63 def __get_pull_request(self, pull_request):
63 def __get_pull_request(self, pull_request):
64 return self._get_instance(PullRequest, pull_request)
64 return self._get_instance(PullRequest, pull_request)
65
65
66 def _extract_mentions(self, s):
66 def _extract_mentions(self, s):
67 user_objects = []
67 user_objects = []
68 for username in extract_mentioned_users(s):
68 for username in extract_mentioned_users(s):
69 user_obj = User.get_by_username(username, case_insensitive=True)
69 user_obj = User.get_by_username(username, case_insensitive=True)
70 if user_obj:
70 if user_obj:
71 user_objects.append(user_obj)
71 user_objects.append(user_obj)
72 return user_objects
72 return user_objects
73
73
74 def _get_renderer(self, global_renderer='rst'):
74 def _get_renderer(self, global_renderer='rst'):
75 try:
75 try:
76 # try reading from visual context
76 # try reading from visual context
77 from pylons import tmpl_context
77 from pylons import tmpl_context
78 global_renderer = tmpl_context.visual.default_renderer
78 global_renderer = tmpl_context.visual.default_renderer
79 except AttributeError:
79 except AttributeError:
80 log.debug("Renderer not set, falling back "
80 log.debug("Renderer not set, falling back "
81 "to default renderer '%s'", global_renderer)
81 "to default renderer '%s'", global_renderer)
82 except Exception:
82 except Exception:
83 log.error(traceback.format_exc())
83 log.error(traceback.format_exc())
84 return global_renderer
84 return global_renderer
85
85
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
87 # group by versions, and count until, and display objects
87 # group by versions, and count until, and display objects
88
88
89 comment_groups = collections.defaultdict(list)
89 comment_groups = collections.defaultdict(list)
90 [comment_groups[
90 [comment_groups[
91 _co.pull_request_version_id].append(_co) for _co in comments]
91 _co.pull_request_version_id].append(_co) for _co in comments]
92
92
93 def yield_comments(pos):
93 def yield_comments(pos):
94 for co in comment_groups[pos]:
94 for co in comment_groups[pos]:
95 yield co
95 yield co
96
96
97 comment_versions = collections.defaultdict(
97 comment_versions = collections.defaultdict(
98 lambda: collections.defaultdict(list))
98 lambda: collections.defaultdict(list))
99 prev_prvid = -1
99 prev_prvid = -1
100 # fake last entry with None, to aggregate on "latest" version which
100 # fake last entry with None, to aggregate on "latest" version which
101 # doesn't have an pull_request_version_id
101 # doesn't have an pull_request_version_id
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
103 prvid = ver.pull_request_version_id
103 prvid = ver.pull_request_version_id
104 if prev_prvid == -1:
104 if prev_prvid == -1:
105 prev_prvid = prvid
105 prev_prvid = prvid
106
106
107 for co in yield_comments(prvid):
107 for co in yield_comments(prvid):
108 comment_versions[prvid]['at'].append(co)
108 comment_versions[prvid]['at'].append(co)
109
109
110 # save until
110 # save until
111 current = comment_versions[prvid]['at']
111 current = comment_versions[prvid]['at']
112 prev_until = comment_versions[prev_prvid]['until']
112 prev_until = comment_versions[prev_prvid]['until']
113 cur_until = prev_until + current
113 cur_until = prev_until + current
114 comment_versions[prvid]['until'].extend(cur_until)
114 comment_versions[prvid]['until'].extend(cur_until)
115
115
116 # save outdated
116 # save outdated
117 if inline:
117 if inline:
118 outdated = [x for x in cur_until
118 outdated = [x for x in cur_until
119 if x.outdated_at_version(show_version)]
119 if x.outdated_at_version(show_version)]
120 else:
120 else:
121 outdated = [x for x in cur_until
121 outdated = [x for x in cur_until
122 if x.older_than_version(show_version)]
122 if x.older_than_version(show_version)]
123 display = [x for x in cur_until if x not in outdated]
123 display = [x for x in cur_until if x not in outdated]
124
124
125 comment_versions[prvid]['outdated'] = outdated
125 comment_versions[prvid]['outdated'] = outdated
126 comment_versions[prvid]['display'] = display
126 comment_versions[prvid]['display'] = display
127
127
128 prev_prvid = prvid
128 prev_prvid = prvid
129
129
130 return comment_versions
130 return comment_versions
131
131
132 def get_unresolved_todos(self, pull_request, show_outdated=True):
132 def get_unresolved_todos(self, pull_request, show_outdated=True):
133
133
134 todos = Session().query(ChangesetComment) \
134 todos = Session().query(ChangesetComment) \
135 .filter(ChangesetComment.pull_request == pull_request) \
135 .filter(ChangesetComment.pull_request == pull_request) \
136 .filter(ChangesetComment.resolved_by == None) \
136 .filter(ChangesetComment.resolved_by == None) \
137 .filter(ChangesetComment.comment_type
137 .filter(ChangesetComment.comment_type
138 == ChangesetComment.COMMENT_TYPE_TODO)
138 == ChangesetComment.COMMENT_TYPE_TODO)
139
139
140 if not show_outdated:
140 if not show_outdated:
141 todos = todos.filter(
141 todos = todos.filter(
142 coalesce(ChangesetComment.display_state, '') !=
142 coalesce(ChangesetComment.display_state, '') !=
143 ChangesetComment.COMMENT_OUTDATED)
143 ChangesetComment.COMMENT_OUTDATED)
144
144
145 todos = todos.all()
145 todos = todos.all()
146
146
147 return todos
147 return todos
148
148
149 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
149 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
150
150
151 todos = Session().query(ChangesetComment) \
151 todos = Session().query(ChangesetComment) \
152 .filter(ChangesetComment.revision == commit_id) \
152 .filter(ChangesetComment.revision == commit_id) \
153 .filter(ChangesetComment.resolved_by == None) \
153 .filter(ChangesetComment.resolved_by == None) \
154 .filter(ChangesetComment.comment_type
154 .filter(ChangesetComment.comment_type
155 == ChangesetComment.COMMENT_TYPE_TODO)
155 == ChangesetComment.COMMENT_TYPE_TODO)
156
156
157 if not show_outdated:
157 if not show_outdated:
158 todos = todos.filter(
158 todos = todos.filter(
159 coalesce(ChangesetComment.display_state, '') !=
159 coalesce(ChangesetComment.display_state, '') !=
160 ChangesetComment.COMMENT_OUTDATED)
160 ChangesetComment.COMMENT_OUTDATED)
161
161
162 todos = todos.all()
162 todos = todos.all()
163
163
164 return todos
164 return todos
165
165
166 def _log_audit_action(self, action, action_data, user, comment):
166 def _log_audit_action(self, action, action_data, user, comment):
167 audit_logger.store(
167 audit_logger.store(
168 action=action,
168 action=action,
169 action_data=action_data,
169 action_data=action_data,
170 user=user,
170 user=user,
171 repo=comment.repo)
171 repo=comment.repo)
172
172
173 def create(self, text, repo, user, commit_id=None, pull_request=None,
173 def create(self, text, repo, user, commit_id=None, pull_request=None,
174 f_path=None, line_no=None, status_change=None,
174 f_path=None, line_no=None, status_change=None,
175 status_change_type=None, comment_type=None,
175 status_change_type=None, comment_type=None,
176 resolves_comment_id=None, closing_pr=False, send_email=True,
176 resolves_comment_id=None, closing_pr=False, send_email=True,
177 renderer=None):
177 renderer=None):
178 """
178 """
179 Creates new comment for commit or pull request.
179 Creates new comment for commit or pull request.
180 IF status_change is not none this comment is associated with a
180 IF status_change is not none this comment is associated with a
181 status change of commit or commit associated with pull request
181 status change of commit or commit associated with pull request
182
182
183 :param text:
183 :param text:
184 :param repo:
184 :param repo:
185 :param user:
185 :param user:
186 :param commit_id:
186 :param commit_id:
187 :param pull_request:
187 :param pull_request:
188 :param f_path:
188 :param f_path:
189 :param line_no:
189 :param line_no:
190 :param status_change: Label for status change
190 :param status_change: Label for status change
191 :param comment_type: Type of comment
191 :param comment_type: Type of comment
192 :param status_change_type: type of status change
192 :param status_change_type: type of status change
193 :param closing_pr:
193 :param closing_pr:
194 :param send_email:
194 :param send_email:
195 :param renderer: pick renderer for this comment
195 :param renderer: pick renderer for this comment
196 """
196 """
197 if not text:
197 if not text:
198 log.warning('Missing text for comment, skipping...')
198 log.warning('Missing text for comment, skipping...')
199 return
199 return
200
200
201 if not renderer:
201 if not renderer:
202 renderer = self._get_renderer()
202 renderer = self._get_renderer()
203
203
204 repo = self._get_repo(repo)
204 repo = self._get_repo(repo)
205 user = self._get_user(user)
205 user = self._get_user(user)
206
206
207 schema = comment_schema.CommentSchema()
207 schema = comment_schema.CommentSchema()
208 validated_kwargs = schema.deserialize(dict(
208 validated_kwargs = schema.deserialize(dict(
209 comment_body=text,
209 comment_body=text,
210 comment_type=comment_type,
210 comment_type=comment_type,
211 comment_file=f_path,
211 comment_file=f_path,
212 comment_line=line_no,
212 comment_line=line_no,
213 renderer_type=renderer,
213 renderer_type=renderer,
214 status_change=status_change_type,
214 status_change=status_change_type,
215 resolves_comment_id=resolves_comment_id,
215 resolves_comment_id=resolves_comment_id,
216 repo=repo.repo_id,
216 repo=repo.repo_id,
217 user=user.user_id,
217 user=user.user_id,
218 ))
218 ))
219
219
220 comment = ChangesetComment()
220 comment = ChangesetComment()
221 comment.renderer = validated_kwargs['renderer_type']
221 comment.renderer = validated_kwargs['renderer_type']
222 comment.text = validated_kwargs['comment_body']
222 comment.text = validated_kwargs['comment_body']
223 comment.f_path = validated_kwargs['comment_file']
223 comment.f_path = validated_kwargs['comment_file']
224 comment.line_no = validated_kwargs['comment_line']
224 comment.line_no = validated_kwargs['comment_line']
225 comment.comment_type = validated_kwargs['comment_type']
225 comment.comment_type = validated_kwargs['comment_type']
226
226
227 comment.repo = repo
227 comment.repo = repo
228 comment.author = user
228 comment.author = user
229 comment.resolved_comment = self.__get_commit_comment(
229 comment.resolved_comment = self.__get_commit_comment(
230 validated_kwargs['resolves_comment_id'])
230 validated_kwargs['resolves_comment_id'])
231
231
232 pull_request_id = pull_request
232 pull_request_id = pull_request
233
233
234 commit_obj = None
234 commit_obj = None
235 pull_request_obj = None
235 pull_request_obj = None
236
236
237 if commit_id:
237 if commit_id:
238 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
238 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
239 # do a lookup, so we don't pass something bad here
239 # do a lookup, so we don't pass something bad here
240 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
240 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
241 comment.revision = commit_obj.raw_id
241 comment.revision = commit_obj.raw_id
242
242
243 elif pull_request_id:
243 elif pull_request_id:
244 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
244 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
245 pull_request_obj = self.__get_pull_request(pull_request_id)
245 pull_request_obj = self.__get_pull_request(pull_request_id)
246 comment.pull_request = pull_request_obj
246 comment.pull_request = pull_request_obj
247 else:
247 else:
248 raise Exception('Please specify commit or pull_request_id')
248 raise Exception('Please specify commit or pull_request_id')
249
249
250 Session().add(comment)
250 Session().add(comment)
251 Session().flush()
251 Session().flush()
252 kwargs = {
252 kwargs = {
253 'user': user,
253 'user': user,
254 'renderer_type': renderer,
254 'renderer_type': renderer,
255 'repo_name': repo.repo_name,
255 'repo_name': repo.repo_name,
256 'status_change': status_change,
256 'status_change': status_change,
257 'status_change_type': status_change_type,
257 'status_change_type': status_change_type,
258 'comment_body': text,
258 'comment_body': text,
259 'comment_file': f_path,
259 'comment_file': f_path,
260 'comment_line': line_no,
260 'comment_line': line_no,
261 'comment_type': comment_type or 'note'
261 'comment_type': comment_type or 'note'
262 }
262 }
263
263
264 if commit_obj:
264 if commit_obj:
265 recipients = ChangesetComment.get_users(
265 recipients = ChangesetComment.get_users(
266 revision=commit_obj.raw_id)
266 revision=commit_obj.raw_id)
267 # add commit author if it's in RhodeCode system
267 # add commit author if it's in RhodeCode system
268 cs_author = User.get_from_cs_author(commit_obj.author)
268 cs_author = User.get_from_cs_author(commit_obj.author)
269 if not cs_author:
269 if not cs_author:
270 # use repo owner if we cannot extract the author correctly
270 # use repo owner if we cannot extract the author correctly
271 cs_author = repo.user
271 cs_author = repo.user
272 recipients += [cs_author]
272 recipients += [cs_author]
273
273
274 commit_comment_url = self.get_url(comment)
274 commit_comment_url = self.get_url(comment)
275
275
276 target_repo_url = h.link_to(
276 target_repo_url = h.link_to(
277 repo.repo_name,
277 repo.repo_name,
278 h.route_url('repo_summary', repo_name=repo.repo_name))
278 h.route_url('repo_summary', repo_name=repo.repo_name))
279
279
280 # commit specifics
280 # commit specifics
281 kwargs.update({
281 kwargs.update({
282 'commit': commit_obj,
282 'commit': commit_obj,
283 'commit_message': commit_obj.message,
283 'commit_message': commit_obj.message,
284 'commit_target_repo': target_repo_url,
284 'commit_target_repo': target_repo_url,
285 'commit_comment_url': commit_comment_url,
285 'commit_comment_url': commit_comment_url,
286 })
286 })
287
287
288 elif pull_request_obj:
288 elif pull_request_obj:
289 # get the current participants of this pull request
289 # get the current participants of this pull request
290 recipients = ChangesetComment.get_users(
290 recipients = ChangesetComment.get_users(
291 pull_request_id=pull_request_obj.pull_request_id)
291 pull_request_id=pull_request_obj.pull_request_id)
292 # add pull request author
292 # add pull request author
293 recipients += [pull_request_obj.author]
293 recipients += [pull_request_obj.author]
294
294
295 # add the reviewers to notification
295 # add the reviewers to notification
296 recipients += [x.user for x in pull_request_obj.reviewers]
296 recipients += [x.user for x in pull_request_obj.reviewers]
297
297
298 pr_target_repo = pull_request_obj.target_repo
298 pr_target_repo = pull_request_obj.target_repo
299 pr_source_repo = pull_request_obj.source_repo
299 pr_source_repo = pull_request_obj.source_repo
300
300
301 pr_comment_url = h.url(
301 pr_comment_url = h.url(
302 'pullrequest_show',
302 'pullrequest_show',
303 repo_name=pr_target_repo.repo_name,
303 repo_name=pr_target_repo.repo_name,
304 pull_request_id=pull_request_obj.pull_request_id,
304 pull_request_id=pull_request_obj.pull_request_id,
305 anchor='comment-%s' % comment.comment_id,
305 anchor='comment-%s' % comment.comment_id,
306 qualified=True,)
306 qualified=True,)
307
307
308 # set some variables for email notification
308 # set some variables for email notification
309 pr_target_repo_url = h.route_url(
309 pr_target_repo_url = h.route_url(
310 'repo_summary', repo_name=pr_target_repo.repo_name)
310 'repo_summary', repo_name=pr_target_repo.repo_name)
311
311
312 pr_source_repo_url = h.route_url(
312 pr_source_repo_url = h.route_url(
313 'repo_summary', repo_name=pr_source_repo.repo_name)
313 'repo_summary', repo_name=pr_source_repo.repo_name)
314
314
315 # pull request specifics
315 # pull request specifics
316 kwargs.update({
316 kwargs.update({
317 'pull_request': pull_request_obj,
317 'pull_request': pull_request_obj,
318 'pr_id': pull_request_obj.pull_request_id,
318 'pr_id': pull_request_obj.pull_request_id,
319 'pr_target_repo': pr_target_repo,
319 'pr_target_repo': pr_target_repo,
320 'pr_target_repo_url': pr_target_repo_url,
320 'pr_target_repo_url': pr_target_repo_url,
321 'pr_source_repo': pr_source_repo,
321 'pr_source_repo': pr_source_repo,
322 'pr_source_repo_url': pr_source_repo_url,
322 'pr_source_repo_url': pr_source_repo_url,
323 'pr_comment_url': pr_comment_url,
323 'pr_comment_url': pr_comment_url,
324 'pr_closing': closing_pr,
324 'pr_closing': closing_pr,
325 })
325 })
326 if send_email:
326 if send_email:
327 # pre-generate the subject for notification itself
327 # pre-generate the subject for notification itself
328 (subject,
328 (subject,
329 _h, _e, # we don't care about those
329 _h, _e, # we don't care about those
330 body_plaintext) = EmailNotificationModel().render_email(
330 body_plaintext) = EmailNotificationModel().render_email(
331 notification_type, **kwargs)
331 notification_type, **kwargs)
332
332
333 mention_recipients = set(
333 mention_recipients = set(
334 self._extract_mentions(text)).difference(recipients)
334 self._extract_mentions(text)).difference(recipients)
335
335
336 # create notification objects, and emails
336 # create notification objects, and emails
337 NotificationModel().create(
337 NotificationModel().create(
338 created_by=user,
338 created_by=user,
339 notification_subject=subject,
339 notification_subject=subject,
340 notification_body=body_plaintext,
340 notification_body=body_plaintext,
341 notification_type=notification_type,
341 notification_type=notification_type,
342 recipients=recipients,
342 recipients=recipients,
343 mention_recipients=mention_recipients,
343 mention_recipients=mention_recipients,
344 email_kwargs=kwargs,
344 email_kwargs=kwargs,
345 )
345 )
346
346
347 Session().flush()
347 Session().flush()
348 if comment.pull_request:
348 if comment.pull_request:
349 action = 'repo.pull_request.comment.create'
349 action = 'repo.pull_request.comment.create'
350 else:
350 else:
351 action = 'repo.commit.comment.create'
351 action = 'repo.commit.comment.create'
352
352
353 comment_data = comment.get_api_data()
353 comment_data = comment.get_api_data()
354 self._log_audit_action(
354 self._log_audit_action(
355 action, {'data': comment_data}, user, comment)
355 action, {'data': comment_data}, user, comment)
356
356
357 registry = get_current_registry()
358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
359 channelstream_config = rhodecode_plugins.get('channelstream', {})
360 msg_url = ''
357 msg_url = ''
358 channel = None
361 if commit_obj:
359 if commit_obj:
362 msg_url = commit_comment_url
360 msg_url = commit_comment_url
363 repo_name = repo.repo_name
361 repo_name = repo.repo_name
362 channel = u'/repo${}$/commit/{}'.format(
363 repo_name,
364 commit_obj.raw_id
365 )
364 elif pull_request_obj:
366 elif pull_request_obj:
365 msg_url = pr_comment_url
367 msg_url = pr_comment_url
366 repo_name = pr_target_repo.repo_name
368 repo_name = pr_target_repo.repo_name
367
369 channel = u'/repo${}$/pr/{}'.format(
368 if channelstream_config.get('enabled'):
369 message = '<strong>{}</strong> {} - ' \
370 '<a onclick="window.location=\'{}\';' \
371 'window.location.reload()">' \
372 '<strong>{}</strong></a>'
373 message = message.format(
374 user.username, _('made a comment'), msg_url,
375 _('Show it now'))
376 channel = '/repo${}$/pr/{}'.format(
377 repo_name,
370 repo_name,
378 pull_request_id
371 pull_request_id
379 )
372 )
380 payload = {
373
381 'type': 'message',
374 message = '<strong>{}</strong> {} - ' \
382 'timestamp': datetime.utcnow(),
375 '<a onclick="window.location=\'{}\';' \
383 'user': 'system',
376 'window.location.reload()">' \
384 'exclude_users': [user.username],
377 '<strong>{}</strong></a>'
385 'channel': channel,
378 message = message.format(
386 'message': {
379 user.username, _('made a comment'), msg_url,
387 'message': message,
380 _('Show it now'))
388 'level': 'info',
381
389 'topic': '/notifications'
382 channelstream.post_message(
390 }
383 channel, message, user.username,
391 }
384 registry=get_current_registry())
392 channelstream_request(channelstream_config, [payload],
393 '/message', raise_exc=False)
394
385
395 return comment
386 return comment
396
387
397 def delete(self, comment, user):
388 def delete(self, comment, user):
398 """
389 """
399 Deletes given comment
390 Deletes given comment
400 """
391 """
401 comment = self.__get_commit_comment(comment)
392 comment = self.__get_commit_comment(comment)
402 old_data = comment.get_api_data()
393 old_data = comment.get_api_data()
403 Session().delete(comment)
394 Session().delete(comment)
404
395
405 if comment.pull_request:
396 if comment.pull_request:
406 action = 'repo.pull_request.comment.delete'
397 action = 'repo.pull_request.comment.delete'
407 else:
398 else:
408 action = 'repo.commit.comment.delete'
399 action = 'repo.commit.comment.delete'
409
400
410 self._log_audit_action(
401 self._log_audit_action(
411 action, {'old_data': old_data}, user, comment)
402 action, {'old_data': old_data}, user, comment)
412
403
413 return comment
404 return comment
414
405
415 def get_all_comments(self, repo_id, revision=None, pull_request=None):
406 def get_all_comments(self, repo_id, revision=None, pull_request=None):
416 q = ChangesetComment.query()\
407 q = ChangesetComment.query()\
417 .filter(ChangesetComment.repo_id == repo_id)
408 .filter(ChangesetComment.repo_id == repo_id)
418 if revision:
409 if revision:
419 q = q.filter(ChangesetComment.revision == revision)
410 q = q.filter(ChangesetComment.revision == revision)
420 elif pull_request:
411 elif pull_request:
421 pull_request = self.__get_pull_request(pull_request)
412 pull_request = self.__get_pull_request(pull_request)
422 q = q.filter(ChangesetComment.pull_request == pull_request)
413 q = q.filter(ChangesetComment.pull_request == pull_request)
423 else:
414 else:
424 raise Exception('Please specify commit or pull_request')
415 raise Exception('Please specify commit or pull_request')
425 q = q.order_by(ChangesetComment.created_on)
416 q = q.order_by(ChangesetComment.created_on)
426 return q.all()
417 return q.all()
427
418
428 def get_url(self, comment, request=None, permalink=False):
419 def get_url(self, comment, request=None, permalink=False):
429 if not request:
420 if not request:
430 request = get_current_request()
421 request = get_current_request()
431
422
432 comment = self.__get_commit_comment(comment)
423 comment = self.__get_commit_comment(comment)
433 if comment.pull_request:
424 if comment.pull_request:
434 pull_request = comment.pull_request
425 pull_request = comment.pull_request
435 if permalink:
426 if permalink:
436 return request.route_url(
427 return request.route_url(
437 'pull_requests_global',
428 'pull_requests_global',
438 pull_request_id=pull_request.pull_request_id,
429 pull_request_id=pull_request.pull_request_id,
439 _anchor='comment-%s' % comment.comment_id)
430 _anchor='comment-%s' % comment.comment_id)
440 else:
431 else:
441 return request.route_url('pullrequest_show',
432 return request.route_url('pullrequest_show',
442 repo_name=safe_str(pull_request.target_repo.repo_name),
433 repo_name=safe_str(pull_request.target_repo.repo_name),
443 pull_request_id=pull_request.pull_request_id,
434 pull_request_id=pull_request.pull_request_id,
444 _anchor='comment-%s' % comment.comment_id)
435 _anchor='comment-%s' % comment.comment_id)
445
436
446 else:
437 else:
447 repo = comment.repo
438 repo = comment.repo
448 commit_id = comment.revision
439 commit_id = comment.revision
449
440
450 if permalink:
441 if permalink:
451 return request.route_url(
442 return request.route_url(
452 'repo_commit', repo_name=safe_str(repo.repo_id),
443 'repo_commit', repo_name=safe_str(repo.repo_id),
453 commit_id=commit_id,
444 commit_id=commit_id,
454 _anchor='comment-%s' % comment.comment_id)
445 _anchor='comment-%s' % comment.comment_id)
455
446
456 else:
447 else:
457 return request.route_url(
448 return request.route_url(
458 'repo_commit', repo_name=safe_str(repo.repo_name),
449 'repo_commit', repo_name=safe_str(repo.repo_name),
459 commit_id=commit_id,
450 commit_id=commit_id,
460 _anchor='comment-%s' % comment.comment_id)
451 _anchor='comment-%s' % comment.comment_id)
461
452
462 def get_comments(self, repo_id, revision=None, pull_request=None):
453 def get_comments(self, repo_id, revision=None, pull_request=None):
463 """
454 """
464 Gets main comments based on revision or pull_request_id
455 Gets main comments based on revision or pull_request_id
465
456
466 :param repo_id:
457 :param repo_id:
467 :param revision:
458 :param revision:
468 :param pull_request:
459 :param pull_request:
469 """
460 """
470
461
471 q = ChangesetComment.query()\
462 q = ChangesetComment.query()\
472 .filter(ChangesetComment.repo_id == repo_id)\
463 .filter(ChangesetComment.repo_id == repo_id)\
473 .filter(ChangesetComment.line_no == None)\
464 .filter(ChangesetComment.line_no == None)\
474 .filter(ChangesetComment.f_path == None)
465 .filter(ChangesetComment.f_path == None)
475 if revision:
466 if revision:
476 q = q.filter(ChangesetComment.revision == revision)
467 q = q.filter(ChangesetComment.revision == revision)
477 elif pull_request:
468 elif pull_request:
478 pull_request = self.__get_pull_request(pull_request)
469 pull_request = self.__get_pull_request(pull_request)
479 q = q.filter(ChangesetComment.pull_request == pull_request)
470 q = q.filter(ChangesetComment.pull_request == pull_request)
480 else:
471 else:
481 raise Exception('Please specify commit or pull_request')
472 raise Exception('Please specify commit or pull_request')
482 q = q.order_by(ChangesetComment.created_on)
473 q = q.order_by(ChangesetComment.created_on)
483 return q.all()
474 return q.all()
484
475
485 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
476 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
486 q = self._get_inline_comments_query(repo_id, revision, pull_request)
477 q = self._get_inline_comments_query(repo_id, revision, pull_request)
487 return self._group_comments_by_path_and_line_number(q)
478 return self._group_comments_by_path_and_line_number(q)
488
479
489 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
480 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
490 version=None):
481 version=None):
491 inline_cnt = 0
482 inline_cnt = 0
492 for fname, per_line_comments in inline_comments.iteritems():
483 for fname, per_line_comments in inline_comments.iteritems():
493 for lno, comments in per_line_comments.iteritems():
484 for lno, comments in per_line_comments.iteritems():
494 for comm in comments:
485 for comm in comments:
495 if not comm.outdated_at_version(version) and skip_outdated:
486 if not comm.outdated_at_version(version) and skip_outdated:
496 inline_cnt += 1
487 inline_cnt += 1
497
488
498 return inline_cnt
489 return inline_cnt
499
490
500 def get_outdated_comments(self, repo_id, pull_request):
491 def get_outdated_comments(self, repo_id, pull_request):
501 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
492 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
502 # of a pull request.
493 # of a pull request.
503 q = self._all_inline_comments_of_pull_request(pull_request)
494 q = self._all_inline_comments_of_pull_request(pull_request)
504 q = q.filter(
495 q = q.filter(
505 ChangesetComment.display_state ==
496 ChangesetComment.display_state ==
506 ChangesetComment.COMMENT_OUTDATED
497 ChangesetComment.COMMENT_OUTDATED
507 ).order_by(ChangesetComment.comment_id.asc())
498 ).order_by(ChangesetComment.comment_id.asc())
508
499
509 return self._group_comments_by_path_and_line_number(q)
500 return self._group_comments_by_path_and_line_number(q)
510
501
511 def _get_inline_comments_query(self, repo_id, revision, pull_request):
502 def _get_inline_comments_query(self, repo_id, revision, pull_request):
512 # TODO: johbo: Split this into two methods: One for PR and one for
503 # TODO: johbo: Split this into two methods: One for PR and one for
513 # commit.
504 # commit.
514 if revision:
505 if revision:
515 q = Session().query(ChangesetComment).filter(
506 q = Session().query(ChangesetComment).filter(
516 ChangesetComment.repo_id == repo_id,
507 ChangesetComment.repo_id == repo_id,
517 ChangesetComment.line_no != null(),
508 ChangesetComment.line_no != null(),
518 ChangesetComment.f_path != null(),
509 ChangesetComment.f_path != null(),
519 ChangesetComment.revision == revision)
510 ChangesetComment.revision == revision)
520
511
521 elif pull_request:
512 elif pull_request:
522 pull_request = self.__get_pull_request(pull_request)
513 pull_request = self.__get_pull_request(pull_request)
523 if not CommentsModel.use_outdated_comments(pull_request):
514 if not CommentsModel.use_outdated_comments(pull_request):
524 q = self._visible_inline_comments_of_pull_request(pull_request)
515 q = self._visible_inline_comments_of_pull_request(pull_request)
525 else:
516 else:
526 q = self._all_inline_comments_of_pull_request(pull_request)
517 q = self._all_inline_comments_of_pull_request(pull_request)
527
518
528 else:
519 else:
529 raise Exception('Please specify commit or pull_request_id')
520 raise Exception('Please specify commit or pull_request_id')
530 q = q.order_by(ChangesetComment.comment_id.asc())
521 q = q.order_by(ChangesetComment.comment_id.asc())
531 return q
522 return q
532
523
533 def _group_comments_by_path_and_line_number(self, q):
524 def _group_comments_by_path_and_line_number(self, q):
534 comments = q.all()
525 comments = q.all()
535 paths = collections.defaultdict(lambda: collections.defaultdict(list))
526 paths = collections.defaultdict(lambda: collections.defaultdict(list))
536 for co in comments:
527 for co in comments:
537 paths[co.f_path][co.line_no].append(co)
528 paths[co.f_path][co.line_no].append(co)
538 return paths
529 return paths
539
530
540 @classmethod
531 @classmethod
541 def needed_extra_diff_context(cls):
532 def needed_extra_diff_context(cls):
542 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
533 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
543
534
544 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
535 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
545 if not CommentsModel.use_outdated_comments(pull_request):
536 if not CommentsModel.use_outdated_comments(pull_request):
546 return
537 return
547
538
548 comments = self._visible_inline_comments_of_pull_request(pull_request)
539 comments = self._visible_inline_comments_of_pull_request(pull_request)
549 comments_to_outdate = comments.all()
540 comments_to_outdate = comments.all()
550
541
551 for comment in comments_to_outdate:
542 for comment in comments_to_outdate:
552 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
543 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
553
544
554 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
545 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
555 diff_line = _parse_comment_line_number(comment.line_no)
546 diff_line = _parse_comment_line_number(comment.line_no)
556
547
557 try:
548 try:
558 old_context = old_diff_proc.get_context_of_line(
549 old_context = old_diff_proc.get_context_of_line(
559 path=comment.f_path, diff_line=diff_line)
550 path=comment.f_path, diff_line=diff_line)
560 new_context = new_diff_proc.get_context_of_line(
551 new_context = new_diff_proc.get_context_of_line(
561 path=comment.f_path, diff_line=diff_line)
552 path=comment.f_path, diff_line=diff_line)
562 except (diffs.LineNotInDiffException,
553 except (diffs.LineNotInDiffException,
563 diffs.FileNotInDiffException):
554 diffs.FileNotInDiffException):
564 comment.display_state = ChangesetComment.COMMENT_OUTDATED
555 comment.display_state = ChangesetComment.COMMENT_OUTDATED
565 return
556 return
566
557
567 if old_context == new_context:
558 if old_context == new_context:
568 return
559 return
569
560
570 if self._should_relocate_diff_line(diff_line):
561 if self._should_relocate_diff_line(diff_line):
571 new_diff_lines = new_diff_proc.find_context(
562 new_diff_lines = new_diff_proc.find_context(
572 path=comment.f_path, context=old_context,
563 path=comment.f_path, context=old_context,
573 offset=self.DIFF_CONTEXT_BEFORE)
564 offset=self.DIFF_CONTEXT_BEFORE)
574 if not new_diff_lines:
565 if not new_diff_lines:
575 comment.display_state = ChangesetComment.COMMENT_OUTDATED
566 comment.display_state = ChangesetComment.COMMENT_OUTDATED
576 else:
567 else:
577 new_diff_line = self._choose_closest_diff_line(
568 new_diff_line = self._choose_closest_diff_line(
578 diff_line, new_diff_lines)
569 diff_line, new_diff_lines)
579 comment.line_no = _diff_to_comment_line_number(new_diff_line)
570 comment.line_no = _diff_to_comment_line_number(new_diff_line)
580 else:
571 else:
581 comment.display_state = ChangesetComment.COMMENT_OUTDATED
572 comment.display_state = ChangesetComment.COMMENT_OUTDATED
582
573
583 def _should_relocate_diff_line(self, diff_line):
574 def _should_relocate_diff_line(self, diff_line):
584 """
575 """
585 Checks if relocation shall be tried for the given `diff_line`.
576 Checks if relocation shall be tried for the given `diff_line`.
586
577
587 If a comment points into the first lines, then we can have a situation
578 If a comment points into the first lines, then we can have a situation
588 that after an update another line has been added on top. In this case
579 that after an update another line has been added on top. In this case
589 we would find the context still and move the comment around. This
580 we would find the context still and move the comment around. This
590 would be wrong.
581 would be wrong.
591 """
582 """
592 should_relocate = (
583 should_relocate = (
593 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
584 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
594 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
585 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
595 return should_relocate
586 return should_relocate
596
587
597 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
588 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
598 candidate = new_diff_lines[0]
589 candidate = new_diff_lines[0]
599 best_delta = _diff_line_delta(diff_line, candidate)
590 best_delta = _diff_line_delta(diff_line, candidate)
600 for new_diff_line in new_diff_lines[1:]:
591 for new_diff_line in new_diff_lines[1:]:
601 delta = _diff_line_delta(diff_line, new_diff_line)
592 delta = _diff_line_delta(diff_line, new_diff_line)
602 if delta < best_delta:
593 if delta < best_delta:
603 candidate = new_diff_line
594 candidate = new_diff_line
604 best_delta = delta
595 best_delta = delta
605 return candidate
596 return candidate
606
597
607 def _visible_inline_comments_of_pull_request(self, pull_request):
598 def _visible_inline_comments_of_pull_request(self, pull_request):
608 comments = self._all_inline_comments_of_pull_request(pull_request)
599 comments = self._all_inline_comments_of_pull_request(pull_request)
609 comments = comments.filter(
600 comments = comments.filter(
610 coalesce(ChangesetComment.display_state, '') !=
601 coalesce(ChangesetComment.display_state, '') !=
611 ChangesetComment.COMMENT_OUTDATED)
602 ChangesetComment.COMMENT_OUTDATED)
612 return comments
603 return comments
613
604
614 def _all_inline_comments_of_pull_request(self, pull_request):
605 def _all_inline_comments_of_pull_request(self, pull_request):
615 comments = Session().query(ChangesetComment)\
606 comments = Session().query(ChangesetComment)\
616 .filter(ChangesetComment.line_no != None)\
607 .filter(ChangesetComment.line_no != None)\
617 .filter(ChangesetComment.f_path != None)\
608 .filter(ChangesetComment.f_path != None)\
618 .filter(ChangesetComment.pull_request == pull_request)
609 .filter(ChangesetComment.pull_request == pull_request)
619 return comments
610 return comments
620
611
621 def _all_general_comments_of_pull_request(self, pull_request):
612 def _all_general_comments_of_pull_request(self, pull_request):
622 comments = Session().query(ChangesetComment)\
613 comments = Session().query(ChangesetComment)\
623 .filter(ChangesetComment.line_no == None)\
614 .filter(ChangesetComment.line_no == None)\
624 .filter(ChangesetComment.f_path == None)\
615 .filter(ChangesetComment.f_path == None)\
625 .filter(ChangesetComment.pull_request == pull_request)
616 .filter(ChangesetComment.pull_request == pull_request)
626 return comments
617 return comments
627
618
628 @staticmethod
619 @staticmethod
629 def use_outdated_comments(pull_request):
620 def use_outdated_comments(pull_request):
630 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
621 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
631 settings = settings_model.get_general_settings()
622 settings = settings_model.get_general_settings()
632 return settings.get('rhodecode_use_outdated_comments', False)
623 return settings.get('rhodecode_use_outdated_comments', False)
633
624
634
625
635 def _parse_comment_line_number(line_no):
626 def _parse_comment_line_number(line_no):
636 """
627 """
637 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
628 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
638 """
629 """
639 old_line = None
630 old_line = None
640 new_line = None
631 new_line = None
641 if line_no.startswith('o'):
632 if line_no.startswith('o'):
642 old_line = int(line_no[1:])
633 old_line = int(line_no[1:])
643 elif line_no.startswith('n'):
634 elif line_no.startswith('n'):
644 new_line = int(line_no[1:])
635 new_line = int(line_no[1:])
645 else:
636 else:
646 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
637 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
647 return diffs.DiffLineNumber(old_line, new_line)
638 return diffs.DiffLineNumber(old_line, new_line)
648
639
649
640
650 def _diff_to_comment_line_number(diff_line):
641 def _diff_to_comment_line_number(diff_line):
651 if diff_line.new is not None:
642 if diff_line.new is not None:
652 return u'n{}'.format(diff_line.new)
643 return u'n{}'.format(diff_line.new)
653 elif diff_line.old is not None:
644 elif diff_line.old is not None:
654 return u'o{}'.format(diff_line.old)
645 return u'o{}'.format(diff_line.old)
655 return u''
646 return u''
656
647
657
648
658 def _diff_line_delta(a, b):
649 def _diff_line_delta(a, b):
659 if None not in (a.new, b.new):
650 if None not in (a.new, b.new):
660 return abs(a.new - b.new)
651 return abs(a.new - b.new)
661 elif None not in (a.old, b.old):
652 elif None not in (a.old, b.old):
662 return abs(a.old - b.old)
653 return abs(a.old - b.old)
663 else:
654 else:
664 raise ValueError(
655 raise ValueError(
665 "Cannot compute delta between {} and {}".format(a, b))
656 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,141 +1,150 b''
1 ccLog = Logger.get('RhodeCodeApp');
1 ccLog = Logger.get('RhodeCodeApp');
2 ccLog.setLevel(Logger.OFF);
2 ccLog.setLevel(Logger.OFF);
3
3
4 var rhodeCodeApp = Polymer({
4 var rhodeCodeApp = Polymer({
5 is: 'rhodecode-app',
5 is: 'rhodecode-app',
6 attached: function () {
6 attached: function () {
7 ccLog.debug('rhodeCodeApp created');
7 ccLog.debug('rhodeCodeApp created');
8 $.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
8 $.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
9 $.Topic('/favicon/update').subscribe(this.faviconUpdate.bind(this));
9 $.Topic('/favicon/update').subscribe(this.faviconUpdate.bind(this));
10 $.Topic('/connection_controller/subscribe').subscribe(
10 $.Topic('/connection_controller/subscribe').subscribe(
11 this.subscribeToChannelTopic.bind(this));
11 this.subscribeToChannelTopic.bind(this));
12 // this event can be used to coordinate plugins to do their
12 // this event can be used to coordinate plugins to do their
13 // initialization before channelstream is kicked off
13 // initialization before channelstream is kicked off
14 $.Topic('/__MAIN_APP__').publish({});
14 $.Topic('/__MAIN_APP__').publish({});
15
15
16 for (var i = 0; i < alertMessagePayloads.length; i++) {
16 for (var i = 0; i < alertMessagePayloads.length; i++) {
17 $.Topic('/notifications').publish(alertMessagePayloads[i]);
17 $.Topic('/notifications').publish(alertMessagePayloads[i]);
18 }
18 }
19 this.kickoffChannelstreamPlugin();
19 this.kickoffChannelstreamPlugin();
20 },
20 },
21
21
22 /** proxy to channelstream connection */
22 /** proxy to channelstream connection */
23 getChannelStreamConnection: function () {
23 getChannelStreamConnection: function () {
24 return this.$['channelstream-connection'];
24 return this.$['channelstream-connection'];
25 },
25 },
26
26
27 handleNotifications: function (data) {
27 handleNotifications: function (data) {
28 var elem = document.getElementById('notifications');
28 var elem = document.getElementById('notifications');
29 if(elem){
29 if(elem){
30 elem.handleNotification(data);
30 elem.handleNotification(data);
31 }
31 }
32
32
33 },
33 },
34
34
35 faviconUpdate: function (data) {
35 faviconUpdate: function (data) {
36 this.$$('rhodecode-favicon').counter = data.count;
36 this.$$('rhodecode-favicon').counter = data.count;
37 },
37 },
38
38
39 /** opens connection to ws server */
39 /** opens connection to ws server */
40 kickoffChannelstreamPlugin: function (data) {
40 kickoffChannelstreamPlugin: function (data) {
41 ccLog.debug('kickoffChannelstreamPlugin');
41 ccLog.debug('kickoffChannelstreamPlugin');
42 var channels = ['broadcast'];
42 var channels = ['broadcast'];
43 var addChannels = this.checkViewChannels();
43 var addChannels = this.checkViewChannels();
44 for (var i = 0; i < addChannels.length; i++) {
44 for (var i = 0; i < addChannels.length; i++) {
45 channels.push(addChannels[i]);
45 channels.push(addChannels[i]);
46 }
46 }
47 if (window.CHANNELSTREAM_SETTINGS && CHANNELSTREAM_SETTINGS.enabled){
47 if (window.CHANNELSTREAM_SETTINGS && CHANNELSTREAM_SETTINGS.enabled){
48 var channelstreamConnection = this.getChannelStreamConnection();
48 var channelstreamConnection = this.getChannelStreamConnection();
49 channelstreamConnection.connectUrl = CHANNELSTREAM_URLS.connect;
49 channelstreamConnection.connectUrl = CHANNELSTREAM_URLS.connect;
50 channelstreamConnection.subscribeUrl = CHANNELSTREAM_URLS.subscribe;
50 channelstreamConnection.subscribeUrl = CHANNELSTREAM_URLS.subscribe;
51 channelstreamConnection.websocketUrl = CHANNELSTREAM_URLS.ws + '/ws';
51 channelstreamConnection.websocketUrl = CHANNELSTREAM_URLS.ws + '/ws';
52 channelstreamConnection.longPollUrl = CHANNELSTREAM_URLS.longpoll + '/listen';
52 channelstreamConnection.longPollUrl = CHANNELSTREAM_URLS.longpoll + '/listen';
53 // some channels might already be registered by topic
53 // some channels might already be registered by topic
54 for (var i = 0; i < channels.length; i++) {
54 for (var i = 0; i < channels.length; i++) {
55 channelstreamConnection.push('channels', channels[i]);
55 channelstreamConnection.push('channels', channels[i]);
56 }
56 }
57 // append any additional channels registered in other plugins
57 // append any additional channels registered in other plugins
58 $.Topic('/connection_controller/subscribe').processPrepared();
58 $.Topic('/connection_controller/subscribe').processPrepared();
59 channelstreamConnection.connect();
59 channelstreamConnection.connect();
60 }
60 }
61 },
61 },
62
62
63 checkViewChannels: function () {
63 checkViewChannels: function () {
64 var channels = []
64 // subscribe to different channels data is sent.
65
66 var channels = [];
65 // subscribe to PR repo channel for PR's'
67 // subscribe to PR repo channel for PR's'
66 if (templateContext.pull_request_data.pull_request_id) {
68 if (templateContext.pull_request_data.pull_request_id) {
67 var channelName = '/repo$' + templateContext.repo_name + '$/pr/' +
69 var channelName = '/repo$' + templateContext.repo_name + '$/pr/' +
68 String(templateContext.pull_request_data.pull_request_id);
70 String(templateContext.pull_request_data.pull_request_id);
69 channels.push(channelName);
71 channels.push(channelName);
70 }
72 }
73
74 if (templateContext.commit_data.commit_id) {
75 var channelName = '/repo$' + templateContext.repo_name + '$/commit/' +
76 String(templateContext.commit_data.commit_id);
77 channels.push(channelName);
78 }
79
71 return channels;
80 return channels;
72 },
81 },
73
82
74 /** subscribes users from channels in channelstream */
83 /** subscribes users from channels in channelstream */
75 subscribeToChannelTopic: function (channels) {
84 subscribeToChannelTopic: function (channels) {
76 var channelstreamConnection = this.getChannelStreamConnection();
85 var channelstreamConnection = this.getChannelStreamConnection();
77 var toSubscribe = channelstreamConnection.calculateSubscribe(channels);
86 var toSubscribe = channelstreamConnection.calculateSubscribe(channels);
78 ccLog.debug('subscribeToChannelTopic', toSubscribe);
87 ccLog.debug('subscribeToChannelTopic', toSubscribe);
79 if (toSubscribe.length > 0) {
88 if (toSubscribe.length > 0) {
80 // if we are connected then subscribe
89 // if we are connected then subscribe
81 if (channelstreamConnection.connected) {
90 if (channelstreamConnection.connected) {
82 channelstreamConnection.subscribe(toSubscribe);
91 channelstreamConnection.subscribe(toSubscribe);
83 }
92 }
84 // not connected? just push channels onto the stack
93 // not connected? just push channels onto the stack
85 else {
94 else {
86 for (var i = 0; i < toSubscribe.length; i++) {
95 for (var i = 0; i < toSubscribe.length; i++) {
87 channelstreamConnection.push('channels', toSubscribe[i]);
96 channelstreamConnection.push('channels', toSubscribe[i]);
88 }
97 }
89 }
98 }
90 }
99 }
91 },
100 },
92
101
93 /** publish received messages into correct topic */
102 /** publish received messages into correct topic */
94 receivedMessage: function (event) {
103 receivedMessage: function (event) {
95 for (var i = 0; i < event.detail.length; i++) {
104 for (var i = 0; i < event.detail.length; i++) {
96 var message = event.detail[i];
105 var message = event.detail[i];
97 if (message.message.topic) {
106 if (message.message.topic) {
98 ccLog.debug('publishing', message.message.topic);
107 ccLog.debug('publishing', message.message.topic);
99 $.Topic(message.message.topic).publish(message);
108 $.Topic(message.message.topic).publish(message);
100 }
109 }
101 else if (message.type === 'presence'){
110 else if (message.type === 'presence'){
102 $.Topic('/connection_controller/presence').publish(message);
111 $.Topic('/connection_controller/presence').publish(message);
103 }
112 }
104 else {
113 else {
105 ccLog.warn('unhandled message', message);
114 ccLog.warn('unhandled message', message);
106 }
115 }
107 }
116 }
108 },
117 },
109
118
110 handleConnected: function (event) {
119 handleConnected: function (event) {
111 var channelstreamConnection = this.getChannelStreamConnection();
120 var channelstreamConnection = this.getChannelStreamConnection();
112 channelstreamConnection.set('channelsState',
121 channelstreamConnection.set('channelsState',
113 event.detail.channels_info);
122 event.detail.channels_info);
114 channelstreamConnection.set('userState', event.detail.state);
123 channelstreamConnection.set('userState', event.detail.state);
115 channelstreamConnection.set('channels', event.detail.channels);
124 channelstreamConnection.set('channels', event.detail.channels);
116 this.propagageChannelsState();
125 this.propagageChannelsState();
117 },
126 },
118 handleSubscribed: function (event) {
127 handleSubscribed: function (event) {
119 var channelstreamConnection = this.getChannelStreamConnection();
128 var channelstreamConnection = this.getChannelStreamConnection();
120 var channelInfo = event.detail.channels_info;
129 var channelInfo = event.detail.channels_info;
121 var channelKeys = Object.keys(event.detail.channels_info);
130 var channelKeys = Object.keys(event.detail.channels_info);
122 for (var i = 0; i < channelKeys.length; i++) {
131 for (var i = 0; i < channelKeys.length; i++) {
123 var key = channelKeys[i];
132 var key = channelKeys[i];
124 channelstreamConnection.set(['channelsState', key], channelInfo[key]);
133 channelstreamConnection.set(['channelsState', key], channelInfo[key]);
125 }
134 }
126 channelstreamConnection.set('channels', event.detail.channels);
135 channelstreamConnection.set('channels', event.detail.channels);
127 this.propagageChannelsState();
136 this.propagageChannelsState();
128 },
137 },
129 /** propagates channel states on topics */
138 /** propagates channel states on topics */
130 propagageChannelsState: function (event) {
139 propagageChannelsState: function (event) {
131 var channelstreamConnection = this.getChannelStreamConnection();
140 var channelstreamConnection = this.getChannelStreamConnection();
132 var channel_data = channelstreamConnection.channelsState;
141 var channel_data = channelstreamConnection.channelsState;
133 var channels = channelstreamConnection.channels;
142 var channels = channelstreamConnection.channels;
134 for (var i = 0; i < channels.length; i++) {
143 for (var i = 0; i < channels.length; i++) {
135 var key = channels[i];
144 var key = channels[i];
136 $.Topic('/connection_controller/channel_update').publish(
145 $.Topic('/connection_controller/channel_update').publish(
137 {channel: key, state: channel_data[key]}
146 {channel: key, state: channel_data[key]}
138 );
147 );
139 }
148 }
140 }
149 }
141 });
150 });
General Comments 0
You need to be logged in to leave comments. Login now