Show More
@@ -0,0 +1,79 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2010-2016 RhodeCode GmbH | |||
|
4 | # | |||
|
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 | |||
|
7 | # (only), as published by the Free Software Foundation. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
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/>. | |||
|
16 | # | |||
|
17 | # This program is dual-licensed. If you wish to learn more about the | |||
|
18 | # RhodeCode Enterprise Edition, including its added features, Support services, | |||
|
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |||
|
20 | ||||
|
21 | import os | |||
|
22 | ||||
|
23 | from pyramid.settings import asbool | |||
|
24 | ||||
|
25 | from rhodecode.config.routing import ADMIN_PREFIX | |||
|
26 | from rhodecode.lib.ext_json import json | |||
|
27 | ||||
|
28 | ||||
|
29 | def url_gen(request): | |||
|
30 | urls = { | |||
|
31 | 'connect': request.route_url('channelstream_connect'), | |||
|
32 | 'subscribe': request.route_url('channelstream_subscribe') | |||
|
33 | } | |||
|
34 | return json.dumps(urls) | |||
|
35 | ||||
|
36 | ||||
|
37 | PLUGIN_DEFINITION = { | |||
|
38 | 'name': 'channelstream', | |||
|
39 | 'config': { | |||
|
40 | 'javascript': [], | |||
|
41 | 'css': [], | |||
|
42 | 'template_hooks': { | |||
|
43 | 'plugin_init_template': 'rhodecode:templates/channelstream/plugin_init.html' | |||
|
44 | }, | |||
|
45 | 'url_gen': url_gen, | |||
|
46 | 'static': None, | |||
|
47 | 'enabled': False, | |||
|
48 | 'server': '', | |||
|
49 | 'secret': '' | |||
|
50 | } | |||
|
51 | } | |||
|
52 | ||||
|
53 | ||||
|
54 | def includeme(config): | |||
|
55 | settings = config.registry.settings | |||
|
56 | PLUGIN_DEFINITION['config']['enabled'] = asbool( | |||
|
57 | settings.get('channelstream.enabled')) | |||
|
58 | PLUGIN_DEFINITION['config']['server'] = settings.get( | |||
|
59 | 'channelstream.server', '') | |||
|
60 | PLUGIN_DEFINITION['config']['secret'] = settings.get( | |||
|
61 | 'channelstream.secret', '') | |||
|
62 | PLUGIN_DEFINITION['config']['history.location'] = settings.get( | |||
|
63 | 'channelstream.history.location', '') | |||
|
64 | config.register_rhodecode_plugin( | |||
|
65 | PLUGIN_DEFINITION['name'], | |||
|
66 | PLUGIN_DEFINITION['config'] | |||
|
67 | ) | |||
|
68 | # create plugin history location | |||
|
69 | history_dir = PLUGIN_DEFINITION['config']['history.location'] | |||
|
70 | if history_dir and not os.path.exists(history_dir): | |||
|
71 | os.makedirs(history_dir, 0750) | |||
|
72 | ||||
|
73 | config.add_route( | |||
|
74 | name='channelstream_connect', | |||
|
75 | pattern=ADMIN_PREFIX + '/channelstream/connect') | |||
|
76 | config.add_route( | |||
|
77 | name='channelstream_subscribe', | |||
|
78 | pattern=ADMIN_PREFIX + '/channelstream/subscribe') | |||
|
79 | config.scan('rhodecode.channelstream') |
@@ -0,0 +1,177 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2010-2016 RhodeCode GmbH | |||
|
4 | # | |||
|
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 | |||
|
7 | # (only), as published by the Free Software Foundation. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
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/>. | |||
|
16 | # | |||
|
17 | # This program is dual-licensed. If you wish to learn more about the | |||
|
18 | # RhodeCode Enterprise Edition, including its added features, Support services, | |||
|
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |||
|
20 | ||||
|
21 | """ | |||
|
22 | Channel Stream controller for rhodecode | |||
|
23 | ||||
|
24 | :created_on: Oct 10, 2015 | |||
|
25 | :author: marcinl | |||
|
26 | :copyright: (c) 2013-2015 RhodeCode GmbH. | |||
|
27 | :license: Commercial License, see LICENSE for more details. | |||
|
28 | """ | |||
|
29 | ||||
|
30 | import logging | |||
|
31 | import uuid | |||
|
32 | ||||
|
33 | from pylons import tmpl_context as c | |||
|
34 | from pyramid.settings import asbool | |||
|
35 | from pyramid.view import view_config | |||
|
36 | from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPBadGateway | |||
|
37 | ||||
|
38 | from rhodecode.lib.channelstream import ( | |||
|
39 | channelstream_request, | |||
|
40 | ChannelstreamConnectionException, | |||
|
41 | ChannelstreamPermissionException, | |||
|
42 | check_channel_permissions, | |||
|
43 | get_connection_validators, | |||
|
44 | get_user_data, | |||
|
45 | parse_channels_info, | |||
|
46 | update_history_from_logs, | |||
|
47 | STATE_PUBLIC_KEYS) | |||
|
48 | from rhodecode.lib.auth import NotAnonymous | |||
|
49 | from rhodecode.lib.utils2 import str2bool | |||
|
50 | ||||
|
51 | log = logging.getLogger(__name__) | |||
|
52 | ||||
|
53 | ||||
|
54 | class ChannelstreamView(object): | |||
|
55 | def __init__(self, context, request): | |||
|
56 | self.context = context | |||
|
57 | self.request = request | |||
|
58 | ||||
|
59 | # Some of the decorators rely on this attribute to be present | |||
|
60 | # on the class of the decorated method. | |||
|
61 | self._rhodecode_user = request.user | |||
|
62 | registry = request.registry | |||
|
63 | self.channelstream_config = registry.rhodecode_plugins['channelstream'] | |||
|
64 | if not self.channelstream_config.get('enabled'): | |||
|
65 | log.exception('Channelstream plugin is disabled') | |||
|
66 | raise HTTPBadRequest() | |||
|
67 | ||||
|
68 | @NotAnonymous() | |||
|
69 | @view_config(route_name='channelstream_connect', renderer='json') | |||
|
70 | def connect(self): | |||
|
71 | """ handle authorization of users trying to connect """ | |||
|
72 | try: | |||
|
73 | json_body = self.request.json_body | |||
|
74 | except Exception: | |||
|
75 | log.exception('Failed to decode json from request') | |||
|
76 | raise HTTPBadRequest() | |||
|
77 | try: | |||
|
78 | channels = check_channel_permissions( | |||
|
79 | json_body.get('channels'), | |||
|
80 | get_connection_validators(self.request.registry)) | |||
|
81 | except ChannelstreamPermissionException: | |||
|
82 | log.error('Incorrect permissions for requested channels') | |||
|
83 | raise HTTPForbidden() | |||
|
84 | ||||
|
85 | user = c.rhodecode_user | |||
|
86 | if user.user_id: | |||
|
87 | user_data = get_user_data(user.user_id) | |||
|
88 | else: | |||
|
89 | user_data = { | |||
|
90 | 'id': None, | |||
|
91 | 'username': None, | |||
|
92 | 'first_name': None, | |||
|
93 | 'last_name': None, | |||
|
94 | 'icon_link': None, | |||
|
95 | 'display_name': None, | |||
|
96 | 'display_link': None, | |||
|
97 | } | |||
|
98 | payload = { | |||
|
99 | 'username': user.username, | |||
|
100 | 'user_state': user_data, | |||
|
101 | 'conn_id': str(uuid.uuid4()), | |||
|
102 | 'channels': channels, | |||
|
103 | 'channel_configs': {}, | |||
|
104 | 'state_public_keys': STATE_PUBLIC_KEYS, | |||
|
105 | 'info': { | |||
|
106 | 'exclude_channels': ['broadcast'] | |||
|
107 | } | |||
|
108 | } | |||
|
109 | filtered_channels = [channel for channel in channels | |||
|
110 | if channel != 'broadcast'] | |||
|
111 | for channel in filtered_channels: | |||
|
112 | payload['channel_configs'][channel] = { | |||
|
113 | 'notify_presence': True, | |||
|
114 | 'history_size': 100, | |||
|
115 | 'store_history': True, | |||
|
116 | 'broadcast_presence_with_user_lists': True | |||
|
117 | } | |||
|
118 | # connect user to server | |||
|
119 | try: | |||
|
120 | connect_result = channelstream_request(self.channelstream_config, | |||
|
121 | payload, '/connect') | |||
|
122 | except ChannelstreamConnectionException: | |||
|
123 | log.exception('Channelstream service is down') | |||
|
124 | return HTTPBadGateway() | |||
|
125 | ||||
|
126 | connect_result['channels'] = channels | |||
|
127 | connect_result['channels_info'] = parse_channels_info( | |||
|
128 | connect_result['channels_info'], | |||
|
129 | include_channel_info=filtered_channels) | |||
|
130 | update_history_from_logs(self.channelstream_config, | |||
|
131 | filtered_channels, connect_result) | |||
|
132 | return connect_result | |||
|
133 | ||||
|
134 | @NotAnonymous() | |||
|
135 | @view_config(route_name='channelstream_subscribe', renderer='json') | |||
|
136 | def subscribe(self): | |||
|
137 | """ can be used to subscribe specific connection to other channels """ | |||
|
138 | try: | |||
|
139 | json_body = self.request.json_body | |||
|
140 | except Exception: | |||
|
141 | log.exception('Failed to decode json from request') | |||
|
142 | raise HTTPBadRequest() | |||
|
143 | try: | |||
|
144 | channels = check_channel_permissions( | |||
|
145 | json_body.get('channels'), | |||
|
146 | get_connection_validators(self.request.registry)) | |||
|
147 | except ChannelstreamPermissionException: | |||
|
148 | log.error('Incorrect permissions for requested channels') | |||
|
149 | raise HTTPForbidden() | |||
|
150 | payload = {'conn_id': json_body.get('conn_id', ''), | |||
|
151 | 'channels': channels, | |||
|
152 | 'channel_configs': {}, | |||
|
153 | 'info': { | |||
|
154 | 'exclude_channels': ['broadcast']} | |||
|
155 | } | |||
|
156 | filtered_channels = [chan for chan in channels if chan != 'broadcast'] | |||
|
157 | for channel in filtered_channels: | |||
|
158 | payload['channel_configs'][channel] = { | |||
|
159 | 'notify_presence': True, | |||
|
160 | 'history_size': 100, | |||
|
161 | 'store_history': True, | |||
|
162 | 'broadcast_presence_with_user_lists': True | |||
|
163 | } | |||
|
164 | try: | |||
|
165 | connect_result = channelstream_request( | |||
|
166 | self.channelstream_config, payload, '/subscribe') | |||
|
167 | except ChannelstreamConnectionException: | |||
|
168 | log.exception('Channelstream service is down') | |||
|
169 | return HTTPBadGateway() | |||
|
170 | # include_channel_info will limit history only to new channel | |||
|
171 | # to not overwrite histories on other channels in client | |||
|
172 | connect_result['channels_info'] = parse_channels_info( | |||
|
173 | connect_result['channels_info'], | |||
|
174 | include_channel_info=filtered_channels) | |||
|
175 | update_history_from_logs(self.channelstream_config, | |||
|
176 | filtered_channels, connect_result) | |||
|
177 | return connect_result |
@@ -0,0 +1,219 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | # Copyright (C) 2016-2016 RhodeCode GmbH | |||
|
4 | # | |||
|
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 | |||
|
7 | # (only), as published by the Free Software Foundation. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
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/>. | |||
|
16 | # | |||
|
17 | # This program is dual-licensed. If you wish to learn more about the | |||
|
18 | # RhodeCode Enterprise Edition, including its added features, Support services, | |||
|
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |||
|
20 | ||||
|
21 | import logging | |||
|
22 | import os | |||
|
23 | ||||
|
24 | import itsdangerous | |||
|
25 | import requests | |||
|
26 | ||||
|
27 | from dogpile.core import ReadWriteMutex | |||
|
28 | ||||
|
29 | import rhodecode.lib.helpers as h | |||
|
30 | ||||
|
31 | from rhodecode.lib.auth import HasRepoPermissionAny | |||
|
32 | from rhodecode.lib.ext_json import json | |||
|
33 | from rhodecode.model.db import User | |||
|
34 | ||||
|
35 | log = logging.getLogger(__name__) | |||
|
36 | ||||
|
37 | LOCK = ReadWriteMutex() | |||
|
38 | ||||
|
39 | STATE_PUBLIC_KEYS = ['id', 'username', 'first_name', 'last_name', | |||
|
40 | 'icon_link', 'display_name', 'display_link'] | |||
|
41 | ||||
|
42 | ||||
|
43 | class ChannelstreamException(Exception): | |||
|
44 | pass | |||
|
45 | ||||
|
46 | ||||
|
47 | class ChannelstreamConnectionException(Exception): | |||
|
48 | pass | |||
|
49 | ||||
|
50 | ||||
|
51 | class ChannelstreamPermissionException(Exception): | |||
|
52 | pass | |||
|
53 | ||||
|
54 | ||||
|
55 | def channelstream_request(config, payload, endpoint, raise_exc=True): | |||
|
56 | signer = itsdangerous.TimestampSigner(config['secret']) | |||
|
57 | sig_for_server = signer.sign(endpoint) | |||
|
58 | secret_headers = {'x-channelstream-secret': sig_for_server, | |||
|
59 | 'x-channelstream-endpoint': endpoint, | |||
|
60 | 'Content-Type': 'application/json'} | |||
|
61 | req_url = 'http://{}{}'.format(config['server'], endpoint) | |||
|
62 | response = None | |||
|
63 | try: | |||
|
64 | response = requests.post(req_url, data=json.dumps(payload), | |||
|
65 | headers=secret_headers).json() | |||
|
66 | except requests.ConnectionError: | |||
|
67 | log.exception('ConnectionError happened') | |||
|
68 | if raise_exc: | |||
|
69 | raise ChannelstreamConnectionException() | |||
|
70 | except Exception: | |||
|
71 | log.exception('Exception related to channelstream happened') | |||
|
72 | if raise_exc: | |||
|
73 | raise ChannelstreamConnectionException() | |||
|
74 | return response | |||
|
75 | ||||
|
76 | ||||
|
77 | def get_user_data(user_id): | |||
|
78 | user = User.get(user_id) | |||
|
79 | return { | |||
|
80 | 'id': user.user_id, | |||
|
81 | 'username': user.username, | |||
|
82 | 'first_name': user.name, | |||
|
83 | 'last_name': user.lastname, | |||
|
84 | 'icon_link': h.gravatar_url(user.email, 14), | |||
|
85 | 'display_name': h.person(user, 'username_or_name_or_email'), | |||
|
86 | 'display_link': h.link_to_user(user), | |||
|
87 | } | |||
|
88 | ||||
|
89 | ||||
|
90 | def broadcast_validator(channel_name): | |||
|
91 | """ checks if user can access the broadcast channel """ | |||
|
92 | if channel_name == 'broadcast': | |||
|
93 | return True | |||
|
94 | ||||
|
95 | ||||
|
96 | def repo_validator(channel_name): | |||
|
97 | """ checks if user can access the broadcast channel """ | |||
|
98 | channel_prefix = '/repo$' | |||
|
99 | if channel_name.startswith(channel_prefix): | |||
|
100 | elements = channel_name[len(channel_prefix):].split('$') | |||
|
101 | repo_name = elements[0] | |||
|
102 | can_access = HasRepoPermissionAny( | |||
|
103 | 'repository.read', | |||
|
104 | 'repository.write', | |||
|
105 | 'repository.admin')(repo_name) | |||
|
106 | log.debug('permission check for {} channel ' | |||
|
107 | 'resulted in {}'.format(repo_name, can_access)) | |||
|
108 | if can_access: | |||
|
109 | return True | |||
|
110 | return False | |||
|
111 | ||||
|
112 | ||||
|
113 | def check_channel_permissions(channels, plugin_validators, should_raise=True): | |||
|
114 | valid_channels = [] | |||
|
115 | ||||
|
116 | validators = [broadcast_validator, repo_validator] | |||
|
117 | if plugin_validators: | |||
|
118 | validators.extend(plugin_validators) | |||
|
119 | for channel_name in channels: | |||
|
120 | is_valid = False | |||
|
121 | for validator in validators: | |||
|
122 | if validator(channel_name): | |||
|
123 | is_valid = True | |||
|
124 | break | |||
|
125 | if is_valid: | |||
|
126 | valid_channels.append(channel_name) | |||
|
127 | else: | |||
|
128 | if should_raise: | |||
|
129 | raise ChannelstreamPermissionException() | |||
|
130 | return valid_channels | |||
|
131 | ||||
|
132 | ||||
|
133 | def get_channels_info(self, channels): | |||
|
134 | payload = {'channels': channels} | |||
|
135 | # gather persistence info | |||
|
136 | return channelstream_request(self._config(), payload, '/info') | |||
|
137 | ||||
|
138 | ||||
|
139 | def parse_channels_info(info_result, include_channel_info=None): | |||
|
140 | """ | |||
|
141 | Returns data that contains only secure information that can be | |||
|
142 | presented to clients | |||
|
143 | """ | |||
|
144 | include_channel_info = include_channel_info or [] | |||
|
145 | ||||
|
146 | user_state_dict = {} | |||
|
147 | for userinfo in info_result['users']: | |||
|
148 | user_state_dict[userinfo['user']] = { | |||
|
149 | k: v for k, v in userinfo['state'].items() | |||
|
150 | if k in STATE_PUBLIC_KEYS | |||
|
151 | } | |||
|
152 | ||||
|
153 | channels_info = {} | |||
|
154 | ||||
|
155 | for c_name, c_info in info_result['channels'].items(): | |||
|
156 | if c_name not in include_channel_info: | |||
|
157 | continue | |||
|
158 | connected_list = [] | |||
|
159 | for userinfo in c_info['users']: | |||
|
160 | connected_list.append({ | |||
|
161 | 'user': userinfo['user'], | |||
|
162 | 'state': user_state_dict[userinfo['user']] | |||
|
163 | }) | |||
|
164 | channels_info[c_name] = {'users': connected_list, | |||
|
165 | 'history': c_info['history']} | |||
|
166 | ||||
|
167 | return channels_info | |||
|
168 | ||||
|
169 | ||||
|
170 | def log_filepath(history_location, channel_name): | |||
|
171 | filename = '{}.log'.format(channel_name.encode('hex')) | |||
|
172 | filepath = os.path.join(history_location, filename) | |||
|
173 | return filepath | |||
|
174 | ||||
|
175 | ||||
|
176 | def read_history(history_location, channel_name): | |||
|
177 | filepath = log_filepath(history_location, channel_name) | |||
|
178 | if not os.path.exists(filepath): | |||
|
179 | return [] | |||
|
180 | history_lines_limit = -100 | |||
|
181 | history = [] | |||
|
182 | with open(filepath, 'rb') as f: | |||
|
183 | for line in f.readlines()[history_lines_limit:]: | |||
|
184 | try: | |||
|
185 | history.append(json.loads(line)) | |||
|
186 | except Exception: | |||
|
187 | log.exception('Failed to load history') | |||
|
188 | return history | |||
|
189 | ||||
|
190 | ||||
|
191 | def update_history_from_logs(config, channels, payload): | |||
|
192 | history_location = config.get('history.location') | |||
|
193 | for channel in channels: | |||
|
194 | history = read_history(history_location, channel) | |||
|
195 | payload['channels_info'][channel]['history'] = history | |||
|
196 | ||||
|
197 | ||||
|
198 | def write_history(config, message): | |||
|
199 | """ writes a messge to a base64encoded filename """ | |||
|
200 | history_location = config.get('history.location') | |||
|
201 | if not os.path.exists(history_location): | |||
|
202 | return | |||
|
203 | try: | |||
|
204 | LOCK.acquire_write_lock() | |||
|
205 | filepath = log_filepath(history_location, message['channel']) | |||
|
206 | with open(filepath, 'ab') as f: | |||
|
207 | json.dump(message, f) | |||
|
208 | f.write('\n') | |||
|
209 | finally: | |||
|
210 | LOCK.release_write_lock() | |||
|
211 | ||||
|
212 | ||||
|
213 | def get_connection_validators(registry): | |||
|
214 | validators = [] | |||
|
215 | for k, config in registry.rhodecode_plugins.iteritems(): | |||
|
216 | validator = config.get('channelstream', {}).get('connect_validator') | |||
|
217 | if validator: | |||
|
218 | validators.append(validator) | |||
|
219 | return validators |
@@ -0,0 +1,268 b'' | |||||
|
1 | // Mix-ins | |||
|
2 | .borderRadius(@radius) { | |||
|
3 | -moz-border-radius: @radius; | |||
|
4 | -webkit-border-radius: @radius; | |||
|
5 | border-radius: @radius; | |||
|
6 | } | |||
|
7 | ||||
|
8 | .boxShadow(@boxShadow) { | |||
|
9 | -moz-box-shadow: @boxShadow; | |||
|
10 | -webkit-box-shadow: @boxShadow; | |||
|
11 | box-shadow: @boxShadow; | |||
|
12 | } | |||
|
13 | ||||
|
14 | .opacity(@opacity) { | |||
|
15 | @opacityPercent: @opacity * 100; | |||
|
16 | opacity: @opacity; | |||
|
17 | -ms-filter: ~"progid:DXImageTransform.Microsoft.Alpha(Opacity=@{opacityPercent})"; | |||
|
18 | filter: ~"alpha(opacity=@{opacityPercent})"; | |||
|
19 | } | |||
|
20 | ||||
|
21 | .wordWrap(@wordWrap: break-word) { | |||
|
22 | -ms-word-wrap: @wordWrap; | |||
|
23 | word-wrap: @wordWrap; | |||
|
24 | } | |||
|
25 | ||||
|
26 | // Variables | |||
|
27 | @black: #000000; | |||
|
28 | @grey: #999999; | |||
|
29 | @light-grey: #CCCCCC; | |||
|
30 | @white: #FFFFFF; | |||
|
31 | @near-black: #030303; | |||
|
32 | @green: #51A351; | |||
|
33 | @red: #BD362F; | |||
|
34 | @blue: #2F96B4; | |||
|
35 | @orange: #F89406; | |||
|
36 | @default-container-opacity: .8; | |||
|
37 | ||||
|
38 | // Styles | |||
|
39 | .toast-title { | |||
|
40 | font-weight: bold; | |||
|
41 | } | |||
|
42 | ||||
|
43 | .toast-message { | |||
|
44 | .wordWrap(); | |||
|
45 | ||||
|
46 | a, | |||
|
47 | label { | |||
|
48 | color: @near-black; | |||
|
49 | } | |||
|
50 | ||||
|
51 | a:hover { | |||
|
52 | color: @light-grey; | |||
|
53 | text-decoration: none; | |||
|
54 | } | |||
|
55 | } | |||
|
56 | ||||
|
57 | .toast-close-button { | |||
|
58 | position: relative; | |||
|
59 | right: -0.3em; | |||
|
60 | top: -0.3em; | |||
|
61 | float: right; | |||
|
62 | font-size: 20px; | |||
|
63 | font-weight: bold; | |||
|
64 | color: @black; | |||
|
65 | -webkit-text-shadow: 0 1px 0 rgba(255,255,255,1); | |||
|
66 | text-shadow: 0 1px 0 rgba(255,255,255,1); | |||
|
67 | .opacity(0.8); | |||
|
68 | ||||
|
69 | &:hover, | |||
|
70 | &:focus { | |||
|
71 | color: @black; | |||
|
72 | text-decoration: none; | |||
|
73 | cursor: pointer; | |||
|
74 | .opacity(0.4); | |||
|
75 | } | |||
|
76 | } | |||
|
77 | ||||
|
78 | /*Additional properties for button version | |||
|
79 | iOS requires the button element instead of an anchor tag. | |||
|
80 | If you want the anchor version, it requires `href="#"`.*/ | |||
|
81 | button.toast-close-button { | |||
|
82 | padding: 0; | |||
|
83 | cursor: pointer; | |||
|
84 | background: transparent; | |||
|
85 | border: 0; | |||
|
86 | -webkit-appearance: none; | |||
|
87 | } | |||
|
88 | ||||
|
89 | //#endregion | |||
|
90 | ||||
|
91 | .toast-top-center { | |||
|
92 | top: 0; | |||
|
93 | right: 0; | |||
|
94 | width: 100%; | |||
|
95 | } | |||
|
96 | ||||
|
97 | .toast-bottom-center { | |||
|
98 | bottom: 0; | |||
|
99 | right: 0; | |||
|
100 | width: 100%; | |||
|
101 | } | |||
|
102 | ||||
|
103 | .toast-top-full-width { | |||
|
104 | top: 0; | |||
|
105 | right: 0; | |||
|
106 | width: 100%; | |||
|
107 | } | |||
|
108 | ||||
|
109 | .toast-bottom-full-width { | |||
|
110 | bottom: 0; | |||
|
111 | right: 0; | |||
|
112 | width: 100%; | |||
|
113 | } | |||
|
114 | ||||
|
115 | .toast-top-left { | |||
|
116 | top: 12px; | |||
|
117 | left: 12px; | |||
|
118 | } | |||
|
119 | ||||
|
120 | .toast-top-right { | |||
|
121 | top: 12px; | |||
|
122 | right: 12px; | |||
|
123 | } | |||
|
124 | ||||
|
125 | .toast-bottom-right { | |||
|
126 | right: 12px; | |||
|
127 | bottom: 12px; | |||
|
128 | } | |||
|
129 | ||||
|
130 | .toast-bottom-left { | |||
|
131 | bottom: 12px; | |||
|
132 | left: 12px; | |||
|
133 | } | |||
|
134 | ||||
|
135 | #toast-container { | |||
|
136 | position: fixed; | |||
|
137 | z-index: 999999; | |||
|
138 | // The container should not be clickable. | |||
|
139 | pointer-events: none; | |||
|
140 | * { | |||
|
141 | -moz-box-sizing: border-box; | |||
|
142 | -webkit-box-sizing: border-box; | |||
|
143 | box-sizing: border-box; | |||
|
144 | } | |||
|
145 | ||||
|
146 | > div { | |||
|
147 | position: relative; | |||
|
148 | // The toast itself should be clickable. | |||
|
149 | pointer-events: auto; | |||
|
150 | overflow: hidden; | |||
|
151 | margin: 0 0 6px; | |||
|
152 | padding: 15px; | |||
|
153 | width: 300px; | |||
|
154 | .borderRadius(1px 1px 1px 1px); | |||
|
155 | background-position: 15px center; | |||
|
156 | background-repeat: no-repeat; | |||
|
157 | color: @near-black; | |||
|
158 | .opacity(@default-container-opacity); | |||
|
159 | } | |||
|
160 | ||||
|
161 | > :hover { | |||
|
162 | .opacity(1); | |||
|
163 | cursor: pointer; | |||
|
164 | } | |||
|
165 | ||||
|
166 | > .toast-info { | |||
|
167 | //background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important; | |||
|
168 | } | |||
|
169 | ||||
|
170 | > .toast-error { | |||
|
171 | //background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important; | |||
|
172 | } | |||
|
173 | ||||
|
174 | > .toast-success { | |||
|
175 | //background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important; | |||
|
176 | } | |||
|
177 | ||||
|
178 | > .toast-warning { | |||
|
179 | //background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important; | |||
|
180 | } | |||
|
181 | ||||
|
182 | /*overrides*/ | |||
|
183 | &.toast-top-center > div, | |||
|
184 | &.toast-bottom-center > div { | |||
|
185 | width: 400px; | |||
|
186 | margin-left: auto; | |||
|
187 | margin-right: auto; | |||
|
188 | } | |||
|
189 | ||||
|
190 | &.toast-top-full-width > div, | |||
|
191 | &.toast-bottom-full-width > div { | |||
|
192 | width: 96%; | |||
|
193 | margin-left: auto; | |||
|
194 | margin-right: auto; | |||
|
195 | } | |||
|
196 | } | |||
|
197 | ||||
|
198 | .toast { | |||
|
199 | border-color: @near-black; | |||
|
200 | border-style: solid; | |||
|
201 | border-width: 2px 2px 2px 25px; | |||
|
202 | background-color: @white; | |||
|
203 | } | |||
|
204 | ||||
|
205 | .toast-success { | |||
|
206 | border-color: @green; | |||
|
207 | } | |||
|
208 | ||||
|
209 | .toast-error { | |||
|
210 | border-color: @red; | |||
|
211 | } | |||
|
212 | ||||
|
213 | .toast-info { | |||
|
214 | border-color: @blue; | |||
|
215 | } | |||
|
216 | ||||
|
217 | .toast-warning { | |||
|
218 | border-color: @orange; | |||
|
219 | } | |||
|
220 | ||||
|
221 | .toast-progress { | |||
|
222 | position: absolute; | |||
|
223 | left: 0; | |||
|
224 | bottom: 0; | |||
|
225 | height: 4px; | |||
|
226 | background-color: @black; | |||
|
227 | .opacity(0.4); | |||
|
228 | } | |||
|
229 | ||||
|
230 | /*Responsive Design*/ | |||
|
231 | ||||
|
232 | @media all and (max-width: 240px) { | |||
|
233 | #toast-container { | |||
|
234 | ||||
|
235 | > div { | |||
|
236 | padding: 8px; | |||
|
237 | width: 11em; | |||
|
238 | } | |||
|
239 | ||||
|
240 | & .toast-close-button { | |||
|
241 | right: -0.2em; | |||
|
242 | top: -0.2em; | |||
|
243 | } | |||
|
244 | } | |||
|
245 | } | |||
|
246 | ||||
|
247 | @media all and (min-width: 241px) and (max-width: 480px) { | |||
|
248 | #toast-container { | |||
|
249 | > div { | |||
|
250 | padding: 8px; | |||
|
251 | width: 18em; | |||
|
252 | } | |||
|
253 | ||||
|
254 | & .toast-close-button { | |||
|
255 | right: -0.2em; | |||
|
256 | top: -0.2em; | |||
|
257 | } | |||
|
258 | } | |||
|
259 | } | |||
|
260 | ||||
|
261 | @media all and (min-width: 481px) and (max-width: 768px) { | |||
|
262 | #toast-container { | |||
|
263 | > div { | |||
|
264 | padding: 15px; | |||
|
265 | width: 25em; | |||
|
266 | } | |||
|
267 | } | |||
|
268 | } |
@@ -0,0 +1,435 b'' | |||||
|
1 | /* | |||
|
2 | * Toastr | |||
|
3 | * Copyright 2012-2015 | |||
|
4 | * Authors: John Papa, Hans Fjällemark, and Tim Ferrell. | |||
|
5 | * All Rights Reserved. | |||
|
6 | * Use, reproduction, distribution, and modification of this code is subject to the terms and | |||
|
7 | * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php | |||
|
8 | * | |||
|
9 | * ARIA Support: Greta Krafsig | |||
|
10 | * | |||
|
11 | * Project: https://github.com/CodeSeven/toastr | |||
|
12 | */ | |||
|
13 | /* global define */ | |||
|
14 | (function (define) { | |||
|
15 | define(['jquery'], function ($) { | |||
|
16 | return (function () { | |||
|
17 | var $container; | |||
|
18 | var listener; | |||
|
19 | var toastId = 0; | |||
|
20 | var toastType = { | |||
|
21 | error: 'error', | |||
|
22 | info: 'info', | |||
|
23 | success: 'success', | |||
|
24 | warning: 'warning' | |||
|
25 | }; | |||
|
26 | ||||
|
27 | var toastr = { | |||
|
28 | clear: clear, | |||
|
29 | remove: remove, | |||
|
30 | error: error, | |||
|
31 | getContainer: getContainer, | |||
|
32 | info: info, | |||
|
33 | options: {}, | |||
|
34 | subscribe: subscribe, | |||
|
35 | success: success, | |||
|
36 | version: '2.1.2', | |||
|
37 | warning: warning | |||
|
38 | }; | |||
|
39 | ||||
|
40 | var previousToast; | |||
|
41 | ||||
|
42 | return toastr; | |||
|
43 | ||||
|
44 | //////////////// | |||
|
45 | ||||
|
46 | function error(message, title, optionsOverride) { | |||
|
47 | return notify({ | |||
|
48 | type: toastType.error, | |||
|
49 | iconClass: getOptions().iconClasses.error, | |||
|
50 | message: message, | |||
|
51 | optionsOverride: optionsOverride, | |||
|
52 | title: title | |||
|
53 | }); | |||
|
54 | } | |||
|
55 | ||||
|
56 | function getContainer(options, create) { | |||
|
57 | if (!options) { options = getOptions(); } | |||
|
58 | $container = $('#' + options.containerId); | |||
|
59 | if ($container.length) { | |||
|
60 | return $container; | |||
|
61 | } | |||
|
62 | if (create) { | |||
|
63 | $container = createContainer(options); | |||
|
64 | } | |||
|
65 | return $container; | |||
|
66 | } | |||
|
67 | ||||
|
68 | function info(message, title, optionsOverride) { | |||
|
69 | return notify({ | |||
|
70 | type: toastType.info, | |||
|
71 | iconClass: getOptions().iconClasses.info, | |||
|
72 | message: message, | |||
|
73 | optionsOverride: optionsOverride, | |||
|
74 | title: title | |||
|
75 | }); | |||
|
76 | } | |||
|
77 | ||||
|
78 | function subscribe(callback) { | |||
|
79 | listener = callback; | |||
|
80 | } | |||
|
81 | ||||
|
82 | function success(message, title, optionsOverride) { | |||
|
83 | return notify({ | |||
|
84 | type: toastType.success, | |||
|
85 | iconClass: getOptions().iconClasses.success, | |||
|
86 | message: message, | |||
|
87 | optionsOverride: optionsOverride, | |||
|
88 | title: title | |||
|
89 | }); | |||
|
90 | } | |||
|
91 | ||||
|
92 | function warning(message, title, optionsOverride) { | |||
|
93 | return notify({ | |||
|
94 | type: toastType.warning, | |||
|
95 | iconClass: getOptions().iconClasses.warning, | |||
|
96 | message: message, | |||
|
97 | optionsOverride: optionsOverride, | |||
|
98 | title: title | |||
|
99 | }); | |||
|
100 | } | |||
|
101 | ||||
|
102 | function clear($toastElement, clearOptions) { | |||
|
103 | var options = getOptions(); | |||
|
104 | if (!$container) { getContainer(options); } | |||
|
105 | if (!clearToast($toastElement, options, clearOptions)) { | |||
|
106 | clearContainer(options); | |||
|
107 | } | |||
|
108 | } | |||
|
109 | ||||
|
110 | function remove($toastElement) { | |||
|
111 | var options = getOptions(); | |||
|
112 | if (!$container) { getContainer(options); } | |||
|
113 | if ($toastElement && $(':focus', $toastElement).length === 0) { | |||
|
114 | removeToast($toastElement); | |||
|
115 | return; | |||
|
116 | } | |||
|
117 | if ($container.children().length) { | |||
|
118 | $container.remove(); | |||
|
119 | } | |||
|
120 | } | |||
|
121 | ||||
|
122 | // internal functions | |||
|
123 | ||||
|
124 | function clearContainer (options) { | |||
|
125 | var toastsToClear = $container.children(); | |||
|
126 | for (var i = toastsToClear.length - 1; i >= 0; i--) { | |||
|
127 | clearToast($(toastsToClear[i]), options); | |||
|
128 | } | |||
|
129 | } | |||
|
130 | ||||
|
131 | function clearToast ($toastElement, options, clearOptions) { | |||
|
132 | var force = clearOptions && clearOptions.force ? clearOptions.force : false; | |||
|
133 | if ($toastElement && (force || $(':focus', $toastElement).length === 0)) { | |||
|
134 | $toastElement[options.hideMethod]({ | |||
|
135 | duration: options.hideDuration, | |||
|
136 | easing: options.hideEasing, | |||
|
137 | complete: function () { removeToast($toastElement); } | |||
|
138 | }); | |||
|
139 | return true; | |||
|
140 | } | |||
|
141 | return false; | |||
|
142 | } | |||
|
143 | ||||
|
144 | function createContainer(options) { | |||
|
145 | $container = $('<div/>') | |||
|
146 | .attr('id', options.containerId) | |||
|
147 | .addClass(options.positionClass) | |||
|
148 | .attr('aria-live', 'polite') | |||
|
149 | .attr('role', 'alert'); | |||
|
150 | ||||
|
151 | $container.appendTo($(options.target)); | |||
|
152 | return $container; | |||
|
153 | } | |||
|
154 | ||||
|
155 | function getDefaults() { | |||
|
156 | return { | |||
|
157 | tapToDismiss: true, | |||
|
158 | toastClass: 'toast', | |||
|
159 | containerId: 'toast-container', | |||
|
160 | debug: false, | |||
|
161 | ||||
|
162 | showMethod: 'fadeIn', //fadeIn, slideDown, and show are built into jQuery | |||
|
163 | showDuration: 300, | |||
|
164 | showEasing: 'swing', //swing and linear are built into jQuery | |||
|
165 | onShown: undefined, | |||
|
166 | hideMethod: 'fadeOut', | |||
|
167 | hideDuration: 1000, | |||
|
168 | hideEasing: 'swing', | |||
|
169 | onHidden: undefined, | |||
|
170 | closeMethod: false, | |||
|
171 | closeDuration: false, | |||
|
172 | closeEasing: false, | |||
|
173 | ||||
|
174 | extendedTimeOut: 1000, | |||
|
175 | iconClasses: { | |||
|
176 | error: 'toast-error', | |||
|
177 | info: 'toast-info', | |||
|
178 | success: 'toast-success', | |||
|
179 | warning: 'toast-warning' | |||
|
180 | }, | |||
|
181 | iconClass: 'toast-info', | |||
|
182 | positionClass: 'toast-top-right', | |||
|
183 | timeOut: 5000, // Set timeOut and extendedTimeOut to 0 to make it sticky | |||
|
184 | titleClass: 'toast-title', | |||
|
185 | messageClass: 'toast-message', | |||
|
186 | escapeHtml: false, | |||
|
187 | target: 'body', | |||
|
188 | closeHtml: '<button type="button">×</button>', | |||
|
189 | newestOnTop: true, | |||
|
190 | preventDuplicates: false, | |||
|
191 | progressBar: false | |||
|
192 | }; | |||
|
193 | } | |||
|
194 | ||||
|
195 | function publish(args) { | |||
|
196 | if (!listener) { return; } | |||
|
197 | listener(args); | |||
|
198 | } | |||
|
199 | ||||
|
200 | function notify(map) { | |||
|
201 | var options = getOptions(); | |||
|
202 | var iconClass = map.iconClass || options.iconClass; | |||
|
203 | ||||
|
204 | if (typeof (map.optionsOverride) !== 'undefined') { | |||
|
205 | options = $.extend(options, map.optionsOverride); | |||
|
206 | iconClass = map.optionsOverride.iconClass || iconClass; | |||
|
207 | } | |||
|
208 | ||||
|
209 | if (shouldExit(options, map)) { return; } | |||
|
210 | ||||
|
211 | toastId++; | |||
|
212 | ||||
|
213 | $container = getContainer(options, true); | |||
|
214 | ||||
|
215 | var intervalId = null; | |||
|
216 | var $toastElement = $('<div/>'); | |||
|
217 | var $titleElement = $('<div/>'); | |||
|
218 | var $messageElement = $('<div/>'); | |||
|
219 | var $progressElement = $('<div/>'); | |||
|
220 | var $closeElement = $(options.closeHtml); | |||
|
221 | var progressBar = { | |||
|
222 | intervalId: null, | |||
|
223 | hideEta: null, | |||
|
224 | maxHideTime: null | |||
|
225 | }; | |||
|
226 | var response = { | |||
|
227 | toastId: toastId, | |||
|
228 | state: 'visible', | |||
|
229 | startTime: new Date(), | |||
|
230 | options: options, | |||
|
231 | map: map | |||
|
232 | }; | |||
|
233 | ||||
|
234 | personalizeToast(); | |||
|
235 | ||||
|
236 | displayToast(); | |||
|
237 | ||||
|
238 | handleEvents(); | |||
|
239 | ||||
|
240 | publish(response); | |||
|
241 | ||||
|
242 | if (options.debug && console) { | |||
|
243 | console.log(response); | |||
|
244 | } | |||
|
245 | ||||
|
246 | return $toastElement; | |||
|
247 | ||||
|
248 | function escapeHtml(source) { | |||
|
249 | if (source == null) | |||
|
250 | source = ""; | |||
|
251 | ||||
|
252 | return new String(source) | |||
|
253 | .replace(/&/g, '&') | |||
|
254 | .replace(/"/g, '"') | |||
|
255 | .replace(/'/g, ''') | |||
|
256 | .replace(/</g, '<') | |||
|
257 | .replace(/>/g, '>'); | |||
|
258 | } | |||
|
259 | ||||
|
260 | function personalizeToast() { | |||
|
261 | setIcon(); | |||
|
262 | setTitle(); | |||
|
263 | setMessage(); | |||
|
264 | setCloseButton(); | |||
|
265 | setProgressBar(); | |||
|
266 | setSequence(); | |||
|
267 | } | |||
|
268 | ||||
|
269 | function handleEvents() { | |||
|
270 | $toastElement.hover(stickAround, delayedHideToast); | |||
|
271 | if (!options.onclick && options.tapToDismiss) { | |||
|
272 | $toastElement.click(hideToast); | |||
|
273 | } | |||
|
274 | ||||
|
275 | if (options.closeButton && $closeElement) { | |||
|
276 | $closeElement.click(function (event) { | |||
|
277 | if (event.stopPropagation) { | |||
|
278 | event.stopPropagation(); | |||
|
279 | } else if (event.cancelBubble !== undefined && event.cancelBubble !== true) { | |||
|
280 | event.cancelBubble = true; | |||
|
281 | } | |||
|
282 | hideToast(true); | |||
|
283 | }); | |||
|
284 | } | |||
|
285 | ||||
|
286 | if (options.onclick) { | |||
|
287 | $toastElement.click(function (event) { | |||
|
288 | options.onclick(event); | |||
|
289 | hideToast(); | |||
|
290 | }); | |||
|
291 | } | |||
|
292 | } | |||
|
293 | ||||
|
294 | function displayToast() { | |||
|
295 | $toastElement.hide(); | |||
|
296 | ||||
|
297 | $toastElement[options.showMethod]( | |||
|
298 | {duration: options.showDuration, easing: options.showEasing, complete: options.onShown} | |||
|
299 | ); | |||
|
300 | ||||
|
301 | if (options.timeOut > 0) { | |||
|
302 | intervalId = setTimeout(hideToast, options.timeOut); | |||
|
303 | progressBar.maxHideTime = parseFloat(options.timeOut); | |||
|
304 | progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime; | |||
|
305 | if (options.progressBar) { | |||
|
306 | progressBar.intervalId = setInterval(updateProgress, 10); | |||
|
307 | } | |||
|
308 | } | |||
|
309 | } | |||
|
310 | ||||
|
311 | function setIcon() { | |||
|
312 | if (map.iconClass) { | |||
|
313 | $toastElement.addClass(options.toastClass).addClass(iconClass); | |||
|
314 | } | |||
|
315 | } | |||
|
316 | ||||
|
317 | function setSequence() { | |||
|
318 | if (options.newestOnTop) { | |||
|
319 | $container.prepend($toastElement); | |||
|
320 | } else { | |||
|
321 | $container.append($toastElement); | |||
|
322 | } | |||
|
323 | } | |||
|
324 | ||||
|
325 | function setTitle() { | |||
|
326 | if (map.title) { | |||
|
327 | $titleElement.append(!options.escapeHtml ? map.title : escapeHtml(map.title)).addClass(options.titleClass); | |||
|
328 | $toastElement.append($titleElement); | |||
|
329 | } | |||
|
330 | } | |||
|
331 | ||||
|
332 | function setMessage() { | |||
|
333 | if (map.message) { | |||
|
334 | $messageElement.append(!options.escapeHtml ? map.message : escapeHtml(map.message)).addClass(options.messageClass); | |||
|
335 | $toastElement.append($messageElement); | |||
|
336 | } | |||
|
337 | } | |||
|
338 | ||||
|
339 | function setCloseButton() { | |||
|
340 | if (options.closeButton) { | |||
|
341 | $closeElement.addClass('toast-close-button').attr('role', 'button'); | |||
|
342 | $toastElement.prepend($closeElement); | |||
|
343 | } | |||
|
344 | } | |||
|
345 | ||||
|
346 | function setProgressBar() { | |||
|
347 | if (options.progressBar) { | |||
|
348 | $progressElement.addClass('toast-progress'); | |||
|
349 | $toastElement.prepend($progressElement); | |||
|
350 | } | |||
|
351 | } | |||
|
352 | ||||
|
353 | function shouldExit(options, map) { | |||
|
354 | if (options.preventDuplicates) { | |||
|
355 | if (map.message === previousToast) { | |||
|
356 | return true; | |||
|
357 | } else { | |||
|
358 | previousToast = map.message; | |||
|
359 | } | |||
|
360 | } | |||
|
361 | return false; | |||
|
362 | } | |||
|
363 | ||||
|
364 | function hideToast(override) { | |||
|
365 | var method = override && options.closeMethod !== false ? options.closeMethod : options.hideMethod; | |||
|
366 | var duration = override && options.closeDuration !== false ? | |||
|
367 | options.closeDuration : options.hideDuration; | |||
|
368 | var easing = override && options.closeEasing !== false ? options.closeEasing : options.hideEasing; | |||
|
369 | if ($(':focus', $toastElement).length && !override) { | |||
|
370 | return; | |||
|
371 | } | |||
|
372 | clearTimeout(progressBar.intervalId); | |||
|
373 | return $toastElement[method]({ | |||
|
374 | duration: duration, | |||
|
375 | easing: easing, | |||
|
376 | complete: function () { | |||
|
377 | removeToast($toastElement); | |||
|
378 | if (options.onHidden && response.state !== 'hidden') { | |||
|
379 | options.onHidden(); | |||
|
380 | } | |||
|
381 | response.state = 'hidden'; | |||
|
382 | response.endTime = new Date(); | |||
|
383 | publish(response); | |||
|
384 | } | |||
|
385 | }); | |||
|
386 | } | |||
|
387 | ||||
|
388 | function delayedHideToast() { | |||
|
389 | if (options.timeOut > 0 || options.extendedTimeOut > 0) { | |||
|
390 | intervalId = setTimeout(hideToast, options.extendedTimeOut); | |||
|
391 | progressBar.maxHideTime = parseFloat(options.extendedTimeOut); | |||
|
392 | progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime; | |||
|
393 | } | |||
|
394 | } | |||
|
395 | ||||
|
396 | function stickAround() { | |||
|
397 | clearTimeout(intervalId); | |||
|
398 | progressBar.hideEta = 0; | |||
|
399 | $toastElement.stop(true, true)[options.showMethod]( | |||
|
400 | {duration: options.showDuration, easing: options.showEasing} | |||
|
401 | ); | |||
|
402 | } | |||
|
403 | ||||
|
404 | function updateProgress() { | |||
|
405 | var percentage = ((progressBar.hideEta - (new Date().getTime())) / progressBar.maxHideTime) * 100; | |||
|
406 | $progressElement.width(percentage + '%'); | |||
|
407 | } | |||
|
408 | } | |||
|
409 | ||||
|
410 | function getOptions() { | |||
|
411 | return $.extend({}, getDefaults(), toastr.options); | |||
|
412 | } | |||
|
413 | ||||
|
414 | function removeToast($toastElement) { | |||
|
415 | if (!$container) { $container = getContainer(); } | |||
|
416 | if ($toastElement.is(':visible')) { | |||
|
417 | return; | |||
|
418 | } | |||
|
419 | $toastElement.remove(); | |||
|
420 | $toastElement = null; | |||
|
421 | if ($container.children().length === 0) { | |||
|
422 | $container.remove(); | |||
|
423 | previousToast = undefined; | |||
|
424 | } | |||
|
425 | } | |||
|
426 | ||||
|
427 | })(); | |||
|
428 | }); | |||
|
429 | }(typeof define === 'function' && define.amd ? define : function (deps, factory) { | |||
|
430 | if (typeof module !== 'undefined' && module.exports) { //Node | |||
|
431 | module.exports = factory(require('jquery')); | |||
|
432 | } else { | |||
|
433 | window.toastr = factory(window.jQuery); | |||
|
434 | } | |||
|
435 | })); |
@@ -0,0 +1,219 b'' | |||||
|
1 | "use strict"; | |||
|
2 | /** leak object to top level scope **/ | |||
|
3 | var ccLog = undefined; | |||
|
4 | // global code-mirror logger;, to enable run | |||
|
5 | // Logger.get('ConnectionController').setLevel(Logger.DEBUG) | |||
|
6 | ccLog = Logger.get('ConnectionController'); | |||
|
7 | ccLog.setLevel(Logger.OFF); | |||
|
8 | ||||
|
9 | var ConnectionController; | |||
|
10 | var connCtrlr; | |||
|
11 | var registerViewChannels; | |||
|
12 | ||||
|
13 | (function () { | |||
|
14 | ConnectionController = function (webappUrl, serverUrl, urls) { | |||
|
15 | var self = this; | |||
|
16 | ||||
|
17 | var channels = ['broadcast']; | |||
|
18 | this.state = { | |||
|
19 | open: false, | |||
|
20 | webappUrl: webappUrl, | |||
|
21 | serverUrl: serverUrl, | |||
|
22 | connId: null, | |||
|
23 | socket: null, | |||
|
24 | channels: channels, | |||
|
25 | heartbeat: null, | |||
|
26 | channelsInfo: {}, | |||
|
27 | urls: urls | |||
|
28 | }; | |||
|
29 | this.channelNameParsers = []; | |||
|
30 | ||||
|
31 | this.addChannelNameParser = function (fn) { | |||
|
32 | if (this.channelNameParsers.indexOf(fn) === -1) { | |||
|
33 | this.channelNameParsers.push(fn); | |||
|
34 | } | |||
|
35 | }; | |||
|
36 | ||||
|
37 | this.listen = function () { | |||
|
38 | if (window.WebSocket) { | |||
|
39 | ccLog.debug('attempting to create socket'); | |||
|
40 | var socket_url = self.state.serverUrl + "/ws?conn_id=" + self.state.connId; | |||
|
41 | var socket_conf = { | |||
|
42 | url: socket_url, | |||
|
43 | handleAs: 'json', | |||
|
44 | headers: { | |||
|
45 | "Accept": "application/json", | |||
|
46 | "Content-Type": "application/json" | |||
|
47 | } | |||
|
48 | }; | |||
|
49 | self.state.socket = new WebSocket(socket_conf.url); | |||
|
50 | ||||
|
51 | self.state.socket.onopen = function (event) { | |||
|
52 | ccLog.debug('open event', event); | |||
|
53 | if (self.state.heartbeat === null) { | |||
|
54 | self.state.heartbeat = setInterval(function () { | |||
|
55 | if (self.state.socket.readyState === WebSocket.OPEN) { | |||
|
56 | self.state.socket.send('heartbeat'); | |||
|
57 | } | |||
|
58 | }, 10000) | |||
|
59 | } | |||
|
60 | }; | |||
|
61 | self.state.socket.onmessage = function (event) { | |||
|
62 | var data = $.parseJSON(event.data); | |||
|
63 | for (var i = 0; i < data.length; i++) { | |||
|
64 | if (data[i].message.topic) { | |||
|
65 | ccLog.debug('publishing', | |||
|
66 | data[i].message.topic, data[i]); | |||
|
67 | $.Topic(data[i].message.topic).publish(data[i]) | |||
|
68 | } | |||
|
69 | else { | |||
|
70 | cclog.warning('unhandled message', data); | |||
|
71 | } | |||
|
72 | } | |||
|
73 | }; | |||
|
74 | self.state.socket.onclose = function (event) { | |||
|
75 | ccLog.debug('closed event', event); | |||
|
76 | setTimeout(function () { | |||
|
77 | self.connect(true); | |||
|
78 | }, 5000); | |||
|
79 | }; | |||
|
80 | ||||
|
81 | self.state.socket.onerror = function (event) { | |||
|
82 | ccLog.debug('error event', event); | |||
|
83 | }; | |||
|
84 | } | |||
|
85 | else { | |||
|
86 | ccLog.debug('attempting to create long polling connection'); | |||
|
87 | var poolUrl = self.state.serverUrl + "/listen?conn_id=" + self.state.connId; | |||
|
88 | self.state.socket = $.ajax({ | |||
|
89 | url: poolUrl | |||
|
90 | }).done(function (data) { | |||
|
91 | ccLog.debug('data', data); | |||
|
92 | var data = $.parseJSON(data); | |||
|
93 | for (var i = 0; i < data.length; i++) { | |||
|
94 | if (data[i].message.topic) { | |||
|
95 | ccLog.info('publishing', | |||
|
96 | data[i].message.topic, data[i]); | |||
|
97 | $.Topic(data[i].message.topic).publish(data[i]) | |||
|
98 | } | |||
|
99 | else { | |||
|
100 | cclog.warning('unhandled message', data); | |||
|
101 | } | |||
|
102 | } | |||
|
103 | self.listen(); | |||
|
104 | }).fail(function () { | |||
|
105 | ccLog.debug('longpoll error'); | |||
|
106 | setTimeout(function () { | |||
|
107 | self.connect(true); | |||
|
108 | }, 5000); | |||
|
109 | }); | |||
|
110 | } | |||
|
111 | ||||
|
112 | }; | |||
|
113 | ||||
|
114 | this.connect = function (create_new_socket) { | |||
|
115 | var connReq = {'channels': self.state.channels}; | |||
|
116 | ccLog.debug('try obtaining connection info', connReq); | |||
|
117 | $.ajax({ | |||
|
118 | url: self.state.urls.connect, | |||
|
119 | type: "POST", | |||
|
120 | contentType: "application/json", | |||
|
121 | data: JSON.stringify(connReq), | |||
|
122 | dataType: "json" | |||
|
123 | }).done(function (data) { | |||
|
124 | ccLog.debug('Got connection:', data.conn_id); | |||
|
125 | self.state.channels = data.channels; | |||
|
126 | self.state.channelsInfo = data.channels_info; | |||
|
127 | self.state.connId = data.conn_id; | |||
|
128 | if (create_new_socket) { | |||
|
129 | self.listen(); | |||
|
130 | } | |||
|
131 | self.update(); | |||
|
132 | }).fail(function () { | |||
|
133 | setTimeout(function () { | |||
|
134 | self.connect(create_new_socket); | |||
|
135 | }, 5000); | |||
|
136 | }); | |||
|
137 | self.update(); | |||
|
138 | }; | |||
|
139 | ||||
|
140 | this.subscribeToChannels = function (channels) { | |||
|
141 | var new_channels = []; | |||
|
142 | for (var i = 0; i < channels.length; i++) { | |||
|
143 | var channel = channels[i]; | |||
|
144 | if (self.state.channels.indexOf(channel)) { | |||
|
145 | self.state.channels.push(channel); | |||
|
146 | new_channels.push(channel) | |||
|
147 | } | |||
|
148 | } | |||
|
149 | /** | |||
|
150 | * only execute the request if socket is present because subscribe | |||
|
151 | * can actually add channels before initial app connection | |||
|
152 | **/ | |||
|
153 | if (new_channels && self.state.socket !== null) { | |||
|
154 | var connReq = { | |||
|
155 | 'channels': self.state.channels, | |||
|
156 | 'conn_id': self.state.connId | |||
|
157 | }; | |||
|
158 | $.ajax({ | |||
|
159 | url: self.state.urls.subscribe, | |||
|
160 | type: "POST", | |||
|
161 | contentType: "application/json", | |||
|
162 | data: JSON.stringify(connReq), | |||
|
163 | dataType: "json" | |||
|
164 | }).done(function (data) { | |||
|
165 | self.state.channels = data.channels; | |||
|
166 | self.state.channelsInfo = data.channels_info; | |||
|
167 | self.update(); | |||
|
168 | }); | |||
|
169 | } | |||
|
170 | self.update(); | |||
|
171 | }; | |||
|
172 | ||||
|
173 | this.update = function () { | |||
|
174 | for (var key in this.state.channelsInfo) { | |||
|
175 | if (this.state.channelsInfo.hasOwnProperty(key)) { | |||
|
176 | // update channels with latest info | |||
|
177 | $.Topic('/connection_controller/channel_update').publish( | |||
|
178 | {channel: key, state: this.state.channelsInfo[key]}); | |||
|
179 | } | |||
|
180 | } | |||
|
181 | /** | |||
|
182 | * checks current channel list in state and if channel is not present | |||
|
183 | * converts them into executable "commands" and pushes them on topics | |||
|
184 | */ | |||
|
185 | for (var i = 0; i < this.state.channels.length; i++) { | |||
|
186 | var channel = this.state.channels[i]; | |||
|
187 | for (var j = 0; j < this.channelNameParsers.length; j++) { | |||
|
188 | this.channelNameParsers[j](channel); | |||
|
189 | } | |||
|
190 | } | |||
|
191 | }; | |||
|
192 | ||||
|
193 | this.run = function () { | |||
|
194 | this.connect(true); | |||
|
195 | }; | |||
|
196 | ||||
|
197 | $.Topic('/connection_controller/subscribe').subscribe( | |||
|
198 | self.subscribeToChannels); | |||
|
199 | }; | |||
|
200 | ||||
|
201 | $.Topic('/plugins/__REGISTER__').subscribe(function (data) { | |||
|
202 | // enable chat controller | |||
|
203 | if (window.CHANNELSTREAM_SETTINGS && window.CHANNELSTREAM_SETTINGS.enabled) { | |||
|
204 | $(document).ready(function () { | |||
|
205 | connCtrlr.run(); | |||
|
206 | }); | |||
|
207 | } | |||
|
208 | }); | |||
|
209 | ||||
|
210 | registerViewChannels = function (){ | |||
|
211 | // subscribe to PR repo channel for PR's' | |||
|
212 | if (templateContext.pull_request_data.pull_request_id) { | |||
|
213 | var channelName = '/repo$' + templateContext.repo_name + '$/pr/' + | |||
|
214 | String(templateContext.pull_request_data.pull_request_id); | |||
|
215 | connCtrlr.state.channels.push(channelName); | |||
|
216 | } | |||
|
217 | } | |||
|
218 | ||||
|
219 | })(); |
@@ -0,0 +1,60 b'' | |||||
|
1 | "use strict"; | |||
|
2 | ||||
|
3 | toastr.options = { | |||
|
4 | "closeButton": true, | |||
|
5 | "debug": false, | |||
|
6 | "newestOnTop": false, | |||
|
7 | "progressBar": false, | |||
|
8 | "positionClass": "toast-top-center", | |||
|
9 | "preventDuplicates": false, | |||
|
10 | "onclick": null, | |||
|
11 | "showDuration": "300", | |||
|
12 | "hideDuration": "300", | |||
|
13 | "timeOut": "0", | |||
|
14 | "extendedTimeOut": "0", | |||
|
15 | "showEasing": "swing", | |||
|
16 | "hideEasing": "linear", | |||
|
17 | "showMethod": "fadeIn", | |||
|
18 | "hideMethod": "fadeOut" | |||
|
19 | }; | |||
|
20 | ||||
|
21 | function notifySystem(data) { | |||
|
22 | var notification = new Notification(data.message.level + ': ' + data.message.message); | |||
|
23 | }; | |||
|
24 | ||||
|
25 | function notifyToaster(data){ | |||
|
26 | toastr[data.message.level](data.message.message); | |||
|
27 | } | |||
|
28 | ||||
|
29 | function handleNotifications(data) { | |||
|
30 | ||||
|
31 | if (!templateContext.rhodecode_user.notification_status && !data.testMessage) { | |||
|
32 | // do not act if notifications are disabled | |||
|
33 | return | |||
|
34 | } | |||
|
35 | // use only js notifications for now | |||
|
36 | var onlyJS = true; | |||
|
37 | if (!("Notification" in window) || onlyJS) { | |||
|
38 | // use legacy notificartion | |||
|
39 | notifyToaster(data); | |||
|
40 | } | |||
|
41 | else { | |||
|
42 | // Let's check whether notification permissions have already been granted | |||
|
43 | if (Notification.permission === "granted") { | |||
|
44 | notifySystem(data); | |||
|
45 | } | |||
|
46 | // Otherwise, we need to ask the user for permission | |||
|
47 | else if (Notification.permission !== 'denied') { | |||
|
48 | Notification.requestPermission(function (permission) { | |||
|
49 | if (permission === "granted") { | |||
|
50 | notifySystem(data); | |||
|
51 | } | |||
|
52 | }); | |||
|
53 | } | |||
|
54 | else{ | |||
|
55 | notifyToaster(data); | |||
|
56 | } | |||
|
57 | } | |||
|
58 | }; | |||
|
59 | ||||
|
60 | $.Topic('/notifications').subscribe(handleNotifications); |
@@ -0,0 +1,24 b'' | |||||
|
1 | <script> | |||
|
2 | var CHANNELSTREAM_URLS = ${config['url_gen'](request)|n}; | |||
|
3 | %if request.registry.rhodecode_plugins['channelstream']['enabled'] and c.rhodecode_user.username != h.DEFAULT_USER: | |||
|
4 | var CHANNELSTREAM_SETTINGS = { | |||
|
5 | 'enabled': true, | |||
|
6 | 'ws_location': '${request.registry.settings.get('channelstream.ws_url')}', | |||
|
7 | 'webapp_location': '${h.url('/', qualified=True)[:-1]}' | |||
|
8 | }; | |||
|
9 | %else: | |||
|
10 | var CHANNELSTREAM_SETTINGS = { | |||
|
11 | 'enabled':false, | |||
|
12 | 'ws_location': '', | |||
|
13 | 'webapp_location': ''}; | |||
|
14 | %endif | |||
|
15 | ||||
|
16 | if (CHANNELSTREAM_SETTINGS.enabled) { | |||
|
17 | connCtrlr = new ConnectionController( | |||
|
18 | CHANNELSTREAM_SETTINGS.webapp_location, | |||
|
19 | CHANNELSTREAM_SETTINGS.ws_location, | |||
|
20 | CHANNELSTREAM_URLS | |||
|
21 | ); | |||
|
22 | registerViewChannels(); | |||
|
23 | } | |||
|
24 | </script> |
@@ -32,6 +32,7 b' module.exports = function(grunt) {' | |||||
32 | '<%= dirs.js.src %>/plugins/jquery.mark.js', |
|
32 | '<%= dirs.js.src %>/plugins/jquery.mark.js', | |
33 | '<%= dirs.js.src %>/plugins/jquery.timeago.js', |
|
33 | '<%= dirs.js.src %>/plugins/jquery.timeago.js', | |
34 | '<%= dirs.js.src %>/plugins/jquery.timeago-extension.js', |
|
34 | '<%= dirs.js.src %>/plugins/jquery.timeago-extension.js', | |
|
35 | '<%= dirs.js.src %>/plugins/toastr.js', | |||
35 |
|
36 | |||
36 | // Select2 |
|
37 | // Select2 | |
37 | '<%= dirs.js.src %>/select2/select2.js', |
|
38 | '<%= dirs.js.src %>/select2/select2.js', | |
@@ -64,6 +65,7 b' module.exports = function(grunt) {' | |||||
64 |
|
65 | |||
65 | // Rhodecode components |
|
66 | // Rhodecode components | |
66 | '<%= dirs.js.src %>/rhodecode/init.js', |
|
67 | '<%= dirs.js.src %>/rhodecode/init.js', | |
|
68 | '<%= dirs.js.src %>/rhodecode/connection_controller.js', | |||
67 | '<%= dirs.js.src %>/rhodecode/codemirror.js', |
|
69 | '<%= dirs.js.src %>/rhodecode/codemirror.js', | |
68 | '<%= dirs.js.src %>/rhodecode/comments.js', |
|
70 | '<%= dirs.js.src %>/rhodecode/comments.js', | |
69 | '<%= dirs.js.src %>/rhodecode/constants.js', |
|
71 | '<%= dirs.js.src %>/rhodecode/constants.js', | |
@@ -78,6 +80,7 b' module.exports = function(grunt) {' | |||||
78 | '<%= dirs.js.src %>/rhodecode/select2_widgets.js', |
|
80 | '<%= dirs.js.src %>/rhodecode/select2_widgets.js', | |
79 | '<%= dirs.js.src %>/rhodecode/tooltips.js', |
|
81 | '<%= dirs.js.src %>/rhodecode/tooltips.js', | |
80 | '<%= dirs.js.src %>/rhodecode/users.js', |
|
82 | '<%= dirs.js.src %>/rhodecode/users.js', | |
|
83 | '<%= dirs.js.src %>/rhodecode/utils/notifications.js', | |||
81 | '<%= dirs.js.src %>/rhodecode/appenlight.js', |
|
84 | '<%= dirs.js.src %>/rhodecode/appenlight.js', | |
82 |
|
85 | |||
83 | // Rhodecode main module |
|
86 | // Rhodecode main module |
@@ -391,6 +391,17 b' beaker.session.auto = false' | |||||
391 | search.module = rhodecode.lib.index.whoosh |
|
391 | search.module = rhodecode.lib.index.whoosh | |
392 | search.location = %(here)s/data/index |
|
392 | search.location = %(here)s/data/index | |
393 |
|
393 | |||
|
394 | ######################################## | |||
|
395 | ### CHANNELSTREAM CONFIG #### | |||
|
396 | ######################################## | |||
|
397 | ||||
|
398 | channelstream.enabled = true | |||
|
399 | # location of channelstream server on the backend | |||
|
400 | channelstream.server = 127.0.0.1:9800 | |||
|
401 | # location of the channelstream server from outside world | |||
|
402 | channelstream.ws_url = ws://127.0.0.1:9800 | |||
|
403 | channelstream.secret = secret | |||
|
404 | ||||
394 | ################################### |
|
405 | ################################### | |
395 | ## APPENLIGHT CONFIG ## |
|
406 | ## APPENLIGHT CONFIG ## | |
396 | ################################### |
|
407 | ################################### |
@@ -365,6 +365,17 b' beaker.session.auto = false' | |||||
365 | search.module = rhodecode.lib.index.whoosh |
|
365 | search.module = rhodecode.lib.index.whoosh | |
366 | search.location = %(here)s/data/index |
|
366 | search.location = %(here)s/data/index | |
367 |
|
367 | |||
|
368 | ######################################## | |||
|
369 | ### CHANNELSTREAM CONFIG #### | |||
|
370 | ######################################## | |||
|
371 | ||||
|
372 | channelstream.enabled = true | |||
|
373 | # location of channelstream server on the backend | |||
|
374 | channelstream.server = 127.0.0.1:9800 | |||
|
375 | # location of the channelstream server from outside world | |||
|
376 | channelstream.ws_url = ws://127.0.0.1:9800 | |||
|
377 | channelstream.secret = secret | |||
|
378 | ||||
368 | ################################### |
|
379 | ################################### | |
369 | ## APPENLIGHT CONFIG ## |
|
380 | ## APPENLIGHT CONFIG ## | |
370 | ################################### |
|
381 | ################################### |
@@ -166,6 +166,7 b' let' | |||||
166 | ln -s ${self.supervisor}/bin/supervisor* $out/bin/ |
|
166 | ln -s ${self.supervisor}/bin/supervisor* $out/bin/ | |
167 | ln -s ${self.gunicorn}/bin/gunicorn $out/bin/ |
|
167 | ln -s ${self.gunicorn}/bin/gunicorn $out/bin/ | |
168 | ln -s ${self.PasteScript}/bin/paster $out/bin/ |
|
168 | ln -s ${self.PasteScript}/bin/paster $out/bin/ | |
|
169 | ln -s ${self.channelstream}/bin/channelstream $out/bin/ | |||
169 | ln -s ${self.pyramid}/bin/* $out/bin/ #*/ |
|
170 | ln -s ${self.pyramid}/bin/* $out/bin/ #*/ | |
170 |
|
171 | |||
171 | # rhodecode-tools |
|
172 | # rhodecode-tools |
@@ -112,6 +112,13 b' def load_environment(global_conf, app_co' | |||||
112 | # sets the c attribute access when don't existing attribute are accessed |
|
112 | # sets the c attribute access when don't existing attribute are accessed | |
113 | config['pylons.strict_tmpl_context'] = True |
|
113 | config['pylons.strict_tmpl_context'] = True | |
114 |
|
114 | |||
|
115 | # configure channelstream | |||
|
116 | config['channelstream_config'] = { | |||
|
117 | 'enabled': asbool(config.get('channelstream.enabled', False)), | |||
|
118 | 'server': config.get('channelstream.server'), | |||
|
119 | 'secret': config.get('channelstream.secret') | |||
|
120 | } | |||
|
121 | ||||
115 | # Limit backends to "vcs.backends" from configuration |
|
122 | # Limit backends to "vcs.backends" from configuration | |
116 | backends = config['vcs.backends'] = aslist( |
|
123 | backends = config['vcs.backends'] = aslist( | |
117 | config.get('vcs.backends', 'hg,git'), sep=',') |
|
124 | config.get('vcs.backends', 'hg,git'), sep=',') |
@@ -22,6 +22,7 b'' | |||||
22 | Pylons middleware initialization |
|
22 | Pylons middleware initialization | |
23 | """ |
|
23 | """ | |
24 | import logging |
|
24 | import logging | |
|
25 | from collections import OrderedDict | |||
25 |
|
26 | |||
26 | from paste.registry import RegistryManager |
|
27 | from paste.registry import RegistryManager | |
27 | from paste.gzipper import make_gzip_middleware |
|
28 | from paste.gzipper import make_gzip_middleware | |
@@ -68,6 +69,12 b' class SkippableRoutesMiddleware(RoutesMi' | |||||
68 | def __call__(self, environ, start_response): |
|
69 | def __call__(self, environ, start_response): | |
69 | for prefix in self.skip_prefixes: |
|
70 | for prefix in self.skip_prefixes: | |
70 | if environ['PATH_INFO'].startswith(prefix): |
|
71 | if environ['PATH_INFO'].startswith(prefix): | |
|
72 | # added to avoid the case when a missing /_static route falls | |||
|
73 | # through to pylons and causes an exception as pylons is | |||
|
74 | # expecting wsgiorg.routingargs to be set in the environ | |||
|
75 | # by RoutesMiddleware. | |||
|
76 | if 'wsgiorg.routing_args' not in environ: | |||
|
77 | environ['wsgiorg.routing_args'] = (None, {}) | |||
71 | return self.app(environ, start_response) |
|
78 | return self.app(environ, start_response) | |
72 |
|
79 | |||
73 | return super(SkippableRoutesMiddleware, self).__call__( |
|
80 | return super(SkippableRoutesMiddleware, self).__call__( | |
@@ -228,7 +235,7 b' def includeme(config):' | |||||
228 | settings = config.registry.settings |
|
235 | settings = config.registry.settings | |
229 |
|
236 | |||
230 | # plugin information |
|
237 | # plugin information | |
231 |
config.registry.rhodecode_plugins = |
|
238 | config.registry.rhodecode_plugins = OrderedDict() | |
232 |
|
239 | |||
233 | config.add_directive( |
|
240 | config.add_directive( | |
234 | 'register_rhodecode_plugin', register_rhodecode_plugin) |
|
241 | 'register_rhodecode_plugin', register_rhodecode_plugin) | |
@@ -239,6 +246,7 b' def includeme(config):' | |||||
239 | # Includes which are required. The application would fail without them. |
|
246 | # Includes which are required. The application would fail without them. | |
240 | config.include('pyramid_mako') |
|
247 | config.include('pyramid_mako') | |
241 | config.include('pyramid_beaker') |
|
248 | config.include('pyramid_beaker') | |
|
249 | config.include('rhodecode.channelstream') | |||
242 | config.include('rhodecode.admin') |
|
250 | config.include('rhodecode.admin') | |
243 | config.include('rhodecode.authentication') |
|
251 | config.include('rhodecode.authentication') | |
244 | config.include('rhodecode.integrations') |
|
252 | config.include('rhodecode.integrations') |
@@ -560,6 +560,13 b' def make_map(config):' | |||||
560 | action='my_account_auth_tokens_add', conditions={'method': ['POST']}) |
|
560 | action='my_account_auth_tokens_add', conditions={'method': ['POST']}) | |
561 | m.connect('my_account_auth_tokens', '/my_account/auth_tokens', |
|
561 | m.connect('my_account_auth_tokens', '/my_account/auth_tokens', | |
562 | action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']}) |
|
562 | action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']}) | |
|
563 | m.connect('my_account_notifications', '/my_account/notifications', | |||
|
564 | action='my_notifications', | |||
|
565 | conditions={'method': ['GET']}) | |||
|
566 | m.connect('my_account_notifications_toggle_visibility', | |||
|
567 | '/my_account/toggle_visibility', | |||
|
568 | action='my_notifications_toggle_visibility', | |||
|
569 | conditions={'method': ['POST']}) | |||
563 |
|
570 | |||
564 | # NOTIFICATION REST ROUTES |
|
571 | # NOTIFICATION REST ROUTES | |
565 | with rmap.submapper(path_prefix=ADMIN_PREFIX, |
|
572 | with rmap.submapper(path_prefix=ADMIN_PREFIX, | |
@@ -568,7 +575,6 b' def make_map(config):' | |||||
568 | action='index', conditions={'method': ['GET']}) |
|
575 | action='index', conditions={'method': ['GET']}) | |
569 | m.connect('notifications_mark_all_read', '/notifications/mark_all_read', |
|
576 | m.connect('notifications_mark_all_read', '/notifications/mark_all_read', | |
570 | action='mark_all_read', conditions={'method': ['POST']}) |
|
577 | action='mark_all_read', conditions={'method': ['POST']}) | |
571 |
|
||||
572 | m.connect('/notifications/{notification_id}', |
|
578 | m.connect('/notifications/{notification_id}', | |
573 | action='update', conditions={'method': ['PUT']}) |
|
579 | action='update', conditions={'method': ['PUT']}) | |
574 | m.connect('/notifications/{notification_id}', |
|
580 | m.connect('/notifications/{notification_id}', |
@@ -346,3 +346,17 b' class MyAccountController(BaseController' | |||||
346 | h.flash(_("Auth token successfully deleted"), category='success') |
|
346 | h.flash(_("Auth token successfully deleted"), category='success') | |
347 |
|
347 | |||
348 | return redirect(url('my_account_auth_tokens')) |
|
348 | return redirect(url('my_account_auth_tokens')) | |
|
349 | ||||
|
350 | def my_notifications(self): | |||
|
351 | c.active = 'notifications' | |||
|
352 | return render('admin/my_account/my_account.html') | |||
|
353 | ||||
|
354 | @auth.CSRFRequired() | |||
|
355 | def my_notifications_toggle_visibility(self): | |||
|
356 | user = c.rhodecode_user.get_instance() | |||
|
357 | user_data = user.user_data | |||
|
358 | status = user_data.get('notification_status', False) | |||
|
359 | user_data['notification_status'] = not status | |||
|
360 | user.user_data = user_data | |||
|
361 | Session().commit() | |||
|
362 | return redirect(url('my_account_notifications')) |
@@ -86,6 +86,7 b' class NotificationsController(BaseContro' | |||||
86 |
|
86 | |||
87 | return render('admin/notifications/notifications.html') |
|
87 | return render('admin/notifications/notifications.html') | |
88 |
|
88 | |||
|
89 | ||||
89 | @auth.CSRFRequired() |
|
90 | @auth.CSRFRequired() | |
90 | def mark_all_read(self): |
|
91 | def mark_all_read(self): | |
91 | if request.is_xhr: |
|
92 | if request.is_xhr: |
@@ -332,6 +332,7 b' def attach_context_attributes(context, r' | |||||
332 | 'rhodecode_user': { |
|
332 | 'rhodecode_user': { | |
333 | 'username': None, |
|
333 | 'username': None, | |
334 | 'email': None, |
|
334 | 'email': None, | |
|
335 | 'notification_status': False | |||
335 | }, |
|
336 | }, | |
336 | 'visual': { |
|
337 | 'visual': { | |
337 | 'default_renderer': None |
|
338 | 'default_renderer': None |
@@ -26,10 +26,15 b' import logging' | |||||
26 | import traceback |
|
26 | import traceback | |
27 | import collections |
|
27 | import collections | |
28 |
|
28 | |||
|
29 | from datetime import datetime | |||
|
30 | ||||
|
31 | from pylons.i18n.translation import _ | |||
|
32 | from pyramid.threadlocal import get_current_registry | |||
29 | from sqlalchemy.sql.expression import null |
|
33 | from sqlalchemy.sql.expression import null | |
30 | from sqlalchemy.sql.functions import coalesce |
|
34 | from sqlalchemy.sql.functions import coalesce | |
31 |
|
35 | |||
32 | from rhodecode.lib import helpers as h, diffs |
|
36 | from rhodecode.lib import helpers as h, diffs | |
|
37 | from rhodecode.lib.channelstream import channelstream_request | |||
33 | from rhodecode.lib.utils import action_logger |
|
38 | from rhodecode.lib.utils import action_logger | |
34 | from rhodecode.lib.utils2 import extract_mentioned_users |
|
39 | from rhodecode.lib.utils2 import extract_mentioned_users | |
35 | from rhodecode.model import BaseModel |
|
40 | from rhodecode.model import BaseModel | |
@@ -134,84 +139,82 b' class ChangesetCommentsModel(BaseModel):' | |||||
134 |
|
139 | |||
135 | Session().add(comment) |
|
140 | Session().add(comment) | |
136 | Session().flush() |
|
141 | Session().flush() | |
137 |
|
142 | kwargs = { | ||
138 | if send_email: |
|
143 | 'user': user, | |
139 | kwargs = { |
|
144 | 'renderer_type': renderer, | |
140 | 'user': user, |
|
145 | 'repo_name': repo.repo_name, | |
141 | 'renderer_type': renderer, |
|
146 | 'status_change': status_change, | |
142 | 'repo_name': repo.repo_name, |
|
147 | 'comment_body': text, | |
143 | 'status_change': status_change, |
|
148 | 'comment_file': f_path, | |
144 |
|
|
149 | 'comment_line': line_no, | |
145 | 'comment_file': f_path, |
|
150 | } | |
146 | 'comment_line': line_no, |
|
|||
147 | } |
|
|||
148 |
|
151 | |||
149 |
|
|
152 | if commit_obj: | |
150 |
|
|
153 | recipients = ChangesetComment.get_users( | |
151 |
|
|
154 | revision=commit_obj.raw_id) | |
152 |
|
|
155 | # add commit author if it's in RhodeCode system | |
153 |
|
|
156 | cs_author = User.get_from_cs_author(commit_obj.author) | |
154 |
|
|
157 | if not cs_author: | |
155 |
|
|
158 | # use repo owner if we cannot extract the author correctly | |
156 |
|
|
159 | cs_author = repo.user | |
157 |
|
|
160 | recipients += [cs_author] | |
158 |
|
161 | |||
159 |
|
|
162 | commit_comment_url = self.get_url(comment) | |
160 |
|
163 | |||
161 |
|
|
164 | target_repo_url = h.link_to( | |
162 |
|
|
165 | repo.repo_name, | |
163 |
|
|
166 | h.url('summary_home', | |
164 |
|
|
167 | repo_name=repo.repo_name, qualified=True)) | |
165 |
|
168 | |||
166 |
|
|
169 | # commit specifics | |
167 |
|
|
170 | kwargs.update({ | |
168 |
|
|
171 | 'commit': commit_obj, | |
169 |
|
|
172 | 'commit_message': commit_obj.message, | |
170 |
|
|
173 | 'commit_target_repo': target_repo_url, | |
171 |
|
|
174 | 'commit_comment_url': commit_comment_url, | |
172 |
|
|
175 | }) | |
173 |
|
176 | |||
174 |
|
|
177 | elif pull_request_obj: | |
175 |
|
|
178 | # get the current participants of this pull request | |
176 |
|
|
179 | recipients = ChangesetComment.get_users( | |
177 |
|
|
180 | pull_request_id=pull_request_obj.pull_request_id) | |
178 |
|
|
181 | # add pull request author | |
179 |
|
|
182 | recipients += [pull_request_obj.author] | |
180 |
|
183 | |||
181 |
|
|
184 | # add the reviewers to notification | |
182 |
|
|
185 | recipients += [x.user for x in pull_request_obj.reviewers] | |
183 |
|
186 | |||
184 |
|
|
187 | pr_target_repo = pull_request_obj.target_repo | |
185 |
|
|
188 | pr_source_repo = pull_request_obj.source_repo | |
186 |
|
189 | |||
187 |
|
|
190 | pr_comment_url = h.url( | |
188 |
|
|
191 | 'pullrequest_show', | |
189 |
|
|
192 | repo_name=pr_target_repo.repo_name, | |
190 |
|
|
193 | pull_request_id=pull_request_obj.pull_request_id, | |
191 |
|
|
194 | anchor='comment-%s' % comment.comment_id, | |
192 |
|
|
195 | qualified=True,) | |
193 |
|
196 | |||
194 |
|
|
197 | # set some variables for email notification | |
195 |
|
|
198 | pr_target_repo_url = h.url( | |
196 |
|
|
199 | 'summary_home', repo_name=pr_target_repo.repo_name, | |
197 |
|
|
200 | qualified=True) | |
198 |
|
201 | |||
199 |
|
|
202 | pr_source_repo_url = h.url( | |
200 |
|
|
203 | 'summary_home', repo_name=pr_source_repo.repo_name, | |
201 |
|
|
204 | qualified=True) | |
202 |
|
205 | |||
203 |
|
|
206 | # pull request specifics | |
204 |
|
|
207 | kwargs.update({ | |
205 |
|
|
208 | 'pull_request': pull_request_obj, | |
206 |
|
|
209 | 'pr_id': pull_request_obj.pull_request_id, | |
207 |
|
|
210 | 'pr_target_repo': pr_target_repo, | |
208 |
|
|
211 | 'pr_target_repo_url': pr_target_repo_url, | |
209 |
|
|
212 | 'pr_source_repo': pr_source_repo, | |
210 |
|
|
213 | 'pr_source_repo_url': pr_source_repo_url, | |
211 |
|
|
214 | 'pr_comment_url': pr_comment_url, | |
212 |
|
|
215 | 'pr_closing': closing_pr, | |
213 |
|
|
216 | }) | |
214 |
|
217 | if send_email: | ||
215 | # pre-generate the subject for notification itself |
|
218 | # pre-generate the subject for notification itself | |
216 | (subject, |
|
219 | (subject, | |
217 | _h, _e, # we don't care about those |
|
220 | _h, _e, # we don't care about those | |
@@ -240,6 +243,44 b' class ChangesetCommentsModel(BaseModel):' | |||||
240 | ) |
|
243 | ) | |
241 | action_logger(user, action, comment.repo) |
|
244 | action_logger(user, action, comment.repo) | |
242 |
|
245 | |||
|
246 | registry = get_current_registry() | |||
|
247 | rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {}) | |||
|
248 | channelstream_config = rhodecode_plugins.get('channelstream') | |||
|
249 | msg_url = '' | |||
|
250 | if commit_obj: | |||
|
251 | msg_url = commit_comment_url | |||
|
252 | repo_name = repo.repo_name | |||
|
253 | elif pull_request_obj: | |||
|
254 | msg_url = pr_comment_url | |||
|
255 | repo_name = pr_target_repo.repo_name | |||
|
256 | ||||
|
257 | if channelstream_config: | |||
|
258 | message = '<strong>{}</strong> {} - ' \ | |||
|
259 | '<a onclick="window.location=\'{}\';' \ | |||
|
260 | 'window.location.reload()">' \ | |||
|
261 | '<strong>{}</strong></a>' | |||
|
262 | message = message.format( | |||
|
263 | user.username, _('made a comment'), msg_url, | |||
|
264 | _('Refresh page')) | |||
|
265 | if channelstream_config: | |||
|
266 | channel = '/repo${}$/pr/{}'.format( | |||
|
267 | repo_name, | |||
|
268 | pull_request_id | |||
|
269 | ) | |||
|
270 | payload = { | |||
|
271 | 'type': 'message', | |||
|
272 | 'timestamp': datetime.utcnow(), | |||
|
273 | 'user': 'system', | |||
|
274 | 'channel': channel, | |||
|
275 | 'message': { | |||
|
276 | 'message': message, | |||
|
277 | 'level': 'info', | |||
|
278 | 'topic': '/notifications' | |||
|
279 | } | |||
|
280 | } | |||
|
281 | channelstream_request(channelstream_config, [payload], | |||
|
282 | '/message', raise_exc=False) | |||
|
283 | ||||
243 | return comment |
|
284 | return comment | |
244 |
|
285 | |||
245 | def delete(self, comment): |
|
286 | def delete(self, comment): |
@@ -25,6 +25,7 b'' | |||||
25 | @import 'comments'; |
|
25 | @import 'comments'; | |
26 | @import 'panels-bootstrap'; |
|
26 | @import 'panels-bootstrap'; | |
27 | @import 'panels'; |
|
27 | @import 'panels'; | |
|
28 | @import 'toastr'; | |||
28 | @import 'deform'; |
|
29 | @import 'deform'; | |
29 |
|
30 | |||
30 |
|
31 | |||
@@ -1615,6 +1616,10 b' BIN_FILENODE = 7' | |||||
1615 | float: right; |
|
1616 | float: right; | |
1616 | } |
|
1617 | } | |
1617 |
|
1618 | |||
|
1619 | #notification-status{ | |||
|
1620 | display: inline; | |||
|
1621 | } | |||
|
1622 | ||||
1618 | // Repositories |
|
1623 | // Repositories | |
1619 |
|
1624 | |||
1620 | #summary.fields{ |
|
1625 | #summary.fields{ |
@@ -1,3 +1,4 b'' | |||||
1 | /plugins/__REGISTER__ - launched after the onDomReady() code from rhodecode.js is executed |
|
1 | /plugins/__REGISTER__ - launched after the onDomReady() code from rhodecode.js is executed | |
2 | /ui/plugins/code/anchor_focus - launched when rc starts to scroll on load to anchor on PR/Codeview |
|
2 | /ui/plugins/code/anchor_focus - launched when rc starts to scroll on load to anchor on PR/Codeview | |
3 | /ui/plugins/code/comment_form_built - launched when injectInlineForm() is executed and the form object is created |
|
3 | /ui/plugins/code/comment_form_built - launched when injectInlineForm() is executed and the form object is created | |
|
4 | /notifications - shows new event notifications No newline at end of file |
@@ -39,6 +39,7 b'' | |||||
39 | <li class="${'active' if c.active=='watched' else ''}"><a href="${h.url('my_account_watched')}">${_('Watched')}</a></li> |
|
39 | <li class="${'active' if c.active=='watched' else ''}"><a href="${h.url('my_account_watched')}">${_('Watched')}</a></li> | |
40 | <li class="${'active' if c.active=='pullrequests' else ''}"><a href="${h.url('my_account_pullrequests')}">${_('Pull Requests')}</a></li> |
|
40 | <li class="${'active' if c.active=='pullrequests' else ''}"><a href="${h.url('my_account_pullrequests')}">${_('Pull Requests')}</a></li> | |
41 | <li class="${'active' if c.active=='perms' else ''}"><a href="${h.url('my_account_perms')}">${_('My Permissions')}</a></li> |
|
41 | <li class="${'active' if c.active=='perms' else ''}"><a href="${h.url('my_account_perms')}">${_('My Permissions')}</a></li> | |
|
42 | <li class="${'active' if c.active=='my_notifications' else ''}"><a href="${h.url('my_account_notifications')}">${_('My Live Notifications')}</a></li> | |||
42 | </ul> |
|
43 | </ul> | |
43 | </div> |
|
44 | </div> | |
44 |
|
45 |
@@ -1,72 +1,56 b'' | |||||
1 | <%namespace name="base" file="/base/base.html"/> |
|
|||
2 |
|
||||
3 |
|
|
1 | <div class="panel panel-default"> | |
4 | <div class="panel-heading"> |
|
2 | <div class="panel-heading"> | |
5 |
<h3 class="panel-title">${_(' |
|
3 | <h3 class="panel-title">${_('Your live notification settings')}</h3> | |
6 | </div> |
|
4 | </div> | |
7 |
|
5 | |||
8 | <div class="panel-body"> |
|
6 | <div class="panel-body"> | |
9 | <div class="emails_wrap"> |
|
7 | ||
10 | <table class="rctable account_emails"> |
|
8 | <p><strong>IMPORTANT:</strong> This feature requires enabled channelstream websocket server to function correctly.</p> | |
11 | <tr> |
|
9 | ||
12 | <td class="td-user"> |
|
10 | <p class="hidden">Status of browser notifications permission: <strong id="browser-notification-status"></strong></p> | |
13 | ${base.gravatar(c.user.email, 16)} |
|
|||
14 | <span class="user email">${c.user.email}</span> |
|
|||
15 | </td> |
|
|||
16 | <td class="td-tags"> |
|
|||
17 | <span class="tag tag1">${_('Primary')}</span> |
|
|||
18 | </td> |
|
|||
19 | </tr> |
|
|||
20 | %if c.user_email_map: |
|
|||
21 | %for em in c.user_email_map: |
|
|||
22 | <tr> |
|
|||
23 | <td class="td-user"> |
|
|||
24 | ${base.gravatar(em.email, 16)} |
|
|||
25 | <span class="user email">${em.email}</span> |
|
|||
26 | </td> |
|
|||
27 | <td class="td-action"> |
|
|||
28 | ${h.secure_form(url('my_account_emails'),method='delete')} |
|
|||
29 | ${h.hidden('del_email_id',em.email_id)} |
|
|||
30 | <button class="btn btn-link btn-danger" type="submit" id="remove_email_%s" % em.email_id |
|
|||
31 | onclick="return confirm('${_('Confirm to delete this email: %s') % em.email}');"> |
|
|||
32 | ${_('Delete')} |
|
|||
33 | </button> |
|
|||
34 | ${h.end_form()} |
|
|||
35 | </td> |
|
|||
36 | </tr> |
|
|||
37 | %endfor |
|
|||
38 | %else: |
|
|||
39 | <tr class="noborder"> |
|
|||
40 | <td colspan="3"> |
|
|||
41 | <div class="td-email"> |
|
|||
42 | ${_('No additional emails specified')} |
|
|||
43 | </div> |
|
|||
44 | </td> |
|
|||
45 | </tr> |
|
|||
46 | %endif |
|
|||
47 | </table> |
|
|||
48 | </div> |
|
|||
49 |
|
11 | |||
50 | <div> |
|
12 | ${h.secure_form(url('my_account_notifications_toggle_visibility'), method='post', id='notification-status')} | |
51 | ${h.secure_form(url('my_account_emails'), method='post')} |
|
13 | <button class="btn btn-default" type="submit"> | |
52 | <div class="form"> |
|
14 | ${_('Notifications')} <strong>${_('Enabled') if c.rhodecode_user.get_instance().user_data.get('notification_status') else _('Disabled')}</strong> | |
53 | <!-- fields --> |
|
15 | </button> | |
54 | <div class="fields"> |
|
|||
55 | <div class="field"> |
|
|||
56 | <div class="label"> |
|
|||
57 | <label for="new_email">${_('New email address')}:</label> |
|
|||
58 | </div> |
|
|||
59 | <div class="input"> |
|
|||
60 | ${h.text('new_email', class_='medium')} |
|
|||
61 | </div> |
|
|||
62 | </div> |
|
|||
63 | <div class="buttons"> |
|
|||
64 | ${h.submit('save',_('Add'),class_="btn")} |
|
|||
65 | ${h.reset('reset',_('Reset'),class_="btn")} |
|
|||
66 | </div> |
|
|||
67 | </div> |
|
|||
68 | </div> |
|
|||
69 | ${h.end_form()} |
|
16 | ${h.end_form()} | |
70 | </div> |
|
17 | ||
|
18 | <a class="btn btn-info" id="test-notification">Test notification</a> | |||
|
19 | ||||
71 | </div> |
|
20 | </div> | |
72 | </div> |
|
21 | </div> | |
|
22 | ||||
|
23 | <script type="application/javascript"> | |||
|
24 | ||||
|
25 | function checkBrowserStatus(){ | |||
|
26 | var browserStatus = 'Unknown'; | |||
|
27 | ||||
|
28 | if (!("Notification" in window)) { | |||
|
29 | browserStatus = 'Not supported' | |||
|
30 | } | |||
|
31 | else if(Notification.permission === 'denied'){ | |||
|
32 | browserStatus = 'Denied'; | |||
|
33 | $('.flash_msg').append('<div class="alert alert-error">Notifications are blocked on browser level - you need to enable them in your browser settings.</div>') | |||
|
34 | } | |||
|
35 | else if(Notification.permission === 'granted'){ | |||
|
36 | browserStatus = 'Allowed'; | |||
|
37 | } | |||
|
38 | ||||
|
39 | $('#browser-notification-status').text(browserStatus); | |||
|
40 | }; | |||
|
41 | ||||
|
42 | checkBrowserStatus(); | |||
|
43 | ||||
|
44 | $('#test-notification').on('click', function(e){ | |||
|
45 | var levels = ['info', 'error', 'warning', 'success']; | |||
|
46 | var level = levels[Math.floor(Math.random()*levels.length)]; | |||
|
47 | var payload = { | |||
|
48 | message: { | |||
|
49 | message: 'This is a test notification.', | |||
|
50 | level: level, | |||
|
51 | testMessage: true | |||
|
52 | } | |||
|
53 | }; | |||
|
54 | $.Topic('/notifications').publish(payload); | |||
|
55 | }) | |||
|
56 | </script> |
@@ -3,7 +3,7 b' from pyramid.renderers import render as ' | |||||
3 | from pyramid.threadlocal import get_current_registry, get_current_request |
|
3 | from pyramid.threadlocal import get_current_registry, get_current_request | |
4 | pyramid_registry = get_current_registry() |
|
4 | pyramid_registry = get_current_registry() | |
5 | %> |
|
5 | %> | |
6 |
% for plugin, config in pyramid_registry |
|
6 | % for plugin, config in getattr(pyramid_registry, 'rhodecode_plugins', {}).items(): | |
7 | % if config['template_hooks'].get('plugin_init_template'): |
|
7 | % if config['template_hooks'].get('plugin_init_template'): | |
8 | ${pyramid_render(config['template_hooks'].get('plugin_init_template'), |
|
8 | ${pyramid_render(config['template_hooks'].get('plugin_init_template'), | |
9 | {'config':config}, request=get_current_request(), package='rc_ae')|n} |
|
9 | {'config':config}, request=get_current_request(), package='rc_ae')|n} |
@@ -11,6 +11,7 b" if hasattr(c, 'rhodecode_db_repo'):" | |||||
11 | if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id: |
|
11 | if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id: | |
12 | c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username |
|
12 | c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username | |
13 | c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email |
|
13 | c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email | |
|
14 | c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True) | |||
14 |
|
15 | |||
15 | c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer') |
|
16 | c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer') | |
16 | %> |
|
17 | %> | |
@@ -77,7 +78,6 b" c.template_context['visual']['default_re" | |||||
77 | } |
|
78 | } | |
78 | }; |
|
79 | }; | |
79 | </script> |
|
80 | </script> | |
80 |
|
||||
81 | <!--[if lt IE 9]> |
|
81 | <!--[if lt IE 9]> | |
82 | <script language="javascript" type="text/javascript" src="${h.asset('js/excanvas.min.js')}"></script> |
|
82 | <script language="javascript" type="text/javascript" src="${h.asset('js/excanvas.min.js')}"></script> | |
83 | <![endif]--> |
|
83 | <![endif]--> | |
@@ -105,7 +105,6 b" c.template_context['visual']['default_re" | |||||
105 |
|
105 | |||
106 | <%def name="head_extra()"></%def> |
|
106 | <%def name="head_extra()"></%def> | |
107 | ${self.head_extra()} |
|
107 | ${self.head_extra()} | |
108 |
|
||||
109 | <%include file="/base/plugins_base.html"/> |
|
108 | <%include file="/base/plugins_base.html"/> | |
110 |
|
109 | |||
111 | ## extra stuff |
|
110 | ## extra stuff |
General Comments 0
You need to be logged in to leave comments.
Login now