From e9f425c9920e67af14ca0a30857c7d9fd2326cff Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Fri, 12 Aug 2016 12:31:44 +0000 Subject: [PATCH 001/125] release: Bump version 4.3.0 to 4.4.0 --- .bumpversion.cfg | 2 +- rhodecode/VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 647990df..aac41fcb 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.3.0 +current_version = 4.4.0 message = release: Bump version {current_version} to {new_version} [bumpversion:file:rhodecode/VERSION] diff --git a/rhodecode/VERSION b/rhodecode/VERSION index 81911389..64b5ae39 100644 --- a/rhodecode/VERSION +++ b/rhodecode/VERSION @@ -1 +1 @@ -4.3.0 \ No newline at end of file +4.4.0 \ No newline at end of file From c9537b89c182cf3e0a539b9f9215ae4d46317b80 Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Fri, 12 Aug 2016 15:53:23 +0200 Subject: [PATCH 002/125] configs: disable channelstream by default --- configs/development.ini | 2 +- configs/production.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/configs/development.ini b/configs/development.ini index e1d33cd9..25f4903f 100644 --- a/configs/development.ini +++ b/configs/development.ini @@ -414,7 +414,7 @@ search.location = %(here)s/data/index ## channelstream enables persistent connections and live notification ## in the system. It's also used by the chat system -channelstream.enabled = true +channelstream.enabled = false ## location of channelstream server on the backend channelstream.server = 127.0.0.1:9800 ## location of the channelstream server from outside world diff --git a/configs/production.ini b/configs/production.ini index 59b8a8c4..82a028ff 100644 --- a/configs/production.ini +++ b/configs/production.ini @@ -388,7 +388,7 @@ search.location = %(here)s/data/index ## channelstream enables persistent connections and live notification ## in the system. It's also used by the chat system -channelstream.enabled = true +channelstream.enabled = false ## location of channelstream server on the backend channelstream.server = 127.0.0.1:9800 ## location of the channelstream server from outside world From b51406ec1db058b73308656240ed34bfd90d0f13 Mon Sep 17 00:00:00 2001 From: Daniel Dourvaris Date: Fri, 5 Aug 2016 06:55:35 +0300 Subject: [PATCH 003/125] account: convert change password form to colander schema and fix bug where user could use same password as before when changing password, fixes #2264 --- rhodecode/config/routing.py | 4 +- rhodecode/controllers/admin/my_account.py | 69 ++++++++++-------- rhodecode/forms/__init__.py | 33 +++++++++ rhodecode/lib/auth.py | 7 +- rhodecode/model/forms.py | 14 ---- rhodecode/model/user.py | 1 - .../validation_schema/schemas/user_schema.py | 61 ++++++++++++++++ .../model/validation_schema/validators.py | 4 - rhodecode/public/css/deform.less | 19 ++++- .../admin/my_account/my_account_password.html | 45 +----------- rhodecode/templates/base/root.html | 1 - rhodecode/templates/widgets.html | 10 +++ .../tests/functional/test_admin_my_account.py | 73 ++++++++++++++++--- .../tests/models/schemas/test_user_schema.py | 72 ++++++++++++++++++ 14 files changed, 306 insertions(+), 107 deletions(-) create mode 100644 rhodecode/forms/__init__.py create mode 100644 rhodecode/model/validation_schema/schemas/user_schema.py create mode 100644 rhodecode/templates/widgets.html create mode 100644 rhodecode/tests/models/schemas/test_user_schema.py diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py index 1f587845..9cc26c86 100644 --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -531,9 +531,7 @@ def make_map(config): action='my_account_update', conditions={'method': ['POST']}) m.connect('my_account_password', '/my_account/password', - action='my_account_password', conditions={'method': ['GET']}) - m.connect('my_account_password', '/my_account/password', - action='my_account_password_update', conditions={'method': ['POST']}) + action='my_account_password', conditions={'method': ['GET', 'POST']}) m.connect('my_account_repos', '/my_account/repos', action='my_account_repos', conditions={'method': ['GET']}) diff --git a/rhodecode/controllers/admin/my_account.py b/rhodecode/controllers/admin/my_account.py index 11d8b842..9d9c9f14 100644 --- a/rhodecode/controllers/admin/my_account.py +++ b/rhodecode/controllers/admin/my_account.py @@ -32,6 +32,7 @@ from pylons.controllers.util import redirect from pylons.i18n.translation import _ from sqlalchemy.orm import joinedload +from rhodecode import forms from rhodecode.lib import helpers as h from rhodecode.lib import auth from rhodecode.lib.auth import ( @@ -39,10 +40,12 @@ from rhodecode.lib.auth import ( from rhodecode.lib.base import BaseController, render from rhodecode.lib.utils2 import safe_int, md5 from rhodecode.lib.ext_json import json + +from rhodecode.model.validation_schema.schemas import user_schema from rhodecode.model.db import ( Repository, PullRequest, PullRequestReviewers, UserEmailMap, User, UserFollowing) -from rhodecode.model.forms import UserForm, PasswordChangeForm +from rhodecode.model.forms import UserForm from rhodecode.model.scm import RepoList from rhodecode.model.user import UserModel from rhodecode.model.repo import RepoModel @@ -185,38 +188,44 @@ class MyAccountController(BaseController): force_defaults=False ) - @auth.CSRFRequired() - def my_account_password_update(self): - c.active = 'password' - self.__load_data() - _form = PasswordChangeForm(c.rhodecode_user.username)() - try: - form_result = _form.to_python(request.POST) - UserModel().update_user(c.rhodecode_user.user_id, **form_result) - instance = c.rhodecode_user.get_instance() - instance.update_userdata(force_password_change=False) - Session().commit() - session.setdefault('rhodecode_user', {}).update( - {'password': md5(instance.password)}) - session.save() - h.flash(_("Successfully updated password"), category='success') - except formencode.Invalid as errors: - return htmlfill.render( - render('admin/my_account/my_account.html'), - defaults=errors.value, - errors=errors.error_dict or {}, - prefix_error=False, - encoding="UTF-8", - force_defaults=False) - except Exception: - log.exception("Exception updating password") - h.flash(_('Error occurred during update of user password'), - category='error') - return render('admin/my_account/my_account.html') - + @auth.CSRFRequired(except_methods=['GET']) def my_account_password(self): c.active = 'password' self.__load_data() + + schema = user_schema.ChangePasswordSchema().bind( + username=c.rhodecode_user.username) + + form = forms.Form(schema, + buttons=(forms.buttons.save, forms.buttons.reset)) + + if request.method == 'POST': + controls = request.POST.items() + try: + valid_data = form.validate(controls) + UserModel().update_user(c.rhodecode_user.user_id, **valid_data) + instance = c.rhodecode_user.get_instance() + instance.update_userdata(force_password_change=False) + Session().commit() + except forms.ValidationFailure as e: + request.session.flash( + _('Error occurred during update of user password'), + queue='error') + form = e + except Exception: + log.exception("Exception updating password") + request.session.flash( + _('Error occurred during update of user password'), + queue='error') + else: + session.setdefault('rhodecode_user', {}).update( + {'password': md5(instance.password)}) + session.save() + request.session.flash( + _("Successfully updated password"), queue='success') + return redirect(url('my_account_password')) + + c.form = form return render('admin/my_account/my_account.html') def my_account_repos(self): diff --git a/rhodecode/forms/__init__.py b/rhodecode/forms/__init__.py new file mode 100644 index 00000000..e49260a8 --- /dev/null +++ b/rhodecode/forms/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +""" +Base module for form rendering / validation - currently just a wrapper for +deform - later can be replaced with something custom. +""" + +from rhodecode.translation import _ +from deform import Button, Form, widget, ValidationFailure + + +class buttons: + save = Button(name='Save', type='submit') + reset = Button(name=_('Reset'), type='reset') + delete = Button(name=_('Delete'), type='submit') diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py index f95b5104..01718ed2 100644 --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -1116,9 +1116,11 @@ class CSRFRequired(object): For use with the ``webhelpers.secure_form`` helper functions. """ - def __init__(self, token=csrf_token_key, header='X-CSRF-Token'): + def __init__(self, token=csrf_token_key, header='X-CSRF-Token', + except_methods=None): self.token = token self.header = header + self.except_methods = except_methods or [] def __call__(self, func): return get_cython_compat_decorator(self.__wrapper, func) @@ -1131,6 +1133,9 @@ class CSRFRequired(object): return supplied_token and supplied_token == cur_token def __wrapper(self, func, *fargs, **fkwargs): + if request.method in self.except_methods: + return func(*fargs, **fkwargs) + cur_token = get_csrf_token(save_if_missing=False) if self.check_csrf(request, cur_token): if request.POST.get(self.token): diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py index c68a4cd7..7e0a6017 100644 --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -102,20 +102,6 @@ def LoginForm(): return _LoginForm -def PasswordChangeForm(username): - class _PasswordChangeForm(formencode.Schema): - allow_extra_fields = True - filter_extra_fields = True - - current_password = v.ValidOldPassword(username)(not_empty=True) - new_password = All(v.ValidPassword(), v.UnicodeString(strip=False, min=6)) - new_password_confirmation = All(v.ValidPassword(), v.UnicodeString(strip=False, min=6)) - - chained_validators = [v.ValidPasswordsMatch('new_password', - 'new_password_confirmation')] - return _PasswordChangeForm - - def UserForm(edit=False, available_languages=[], old_data={}): class _UserForm(formencode.Schema): allow_extra_fields = True diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py index ad63f9c7..2bb82d6b 100644 --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -147,7 +147,6 @@ class UserModel(BaseModel): # cleanups, my_account password change form kwargs.pop('current_password', None) kwargs.pop('new_password', None) - kwargs.pop('new_password_confirmation', None) # cleanups, user edit password change form kwargs.pop('password_confirmation', None) diff --git a/rhodecode/model/validation_schema/schemas/user_schema.py b/rhodecode/model/validation_schema/schemas/user_schema.py new file mode 100644 index 00000000..4c9270ab --- /dev/null +++ b/rhodecode/model/validation_schema/schemas/user_schema.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import colander + +from rhodecode import forms +from rhodecode.model.db import User +from rhodecode.translation import _ +from rhodecode.lib.auth import check_password + + +@colander.deferred +def deferred_user_password_validator(node, kw): + username = kw.get('username') + user = User.get_by_username(username) + + def _user_password_validator(node, value): + if not check_password(value, user.password): + msg = _('Password is incorrect') + raise colander.Invalid(node, msg) + return _user_password_validator + + +class ChangePasswordSchema(colander.Schema): + + current_password = colander.SchemaNode( + colander.String(), + missing=colander.required, + widget=forms.widget.PasswordWidget(redisplay=True), + validator=deferred_user_password_validator) + + new_password = colander.SchemaNode( + colander.String(), + missing=colander.required, + widget=forms.widget.CheckedPasswordWidget(redisplay=True), + validator=colander.Length(min=6)) + + + def validator(self, form, values): + if values['current_password'] == values['new_password']: + exc = colander.Invalid(form) + exc['new_password'] = _('New password must be different ' + 'to old password') + raise exc diff --git a/rhodecode/model/validation_schema/validators.py b/rhodecode/model/validation_schema/validators.py index ffbe8ab5..1ebea6dc 100644 --- a/rhodecode/model/validation_schema/validators.py +++ b/rhodecode/model/validation_schema/validators.py @@ -13,7 +13,3 @@ def ip_addr_validator(node, value): except ValueError: msg = _(u'Please enter a valid IPv4 or IpV6 address') raise colander.Invalid(node, msg) - - - - diff --git a/rhodecode/public/css/deform.less b/rhodecode/public/css/deform.less index 56cc2fef..6e246af3 100644 --- a/rhodecode/public/css/deform.less +++ b/rhodecode/public/css/deform.less @@ -12,6 +12,7 @@ .control-label { width: 200px; + padding: 10px; float: left; } .control-inputs { @@ -26,6 +27,13 @@ .form-group { clear: left; + margin-bottom: 20px; + + &:after { /* clear fix */ + content: " "; + display: block; + clear: left; + } } .form-control { @@ -34,6 +42,11 @@ .error-block { color: red; + margin: 0; + } + + .help-block { + margin: 0; } .deform-seq-container .control-inputs { @@ -62,7 +75,9 @@ } } - .form-control.select2-container { height: 40px; } + .form-control.select2-container { + height: 40px; + } .deform-two-field-sequence .deform-seq-container .deform-seq-item label { display: none; @@ -74,7 +89,7 @@ display: none; } .deform-two-field-sequence .deform-seq-container .deform-seq-item.form-group { - background: red; + margin: 0; } .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group .form-group { width: 45%; padding: 0 2px; float: left; clear: none; diff --git a/rhodecode/templates/admin/my_account/my_account_password.html b/rhodecode/templates/admin/my_account/my_account_password.html index 34335363..9c78546e 100644 --- a/rhodecode/templates/admin/my_account/my_account_password.html +++ b/rhodecode/templates/admin/my_account/my_account_password.html @@ -1,42 +1,5 @@ -
-
-

${_('Change Your Account Password')}

-
- ${h.secure_form(url('my_account_password'), method='post')} -
-
-
-
- -
-
- ${h.password('current_password',class_='medium',autocomplete="off")} -
-
+<%namespace name="widgets" file="/widgets.html"/> -
-
- -
-
- ${h.password('new_password',class_='medium', autocomplete="off")} -
-
- -
-
- -
-
- ${h.password('new_password_confirmation',class_='medium', autocomplete="off")} -
-
- -
- ${h.submit('save',_('Save'),class_="btn")} - ${h.reset('reset',_('Reset'),class_="btn")} -
-
-
- ${h.end_form()} -
\ No newline at end of file +<%widgets:panel title="${_('Change Your Account Password')}"> +${c.form.render() | n} + diff --git a/rhodecode/templates/base/root.html b/rhodecode/templates/base/root.html index 66ef1f69..e202ab2f 100644 --- a/rhodecode/templates/base/root.html +++ b/rhodecode/templates/base/root.html @@ -15,7 +15,6 @@ if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id: c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer') %> - ${self.title()} diff --git a/rhodecode/templates/widgets.html b/rhodecode/templates/widgets.html new file mode 100644 index 00000000..dbedaa86 --- /dev/null +++ b/rhodecode/templates/widgets.html @@ -0,0 +1,10 @@ +<%def name="panel(title, class_='default')"> +
+
+

${title}

+
+
+ ${caller.body()} +
+
+ diff --git a/rhodecode/tests/functional/test_admin_my_account.py b/rhodecode/tests/functional/test_admin_my_account.py index be713a05..a97ad583 100644 --- a/rhodecode/tests/functional/test_admin_my_account.py +++ b/rhodecode/tests/functional/test_admin_my_account.py @@ -21,6 +21,7 @@ import pytest from rhodecode.lib import helpers as h +from rhodecode.lib.auth import check_password from rhodecode.model.db import User, UserFollowing, Repository, UserApiKeys from rhodecode.model.meta import Session from rhodecode.tests import ( @@ -34,6 +35,7 @@ fixture = Fixture() class TestMyAccountController(TestController): test_user_1 = 'testme' + test_user_1_password = '0jd83nHNS/d23n' destroy_users = set() @classmethod @@ -158,7 +160,8 @@ class TestMyAccountController(TestController): ('email', {'email': 'some@email.com'}), ]) def test_my_account_update(self, name, attrs): - usr = fixture.create_user(self.test_user_1, password='qweqwe', + usr = fixture.create_user(self.test_user_1, + password=self.test_user_1_password, email='testme@rhodecode.org', extern_type='rhodecode', extern_name=self.test_user_1, @@ -167,7 +170,8 @@ class TestMyAccountController(TestController): params = usr.get_api_data() # current user data user_id = usr.user_id - self.log_user(username=self.test_user_1, password='qweqwe') + self.log_user( + username=self.test_user_1, password=self.test_user_1_password) params.update({'password_confirmation': ''}) params.update({'new_password': ''}) @@ -321,8 +325,55 @@ class TestMyAccountController(TestController): response = response.follow() response.mustcontain(no=[api_key]) - def test_password_is_updated_in_session_on_password_change( - self, user_util): + def test_valid_change_password(self, user_util): + new_password = 'my_new_valid_password' + user = user_util.create_user(password=self.test_user_1_password) + session = self.log_user(user.username, self.test_user_1_password) + form_data = [ + ('current_password', self.test_user_1_password), + ('__start__', 'new_password:mapping'), + ('new_password', new_password), + ('new_password-confirm', new_password), + ('__end__', 'new_password:mapping'), + ('csrf_token', self.csrf_token), + ] + response = self.app.post(url('my_account_password'), form_data).follow() + assert 'Successfully updated password' in response + + # check_password depends on user being in session + Session().add(user) + try: + assert check_password(new_password, user.password) + finally: + Session().expunge(user) + + @pytest.mark.parametrize('current_pw,new_pw,confirm_pw', [ + ('', 'abcdef123', 'abcdef123'), + ('wrong_pw', 'abcdef123', 'abcdef123'), + (test_user_1_password, test_user_1_password, test_user_1_password), + (test_user_1_password, '', ''), + (test_user_1_password, 'abcdef123', ''), + (test_user_1_password, '', 'abcdef123'), + (test_user_1_password, 'not_the', 'same_pw'), + (test_user_1_password, 'short', 'short'), + ]) + def test_invalid_change_password(self, current_pw, new_pw, confirm_pw, + user_util): + user = user_util.create_user(password=self.test_user_1_password) + session = self.log_user(user.username, self.test_user_1_password) + old_password_hash = session['password'] + form_data = [ + ('current_password', current_pw), + ('__start__', 'new_password:mapping'), + ('new_password', new_pw), + ('new_password-confirm', confirm_pw), + ('__end__', 'new_password:mapping'), + ('csrf_token', self.csrf_token), + ] + response = self.app.post(url('my_account_password'), form_data) + assert 'Error occurred' in response + + def test_password_is_updated_in_session_on_password_change(self, user_util): old_password = 'abcdef123' new_password = 'abcdef124' @@ -330,12 +381,14 @@ class TestMyAccountController(TestController): session = self.log_user(user.username, old_password) old_password_hash = session['password'] - form_data = { - 'current_password': old_password, - 'new_password': new_password, - 'new_password_confirmation': new_password, - 'csrf_token': self.csrf_token - } + form_data = [ + ('current_password', old_password), + ('__start__', 'new_password:mapping'), + ('new_password', new_password), + ('new_password-confirm', new_password), + ('__end__', 'new_password:mapping'), + ('csrf_token', self.csrf_token), + ] self.app.post(url('my_account_password'), form_data) response = self.app.get(url('home')) diff --git a/rhodecode/tests/models/schemas/test_user_schema.py b/rhodecode/tests/models/schemas/test_user_schema.py new file mode 100644 index 00000000..f3bcf413 --- /dev/null +++ b/rhodecode/tests/models/schemas/test_user_schema.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import colander +import pytest + +from rhodecode.model import validation_schema +from rhodecode.model.validation_schema.schemas import user_schema + + +class TestChangePasswordSchema(object): + original_password = 'm092d903fnio0m' + + def test_deserialize_bad_data(self, user_regular): + schema = user_schema.ChangePasswordSchema().bind( + username=user_regular.username) + + with pytest.raises(validation_schema.Invalid) as exc_info: + schema.deserialize('err') + err = exc_info.value.asdict() + assert err[''] == '"err" is not a mapping type: ' \ + 'Does not implement dict-like functionality.' + + def test_validate_valid_change_password_data(self, user_util): + user = user_util.create_user(password=self.original_password) + schema = user_schema.ChangePasswordSchema().bind( + username=user.username) + + schema.deserialize({ + 'current_password': self.original_password, + 'new_password': '23jf04rm04imr' + }) + + @pytest.mark.parametrize( + 'current_password,new_password,key,message', [ + ('', 'abcdef123', 'current_password', 'required'), + ('wrong_pw', 'abcdef123', 'current_password', 'incorrect'), + (original_password, original_password, 'new_password', 'different'), + (original_password, '', 'new_password', 'Required'), + (original_password, 'short', 'new_password', 'minimum'), + ]) + def test_validate_invalid_change_password_data(self, current_password, + new_password, key, message, + user_util): + user = user_util.create_user(password=self.original_password) + schema = user_schema.ChangePasswordSchema().bind( + username=user.username) + + with pytest.raises(validation_schema.Invalid) as exc_info: + schema.deserialize({ + 'current_password': current_password, + 'new_password': new_password + }) + err = exc_info.value.asdict() + assert message.lower() in err[key].lower() From 2a0ed7c3dcf7cc7d3ba6a59a538c51b50966d892 Mon Sep 17 00:00:00 2001 From: Martin Bornhold Date: Mon, 15 Aug 2016 11:07:03 +0200 Subject: [PATCH 004/125] nix: Remove Fabric from dependencies. #4167 --- pkgs/python-packages.nix | 15 +-------------- requirements.txt | 1 - 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/pkgs/python-packages.nix b/pkgs/python-packages.nix index 1956674c..c4289eae 100644 --- a/pkgs/python-packages.nix +++ b/pkgs/python-packages.nix @@ -51,19 +51,6 @@ license = [ { fullName = "BSD-like (http://repoze.org/license.html)"; } ]; }; }; - Fabric = super.buildPythonPackage { - name = "Fabric-1.10.0"; - buildInputs = with self; []; - doCheck = false; - propagatedBuildInputs = with self; [paramiko]; - src = fetchurl { - url = "https://pypi.python.org/packages/e3/5f/b6ebdb5241d5ec9eab582a5c8a01255c1107da396f849e538801d2fe64a5/Fabric-1.10.0.tar.gz"; - md5 = "2cb96473387f0e7aa035210892352f4a"; - }; - meta = { - license = [ pkgs.lib.licenses.bsdOriginal ]; - }; - }; FormEncode = super.buildPythonPackage { name = "FormEncode-1.2.4"; buildInputs = with self; []; @@ -1430,7 +1417,7 @@ }; }; rhodecode-enterprise-ce = super.buildPythonPackage { - name = "rhodecode-enterprise-ce-4.3.0"; + name = "rhodecode-enterprise-ce-4.4.0"; buildInputs = with self; [WebTest configobj cssselect flake8 lxml mock pytest pytest-cov pytest-runner]; doCheck = true; propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments Pylons Pyro4 Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress celery channelstream colander decorator deform docutils gevent gunicorn infrae.cache ipython iso8601 kombu msgpack-python packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt]; diff --git a/requirements.txt b/requirements.txt index 2f1ae486..9f688c8c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ Babel==1.3 Beaker==1.7.0 CProfileV==1.0.6 -Fabric==1.10.0 FormEncode==1.2.4 Jinja2==2.7.3 Mako==1.0.1 From 2e3256ff49cd8c69a3845aa73e94d638bd6441ea Mon Sep 17 00:00:00 2001 From: Daniel Dourvaris Date: Mon, 15 Aug 2016 13:24:16 +0300 Subject: [PATCH 005/125] integrations: add repo group integrations, fixes #4175 --- rhodecode/integrations/routes.py | 73 ++++++++++++++++++- rhodecode/integrations/views.py | 33 ++++++++- rhodecode/model/db.py | 7 ++ rhodecode/model/integration.py | 12 ++- .../templates/admin/integrations/base.html | 2 + .../templates/admin/integrations/list.html | 17 ++++- .../admin/repo_groups/repo_group_edit.html | 7 +- 7 files changed, 138 insertions(+), 13 deletions(-) diff --git a/rhodecode/integrations/routes.py b/rhodecode/integrations/routes.py index 859be9e1..dbadbb61 100644 --- a/rhodecode/integrations/routes.py +++ b/rhodecode/integrations/routes.py @@ -20,7 +20,7 @@ import logging -from rhodecode.model.db import Repository, Integration +from rhodecode.model.db import Repository, Integration, RepoGroup from rhodecode.config.routing import ( ADMIN_PREFIX, add_route_requirements, URL_NAME_REQUIREMENTS) from rhodecode.integrations import integration_type_registry @@ -29,6 +29,8 @@ log = logging.getLogger(__name__) def includeme(config): + + # global integrations config.add_route('global_integrations_home', ADMIN_PREFIX + '/integrations') config.add_route('global_integrations_list', @@ -58,6 +60,8 @@ def includeme(config): request_method='POST', route_name=route_name) + + # repo integrations config.add_route('repo_integrations_home', add_route_requirements( '{repo_name}/settings/integrations', @@ -101,26 +105,87 @@ def includeme(config): route_name=route_name) + # repo group integrations + config.add_route('repo_group_integrations_home', + add_route_requirements( + '{repo_group_name}/settings/integrations', + URL_NAME_REQUIREMENTS + ), + custom_predicates=(valid_repo_group,)) + config.add_route('repo_group_integrations_list', + add_route_requirements( + '{repo_group_name}/settings/integrations/{integration}', + URL_NAME_REQUIREMENTS + ), + custom_predicates=(valid_repo_group, valid_integration)) + for route_name in ['repo_group_integrations_home', 'repo_group_integrations_list']: + config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView', + attr='index', + request_method='GET', + route_name=route_name) + + config.add_route('repo_group_integrations_create', + add_route_requirements( + '{repo_group_name}/settings/integrations/{integration}/new', + URL_NAME_REQUIREMENTS + ), + custom_predicates=(valid_repo_group, valid_integration)) + config.add_route('repo_group_integrations_edit', + add_route_requirements( + '{repo_group_name}/settings/integrations/{integration}/{integration_id}', + URL_NAME_REQUIREMENTS + ), + custom_predicates=(valid_repo_group, valid_integration)) + for route_name in ['repo_group_integrations_edit', 'repo_group_integrations_create']: + config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView', + attr='settings_get', + renderer='rhodecode:templates/admin/integrations/edit.html', + request_method='GET', + route_name=route_name) + config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView', + attr='settings_post', + renderer='rhodecode:templates/admin/integrations/edit.html', + request_method='POST', + route_name=route_name) + + def valid_repo(info, request): repo = Repository.get_by_repo_name(info['match']['repo_name']) if repo: return True +def valid_repo_group(info, request): + repo_group = RepoGroup.get_by_group_name(info['match']['repo_group_name']) + if repo_group: + return True + return False + + def valid_integration(info, request): integration_type = info['match']['integration'] integration_id = info['match'].get('integration_id') repo_name = info['match'].get('repo_name') + repo_group_name = info['match'].get('repo_group_name') if integration_type not in integration_type_registry: return False - repo = None + repo, repo_group = None, None if repo_name: - repo = Repository.get_by_repo_name(info['match']['repo_name']) + repo = Repository.get_by_repo_name(repo_name) if not repo: return False + if repo_group_name: + repo_group = RepoGroup.get_by_group_name(repo_group_name) + if not repo_group: + return False + + if repo_name and repo_group: + raise Exception('Either repo or repo_group can be set, not both') + + if integration_id: integration = Integration.get(integration_id) if not integration: @@ -129,5 +194,7 @@ def valid_integration(info, request): return False if repo and repo.repo_id != integration.repo_id: return False + if repo_group and repo_group.repo_group_id != integration.repo_group_id: + return False return True diff --git a/rhodecode/integrations/views.py b/rhodecode/integrations/views.py index 102a95cb..811c82c5 100644 --- a/rhodecode/integrations/views.py +++ b/rhodecode/integrations/views.py @@ -29,7 +29,7 @@ from pyramid.response import Response from rhodecode.lib import auth from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator -from rhodecode.model.db import Repository, Session, Integration +from rhodecode.model.db import Repository, RepoGroup, Session, Integration from rhodecode.model.scm import ScmModel from rhodecode.model.integration import IntegrationModel from rhodecode.admin.navigation import navigation_list @@ -59,6 +59,7 @@ class IntegrationSettingsViewBase(object): self.IntegrationType = None self.repo = None + self.repo_group = None self.integration = None self.integrations = {} @@ -68,6 +69,10 @@ class IntegrationSettingsViewBase(object): repo_name = request.matchdict['repo_name'] self.repo = Repository.get_by_repo_name(repo_name) + if 'repo_group_name' in request.matchdict: # we're in repo_group context + repo_group_name = request.matchdict['repo_group_name'] + self.repo_group = RepoGroup.get_by_group_name(repo_group_name) + if 'integration' in request.matchdict: # we're in integration context integration_type = request.matchdict['integration'] self.IntegrationType = integration_type_registry[integration_type] @@ -76,7 +81,10 @@ class IntegrationSettingsViewBase(object): integration_id = request.matchdict['integration_id'] self.integration = Integration.get(integration_id) else: # list integrations context - for integration in IntegrationModel().get_integrations(self.repo): + integrations = IntegrationModel().get_integrations( + repo=self.repo, repo_group=self.repo_group) + + for integration in integrations: self.integrations.setdefault(integration.integration_type, [] ).append(integration) @@ -91,7 +99,9 @@ class IntegrationSettingsViewBase(object): c.active = 'integrations' c.rhodecode_user = self.request.user c.repo = self.repo + c.repo_group = self.repo_group c.repo_name = self.repo and self.repo.repo_name or None + c.repo_group_name = self.repo_group and self.repo_group.group_name or None if self.repo: c.repo_info = self.repo c.rhodecode_db_repo = self.repo @@ -121,7 +131,11 @@ class IntegrationSettingsViewBase(object): defaults['enabled'] = self.integration.enabled else: if self.repo: - scope = self.repo.repo_name + scope = _('{repo_name} repository').format( + repo_name=self.repo.repo_name) + elif self.repo_group: + scope = _('{repo_group_name} repo group').format( + repo_group_name=self.repo_group.group_name) else: scope = _('Global') @@ -207,6 +221,8 @@ class IntegrationSettingsViewBase(object): self.integration.integration_type = self.IntegrationType.key if self.repo: self.integration.repo = self.repo + elif self.repo_group: + self.integration.repo_group = self.repo_group Session().add(self.integration) self.integration.enabled = valid_data.pop('enabled', False) @@ -226,6 +242,12 @@ class IntegrationSettingsViewBase(object): 'repo_integrations_edit', repo_name=self.repo.repo_name, integration=self.integration.integration_type, integration_id=self.integration.integration_id) + elif self.repo: + redirect_to = self.request.route_url( + 'repo_group_integrations_edit', + repo_group_name=self.repo_group.group_name, + integration=self.integration.integration_type, + integration_id=self.integration.integration_id) else: redirect_to = self.request.route_url( 'global_integrations_edit', @@ -270,3 +292,8 @@ class RepoIntegrationsView(IntegrationSettingsViewBase): def perm_check(self, user): return auth.HasRepoPermissionAll('repository.admin' )(repo_name=self.repo.repo_name, user=user) + +class RepoGroupIntegrationsView(IntegrationSettingsViewBase): + def perm_check(self, user): + return auth.HasRepoGroupPermissionAll('group.admin' + )(group_name=self.repo_group.group_name, user=user) diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index 24833bdb..31e98c2c 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -3490,9 +3490,16 @@ class Integration(Base, BaseModel): nullable=True, unique=None, default=None) repo = relationship('Repository', lazy='joined') + repo_group_id = Column( + 'repo_group_id', Integer(), ForeignKey('groups.group_id'), + nullable=True, unique=None, default=None) + repo_group = relationship('RepoGroup', lazy='joined') + def __repr__(self): if self.repo: scope = 'repo=%r' % self.repo + elif self.repo_group: + scope = 'repo_group=%r' % self.repo_group else: scope = 'global' diff --git a/rhodecode/model/integration.py b/rhodecode/model/integration.py index e9accc66..e5373d75 100644 --- a/rhodecode/model/integration.py +++ b/rhodecode/model/integration.py @@ -100,10 +100,13 @@ class IntegrationModel(BaseModel): if handler: handler.send_event(event) - def get_integrations(self, repo=None): + def get_integrations(self, repo=None, repo_group=None): if repo: return self.sa.query(Integration).filter( Integration.repo_id==repo.repo_id).all() + elif repo_group: + return self.sa.query(Integration).filter( + Integration.repo_group_id==repo_group.group_id).all() # global integrations return self.sa.query(Integration).filter( @@ -116,9 +119,14 @@ class IntegrationModel(BaseModel): query = self.sa.query(Integration).filter(Integration.enabled==True) if isinstance(event, events.RepoEvent): # global + repo integrations + # + repo_group integrations + parent_groups = event.repo.groups_with_parents query = query.filter( or_(Integration.repo_id==None, - Integration.repo_id==event.repo.repo_id)) + Integration.repo_id==event.repo.repo_id, + Integration.repo_group_id.in_( + [group.group_id for group in parent_groups] + ))) if cache: query = query.options(FromCache( "sql_cache_short", diff --git a/rhodecode/templates/admin/integrations/base.html b/rhodecode/templates/admin/integrations/base.html index 6fdba05a..ca97f644 100644 --- a/rhodecode/templates/admin/integrations/base.html +++ b/rhodecode/templates/admin/integrations/base.html @@ -3,6 +3,8 @@ def inherit(context): if context['c'].repo: return "/admin/repos/repo_edit.html" + elif context['c'].repo_group: + return "/admin/repo_groups/repo_group_edit.html" else: return "/admin/settings/settings.html" %> diff --git a/rhodecode/templates/admin/integrations/list.html b/rhodecode/templates/admin/integrations/list.html index 38142f4b..0b0a7f37 100644 --- a/rhodecode/templates/admin/integrations/list.html +++ b/rhodecode/templates/admin/integrations/list.html @@ -37,11 +37,15 @@ %for integration in available_integrations: <% if c.repo: - create_url = request.route_url('repo_integrations_create', + create_url = request.route_path('repo_integrations_create', repo_name=c.repo.repo_name, integration=integration) + elif c.repo_group: + create_url = request.route_path('repo_group_integrations_create', + repo_group_name=c.repo_group.group_name, + integration=integration) else: - create_url = request.route_url('global_integrations_create', + create_url = request.route_path('global_integrations_create', integration=integration) %> @@ -90,12 +94,17 @@ %else: <% if c.repo: - edit_url = request.route_url('repo_integrations_edit', + edit_url = request.route_path('repo_integrations_edit', repo_name=c.repo.repo_name, integration=integration.integration_type, integration_id=integration.integration_id) + elif c.repo_group: + edit_url = request.route_path('repo_group_integrations_edit', + repo_group_name=c.repo_group.group_name, + integration=integration.integration_type, + integration_id=integration.integration_id) else: - edit_url = request.route_url('global_integrations_edit', + edit_url = request.route_path('global_integrations_edit', integration=integration.integration_type, integration_id=integration.integration_id) %> diff --git a/rhodecode/templates/admin/repo_groups/repo_group_edit.html b/rhodecode/templates/admin/repo_groups/repo_group_edit.html index 3d28f93e..82d0a257 100644 --- a/rhodecode/templates/admin/repo_groups/repo_group_edit.html +++ b/rhodecode/templates/admin/repo_groups/repo_group_edit.html @@ -30,6 +30,10 @@ ${self.menu_items(active='admin')} +<%def name="main_content()"> + <%include file="/admin/repo_groups/repo_group_edit_${c.active}.html"/> + + <%def name="main()">
- <%include file="/admin/repo_groups/repo_group_edit_${c.active}.html"/> + ${self.main_content()}
From 61df8a609bb6125ef889983177d29331032ea38b Mon Sep 17 00:00:00 2001 From: lisaq Date: Wed, 17 Aug 2016 15:48:56 +0200 Subject: [PATCH 006/125] pr: adding last updated column to user pull request list, fixes #4162 --- .../admin/my_account/my_account_pullrequests.html | 10 ++++------ rhodecode/templates/pullrequests/pullrequests.html | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/rhodecode/templates/admin/my_account/my_account_pullrequests.html b/rhodecode/templates/admin/my_account/my_account_pullrequests.html index 92ab8c35..424f1971 100644 --- a/rhodecode/templates/admin/my_account/my_account_pullrequests.html +++ b/rhodecode/templates/admin/my_account/my_account_pullrequests.html @@ -24,7 +24,7 @@ ${_('Author')} ${_('Title')} - ${_('Opened On')} + ${_('Last Update')} %for pull_request in c.my_pull_requests: @@ -54,9 +54,8 @@
${pull_request.description} - - ${h.age_component(pull_request.created_on)} + ${h.age_component(pull_request.updated_on)} ${h.secure_form(url('pullrequest_delete', repo_name=pull_request.target_repo.repo_name, pull_request_id=pull_request.pull_request_id),method='delete')} @@ -89,7 +88,7 @@ ${_('Author')} ${_('Title')} - ${_('Opened On')} + ${_('Last Update')} %for pull_request in c.participate_in_pull_requests: @@ -118,9 +117,8 @@
${pull_request.description} - - ${h.age_component(pull_request.created_on)} + ${h.age_component(pull_request.updated_on)} %endfor diff --git a/rhodecode/templates/pullrequests/pullrequests.html b/rhodecode/templates/pullrequests/pullrequests.html index 610e8a3c..fec27e2e 100644 --- a/rhodecode/templates/pullrequests/pullrequests.html +++ b/rhodecode/templates/pullrequests/pullrequests.html @@ -105,7 +105,7 @@ $(document).ready(function() { { data: {"_": "comments", "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false}, { data: {"_": "updated_on", - "sort": "updated_on_raw"}, title: "${_('Updated on')}", className: "td-time" } + "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" } ], language: { paginate: DEFAULT_GRID_PAGINATION, From bb1f512c92f3af595cbf5d278878c28171b0a9b8 Mon Sep 17 00:00:00 2001 From: lisaq Date: Wed, 17 Aug 2016 18:39:49 +0200 Subject: [PATCH 007/125] pr: adding pr count to user pull request tables fixes #4160 --- .../templates/admin/my_account/my_account_pullrequests.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rhodecode/templates/admin/my_account/my_account_pullrequests.html b/rhodecode/templates/admin/my_account/my_account_pullrequests.html index 424f1971..7d1124a1 100644 --- a/rhodecode/templates/admin/my_account/my_account_pullrequests.html +++ b/rhodecode/templates/admin/my_account/my_account_pullrequests.html @@ -12,7 +12,7 @@
-

${_('Pull Requests You Opened')}

+

${_('Pull Requests You Opened')}: ${len(c.my_pull_requests)}

@@ -75,7 +75,7 @@
-

${_('Pull Requests You Participate In')}

+

${_('Pull Requests You Participate In')}: ${len(c.participate_in_pull_requests)}

From df366bf250dd9ae4ee273d2203453ec792332d91 Mon Sep 17 00:00:00 2001 From: lisaq Date: Wed, 17 Aug 2016 16:20:49 +0200 Subject: [PATCH 008/125] docs: fixing errors in PostgreSQL setup fixes #4174 --- docs/install/using-postgresql.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/install/using-postgresql.rst b/docs/install/using-postgresql.rst index e2d91146..db41ffc9 100644 --- a/docs/install/using-postgresql.rst +++ b/docs/install/using-postgresql.rst @@ -3,20 +3,20 @@ PostgreSQL ---------- -To use a PostgreSQL database you should install and configurevthe database -before installing |RCV|. This is becausevduring |RCV| installation you will -setup a connection to your PostgreSQL database. To work with PostgreSQL, +To use a PostgreSQL database, you should install and configure the database +before installing |RCV|. This is because during |RCV| installation you will +setup the connection to your PostgreSQL database. To work with PostgreSQL, use the following steps: -1. Depending on your |os|, install avPostgreSQL database following the +1. Depending on your |os|, install a PostgreSQL database following the appropriate instructions from the `PostgreSQL website`_. -2. Configure the database with a username and password which you will use +2. Configure the database with a username and password, which you will use with |RCV|. 3. Install |RCV|, and during installation select PostgreSQL as your database. -4. Enter the following information to during the database setup: +4. Enter the following information during the database setup: * Your network IP Address - * The port number for MySQL access. The default MySQL port is ``5434`` + * The port number for PostgreSQL access; the default port is ``5434`` * Your database username * Your database password * A new database name From 8dff55f435d3f76c0c04b14bc4889dcb46dc93e4 Mon Sep 17 00:00:00 2001 From: Daniel Dourvaris Date: Thu, 18 Aug 2016 14:55:37 +0300 Subject: [PATCH 009/125] db: add migration for repo_group_id on integrations, version 56 --- rhodecode/__init__.py | 2 +- rhodecode/lib/dbmigrate/schema/db_4_4_0_0.py | 3506 +++++++++++++++++ .../dbmigrate/versions/056_version_4_4_0.py | 36 + 3 files changed, 3543 insertions(+), 1 deletion(-) create mode 100644 rhodecode/lib/dbmigrate/schema/db_4_4_0_0.py create mode 100644 rhodecode/lib/dbmigrate/versions/056_version_4_4_0.py diff --git a/rhodecode/__init__.py b/rhodecode/__init__.py index b4c66f8c..113e10a1 100644 --- a/rhodecode/__init__.py +++ b/rhodecode/__init__.py @@ -51,7 +51,7 @@ PYRAMID_SETTINGS = {} EXTENSIONS = {} __version__ = ('.'.join((str(each) for each in VERSION[:3]))) -__dbversion__ = 55 # defines current db version for migrations +__dbversion__ = 56 # defines current db version for migrations __platform__ = platform.system() __license__ = 'AGPLv3, and Commercial License' __author__ = 'RhodeCode GmbH' diff --git a/rhodecode/lib/dbmigrate/schema/db_4_4_0_0.py b/rhodecode/lib/dbmigrate/schema/db_4_4_0_0.py new file mode 100644 index 00000000..31e98c2c --- /dev/null +++ b/rhodecode/lib/dbmigrate/schema/db_4_4_0_0.py @@ -0,0 +1,3506 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +""" +Database Models for RhodeCode Enterprise +""" + +import os +import sys +import time +import hashlib +import logging +import datetime +import warnings +import ipaddress +import functools +import traceback +import collections + + +from sqlalchemy import * +from sqlalchemy.exc import IntegrityError +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import ( + relationship, joinedload, class_mapper, validates, aliased) +from sqlalchemy.sql.expression import true +from beaker.cache import cache_region, region_invalidate +from webob.exc import HTTPNotFound +from zope.cachedescriptors.property import Lazy as LazyProperty + +from pylons import url +from pylons.i18n.translation import lazy_ugettext as _ + +from rhodecode.lib.vcs import get_backend, get_vcs_instance +from rhodecode.lib.vcs.utils.helpers import get_scm +from rhodecode.lib.vcs.exceptions import VCSError +from rhodecode.lib.vcs.backends.base import ( + EmptyCommit, Reference, MergeFailureReason) +from rhodecode.lib.utils2 import ( + str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe, + time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict) +from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict +from rhodecode.lib.ext_json import json +from rhodecode.lib.caching_query import FromCache +from rhodecode.lib.encrypt import AESCipher + +from rhodecode.model.meta import Base, Session + +URL_SEP = '/' +log = logging.getLogger(__name__) + +# ============================================================================= +# BASE CLASSES +# ============================================================================= + +# this is propagated from .ini file rhodecode.encrypted_values.secret or +# beaker.session.secret if first is not set. +# and initialized at environment.py +ENCRYPTION_KEY = None + +# used to sort permissions by types, '#' used here is not allowed to be in +# usernames, and it's very early in sorted string.printable table. +PERMISSION_TYPE_SORT = { + 'admin': '####', + 'write': '###', + 'read': '##', + 'none': '#', +} + + +def display_sort(obj): + """ + Sort function used to sort permissions in .permissions() function of + Repository, RepoGroup, UserGroup. Also it put the default user in front + of all other resources + """ + + if obj.username == User.DEFAULT_USER: + return '#####' + prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '') + return prefix + obj.username + + +def _hash_key(k): + return md5_safe(k) + + +class EncryptedTextValue(TypeDecorator): + """ + Special column for encrypted long text data, use like:: + + value = Column("encrypted_value", EncryptedValue(), nullable=False) + + This column is intelligent so if value is in unencrypted form it return + unencrypted form, but on save it always encrypts + """ + impl = Text + + def process_bind_param(self, value, dialect): + if not value: + return value + if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'): + # protect against double encrypting if someone manually starts + # doing + raise ValueError('value needs to be in unencrypted format, ie. ' + 'not starting with enc$aes') + return 'enc$aes_hmac$%s' % AESCipher( + ENCRYPTION_KEY, hmac=True).encrypt(value) + + def process_result_value(self, value, dialect): + import rhodecode + + if not value: + return value + + parts = value.split('$', 3) + if not len(parts) == 3: + # probably not encrypted values + return value + else: + if parts[0] != 'enc': + # parts ok but without our header ? + return value + enc_strict_mode = str2bool(rhodecode.CONFIG.get( + 'rhodecode.encrypted_values.strict') or True) + # at that stage we know it's our encryption + if parts[1] == 'aes': + decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2]) + elif parts[1] == 'aes_hmac': + decrypted_data = AESCipher( + ENCRYPTION_KEY, hmac=True, + strict_verification=enc_strict_mode).decrypt(parts[2]) + else: + raise ValueError( + 'Encryption type part is wrong, must be `aes` ' + 'or `aes_hmac`, got `%s` instead' % (parts[1])) + return decrypted_data + + +class BaseModel(object): + """ + Base Model for all classes + """ + + @classmethod + def _get_keys(cls): + """return column names for this model """ + return class_mapper(cls).c.keys() + + def get_dict(self): + """ + return dict with keys and values corresponding + to this model data """ + + d = {} + for k in self._get_keys(): + d[k] = getattr(self, k) + + # also use __json__() if present to get additional fields + _json_attr = getattr(self, '__json__', None) + if _json_attr: + # update with attributes from __json__ + if callable(_json_attr): + _json_attr = _json_attr() + for k, val in _json_attr.iteritems(): + d[k] = val + return d + + def get_appstruct(self): + """return list with keys and values tuples corresponding + to this model data """ + + l = [] + for k in self._get_keys(): + l.append((k, getattr(self, k),)) + return l + + def populate_obj(self, populate_dict): + """populate model with data from given populate_dict""" + + for k in self._get_keys(): + if k in populate_dict: + setattr(self, k, populate_dict[k]) + + @classmethod + def query(cls): + return Session().query(cls) + + @classmethod + def get(cls, id_): + if id_: + return cls.query().get(id_) + + @classmethod + def get_or_404(cls, id_): + try: + id_ = int(id_) + except (TypeError, ValueError): + raise HTTPNotFound + + res = cls.query().get(id_) + if not res: + raise HTTPNotFound + return res + + @classmethod + def getAll(cls): + # deprecated and left for backward compatibility + return cls.get_all() + + @classmethod + def get_all(cls): + return cls.query().all() + + @classmethod + def delete(cls, id_): + obj = cls.query().get(id_) + Session().delete(obj) + + @classmethod + def identity_cache(cls, session, attr_name, value): + exist_in_session = [] + for (item_cls, pkey), instance in session.identity_map.items(): + if cls == item_cls and getattr(instance, attr_name) == value: + exist_in_session.append(instance) + if exist_in_session: + if len(exist_in_session) == 1: + return exist_in_session[0] + log.exception( + 'multiple objects with attr %s and ' + 'value %s found with same name: %r', + attr_name, value, exist_in_session) + + def __repr__(self): + if hasattr(self, '__unicode__'): + # python repr needs to return str + try: + return safe_str(self.__unicode__()) + except UnicodeDecodeError: + pass + return '' % (self.__class__.__name__) + + +class RhodeCodeSetting(Base, BaseModel): + __tablename__ = 'rhodecode_settings' + __table_args__ = ( + UniqueConstraint('app_settings_name'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + SETTINGS_TYPES = { + 'str': safe_str, + 'int': safe_int, + 'unicode': safe_unicode, + 'bool': str2bool, + 'list': functools.partial(aslist, sep=',') + } + DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions' + GLOBAL_CONF_KEY = 'app_settings' + + app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None) + _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None) + _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None) + + def __init__(self, key='', val='', type='unicode'): + self.app_settings_name = key + self.app_settings_type = type + self.app_settings_value = val + + @validates('_app_settings_value') + def validate_settings_value(self, key, val): + assert type(val) == unicode + return val + + @hybrid_property + def app_settings_value(self): + v = self._app_settings_value + _type = self.app_settings_type + if _type: + _type = self.app_settings_type.split('.')[0] + # decode the encrypted value + if 'encrypted' in self.app_settings_type: + cipher = EncryptedTextValue() + v = safe_unicode(cipher.process_result_value(v, None)) + + converter = self.SETTINGS_TYPES.get(_type) or \ + self.SETTINGS_TYPES['unicode'] + return converter(v) + + @app_settings_value.setter + def app_settings_value(self, val): + """ + Setter that will always make sure we use unicode in app_settings_value + + :param val: + """ + val = safe_unicode(val) + # encode the encrypted value + if 'encrypted' in self.app_settings_type: + cipher = EncryptedTextValue() + val = safe_unicode(cipher.process_bind_param(val, None)) + self._app_settings_value = val + + @hybrid_property + def app_settings_type(self): + return self._app_settings_type + + @app_settings_type.setter + def app_settings_type(self, val): + if val.split('.')[0] not in self.SETTINGS_TYPES: + raise Exception('type must be one of %s got %s' + % (self.SETTINGS_TYPES.keys(), val)) + self._app_settings_type = val + + def __unicode__(self): + return u"<%s('%s:%s[%s]')>" % ( + self.__class__.__name__, + self.app_settings_name, self.app_settings_value, + self.app_settings_type + ) + + +class RhodeCodeUi(Base, BaseModel): + __tablename__ = 'rhodecode_ui' + __table_args__ = ( + UniqueConstraint('ui_key'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + HOOK_REPO_SIZE = 'changegroup.repo_size' + # HG + HOOK_PRE_PULL = 'preoutgoing.pre_pull' + HOOK_PULL = 'outgoing.pull_logger' + HOOK_PRE_PUSH = 'prechangegroup.pre_push' + HOOK_PUSH = 'changegroup.push_logger' + + # TODO: johbo: Unify way how hooks are configured for git and hg, + # git part is currently hardcoded. + + # SVN PATTERNS + SVN_BRANCH_ID = 'vcs_svn_branch' + SVN_TAG_ID = 'vcs_svn_tag' + + ui_id = Column( + "ui_id", Integer(), nullable=False, unique=True, default=None, + primary_key=True) + ui_section = Column( + "ui_section", String(255), nullable=True, unique=None, default=None) + ui_key = Column( + "ui_key", String(255), nullable=True, unique=None, default=None) + ui_value = Column( + "ui_value", String(255), nullable=True, unique=None, default=None) + ui_active = Column( + "ui_active", Boolean(), nullable=True, unique=None, default=True) + + def __repr__(self): + return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section, + self.ui_key, self.ui_value) + + +class RepoRhodeCodeSetting(Base, BaseModel): + __tablename__ = 'repo_rhodecode_settings' + __table_args__ = ( + UniqueConstraint( + 'app_settings_name', 'repository_id', + name='uq_repo_rhodecode_setting_name_repo_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + repository_id = Column( + "repository_id", Integer(), ForeignKey('repositories.repo_id'), + nullable=False) + app_settings_id = Column( + "app_settings_id", Integer(), nullable=False, unique=True, + default=None, primary_key=True) + app_settings_name = Column( + "app_settings_name", String(255), nullable=True, unique=None, + default=None) + _app_settings_value = Column( + "app_settings_value", String(4096), nullable=True, unique=None, + default=None) + _app_settings_type = Column( + "app_settings_type", String(255), nullable=True, unique=None, + default=None) + + repository = relationship('Repository') + + def __init__(self, repository_id, key='', val='', type='unicode'): + self.repository_id = repository_id + self.app_settings_name = key + self.app_settings_type = type + self.app_settings_value = val + + @validates('_app_settings_value') + def validate_settings_value(self, key, val): + assert type(val) == unicode + return val + + @hybrid_property + def app_settings_value(self): + v = self._app_settings_value + type_ = self.app_settings_type + SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES + converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode'] + return converter(v) + + @app_settings_value.setter + def app_settings_value(self, val): + """ + Setter that will always make sure we use unicode in app_settings_value + + :param val: + """ + self._app_settings_value = safe_unicode(val) + + @hybrid_property + def app_settings_type(self): + return self._app_settings_type + + @app_settings_type.setter + def app_settings_type(self, val): + SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES + if val not in SETTINGS_TYPES: + raise Exception('type must be one of %s got %s' + % (SETTINGS_TYPES.keys(), val)) + self._app_settings_type = val + + def __unicode__(self): + return u"<%s('%s:%s:%s[%s]')>" % ( + self.__class__.__name__, self.repository.repo_name, + self.app_settings_name, self.app_settings_value, + self.app_settings_type + ) + + +class RepoRhodeCodeUi(Base, BaseModel): + __tablename__ = 'repo_rhodecode_ui' + __table_args__ = ( + UniqueConstraint( + 'repository_id', 'ui_section', 'ui_key', + name='uq_repo_rhodecode_ui_repository_id_section_key'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + repository_id = Column( + "repository_id", Integer(), ForeignKey('repositories.repo_id'), + nullable=False) + ui_id = Column( + "ui_id", Integer(), nullable=False, unique=True, default=None, + primary_key=True) + ui_section = Column( + "ui_section", String(255), nullable=True, unique=None, default=None) + ui_key = Column( + "ui_key", String(255), nullable=True, unique=None, default=None) + ui_value = Column( + "ui_value", String(255), nullable=True, unique=None, default=None) + ui_active = Column( + "ui_active", Boolean(), nullable=True, unique=None, default=True) + + repository = relationship('Repository') + + def __repr__(self): + return '<%s[%s:%s]%s=>%s]>' % ( + self.__class__.__name__, self.repository.repo_name, + self.ui_section, self.ui_key, self.ui_value) + + +class User(Base, BaseModel): + __tablename__ = 'users' + __table_args__ = ( + UniqueConstraint('username'), UniqueConstraint('email'), + Index('u_username_idx', 'username'), + Index('u_email_idx', 'email'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + DEFAULT_USER = 'default' + DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org' + DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}' + + user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + username = Column("username", String(255), nullable=True, unique=None, default=None) + password = Column("password", String(255), nullable=True, unique=None, default=None) + active = Column("active", Boolean(), nullable=True, unique=None, default=True) + admin = Column("admin", Boolean(), nullable=True, unique=None, default=False) + name = Column("firstname", String(255), nullable=True, unique=None, default=None) + lastname = Column("lastname", String(255), nullable=True, unique=None, default=None) + _email = Column("email", String(255), nullable=True, unique=None, default=None) + last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None) + extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None) + extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None) + api_key = Column("api_key", String(255), nullable=True, unique=None, default=None) + inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data + + user_log = relationship('UserLog') + user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all') + + repositories = relationship('Repository') + repository_groups = relationship('RepoGroup') + user_groups = relationship('UserGroup') + + user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all') + followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all') + + repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all') + repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all') + user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all') + + group_member = relationship('UserGroupMember', cascade='all') + + notifications = relationship('UserNotification', cascade='all') + # notifications assigned to this user + user_created_notifications = relationship('Notification', cascade='all') + # comments created by this user + user_comments = relationship('ChangesetComment', cascade='all') + # user profile extra info + user_emails = relationship('UserEmailMap', cascade='all') + user_ip_map = relationship('UserIpMap', cascade='all') + user_auth_tokens = relationship('UserApiKeys', cascade='all') + # gists + user_gists = relationship('Gist', cascade='all') + # user pull requests + user_pull_requests = relationship('PullRequest', cascade='all') + # external identities + extenal_identities = relationship( + 'ExternalIdentity', + primaryjoin="User.user_id==ExternalIdentity.local_user_id", + cascade='all') + + def __unicode__(self): + return u"<%s('id:%s:%s')>" % (self.__class__.__name__, + self.user_id, self.username) + + @hybrid_property + def email(self): + return self._email + + @email.setter + def email(self, val): + self._email = val.lower() if val else None + + @property + def firstname(self): + # alias for future + return self.name + + @property + def emails(self): + other = UserEmailMap.query().filter(UserEmailMap.user==self).all() + return [self.email] + [x.email for x in other] + + @property + def auth_tokens(self): + return [self.api_key] + [x.api_key for x in self.extra_auth_tokens] + + @property + def extra_auth_tokens(self): + return UserApiKeys.query().filter(UserApiKeys.user == self).all() + + @property + def feed_token(self): + feed_tokens = UserApiKeys.query()\ + .filter(UserApiKeys.user == self)\ + .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\ + .all() + if feed_tokens: + return feed_tokens[0].api_key + else: + # use the main token so we don't end up with nothing... + return self.api_key + + @classmethod + def extra_valid_auth_tokens(cls, user, role=None): + tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\ + .filter(or_(UserApiKeys.expires == -1, + UserApiKeys.expires >= time.time())) + if role: + tokens = tokens.filter(or_(UserApiKeys.role == role, + UserApiKeys.role == UserApiKeys.ROLE_ALL)) + return tokens.all() + + @property + def ip_addresses(self): + ret = UserIpMap.query().filter(UserIpMap.user == self).all() + return [x.ip_addr for x in ret] + + @property + def username_and_name(self): + return '%s (%s %s)' % (self.username, self.firstname, self.lastname) + + @property + def username_or_name_or_email(self): + full_name = self.full_name if self.full_name is not ' ' else None + return self.username or full_name or self.email + + @property + def full_name(self): + return '%s %s' % (self.firstname, self.lastname) + + @property + def full_name_or_username(self): + return ('%s %s' % (self.firstname, self.lastname) + if (self.firstname and self.lastname) else self.username) + + @property + def full_contact(self): + return '%s %s <%s>' % (self.firstname, self.lastname, self.email) + + @property + def short_contact(self): + return '%s %s' % (self.firstname, self.lastname) + + @property + def is_admin(self): + return self.admin + + @property + def AuthUser(self): + """ + Returns instance of AuthUser for this user + """ + from rhodecode.lib.auth import AuthUser + return AuthUser(user_id=self.user_id, api_key=self.api_key, + username=self.username) + + @hybrid_property + def user_data(self): + if not self._user_data: + return {} + + try: + return json.loads(self._user_data) + except TypeError: + return {} + + @user_data.setter + def user_data(self, val): + if not isinstance(val, dict): + raise Exception('user_data must be dict, got %s' % type(val)) + try: + self._user_data = json.dumps(val) + except Exception: + log.error(traceback.format_exc()) + + @classmethod + def get_by_username(cls, username, case_insensitive=False, + cache=False, identity_cache=False): + session = Session() + + if case_insensitive: + q = cls.query().filter( + func.lower(cls.username) == func.lower(username)) + else: + q = cls.query().filter(cls.username == username) + + if cache: + if identity_cache: + val = cls.identity_cache(session, 'username', username) + if val: + return val + else: + q = q.options( + FromCache("sql_cache_short", + "get_user_by_name_%s" % _hash_key(username))) + + return q.scalar() + + @classmethod + def get_by_auth_token(cls, auth_token, cache=False, fallback=True): + q = cls.query().filter(cls.api_key == auth_token) + + if cache: + q = q.options(FromCache("sql_cache_short", + "get_auth_token_%s" % auth_token)) + res = q.scalar() + + if fallback and not res: + #fallback to additional keys + _res = UserApiKeys.query()\ + .filter(UserApiKeys.api_key == auth_token)\ + .filter(or_(UserApiKeys.expires == -1, + UserApiKeys.expires >= time.time()))\ + .first() + if _res: + res = _res.user + return res + + @classmethod + def get_by_email(cls, email, case_insensitive=False, cache=False): + + if case_insensitive: + q = cls.query().filter(func.lower(cls.email) == func.lower(email)) + + else: + q = cls.query().filter(cls.email == email) + + if cache: + q = q.options(FromCache("sql_cache_short", + "get_email_key_%s" % _hash_key(email))) + + ret = q.scalar() + if ret is None: + q = UserEmailMap.query() + # try fetching in alternate email map + if case_insensitive: + q = q.filter(func.lower(UserEmailMap.email) == func.lower(email)) + else: + q = q.filter(UserEmailMap.email == email) + q = q.options(joinedload(UserEmailMap.user)) + if cache: + q = q.options(FromCache("sql_cache_short", + "get_email_map_key_%s" % email)) + ret = getattr(q.scalar(), 'user', None) + + return ret + + @classmethod + def get_from_cs_author(cls, author): + """ + Tries to get User objects out of commit author string + + :param author: + """ + from rhodecode.lib.helpers import email, author_name + # Valid email in the attribute passed, see if they're in the system + _email = email(author) + if _email: + user = cls.get_by_email(_email, case_insensitive=True) + if user: + return user + # Maybe we can match by username? + _author = author_name(author) + user = cls.get_by_username(_author, case_insensitive=True) + if user: + return user + + def update_userdata(self, **kwargs): + usr = self + old = usr.user_data + old.update(**kwargs) + usr.user_data = old + Session().add(usr) + log.debug('updated userdata with ', kwargs) + + def update_lastlogin(self): + """Update user lastlogin""" + self.last_login = datetime.datetime.now() + Session().add(self) + log.debug('updated user %s lastlogin', self.username) + + def update_lastactivity(self): + """Update user lastactivity""" + usr = self + old = usr.user_data + old.update({'last_activity': time.time()}) + usr.user_data = old + Session().add(usr) + log.debug('updated user %s lastactivity', usr.username) + + def update_password(self, new_password, change_api_key=False): + from rhodecode.lib.auth import get_crypt_password,generate_auth_token + + self.password = get_crypt_password(new_password) + if change_api_key: + self.api_key = generate_auth_token(self.username) + Session().add(self) + + @classmethod + def get_first_super_admin(cls): + user = User.query().filter(User.admin == true()).first() + if user is None: + raise Exception('FATAL: Missing administrative account!') + return user + + @classmethod + def get_all_super_admins(cls): + """ + Returns all admin accounts sorted by username + """ + return User.query().filter(User.admin == true())\ + .order_by(User.username.asc()).all() + + @classmethod + def get_default_user(cls, cache=False): + user = User.get_by_username(User.DEFAULT_USER, cache=cache) + if user is None: + raise Exception('FATAL: Missing default account!') + return user + + def _get_default_perms(self, user, suffix=''): + from rhodecode.model.permission import PermissionModel + return PermissionModel().get_default_perms(user.user_perms, suffix) + + def get_default_perms(self, suffix=''): + return self._get_default_perms(self, suffix) + + def get_api_data(self, include_secrets=False, details='full'): + """ + Common function for generating user related data for API + + :param include_secrets: By default secrets in the API data will be replaced + by a placeholder value to prevent exposing this data by accident. In case + this data shall be exposed, set this flag to ``True``. + + :param details: details can be 'basic|full' basic gives only a subset of + the available user information that includes user_id, name and emails. + """ + user = self + user_data = self.user_data + data = { + 'user_id': user.user_id, + 'username': user.username, + 'firstname': user.name, + 'lastname': user.lastname, + 'email': user.email, + 'emails': user.emails, + } + if details == 'basic': + return data + + api_key_length = 40 + api_key_replacement = '*' * api_key_length + + extras = { + 'api_key': api_key_replacement, + 'api_keys': [api_key_replacement], + 'active': user.active, + 'admin': user.admin, + 'extern_type': user.extern_type, + 'extern_name': user.extern_name, + 'last_login': user.last_login, + 'ip_addresses': user.ip_addresses, + 'language': user_data.get('language') + } + data.update(extras) + + if include_secrets: + data['api_key'] = user.api_key + data['api_keys'] = user.auth_tokens + return data + + def __json__(self): + data = { + 'full_name': self.full_name, + 'full_name_or_username': self.full_name_or_username, + 'short_contact': self.short_contact, + 'full_contact': self.full_contact, + } + data.update(self.get_api_data()) + return data + + +class UserApiKeys(Base, BaseModel): + __tablename__ = 'user_api_keys' + __table_args__ = ( + Index('uak_api_key_idx', 'api_key'), + Index('uak_api_key_expires_idx', 'api_key', 'expires'), + UniqueConstraint('api_key'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + __mapper_args__ = {} + + # ApiKey role + ROLE_ALL = 'token_role_all' + ROLE_HTTP = 'token_role_http' + ROLE_VCS = 'token_role_vcs' + ROLE_API = 'token_role_api' + ROLE_FEED = 'token_role_feed' + ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED] + + user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) + api_key = Column("api_key", String(255), nullable=False, unique=True) + description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql')) + expires = Column('expires', Float(53), nullable=False) + role = Column('role', String(255), nullable=True) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + + user = relationship('User', lazy='joined') + + @classmethod + def _get_role_name(cls, role): + return { + cls.ROLE_ALL: _('all'), + cls.ROLE_HTTP: _('http/web interface'), + cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'), + cls.ROLE_API: _('api calls'), + cls.ROLE_FEED: _('feed access'), + }.get(role, role) + + @property + def expired(self): + if self.expires == -1: + return False + return time.time() > self.expires + + @property + def role_humanized(self): + return self._get_role_name(self.role) + + +class UserEmailMap(Base, BaseModel): + __tablename__ = 'user_email_map' + __table_args__ = ( + Index('uem_email_idx', 'email'), + UniqueConstraint('email'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + __mapper_args__ = {} + + email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) + _email = Column("email", String(255), nullable=True, unique=False, default=None) + user = relationship('User', lazy='joined') + + @validates('_email') + def validate_email(self, key, email): + # check if this email is not main one + main_email = Session().query(User).filter(User.email == email).scalar() + if main_email is not None: + raise AttributeError('email %s is present is user table' % email) + return email + + @hybrid_property + def email(self): + return self._email + + @email.setter + def email(self, val): + self._email = val.lower() if val else None + + +class UserIpMap(Base, BaseModel): + __tablename__ = 'user_ip_map' + __table_args__ = ( + UniqueConstraint('user_id', 'ip_addr'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + __mapper_args__ = {} + + ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) + ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None) + active = Column("active", Boolean(), nullable=True, unique=None, default=True) + description = Column("description", String(10000), nullable=True, unique=None, default=None) + user = relationship('User', lazy='joined') + + @classmethod + def _get_ip_range(cls, ip_addr): + net = ipaddress.ip_network(ip_addr, strict=False) + return [str(net.network_address), str(net.broadcast_address)] + + def __json__(self): + return { + 'ip_addr': self.ip_addr, + 'ip_range': self._get_ip_range(self.ip_addr), + } + + def __unicode__(self): + return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__, + self.user_id, self.ip_addr) + +class UserLog(Base, BaseModel): + __tablename__ = 'user_logs' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) + username = Column("username", String(255), nullable=True, unique=None, default=None) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True) + repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None) + user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None) + action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None) + action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None) + + def __unicode__(self): + return u"<%s('id:%s:%s')>" % (self.__class__.__name__, + self.repository_name, + self.action) + + @property + def action_as_day(self): + return datetime.date(*self.action_date.timetuple()[:3]) + + user = relationship('User') + repository = relationship('Repository', cascade='') + + +class UserGroup(Base, BaseModel): + __tablename__ = 'users_groups' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + + users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None) + user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None) + users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None) + inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data + + members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined") + users_group_to_perm = relationship('UserGroupToPerm', cascade='all') + users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all') + users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all') + user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all') + user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all') + + user = relationship('User') + + @hybrid_property + def group_data(self): + if not self._group_data: + return {} + + try: + return json.loads(self._group_data) + except TypeError: + return {} + + @group_data.setter + def group_data(self, val): + try: + self._group_data = json.dumps(val) + except Exception: + log.error(traceback.format_exc()) + + def __unicode__(self): + return u"<%s('id:%s:%s')>" % (self.__class__.__name__, + self.users_group_id, + self.users_group_name) + + @classmethod + def get_by_group_name(cls, group_name, cache=False, + case_insensitive=False): + if case_insensitive: + q = cls.query().filter(func.lower(cls.users_group_name) == + func.lower(group_name)) + + else: + q = cls.query().filter(cls.users_group_name == group_name) + if cache: + q = q.options(FromCache( + "sql_cache_short", + "get_group_%s" % _hash_key(group_name))) + return q.scalar() + + @classmethod + def get(cls, user_group_id, cache=False): + user_group = cls.query() + if cache: + user_group = user_group.options(FromCache("sql_cache_short", + "get_users_group_%s" % user_group_id)) + return user_group.get(user_group_id) + + def permissions(self, with_admins=True, with_owner=True): + q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self) + q = q.options(joinedload(UserUserGroupToPerm.user_group), + joinedload(UserUserGroupToPerm.user), + joinedload(UserUserGroupToPerm.permission),) + + # get owners and admins and permissions. We do a trick of re-writing + # objects from sqlalchemy to named-tuples due to sqlalchemy session + # has a global reference and changing one object propagates to all + # others. This means if admin is also an owner admin_row that change + # would propagate to both objects + perm_rows = [] + for _usr in q.all(): + usr = AttributeDict(_usr.user.get_dict()) + usr.permission = _usr.permission.permission_name + perm_rows.append(usr) + + # filter the perm rows by 'default' first and then sort them by + # admin,write,read,none permissions sorted again alphabetically in + # each group + perm_rows = sorted(perm_rows, key=display_sort) + + _admin_perm = 'usergroup.admin' + owner_row = [] + if with_owner: + usr = AttributeDict(self.user.get_dict()) + usr.owner_row = True + usr.permission = _admin_perm + owner_row.append(usr) + + super_admin_rows = [] + if with_admins: + for usr in User.get_all_super_admins(): + # if this admin is also owner, don't double the record + if usr.user_id == owner_row[0].user_id: + owner_row[0].admin_row = True + else: + usr = AttributeDict(usr.get_dict()) + usr.admin_row = True + usr.permission = _admin_perm + super_admin_rows.append(usr) + + return super_admin_rows + owner_row + perm_rows + + def permission_user_groups(self): + q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self) + q = q.options(joinedload(UserGroupUserGroupToPerm.user_group), + joinedload(UserGroupUserGroupToPerm.target_user_group), + joinedload(UserGroupUserGroupToPerm.permission),) + + perm_rows = [] + for _user_group in q.all(): + usr = AttributeDict(_user_group.user_group.get_dict()) + usr.permission = _user_group.permission.permission_name + perm_rows.append(usr) + + return perm_rows + + def _get_default_perms(self, user_group, suffix=''): + from rhodecode.model.permission import PermissionModel + return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix) + + def get_default_perms(self, suffix=''): + return self._get_default_perms(self, suffix) + + def get_api_data(self, with_group_members=True, include_secrets=False): + """ + :param include_secrets: See :meth:`User.get_api_data`, this parameter is + basically forwarded. + + """ + user_group = self + + data = { + 'users_group_id': user_group.users_group_id, + 'group_name': user_group.users_group_name, + 'group_description': user_group.user_group_description, + 'active': user_group.users_group_active, + 'owner': user_group.user.username, + } + if with_group_members: + users = [] + for user in user_group.members: + user = user.user + users.append(user.get_api_data(include_secrets=include_secrets)) + data['users'] = users + + return data + + +class UserGroupMember(Base, BaseModel): + __tablename__ = 'users_groups_members' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + + users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + + user = relationship('User', lazy='joined') + users_group = relationship('UserGroup') + + def __init__(self, gr_id='', u_id=''): + self.users_group_id = gr_id + self.user_id = u_id + + +class RepositoryField(Base, BaseModel): + __tablename__ = 'repositories_fields' + __table_args__ = ( + UniqueConstraint('repository_id', 'field_key'), # no-multi field + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields + + repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) + field_key = Column("field_key", String(250)) + field_label = Column("field_label", String(1024), nullable=False) + field_value = Column("field_value", String(10000), nullable=False) + field_desc = Column("field_desc", String(1024), nullable=False) + field_type = Column("field_type", String(255), nullable=False, unique=None) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + + repository = relationship('Repository') + + @property + def field_key_prefixed(self): + return 'ex_%s' % self.field_key + + @classmethod + def un_prefix_key(cls, key): + if key.startswith(cls.PREFIX): + return key[len(cls.PREFIX):] + return key + + @classmethod + def get_by_key_name(cls, key, repo): + row = cls.query()\ + .filter(cls.repository == repo)\ + .filter(cls.field_key == key).scalar() + return row + + +class Repository(Base, BaseModel): + __tablename__ = 'repositories' + __table_args__ = ( + Index('r_repo_name_idx', 'repo_name', mysql_length=255), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}' + DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}' + + STATE_CREATED = 'repo_state_created' + STATE_PENDING = 'repo_state_pending' + STATE_ERROR = 'repo_state_error' + + LOCK_AUTOMATIC = 'lock_auto' + LOCK_API = 'lock_api' + LOCK_WEB = 'lock_web' + LOCK_PULL = 'lock_pull' + + NAME_SEP = URL_SEP + + repo_id = Column( + "repo_id", Integer(), nullable=False, unique=True, default=None, + primary_key=True) + _repo_name = Column( + "repo_name", Text(), nullable=False, default=None) + _repo_name_hash = Column( + "repo_name_hash", String(255), nullable=False, unique=True) + repo_state = Column("repo_state", String(255), nullable=True) + + clone_uri = Column( + "clone_uri", EncryptedTextValue(), nullable=True, unique=False, + default=None) + repo_type = Column( + "repo_type", String(255), nullable=False, unique=False, default=None) + user_id = Column( + "user_id", Integer(), ForeignKey('users.user_id'), nullable=False, + unique=False, default=None) + private = Column( + "private", Boolean(), nullable=True, unique=None, default=None) + enable_statistics = Column( + "statistics", Boolean(), nullable=True, unique=None, default=True) + enable_downloads = Column( + "downloads", Boolean(), nullable=True, unique=None, default=True) + description = Column( + "description", String(10000), nullable=True, unique=None, default=None) + created_on = Column( + 'created_on', DateTime(timezone=False), nullable=True, unique=None, + default=datetime.datetime.now) + updated_on = Column( + 'updated_on', DateTime(timezone=False), nullable=True, unique=None, + default=datetime.datetime.now) + _landing_revision = Column( + "landing_revision", String(255), nullable=False, unique=False, + default=None) + enable_locking = Column( + "enable_locking", Boolean(), nullable=False, unique=None, + default=False) + _locked = Column( + "locked", String(255), nullable=True, unique=False, default=None) + _changeset_cache = Column( + "changeset_cache", LargeBinary(), nullable=True) # JSON data + + fork_id = Column( + "fork_id", Integer(), ForeignKey('repositories.repo_id'), + nullable=True, unique=False, default=None) + group_id = Column( + "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, + unique=False, default=None) + + user = relationship('User', lazy='joined') + fork = relationship('Repository', remote_side=repo_id, lazy='joined') + group = relationship('RepoGroup', lazy='joined') + repo_to_perm = relationship( + 'UserRepoToPerm', cascade='all', + order_by='UserRepoToPerm.repo_to_perm_id') + users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all') + stats = relationship('Statistics', cascade='all', uselist=False) + + followers = relationship( + 'UserFollowing', + primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', + cascade='all') + extra_fields = relationship( + 'RepositoryField', cascade="all, delete, delete-orphan") + logs = relationship('UserLog') + comments = relationship( + 'ChangesetComment', cascade="all, delete, delete-orphan") + pull_requests_source = relationship( + 'PullRequest', + primaryjoin='PullRequest.source_repo_id==Repository.repo_id', + cascade="all, delete, delete-orphan") + pull_requests_target = relationship( + 'PullRequest', + primaryjoin='PullRequest.target_repo_id==Repository.repo_id', + cascade="all, delete, delete-orphan") + ui = relationship('RepoRhodeCodeUi', cascade="all") + settings = relationship('RepoRhodeCodeSetting', cascade="all") + integrations = relationship('Integration', + cascade="all, delete, delete-orphan") + + def __unicode__(self): + return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id, + safe_unicode(self.repo_name)) + + @hybrid_property + def landing_rev(self): + # always should return [rev_type, rev] + if self._landing_revision: + _rev_info = self._landing_revision.split(':') + if len(_rev_info) < 2: + _rev_info.insert(0, 'rev') + return [_rev_info[0], _rev_info[1]] + return [None, None] + + @landing_rev.setter + def landing_rev(self, val): + if ':' not in val: + raise ValueError('value must be delimited with `:` and consist ' + 'of :, got %s instead' % val) + self._landing_revision = val + + @hybrid_property + def locked(self): + if self._locked: + user_id, timelocked, reason = self._locked.split(':') + lock_values = int(user_id), timelocked, reason + else: + lock_values = [None, None, None] + return lock_values + + @locked.setter + def locked(self, val): + if val and isinstance(val, (list, tuple)): + self._locked = ':'.join(map(str, val)) + else: + self._locked = None + + @hybrid_property + def changeset_cache(self): + from rhodecode.lib.vcs.backends.base import EmptyCommit + dummy = EmptyCommit().__json__() + if not self._changeset_cache: + return dummy + try: + return json.loads(self._changeset_cache) + except TypeError: + return dummy + except Exception: + log.error(traceback.format_exc()) + return dummy + + @changeset_cache.setter + def changeset_cache(self, val): + try: + self._changeset_cache = json.dumps(val) + except Exception: + log.error(traceback.format_exc()) + + @hybrid_property + def repo_name(self): + return self._repo_name + + @repo_name.setter + def repo_name(self, value): + self._repo_name = value + self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest() + + @classmethod + def normalize_repo_name(cls, repo_name): + """ + Normalizes os specific repo_name to the format internally stored inside + database using URL_SEP + + :param cls: + :param repo_name: + """ + return cls.NAME_SEP.join(repo_name.split(os.sep)) + + @classmethod + def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False): + session = Session() + q = session.query(cls).filter(cls.repo_name == repo_name) + + if cache: + if identity_cache: + val = cls.identity_cache(session, 'repo_name', repo_name) + if val: + return val + else: + q = q.options( + FromCache("sql_cache_short", + "get_repo_by_name_%s" % _hash_key(repo_name))) + + return q.scalar() + + @classmethod + def get_by_full_path(cls, repo_full_path): + repo_name = repo_full_path.split(cls.base_path(), 1)[-1] + repo_name = cls.normalize_repo_name(repo_name) + return cls.get_by_repo_name(repo_name.strip(URL_SEP)) + + @classmethod + def get_repo_forks(cls, repo_id): + return cls.query().filter(Repository.fork_id == repo_id) + + @classmethod + def base_path(cls): + """ + Returns base path when all repos are stored + + :param cls: + """ + q = Session().query(RhodeCodeUi)\ + .filter(RhodeCodeUi.ui_key == cls.NAME_SEP) + q = q.options(FromCache("sql_cache_short", "repository_repo_path")) + return q.one().ui_value + + @classmethod + def is_valid(cls, repo_name): + """ + returns True if given repo name is a valid filesystem repository + + :param cls: + :param repo_name: + """ + from rhodecode.lib.utils import is_valid_repo + + return is_valid_repo(repo_name, cls.base_path()) + + @classmethod + def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None), + case_insensitive=True): + q = Repository.query() + + if not isinstance(user_id, Optional): + q = q.filter(Repository.user_id == user_id) + + if not isinstance(group_id, Optional): + q = q.filter(Repository.group_id == group_id) + + if case_insensitive: + q = q.order_by(func.lower(Repository.repo_name)) + else: + q = q.order_by(Repository.repo_name) + return q.all() + + @property + def forks(self): + """ + Return forks of this repo + """ + return Repository.get_repo_forks(self.repo_id) + + @property + def parent(self): + """ + Returns fork parent + """ + return self.fork + + @property + def just_name(self): + return self.repo_name.split(self.NAME_SEP)[-1] + + @property + def groups_with_parents(self): + groups = [] + if self.group is None: + return groups + + cur_gr = self.group + groups.insert(0, cur_gr) + while 1: + gr = getattr(cur_gr, 'parent_group', None) + cur_gr = cur_gr.parent_group + if gr is None: + break + groups.insert(0, gr) + + return groups + + @property + def groups_and_repo(self): + return self.groups_with_parents, self + + @LazyProperty + def repo_path(self): + """ + Returns base full path for that repository means where it actually + exists on a filesystem + """ + q = Session().query(RhodeCodeUi).filter( + RhodeCodeUi.ui_key == self.NAME_SEP) + q = q.options(FromCache("sql_cache_short", "repository_repo_path")) + return q.one().ui_value + + @property + def repo_full_path(self): + p = [self.repo_path] + # we need to split the name by / since this is how we store the + # names in the database, but that eventually needs to be converted + # into a valid system path + p += self.repo_name.split(self.NAME_SEP) + return os.path.join(*map(safe_unicode, p)) + + @property + def cache_keys(self): + """ + Returns associated cache keys for that repo + """ + return CacheKey.query()\ + .filter(CacheKey.cache_args == self.repo_name)\ + .order_by(CacheKey.cache_key)\ + .all() + + def get_new_name(self, repo_name): + """ + returns new full repository name based on assigned group and new new + + :param group_name: + """ + path_prefix = self.group.full_path_splitted if self.group else [] + return self.NAME_SEP.join(path_prefix + [repo_name]) + + @property + def _config(self): + """ + Returns db based config object. + """ + from rhodecode.lib.utils import make_db_config + return make_db_config(clear_session=False, repo=self) + + def permissions(self, with_admins=True, with_owner=True): + q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self) + q = q.options(joinedload(UserRepoToPerm.repository), + joinedload(UserRepoToPerm.user), + joinedload(UserRepoToPerm.permission),) + + # get owners and admins and permissions. We do a trick of re-writing + # objects from sqlalchemy to named-tuples due to sqlalchemy session + # has a global reference and changing one object propagates to all + # others. This means if admin is also an owner admin_row that change + # would propagate to both objects + perm_rows = [] + for _usr in q.all(): + usr = AttributeDict(_usr.user.get_dict()) + usr.permission = _usr.permission.permission_name + perm_rows.append(usr) + + # filter the perm rows by 'default' first and then sort them by + # admin,write,read,none permissions sorted again alphabetically in + # each group + perm_rows = sorted(perm_rows, key=display_sort) + + _admin_perm = 'repository.admin' + owner_row = [] + if with_owner: + usr = AttributeDict(self.user.get_dict()) + usr.owner_row = True + usr.permission = _admin_perm + owner_row.append(usr) + + super_admin_rows = [] + if with_admins: + for usr in User.get_all_super_admins(): + # if this admin is also owner, don't double the record + if usr.user_id == owner_row[0].user_id: + owner_row[0].admin_row = True + else: + usr = AttributeDict(usr.get_dict()) + usr.admin_row = True + usr.permission = _admin_perm + super_admin_rows.append(usr) + + return super_admin_rows + owner_row + perm_rows + + def permission_user_groups(self): + q = UserGroupRepoToPerm.query().filter( + UserGroupRepoToPerm.repository == self) + q = q.options(joinedload(UserGroupRepoToPerm.repository), + joinedload(UserGroupRepoToPerm.users_group), + joinedload(UserGroupRepoToPerm.permission),) + + perm_rows = [] + for _user_group in q.all(): + usr = AttributeDict(_user_group.users_group.get_dict()) + usr.permission = _user_group.permission.permission_name + perm_rows.append(usr) + + return perm_rows + + def get_api_data(self, include_secrets=False): + """ + Common function for generating repo api data + + :param include_secrets: See :meth:`User.get_api_data`. + + """ + # TODO: mikhail: Here there is an anti-pattern, we probably need to + # move this methods on models level. + from rhodecode.model.settings import SettingsModel + + repo = self + _user_id, _time, _reason = self.locked + + data = { + 'repo_id': repo.repo_id, + 'repo_name': repo.repo_name, + 'repo_type': repo.repo_type, + 'clone_uri': repo.clone_uri or '', + 'url': url('summary_home', repo_name=self.repo_name, qualified=True), + 'private': repo.private, + 'created_on': repo.created_on, + 'description': repo.description, + 'landing_rev': repo.landing_rev, + 'owner': repo.user.username, + 'fork_of': repo.fork.repo_name if repo.fork else None, + 'enable_statistics': repo.enable_statistics, + 'enable_locking': repo.enable_locking, + 'enable_downloads': repo.enable_downloads, + 'last_changeset': repo.changeset_cache, + 'locked_by': User.get(_user_id).get_api_data( + include_secrets=include_secrets) if _user_id else None, + 'locked_date': time_to_datetime(_time) if _time else None, + 'lock_reason': _reason if _reason else None, + } + + # TODO: mikhail: should be per-repo settings here + rc_config = SettingsModel().get_all_settings() + repository_fields = str2bool( + rc_config.get('rhodecode_repository_fields')) + if repository_fields: + for f in self.extra_fields: + data[f.field_key_prefixed] = f.field_value + + return data + + @classmethod + def lock(cls, repo, user_id, lock_time=None, lock_reason=None): + if not lock_time: + lock_time = time.time() + if not lock_reason: + lock_reason = cls.LOCK_AUTOMATIC + repo.locked = [user_id, lock_time, lock_reason] + Session().add(repo) + Session().commit() + + @classmethod + def unlock(cls, repo): + repo.locked = None + Session().add(repo) + Session().commit() + + @classmethod + def getlock(cls, repo): + return repo.locked + + def is_user_lock(self, user_id): + if self.lock[0]: + lock_user_id = safe_int(self.lock[0]) + user_id = safe_int(user_id) + # both are ints, and they are equal + return all([lock_user_id, user_id]) and lock_user_id == user_id + + return False + + def get_locking_state(self, action, user_id, only_when_enabled=True): + """ + Checks locking on this repository, if locking is enabled and lock is + present returns a tuple of make_lock, locked, locked_by. + make_lock can have 3 states None (do nothing) True, make lock + False release lock, This value is later propagated to hooks, which + do the locking. Think about this as signals passed to hooks what to do. + + """ + # TODO: johbo: This is part of the business logic and should be moved + # into the RepositoryModel. + + if action not in ('push', 'pull'): + raise ValueError("Invalid action value: %s" % repr(action)) + + # defines if locked error should be thrown to user + currently_locked = False + # defines if new lock should be made, tri-state + make_lock = None + repo = self + user = User.get(user_id) + + lock_info = repo.locked + + if repo and (repo.enable_locking or not only_when_enabled): + if action == 'push': + # check if it's already locked !, if it is compare users + locked_by_user_id = lock_info[0] + if user.user_id == locked_by_user_id: + log.debug( + 'Got `push` action from user %s, now unlocking', user) + # unlock if we have push from user who locked + make_lock = False + else: + # we're not the same user who locked, ban with + # code defined in settings (default is 423 HTTP Locked) ! + log.debug('Repo %s is currently locked by %s', repo, user) + currently_locked = True + elif action == 'pull': + # [0] user [1] date + if lock_info[0] and lock_info[1]: + log.debug('Repo %s is currently locked by %s', repo, user) + currently_locked = True + else: + log.debug('Setting lock on repo %s by %s', repo, user) + make_lock = True + + else: + log.debug('Repository %s do not have locking enabled', repo) + + log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s', + make_lock, currently_locked, lock_info) + + from rhodecode.lib.auth import HasRepoPermissionAny + perm_check = HasRepoPermissionAny('repository.write', 'repository.admin') + if make_lock and not perm_check(repo_name=repo.repo_name, user=user): + # if we don't have at least write permission we cannot make a lock + log.debug('lock state reset back to FALSE due to lack ' + 'of at least read permission') + make_lock = False + + return make_lock, currently_locked, lock_info + + @property + def last_db_change(self): + return self.updated_on + + @property + def clone_uri_hidden(self): + clone_uri = self.clone_uri + if clone_uri: + import urlobject + url_obj = urlobject.URLObject(clone_uri) + if url_obj.password: + clone_uri = url_obj.with_password('*****') + return clone_uri + + def clone_url(self, **override): + qualified_home_url = url('home', qualified=True) + + uri_tmpl = None + if 'with_id' in override: + uri_tmpl = self.DEFAULT_CLONE_URI_ID + del override['with_id'] + + if 'uri_tmpl' in override: + uri_tmpl = override['uri_tmpl'] + del override['uri_tmpl'] + + # we didn't override our tmpl from **overrides + if not uri_tmpl: + uri_tmpl = self.DEFAULT_CLONE_URI + try: + from pylons import tmpl_context as c + uri_tmpl = c.clone_uri_tmpl + except Exception: + # in any case if we call this outside of request context, + # ie, not having tmpl_context set up + pass + + return get_clone_url(uri_tmpl=uri_tmpl, + qualifed_home_url=qualified_home_url, + repo_name=self.repo_name, + repo_id=self.repo_id, **override) + + def set_state(self, state): + self.repo_state = state + Session().add(self) + #========================================================================== + # SCM PROPERTIES + #========================================================================== + + def get_commit(self, commit_id=None, commit_idx=None, pre_load=None): + return get_commit_safe( + self.scm_instance(), commit_id, commit_idx, pre_load=pre_load) + + def get_changeset(self, rev=None, pre_load=None): + warnings.warn("Use get_commit", DeprecationWarning) + commit_id = None + commit_idx = None + if isinstance(rev, basestring): + commit_id = rev + else: + commit_idx = rev + return self.get_commit(commit_id=commit_id, commit_idx=commit_idx, + pre_load=pre_load) + + def get_landing_commit(self): + """ + Returns landing commit, or if that doesn't exist returns the tip + """ + _rev_type, _rev = self.landing_rev + commit = self.get_commit(_rev) + if isinstance(commit, EmptyCommit): + return self.get_commit() + return commit + + def update_commit_cache(self, cs_cache=None, config=None): + """ + Update cache of last changeset for repository, keys should be:: + + short_id + raw_id + revision + parents + message + date + author + + :param cs_cache: + """ + from rhodecode.lib.vcs.backends.base import BaseChangeset + if cs_cache is None: + # use no-cache version here + scm_repo = self.scm_instance(cache=False, config=config) + if scm_repo: + cs_cache = scm_repo.get_commit( + pre_load=["author", "date", "message", "parents"]) + else: + cs_cache = EmptyCommit() + + if isinstance(cs_cache, BaseChangeset): + cs_cache = cs_cache.__json__() + + def is_outdated(new_cs_cache): + if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or + new_cs_cache['revision'] != self.changeset_cache['revision']): + return True + return False + + # check if we have maybe already latest cached revision + if is_outdated(cs_cache) or not self.changeset_cache: + _default = datetime.datetime.fromtimestamp(0) + last_change = cs_cache.get('date') or _default + log.debug('updated repo %s with new cs cache %s', + self.repo_name, cs_cache) + self.updated_on = last_change + self.changeset_cache = cs_cache + Session().add(self) + Session().commit() + else: + log.debug('Skipping update_commit_cache for repo:`%s` ' + 'commit already with latest changes', self.repo_name) + + @property + def tip(self): + return self.get_commit('tip') + + @property + def author(self): + return self.tip.author + + @property + def last_change(self): + return self.scm_instance().last_change + + def get_comments(self, revisions=None): + """ + Returns comments for this repository grouped by revisions + + :param revisions: filter query by revisions only + """ + cmts = ChangesetComment.query()\ + .filter(ChangesetComment.repo == self) + if revisions: + cmts = cmts.filter(ChangesetComment.revision.in_(revisions)) + grouped = collections.defaultdict(list) + for cmt in cmts.all(): + grouped[cmt.revision].append(cmt) + return grouped + + def statuses(self, revisions=None): + """ + Returns statuses for this repository + + :param revisions: list of revisions to get statuses for + """ + statuses = ChangesetStatus.query()\ + .filter(ChangesetStatus.repo == self)\ + .filter(ChangesetStatus.version == 0) + + if revisions: + # Try doing the filtering in chunks to avoid hitting limits + size = 500 + status_results = [] + for chunk in xrange(0, len(revisions), size): + status_results += statuses.filter( + ChangesetStatus.revision.in_( + revisions[chunk: chunk+size]) + ).all() + else: + status_results = statuses.all() + + grouped = {} + + # maybe we have open new pullrequest without a status? + stat = ChangesetStatus.STATUS_UNDER_REVIEW + status_lbl = ChangesetStatus.get_status_lbl(stat) + for pr in PullRequest.query().filter(PullRequest.source_repo == self).all(): + for rev in pr.revisions: + pr_id = pr.pull_request_id + pr_repo = pr.target_repo.repo_name + grouped[rev] = [stat, status_lbl, pr_id, pr_repo] + + for stat in status_results: + pr_id = pr_repo = None + if stat.pull_request: + pr_id = stat.pull_request.pull_request_id + pr_repo = stat.pull_request.target_repo.repo_name + grouped[stat.revision] = [str(stat.status), stat.status_lbl, + pr_id, pr_repo] + return grouped + + # ========================================================================== + # SCM CACHE INSTANCE + # ========================================================================== + + def scm_instance(self, **kwargs): + import rhodecode + + # Passing a config will not hit the cache currently only used + # for repo2dbmapper + config = kwargs.pop('config', None) + cache = kwargs.pop('cache', None) + full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache')) + # if cache is NOT defined use default global, else we have a full + # control over cache behaviour + if cache is None and full_cache and not config: + return self._get_instance_cached() + return self._get_instance(cache=bool(cache), config=config) + + def _get_instance_cached(self): + @cache_region('long_term') + def _get_repo(cache_key): + return self._get_instance() + + invalidator_context = CacheKey.repo_context_cache( + _get_repo, self.repo_name, None, thread_scoped=True) + + with invalidator_context as context: + context.invalidate() + repo = context.compute() + + return repo + + def _get_instance(self, cache=True, config=None): + config = config or self._config + custom_wire = { + 'cache': cache # controls the vcs.remote cache + } + + repo = get_vcs_instance( + repo_path=safe_str(self.repo_full_path), + config=config, + with_wire=custom_wire, + create=False) + + return repo + + def __json__(self): + return {'landing_rev': self.landing_rev} + + def get_dict(self): + + # Since we transformed `repo_name` to a hybrid property, we need to + # keep compatibility with the code which uses `repo_name` field. + + result = super(Repository, self).get_dict() + result['repo_name'] = result.pop('_repo_name', None) + return result + + +class RepoGroup(Base, BaseModel): + __tablename__ = 'groups' + __table_args__ = ( + UniqueConstraint('group_name', 'group_parent_id'), + CheckConstraint('group_id != group_parent_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + __mapper_args__ = {'order_by': 'group_name'} + + CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups + + group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + group_name = Column("group_name", String(255), nullable=False, unique=True, default=None) + group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None) + group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None) + enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + + repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id') + users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all') + parent_group = relationship('RepoGroup', remote_side=group_id) + user = relationship('User') + + def __init__(self, group_name='', parent_group=None): + self.group_name = group_name + self.parent_group = parent_group + + def __unicode__(self): + return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id, + self.group_name) + + @classmethod + def _generate_choice(cls, repo_group): + from webhelpers.html import literal as _literal + _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k)) + return repo_group.group_id, _name(repo_group.full_path_splitted) + + @classmethod + def groups_choices(cls, groups=None, show_empty_group=True): + if not groups: + groups = cls.query().all() + + repo_groups = [] + if show_empty_group: + repo_groups = [('-1', u'-- %s --' % _('No parent'))] + + repo_groups.extend([cls._generate_choice(x) for x in groups]) + + repo_groups = sorted( + repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0]) + return repo_groups + + @classmethod + def url_sep(cls): + return URL_SEP + + @classmethod + def get_by_group_name(cls, group_name, cache=False, case_insensitive=False): + if case_insensitive: + gr = cls.query().filter(func.lower(cls.group_name) + == func.lower(group_name)) + else: + gr = cls.query().filter(cls.group_name == group_name) + if cache: + gr = gr.options(FromCache( + "sql_cache_short", + "get_group_%s" % _hash_key(group_name))) + return gr.scalar() + + @classmethod + def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None), + case_insensitive=True): + q = RepoGroup.query() + + if not isinstance(user_id, Optional): + q = q.filter(RepoGroup.user_id == user_id) + + if not isinstance(group_id, Optional): + q = q.filter(RepoGroup.group_parent_id == group_id) + + if case_insensitive: + q = q.order_by(func.lower(RepoGroup.group_name)) + else: + q = q.order_by(RepoGroup.group_name) + return q.all() + + @property + def parents(self): + parents_recursion_limit = 10 + groups = [] + if self.parent_group is None: + return groups + cur_gr = self.parent_group + groups.insert(0, cur_gr) + cnt = 0 + while 1: + cnt += 1 + gr = getattr(cur_gr, 'parent_group', None) + cur_gr = cur_gr.parent_group + if gr is None: + break + if cnt == parents_recursion_limit: + # this will prevent accidental infinit loops + log.error(('more than %s parents found for group %s, stopping ' + 'recursive parent fetching' % (parents_recursion_limit, self))) + break + + groups.insert(0, gr) + return groups + + @property + def children(self): + return RepoGroup.query().filter(RepoGroup.parent_group == self) + + @property + def name(self): + return self.group_name.split(RepoGroup.url_sep())[-1] + + @property + def full_path(self): + return self.group_name + + @property + def full_path_splitted(self): + return self.group_name.split(RepoGroup.url_sep()) + + @property + def repositories(self): + return Repository.query()\ + .filter(Repository.group == self)\ + .order_by(Repository.repo_name) + + @property + def repositories_recursive_count(self): + cnt = self.repositories.count() + + def children_count(group): + cnt = 0 + for child in group.children: + cnt += child.repositories.count() + cnt += children_count(child) + return cnt + + return cnt + children_count(self) + + def _recursive_objects(self, include_repos=True): + all_ = [] + + def _get_members(root_gr): + if include_repos: + for r in root_gr.repositories: + all_.append(r) + childs = root_gr.children.all() + if childs: + for gr in childs: + all_.append(gr) + _get_members(gr) + + _get_members(self) + return [self] + all_ + + def recursive_groups_and_repos(self): + """ + Recursive return all groups, with repositories in those groups + """ + return self._recursive_objects() + + def recursive_groups(self): + """ + Returns all children groups for this group including children of children + """ + return self._recursive_objects(include_repos=False) + + def get_new_name(self, group_name): + """ + returns new full group name based on parent and new name + + :param group_name: + """ + path_prefix = (self.parent_group.full_path_splitted if + self.parent_group else []) + return RepoGroup.url_sep().join(path_prefix + [group_name]) + + def permissions(self, with_admins=True, with_owner=True): + q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self) + q = q.options(joinedload(UserRepoGroupToPerm.group), + joinedload(UserRepoGroupToPerm.user), + joinedload(UserRepoGroupToPerm.permission),) + + # get owners and admins and permissions. We do a trick of re-writing + # objects from sqlalchemy to named-tuples due to sqlalchemy session + # has a global reference and changing one object propagates to all + # others. This means if admin is also an owner admin_row that change + # would propagate to both objects + perm_rows = [] + for _usr in q.all(): + usr = AttributeDict(_usr.user.get_dict()) + usr.permission = _usr.permission.permission_name + perm_rows.append(usr) + + # filter the perm rows by 'default' first and then sort them by + # admin,write,read,none permissions sorted again alphabetically in + # each group + perm_rows = sorted(perm_rows, key=display_sort) + + _admin_perm = 'group.admin' + owner_row = [] + if with_owner: + usr = AttributeDict(self.user.get_dict()) + usr.owner_row = True + usr.permission = _admin_perm + owner_row.append(usr) + + super_admin_rows = [] + if with_admins: + for usr in User.get_all_super_admins(): + # if this admin is also owner, don't double the record + if usr.user_id == owner_row[0].user_id: + owner_row[0].admin_row = True + else: + usr = AttributeDict(usr.get_dict()) + usr.admin_row = True + usr.permission = _admin_perm + super_admin_rows.append(usr) + + return super_admin_rows + owner_row + perm_rows + + def permission_user_groups(self): + q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self) + q = q.options(joinedload(UserGroupRepoGroupToPerm.group), + joinedload(UserGroupRepoGroupToPerm.users_group), + joinedload(UserGroupRepoGroupToPerm.permission),) + + perm_rows = [] + for _user_group in q.all(): + usr = AttributeDict(_user_group.users_group.get_dict()) + usr.permission = _user_group.permission.permission_name + perm_rows.append(usr) + + return perm_rows + + def get_api_data(self): + """ + Common function for generating api data + + """ + group = self + data = { + 'group_id': group.group_id, + 'group_name': group.group_name, + 'group_description': group.group_description, + 'parent_group': group.parent_group.group_name if group.parent_group else None, + 'repositories': [x.repo_name for x in group.repositories], + 'owner': group.user.username, + } + return data + + +class Permission(Base, BaseModel): + __tablename__ = 'permissions' + __table_args__ = ( + Index('p_perm_name_idx', 'permission_name'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + PERMS = [ + ('hg.admin', _('RhodeCode Super Administrator')), + + ('repository.none', _('Repository no access')), + ('repository.read', _('Repository read access')), + ('repository.write', _('Repository write access')), + ('repository.admin', _('Repository admin access')), + + ('group.none', _('Repository group no access')), + ('group.read', _('Repository group read access')), + ('group.write', _('Repository group write access')), + ('group.admin', _('Repository group admin access')), + + ('usergroup.none', _('User group no access')), + ('usergroup.read', _('User group read access')), + ('usergroup.write', _('User group write access')), + ('usergroup.admin', _('User group admin access')), + + ('hg.repogroup.create.false', _('Repository Group creation disabled')), + ('hg.repogroup.create.true', _('Repository Group creation enabled')), + + ('hg.usergroup.create.false', _('User Group creation disabled')), + ('hg.usergroup.create.true', _('User Group creation enabled')), + + ('hg.create.none', _('Repository creation disabled')), + ('hg.create.repository', _('Repository creation enabled')), + ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')), + ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')), + + ('hg.fork.none', _('Repository forking disabled')), + ('hg.fork.repository', _('Repository forking enabled')), + + ('hg.register.none', _('Registration disabled')), + ('hg.register.manual_activate', _('User Registration with manual account activation')), + ('hg.register.auto_activate', _('User Registration with automatic account activation')), + + ('hg.extern_activate.manual', _('Manual activation of external account')), + ('hg.extern_activate.auto', _('Automatic activation of external account')), + + ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')), + ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')), + ] + + # definition of system default permissions for DEFAULT user + DEFAULT_USER_PERMISSIONS = [ + 'repository.read', + 'group.read', + 'usergroup.read', + 'hg.create.repository', + 'hg.repogroup.create.false', + 'hg.usergroup.create.false', + 'hg.create.write_on_repogroup.true', + 'hg.fork.repository', + 'hg.register.manual_activate', + 'hg.extern_activate.auto', + 'hg.inherit_default_perms.true', + ] + + # defines which permissions are more important higher the more important + # Weight defines which permissions are more important. + # The higher number the more important. + PERM_WEIGHTS = { + 'repository.none': 0, + 'repository.read': 1, + 'repository.write': 3, + 'repository.admin': 4, + + 'group.none': 0, + 'group.read': 1, + 'group.write': 3, + 'group.admin': 4, + + 'usergroup.none': 0, + 'usergroup.read': 1, + 'usergroup.write': 3, + 'usergroup.admin': 4, + + 'hg.repogroup.create.false': 0, + 'hg.repogroup.create.true': 1, + + 'hg.usergroup.create.false': 0, + 'hg.usergroup.create.true': 1, + + 'hg.fork.none': 0, + 'hg.fork.repository': 1, + 'hg.create.none': 0, + 'hg.create.repository': 1 + } + + permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None) + permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None) + + def __unicode__(self): + return u"<%s('%s:%s')>" % ( + self.__class__.__name__, self.permission_id, self.permission_name + ) + + @classmethod + def get_by_key(cls, key): + return cls.query().filter(cls.permission_name == key).scalar() + + @classmethod + def get_default_repo_perms(cls, user_id, repo_id=None): + q = Session().query(UserRepoToPerm, Repository, Permission)\ + .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\ + .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\ + .filter(UserRepoToPerm.user_id == user_id) + if repo_id: + q = q.filter(UserRepoToPerm.repository_id == repo_id) + return q.all() + + @classmethod + def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None): + q = Session().query(UserGroupRepoToPerm, Repository, Permission)\ + .join( + Permission, + UserGroupRepoToPerm.permission_id == Permission.permission_id)\ + .join( + Repository, + UserGroupRepoToPerm.repository_id == Repository.repo_id)\ + .join( + UserGroup, + UserGroupRepoToPerm.users_group_id == + UserGroup.users_group_id)\ + .join( + UserGroupMember, + UserGroupRepoToPerm.users_group_id == + UserGroupMember.users_group_id)\ + .filter( + UserGroupMember.user_id == user_id, + UserGroup.users_group_active == true()) + if repo_id: + q = q.filter(UserGroupRepoToPerm.repository_id == repo_id) + return q.all() + + @classmethod + def get_default_group_perms(cls, user_id, repo_group_id=None): + q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\ + .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\ + .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\ + .filter(UserRepoGroupToPerm.user_id == user_id) + if repo_group_id: + q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id) + return q.all() + + @classmethod + def get_default_group_perms_from_user_group( + cls, user_id, repo_group_id=None): + q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\ + .join( + Permission, + UserGroupRepoGroupToPerm.permission_id == + Permission.permission_id)\ + .join( + RepoGroup, + UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\ + .join( + UserGroup, + UserGroupRepoGroupToPerm.users_group_id == + UserGroup.users_group_id)\ + .join( + UserGroupMember, + UserGroupRepoGroupToPerm.users_group_id == + UserGroupMember.users_group_id)\ + .filter( + UserGroupMember.user_id == user_id, + UserGroup.users_group_active == true()) + if repo_group_id: + q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id) + return q.all() + + @classmethod + def get_default_user_group_perms(cls, user_id, user_group_id=None): + q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\ + .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\ + .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\ + .filter(UserUserGroupToPerm.user_id == user_id) + if user_group_id: + q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id) + return q.all() + + @classmethod + def get_default_user_group_perms_from_user_group( + cls, user_id, user_group_id=None): + TargetUserGroup = aliased(UserGroup, name='target_user_group') + q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\ + .join( + Permission, + UserGroupUserGroupToPerm.permission_id == + Permission.permission_id)\ + .join( + TargetUserGroup, + UserGroupUserGroupToPerm.target_user_group_id == + TargetUserGroup.users_group_id)\ + .join( + UserGroup, + UserGroupUserGroupToPerm.user_group_id == + UserGroup.users_group_id)\ + .join( + UserGroupMember, + UserGroupUserGroupToPerm.user_group_id == + UserGroupMember.users_group_id)\ + .filter( + UserGroupMember.user_id == user_id, + UserGroup.users_group_active == true()) + if user_group_id: + q = q.filter( + UserGroupUserGroupToPerm.user_group_id == user_group_id) + + return q.all() + + +class UserRepoToPerm(Base, BaseModel): + __tablename__ = 'repo_to_perm' + __table_args__ = ( + UniqueConstraint('user_id', 'repository_id', 'permission_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) + + user = relationship('User') + repository = relationship('Repository') + permission = relationship('Permission') + + @classmethod + def create(cls, user, repository, permission): + n = cls() + n.user = user + n.repository = repository + n.permission = permission + Session().add(n) + return n + + def __unicode__(self): + return u'<%s => %s >' % (self.user, self.repository) + + +class UserUserGroupToPerm(Base, BaseModel): + __tablename__ = 'user_user_group_to_perm' + __table_args__ = ( + UniqueConstraint('user_id', 'user_group_id', 'permission_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + + user = relationship('User') + user_group = relationship('UserGroup') + permission = relationship('Permission') + + @classmethod + def create(cls, user, user_group, permission): + n = cls() + n.user = user + n.user_group = user_group + n.permission = permission + Session().add(n) + return n + + def __unicode__(self): + return u'<%s => %s >' % (self.user, self.user_group) + + +class UserToPerm(Base, BaseModel): + __tablename__ = 'user_to_perm' + __table_args__ = ( + UniqueConstraint('user_id', 'permission_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + + user = relationship('User') + permission = relationship('Permission', lazy='joined') + + def __unicode__(self): + return u'<%s => %s >' % (self.user, self.permission) + + +class UserGroupRepoToPerm(Base, BaseModel): + __tablename__ = 'users_group_repo_to_perm' + __table_args__ = ( + UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) + + users_group = relationship('UserGroup') + permission = relationship('Permission') + repository = relationship('Repository') + + @classmethod + def create(cls, users_group, repository, permission): + n = cls() + n.users_group = users_group + n.repository = repository + n.permission = permission + Session().add(n) + return n + + def __unicode__(self): + return u' %s >' % (self.users_group, self.repository) + + +class UserGroupUserGroupToPerm(Base, BaseModel): + __tablename__ = 'user_group_user_group_to_perm' + __table_args__ = ( + UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'), + CheckConstraint('target_user_group_id != user_group_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + + target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id') + user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id') + permission = relationship('Permission') + + @classmethod + def create(cls, target_user_group, user_group, permission): + n = cls() + n.target_user_group = target_user_group + n.user_group = user_group + n.permission = permission + Session().add(n) + return n + + def __unicode__(self): + return u' %s >' % (self.target_user_group, self.user_group) + + +class UserGroupToPerm(Base, BaseModel): + __tablename__ = 'users_group_to_perm' + __table_args__ = ( + UniqueConstraint('users_group_id', 'permission_id',), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + + users_group = relationship('UserGroup') + permission = relationship('Permission') + + +class UserRepoGroupToPerm(Base, BaseModel): + __tablename__ = 'user_repo_group_to_perm' + __table_args__ = ( + UniqueConstraint('user_id', 'group_id', 'permission_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + + user = relationship('User') + group = relationship('RepoGroup') + permission = relationship('Permission') + + @classmethod + def create(cls, user, repository_group, permission): + n = cls() + n.user = user + n.group = repository_group + n.permission = permission + Session().add(n) + return n + + +class UserGroupRepoGroupToPerm(Base, BaseModel): + __tablename__ = 'users_group_repo_group_to_perm' + __table_args__ = ( + UniqueConstraint('users_group_id', 'group_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + + users_group = relationship('UserGroup') + permission = relationship('Permission') + group = relationship('RepoGroup') + + @classmethod + def create(cls, user_group, repository_group, permission): + n = cls() + n.users_group = user_group + n.group = repository_group + n.permission = permission + Session().add(n) + return n + + def __unicode__(self): + return u' %s >' % (self.users_group, self.group) + + +class Statistics(Base, BaseModel): + __tablename__ = 'statistics' + __table_args__ = ( + UniqueConstraint('repository_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None) + stat_on_revision = Column("stat_on_revision", Integer(), nullable=False) + commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data + commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data + languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data + + repository = relationship('Repository', single_parent=True) + + +class UserFollowing(Base, BaseModel): + __tablename__ = 'user_followings' + __table_args__ = ( + UniqueConstraint('user_id', 'follows_repository_id'), + UniqueConstraint('user_id', 'follows_user_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None) + follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) + follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now) + + user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id') + + follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id') + follows_repository = relationship('Repository', order_by='Repository.repo_name') + + @classmethod + def get_repo_followers(cls, repo_id): + return cls.query().filter(cls.follows_repo_id == repo_id) + + +class CacheKey(Base, BaseModel): + __tablename__ = 'cache_invalidation' + __table_args__ = ( + UniqueConstraint('cache_key'), + Index('key_idx', 'cache_key'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + CACHE_TYPE_ATOM = 'ATOM' + CACHE_TYPE_RSS = 'RSS' + CACHE_TYPE_README = 'README' + + cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None) + cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None) + cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False) + + def __init__(self, cache_key, cache_args=''): + self.cache_key = cache_key + self.cache_args = cache_args + self.cache_active = False + + def __unicode__(self): + return u"<%s('%s:%s[%s]')>" % ( + self.__class__.__name__, + self.cache_id, self.cache_key, self.cache_active) + + def _cache_key_partition(self): + prefix, repo_name, suffix = self.cache_key.partition(self.cache_args) + return prefix, repo_name, suffix + + def get_prefix(self): + """ + Try to extract prefix from existing cache key. The key could consist + of prefix, repo_name, suffix + """ + # this returns prefix, repo_name, suffix + return self._cache_key_partition()[0] + + def get_suffix(self): + """ + get suffix that might have been used in _get_cache_key to + generate self.cache_key. Only used for informational purposes + in repo_edit.html. + """ + # prefix, repo_name, suffix + return self._cache_key_partition()[2] + + @classmethod + def delete_all_cache(cls): + """ + Delete all cache keys from database. + Should only be run when all instances are down and all entries + thus stale. + """ + cls.query().delete() + Session().commit() + + @classmethod + def get_cache_key(cls, repo_name, cache_type): + """ + + Generate a cache key for this process of RhodeCode instance. + Prefix most likely will be process id or maybe explicitly set + instance_id from .ini file. + """ + import rhodecode + prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '') + + repo_as_unicode = safe_unicode(repo_name) + key = u'{}_{}'.format(repo_as_unicode, cache_type) \ + if cache_type else repo_as_unicode + + return u'{}{}'.format(prefix, key) + + @classmethod + def set_invalidate(cls, repo_name, delete=False): + """ + Mark all caches of a repo as invalid in the database. + """ + + try: + qry = Session().query(cls).filter(cls.cache_args == repo_name) + if delete: + log.debug('cache objects deleted for repo %s', + safe_str(repo_name)) + qry.delete() + else: + log.debug('cache objects marked as invalid for repo %s', + safe_str(repo_name)) + qry.update({"cache_active": False}) + + Session().commit() + except Exception: + log.exception( + 'Cache key invalidation failed for repository %s', + safe_str(repo_name)) + Session().rollback() + + @classmethod + def get_active_cache(cls, cache_key): + inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar() + if inv_obj: + return inv_obj + return None + + @classmethod + def repo_context_cache(cls, compute_func, repo_name, cache_type, + thread_scoped=False): + """ + @cache_region('long_term') + def _heavy_calculation(cache_key): + return 'result' + + cache_context = CacheKey.repo_context_cache( + _heavy_calculation, repo_name, cache_type) + + with cache_context as context: + context.invalidate() + computed = context.compute() + + assert computed == 'result' + """ + from rhodecode.lib import caches + return caches.InvalidationContext( + compute_func, repo_name, cache_type, thread_scoped=thread_scoped) + + +class ChangesetComment(Base, BaseModel): + __tablename__ = 'changeset_comments' + __table_args__ = ( + Index('cc_revision_idx', 'revision'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + + COMMENT_OUTDATED = u'comment_outdated' + + comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True) + repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False) + revision = Column('revision', String(40), nullable=True) + pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True) + pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True) + line_no = Column('line_no', Unicode(10), nullable=True) + hl_lines = Column('hl_lines', Unicode(512), nullable=True) + f_path = Column('f_path', Unicode(1000), nullable=True) + user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False) + text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + renderer = Column('renderer', Unicode(64), nullable=True) + display_state = Column('display_state', Unicode(128), nullable=True) + + author = relationship('User', lazy='joined') + repo = relationship('Repository') + status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan") + pull_request = relationship('PullRequest', lazy='joined') + pull_request_version = relationship('PullRequestVersion') + + @classmethod + def get_users(cls, revision=None, pull_request_id=None): + """ + Returns user associated with this ChangesetComment. ie those + who actually commented + + :param cls: + :param revision: + """ + q = Session().query(User)\ + .join(ChangesetComment.author) + if revision: + q = q.filter(cls.revision == revision) + elif pull_request_id: + q = q.filter(cls.pull_request_id == pull_request_id) + return q.all() + + def render(self, mentions=False): + from rhodecode.lib import helpers as h + return h.render(self.text, renderer=self.renderer, mentions=mentions) + + def __repr__(self): + if self.comment_id: + return '' % self.comment_id + else: + return '' % id(self) + + +class ChangesetStatus(Base, BaseModel): + __tablename__ = 'changeset_statuses' + __table_args__ = ( + Index('cs_revision_idx', 'revision'), + Index('cs_version_idx', 'version'), + UniqueConstraint('repo_id', 'revision', 'version'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed' + STATUS_APPROVED = 'approved' + STATUS_REJECTED = 'rejected' + STATUS_UNDER_REVIEW = 'under_review' + + STATUSES = [ + (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default + (STATUS_APPROVED, _("Approved")), + (STATUS_REJECTED, _("Rejected")), + (STATUS_UNDER_REVIEW, _("Under Review")), + ] + + changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True) + repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None) + revision = Column('revision', String(40), nullable=False) + status = Column('status', String(128), nullable=False, default=DEFAULT) + changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id')) + modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now) + version = Column('version', Integer(), nullable=False, default=0) + pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True) + + author = relationship('User', lazy='joined') + repo = relationship('Repository') + comment = relationship('ChangesetComment', lazy='joined') + pull_request = relationship('PullRequest', lazy='joined') + + def __unicode__(self): + return u"<%s('%s[%s]:%s')>" % ( + self.__class__.__name__, + self.status, self.version, self.author + ) + + @classmethod + def get_status_lbl(cls, value): + return dict(cls.STATUSES).get(value) + + @property + def status_lbl(self): + return ChangesetStatus.get_status_lbl(self.status) + + +class _PullRequestBase(BaseModel): + """ + Common attributes of pull request and version entries. + """ + + # .status values + STATUS_NEW = u'new' + STATUS_OPEN = u'open' + STATUS_CLOSED = u'closed' + + title = Column('title', Unicode(255), nullable=True) + description = Column( + 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), + nullable=True) + # new/open/closed status of pull request (not approve/reject/etc) + status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW) + created_on = Column( + 'created_on', DateTime(timezone=False), nullable=False, + default=datetime.datetime.now) + updated_on = Column( + 'updated_on', DateTime(timezone=False), nullable=False, + default=datetime.datetime.now) + + @declared_attr + def user_id(cls): + return Column( + "user_id", Integer(), ForeignKey('users.user_id'), nullable=False, + unique=None) + + # 500 revisions max + _revisions = Column( + 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql')) + + @declared_attr + def source_repo_id(cls): + # TODO: dan: rename column to source_repo_id + return Column( + 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'), + nullable=False) + + source_ref = Column('org_ref', Unicode(255), nullable=False) + + @declared_attr + def target_repo_id(cls): + # TODO: dan: rename column to target_repo_id + return Column( + 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'), + nullable=False) + + target_ref = Column('other_ref', Unicode(255), nullable=False) + + # TODO: dan: rename column to last_merge_source_rev + _last_merge_source_rev = Column( + 'last_merge_org_rev', String(40), nullable=True) + # TODO: dan: rename column to last_merge_target_rev + _last_merge_target_rev = Column( + 'last_merge_other_rev', String(40), nullable=True) + _last_merge_status = Column('merge_status', Integer(), nullable=True) + merge_rev = Column('merge_rev', String(40), nullable=True) + + @hybrid_property + def revisions(self): + return self._revisions.split(':') if self._revisions else [] + + @revisions.setter + def revisions(self, val): + self._revisions = ':'.join(val) + + @declared_attr + def author(cls): + return relationship('User', lazy='joined') + + @declared_attr + def source_repo(cls): + return relationship( + 'Repository', + primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__) + + @property + def source_ref_parts(self): + refs = self.source_ref.split(':') + return Reference(refs[0], refs[1], refs[2]) + + @declared_attr + def target_repo(cls): + return relationship( + 'Repository', + primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__) + + @property + def target_ref_parts(self): + refs = self.target_ref.split(':') + return Reference(refs[0], refs[1], refs[2]) + + +class PullRequest(Base, _PullRequestBase): + __tablename__ = 'pull_requests' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + + pull_request_id = Column( + 'pull_request_id', Integer(), nullable=False, primary_key=True) + + def __repr__(self): + if self.pull_request_id: + return '' % self.pull_request_id + else: + return '' % id(self) + + reviewers = relationship('PullRequestReviewers', + cascade="all, delete, delete-orphan") + statuses = relationship('ChangesetStatus') + comments = relationship('ChangesetComment', + cascade="all, delete, delete-orphan") + versions = relationship('PullRequestVersion', + cascade="all, delete, delete-orphan") + + def is_closed(self): + return self.status == self.STATUS_CLOSED + + def get_api_data(self): + from rhodecode.model.pull_request import PullRequestModel + pull_request = self + merge_status = PullRequestModel().merge_status(pull_request) + data = { + 'pull_request_id': pull_request.pull_request_id, + 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name, + pull_request_id=self.pull_request_id, + qualified=True), + 'title': pull_request.title, + 'description': pull_request.description, + 'status': pull_request.status, + 'created_on': pull_request.created_on, + 'updated_on': pull_request.updated_on, + 'commit_ids': pull_request.revisions, + 'review_status': pull_request.calculated_review_status(), + 'mergeable': { + 'status': merge_status[0], + 'message': unicode(merge_status[1]), + }, + 'source': { + 'clone_url': pull_request.source_repo.clone_url(), + 'repository': pull_request.source_repo.repo_name, + 'reference': { + 'name': pull_request.source_ref_parts.name, + 'type': pull_request.source_ref_parts.type, + 'commit_id': pull_request.source_ref_parts.commit_id, + }, + }, + 'target': { + 'clone_url': pull_request.target_repo.clone_url(), + 'repository': pull_request.target_repo.repo_name, + 'reference': { + 'name': pull_request.target_ref_parts.name, + 'type': pull_request.target_ref_parts.type, + 'commit_id': pull_request.target_ref_parts.commit_id, + }, + }, + 'author': pull_request.author.get_api_data(include_secrets=False, + details='basic'), + 'reviewers': [ + { + 'user': reviewer.get_api_data(include_secrets=False, + details='basic'), + 'review_status': st[0][1].status if st else 'not_reviewed', + } + for reviewer, st in pull_request.reviewers_statuses() + ] + } + + return data + + def __json__(self): + return { + 'revisions': self.revisions, + } + + def calculated_review_status(self): + # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html + # because it's tricky on how to use ChangesetStatusModel from there + warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning) + from rhodecode.model.changeset_status import ChangesetStatusModel + return ChangesetStatusModel().calculated_review_status(self) + + def reviewers_statuses(self): + warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning) + from rhodecode.model.changeset_status import ChangesetStatusModel + return ChangesetStatusModel().reviewers_statuses(self) + + +class PullRequestVersion(Base, _PullRequestBase): + __tablename__ = 'pull_request_versions' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + + pull_request_version_id = Column( + 'pull_request_version_id', Integer(), nullable=False, primary_key=True) + pull_request_id = Column( + 'pull_request_id', Integer(), + ForeignKey('pull_requests.pull_request_id'), nullable=False) + pull_request = relationship('PullRequest') + + def __repr__(self): + if self.pull_request_version_id: + return '' % self.pull_request_version_id + else: + return '' % id(self) + + +class PullRequestReviewers(Base, BaseModel): + __tablename__ = 'pull_request_reviewers' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + + def __init__(self, user=None, pull_request=None): + self.user = user + self.pull_request = pull_request + + pull_requests_reviewers_id = Column( + 'pull_requests_reviewers_id', Integer(), nullable=False, + primary_key=True) + pull_request_id = Column( + "pull_request_id", Integer(), + ForeignKey('pull_requests.pull_request_id'), nullable=False) + user_id = Column( + "user_id", Integer(), ForeignKey('users.user_id'), nullable=True) + + user = relationship('User') + pull_request = relationship('PullRequest') + + +class Notification(Base, BaseModel): + __tablename__ = 'notifications' + __table_args__ = ( + Index('notification_type_idx', 'type'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + + TYPE_CHANGESET_COMMENT = u'cs_comment' + TYPE_MESSAGE = u'message' + TYPE_MENTION = u'mention' + TYPE_REGISTRATION = u'registration' + TYPE_PULL_REQUEST = u'pull_request' + TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment' + + notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True) + subject = Column('subject', Unicode(512), nullable=True) + body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True) + created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + type_ = Column('type', Unicode(255)) + + created_by_user = relationship('User') + notifications_to_users = relationship('UserNotification', lazy='joined', + cascade="all, delete, delete-orphan") + + @property + def recipients(self): + return [x.user for x in UserNotification.query()\ + .filter(UserNotification.notification == self)\ + .order_by(UserNotification.user_id.asc()).all()] + + @classmethod + def create(cls, created_by, subject, body, recipients, type_=None): + if type_ is None: + type_ = Notification.TYPE_MESSAGE + + notification = cls() + notification.created_by_user = created_by + notification.subject = subject + notification.body = body + notification.type_ = type_ + notification.created_on = datetime.datetime.now() + + for u in recipients: + assoc = UserNotification() + assoc.notification = notification + + # if created_by is inside recipients mark his notification + # as read + if u.user_id == created_by.user_id: + assoc.read = True + + u.notifications.append(assoc) + Session().add(notification) + + return notification + + @property + def description(self): + from rhodecode.model.notification import NotificationModel + return NotificationModel().make_description(self) + + +class UserNotification(Base, BaseModel): + __tablename__ = 'user_to_notification' + __table_args__ = ( + UniqueConstraint('user_id', 'notification_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True) + notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True) + read = Column('read', Boolean, default=False) + sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None) + + user = relationship('User', lazy="joined") + notification = relationship('Notification', lazy="joined", + order_by=lambda: Notification.created_on.desc(),) + + def mark_as_read(self): + self.read = True + Session().add(self) + + +class Gist(Base, BaseModel): + __tablename__ = 'gists' + __table_args__ = ( + Index('g_gist_access_id_idx', 'gist_access_id'), + Index('g_created_on_idx', 'created_on'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + GIST_PUBLIC = u'public' + GIST_PRIVATE = u'private' + DEFAULT_FILENAME = u'gistfile1.txt' + + ACL_LEVEL_PUBLIC = u'acl_public' + ACL_LEVEL_PRIVATE = u'acl_private' + + gist_id = Column('gist_id', Integer(), primary_key=True) + gist_access_id = Column('gist_access_id', Unicode(250)) + gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql')) + gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True) + gist_expires = Column('gist_expires', Float(53), nullable=False) + gist_type = Column('gist_type', Unicode(128), nullable=False) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + acl_level = Column('acl_level', Unicode(128), nullable=True) + + owner = relationship('User') + + def __repr__(self): + return '' % (self.gist_type, self.gist_access_id) + + @classmethod + def get_or_404(cls, id_): + res = cls.query().filter(cls.gist_access_id == id_).scalar() + if not res: + raise HTTPNotFound + return res + + @classmethod + def get_by_access_id(cls, gist_access_id): + return cls.query().filter(cls.gist_access_id == gist_access_id).scalar() + + def gist_url(self): + import rhodecode + alias_url = rhodecode.CONFIG.get('gist_alias_url') + if alias_url: + return alias_url.replace('{gistid}', self.gist_access_id) + + return url('gist', gist_id=self.gist_access_id, qualified=True) + + @classmethod + def base_path(cls): + """ + Returns base path when all gists are stored + + :param cls: + """ + from rhodecode.model.gist import GIST_STORE_LOC + q = Session().query(RhodeCodeUi)\ + .filter(RhodeCodeUi.ui_key == URL_SEP) + q = q.options(FromCache("sql_cache_short", "repository_repo_path")) + return os.path.join(q.one().ui_value, GIST_STORE_LOC) + + def get_api_data(self): + """ + Common function for generating gist related data for API + """ + gist = self + data = { + 'gist_id': gist.gist_id, + 'type': gist.gist_type, + 'access_id': gist.gist_access_id, + 'description': gist.gist_description, + 'url': gist.gist_url(), + 'expires': gist.gist_expires, + 'created_on': gist.created_on, + 'modified_at': gist.modified_at, + 'content': None, + 'acl_level': gist.acl_level, + } + return data + + def __json__(self): + data = dict( + ) + data.update(self.get_api_data()) + return data + # SCM functions + + def scm_instance(self, **kwargs): + full_repo_path = os.path.join(self.base_path(), self.gist_access_id) + return get_vcs_instance( + repo_path=safe_str(full_repo_path), create=False) + + +class DbMigrateVersion(Base, BaseModel): + __tablename__ = 'db_migrate_version' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + repository_id = Column('repository_id', String(250), primary_key=True) + repository_path = Column('repository_path', Text) + version = Column('version', Integer) + + +class ExternalIdentity(Base, BaseModel): + __tablename__ = 'external_identities' + __table_args__ = ( + Index('local_user_id_idx', 'local_user_id'), + Index('external_id_idx', 'external_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'}) + + external_id = Column('external_id', Unicode(255), default=u'', + primary_key=True) + external_username = Column('external_username', Unicode(1024), default=u'') + local_user_id = Column('local_user_id', Integer(), + ForeignKey('users.user_id'), primary_key=True) + provider_name = Column('provider_name', Unicode(255), default=u'', + primary_key=True) + access_token = Column('access_token', String(1024), default=u'') + alt_token = Column('alt_token', String(1024), default=u'') + token_secret = Column('token_secret', String(1024), default=u'') + + @classmethod + def by_external_id_and_provider(cls, external_id, provider_name, + local_user_id=None): + """ + Returns ExternalIdentity instance based on search params + + :param external_id: + :param provider_name: + :return: ExternalIdentity + """ + query = cls.query() + query = query.filter(cls.external_id == external_id) + query = query.filter(cls.provider_name == provider_name) + if local_user_id: + query = query.filter(cls.local_user_id == local_user_id) + return query.first() + + @classmethod + def user_by_external_id_and_provider(cls, external_id, provider_name): + """ + Returns User instance based on search params + + :param external_id: + :param provider_name: + :return: User + """ + query = User.query() + query = query.filter(cls.external_id == external_id) + query = query.filter(cls.provider_name == provider_name) + query = query.filter(User.user_id == cls.local_user_id) + return query.first() + + @classmethod + def by_local_user_id(cls, local_user_id): + """ + Returns all tokens for user + + :param local_user_id: + :return: ExternalIdentity + """ + query = cls.query() + query = query.filter(cls.local_user_id == local_user_id) + return query + + +class Integration(Base, BaseModel): + __tablename__ = 'integrations' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + integration_id = Column('integration_id', Integer(), primary_key=True) + integration_type = Column('integration_type', String(255)) + enabled = Column('enabled', Boolean(), nullable=False) + name = Column('name', String(255), nullable=False) + + settings = Column( + 'settings_json', MutationObj.as_mutable( + JsonType(dialect_map=dict(mysql=UnicodeText(16384))))) + repo_id = Column( + 'repo_id', Integer(), ForeignKey('repositories.repo_id'), + nullable=True, unique=None, default=None) + repo = relationship('Repository', lazy='joined') + + repo_group_id = Column( + 'repo_group_id', Integer(), ForeignKey('groups.group_id'), + nullable=True, unique=None, default=None) + repo_group = relationship('RepoGroup', lazy='joined') + + def __repr__(self): + if self.repo: + scope = 'repo=%r' % self.repo + elif self.repo_group: + scope = 'repo_group=%r' % self.repo_group + else: + scope = 'global' + + return '' % (self.integration_type, scope) diff --git a/rhodecode/lib/dbmigrate/versions/056_version_4_4_0.py b/rhodecode/lib/dbmigrate/versions/056_version_4_4_0.py new file mode 100644 index 00000000..e3d2a850 --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/056_version_4_4_0.py @@ -0,0 +1,36 @@ +import logging +import datetime + +from sqlalchemy import * +from sqlalchemy.exc import DatabaseError +from sqlalchemy.orm import relation, backref, class_mapper, joinedload +from sqlalchemy.orm.session import Session +from sqlalchemy.ext.declarative import declarative_base + +from rhodecode.lib.dbmigrate.migrate import * +from rhodecode.lib.dbmigrate.migrate.changeset import * +from rhodecode.lib.utils2 import str2bool + +from rhodecode.model.meta import Base +from rhodecode.model import meta +from rhodecode.lib.dbmigrate.versions import _reset_base, notify + +log = logging.getLogger(__name__) + + +def upgrade(migrate_engine): + """ + Upgrade operations go here. + Don't create your own engine; bind migrate_engine to your metadata + """ + _reset_base(migrate_engine) + from rhodecode.lib.dbmigrate.schema import db_4_4_0_0 + + tbl = db_4_4_0_0.Integration.__table__ + repo_group_id = db_4_4_0_0.Integration.repo_group_id + repo_group_id.create(table=tbl) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine From ed9abdd0eeb72d42826007241d001b0c9d0c88a5 Mon Sep 17 00:00:00 2001 From: lisaq Date: Fri, 12 Aug 2016 17:43:58 +0200 Subject: [PATCH 010/125] tests: fixes #4168 xfail for compare_remote_with_different_commit_indexes --- rhodecode/tests/functional/test_compare.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rhodecode/tests/functional/test_compare.py b/rhodecode/tests/functional/test_compare.py index 49387bdd..6da6772b 100644 --- a/rhodecode/tests/functional/test_compare.py +++ b/rhodecode/tests/functional/test_compare.py @@ -32,7 +32,7 @@ from rhodecode.tests.utils import AssertResponse @pytest.mark.usefixtures("autologin_user", "app") class TestCompareController: - @pytest.mark.xfail_backends("svn", "git") + @pytest.mark.xfail_backends("svn", reason="Requires pull") def test_compare_remote_with_different_commit_indexes(self, backend): # Preparing the following repository structure: # @@ -80,8 +80,10 @@ class TestCompareController: origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id]) # Verify test fixture setup - assert 5 == len(fork.scm_instance().commit_ids) - assert 2 == len(origin_repo.commit_ids) + # This does not work for git + if backend.alias != 'git': + assert 5 == len(fork.scm_instance().commit_ids) + assert 2 == len(origin_repo.commit_ids) # Comparing the revisions response = self.app.get( From d7d98230d2310cf2922ab4cc439302e051a5bfe4 Mon Sep 17 00:00:00 2001 From: lisaq Date: Sat, 13 Aug 2016 17:54:09 +0200 Subject: [PATCH 011/125] tests: fixes #4168 git commit ids in compare tests --- rhodecode/tests/functional/test_compare.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rhodecode/tests/functional/test_compare.py b/rhodecode/tests/functional/test_compare.py index 6da6772b..08488957 100644 --- a/rhodecode/tests/functional/test_compare.py +++ b/rhodecode/tests/functional/test_compare.py @@ -226,7 +226,7 @@ class TestCompareController: response.mustcontain("Repositories unrelated.") - @pytest.mark.xfail_backends("svn", "git") + @pytest.mark.xfail_backends("svn") def test_compare_cherry_pick_commits_from_bottom(self, backend): # repo1: @@ -277,10 +277,10 @@ class TestCompareController: repo_name=repo2.repo_name, source_ref_type="rev", # parent of commit2, in target repo2 - source_ref=commit1.short_id, + source_ref=commit1.raw_id, target_repo=repo1.repo_name, target_ref_type="rev", - target_ref=commit4.short_id, + target_ref=commit4.raw_id, merge='1',)) response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id)) response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id)) @@ -293,7 +293,7 @@ class TestCompareController: ('file1', 'a_c--826e8142e6ba'), ]) - @pytest.mark.xfail_backends("svn", "git") + @pytest.mark.xfail_backends("svn") def test_compare_cherry_pick_commits_from_top(self, backend): # repo1: # commit0: @@ -343,9 +343,9 @@ class TestCompareController: repo_name=repo1.repo_name, source_ref_type="rev", # parent of commit3, not in source repo2 - source_ref=commit2.short_id, + source_ref=commit2.raw_id, target_ref_type="rev", - target_ref=commit5.short_id, + target_ref=commit5.raw_id, merge='1',)) response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id)) From b07694de35a6a80e0c9e7c36d42aa7680f266571 Mon Sep 17 00:00:00 2001 From: lisaq Date: Sat, 13 Aug 2016 20:45:26 +0200 Subject: [PATCH 012/125] tests: fixes #4168 unique results for svn changesets in tests --- rhodecode/tests/functional/test_changeset.py | 31 +++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/rhodecode/tests/functional/test_changeset.py b/rhodecode/tests/functional/test_changeset.py index 520e14e8..65b4bd6e 100644 --- a/rhodecode/tests/functional/test_changeset.py +++ b/rhodecode/tests/functional/test_changeset.py @@ -57,7 +57,6 @@ class TestChangesetController(object): revision=self.commit_id[backend.alias])) assert response.body == self.diffs[backend.alias] - @pytest.mark.xfail_backends("svn", reason="Depends on consistent diffs") def test_single_commit_page_different_ops(self, backend): commit_id = { 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd', @@ -75,12 +74,16 @@ class TestChangesetController(object): # files op files response.mustcontain('File no longer present at commit: %s' % _shorten_commit_id(commit_id)) - response.mustcontain('new file 100644') + + # svn uses a different filename + if backend.alias == 'svn': + response.mustcontain('new file 10644') + else: + response.mustcontain('new file 100644') response.mustcontain('Changed theme to ADC theme') # commit msg self._check_diff_menus(response, right_menu=True) - @pytest.mark.xfail_backends("svn", reason="Depends on consistent diffs") def test_commit_range_page_different_ops(self, backend): commit_id_range = { 'hg': ( @@ -101,18 +104,23 @@ class TestChangesetController(object): response.mustcontain(_shorten_commit_id(commit_ids[0])) response.mustcontain(_shorten_commit_id(commit_ids[1])) - response.mustcontain('33 files changed: 1165 inserted, 308 deleted') + + # svn is special + if backend.alias == 'svn': + response.mustcontain('new file 10644') + response.mustcontain('34 files changed: 1184 inserted, 311 deleted') + else: + response.mustcontain('new file 100644') + response.mustcontain('33 files changed: 1165 inserted, 308 deleted') # files op files response.mustcontain('File no longer present at commit: %s' % _shorten_commit_id(commit_ids[1])) - response.mustcontain('new file 100644') response.mustcontain('Added docstrings to vcs.cli') # commit msg response.mustcontain('Changed theme to ADC theme') # commit msg self._check_diff_menus(response) - @pytest.mark.xfail_backends("svn", reason="Depends on consistent diffs") def test_combined_compare_commit_page_different_ops(self, backend): commit_id_range = { 'hg': ( @@ -134,12 +142,19 @@ class TestChangesetController(object): response.mustcontain(_shorten_commit_id(commit_ids[0])) response.mustcontain(_shorten_commit_id(commit_ids[1])) - response.mustcontain('32 files changed: 1165 inserted, 308 deleted') # files op files response.mustcontain('File no longer present at commit: %s' % _shorten_commit_id(commit_ids[1])) - response.mustcontain('new file 100644') + + # svn is special + if backend.alias == 'svn': + response.mustcontain('new file 10644') + response.mustcontain('32 files changed: 1179 inserted, 310 deleted') + else: + response.mustcontain('new file 100644') + response.mustcontain('32 files changed: 1165 inserted, 308 deleted') + response.mustcontain('Added docstrings to vcs.cli') # commit msg response.mustcontain('Changed theme to ADC theme') # commit msg From ea4cb9d4ae61adfb1cc9d6d0b48b9a43f67db9b4 Mon Sep 17 00:00:00 2001 From: lisaq Date: Wed, 17 Aug 2016 17:58:11 +0200 Subject: [PATCH 013/125] tests: fixes #4168 removing git xfails in pr and vcs commit tests --- rhodecode/tests/functional/test_pullrequests.py | 2 -- rhodecode/tests/vcs/test_commits.py | 7 +++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/rhodecode/tests/functional/test_pullrequests.py b/rhodecode/tests/functional/test_pullrequests.py index 00c7cccb..af060d14 100644 --- a/rhodecode/tests/functional/test_pullrequests.py +++ b/rhodecode/tests/functional/test_pullrequests.py @@ -64,8 +64,6 @@ class TestPullrequestsController: assert response.status == '302 Found' assert redirect_url in response.location - @pytest.mark.xfail_backends( - "git", reason="Pending bugfix/feature, issue #6") def test_create_pr_form_with_raw_commit_id(self, backend): repo = backend.repo diff --git a/rhodecode/tests/vcs/test_commits.py b/rhodecode/tests/vcs/test_commits.py index 72068760..8fe10160 100644 --- a/rhodecode/tests/vcs/test_commits.py +++ b/rhodecode/tests/vcs/test_commits.py @@ -325,9 +325,12 @@ class TestCommits(BackendTestMixin): assert line_no == 1 assert commit_id == file_added_commit.raw_id assert commit_loader() == file_added_commit + + # git annotation is generated differently thus different results if self.repo.alias == 'git': - pytest.xfail("TODO: Git returns wrong value in line") - assert line == 'Foobar 3' + assert line == '(Joe Doe 2010-01-03 08:00:00 +0000 1) Foobar 3' + else: + assert line == 'Foobar 3' def test_get_file_annotate_does_not_exist(self): file_added_commit = self.repo.get_commit(commit_idx=2) From 5a3574412b5ea40ada2024465ef1cddcf71c7b12 Mon Sep 17 00:00:00 2001 From: Daniel Dourvaris Date: Wed, 17 Aug 2016 07:12:38 +0300 Subject: [PATCH 014/125] vcs-error: replace disable_vcs middleware with vcs and http exceptions to avoid vcs connection errors showing to the end user fixes #4140 --- rhodecode/config/middleware.py | 16 ++-- rhodecode/lib/exceptions.py | 12 +++ rhodecode/lib/middleware/disable_vcs.py | 76 ------------------- rhodecode/lib/vcs/client.py | 2 +- rhodecode/lib/vcs/client_http.py | 7 +- rhodecode/lib/vcs/exceptions.py | 15 +++- .../tests/lib/middleware/test_disable_vcs.py | 53 ------------- .../lib/middleware/test_vcs_unavailable.py | 42 ++++++++++ 8 files changed, 84 insertions(+), 139 deletions(-) delete mode 100644 rhodecode/lib/middleware/disable_vcs.py delete mode 100644 rhodecode/tests/lib/middleware/test_disable_vcs.py create mode 100644 rhodecode/tests/lib/middleware/test_vcs_unavailable.py diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py index 161c436c..d1f7c136 100644 --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -43,9 +43,10 @@ from rhodecode.config import patches from rhodecode.config.routing import STATIC_FILE_PREFIX from rhodecode.config.environment import ( load_environment, load_pyramid_environment) +from rhodecode.lib.exceptions import VCSServerUnavailable +from rhodecode.lib.vcs.exceptions import VCSCommunicationError from rhodecode.lib.middleware import csrf from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled -from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper from rhodecode.lib.middleware.https_fixup import HttpsFixup from rhodecode.lib.middleware.vcs import VCSMiddleware from rhodecode.lib.plugins.utils import register_rhodecode_plugin @@ -188,10 +189,6 @@ def make_not_found_view(config): pylons_app_as_view = wsgiapp(pylons_app) - # Protect from VCS Server error related pages when server is not available - if not vcs_server_enabled: - pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view) - def pylons_app_with_error_handler(context, request): """ Handle exceptions from rc pylons app: @@ -216,10 +213,17 @@ def make_not_found_view(config): return error_handler(response, request) except HTTPError as e: # pyramid type exceptions return error_handler(e, request) - except Exception: + except Exception as e: + log.exception(e) + if settings.get('debugtoolbar.enabled', False): raise + + if isinstance(e, VCSCommunicationError): + return error_handler(VCSServerUnavailable(), request) + return error_handler(HTTPInternalServerError(), request) + return response return pylons_app_with_error_handler diff --git a/rhodecode/lib/exceptions.py b/rhodecode/lib/exceptions.py index 131e8c63..5904e49f 100644 --- a/rhodecode/lib/exceptions.py +++ b/rhodecode/lib/exceptions.py @@ -23,6 +23,7 @@ Set of custom exceptions used in RhodeCode """ from webob.exc import HTTPClientError +from pyramid.httpexceptions import HTTPBadGateway class LdapUsernameError(Exception): @@ -120,3 +121,14 @@ class NotAllowedToCreateUserError(Exception): class RepositoryCreationError(Exception): pass + + +class VCSServerUnavailable(HTTPBadGateway): + """ HTTP Exception class for VCS Server errors """ + code = 502 + title = 'VCS Server Error' + def __init__(self, message=''): + self.explanation = 'Could not connect to VCS Server' + if message: + self.explanation += ': ' + message + super(VCSServerUnavailable, self).__init__() diff --git a/rhodecode/lib/middleware/disable_vcs.py b/rhodecode/lib/middleware/disable_vcs.py deleted file mode 100644 index 35289bc1..00000000 --- a/rhodecode/lib/middleware/disable_vcs.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2015-2016 RhodeCode GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License, version 3 -# (only), as published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -# This program is dual-licensed. If you wish to learn more about the -# RhodeCode Enterprise Edition, including its added features, Support services, -# and proprietary license terms, please see https://rhodecode.com/licenses/ - -""" -Disable VCS pages when VCS Server is not available -""" - -import logging -import re -from pyramid.httpexceptions import HTTPBadGateway - -log = logging.getLogger(__name__) - - -class VCSServerUnavailable(HTTPBadGateway): - """ HTTP Exception class for when VCS Server is unavailable """ - code = 502 - title = 'VCS Server Required' - explanation = 'A VCS Server is required for this action. There is currently no VCS Server configured.' - -class DisableVCSPagesWrapper(object): - """ - Pyramid view wrapper to disable all pages that require VCS Server to be - running, avoiding that errors explode to the user. - - This Wrapper should be enabled only in case VCS Server is not available - for the instance. - """ - - VCS_NOT_REQUIRED = [ - '^/$', - ('/_admin(?!/settings/mapping)(?!/my_account/repos)' - '(?!/create_repository)(?!/gists)(?!/notifications/)' - ), - ] - _REGEX_VCS_NOT_REQUIRED = [re.compile(path) for path in VCS_NOT_REQUIRED] - - def _check_vcs_requirement(self, path_info): - """ - Tries to match the current path to one of the safe URLs to be rendered. - Displays an error message in case - """ - for regex in self._REGEX_VCS_NOT_REQUIRED: - safe_url = regex.match(path_info) - if safe_url: - return True - - # Url is not safe to be rendered without VCS Server - log.debug('accessing: `%s` with VCS Server disabled', path_info) - return False - - def __init__(self, handler): - self.handler = handler - - def __call__(self, context, request): - if not self._check_vcs_requirement(request.path): - raise VCSServerUnavailable('VCS Server is not available') - - return self.handler(context, request) diff --git a/rhodecode/lib/vcs/client.py b/rhodecode/lib/vcs/client.py index 4dbe7379..10cdc067 100644 --- a/rhodecode/lib/vcs/client.py +++ b/rhodecode/lib/vcs/client.py @@ -305,7 +305,7 @@ def _get_proxy_method(proxy, name): try: return getattr(proxy, name) except CommunicationError: - raise CommunicationError( + raise exceptions.PyroVCSCommunicationError( 'Unable to connect to remote pyro server %s' % proxy) diff --git a/rhodecode/lib/vcs/client_http.py b/rhodecode/lib/vcs/client_http.py index abe5a5db..467956eb 100644 --- a/rhodecode/lib/vcs/client_http.py +++ b/rhodecode/lib/vcs/client_http.py @@ -36,6 +36,7 @@ import urllib2 import urlparse import uuid +import pycurl import msgpack import requests @@ -172,7 +173,11 @@ class RemoteObject(object): def _remote_call(url, payload, exceptions_map, session): - response = session.post(url, data=msgpack.packb(payload)) + try: + response = session.post(url, data=msgpack.packb(payload)) + except pycurl.error as e: + raise exceptions.HttpVCSCommunicationError(e) + response = msgpack.unpackb(response.content) error = response.get('error') if error: diff --git a/rhodecode/lib/vcs/exceptions.py b/rhodecode/lib/vcs/exceptions.py index 09350e8e..c0d8f409 100644 --- a/rhodecode/lib/vcs/exceptions.py +++ b/rhodecode/lib/vcs/exceptions.py @@ -24,6 +24,19 @@ Custom vcs exceptions module. import functools import urllib2 +import pycurl +from Pyro4.errors import CommunicationError + +class VCSCommunicationError(Exception): + pass + + +class PyroVCSCommunicationError(VCSCommunicationError): + pass + + +class HttpVCSCommunicationError(VCSCommunicationError): + pass class VCSError(Exception): @@ -161,7 +174,6 @@ def map_vcs_exceptions(func): try: return func(*args, **kwargs) except Exception as e: - # The error middleware adds information if it finds # __traceback_info__ in a frame object. This way the remote # traceback information is made available in error reports. @@ -182,5 +194,4 @@ def map_vcs_exceptions(func): raise _EXCEPTION_MAP[kind](*e.args) else: raise - return wrapper diff --git a/rhodecode/tests/lib/middleware/test_disable_vcs.py b/rhodecode/tests/lib/middleware/test_disable_vcs.py deleted file mode 100644 index b4a460b8..00000000 --- a/rhodecode/tests/lib/middleware/test_disable_vcs.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2010-2016 RhodeCode GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License, version 3 -# (only), as published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -# This program is dual-licensed. If you wish to learn more about the -# RhodeCode Enterprise Edition, including its added features, Support services, -# and proprietary license terms, please see https://rhodecode.com/licenses/ - -import pytest -from pyramid.response import Response -from pyramid.testing import DummyRequest -from rhodecode.lib.middleware.disable_vcs import ( - DisableVCSPagesWrapper, VCSServerUnavailable) - - -@pytest.mark.parametrize('url, should_raise', [ - ('/', False), - ('/_admin/settings', False), - ('/_admin/i_am_fine', False), - ('/_admin/settings/mappings', True), - ('/_admin/my_account/repos', True), - ('/_admin/create_repository', True), - ('/_admin/gists/1', True), - ('/_admin/notifications/1', True), -]) -def test_vcs_disabled(url, should_raise): - wrapped_view = DisableVCSPagesWrapper(pyramid_view) - request = DummyRequest(path=url) - - if should_raise: - with pytest.raises(VCSServerUnavailable): - response = wrapped_view(None, request) - else: - response = wrapped_view(None, request) - assert response.status_int == 200 - -def pyramid_view(context, request): - """ - A mock pyramid view to be used in the wrapper - """ - return Response('success') diff --git a/rhodecode/tests/lib/middleware/test_vcs_unavailable.py b/rhodecode/tests/lib/middleware/test_vcs_unavailable.py new file mode 100644 index 00000000..b72b611c --- /dev/null +++ b/rhodecode/tests/lib/middleware/test_vcs_unavailable.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import mock +import pytest +import rhodecode.lib.vcs.client as client + +@pytest.mark.usefixtures('autologin_user', 'app') +def test_vcs_available_returns_summary_page(app, backend): + url = '/{repo_name}'.format(repo_name=backend.repo.repo_name) + response = app.get(url) + assert response.status_code == 200 + assert 'Summary' in response.body + + +@pytest.mark.usefixtures('autologin_user', 'app') +def test_vcs_unavailable_returns_vcs_error_page(app, backend): + url = '/{repo_name}'.format(repo_name=backend.repo.repo_name) + + with mock.patch.object(client, '_get_proxy_method') as p: + p.side_effect = client.exceptions.PyroVCSCommunicationError() + response = app.get(url, expect_errors=True) + + assert response.status_code == 502 + assert 'Could not connect to VCS Server' in response.body From 8ed80c1c7fe0149fd3bc8dc803f33a808122e5a3 Mon Sep 17 00:00:00 2001 From: Daniel Dourvaris Date: Tue, 23 Aug 2016 12:57:02 +0300 Subject: [PATCH 015/125] ux: show list of causes for vcs unavailable error page --- rhodecode/config/middleware.py | 5 ++++- rhodecode/lib/exceptions.py | 5 +++++ rhodecode/templates/errors/error_document.html | 8 +++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py index d1f7c136..65ba0427 100644 --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -248,7 +248,6 @@ def webob_to_pyramid_http_response(webob_response): def error_handler(exception, request): - # TODO: dan: replace the old pylons error controller with this from rhodecode.model.settings import SettingsModel from rhodecode.lib.utils2 import AttributeDict @@ -277,6 +276,10 @@ def error_handler(exception, request): if not c.rhodecode_name: c.rhodecode_name = 'Rhodecode' + c.causes = [] + if hasattr(base_response, 'causes'): + c.causes = base_response.causes + response = render_to_response( '/errors/error_document.html', {'c': c}, request=request, response=base_response) diff --git a/rhodecode/lib/exceptions.py b/rhodecode/lib/exceptions.py index 5904e49f..1250ef05 100644 --- a/rhodecode/lib/exceptions.py +++ b/rhodecode/lib/exceptions.py @@ -127,6 +127,11 @@ class VCSServerUnavailable(HTTPBadGateway): """ HTTP Exception class for VCS Server errors """ code = 502 title = 'VCS Server Error' + causes = [ + 'VCS Server is not running', + 'Incorrect vcs.server=host:port', + 'Incorrect vcs.server.protocol', + ] def __init__(self, message=''): self.explanation = 'Could not connect to VCS Server' if message: diff --git a/rhodecode/templates/errors/error_document.html b/rhodecode/templates/errors/error_document.html index ef758fa9..8cd453b0 100644 --- a/rhodecode/templates/errors/error_document.html +++ b/rhodecode/templates/errors/error_document.html @@ -39,11 +39,17 @@

${_('You will be redirected to %s in %s seconds') % (c.redirect_module,c.redirect_time)}

%endif
-

Possible Cause

+

Possible Causes

    + % if c.causes: + %for cause in c.causes: +
  • ${cause}
  • + %endfor + %else:
  • The resource may have been deleted.
  • You may not have access to this repository.
  • The link may be incorrect.
  • + %endif
From 05cff061cf16681be4dfa882fc805e338c7bc6a8 Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Tue, 23 Aug 2016 12:55:23 +0200 Subject: [PATCH 016/125] polymer: introduce webcomponents to rhodecode --- .hgignore | 1 + Gruntfile.js | 29 +++++++++++++++++-- package.json | 5 ++++ .../js/src/components/shared-components.html | 4 +++ rhodecode/templates/base/root.html | 19 ++++++++++++ 5 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 rhodecode/public/js/src/components/shared-components.html diff --git a/.hgignore b/.hgignore index be3a66ca..ab14d10b 100644 --- a/.hgignore +++ b/.hgignore @@ -23,6 +23,7 @@ syntax: regexp ^_dev ^._dev ^build/ +^bower_components/ ^coverage\.xml$ ^data$ ^\.eggs/ diff --git a/Gruntfile.js b/Gruntfile.js index 6223aaee..05513d34 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -8,7 +8,14 @@ module.exports = function(grunt) { "dest": "rhodecode/public/js" } }, - + copy: { + main: { + expand: true, + cwd: 'bower_components', + src: 'webcomponentsjs/**', + dest: '<%= dirs.js.dest %>/vendors', + }, + }, concat: { dist: { src: [ @@ -120,7 +127,7 @@ module.exports = function(grunt) { tasks: ["less:production"] }, js: { - files: ["<%= dirs.js.src %>/**/*.js"], + files: ["<%= dirs.js.src %>/**/*.js", "<%= dirs.js.src %>/components/*.*"], tasks: ["concat:dist"] } }, @@ -132,6 +139,19 @@ module.exports = function(grunt) { jshintrc: '.jshintrc' } } + }, + vulcanize: { + default: { + options: { + abspath: '', + inlineScripts: true, + inlineCss: true, + stripComments: true + }, + files: { + '<%= dirs.js.dest %>/rhodecode-components.html': '<%= dirs.js.src %>/components/shared-components.html' + } + } } }); @@ -139,6 +159,9 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-vulcanize'); + grunt.loadNpmTasks('grunt-crisper'); + grunt.loadNpmTasks('grunt-contrib-copy'); - grunt.registerTask('default', ['less:production', 'concat:dist']); + grunt.registerTask('default', ['copy','vulcanize', 'less:production', 'concat:dist']); }; diff --git a/package.json b/package.json index 26b04bc7..a6d1058a 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,15 @@ "version": "0.0.1", "devDependencies": { "grunt": "^0.4.5", + "grunt-contrib-copy": "^1.0.0", "grunt-contrib-concat": "^0.5.1", "grunt-contrib-jshint": "^0.12.0", "grunt-contrib-less": "^1.1.0", "grunt-contrib-watch": "^0.6.1", + "crisper": "^2.0.2", + "vulcanize": "^1.14.8", + "grunt-crisper": "^1.0.1", + "grunt-vulcanize": "^1.0.0", "jshint": "^2.9.1-rc3" } } diff --git a/rhodecode/public/js/src/components/shared-components.html b/rhodecode/public/js/src/components/shared-components.html new file mode 100644 index 00000000..3f136fbb --- /dev/null +++ b/rhodecode/public/js/src/components/shared-components.html @@ -0,0 +1,4 @@ + + + + diff --git a/rhodecode/templates/base/root.html b/rhodecode/templates/base/root.html index e202ab2f..ae2def1f 100644 --- a/rhodecode/templates/base/root.html +++ b/rhodecode/templates/base/root.html @@ -82,6 +82,25 @@ c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default + + ## avoide escaping the %N From 4764f1a60ac00d6c6d5593d1e41314496da456a6 Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Tue, 23 Aug 2016 16:14:15 +0200 Subject: [PATCH 017/125] polymer: initialize as soon as possible --- rhodecode/templates/base/root.html | 39 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/rhodecode/templates/base/root.html b/rhodecode/templates/base/root.html index ae2def1f..916d47f0 100644 --- a/rhodecode/templates/base/root.html +++ b/rhodecode/templates/base/root.html @@ -42,6 +42,26 @@ c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default ## JAVASCRIPT <%def name="js()"> + + + - ## avoide escaping the %N From cab106e9bda933c5af8c2ed630240b722b3bb05b Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Tue, 23 Aug 2016 16:25:25 +0200 Subject: [PATCH 018/125] notifications: proper check for test messages --- rhodecode/public/js/src/rhodecode/utils/notifications.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rhodecode/public/js/src/rhodecode/utils/notifications.js b/rhodecode/public/js/src/rhodecode/utils/notifications.js index 8488e772..086b4d23 100644 --- a/rhodecode/public/js/src/rhodecode/utils/notifications.js +++ b/rhodecode/public/js/src/rhodecode/utils/notifications.js @@ -27,8 +27,7 @@ function notifyToaster(data){ } function handleNotifications(data) { - - if (!templateContext.rhodecode_user.notification_status && !data.testMessage) { + if (!templateContext.rhodecode_user.notification_status && !data.message.testMessage) { // do not act if notifications are disabled return } From 4ccb5b300e34dfd96737cac6327f01fab751e855 Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Tue, 23 Aug 2016 18:22:07 +0200 Subject: [PATCH 019/125] notifications: replace toggle button with actual toggle element - fixes #4171 --- rhodecode/controllers/admin/my_account.py | 2 +- .../js/src/components/shared-components.html | 2 + .../my_account/my_account_notifications.html | 56 ++++++++++++++++--- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/rhodecode/controllers/admin/my_account.py b/rhodecode/controllers/admin/my_account.py index 9d9c9f14..b449fe11 100644 --- a/rhodecode/controllers/admin/my_account.py +++ b/rhodecode/controllers/admin/my_account.py @@ -368,4 +368,4 @@ class MyAccountController(BaseController): user_data['notification_status'] = not status user.user_data = user_data Session().commit() - return redirect(url('my_account_notifications')) + return json.dumps(user_data['notification_status']) diff --git a/rhodecode/public/js/src/components/shared-components.html b/rhodecode/public/js/src/components/shared-components.html index 3f136fbb..f76fdf9b 100644 --- a/rhodecode/public/js/src/components/shared-components.html +++ b/rhodecode/public/js/src/components/shared-components.html @@ -2,3 +2,5 @@ + + diff --git a/rhodecode/templates/admin/my_account/my_account_notifications.html b/rhodecode/templates/admin/my_account/my_account_notifications.html index ecd19455..478be17b 100644 --- a/rhodecode/templates/admin/my_account/my_account_notifications.html +++ b/rhodecode/templates/admin/my_account/my_account_notifications.html @@ -1,26 +1,65 @@ + From b8952ab744de47076f7c3158fbb520bb8d6ea645 Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Wed, 24 Aug 2016 15:15:34 +0200 Subject: [PATCH 020/125] channelstream: store user permissions in state dict --- rhodecode/channelstream/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rhodecode/channelstream/views.py b/rhodecode/channelstream/views.py index df0f4a4e..213caeab 100644 --- a/rhodecode/channelstream/views.py +++ b/rhodecode/channelstream/views.py @@ -95,6 +95,7 @@ class ChannelstreamView(object): 'display_name': None, 'display_link': None, } + user_data['permissions'] = c.rhodecode_user.permissions payload = { 'username': user.username, 'user_state': user_data, From a9c1ae249cbe045a8f4f2a7a5c89b51626427175 Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Thu, 25 Aug 2016 12:48:15 +0200 Subject: [PATCH 021/125] account: use jsonify decorator --- rhodecode/controllers/admin/my_account.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rhodecode/controllers/admin/my_account.py b/rhodecode/controllers/admin/my_account.py index b449fe11..22d920b3 100644 --- a/rhodecode/controllers/admin/my_account.py +++ b/rhodecode/controllers/admin/my_account.py @@ -38,6 +38,7 @@ from rhodecode.lib import auth from rhodecode.lib.auth import ( LoginRequired, NotAnonymous, AuthUser, generate_auth_token) from rhodecode.lib.base import BaseController, render +from rhodecode.lib.utils import jsonify from rhodecode.lib.utils2 import safe_int, md5 from rhodecode.lib.ext_json import json @@ -361,6 +362,7 @@ class MyAccountController(BaseController): return render('admin/my_account/my_account.html') @auth.CSRFRequired() + @jsonify def my_notifications_toggle_visibility(self): user = c.rhodecode_user.get_instance() user_data = user.user_data @@ -368,4 +370,4 @@ class MyAccountController(BaseController): user_data['notification_status'] = not status user.user_data = user_data Session().commit() - return json.dumps(user_data['notification_status']) + return user_data['notification_status'] From 582c9be761486e2458991b67de425835e0579739 Mon Sep 17 00:00:00 2001 From: lisaq Date: Fri, 19 Aug 2016 17:45:38 +0200 Subject: [PATCH 022/125] js: add check to eliminate js errors when inline comment function references an object which does not exist --- rhodecode/public/js/src/rhodecode/comments.js | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/rhodecode/public/js/src/rhodecode/comments.js b/rhodecode/public/js/src/rhodecode/comments.js index 2ff6f727..d3043a59 100644 --- a/rhodecode/public/js/src/rhodecode/comments.js +++ b/rhodecode/public/js/src/rhodecode/comments.js @@ -282,25 +282,28 @@ var placeInline = function(target_container, lineno, html, show_add_button) { var target_line = $('#' + lineid).get(0); var comment = new $(tableTr('inline-comments', html)); // check if there are comments already ! - var parent_node = target_line.parentNode; - var root_parent = parent_node; - while (1) { - var n = parent_node.nextElementSibling; - // next element are comments ! - if ($(n).hasClass('inline-comments')) { - parent_node = n; + if (target_line) { + var parent_node = target_line.parentNode; + var root_parent = parent_node; + + while (1) { + var n = parent_node.nextElementSibling; + // next element are comments ! + if ($(n).hasClass('inline-comments')) { + parent_node = n; + } + else { + break; + } } - else { - break; + // put in the comment at the bottom + $(comment).insertAfter(parent_node); + $(comment).find('.comment-inline').addClass('inline-comment-injected'); + // scan nodes, and attach add button to last one + if (show_add_button) { + placeAddButton(root_parent); } } - // put in the comment at the bottom - $(comment).insertAfter(parent_node); - $(comment).find('.comment-inline').addClass('inline-comment-injected'); - // scan nodes, and attach add button to last one - if (show_add_button) { - placeAddButton(root_parent); - } return target_line; }; From 4779f3916b6fa412cc8021274fcc22a7f2f6f505 Mon Sep 17 00:00:00 2001 From: lisaq Date: Mon, 22 Aug 2016 17:33:59 +0200 Subject: [PATCH 023/125] diffs: adding inline comment toggle fixes #2884 --- rhodecode/lib/diffs.py | 5 +++-- rhodecode/public/css/diff.less | 12 ++++++++++++ rhodecode/public/js/src/rhodecode/comments.js | 16 +++++++++++++++- .../changeset/changeset_file_comment.html | 2 +- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/rhodecode/lib/diffs.py b/rhodecode/lib/diffs.py index 8abc1a05..4205bb3c 100644 --- a/rhodecode/lib/diffs.py +++ b/rhodecode/lib/diffs.py @@ -55,6 +55,7 @@ def wrap_to_table(str_): return ''' + @@ -691,14 +692,14 @@ class DiffProcessor(object): anchor_link = False ########################################################### - # COMMENT ICON + # COMMENT ICONS ########################################################### _html.append('''\t\n''') + _html.append('''\n''') ########################################################### # OLD LINE NUMBER diff --git a/rhodecode/public/css/diff.less b/rhodecode/public/css/diff.less index 32392717..7d7b1cb3 100644 --- a/rhodecode/public/css/diff.less +++ b/rhodecode/public/css/diff.less @@ -31,6 +31,18 @@ div.diffblock.margined.comm tr { } } + .comment-toggle { + display: inline-block; + visibility: hidden; + width: 20px; + color: @rcblue; + + &.active { + visibility: visible; + cursor: pointer; + } + } + &.line { &:hover, &.hover{ .add-comment-line a{ diff --git a/rhodecode/public/js/src/rhodecode/comments.js b/rhodecode/public/js/src/rhodecode/comments.js index d3043a59..052266c2 100644 --- a/rhodecode/public/js/src/rhodecode/comments.js +++ b/rhodecode/public/js/src/rhodecode/comments.js @@ -38,7 +38,8 @@ var tableTr = function(cls, body){ var comment_id = fromHTML(body).children[0].id.split('comment-')[1]; var id = 'comment-tr-{0}'.format(comment_id); var _html = ('
%s
''') if enable_comments and change['action'] != Action.CONTEXT: _html.append('''''') - _html.append('''
'+ - ''+ + ''+ + ''+ ''+ ''+ ''+ @@ -303,11 +304,24 @@ var placeInline = function(target_container, lineno, html, show_add_button) { if (show_add_button) { placeAddButton(root_parent); } + addCommentToggle(target_line); } return target_line; }; +var addCommentToggle = function(target_line) { + // exposes comment toggle button + $(target_line).siblings('.comment-toggle').addClass('active'); + return; +}; + +var bindToggleButtons = function() { + $('.comment-toggle').on('click', function() { + $(this).parent().nextUntil('tr.line').toggle('inline-comments'); + }); +}; + var linkifyComments = function(comments) { for (var i = 0; i < comments.length; i++) { diff --git a/rhodecode/templates/changeset/changeset_file_comment.html b/rhodecode/templates/changeset/changeset_file_comment.html index b20e2b02..f1efbcb3 100644 --- a/rhodecode/templates/changeset/changeset_file_comment.html +++ b/rhodecode/templates/changeset/changeset_file_comment.html @@ -307,6 +307,6 @@ "#${form_id}", commitId, pullRequestId, lineNo, true); mainCommentForm.initStatusChangeSelector(); - + bindToggleButtons(); From 666df1ef47572b4e6de37efa74357dc53bea459d Mon Sep 17 00:00:00 2001 From: lisaq Date: Tue, 23 Aug 2016 13:33:08 +0200 Subject: [PATCH 024/125] tests: adjusting tests for inline comment toggle --- rhodecode/tests/lib/test_diffs.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/rhodecode/tests/lib/test_diffs.py b/rhodecode/tests/lib/test_diffs.py index 77de05fb..8ba5619d 100644 --- a/rhodecode/tests/lib/test_diffs.py +++ b/rhodecode/tests/lib/test_diffs.py @@ -76,7 +76,7 @@ def test_diffprocessor_as_html_with_comments(): expected_html = textwrap.dedent('''
{2}
- + - + - + - + - + - + - + - + - + - -
... ... @@ -85,7 +85,7 @@ def test_diffprocessor_as_html_with_comments():
2
3
4
5
6
7
8 Date: Thu, 25 Aug 2016 16:26:29 +0200 Subject: [PATCH 025/125] vendor js: add webcomponent polyfills for browsers that may not ship defaults --- .../webcomponentsjs/webcomponents-lite.min.js | 12 ++++++++++++ .../vendors/webcomponentsjs/webcomponents.min.js | 14 ++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 rhodecode/public/js/vendors/webcomponentsjs/webcomponents-lite.min.js create mode 100644 rhodecode/public/js/vendors/webcomponentsjs/webcomponents.min.js diff --git a/rhodecode/public/js/vendors/webcomponentsjs/webcomponents-lite.min.js b/rhodecode/public/js/vendors/webcomponentsjs/webcomponents-lite.min.js new file mode 100644 index 00000000..da22fda4 --- /dev/null +++ b/rhodecode/public/js/vendors/webcomponentsjs/webcomponents-lite.min.js @@ -0,0 +1,12 @@ +/** + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ +// @version 0.7.22 +!function(){window.WebComponents=window.WebComponents||{flags:{}};var e="webcomponents-lite.js",t=document.querySelector('script[src*="'+e+'"]'),n={};if(!n.noOpts){if(location.search.slice(1).split("&").forEach(function(e){var t,o=e.split("=");o[0]&&(t=o[0].match(/wc-(.+)/))&&(n[t[1]]=o[1]||!0)}),t)for(var o,r=0;o=t.attributes[r];r++)"src"!==o.name&&(n[o.name]=o.value||!0);if(n.log&&n.log.split){var i=n.log.split(",");n.log={},i.forEach(function(e){n.log[e]=!0})}else n.log={}}n.register&&(window.CustomElements=window.CustomElements||{flags:{}},window.CustomElements.flags.register=n.register),WebComponents.flags=n}(),function(e){"use strict";function t(e){return void 0!==h[e]}function n(){s.call(this),this._isInvalid=!0}function o(e){return""==e&&n.call(this),e.toLowerCase()}function r(e){var t=e.charCodeAt(0);return t>32&&127>t&&-1==[34,35,60,62,63,96].indexOf(t)?e:encodeURIComponent(e)}function i(e){var t=e.charCodeAt(0);return t>32&&127>t&&-1==[34,35,60,62,96].indexOf(t)?e:encodeURIComponent(e)}function a(e,a,s){function c(e){g.push(e)}var d=a||"scheme start",l=0,u="",w=!1,_=!1,g=[];e:for(;(e[l-1]!=p||0==l)&&!this._isInvalid;){var b=e[l];switch(d){case"scheme start":if(!b||!m.test(b)){if(a){c("Invalid scheme.");break e}u="",d="no scheme";continue}u+=b.toLowerCase(),d="scheme";break;case"scheme":if(b&&v.test(b))u+=b.toLowerCase();else{if(":"!=b){if(a){if(p==b)break e;c("Code point not allowed in scheme: "+b);break e}u="",l=0,d="no scheme";continue}if(this._scheme=u,u="",a)break e;t(this._scheme)&&(this._isRelative=!0),d="file"==this._scheme?"relative":this._isRelative&&s&&s._scheme==this._scheme?"relative or authority":this._isRelative?"authority first slash":"scheme data"}break;case"scheme data":"?"==b?(this._query="?",d="query"):"#"==b?(this._fragment="#",d="fragment"):p!=b&&" "!=b&&"\n"!=b&&"\r"!=b&&(this._schemeData+=r(b));break;case"no scheme":if(s&&t(s._scheme)){d="relative";continue}c("Missing scheme."),n.call(this);break;case"relative or authority":if("/"!=b||"/"!=e[l+1]){c("Expected /, got: "+b),d="relative";continue}d="authority ignore slashes";break;case"relative":if(this._isRelative=!0,"file"!=this._scheme&&(this._scheme=s._scheme),p==b){this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._username=s._username,this._password=s._password;break e}if("/"==b||"\\"==b)"\\"==b&&c("\\ is an invalid code point."),d="relative slash";else if("?"==b)this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query="?",this._username=s._username,this._password=s._password,d="query";else{if("#"!=b){var y=e[l+1],E=e[l+2];("file"!=this._scheme||!m.test(b)||":"!=y&&"|"!=y||p!=E&&"/"!=E&&"\\"!=E&&"?"!=E&&"#"!=E)&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password,this._path=s._path.slice(),this._path.pop()),d="relative path";continue}this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._fragment="#",this._username=s._username,this._password=s._password,d="fragment"}break;case"relative slash":if("/"!=b&&"\\"!=b){"file"!=this._scheme&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password),d="relative path";continue}"\\"==b&&c("\\ is an invalid code point."),d="file"==this._scheme?"file host":"authority ignore slashes";break;case"authority first slash":if("/"!=b){c("Expected '/', got: "+b),d="authority ignore slashes";continue}d="authority second slash";break;case"authority second slash":if(d="authority ignore slashes","/"!=b){c("Expected '/', got: "+b);continue}break;case"authority ignore slashes":if("/"!=b&&"\\"!=b){d="authority";continue}c("Expected authority, got: "+b);break;case"authority":if("@"==b){w&&(c("@ already seen."),u+="%40"),w=!0;for(var L=0;L>>0)+(t++ +"__")};n.prototype={set:function(t,n){var o=t[this.name];return o&&o[0]===t?o[1]=n:e(t,this.name,{value:[t,n],writable:!0}),this},get:function(e){var t;return(t=e[this.name])&&t[0]===e?t[1]:void 0},"delete":function(e){var t=e[this.name];return t&&t[0]===e?(t[0]=t[1]=void 0,!0):!1},has:function(e){var t=e[this.name];return t?t[0]===e:!1}},window.WeakMap=n}(),function(e){function t(e){b.push(e),g||(g=!0,m(o))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function o(){g=!1;var e=b;b=[],e.sort(function(e,t){return e.uid_-t.uid_});var t=!1;e.forEach(function(e){var n=e.takeRecords();r(e),n.length&&(e.callback_(n,e),t=!0)}),t&&o()}function r(e){e.nodes_.forEach(function(t){var n=v.get(t);n&&n.forEach(function(t){t.observer===e&&t.removeTransientObservers()})})}function i(e,t){for(var n=e;n;n=n.parentNode){var o=v.get(n);if(o)for(var r=0;r0){var r=n[o-1],i=f(r,e);if(i)return void(n[o-1]=i)}else t(this.observer);n[o]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.addEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=v.get(e);t||v.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=v.get(e),n=0;n":return">";case" ":return" "}}function t(t){return t.replace(u,e)}var n="undefined"==typeof HTMLTemplateElement;/Trident/.test(navigator.userAgent)&&!function(){var e=document.importNode;document.importNode=function(){var t=e.apply(document,arguments);if(t.nodeType===Node.DOCUMENT_FRAGMENT_NODE){var n=document.createDocumentFragment();return n.appendChild(t),n}return t}}();var o=function(){if(!n){var e=document.createElement("template"),t=document.createElement("template");t.content.appendChild(document.createElement("div")),e.content.appendChild(t);var o=e.cloneNode(!0);return 0===o.content.childNodes.length||0===o.content.firstChild.content.childNodes.length}}(),r="template",i=function(){};if(n){var a=document.implementation.createHTMLDocument("template"),s=!0,c=document.createElement("style");c.textContent=r+"{display:none;}";var d=document.head;d.insertBefore(c,d.firstElementChild),i.prototype=Object.create(HTMLElement.prototype),i.decorate=function(e){if(!e.content){e.content=a.createDocumentFragment();for(var n;n=e.firstChild;)e.content.appendChild(n);if(e.cloneNode=function(e){return i.cloneNode(this,e)},s)try{Object.defineProperty(e,"innerHTML",{get:function(){for(var e="",n=this.content.firstChild;n;n=n.nextSibling)e+=n.outerHTML||t(n.data);return e},set:function(e){for(a.body.innerHTML=e,i.bootstrap(a);this.content.firstChild;)this.content.removeChild(this.content.firstChild);for(;a.body.firstChild;)this.content.appendChild(a.body.firstChild)},configurable:!0})}catch(o){s=!1}i.bootstrap(e.content)}},i.bootstrap=function(e){for(var t,n=e.querySelectorAll(r),o=0,a=n.length;a>o&&(t=n[o]);o++)i.decorate(t)},document.addEventListener("DOMContentLoaded",function(){i.bootstrap(document)});var l=document.createElement;document.createElement=function(){"use strict";var e=l.apply(document,arguments);return"template"===e.localName&&i.decorate(e),e};var u=/[&\u00A0<>]/g}if(n||o){var h=Node.prototype.cloneNode;i.cloneNode=function(e,t){var n=h.call(e,!1);return this.decorate&&this.decorate(n),t&&(n.content.appendChild(h.call(e.content,!0)),this.fixClonedDom(n.content,e.content)),n},i.fixClonedDom=function(e,t){if(t.querySelectorAll)for(var n,o,i=t.querySelectorAll(r),a=e.querySelectorAll(r),s=0,c=a.length;c>s;s++)o=i[s],n=a[s],this.decorate&&this.decorate(o),n.parentNode.replaceChild(o.cloneNode(!0),n)};var f=document.importNode;Node.prototype.cloneNode=function(e){var t=h.call(this,e);return e&&i.fixClonedDom(t,this),t},document.importNode=function(e,t){if(e.localName===r)return i.cloneNode(e,t);var n=f.call(document,e,t);return t&&i.fixClonedDom(n,e),n},o&&(HTMLTemplateElement.prototype.cloneNode=function(e){return i.cloneNode(this,e)})}n&&(window.HTMLTemplateElement=i)}(),function(e){"use strict";if(!window.performance){var t=Date.now();window.performance={now:function(){return Date.now()-t}}}window.requestAnimationFrame||(window.requestAnimationFrame=function(){var e=window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame;return e?function(t){return e(function(){t(performance.now())})}:function(e){return window.setTimeout(e,1e3/60)}}()),window.cancelAnimationFrame||(window.cancelAnimationFrame=function(){return window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||function(e){clearTimeout(e)}}());var n=function(){var e=document.createEvent("Event");return e.initEvent("foo",!0,!0),e.preventDefault(),e.defaultPrevented}();if(!n){var o=Event.prototype.preventDefault;Event.prototype.preventDefault=function(){this.cancelable&&(o.call(this),Object.defineProperty(this,"defaultPrevented",{get:function(){return!0},configurable:!0}))}}var r=/Trident/.test(navigator.userAgent);if((!window.CustomEvent||r&&"function"!=typeof window.CustomEvent)&&(window.CustomEvent=function(e,t){t=t||{};var n=document.createEvent("CustomEvent");return n.initCustomEvent(e,Boolean(t.bubbles),Boolean(t.cancelable),t.detail),n},window.CustomEvent.prototype=window.Event.prototype),!window.Event||r&&"function"!=typeof window.Event){var i=window.Event;window.Event=function(e,t){t=t||{};var n=document.createEvent("Event");return n.initEvent(e,Boolean(t.bubbles),Boolean(t.cancelable)),n},window.Event.prototype=i.prototype}}(window.WebComponents),window.HTMLImports=window.HTMLImports||{flags:{}},function(e){function t(e,t){t=t||p,o(function(){i(e,t)},t)}function n(e){return"complete"===e.readyState||e.readyState===w}function o(e,t){if(n(t))e&&e();else{var r=function(){"complete"!==t.readyState&&t.readyState!==w||(t.removeEventListener(_,r),o(e,t))};t.addEventListener(_,r)}}function r(e){e.target.__loaded=!0}function i(e,t){function n(){c==d&&e&&e({allImports:s,loadedImports:l,errorImports:u})}function o(e){r(e),l.push(this),c++,n()}function i(e){u.push(this),c++,n()}var s=t.querySelectorAll("link[rel=import]"),c=0,d=s.length,l=[],u=[];if(d)for(var h,f=0;d>f&&(h=s[f]);f++)a(h)?(l.push(this),c++,n()):(h.addEventListener("load",o),h.addEventListener("error",i));else n()}function a(e){return u?e.__loaded||e["import"]&&"loading"!==e["import"].readyState:e.__importParsed}function s(e){for(var t,n=0,o=e.length;o>n&&(t=e[n]);n++)c(t)&&d(t)}function c(e){return"link"===e.localName&&"import"===e.rel}function d(e){var t=e["import"];t?r({target:e}):(e.addEventListener("load",r),e.addEventListener("error",r))}var l="import",u=Boolean(l in document.createElement("link")),h=Boolean(window.ShadowDOMPolyfill),f=function(e){return h?window.ShadowDOMPolyfill.wrapIfNeeded(e):e},p=f(document),m={get:function(){var e=window.HTMLImports.currentScript||document.currentScript||("complete"!==document.readyState?document.scripts[document.scripts.length-1]:null);return f(e)},configurable:!0};Object.defineProperty(document,"_currentScript",m),Object.defineProperty(p,"_currentScript",m);var v=/Trident/.test(navigator.userAgent),w=v?"complete":"interactive",_="readystatechange";u&&(new MutationObserver(function(e){for(var t,n=0,o=e.length;o>n&&(t=e[n]);n++)t.addedNodes&&s(t.addedNodes)}).observe(document.head,{childList:!0}),function(){if("loading"===document.readyState)for(var e,t=document.querySelectorAll("link[rel=import]"),n=0,o=t.length;o>n&&(e=t[n]);n++)d(e)}()),t(function(e){window.HTMLImports.ready=!0,window.HTMLImports.readyTime=(new Date).getTime();var t=p.createEvent("CustomEvent");t.initCustomEvent("HTMLImportsLoaded",!0,!0,e),p.dispatchEvent(t)}),e.IMPORT_LINK_TYPE=l,e.useNative=u,e.rootDocument=p,e.whenReady=t,e.isIE=v}(window.HTMLImports),function(e){var t=[],n=function(e){t.push(e)},o=function(){t.forEach(function(t){t(e)})};e.addModule=n,e.initializeModules=o}(window.HTMLImports),window.HTMLImports.addModule(function(e){var t=/(url\()([^)]*)(\))/g,n=/(@import[\s]+(?!url\())([^;]*)(;)/g,o={resolveUrlsInStyle:function(e,t){var n=e.ownerDocument,o=n.createElement("a");return e.textContent=this.resolveUrlsInCssText(e.textContent,t,o),e},resolveUrlsInCssText:function(e,o,r){var i=this.replaceUrls(e,r,o,t);return i=this.replaceUrls(i,r,o,n)},replaceUrls:function(e,t,n,o){return e.replace(o,function(e,o,r,i){var a=r.replace(/["']/g,"");return n&&(a=new URL(a,n).href),t.href=a,a=t.href,o+"'"+a+"'"+i})}};e.path=o}),window.HTMLImports.addModule(function(e){var t={async:!0,ok:function(e){return e.status>=200&&e.status<300||304===e.status||0===e.status},load:function(n,o,r){var i=new XMLHttpRequest;return(e.flags.debug||e.flags.bust)&&(n+="?"+Math.random()),i.open("GET",n,t.async),i.addEventListener("readystatechange",function(e){if(4===i.readyState){var n=null;try{var a=i.getResponseHeader("Location");a&&(n="/"===a.substr(0,1)?location.origin+a:a)}catch(e){console.error(e.message)}o.call(r,!t.ok(i)&&i,i.response||i.responseText,n)}}),i.send(),i},loadDocument:function(e,t,n){this.load(e,t,n).responseType="document"}};e.xhr=t}),window.HTMLImports.addModule(function(e){var t=e.xhr,n=e.flags,o=function(e,t){this.cache={},this.onload=e,this.oncomplete=t,this.inflight=0,this.pending={}};o.prototype={addNodes:function(e){this.inflight+=e.length;for(var t,n=0,o=e.length;o>n&&(t=e[n]);n++)this.require(t);this.checkDone()},addNode:function(e){this.inflight++,this.require(e),this.checkDone()},require:function(e){var t=e.src||e.href;e.__nodeUrl=t,this.dedupe(t,e)||this.fetch(t,e)},dedupe:function(e,t){if(this.pending[e])return this.pending[e].push(t),!0;return this.cache[e]?(this.onload(e,t,this.cache[e]),this.tail(),!0):(this.pending[e]=[t],!1)},fetch:function(e,o){if(n.load&&console.log("fetch",e,o),e)if(e.match(/^data:/)){var r=e.split(","),i=r[0],a=r[1];a=i.indexOf(";base64")>-1?atob(a):decodeURIComponent(a),setTimeout(function(){this.receive(e,o,null,a)}.bind(this),0)}else{var s=function(t,n,r){this.receive(e,o,t,n,r)}.bind(this);t.load(e,s)}else setTimeout(function(){this.receive(e,o,{error:"href must be specified"},null)}.bind(this),0)},receive:function(e,t,n,o,r){this.cache[e]=o;for(var i,a=this.pending[e],s=0,c=a.length;c>s&&(i=a[s]);s++)this.onload(e,i,o,n,r),this.tail();this.pending[e]=null},tail:function(){--this.inflight,this.checkDone()},checkDone:function(){this.inflight||this.oncomplete()}},e.Loader=o}),window.HTMLImports.addModule(function(e){var t=function(e){this.addCallback=e,this.mo=new MutationObserver(this.handler.bind(this))};t.prototype={handler:function(e){for(var t,n=0,o=e.length;o>n&&(t=e[n]);n++)"childList"===t.type&&t.addedNodes.length&&this.addedNodes(t.addedNodes)},addedNodes:function(e){this.addCallback&&this.addCallback(e);for(var t,n=0,o=e.length;o>n&&(t=e[n]);n++)t.children&&t.children.length&&this.addedNodes(t.children)},observe:function(e){this.mo.observe(e,{childList:!0,subtree:!0})}},e.Observer=t}),window.HTMLImports.addModule(function(e){function t(e){return"link"===e.localName&&e.rel===l}function n(e){var t=o(e);return"data:text/javascript;charset=utf-8,"+encodeURIComponent(t)}function o(e){return e.textContent+r(e)}function r(e){var t=e.ownerDocument;t.__importedScripts=t.__importedScripts||0;var n=e.ownerDocument.baseURI,o=t.__importedScripts?"-"+t.__importedScripts:"";return t.__importedScripts++,"\n//# sourceURL="+n+o+".js\n"}function i(e){var t=e.ownerDocument.createElement("style");return t.textContent=e.textContent,a.resolveUrlsInStyle(t),t}var a=e.path,s=e.rootDocument,c=e.flags,d=e.isIE,l=e.IMPORT_LINK_TYPE,u="link[rel="+l+"]",h={documentSelectors:u,importsSelectors:[u,"link[rel=stylesheet]:not([type])","style:not([type])","script:not([type])",'script[type="application/javascript"]','script[type="text/javascript"]'].join(","),map:{link:"parseLink",script:"parseScript",style:"parseStyle"},dynamicElements:[],parseNext:function(){var e=this.nextToParse();e&&this.parse(e)},parse:function(e){if(this.isParsed(e))return void(c.parse&&console.log("[%s] is already parsed",e.localName));var t=this[this.map[e.localName]];t&&(this.markParsing(e),t.call(this,e))},parseDynamic:function(e,t){this.dynamicElements.push(e),t||this.parseNext()},markParsing:function(e){c.parse&&console.log("parsing",e),this.parsingElement=e},markParsingComplete:function(e){e.__importParsed=!0,this.markDynamicParsingComplete(e),e.__importElement&&(e.__importElement.__importParsed=!0,this.markDynamicParsingComplete(e.__importElement)),this.parsingElement=null,c.parse&&console.log("completed",e)},markDynamicParsingComplete:function(e){var t=this.dynamicElements.indexOf(e);t>=0&&this.dynamicElements.splice(t,1)},parseImport:function(e){if(e["import"]=e.__doc,window.HTMLImports.__importsParsingHook&&window.HTMLImports.__importsParsingHook(e),e["import"]&&(e["import"].__importParsed=!0),this.markParsingComplete(e),e.__resource&&!e.__error?e.dispatchEvent(new CustomEvent("load",{bubbles:!1})):e.dispatchEvent(new CustomEvent("error",{bubbles:!1})),e.__pending)for(var t;e.__pending.length;)t=e.__pending.shift(),t&&t({target:e});this.parseNext()},parseLink:function(e){t(e)?this.parseImport(e):(e.href=e.href,this.parseGeneric(e))},parseStyle:function(e){var t=e;e=i(e),t.__appliedElement=e,e.__importElement=t,this.parseGeneric(e)},parseGeneric:function(e){this.trackElement(e),this.addElementToDocument(e)},rootImportForElement:function(e){for(var t=e;t.ownerDocument.__importLink;)t=t.ownerDocument.__importLink;return t},addElementToDocument:function(e){var t=this.rootImportForElement(e.__importElement||e);t.parentNode.insertBefore(e,t)},trackElement:function(e,t){var n=this,o=function(r){e.removeEventListener("load",o),e.removeEventListener("error",o),t&&t(r),n.markParsingComplete(e),n.parseNext()};if(e.addEventListener("load",o),e.addEventListener("error",o),d&&"style"===e.localName){var r=!1;if(-1==e.textContent.indexOf("@import"))r=!0;else if(e.sheet){r=!0;for(var i,a=e.sheet.cssRules,s=a?a.length:0,c=0;s>c&&(i=a[c]);c++)i.type===CSSRule.IMPORT_RULE&&(r=r&&Boolean(i.styleSheet))}r&&setTimeout(function(){e.dispatchEvent(new CustomEvent("load",{bubbles:!1}))})}},parseScript:function(t){var o=document.createElement("script");o.__importElement=t,o.src=t.src?t.src:n(t),e.currentScript=t,this.trackElement(o,function(t){o.parentNode&&o.parentNode.removeChild(o),e.currentScript=null}),this.addElementToDocument(o)},nextToParse:function(){return this._mayParse=[],!this.parsingElement&&(this.nextToParseInDoc(s)||this.nextToParseDynamic())},nextToParseInDoc:function(e,n){if(e&&this._mayParse.indexOf(e)<0){this._mayParse.push(e);for(var o,r=e.querySelectorAll(this.parseSelectorsForNode(e)),i=0,a=r.length;a>i&&(o=r[i]);i++)if(!this.isParsed(o))return this.hasResource(o)?t(o)?this.nextToParseInDoc(o.__doc,o):o:void 0}return n},nextToParseDynamic:function(){return this.dynamicElements[0]},parseSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===s?this.documentSelectors:this.importsSelectors},isParsed:function(e){return e.__importParsed},needsDynamicParsing:function(e){return this.dynamicElements.indexOf(e)>=0},hasResource:function(e){return!t(e)||void 0!==e.__doc}};e.parser=h,e.IMPORT_SELECTOR=u}),window.HTMLImports.addModule(function(e){function t(e){return n(e,a)}function n(e,t){return"link"===e.localName&&e.getAttribute("rel")===t}function o(e){return!!Object.getOwnPropertyDescriptor(e,"baseURI")}function r(e,t){var n=document.implementation.createHTMLDocument(a);n._URL=t;var r=n.createElement("base");r.setAttribute("href",t),n.baseURI||o(n)||Object.defineProperty(n,"baseURI",{value:t});var i=n.createElement("meta");return i.setAttribute("charset","utf-8"),n.head.appendChild(i),n.head.appendChild(r),n.body.innerHTML=e,window.HTMLTemplateElement&&HTMLTemplateElement.bootstrap&&HTMLTemplateElement.bootstrap(n),n}var i=e.flags,a=e.IMPORT_LINK_TYPE,s=e.IMPORT_SELECTOR,c=e.rootDocument,d=e.Loader,l=e.Observer,u=e.parser,h={documents:{},documentPreloadSelectors:s,importsPreloadSelectors:[s].join(","),loadNode:function(e){f.addNode(e)},loadSubtree:function(e){var t=this.marshalNodes(e);f.addNodes(t)},marshalNodes:function(e){return e.querySelectorAll(this.loadSelectorsForNode(e))},loadSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===c?this.documentPreloadSelectors:this.importsPreloadSelectors},loaded:function(e,n,o,a,s){if(i.load&&console.log("loaded",e,n),n.__resource=o,n.__error=a,t(n)){var c=this.documents[e];void 0===c&&(c=a?null:r(o,s||e),c&&(c.__importLink=n,this.bootDocument(c)),this.documents[e]=c),n.__doc=c}u.parseNext()},bootDocument:function(e){this.loadSubtree(e),this.observer.observe(e),u.parseNext()},loadedAll:function(){u.parseNext()}},f=new d(h.loaded.bind(h),h.loadedAll.bind(h));if(h.observer=new l,!document.baseURI){var p={get:function(){var e=document.querySelector("base");return e?e.href:window.location.href},configurable:!0};Object.defineProperty(document,"baseURI",p),Object.defineProperty(c,"baseURI",p)}e.importer=h,e.importLoader=f}),window.HTMLImports.addModule(function(e){var t=e.parser,n=e.importer,o={added:function(e){for(var o,r,i,a,s=0,c=e.length;c>s&&(a=e[s]);s++)o||(o=a.ownerDocument,r=t.isParsed(o)),i=this.shouldLoadNode(a),i&&n.loadNode(a),this.shouldParseNode(a)&&r&&t.parseDynamic(a,i)},shouldLoadNode:function(e){return 1===e.nodeType&&r.call(e,n.loadSelectorsForNode(e))},shouldParseNode:function(e){return 1===e.nodeType&&r.call(e,t.parseSelectorsForNode(e))}};n.observer.addCallback=o.added.bind(o);var r=HTMLElement.prototype.matches||HTMLElement.prototype.matchesSelector||HTMLElement.prototype.webkitMatchesSelector||HTMLElement.prototype.mozMatchesSelector||HTMLElement.prototype.msMatchesSelector}),function(e){function t(){window.HTMLImports.importer.bootDocument(o)}var n=e.initializeModules;e.isIE;if(!e.useNative){n();var o=e.rootDocument;"complete"===document.readyState||"interactive"===document.readyState&&!window.attachEvent?t():document.addEventListener("DOMContentLoaded",t)}}(window.HTMLImports),window.CustomElements=window.CustomElements||{flags:{}},function(e){var t=e.flags,n=[],o=function(e){n.push(e)},r=function(){n.forEach(function(t){t(e)})};e.addModule=o,e.initializeModules=r,e.hasNative=Boolean(document.registerElement),e.isIE=/Trident/.test(navigator.userAgent),e.useNative=!t.register&&e.hasNative&&!window.ShadowDOMPolyfill&&(!window.HTMLImports||window.HTMLImports.useNative)}(window.CustomElements),window.CustomElements.addModule(function(e){function t(e,t){n(e,function(e){return t(e)?!0:void o(e,t)}),o(e,t)}function n(e,t,o){var r=e.firstElementChild;if(!r)for(r=e.firstChild;r&&r.nodeType!==Node.ELEMENT_NODE;)r=r.nextSibling;for(;r;)t(r,o)!==!0&&n(r,t,o),r=r.nextElementSibling;return null}function o(e,n){for(var o=e.shadowRoot;o;)t(o,n),o=o.olderShadowRoot}function r(e,t){i(e,t,[])}function i(e,t,n){if(e=window.wrap(e),!(n.indexOf(e)>=0)){n.push(e);for(var o,r=e.querySelectorAll("link[rel="+a+"]"),s=0,c=r.length;c>s&&(o=r[s]);s++)o["import"]&&i(o["import"],t,n);t(e)}}var a=window.HTMLImports?window.HTMLImports.IMPORT_LINK_TYPE:"none";e.forDocumentTree=r,e.forSubtree=t}),window.CustomElements.addModule(function(e){function t(e,t){return n(e,t)||o(e,t)}function n(t,n){return e.upgrade(t,n)?!0:void(n&&a(t))}function o(e,t){g(e,function(e){return n(e,t)?!0:void 0})}function r(e){L.push(e),E||(E=!0,setTimeout(i))}function i(){E=!1;for(var e,t=L,n=0,o=t.length;o>n&&(e=t[n]);n++)e();L=[]}function a(e){y?r(function(){s(e)}):s(e)}function s(e){ +e.__upgraded__&&!e.__attached&&(e.__attached=!0,e.attachedCallback&&e.attachedCallback())}function c(e){d(e),g(e,function(e){d(e)})}function d(e){y?r(function(){l(e)}):l(e)}function l(e){e.__upgraded__&&e.__attached&&(e.__attached=!1,e.detachedCallback&&e.detachedCallback())}function u(e){for(var t=e,n=window.wrap(document);t;){if(t==n)return!0;t=t.parentNode||t.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&t.host}}function h(e){if(e.shadowRoot&&!e.shadowRoot.__watched){_.dom&&console.log("watching shadow-root for: ",e.localName);for(var t=e.shadowRoot;t;)m(t),t=t.olderShadowRoot}}function f(e,n){if(_.dom){var o=n[0];if(o&&"childList"===o.type&&o.addedNodes&&o.addedNodes){for(var r=o.addedNodes[0];r&&r!==document&&!r.host;)r=r.parentNode;var i=r&&(r.URL||r._URL||r.host&&r.host.localName)||"";i=i.split("/?").shift().split("/").pop()}console.group("mutations (%d) [%s]",n.length,i||"")}var a=u(e);n.forEach(function(e){"childList"===e.type&&(N(e.addedNodes,function(e){e.localName&&t(e,a)}),N(e.removedNodes,function(e){e.localName&&c(e)}))}),_.dom&&console.groupEnd()}function p(e){for(e=window.wrap(e),e||(e=window.wrap(document));e.parentNode;)e=e.parentNode;var t=e.__observer;t&&(f(e,t.takeRecords()),i())}function m(e){if(!e.__observer){var t=new MutationObserver(f.bind(this,e));t.observe(e,{childList:!0,subtree:!0}),e.__observer=t}}function v(e){e=window.wrap(e),_.dom&&console.group("upgradeDocument: ",e.baseURI.split("/").pop());var n=e===window.wrap(document);t(e,n),m(e),_.dom&&console.groupEnd()}function w(e){b(e,v)}var _=e.flags,g=e.forSubtree,b=e.forDocumentTree,y=window.MutationObserver._isPolyfilled&&_["throttle-attached"];e.hasPolyfillMutations=y,e.hasThrottledAttached=y;var E=!1,L=[],N=Array.prototype.forEach.call.bind(Array.prototype.forEach),M=Element.prototype.createShadowRoot;M&&(Element.prototype.createShadowRoot=function(){var e=M.call(this);return window.CustomElements.watchShadow(this),e}),e.watchShadow=h,e.upgradeDocumentTree=w,e.upgradeDocument=v,e.upgradeSubtree=o,e.upgradeAll=t,e.attached=a,e.takeRecords=p}),window.CustomElements.addModule(function(e){function t(t,o){if("template"===t.localName&&window.HTMLTemplateElement&&HTMLTemplateElement.decorate&&HTMLTemplateElement.decorate(t),!t.__upgraded__&&t.nodeType===Node.ELEMENT_NODE){var r=t.getAttribute("is"),i=e.getRegisteredDefinition(t.localName)||e.getRegisteredDefinition(r);if(i&&(r&&i.tag==t.localName||!r&&!i["extends"]))return n(t,i,o)}}function n(t,n,r){return a.upgrade&&console.group("upgrade:",t.localName),n.is&&t.setAttribute("is",n.is),o(t,n),t.__upgraded__=!0,i(t),r&&e.attached(t),e.upgradeSubtree(t,r),a.upgrade&&console.groupEnd(),t}function o(e,t){Object.__proto__?e.__proto__=t.prototype:(r(e,t.prototype,t["native"]),e.__proto__=t.prototype)}function r(e,t,n){for(var o={},r=t;r!==n&&r!==HTMLElement.prototype;){for(var i,a=Object.getOwnPropertyNames(r),s=0;i=a[s];s++)o[i]||(Object.defineProperty(e,i,Object.getOwnPropertyDescriptor(r,i)),o[i]=1);r=Object.getPrototypeOf(r)}}function i(e){e.createdCallback&&e.createdCallback()}var a=e.flags;e.upgrade=t,e.upgradeWithDefinition=n,e.implementPrototype=o}),window.CustomElements.addModule(function(e){function t(t,o){var c=o||{};if(!t)throw new Error("document.registerElement: first argument `name` must not be empty");if(t.indexOf("-")<0)throw new Error("document.registerElement: first argument ('name') must contain a dash ('-'). Argument provided was '"+String(t)+"'.");if(r(t))throw new Error("Failed to execute 'registerElement' on 'Document': Registration failed for type '"+String(t)+"'. The type name is invalid.");if(d(t))throw new Error("DuplicateDefinitionError: a type with name '"+String(t)+"' is already registered");return c.prototype||(c.prototype=Object.create(HTMLElement.prototype)),c.__name=t.toLowerCase(),c["extends"]&&(c["extends"]=c["extends"].toLowerCase()),c.lifecycle=c.lifecycle||{},c.ancestry=i(c["extends"]),a(c),s(c),n(c.prototype),l(c.__name,c),c.ctor=u(c),c.ctor.prototype=c.prototype,c.prototype.constructor=c.ctor,e.ready&&v(document),c.ctor}function n(e){if(!e.setAttribute._polyfilled){var t=e.setAttribute;e.setAttribute=function(e,n){o.call(this,e,n,t)};var n=e.removeAttribute;e.removeAttribute=function(e){o.call(this,e,null,n)},e.setAttribute._polyfilled=!0}}function o(e,t,n){e=e.toLowerCase();var o=this.getAttribute(e);n.apply(this,arguments);var r=this.getAttribute(e);this.attributeChangedCallback&&r!==o&&this.attributeChangedCallback(e,o,r)}function r(e){for(var t=0;t=0&&g(o,HTMLElement),o)}function p(e,t){var n=e[t];e[t]=function(){var e=n.apply(this,arguments);return w(e),e}}var m,v=(e.isIE,e.upgradeDocumentTree),w=e.upgradeAll,_=e.upgradeWithDefinition,g=e.implementPrototype,b=e.useNative,y=["annotation-xml","color-profile","font-face","font-face-src","font-face-uri","font-face-format","font-face-name","missing-glyph"],E={},L="http://www.w3.org/1999/xhtml",N=document.createElement.bind(document),M=document.createElementNS.bind(document);m=Object.__proto__||b?function(e,t){return e instanceof t}:function(e,t){if(e instanceof t)return!0;for(var n=e;n;){if(n===t.prototype)return!0;n=n.__proto__}return!1},p(Node.prototype,"cloneNode"),p(document,"importNode"),document.registerElement=t,document.createElement=f,document.createElementNS=h,e.registry=E,e["instanceof"]=m,e.reservedTagList=y,e.getRegisteredDefinition=d,document.register=document.registerElement}),function(e){function t(){i(window.wrap(document)),window.CustomElements.ready=!0;var e=window.requestAnimationFrame||function(e){setTimeout(e,16)};e(function(){setTimeout(function(){window.CustomElements.readyTime=Date.now(),window.HTMLImports&&(window.CustomElements.elapsed=window.CustomElements.readyTime-window.HTMLImports.readyTime),document.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0}))})})}var n=e.useNative,o=e.initializeModules;e.isIE;if(n){var r=function(){};e.watchShadow=r,e.upgrade=r,e.upgradeAll=r,e.upgradeDocumentTree=r,e.upgradeSubtree=r,e.takeRecords=r,e["instanceof"]=function(e,t){return e instanceof t}}else o();var i=e.upgradeDocumentTree,a=e.upgradeDocument;if(window.wrap||(window.ShadowDOMPolyfill?(window.wrap=window.ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=window.ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}),window.HTMLImports&&(window.HTMLImports.__importsParsingHook=function(e){e["import"]&&a(wrap(e["import"]))}),"complete"===document.readyState||e.flags.eager)t();else if("interactive"!==document.readyState||window.attachEvent||window.HTMLImports&&!window.HTMLImports.ready){var s=window.HTMLImports&&!window.HTMLImports.ready?"HTMLImportsLoaded":"DOMContentLoaded";window.addEventListener(s,t)}else t()}(window.CustomElements),function(e){var t=document.createElement("style");t.textContent="body {transition: opacity ease-in 0.2s; } \nbody[unresolved] {opacity: 0; display: block; overflow: hidden; position: relative; } \n";var n=document.querySelector("head");n.insertBefore(t,n.firstChild)}(window.WebComponents); \ No newline at end of file diff --git a/rhodecode/public/js/vendors/webcomponentsjs/webcomponents.min.js b/rhodecode/public/js/vendors/webcomponentsjs/webcomponents.min.js new file mode 100644 index 00000000..a426238a --- /dev/null +++ b/rhodecode/public/js/vendors/webcomponentsjs/webcomponents.min.js @@ -0,0 +1,14 @@ +/** + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ +// @version 0.7.22 +!function(){window.WebComponents=window.WebComponents||{flags:{}};var e="webcomponents.js",t=document.querySelector('script[src*="'+e+'"]'),n={};if(!n.noOpts){if(location.search.slice(1).split("&").forEach(function(e){var t,r=e.split("=");r[0]&&(t=r[0].match(/wc-(.+)/))&&(n[t[1]]=r[1]||!0)}),t)for(var r,o=0;r=t.attributes[o];o++)"src"!==r.name&&(n[r.name]=r.value||!0);if(n.log&&n.log.split){var i=n.log.split(",");n.log={},i.forEach(function(e){n.log[e]=!0})}else n.log={}}n.shadow=n.shadow||n.shadowdom||n.polyfill,"native"===n.shadow?n.shadow=!1:n.shadow=n.shadow||!HTMLElement.prototype.createShadowRoot,n.register&&(window.CustomElements=window.CustomElements||{flags:{}},window.CustomElements.flags.register=n.register),WebComponents.flags=n}(),WebComponents.flags.shadow&&("undefined"==typeof WeakMap&&!function(){var e=Object.defineProperty,t=Date.now()%1e9,n=function(){this.name="__st"+(1e9*Math.random()>>>0)+(t++ +"__")};n.prototype={set:function(t,n){var r=t[this.name];return r&&r[0]===t?r[1]=n:e(t,this.name,{value:[t,n],writable:!0}),this},get:function(e){var t;return(t=e[this.name])&&t[0]===e?t[1]:void 0},"delete":function(e){var t=e[this.name];return t&&t[0]===e?(t[0]=t[1]=void 0,!0):!1},has:function(e){var t=e[this.name];return t?t[0]===e:!1}},window.WeakMap=n}(),window.ShadowDOMPolyfill={},function(e){"use strict";function t(){if("undefined"!=typeof chrome&&chrome.app&&chrome.app.runtime)return!1;if(navigator.getDeviceStorage)return!1;try{var e=new Function("return true;");return e()}catch(t){return!1}}function n(e){if(!e)throw new Error("Assertion failed")}function r(e,t){for(var n=W(t),r=0;rl;l++)c[l]=new Array(s),c[l][0]=l;for(var u=0;s>u;u++)c[0][u]=u;for(var l=1;a>l;l++)for(var u=1;s>u;u++)if(this.equals(e[t+u-1],r[o+l-1]))c[l][u]=c[l-1][u-1];else{var d=c[l-1][u]+1,p=c[l][u-1]+1;c[l][u]=p>d?d:p}return c},spliceOperationsFromEditDistances:function(e){for(var t=e.length-1,n=e[0].length-1,s=e[t][n],c=[];t>0||n>0;)if(0!=t)if(0!=n){var l,u=e[t-1][n-1],d=e[t-1][n],p=e[t][n-1];l=p>d?u>d?d:u:u>p?p:u,l==u?(u==s?c.push(r):(c.push(o),s=u),t--,n--):l==d?(c.push(a),t--,s=d):(c.push(i),n--,s=p)}else c.push(a),t--;else c.push(i),n--;return c.reverse(),c},calcSplices:function(e,n,s,c,l,u){var d=0,p=0,h=Math.min(s-n,u-l);if(0==n&&0==l&&(d=this.sharedPrefix(e,c,h)),s==e.length&&u==c.length&&(p=this.sharedSuffix(e,c,h-d)),n+=d,l+=d,s-=p,u-=p,s-n==0&&u-l==0)return[];if(n==s){for(var f=t(n,[],0);u>l;)f.removed.push(c[l++]);return[f]}if(l==u)return[t(n,[],s-n)];for(var m=this.spliceOperationsFromEditDistances(this.calcEditDistances(e,n,s,c,l,u)),f=void 0,w=[],v=n,g=l,b=0;br;r++)if(!this.equals(e[r],t[r]))return r;return n},sharedSuffix:function(e,t,n){for(var r=e.length,o=t.length,i=0;n>i&&this.equals(e[--r],t[--o]);)i++;return i},calculateSplices:function(e,t){return this.calcSplices(e,0,e.length,t,0,t.length)},equals:function(e,t){return e===t}},e.ArraySplice=n}(window.ShadowDOMPolyfill),function(e){"use strict";function t(){a=!1;var e=i.slice(0);i=[];for(var t=0;t0){for(var u=0;u0&&r.length>0;){var i=n.pop(),a=r.pop();if(i!==a)break;o=i}return o}function u(e,t,n){t instanceof G.Window&&(t=t.document);var o,i=A(t),a=A(n),s=r(n,e),o=l(i,a);o||(o=a.root);for(var c=o;c;c=c.parent)for(var u=0;u0;i--)if(!g(t[i],e,o,t,r))return!1;return!0}function w(e,t,n,r){var o=ie,i=t[0]||n;return g(i,e,o,t,r)}function v(e,t,n,r){for(var o=ae,i=1;i0&&g(n,e,o,t,r)}function g(e,t,n,r,o){var i=z.get(e);if(!i)return!0;var a=o||s(r,e);if(a===e){if(n===oe)return!0;n===ae&&(n=ie)}else if(n===ae&&!t.bubbles)return!0;if("relatedTarget"in t){var c=B(t),l=c.relatedTarget;if(l){if(l instanceof Object&&l.addEventListener){var d=V(l),p=u(t,e,d);if(p===a)return!0}else p=null;Z.set(t,p)}}J.set(t,n);var h=t.type,f=!1;X.set(t,a),Y.set(t,e),i.depth++;for(var m=0,w=i.length;w>m;m++){var v=i[m];if(v.removed)f=!0;else if(!(v.type!==h||!v.capture&&n===oe||v.capture&&n===ae))try{if("function"==typeof v.handler?v.handler.call(e,t):v.handler.handleEvent(t),ee.get(t))return!1}catch(g){P||(P=g)}}if(i.depth--,f&&0===i.depth){var b=i.slice();i.length=0;for(var m=0;mr;r++)t[r]=a(e[r]);return t.length=o,t}function o(e,t){e.prototype[t]=function(){return r(i(this)[t].apply(i(this),arguments))}}var i=e.unsafeUnwrap,a=e.wrap,s={enumerable:!1};n.prototype={item:function(e){return this[e]}},t(n.prototype,"item"),e.wrappers.NodeList=n,e.addWrapNodeListMethod=o,e.wrapNodeList=r}(window.ShadowDOMPolyfill),function(e){"use strict";e.wrapHTMLCollection=e.wrapNodeList,e.wrappers.HTMLCollection=e.wrappers.NodeList}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){O(e instanceof _)}function n(e){var t=new T;return t[0]=e,t.length=1,t}function r(e,t,n){N(t,"childList",{removedNodes:n,previousSibling:e.previousSibling,nextSibling:e.nextSibling})}function o(e,t){N(e,"childList",{removedNodes:t})}function i(e,t,r,o){if(e instanceof DocumentFragment){var i=s(e);U=!0;for(var a=i.length-1;a>=0;a--)e.removeChild(i[a]),i[a].parentNode_=t;U=!1;for(var a=0;ao;o++)r.appendChild(P(t[o]));return r}function w(e){if(void 0!==e.firstChild_)for(var t=e.firstChild_;t;){var n=t;t=t.nextSibling_,n.parentNode_=n.previousSibling_=n.nextSibling_=void 0}e.firstChild_=e.lastChild_=void 0}function v(e){if(e.invalidateShadowRenderer()){for(var t=e.firstChild;t;){O(t.parentNode===e);var n=t.nextSibling,r=P(t),o=r.parentNode;o&&X.call(o,r),t.previousSibling_=t.nextSibling_=t.parentNode_=null,t=n}e.firstChild_=e.lastChild_=null}else for(var n,i=P(e),a=i.firstChild;a;)n=a.nextSibling,X.call(i,a),a=n}function g(e){var t=e.parentNode;return t&&t.invalidateShadowRenderer()}function b(e){for(var t,n=0;ns;s++)i=b(t[s]),!o&&(a=v(i).root)&&a instanceof e.wrappers.ShadowRoot||(r[n++]=i);return n}function n(e){return String(e).replace(/\/deep\/|::shadow|>>>/g," ")}function r(e){return String(e).replace(/:host\(([^\s]+)\)/g,"$1").replace(/([^\s]):host/g,"$1").replace(":host","*").replace(/\^|\/shadow\/|\/shadow-deep\/|::shadow|\/deep\/|::content|>>>/g," ")}function o(e,t){for(var n,r=e.firstElementChild;r;){if(r.matches(t))return r;if(n=o(r,t))return n;r=r.nextElementSibling}return null}function i(e,t){return e.matches(t)}function a(e,t,n){var r=e.localName;return r===t||r===n&&e.namespaceURI===j}function s(){return!0}function c(e,t,n){return e.localName===n}function l(e,t){return e.namespaceURI===t}function u(e,t,n){return e.namespaceURI===t&&e.localName===n}function d(e,t,n,r,o,i){for(var a=e.firstElementChild;a;)r(a,o,i)&&(n[t++]=a),t=d(a,t,n,r,o,i),a=a.nextElementSibling;return t}function p(n,r,o,i,a){var s,c=g(this),l=v(this).root;if(l instanceof e.wrappers.ShadowRoot)return d(this,r,o,n,i,null);if(c instanceof N)s=S.call(c,i);else{if(!(c instanceof C))return d(this,r,o,n,i,null);s=_.call(c,i)}return t(s,r,o,a)}function h(n,r,o,i,a){var s,c=g(this),l=v(this).root;if(l instanceof e.wrappers.ShadowRoot)return d(this,r,o,n,i,a);if(c instanceof N)s=M.call(c,i,a);else{if(!(c instanceof C))return d(this,r,o,n,i,a);s=T.call(c,i,a)}return t(s,r,o,!1)}function f(n,r,o,i,a){var s,c=g(this),l=v(this).root;if(l instanceof e.wrappers.ShadowRoot)return d(this,r,o,n,i,a);if(c instanceof N)s=L.call(c,i,a);else{if(!(c instanceof C))return d(this,r,o,n,i,a);s=O.call(c,i,a)}return t(s,r,o,!1)}var m=e.wrappers.HTMLCollection,w=e.wrappers.NodeList,v=e.getTreeScope,g=e.unsafeUnwrap,b=e.wrap,y=document.querySelector,E=document.documentElement.querySelector,_=document.querySelectorAll,S=document.documentElement.querySelectorAll,T=document.getElementsByTagName,M=document.documentElement.getElementsByTagName,O=document.getElementsByTagNameNS,L=document.documentElement.getElementsByTagNameNS,N=window.Element,C=window.HTMLDocument||window.Document,j="http://www.w3.org/1999/xhtml",D={ +querySelector:function(t){var r=n(t),i=r!==t;t=r;var a,s=g(this),c=v(this).root;if(c instanceof e.wrappers.ShadowRoot)return o(this,t);if(s instanceof N)a=b(E.call(s,t));else{if(!(s instanceof C))return o(this,t);a=b(y.call(s,t))}return a&&!i&&(c=v(a).root)&&c instanceof e.wrappers.ShadowRoot?o(this,t):a},querySelectorAll:function(e){var t=n(e),r=t!==e;e=t;var o=new w;return o.length=p.call(this,i,0,o,e,r),o}},H={matches:function(t){return t=r(t),e.originalMatches.call(g(this),t)}},x={getElementsByTagName:function(e){var t=new m,n="*"===e?s:a;return t.length=h.call(this,n,0,t,e,e.toLowerCase()),t},getElementsByClassName:function(e){return this.querySelectorAll("."+e)},getElementsByTagNameNS:function(e,t){var n=new m,r=null;return r="*"===e?"*"===t?s:c:"*"===t?l:u,n.length=f.call(this,r,0,n,e||null,t),n}};e.GetElementsByInterface=x,e.SelectorsInterface=D,e.MatchesInterface=H}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){for(;e&&e.nodeType!==Node.ELEMENT_NODE;)e=e.nextSibling;return e}function n(e){for(;e&&e.nodeType!==Node.ELEMENT_NODE;)e=e.previousSibling;return e}var r=e.wrappers.NodeList,o={get firstElementChild(){return t(this.firstChild)},get lastElementChild(){return n(this.lastChild)},get childElementCount(){for(var e=0,t=this.firstElementChild;t;t=t.nextElementSibling)e++;return e},get children(){for(var e=new r,t=0,n=this.firstElementChild;n;n=n.nextElementSibling)e[t++]=n;return e.length=t,e},remove:function(){var e=this.parentNode;e&&e.removeChild(this)}},i={get nextElementSibling(){return t(this.nextSibling)},get previousElementSibling(){return n(this.previousSibling)}},a={getElementById:function(e){return/[ \t\n\r\f]/.test(e)?null:this.querySelector('[id="'+e+'"]')}};e.ChildNodeInterface=i,e.NonElementParentNodeInterface=a,e.ParentNodeInterface=o}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){r.call(this,e)}var n=e.ChildNodeInterface,r=e.wrappers.Node,o=e.enqueueMutation,i=e.mixin,a=e.registerWrapper,s=e.unsafeUnwrap,c=window.CharacterData;t.prototype=Object.create(r.prototype),i(t.prototype,{get nodeValue(){return this.data},set nodeValue(e){this.data=e},get textContent(){return this.data},set textContent(e){this.data=e},get data(){return s(this).data},set data(e){var t=s(this).data;o(this,"characterData",{oldValue:t}),s(this).data=e}}),i(t.prototype,n),a(c,t,document.createTextNode("")),e.wrappers.CharacterData=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){return e>>>0}function n(e){r.call(this,e)}var r=e.wrappers.CharacterData,o=(e.enqueueMutation,e.mixin),i=e.registerWrapper,a=window.Text;n.prototype=Object.create(r.prototype),o(n.prototype,{splitText:function(e){e=t(e);var n=this.data;if(e>n.length)throw new Error("IndexSizeError");var r=n.slice(0,e),o=n.slice(e);this.data=r;var i=this.ownerDocument.createTextNode(o);return this.parentNode&&this.parentNode.insertBefore(i,this.nextSibling),i}}),i(a,n,document.createTextNode("")),e.wrappers.Text=n}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){return i(e).getAttribute("class")}function n(e,t){a(e,"attributes",{name:"class",namespace:null,oldValue:t})}function r(t){e.invalidateRendererBasedOnAttribute(t,"class")}function o(e,o,i){var a=e.ownerElement_;if(null==a)return o.apply(e,i);var s=t(a),c=o.apply(e,i);return t(a)!==s&&(n(a,s),r(a)),c}if(!window.DOMTokenList)return void console.warn("Missing DOMTokenList prototype, please include a compatible classList polyfill such as http://goo.gl/uTcepH.");var i=e.unsafeUnwrap,a=e.enqueueMutation,s=DOMTokenList.prototype.add;DOMTokenList.prototype.add=function(){o(this,s,arguments)};var c=DOMTokenList.prototype.remove;DOMTokenList.prototype.remove=function(){o(this,c,arguments)};var l=DOMTokenList.prototype.toggle;DOMTokenList.prototype.toggle=function(){return o(this,l,arguments)}}(window.ShadowDOMPolyfill),function(e){"use strict";function t(t,n){var r=t.parentNode;if(r&&r.shadowRoot){var o=e.getRendererForHost(r);o.dependsOnAttribute(n)&&o.invalidate()}}function n(e,t,n){u(e,"attributes",{name:t,namespace:null,oldValue:n})}function r(e){a.call(this,e)}var o=e.ChildNodeInterface,i=e.GetElementsByInterface,a=e.wrappers.Node,s=e.ParentNodeInterface,c=e.SelectorsInterface,l=e.MatchesInterface,u=(e.addWrapNodeListMethod,e.enqueueMutation),d=e.mixin,p=(e.oneOf,e.registerWrapper),h=e.unsafeUnwrap,f=e.wrappers,m=window.Element,w=["matches","mozMatchesSelector","msMatchesSelector","webkitMatchesSelector"].filter(function(e){return m.prototype[e]}),v=w[0],g=m.prototype[v],b=new WeakMap;r.prototype=Object.create(a.prototype),d(r.prototype,{createShadowRoot:function(){var t=new f.ShadowRoot(this);h(this).polymerShadowRoot_=t;var n=e.getRendererForHost(this);return n.invalidate(),t},get shadowRoot(){return h(this).polymerShadowRoot_||null},setAttribute:function(e,r){var o=h(this).getAttribute(e);h(this).setAttribute(e,r),n(this,e,o),t(this,e)},removeAttribute:function(e){var r=h(this).getAttribute(e);h(this).removeAttribute(e),n(this,e,r),t(this,e)},get classList(){var e=b.get(this);if(!e){if(e=h(this).classList,!e)return;e.ownerElement_=this,b.set(this,e)}return e},get className(){return h(this).className},set className(e){this.setAttribute("class",e)},get id(){return h(this).id},set id(e){this.setAttribute("id",e)}}),w.forEach(function(e){"matches"!==e&&(r.prototype[e]=function(e){return this.matches(e)})}),m.prototype.webkitCreateShadowRoot&&(r.prototype.webkitCreateShadowRoot=r.prototype.createShadowRoot),d(r.prototype,o),d(r.prototype,i),d(r.prototype,s),d(r.prototype,c),d(r.prototype,l),p(m,r,document.createElementNS(null,"x")),e.invalidateRendererBasedOnAttribute=t,e.matchesNames=w,e.originalMatches=g,e.wrappers.Element=r}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){switch(e){case"&":return"&";case"<":return"<";case">":return">";case'"':return""";case" ":return" "}}function n(e){return e.replace(L,t)}function r(e){return e.replace(N,t)}function o(e){for(var t={},n=0;n"):c+">"+s(e)+"";case Node.TEXT_NODE:var d=e.data;return t&&j[t.localName]?d:r(d);case Node.COMMENT_NODE:return"";default:throw console.error(e),new Error("not implemented")}}function s(e){e instanceof O.HTMLTemplateElement&&(e=e.content);for(var t="",n=e.firstChild;n;n=n.nextSibling)t+=a(n,e);return t}function c(e,t,n){var r=n||"div";e.textContent="";var o=T(e.ownerDocument.createElement(r));o.innerHTML=t;for(var i;i=o.firstChild;)e.appendChild(M(i))}function l(e){m.call(this,e)}function u(e,t){var n=T(e.cloneNode(!1));n.innerHTML=t;for(var r,o=T(document.createDocumentFragment());r=n.firstChild;)o.appendChild(r);return M(o)}function d(t){return function(){return e.renderAllPending(),S(this)[t]}}function p(e){w(l,e,d(e))}function h(t){Object.defineProperty(l.prototype,t,{get:d(t),set:function(n){e.renderAllPending(),S(this)[t]=n},configurable:!0,enumerable:!0})}function f(t){Object.defineProperty(l.prototype,t,{value:function(){return e.renderAllPending(),S(this)[t].apply(S(this),arguments)},configurable:!0,enumerable:!0})}var m=e.wrappers.Element,w=e.defineGetter,v=e.enqueueMutation,g=e.mixin,b=e.nodesWereAdded,y=e.nodesWereRemoved,E=e.registerWrapper,_=e.snapshotNodeList,S=e.unsafeUnwrap,T=e.unwrap,M=e.wrap,O=e.wrappers,L=/[&\u00A0"]/g,N=/[&\u00A0<>]/g,C=o(["area","base","br","col","command","embed","hr","img","input","keygen","link","meta","param","source","track","wbr"]),j=o(["style","script","xmp","iframe","noembed","noframes","plaintext","noscript"]),D="http://www.w3.org/1999/xhtml",H=/MSIE/.test(navigator.userAgent),x=window.HTMLElement,R=window.HTMLTemplateElement;l.prototype=Object.create(m.prototype),g(l.prototype,{get innerHTML(){return s(this)},set innerHTML(e){if(H&&j[this.localName])return void(this.textContent=e);var t=_(this.childNodes);this.invalidateShadowRenderer()?this instanceof O.HTMLTemplateElement?c(this.content,e):c(this,e,this.tagName):!R&&this instanceof O.HTMLTemplateElement?c(this.content,e):S(this).innerHTML=e;var n=_(this.childNodes);v(this,"childList",{addedNodes:n,removedNodes:t}),y(t),b(n,this)},get outerHTML(){return a(this,this.parentNode)},set outerHTML(e){var t=this.parentNode;if(t){t.invalidateShadowRenderer();var n=u(t,e);t.replaceChild(n,this)}},insertAdjacentHTML:function(e,t){var n,r;switch(String(e).toLowerCase()){case"beforebegin":n=this.parentNode,r=this;break;case"afterend":n=this.parentNode,r=this.nextSibling;break;case"afterbegin":n=this,r=this.firstChild;break;case"beforeend":n=this,r=null;break;default:return}var o=u(n,t);n.insertBefore(o,r)},get hidden(){return this.hasAttribute("hidden")},set hidden(e){e?this.setAttribute("hidden",""):this.removeAttribute("hidden")}}),["clientHeight","clientLeft","clientTop","clientWidth","offsetHeight","offsetLeft","offsetTop","offsetWidth","scrollHeight","scrollWidth"].forEach(p),["scrollLeft","scrollTop"].forEach(h),["focus","getBoundingClientRect","getClientRects","scrollIntoView"].forEach(f),E(x,l,document.createElement("b")),e.wrappers.HTMLElement=l,e.getInnerHTML=s,e.setInnerHTML=c}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.mixin,o=e.registerWrapper,i=e.unsafeUnwrap,a=e.wrap,s=window.HTMLCanvasElement;t.prototype=Object.create(n.prototype),r(t.prototype,{getContext:function(){var e=i(this).getContext.apply(i(this),arguments);return e&&a(e)}}),o(s,t,document.createElement("canvas")),e.wrappers.HTMLCanvasElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.mixin,o=e.registerWrapper,i=window.HTMLContentElement;t.prototype=Object.create(n.prototype),r(t.prototype,{constructor:t,get select(){return this.getAttribute("select")},set select(e){this.setAttribute("select",e)},setAttribute:function(e,t){n.prototype.setAttribute.call(this,e,t),"select"===String(e).toLowerCase()&&this.invalidateShadowRenderer(!0)}}),i&&o(i,t),e.wrappers.HTMLContentElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.mixin,o=e.registerWrapper,i=e.wrapHTMLCollection,a=e.unwrap,s=window.HTMLFormElement;t.prototype=Object.create(n.prototype),r(t.prototype,{get elements(){return i(a(this).elements)}}),o(s,t,document.createElement("form")),e.wrappers.HTMLFormElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){r.call(this,e)}function n(e,t){if(!(this instanceof n))throw new TypeError("DOM object constructor cannot be called as a function.");var o=i(document.createElement("img"));r.call(this,o),a(o,this),void 0!==e&&(o.width=e),void 0!==t&&(o.height=t)}var r=e.wrappers.HTMLElement,o=e.registerWrapper,i=e.unwrap,a=e.rewrap,s=window.HTMLImageElement;t.prototype=Object.create(r.prototype),o(s,t,document.createElement("img")),n.prototype=t.prototype,e.wrappers.HTMLImageElement=t,e.wrappers.Image=n}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=(e.mixin,e.wrappers.NodeList,e.registerWrapper),o=window.HTMLShadowElement;t.prototype=Object.create(n.prototype),t.prototype.constructor=t,o&&r(o,t),e.wrappers.HTMLShadowElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){if(!e.defaultView)return e;var t=d.get(e);if(!t){for(t=e.implementation.createHTMLDocument("");t.lastChild;)t.removeChild(t.lastChild);d.set(e,t)}return t}function n(e){for(var n,r=t(e.ownerDocument),o=c(r.createDocumentFragment());n=e.firstChild;)o.appendChild(n);return o}function r(e){if(o.call(this,e),!p){var t=n(e);u.set(this,l(t))}}var o=e.wrappers.HTMLElement,i=e.mixin,a=e.registerWrapper,s=e.unsafeUnwrap,c=e.unwrap,l=e.wrap,u=new WeakMap,d=new WeakMap,p=window.HTMLTemplateElement;r.prototype=Object.create(o.prototype),i(r.prototype,{constructor:r,get content(){return p?l(s(this).content):u.get(this)}}),p&&a(p,r),e.wrappers.HTMLTemplateElement=r}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.registerWrapper,o=window.HTMLMediaElement;o&&(t.prototype=Object.create(n.prototype),r(o,t,document.createElement("audio")),e.wrappers.HTMLMediaElement=t)}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){r.call(this,e)}function n(e){if(!(this instanceof n))throw new TypeError("DOM object constructor cannot be called as a function.");var t=i(document.createElement("audio"));r.call(this,t),a(t,this),t.setAttribute("preload","auto"),void 0!==e&&t.setAttribute("src",e)}var r=e.wrappers.HTMLMediaElement,o=e.registerWrapper,i=e.unwrap,a=e.rewrap,s=window.HTMLAudioElement;s&&(t.prototype=Object.create(r.prototype),o(s,t,document.createElement("audio")),n.prototype=t.prototype,e.wrappers.HTMLAudioElement=t,e.wrappers.Audio=n)}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){return e.replace(/\s+/g," ").trim()}function n(e){o.call(this,e)}function r(e,t,n,i){if(!(this instanceof r))throw new TypeError("DOM object constructor cannot be called as a function.");var a=c(document.createElement("option"));o.call(this,a),s(a,this),void 0!==e&&(a.text=e),void 0!==t&&a.setAttribute("value",t),n===!0&&a.setAttribute("selected",""),a.selected=i===!0}var o=e.wrappers.HTMLElement,i=e.mixin,a=e.registerWrapper,s=e.rewrap,c=e.unwrap,l=e.wrap,u=window.HTMLOptionElement;n.prototype=Object.create(o.prototype),i(n.prototype,{get text(){return t(this.textContent)},set text(e){this.textContent=t(String(e))},get form(){return l(c(this).form)}}),a(u,n,document.createElement("option")),r.prototype=n.prototype,e.wrappers.HTMLOptionElement=n,e.wrappers.Option=r}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.mixin,o=e.registerWrapper,i=e.unwrap,a=e.wrap,s=window.HTMLSelectElement;t.prototype=Object.create(n.prototype),r(t.prototype,{add:function(e,t){"object"==typeof t&&(t=i(t)),i(this).add(i(e),t)},remove:function(e){return void 0===e?void n.prototype.remove.call(this):("object"==typeof e&&(e=i(e)),void i(this).remove(e))},get form(){return a(i(this).form)}}),o(s,t,document.createElement("select")),e.wrappers.HTMLSelectElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.mixin,o=e.registerWrapper,i=e.unwrap,a=e.wrap,s=e.wrapHTMLCollection,c=window.HTMLTableElement;t.prototype=Object.create(n.prototype),r(t.prototype,{get caption(){return a(i(this).caption)},createCaption:function(){return a(i(this).createCaption())},get tHead(){return a(i(this).tHead)},createTHead:function(){return a(i(this).createTHead())},createTFoot:function(){return a(i(this).createTFoot())},get tFoot(){return a(i(this).tFoot)},get tBodies(){return s(i(this).tBodies)},createTBody:function(){return a(i(this).createTBody())},get rows(){return s(i(this).rows)},insertRow:function(e){return a(i(this).insertRow(e))}}),o(c,t,document.createElement("table")),e.wrappers.HTMLTableElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.mixin,o=e.registerWrapper,i=e.wrapHTMLCollection,a=e.unwrap,s=e.wrap,c=window.HTMLTableSectionElement;t.prototype=Object.create(n.prototype),r(t.prototype,{constructor:t,get rows(){return i(a(this).rows)},insertRow:function(e){return s(a(this).insertRow(e))}}),o(c,t,document.createElement("thead")),e.wrappers.HTMLTableSectionElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.mixin,o=e.registerWrapper,i=e.wrapHTMLCollection,a=e.unwrap,s=e.wrap,c=window.HTMLTableRowElement;t.prototype=Object.create(n.prototype),r(t.prototype,{get cells(){return i(a(this).cells)},insertCell:function(e){return s(a(this).insertCell(e))}}),o(c,t,document.createElement("tr")),e.wrappers.HTMLTableRowElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){switch(e.localName){case"content":return new n(e);case"shadow":return new o(e);case"template":return new i(e)}r.call(this,e)}var n=e.wrappers.HTMLContentElement,r=e.wrappers.HTMLElement,o=e.wrappers.HTMLShadowElement,i=e.wrappers.HTMLTemplateElement,a=(e.mixin,e.registerWrapper),s=window.HTMLUnknownElement;t.prototype=Object.create(r.prototype),a(s,t),e.wrappers.HTMLUnknownElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.Element,r=e.wrappers.HTMLElement,o=e.registerWrapper,i=(e.defineWrapGetter,e.unsafeUnwrap),a=e.wrap,s=e.mixin,c="http://www.w3.org/2000/svg",l=window.SVGElement,u=document.createElementNS(c,"title");if(!("classList"in u)){var d=Object.getOwnPropertyDescriptor(n.prototype,"classList");Object.defineProperty(r.prototype,"classList",d),delete n.prototype.classList}t.prototype=Object.create(n.prototype),s(t.prototype,{get ownerSVGElement(){return a(i(this).ownerSVGElement)}}),o(l,t,document.createElementNS(c,"title")),e.wrappers.SVGElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){p.call(this,e)}var n=e.mixin,r=e.registerWrapper,o=e.unwrap,i=e.wrap,a=window.SVGUseElement,s="http://www.w3.org/2000/svg",c=i(document.createElementNS(s,"g")),l=document.createElementNS(s,"use"),u=c.constructor,d=Object.getPrototypeOf(u.prototype),p=d.constructor;t.prototype=Object.create(d),"instanceRoot"in l&&n(t.prototype,{get instanceRoot(){return i(o(this).instanceRoot)},get animatedInstanceRoot(){return i(o(this).animatedInstanceRoot)}}),r(a,t,l),e.wrappers.SVGUseElement=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.EventTarget,r=e.mixin,o=e.registerWrapper,i=e.unsafeUnwrap,a=e.wrap,s=window.SVGElementInstance;s&&(t.prototype=Object.create(n.prototype),r(t.prototype,{get correspondingElement(){return a(i(this).correspondingElement)},get correspondingUseElement(){return a(i(this).correspondingUseElement)},get parentNode(){return a(i(this).parentNode)},get childNodes(){throw new Error("Not implemented")},get firstChild(){return a(i(this).firstChild)},get lastChild(){return a(i(this).lastChild)},get previousSibling(){return a(i(this).previousSibling)},get nextSibling(){return a(i(this).nextSibling)}}),o(s,t),e.wrappers.SVGElementInstance=t)}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){o(e,this)}var n=e.mixin,r=e.registerWrapper,o=e.setWrapper,i=e.unsafeUnwrap,a=e.unwrap,s=e.unwrapIfNeeded,c=e.wrap,l=window.CanvasRenderingContext2D;n(t.prototype,{get canvas(){return c(i(this).canvas)},drawImage:function(){arguments[0]=s(arguments[0]),i(this).drawImage.apply(i(this),arguments)},createPattern:function(){return arguments[0]=a(arguments[0]),i(this).createPattern.apply(i(this),arguments)}}),r(l,t,document.createElement("canvas").getContext("2d")),e.wrappers.CanvasRenderingContext2D=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){i(e,this)}var n=e.addForwardingProperties,r=e.mixin,o=e.registerWrapper,i=e.setWrapper,a=e.unsafeUnwrap,s=e.unwrapIfNeeded,c=e.wrap,l=window.WebGLRenderingContext;if(l){r(t.prototype,{get canvas(){return c(a(this).canvas)},texImage2D:function(){arguments[5]=s(arguments[5]),a(this).texImage2D.apply(a(this),arguments)},texSubImage2D:function(){arguments[6]=s(arguments[6]),a(this).texSubImage2D.apply(a(this),arguments)}});var u=Object.getPrototypeOf(l.prototype);u!==Object.prototype&&n(u,t.prototype);var d=/WebKit/.test(navigator.userAgent)?{drawingBufferHeight:null,drawingBufferWidth:null}:{};o(l,t,d),e.wrappers.WebGLRenderingContext=t}}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.Node,r=e.GetElementsByInterface,o=e.NonElementParentNodeInterface,i=e.ParentNodeInterface,a=e.SelectorsInterface,s=e.mixin,c=e.registerObject,l=e.registerWrapper,u=window.DocumentFragment;t.prototype=Object.create(n.prototype),s(t.prototype,i),s(t.prototype,a),s(t.prototype,r),s(t.prototype,o),l(u,t,document.createDocumentFragment()),e.wrappers.DocumentFragment=t;var d=c(document.createComment(""));e.wrappers.Comment=d}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){var t=d(u(e).ownerDocument.createDocumentFragment());n.call(this,t),c(t,this);var o=e.shadowRoot;f.set(this,o),this.treeScope_=new r(this,a(o||e)),h.set(this,e)}var n=e.wrappers.DocumentFragment,r=e.TreeScope,o=e.elementFromPoint,i=e.getInnerHTML,a=e.getTreeScope,s=e.mixin,c=e.rewrap,l=e.setInnerHTML,u=e.unsafeUnwrap,d=e.unwrap,p=e.wrap,h=new WeakMap,f=new WeakMap;t.prototype=Object.create(n.prototype),s(t.prototype,{constructor:t,get innerHTML(){return i(this)},set innerHTML(e){l(this,e),this.invalidateShadowRenderer()},get olderShadowRoot(){return f.get(this)||null},get host(){return h.get(this)||null},invalidateShadowRenderer:function(){return h.get(this).invalidateShadowRenderer()},elementFromPoint:function(e,t){return o(this,this.ownerDocument,e,t)},getSelection:function(){return document.getSelection()},get activeElement(){var e=d(this).ownerDocument.activeElement;if(!e||!e.nodeType)return null;for(var t=p(e);!this.contains(t);){for(;t.parentNode;)t=t.parentNode;if(!t.host)return null;t=t.host}return t}}),e.wrappers.ShadowRoot=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){var t=d(e).root;return t instanceof h?t.host:null}function n(t,n){if(t.shadowRoot){n=Math.min(t.childNodes.length-1,n);var r=t.childNodes[n];if(r){var o=e.getDestinationInsertionPoints(r);if(o.length>0){var i=o[0].parentNode;i.nodeType==Node.ELEMENT_NODE&&(t=i)}}}return t}function r(e){return e=u(e),t(e)||e}function o(e){a(e,this)}var i=e.registerWrapper,a=e.setWrapper,s=e.unsafeUnwrap,c=e.unwrap,l=e.unwrapIfNeeded,u=e.wrap,d=e.getTreeScope,p=window.Range,h=e.wrappers.ShadowRoot;o.prototype={get startContainer(){return r(s(this).startContainer)},get endContainer(){return r(s(this).endContainer)},get commonAncestorContainer(){return r(s(this).commonAncestorContainer)},setStart:function(e,t){e=n(e,t),s(this).setStart(l(e),t)},setEnd:function(e,t){e=n(e,t),s(this).setEnd(l(e),t)},setStartBefore:function(e){s(this).setStartBefore(l(e))},setStartAfter:function(e){s(this).setStartAfter(l(e))},setEndBefore:function(e){s(this).setEndBefore(l(e))},setEndAfter:function(e){s(this).setEndAfter(l(e))},selectNode:function(e){s(this).selectNode(l(e))},selectNodeContents:function(e){s(this).selectNodeContents(l(e))},compareBoundaryPoints:function(e,t){return s(this).compareBoundaryPoints(e,c(t))},extractContents:function(){return u(s(this).extractContents())},cloneContents:function(){return u(s(this).cloneContents())},insertNode:function(e){s(this).insertNode(l(e))},surroundContents:function(e){s(this).surroundContents(l(e))},cloneRange:function(){return u(s(this).cloneRange())},isPointInRange:function(e,t){return s(this).isPointInRange(l(e),t)},comparePoint:function(e,t){return s(this).comparePoint(l(e),t)},intersectsNode:function(e){return s(this).intersectsNode(l(e))},toString:function(){return s(this).toString()}},p.prototype.createContextualFragment&&(o.prototype.createContextualFragment=function(e){return u(s(this).createContextualFragment(e))}),i(window.Range,o,document.createRange()),e.wrappers.Range=o}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){e.previousSibling_=e.previousSibling,e.nextSibling_=e.nextSibling,e.parentNode_=e.parentNode}function n(n,o,i){var a=x(n),s=x(o),c=i?x(i):null;if(r(o),t(o),i)n.firstChild===i&&(n.firstChild_=i),i.previousSibling_=i.previousSibling;else{n.lastChild_=n.lastChild,n.lastChild===n.firstChild&&(n.firstChild_=n.firstChild);var l=R(a.lastChild);l&&(l.nextSibling_=l.nextSibling)}e.originalInsertBefore.call(a,s,c)}function r(n){var r=x(n),o=r.parentNode;if(o){var i=R(o);t(n),n.previousSibling&&(n.previousSibling.nextSibling_=n),n.nextSibling&&(n.nextSibling.previousSibling_=n),i.lastChild===n&&(i.lastChild_=n),i.firstChild===n&&(i.firstChild_=n),e.originalRemoveChild.call(o,r)}}function o(e){P.set(e,[])}function i(e){var t=P.get(e);return t||P.set(e,t=[]),t}function a(e){for(var t=[],n=0,r=e.firstChild;r;r=r.nextSibling)t[n++]=r;return t}function s(){for(var e=0;em;m++){var w=R(i[u++]);s.get(w)||r(w)}for(var v=h.addedCount,g=i[u]&&R(i[u]),m=0;v>m;m++){var b=o[l++],y=b.node;n(t,y,g),s.set(y,!0),b.sync(s)}d+=v}for(var p=d;p=0;o--){var i=r[o],a=m(i);if(a){var s=i.olderShadowRoot;s&&(n=f(s));for(var c=0;c=0;u--)l=Object.create(l);["createdCallback","attachedCallback","detachedCallback","attributeChangedCallback"].forEach(function(e){var t=o[e];t&&(l[e]=function(){j(this)instanceof r||O(this),t.apply(j(this),arguments)})});var d={prototype:l};i&&(d["extends"]=i),r.prototype=o,r.prototype.constructor=r,e.constructorTable.set(l,r),e.nativePrototypeTable.set(o,l);k.call(C(this),t,d);return r},E([window.HTMLDocument||window.Document],["registerElement"])}E([window.HTMLBodyElement,window.HTMLDocument||window.Document,window.HTMLHeadElement,window.HTMLHtmlElement],["appendChild","compareDocumentPosition","contains","getElementsByClassName","getElementsByTagName","getElementsByTagNameNS","insertBefore","querySelector","querySelectorAll","removeChild","replaceChild"]),E([window.HTMLBodyElement,window.HTMLHeadElement,window.HTMLHtmlElement],_),E([window.HTMLDocument||window.Document],["adoptNode","importNode","contains","createComment","createDocumentFragment","createElement","createElementNS","createEvent","createEventNS","createRange","createTextNode","createTreeWalker","elementFromPoint","getElementById","getElementsByName","getSelection"]),S(t.prototype,l),S(t.prototype,d),S(t.prototype,f),S(t.prototype,p),S(t.prototype,{get implementation(){var e=H.get(this);return e?e:(e=new a(C(this).implementation),H.set(this,e),e)},get defaultView(){return j(C(this).defaultView)}}),T(window.Document,t,document.implementation.createHTMLDocument("")),window.HTMLDocument&&T(window.HTMLDocument,t),D([window.HTMLBodyElement,window.HTMLDocument||window.Document,window.HTMLHeadElement]);var A=document.implementation.createDocument;a.prototype.createDocument=function(){return arguments[2]=C(arguments[2]),j(A.apply(N(this),arguments))},s(a,"createDocumentType"),s(a,"createHTMLDocument"),c(a,"hasFeature"),T(window.DOMImplementation,a),E([window.DOMImplementation],["createDocument","createDocumentType","createHTMLDocument","hasFeature"]),e.adoptNodeNoRemove=r,e.wrappers.DOMImplementation=a,e.wrappers.Document=t}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.EventTarget,r=e.wrappers.Selection,o=e.mixin,i=e.registerWrapper,a=e.renderAllPending,s=e.unwrap,c=e.unwrapIfNeeded,l=e.wrap,u=window.Window,d=window.getComputedStyle,p=window.getDefaultComputedStyle,h=window.getSelection;t.prototype=Object.create(n.prototype),u.prototype.getComputedStyle=function(e,t){return l(this||window).getComputedStyle(c(e),t)},p&&(u.prototype.getDefaultComputedStyle=function(e,t){return l(this||window).getDefaultComputedStyle(c(e),t)}),u.prototype.getSelection=function(){return l(this||window).getSelection()},delete window.getComputedStyle,delete window.getDefaultComputedStyle,delete window.getSelection,["addEventListener","removeEventListener","dispatchEvent"].forEach(function(e){u.prototype[e]=function(){var t=l(this||window);return t[e].apply(t,arguments)},delete window[e]}),o(t.prototype,{getComputedStyle:function(e,t){return a(),d.call(s(this),c(e),t)},getSelection:function(){return a(),new r(h.call(s(this)))},get document(){return l(s(this).document)}}),p&&(t.prototype.getDefaultComputedStyle=function(e,t){return a(),p.call(s(this),c(e),t)}),i(u,t,window),e.wrappers.Window=t}(window.ShadowDOMPolyfill),function(e){"use strict";var t=e.unwrap,n=window.DataTransfer||window.Clipboard,r=n.prototype.setDragImage;r&&(n.prototype.setDragImage=function(e,n,o){r.call(this,t(e),n,o)})}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){var t;t=e instanceof i?e:new i(e&&o(e)),r(t,this)}var n=e.registerWrapper,r=e.setWrapper,o=e.unwrap,i=window.FormData;i&&(n(i,t,new i),e.wrappers.FormData=t)}(window.ShadowDOMPolyfill),function(e){"use strict";var t=e.unwrapIfNeeded,n=XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.send=function(e){return n.call(this,t(e))}}(window.ShadowDOMPolyfill),function(e){"use strict";function t(e){var t=n[e],r=window[t];if(r){var o=document.createElement(e),i=o.constructor;window[t]=i}}var n=(e.isWrapperFor,{a:"HTMLAnchorElement",area:"HTMLAreaElement",audio:"HTMLAudioElement",base:"HTMLBaseElement",body:"HTMLBodyElement",br:"HTMLBRElement",button:"HTMLButtonElement",canvas:"HTMLCanvasElement",caption:"HTMLTableCaptionElement",col:"HTMLTableColElement",content:"HTMLContentElement",data:"HTMLDataElement",datalist:"HTMLDataListElement",del:"HTMLModElement",dir:"HTMLDirectoryElement",div:"HTMLDivElement",dl:"HTMLDListElement",embed:"HTMLEmbedElement",fieldset:"HTMLFieldSetElement",font:"HTMLFontElement",form:"HTMLFormElement",frame:"HTMLFrameElement",frameset:"HTMLFrameSetElement",h1:"HTMLHeadingElement",head:"HTMLHeadElement",hr:"HTMLHRElement",html:"HTMLHtmlElement",iframe:"HTMLIFrameElement",img:"HTMLImageElement",input:"HTMLInputElement",keygen:"HTMLKeygenElement",label:"HTMLLabelElement",legend:"HTMLLegendElement",li:"HTMLLIElement",link:"HTMLLinkElement",map:"HTMLMapElement",marquee:"HTMLMarqueeElement",menu:"HTMLMenuElement",menuitem:"HTMLMenuItemElement",meta:"HTMLMetaElement",meter:"HTMLMeterElement",object:"HTMLObjectElement",ol:"HTMLOListElement",optgroup:"HTMLOptGroupElement",option:"HTMLOptionElement",output:"HTMLOutputElement",p:"HTMLParagraphElement",param:"HTMLParamElement",pre:"HTMLPreElement",progress:"HTMLProgressElement",q:"HTMLQuoteElement",script:"HTMLScriptElement",select:"HTMLSelectElement",shadow:"HTMLShadowElement",source:"HTMLSourceElement",span:"HTMLSpanElement",style:"HTMLStyleElement",table:"HTMLTableElement",tbody:"HTMLTableSectionElement",template:"HTMLTemplateElement",textarea:"HTMLTextAreaElement",thead:"HTMLTableSectionElement",time:"HTMLTimeElement",title:"HTMLTitleElement",tr:"HTMLTableRowElement",track:"HTMLTrackElement",ul:"HTMLUListElement",video:"HTMLVideoElement"});Object.keys(n).forEach(t),Object.getOwnPropertyNames(e.wrappers).forEach(function(t){window[t]=e.wrappers[t]})}(window.ShadowDOMPolyfill),function(e){function t(e,t){var n="";return Array.prototype.forEach.call(e,function(e){n+=e.textContent+"\n\n"}),t||(n=n.replace(d,"")),n}function n(e){var t=document.createElement("style");return t.textContent=e,t}function r(e){var t=n(e);document.head.appendChild(t);var r=[];if(t.sheet)try{r=t.sheet.cssRules}catch(o){}else console.warn("sheet not found",t);return t.parentNode.removeChild(t),r}function o(){C.initialized=!0,document.body.appendChild(C);var e=C.contentDocument,t=e.createElement("base");t.href=document.baseURI,e.head.appendChild(t)}function i(e){C.initialized||o(),document.body.appendChild(C),e(C.contentDocument),document.body.removeChild(C)}function a(e,t){if(t){var o;if(e.match("@import")&&D){var a=n(e);i(function(e){e.head.appendChild(a.impl),o=Array.prototype.slice.call(a.sheet.cssRules,0),t(o)})}else o=r(e),t(o)}}function s(e){e&&l().appendChild(document.createTextNode(e))}function c(e,t){var r=n(e);r.setAttribute(t,""),r.setAttribute(x,""),document.head.appendChild(r)}function l(){return j||(j=document.createElement("style"),j.setAttribute(x,""),j[x]=!0),j}var u={strictStyling:!1,registry:{},shimStyling:function(e,n,r){var o=this.prepareRoot(e,n,r),i=this.isTypeExtension(r),a=this.makeScopeSelector(n,i),s=t(o,!0);s=this.scopeCssText(s,a),e&&(e.shimmedStyle=s),this.addCssToDocument(s,n)},shimStyle:function(e,t){return this.shimCssText(e.textContent,t)},shimCssText:function(e,t){return e=this.insertDirectives(e),this.scopeCssText(e,t)},makeScopeSelector:function(e,t){return e?t?"[is="+e+"]":e:""},isTypeExtension:function(e){return e&&e.indexOf("-")<0},prepareRoot:function(e,t,n){var r=this.registerRoot(e,t,n);return this.replaceTextInStyles(r.rootStyles,this.insertDirectives),this.removeStyles(e,r.rootStyles),this.strictStyling&&this.applyScopeToContent(e,t),r.scopeStyles},removeStyles:function(e,t){for(var n,r=0,o=t.length;o>r&&(n=t[r]);r++)n.parentNode.removeChild(n)},registerRoot:function(e,t,n){var r=this.registry[t]={root:e,name:t,extendsName:n},o=this.findStyles(e);r.rootStyles=o,r.scopeStyles=r.rootStyles;var i=this.registry[r.extendsName];return i&&(r.scopeStyles=i.scopeStyles.concat(r.scopeStyles)),r},findStyles:function(e){if(!e)return[];var t=e.querySelectorAll("style");return Array.prototype.filter.call(t,function(e){return!e.hasAttribute(R)})},applyScopeToContent:function(e,t){e&&(Array.prototype.forEach.call(e.querySelectorAll("*"),function(e){e.setAttribute(t,"")}),Array.prototype.forEach.call(e.querySelectorAll("template"),function(e){this.applyScopeToContent(e.content,t)},this))},insertDirectives:function(e){return e=this.insertPolyfillDirectivesInCssText(e),this.insertPolyfillRulesInCssText(e)},insertPolyfillDirectivesInCssText:function(e){return e=e.replace(p,function(e,t){return t.slice(0,-2)+"{"}),e.replace(h,function(e,t){return t+" {"})},insertPolyfillRulesInCssText:function(e){return e=e.replace(f,function(e,t){return t.slice(0,-1)}),e.replace(m,function(e,t,n,r){var o=e.replace(t,"").replace(n,"");return r+o})},scopeCssText:function(e,t){var n=this.extractUnscopedRulesFromCssText(e);if(e=this.insertPolyfillHostInCssText(e),e=this.convertColonHost(e),e=this.convertColonHostContext(e),e=this.convertShadowDOMSelectors(e),t){var e,r=this;a(e,function(n){e=r.scopeRules(n,t)})}return e=e+"\n"+n,e.trim()},extractUnscopedRulesFromCssText:function(e){for(var t,n="";t=w.exec(e);)n+=t[1].slice(0,-1)+"\n\n";for(;t=v.exec(e);)n+=t[0].replace(t[2],"").replace(t[1],t[3])+"\n\n";return n},convertColonHost:function(e){return this.convertColonRule(e,E,this.colonHostPartReplacer)},convertColonHostContext:function(e){return this.convertColonRule(e,_,this.colonHostContextPartReplacer)},convertColonRule:function(e,t,n){return e.replace(t,function(e,t,r,o){if(t=O,r){for(var i,a=r.split(","),s=[],c=0,l=a.length;l>c&&(i=a[c]);c++)i=i.trim(),s.push(n(t,i,o));return s.join(",")}return t+o})},colonHostContextPartReplacer:function(e,t,n){return t.match(g)?this.colonHostPartReplacer(e,t,n):e+t+n+", "+t+" "+e+n},colonHostPartReplacer:function(e,t,n){return e+t.replace(g,"")+n},convertShadowDOMSelectors:function(e){for(var t=0;t","+","~"],r=e,o="["+t+"]";return n.forEach(function(e){var t=r.split(e);r=t.map(function(e){var t=e.trim().replace(L,"");return t&&n.indexOf(t)<0&&t.indexOf(o)<0&&(e=t.replace(/([^:]*)(:*)(.*)/,"$1"+o+"$2$3")),e}).join(e)}),r},insertPolyfillHostInCssText:function(e){return e.replace(M,b).replace(T,g)},propertiesFromRule:function(e){var t=e.style.cssText;e.style.content&&!e.style.content.match(/['"]+|attr/)&&(t=t.replace(/content:[^;]*;/g,"content: '"+e.style.content+"';"));var n=e.style;for(var r in n)"initial"===n[r]&&(t+=r+": initial; ");return t},replaceTextInStyles:function(e,t){e&&t&&(e instanceof Array||(e=[e]),Array.prototype.forEach.call(e,function(e){e.textContent=t.call(this,e.textContent)},this))},addCssToDocument:function(e,t){e.match("@import")?c(e,t):s(e)}},d=/\/\*[^*]*\*+([^\/*][^*]*\*+)*\//gim,p=/\/\*\s*@polyfill ([^*]*\*+([^\/*][^*]*\*+)*\/)([^{]*?){/gim,h=/polyfill-next-selector[^}]*content\:[\s]*?['"](.*?)['"][;\s]*}([^{]*?){/gim,f=/\/\*\s@polyfill-rule([^*]*\*+([^\/*][^*]*\*+)*)\//gim,m=/(polyfill-rule)[^}]*(content\:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim,w=/\/\*\s@polyfill-unscoped-rule([^*]*\*+([^\/*][^*]*\*+)*)\//gim,v=/(polyfill-unscoped-rule)[^}]*(content\:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim,g="-shadowcsshost",b="-shadowcsscontext",y=")(?:\\(((?:\\([^)(]*\\)|[^)(]*)+?)\\))?([^,{]*)",E=new RegExp("("+g+y,"gim"),_=new RegExp("("+b+y,"gim"),S="([>\\s~+[.,{:][\\s\\S]*)?$",T=/\:host/gim,M=/\:host-context/gim,O=g+"-no-combinator",L=new RegExp(g,"gim"),N=(new RegExp(b,"gim"),[/>>>/g,/::shadow/g,/::content/g,/\/deep\//g,/\/shadow\//g,/\/shadow-deep\//g,/\^\^/g,/\^/g]),C=document.createElement("iframe");C.style.display="none";var j,D=navigator.userAgent.match("Chrome"),H="shim-shadowdom",x="shim-shadowdom-css",R="no-shim";if(window.ShadowDOMPolyfill){s("style { display: none !important; }\n");var I=ShadowDOMPolyfill.wrap(document),P=I.querySelector("head");P.insertBefore(l(),P.childNodes[0]),document.addEventListener("DOMContentLoaded",function(){e.urlResolver;if(window.HTMLImports&&!HTMLImports.useNative){var t="link[rel=stylesheet]["+H+"]",n="style["+H+"]";HTMLImports.importer.documentPreloadSelectors+=","+t,HTMLImports.importer.importsPreloadSelectors+=","+t,HTMLImports.parser.documentSelectors=[HTMLImports.parser.documentSelectors,t,n].join(",");var r=HTMLImports.parser.parseGeneric;HTMLImports.parser.parseGeneric=function(e){if(!e[x]){var t=e.__importElement||e;if(!t.hasAttribute(H))return void r.call(this,e);e.__resource&&(t=e.ownerDocument.createElement("style"),t.textContent=e.__resource),HTMLImports.path.resolveUrlsInStyle(t,e.href),t.textContent=u.shimStyle(t),t.removeAttribute(H,""),t.setAttribute(x,""),t[x]=!0,t.parentNode!==P&&(e.parentNode===P?P.replaceChild(t,e):this.addElementToDocument(t)),t.__importParsed=!0,this.markParsingComplete(e),this.parseNext()}};var o=HTMLImports.parser.hasResource;HTMLImports.parser.hasResource=function(e){return"link"===e.localName&&"stylesheet"===e.rel&&e.hasAttribute(H)?e.__resource:o.call(this,e)}}})}e.ShadowCSS=u}(window.WebComponents)),function(e){window.ShadowDOMPolyfill?(window.wrap=ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}}(window.WebComponents),function(e){"use strict";function t(e){return void 0!==p[e]}function n(){s.call(this),this._isInvalid=!0}function r(e){return""==e&&n.call(this),e.toLowerCase()}function o(e){var t=e.charCodeAt(0);return t>32&&127>t&&-1==[34,35,60,62,63,96].indexOf(t)?e:encodeURIComponent(e)}function i(e){var t=e.charCodeAt(0);return t>32&&127>t&&-1==[34,35,60,62,96].indexOf(t)?e:encodeURIComponent(e)}function a(e,a,s){function c(e){b.push(e)}var l=a||"scheme start",u=0,d="",v=!1,g=!1,b=[];e:for(;(e[u-1]!=f||0==u)&&!this._isInvalid;){var y=e[u];switch(l){case"scheme start":if(!y||!m.test(y)){if(a){c("Invalid scheme.");break e}d="",l="no scheme";continue}d+=y.toLowerCase(),l="scheme";break;case"scheme":if(y&&w.test(y))d+=y.toLowerCase();else{if(":"!=y){if(a){if(f==y)break e;c("Code point not allowed in scheme: "+y);break e}d="",u=0,l="no scheme";continue}if(this._scheme=d,d="",a)break e;t(this._scheme)&&(this._isRelative=!0),l="file"==this._scheme?"relative":this._isRelative&&s&&s._scheme==this._scheme?"relative or authority":this._isRelative?"authority first slash":"scheme data"}break;case"scheme data":"?"==y?(this._query="?",l="query"):"#"==y?(this._fragment="#",l="fragment"):f!=y&&" "!=y&&"\n"!=y&&"\r"!=y&&(this._schemeData+=o(y));break;case"no scheme":if(s&&t(s._scheme)){l="relative";continue}c("Missing scheme."),n.call(this);break;case"relative or authority":if("/"!=y||"/"!=e[u+1]){c("Expected /, got: "+y),l="relative";continue}l="authority ignore slashes";break;case"relative":if(this._isRelative=!0,"file"!=this._scheme&&(this._scheme=s._scheme),f==y){this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._username=s._username,this._password=s._password;break e}if("/"==y||"\\"==y)"\\"==y&&c("\\ is an invalid code point."),l="relative slash";else if("?"==y)this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query="?",this._username=s._username,this._password=s._password,l="query";else{if("#"!=y){var E=e[u+1],_=e[u+2];("file"!=this._scheme||!m.test(y)||":"!=E&&"|"!=E||f!=_&&"/"!=_&&"\\"!=_&&"?"!=_&&"#"!=_)&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password,this._path=s._path.slice(),this._path.pop()),l="relative path";continue}this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._fragment="#",this._username=s._username,this._password=s._password,l="fragment"}break;case"relative slash":if("/"!=y&&"\\"!=y){"file"!=this._scheme&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password),l="relative path";continue}"\\"==y&&c("\\ is an invalid code point."),l="file"==this._scheme?"file host":"authority ignore slashes";break;case"authority first slash":if("/"!=y){c("Expected '/', got: "+y),l="authority ignore slashes";continue}l="authority second slash";break;case"authority second slash":if(l="authority ignore slashes","/"!=y){c("Expected '/', got: "+y);continue}break;case"authority ignore slashes":if("/"!=y&&"\\"!=y){l="authority";continue}c("Expected authority, got: "+y);break;case"authority":if("@"==y){v&&(c("@ already seen."),d+="%40"),v=!0;for(var S=0;S0){var o=n[r-1],i=h(o,e);if(i)return void(n[r-1]=i)}else t(this.observer);n[r]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.addEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=w.get(e);t||w.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=w.get(e),n=0;nh&&(p=s[h]);h++)a(p)?(u.push(this),c++,n()):(p.addEventListener("load",r),p.addEventListener("error",i));else n()}function a(e){return d?e.__loaded||e["import"]&&"loading"!==e["import"].readyState:e.__importParsed}function s(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)c(t)&&l(t)}function c(e){return"link"===e.localName&&"import"===e.rel}function l(e){var t=e["import"];t?o({target:e}):(e.addEventListener("load",o),e.addEventListener("error",o))}var u="import",d=Boolean(u in document.createElement("link")),p=Boolean(window.ShadowDOMPolyfill),h=function(e){return p?window.ShadowDOMPolyfill.wrapIfNeeded(e):e},f=h(document),m={get:function(){var e=window.HTMLImports.currentScript||document.currentScript||("complete"!==document.readyState?document.scripts[document.scripts.length-1]:null);return h(e)},configurable:!0};Object.defineProperty(document,"_currentScript",m),Object.defineProperty(f,"_currentScript",m);var w=/Trident/.test(navigator.userAgent),v=w?"complete":"interactive",g="readystatechange";d&&(new MutationObserver(function(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)t.addedNodes&&s(t.addedNodes)}).observe(document.head,{childList:!0}),function(){if("loading"===document.readyState)for(var e,t=document.querySelectorAll("link[rel=import]"),n=0,r=t.length;r>n&&(e=t[n]);n++)l(e)}()),t(function(e){window.HTMLImports.ready=!0,window.HTMLImports.readyTime=(new Date).getTime();var t=f.createEvent("CustomEvent");t.initCustomEvent("HTMLImportsLoaded",!0,!0,e),f.dispatchEvent(t)}),e.IMPORT_LINK_TYPE=u,e.useNative=d,e.rootDocument=f,e.whenReady=t,e.isIE=w}(window.HTMLImports),function(e){var t=[],n=function(e){t.push(e)},r=function(){t.forEach(function(t){t(e)})};e.addModule=n,e.initializeModules=r}(window.HTMLImports),window.HTMLImports.addModule(function(e){var t=/(url\()([^)]*)(\))/g,n=/(@import[\s]+(?!url\())([^;]*)(;)/g,r={resolveUrlsInStyle:function(e,t){var n=e.ownerDocument,r=n.createElement("a");return e.textContent=this.resolveUrlsInCssText(e.textContent,t,r),e},resolveUrlsInCssText:function(e,r,o){var i=this.replaceUrls(e,o,r,t);return i=this.replaceUrls(i,o,r,n)},replaceUrls:function(e,t,n,r){return e.replace(r,function(e,r,o,i){var a=o.replace(/["']/g,"");return n&&(a=new URL(a,n).href),t.href=a,a=t.href,r+"'"+a+"'"+i})}};e.path=r}),window.HTMLImports.addModule(function(e){var t={async:!0,ok:function(e){return e.status>=200&&e.status<300||304===e.status||0===e.status},load:function(n,r,o){var i=new XMLHttpRequest;return(e.flags.debug||e.flags.bust)&&(n+="?"+Math.random()),i.open("GET",n,t.async),i.addEventListener("readystatechange",function(e){if(4===i.readyState){var n=null;try{var a=i.getResponseHeader("Location");a&&(n="/"===a.substr(0,1)?location.origin+a:a)}catch(e){console.error(e.message)}r.call(o,!t.ok(i)&&i,i.response||i.responseText,n)}}),i.send(),i},loadDocument:function(e,t,n){this.load(e,t,n).responseType="document"}};e.xhr=t}),window.HTMLImports.addModule(function(e){var t=e.xhr,n=e.flags,r=function(e,t){this.cache={},this.onload=e,this.oncomplete=t,this.inflight=0,this.pending={}};r.prototype={addNodes:function(e){this.inflight+=e.length;for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)this.require(t);this.checkDone()},addNode:function(e){this.inflight++,this.require(e),this.checkDone()},require:function(e){var t=e.src||e.href;e.__nodeUrl=t,this.dedupe(t,e)||this.fetch(t,e)},dedupe:function(e,t){if(this.pending[e])return this.pending[e].push(t),!0;return this.cache[e]?(this.onload(e,t,this.cache[e]),this.tail(),!0):(this.pending[e]=[t],!1)},fetch:function(e,r){if(n.load&&console.log("fetch",e,r),e)if(e.match(/^data:/)){var o=e.split(","),i=o[0],a=o[1];a=i.indexOf(";base64")>-1?atob(a):decodeURIComponent(a),setTimeout(function(){this.receive(e,r,null,a)}.bind(this),0)}else{var s=function(t,n,o){this.receive(e,r,t,n,o)}.bind(this);t.load(e,s)}else setTimeout(function(){this.receive(e,r,{error:"href must be specified"},null)}.bind(this),0)},receive:function(e,t,n,r,o){this.cache[e]=r;for(var i,a=this.pending[e],s=0,c=a.length;c>s&&(i=a[s]);s++)this.onload(e,i,r,n,o),this.tail();this.pending[e]=null},tail:function(){--this.inflight,this.checkDone()},checkDone:function(){this.inflight||this.oncomplete()}},e.Loader=r}),window.HTMLImports.addModule(function(e){var t=function(e){this.addCallback=e,this.mo=new MutationObserver(this.handler.bind(this))};t.prototype={handler:function(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)"childList"===t.type&&t.addedNodes.length&&this.addedNodes(t.addedNodes)},addedNodes:function(e){this.addCallback&&this.addCallback(e);for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)t.children&&t.children.length&&this.addedNodes(t.children)},observe:function(e){this.mo.observe(e,{childList:!0,subtree:!0})}},e.Observer=t}),window.HTMLImports.addModule(function(e){function t(e){return"link"===e.localName&&e.rel===u}function n(e){var t=r(e);return"data:text/javascript;charset=utf-8,"+encodeURIComponent(t)}function r(e){return e.textContent+o(e)}function o(e){var t=e.ownerDocument;t.__importedScripts=t.__importedScripts||0;var n=e.ownerDocument.baseURI,r=t.__importedScripts?"-"+t.__importedScripts:"";return t.__importedScripts++,"\n//# sourceURL="+n+r+".js\n"}function i(e){var t=e.ownerDocument.createElement("style");return t.textContent=e.textContent,a.resolveUrlsInStyle(t),t}var a=e.path,s=e.rootDocument,c=e.flags,l=e.isIE,u=e.IMPORT_LINK_TYPE,d="link[rel="+u+"]",p={documentSelectors:d,importsSelectors:[d,"link[rel=stylesheet]:not([type])","style:not([type])","script:not([type])",'script[type="application/javascript"]','script[type="text/javascript"]'].join(","),map:{link:"parseLink",script:"parseScript",style:"parseStyle"},dynamicElements:[],parseNext:function(){var e=this.nextToParse();e&&this.parse(e)},parse:function(e){if(this.isParsed(e))return void(c.parse&&console.log("[%s] is already parsed",e.localName));var t=this[this.map[e.localName]];t&&(this.markParsing(e),t.call(this,e))},parseDynamic:function(e,t){this.dynamicElements.push(e),t||this.parseNext()},markParsing:function(e){c.parse&&console.log("parsing",e),this.parsingElement=e},markParsingComplete:function(e){e.__importParsed=!0,this.markDynamicParsingComplete(e),e.__importElement&&(e.__importElement.__importParsed=!0,this.markDynamicParsingComplete(e.__importElement)),this.parsingElement=null,c.parse&&console.log("completed",e)},markDynamicParsingComplete:function(e){var t=this.dynamicElements.indexOf(e);t>=0&&this.dynamicElements.splice(t,1)},parseImport:function(e){if(e["import"]=e.__doc,window.HTMLImports.__importsParsingHook&&window.HTMLImports.__importsParsingHook(e),e["import"]&&(e["import"].__importParsed=!0),this.markParsingComplete(e),e.__resource&&!e.__error?e.dispatchEvent(new CustomEvent("load",{bubbles:!1})):e.dispatchEvent(new CustomEvent("error",{bubbles:!1})),e.__pending)for(var t;e.__pending.length;)t=e.__pending.shift(),t&&t({target:e});this.parseNext()},parseLink:function(e){t(e)?this.parseImport(e):(e.href=e.href,this.parseGeneric(e))},parseStyle:function(e){var t=e;e=i(e),t.__appliedElement=e,e.__importElement=t,this.parseGeneric(e)},parseGeneric:function(e){this.trackElement(e),this.addElementToDocument(e)},rootImportForElement:function(e){for(var t=e;t.ownerDocument.__importLink;)t=t.ownerDocument.__importLink;return t},addElementToDocument:function(e){var t=this.rootImportForElement(e.__importElement||e);t.parentNode.insertBefore(e,t)},trackElement:function(e,t){var n=this,r=function(o){e.removeEventListener("load",r),e.removeEventListener("error",r),t&&t(o),n.markParsingComplete(e),n.parseNext()};if(e.addEventListener("load",r),e.addEventListener("error",r),l&&"style"===e.localName){var o=!1;if(-1==e.textContent.indexOf("@import"))o=!0;else if(e.sheet){o=!0;for(var i,a=e.sheet.cssRules,s=a?a.length:0,c=0;s>c&&(i=a[c]);c++)i.type===CSSRule.IMPORT_RULE&&(o=o&&Boolean(i.styleSheet))}o&&setTimeout(function(){e.dispatchEvent(new CustomEvent("load",{bubbles:!1}))})}},parseScript:function(t){var r=document.createElement("script");r.__importElement=t,r.src=t.src?t.src:n(t),e.currentScript=t,this.trackElement(r,function(t){r.parentNode&&r.parentNode.removeChild(r),e.currentScript=null}),this.addElementToDocument(r)},nextToParse:function(){return this._mayParse=[],!this.parsingElement&&(this.nextToParseInDoc(s)||this.nextToParseDynamic())},nextToParseInDoc:function(e,n){if(e&&this._mayParse.indexOf(e)<0){this._mayParse.push(e);for(var r,o=e.querySelectorAll(this.parseSelectorsForNode(e)),i=0,a=o.length;a>i&&(r=o[i]);i++)if(!this.isParsed(r))return this.hasResource(r)?t(r)?this.nextToParseInDoc(r.__doc,r):r:void 0}return n},nextToParseDynamic:function(){return this.dynamicElements[0]},parseSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===s?this.documentSelectors:this.importsSelectors},isParsed:function(e){return e.__importParsed},needsDynamicParsing:function(e){return this.dynamicElements.indexOf(e)>=0},hasResource:function(e){return!t(e)||void 0!==e.__doc}};e.parser=p,e.IMPORT_SELECTOR=d}),window.HTMLImports.addModule(function(e){function t(e){return n(e,a)}function n(e,t){return"link"===e.localName&&e.getAttribute("rel")===t}function r(e){return!!Object.getOwnPropertyDescriptor(e,"baseURI")}function o(e,t){var n=document.implementation.createHTMLDocument(a);n._URL=t;var o=n.createElement("base");o.setAttribute("href",t),n.baseURI||r(n)||Object.defineProperty(n,"baseURI",{value:t});var i=n.createElement("meta");return i.setAttribute("charset","utf-8"),n.head.appendChild(i),n.head.appendChild(o),n.body.innerHTML=e,window.HTMLTemplateElement&&HTMLTemplateElement.bootstrap&&HTMLTemplateElement.bootstrap(n),n}var i=e.flags,a=e.IMPORT_LINK_TYPE,s=e.IMPORT_SELECTOR,c=e.rootDocument,l=e.Loader,u=e.Observer,d=e.parser,p={documents:{},documentPreloadSelectors:s,importsPreloadSelectors:[s].join(","),loadNode:function(e){h.addNode(e)},loadSubtree:function(e){var t=this.marshalNodes(e);h.addNodes(t)},marshalNodes:function(e){return e.querySelectorAll(this.loadSelectorsForNode(e))},loadSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===c?this.documentPreloadSelectors:this.importsPreloadSelectors},loaded:function(e,n,r,a,s){if(i.load&&console.log("loaded",e,n),n.__resource=r,n.__error=a,t(n)){var c=this.documents[e];void 0===c&&(c=a?null:o(r,s||e),c&&(c.__importLink=n,this.bootDocument(c)),this.documents[e]=c),n.__doc=c}d.parseNext()},bootDocument:function(e){this.loadSubtree(e),this.observer.observe(e),d.parseNext()},loadedAll:function(){d.parseNext()}},h=new l(p.loaded.bind(p),p.loadedAll.bind(p));if(p.observer=new u,!document.baseURI){var f={get:function(){var e=document.querySelector("base");return e?e.href:window.location.href},configurable:!0};Object.defineProperty(document,"baseURI",f),Object.defineProperty(c,"baseURI",f)}e.importer=p,e.importLoader=h}),window.HTMLImports.addModule(function(e){var t=e.parser,n=e.importer,r={added:function(e){for(var r,o,i,a,s=0,c=e.length;c>s&&(a=e[s]);s++)r||(r=a.ownerDocument,o=t.isParsed(r)),i=this.shouldLoadNode(a),i&&n.loadNode(a),this.shouldParseNode(a)&&o&&t.parseDynamic(a,i)},shouldLoadNode:function(e){return 1===e.nodeType&&o.call(e,n.loadSelectorsForNode(e))},shouldParseNode:function(e){return 1===e.nodeType&&o.call(e,t.parseSelectorsForNode(e))}};n.observer.addCallback=r.added.bind(r);var o=HTMLElement.prototype.matches||HTMLElement.prototype.matchesSelector||HTMLElement.prototype.webkitMatchesSelector||HTMLElement.prototype.mozMatchesSelector||HTMLElement.prototype.msMatchesSelector}),function(e){function t(){window.HTMLImports.importer.bootDocument(r)}var n=e.initializeModules;e.isIE;if(!e.useNative){n();var r=e.rootDocument;"complete"===document.readyState||"interactive"===document.readyState&&!window.attachEvent?t():document.addEventListener("DOMContentLoaded",t)}}(window.HTMLImports),window.CustomElements=window.CustomElements||{flags:{}},function(e){var t=e.flags,n=[],r=function(e){n.push(e)},o=function(){n.forEach(function(t){t(e)})};e.addModule=r,e.initializeModules=o,e.hasNative=Boolean(document.registerElement),e.isIE=/Trident/.test(navigator.userAgent),e.useNative=!t.register&&e.hasNative&&!window.ShadowDOMPolyfill&&(!window.HTMLImports||window.HTMLImports.useNative)}(window.CustomElements),window.CustomElements.addModule(function(e){function t(e,t){n(e,function(e){return t(e)?!0:void r(e,t)}),r(e,t)}function n(e,t,r){var o=e.firstElementChild;if(!o)for(o=e.firstChild;o&&o.nodeType!==Node.ELEMENT_NODE;)o=o.nextSibling;for(;o;)t(o,r)!==!0&&n(o,t,r),o=o.nextElementSibling;return null}function r(e,n){for(var r=e.shadowRoot;r;)t(r,n),r=r.olderShadowRoot}function o(e,t){i(e,t,[])}function i(e,t,n){if(e=window.wrap(e),!(n.indexOf(e)>=0)){n.push(e);for(var r,o=e.querySelectorAll("link[rel="+a+"]"),s=0,c=o.length;c>s&&(r=o[s]);s++)r["import"]&&i(r["import"],t,n);t(e)}}var a=window.HTMLImports?window.HTMLImports.IMPORT_LINK_TYPE:"none";e.forDocumentTree=o,e.forSubtree=t}),window.CustomElements.addModule(function(e){function t(e,t){return n(e,t)||r(e,t)}function n(t,n){return e.upgrade(t,n)?!0:void(n&&a(t))}function r(e,t){b(e,function(e){return n(e,t)?!0:void 0})}function o(e){S.push(e),_||(_=!0,setTimeout(i))}function i(){_=!1;for(var e,t=S,n=0,r=t.length;r>n&&(e=t[n]);n++)e();S=[]}function a(e){E?o(function(){s(e)}):s(e)}function s(e){e.__upgraded__&&!e.__attached&&(e.__attached=!0,e.attachedCallback&&e.attachedCallback())}function c(e){l(e),b(e,function(e){l(e)})}function l(e){E?o(function(){u(e)}):u(e)}function u(e){e.__upgraded__&&e.__attached&&(e.__attached=!1,e.detachedCallback&&e.detachedCallback())}function d(e){for(var t=e,n=window.wrap(document);t;){if(t==n)return!0;t=t.parentNode||t.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&t.host}}function p(e){if(e.shadowRoot&&!e.shadowRoot.__watched){g.dom&&console.log("watching shadow-root for: ",e.localName);for(var t=e.shadowRoot;t;)m(t),t=t.olderShadowRoot}}function h(e,n){if(g.dom){var r=n[0];if(r&&"childList"===r.type&&r.addedNodes&&r.addedNodes){for(var o=r.addedNodes[0];o&&o!==document&&!o.host;)o=o.parentNode;var i=o&&(o.URL||o._URL||o.host&&o.host.localName)||"";i=i.split("/?").shift().split("/").pop()}console.group("mutations (%d) [%s]",n.length,i||"")}var a=d(e);n.forEach(function(e){"childList"===e.type&&(T(e.addedNodes,function(e){e.localName&&t(e,a)}),T(e.removedNodes,function(e){e.localName&&c(e)}))}),g.dom&&console.groupEnd()}function f(e){for(e=window.wrap(e),e||(e=window.wrap(document));e.parentNode;)e=e.parentNode;var t=e.__observer;t&&(h(e,t.takeRecords()),i())}function m(e){if(!e.__observer){var t=new MutationObserver(h.bind(this,e));t.observe(e,{childList:!0,subtree:!0}),e.__observer=t}}function w(e){e=window.wrap(e),g.dom&&console.group("upgradeDocument: ",e.baseURI.split("/").pop());var n=e===window.wrap(document);t(e,n),m(e),g.dom&&console.groupEnd()}function v(e){y(e,w)}var g=e.flags,b=e.forSubtree,y=e.forDocumentTree,E=window.MutationObserver._isPolyfilled&&g["throttle-attached"];e.hasPolyfillMutations=E,e.hasThrottledAttached=E;var _=!1,S=[],T=Array.prototype.forEach.call.bind(Array.prototype.forEach),M=Element.prototype.createShadowRoot;M&&(Element.prototype.createShadowRoot=function(){var e=M.call(this);return window.CustomElements.watchShadow(this),e}),e.watchShadow=p,e.upgradeDocumentTree=v,e.upgradeDocument=w,e.upgradeSubtree=r,e.upgradeAll=t,e.attached=a,e.takeRecords=f}),window.CustomElements.addModule(function(e){function t(t,r){if("template"===t.localName&&window.HTMLTemplateElement&&HTMLTemplateElement.decorate&&HTMLTemplateElement.decorate(t),!t.__upgraded__&&t.nodeType===Node.ELEMENT_NODE){var o=t.getAttribute("is"),i=e.getRegisteredDefinition(t.localName)||e.getRegisteredDefinition(o);if(i&&(o&&i.tag==t.localName||!o&&!i["extends"]))return n(t,i,r)}}function n(t,n,o){return a.upgrade&&console.group("upgrade:",t.localName),n.is&&t.setAttribute("is",n.is),r(t,n),t.__upgraded__=!0,i(t),o&&e.attached(t),e.upgradeSubtree(t,o),a.upgrade&&console.groupEnd(),t}function r(e,t){Object.__proto__?e.__proto__=t.prototype:(o(e,t.prototype,t["native"]),e.__proto__=t.prototype)}function o(e,t,n){for(var r={},o=t;o!==n&&o!==HTMLElement.prototype;){for(var i,a=Object.getOwnPropertyNames(o),s=0;i=a[s];s++)r[i]||(Object.defineProperty(e,i,Object.getOwnPropertyDescriptor(o,i)),r[i]=1);o=Object.getPrototypeOf(o)}}function i(e){e.createdCallback&&e.createdCallback()}var a=e.flags;e.upgrade=t,e.upgradeWithDefinition=n,e.implementPrototype=r}),window.CustomElements.addModule(function(e){function t(t,r){var c=r||{};if(!t)throw new Error("document.registerElement: first argument `name` must not be empty");if(t.indexOf("-")<0)throw new Error("document.registerElement: first argument ('name') must contain a dash ('-'). Argument provided was '"+String(t)+"'.");if(o(t))throw new Error("Failed to execute 'registerElement' on 'Document': Registration failed for type '"+String(t)+"'. The type name is invalid.");if(l(t))throw new Error("DuplicateDefinitionError: a type with name '"+String(t)+"' is already registered");return c.prototype||(c.prototype=Object.create(HTMLElement.prototype)),c.__name=t.toLowerCase(),c["extends"]&&(c["extends"]=c["extends"].toLowerCase()),c.lifecycle=c.lifecycle||{},c.ancestry=i(c["extends"]),a(c),s(c),n(c.prototype),u(c.__name,c),c.ctor=d(c),c.ctor.prototype=c.prototype,c.prototype.constructor=c.ctor,e.ready&&w(document),c.ctor}function n(e){if(!e.setAttribute._polyfilled){var t=e.setAttribute;e.setAttribute=function(e,n){r.call(this,e,n,t)};var n=e.removeAttribute;e.removeAttribute=function(e){r.call(this,e,null,n)},e.setAttribute._polyfilled=!0}}function r(e,t,n){e=e.toLowerCase();var r=this.getAttribute(e);n.apply(this,arguments);var o=this.getAttribute(e);this.attributeChangedCallback&&o!==r&&this.attributeChangedCallback(e,r,o)}function o(e){for(var t=0;t=0&&b(r,HTMLElement),r)}function f(e,t){var n=e[t];e[t]=function(){var e=n.apply(this,arguments);return v(e),e}}var m,w=(e.isIE,e.upgradeDocumentTree),v=e.upgradeAll,g=e.upgradeWithDefinition,b=e.implementPrototype,y=e.useNative,E=["annotation-xml","color-profile","font-face","font-face-src","font-face-uri","font-face-format","font-face-name","missing-glyph"],_={},S="http://www.w3.org/1999/xhtml",T=document.createElement.bind(document),M=document.createElementNS.bind(document);m=Object.__proto__||y?function(e,t){return e instanceof t}:function(e,t){if(e instanceof t)return!0;for(var n=e;n;){if(n===t.prototype)return!0;n=n.__proto__}return!1},f(Node.prototype,"cloneNode"),f(document,"importNode"),document.registerElement=t,document.createElement=h,document.createElementNS=p,e.registry=_,e["instanceof"]=m,e.reservedTagList=E,e.getRegisteredDefinition=l,document.register=document.registerElement}),function(e){function t(){i(window.wrap(document)),window.CustomElements.ready=!0;var e=window.requestAnimationFrame||function(e){setTimeout(e,16)};e(function(){setTimeout(function(){window.CustomElements.readyTime=Date.now(),window.HTMLImports&&(window.CustomElements.elapsed=window.CustomElements.readyTime-window.HTMLImports.readyTime),document.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0}))})})}var n=e.useNative,r=e.initializeModules;e.isIE;if(n){var o=function(){};e.watchShadow=o,e.upgrade=o,e.upgradeAll=o,e.upgradeDocumentTree=o,e.upgradeSubtree=o,e.takeRecords=o,e["instanceof"]=function(e,t){return e instanceof t}}else r();var i=e.upgradeDocumentTree,a=e.upgradeDocument;if(window.wrap||(window.ShadowDOMPolyfill?(window.wrap=window.ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=window.ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}),window.HTMLImports&&(window.HTMLImports.__importsParsingHook=function(e){e["import"]&&a(wrap(e["import"]))}),"complete"===document.readyState||e.flags.eager)t();else if("interactive"!==document.readyState||window.attachEvent||window.HTMLImports&&!window.HTMLImports.ready){var s=window.HTMLImports&&!window.HTMLImports.ready?"HTMLImportsLoaded":"DOMContentLoaded";window.addEventListener(s,t)}else t()}(window.CustomElements),function(e){Function.prototype.bind||(Function.prototype.bind=function(e){var t=this,n=Array.prototype.slice.call(arguments,1);return function(){var r=n.slice();return r.push.apply(r,arguments),t.apply(e,r)}})}(window.WebComponents),function(e){var t=document.createElement("style");t.textContent="body {transition: opacity ease-in 0.2s; } \nbody[unresolved] {opacity: 0; display: block; overflow: hidden; position: relative; } \n";var n=document.querySelector("head");n.insertBefore(t,n.firstChild)}(window.WebComponents),function(e){window.Platform=e}(window.WebComponents); \ No newline at end of file From 257a6ab0276621278f69bee62de4272cc014ef8e Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Thu, 25 Aug 2016 18:09:50 +0200 Subject: [PATCH 026/125] notifications: use rhodecode-toast for notifications instead of toastr jquery plugin --- .hgignore | 3 + Gruntfile.js | 23 +- rhodecode/public/css/main.less | 6 +- rhodecode/public/css/polymer.less | 33 ++ rhodecode/public/css/toastr.less | 268 ----------- .../js/src/components/rhodecode-toast.html | 79 ++++ .../src/components/rhodecode-unsafe-html.html | 22 + .../js/src/components/shared-components.html | 3 + .../src/components/shared-styles-prefix.html | 3 + .../src/components/shared-styles-suffix.html | 3 + rhodecode/public/js/src/plugins/toastr.js | 435 ------------------ .../js/src/rhodecode/utils/notifications.js | 24 +- .../my_account/my_account_notifications.html | 4 +- rhodecode/templates/base/root.html | 1 + 14 files changed, 177 insertions(+), 730 deletions(-) create mode 100644 rhodecode/public/css/polymer.less delete mode 100644 rhodecode/public/css/toastr.less create mode 100644 rhodecode/public/js/src/components/rhodecode-toast.html create mode 100644 rhodecode/public/js/src/components/rhodecode-unsafe-html.html create mode 100644 rhodecode/public/js/src/components/shared-styles-prefix.html create mode 100644 rhodecode/public/js/src/components/shared-styles-suffix.html delete mode 100644 rhodecode/public/js/src/plugins/toastr.js diff --git a/.hgignore b/.hgignore index ab14d10b..13e7bc0a 100644 --- a/.hgignore +++ b/.hgignore @@ -39,7 +39,10 @@ syntax: regexp ^rcextensions/ ^result$ ^rhodecode/public/css/style.css$ +^rhodecode/public/css/style-polymer.css$ ^rhodecode/public/js/scripts.js$ +^rhodecode/public/js/rhodecode-components.html$ +^rhodecode/public/js/src/components/shared-styles.html$ ^rhodecode\.db$ ^rhodecode\.log$ ^rhodecode_dev\.log$ diff --git a/Gruntfile.js b/Gruntfile.js index 05513d34..c22a58c7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -17,6 +17,16 @@ module.exports = function(grunt) { }, }, concat: { + polymercss:{ + src: [ + // Base libraries + '<%= dirs.js.src %>/components/shared-styles-prefix.html', + '<%= dirs.css %>/style-polymer.css', + '<%= dirs.js.src %>/components/shared-styles-suffix.html' + ], + dest: '<%= dirs.js.dest %>/src/components/shared-styles.html', + nonull: true + }, dist: { src: [ // Base libraries @@ -39,7 +49,6 @@ module.exports = function(grunt) { '<%= dirs.js.src %>/plugins/jquery.mark.js', '<%= dirs.js.src %>/plugins/jquery.timeago.js', '<%= dirs.js.src %>/plugins/jquery.timeago-extension.js', - '<%= dirs.js.src %>/plugins/toastr.js', // Select2 '<%= dirs.js.src %>/select2/select2.js', @@ -106,7 +115,8 @@ module.exports = function(grunt) { optimization: 0 }, files: { - "<%= dirs.css %>/style.css": "<%= dirs.css %>/main.less" + "<%= dirs.css %>/style.css": "<%= dirs.css %>/main.less", + "<%= dirs.css %>/style-polymer.css": "<%= dirs.css %>/polymer.less" } }, production: { @@ -116,7 +126,8 @@ module.exports = function(grunt) { optimization: 2 }, files: { - "<%= dirs.css %>/style.css": "<%= dirs.css %>/main.less" + "<%= dirs.css %>/style.css": "<%= dirs.css %>/main.less", + "<%= dirs.css %>/style-polymer.css": "<%= dirs.css %>/polymer.less" } } }, @@ -124,11 +135,11 @@ module.exports = function(grunt) { watch: { less: { files: ["<%= dirs.css %>/*.less"], - tasks: ["less:production"] + tasks: ["less:development", 'concat:polymercss', "vulcanize"] }, js: { files: ["<%= dirs.js.src %>/**/*.js", "<%= dirs.js.src %>/components/*.*"], - tasks: ["concat:dist"] + tasks: ["vulcanize", "concat:dist"] } }, @@ -163,5 +174,5 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-crisper'); grunt.loadNpmTasks('grunt-contrib-copy'); - grunt.registerTask('default', ['copy','vulcanize', 'less:production', 'concat:dist']); + grunt.registerTask('default', ['less:production', 'concat:polymercss', 'copy','vulcanize', 'concat:dist']); }; diff --git a/rhodecode/public/css/main.less b/rhodecode/public/css/main.less index 7129599a..05141243 100644 --- a/rhodecode/public/css/main.less +++ b/rhodecode/public/css/main.less @@ -25,7 +25,6 @@ @import 'comments'; @import 'panels-bootstrap'; @import 'panels'; -@import 'toastr'; @import 'deform'; @@ -2103,3 +2102,8 @@ input[type=radio] { padding: 0; border: none; } + +.toggle-ajax-spinner{ + height: 16px; + width: 16px; +} diff --git a/rhodecode/public/css/polymer.less b/rhodecode/public/css/polymer.less new file mode 100644 index 00000000..795a2de9 --- /dev/null +++ b/rhodecode/public/css/polymer.less @@ -0,0 +1,33 @@ +//Primary CSS +//--- IMPORTS ------------------// +@import 'helpers'; +@import 'mixins'; +@import 'rcicons'; +@import 'fonts'; +@import 'variables'; +@import 'legacy_code_styles'; +@import 'type'; +@import 'alerts'; +@import 'buttons'; +@import 'tags'; +@import 'examples'; +@import 'login'; +@import 'comments'; + + +.toast-level { + display: inline-block; + min-width: 100px; + font-weight: bold; + text-transform: uppercase; + &.info, &.success { + color: #0ac878; + } + &.error, &.danger { + color: #e85e4d; + } + &.warning { + color: #ffc854; + } +} + diff --git a/rhodecode/public/css/toastr.less b/rhodecode/public/css/toastr.less deleted file mode 100644 index c6480a74..00000000 --- a/rhodecode/public/css/toastr.less +++ /dev/null @@ -1,268 +0,0 @@ -// Mix-ins -.borderRadius(@radius) { - -moz-border-radius: @radius; - -webkit-border-radius: @radius; - border-radius: @radius; -} - -.boxShadow(@boxShadow) { - -moz-box-shadow: @boxShadow; - -webkit-box-shadow: @boxShadow; - box-shadow: @boxShadow; -} - -.opacity(@opacity) { - @opacityPercent: @opacity * 100; - opacity: @opacity; - -ms-filter: ~"progid:DXImageTransform.Microsoft.Alpha(Opacity=@{opacityPercent})"; - filter: ~"alpha(opacity=@{opacityPercent})"; -} - -.wordWrap(@wordWrap: break-word) { - -ms-word-wrap: @wordWrap; - word-wrap: @wordWrap; -} - -// Variables -@black: #000000; -@grey: #999999; -@light-grey: #CCCCCC; -@white: #FFFFFF; -@near-black: #030303; -@green: #51A351; -@red: #BD362F; -@blue: #2F96B4; -@orange: #F89406; -@default-container-opacity: .8; - -// Styles -.toast-title { - font-weight: bold; -} - -.toast-message { - .wordWrap(); - - a, - label { - color: @near-black; - } - - a:hover { - color: @light-grey; - text-decoration: none; - } -} - -.toast-close-button { - position: relative; - right: -0.3em; - top: -0.3em; - float: right; - font-size: 20px; - font-weight: bold; - color: @black; - -webkit-text-shadow: 0 1px 0 rgba(255,255,255,1); - text-shadow: 0 1px 0 rgba(255,255,255,1); - .opacity(0.8); - - &:hover, - &:focus { - color: @black; - text-decoration: none; - cursor: pointer; - .opacity(0.4); - } -} - -/*Additional properties for button version - iOS requires the button element instead of an anchor tag. - If you want the anchor version, it requires `href="#"`.*/ -button.toast-close-button { - padding: 0; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; -} - -//#endregion - -.toast-top-center { - top: 0; - right: 0; - width: 100%; -} - -.toast-bottom-center { - bottom: 0; - right: 0; - width: 100%; -} - -.toast-top-full-width { - top: 0; - right: 0; - width: 100%; -} - -.toast-bottom-full-width { - bottom: 0; - right: 0; - width: 100%; -} - -.toast-top-left { - top: 12px; - left: 12px; -} - -.toast-top-right { - top: 12px; - right: 12px; -} - -.toast-bottom-right { - right: 12px; - bottom: 12px; -} - -.toast-bottom-left { - bottom: 12px; - left: 12px; -} - -#toast-container { - position: fixed; - z-index: 999999; - // The container should not be clickable. - pointer-events: none; - * { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - } - - > div { - position: relative; - // The toast itself should be clickable. - pointer-events: auto; - overflow: hidden; - margin: 0 0 6px; - padding: 15px; - width: 300px; - .borderRadius(1px 1px 1px 1px); - background-position: 15px center; - background-repeat: no-repeat; - color: @near-black; - .opacity(@default-container-opacity); - } - - > :hover { - .opacity(1); - cursor: pointer; - } - - > .toast-info { - //background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important; - } - - > .toast-error { - //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; - } - - > .toast-success { - //background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important; - } - - > .toast-warning { - //background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important; - } - - /*overrides*/ - &.toast-top-center > div, - &.toast-bottom-center > div { - width: 400px; - margin-left: auto; - margin-right: auto; - } - - &.toast-top-full-width > div, - &.toast-bottom-full-width > div { - width: 96%; - margin-left: auto; - margin-right: auto; - } -} - -.toast { - border-color: @near-black; - border-style: solid; - border-width: 2px 2px 2px 25px; - background-color: @white; -} - -.toast-success { - border-color: @green; -} - -.toast-error { - border-color: @red; -} - -.toast-info { - border-color: @blue; -} - -.toast-warning { - border-color: @orange; -} - -.toast-progress { - position: absolute; - left: 0; - bottom: 0; - height: 4px; - background-color: @black; - .opacity(0.4); -} - -/*Responsive Design*/ - -@media all and (max-width: 240px) { - #toast-container { - - > div { - padding: 8px; - width: 11em; - } - - & .toast-close-button { - right: -0.2em; - top: -0.2em; - } - } -} - -@media all and (min-width: 241px) and (max-width: 480px) { - #toast-container { - > div { - padding: 8px; - width: 18em; - } - - & .toast-close-button { - right: -0.2em; - top: -0.2em; - } - } -} - -@media all and (min-width: 481px) and (max-width: 768px) { - #toast-container { - > div { - padding: 15px; - width: 25em; - } - } -} diff --git a/rhodecode/public/js/src/components/rhodecode-toast.html b/rhodecode/public/js/src/components/rhodecode-toast.html new file mode 100644 index 00000000..c0f42f54 --- /dev/null +++ b/rhodecode/public/js/src/components/rhodecode-toast.html @@ -0,0 +1,79 @@ + + + + + + + diff --git a/rhodecode/public/js/src/components/rhodecode-unsafe-html.html b/rhodecode/public/js/src/components/rhodecode-unsafe-html.html new file mode 100644 index 00000000..825b3152 --- /dev/null +++ b/rhodecode/public/js/src/components/rhodecode-unsafe-html.html @@ -0,0 +1,22 @@ + + + + + + diff --git a/rhodecode/public/js/src/components/shared-components.html b/rhodecode/public/js/src/components/shared-components.html index f76fdf9b..dc2a87d7 100644 --- a/rhodecode/public/js/src/components/shared-components.html +++ b/rhodecode/public/js/src/components/shared-components.html @@ -4,3 +4,6 @@ + + + diff --git a/rhodecode/public/js/src/components/shared-styles-prefix.html b/rhodecode/public/js/src/components/shared-styles-prefix.html new file mode 100644 index 00000000..1c2dfb57 --- /dev/null +++ b/rhodecode/public/js/src/components/shared-styles-prefix.html @@ -0,0 +1,3 @@ + + + diff --git a/rhodecode/public/js/src/plugins/toastr.js b/rhodecode/public/js/src/plugins/toastr.js deleted file mode 100644 index d2581b85..00000000 --- a/rhodecode/public/js/src/plugins/toastr.js +++ /dev/null @@ -1,435 +0,0 @@ -/* - * Toastr - * Copyright 2012-2015 - * Authors: John Papa, Hans Fjällemark, and Tim Ferrell. - * All Rights Reserved. - * Use, reproduction, distribution, and modification of this code is subject to the terms and - * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php - * - * ARIA Support: Greta Krafsig - * - * Project: https://github.com/CodeSeven/toastr - */ -/* global define */ -(function (define) { - define(['jquery'], function ($) { - return (function () { - var $container; - var listener; - var toastId = 0; - var toastType = { - error: 'error', - info: 'info', - success: 'success', - warning: 'warning' - }; - - var toastr = { - clear: clear, - remove: remove, - error: error, - getContainer: getContainer, - info: info, - options: {}, - subscribe: subscribe, - success: success, - version: '2.1.2', - warning: warning - }; - - var previousToast; - - return toastr; - - //////////////// - - function error(message, title, optionsOverride) { - return notify({ - type: toastType.error, - iconClass: getOptions().iconClasses.error, - message: message, - optionsOverride: optionsOverride, - title: title - }); - } - - function getContainer(options, create) { - if (!options) { options = getOptions(); } - $container = $('#' + options.containerId); - if ($container.length) { - return $container; - } - if (create) { - $container = createContainer(options); - } - return $container; - } - - function info(message, title, optionsOverride) { - return notify({ - type: toastType.info, - iconClass: getOptions().iconClasses.info, - message: message, - optionsOverride: optionsOverride, - title: title - }); - } - - function subscribe(callback) { - listener = callback; - } - - function success(message, title, optionsOverride) { - return notify({ - type: toastType.success, - iconClass: getOptions().iconClasses.success, - message: message, - optionsOverride: optionsOverride, - title: title - }); - } - - function warning(message, title, optionsOverride) { - return notify({ - type: toastType.warning, - iconClass: getOptions().iconClasses.warning, - message: message, - optionsOverride: optionsOverride, - title: title - }); - } - - function clear($toastElement, clearOptions) { - var options = getOptions(); - if (!$container) { getContainer(options); } - if (!clearToast($toastElement, options, clearOptions)) { - clearContainer(options); - } - } - - function remove($toastElement) { - var options = getOptions(); - if (!$container) { getContainer(options); } - if ($toastElement && $(':focus', $toastElement).length === 0) { - removeToast($toastElement); - return; - } - if ($container.children().length) { - $container.remove(); - } - } - - // internal functions - - function clearContainer (options) { - var toastsToClear = $container.children(); - for (var i = toastsToClear.length - 1; i >= 0; i--) { - clearToast($(toastsToClear[i]), options); - } - } - - function clearToast ($toastElement, options, clearOptions) { - var force = clearOptions && clearOptions.force ? clearOptions.force : false; - if ($toastElement && (force || $(':focus', $toastElement).length === 0)) { - $toastElement[options.hideMethod]({ - duration: options.hideDuration, - easing: options.hideEasing, - complete: function () { removeToast($toastElement); } - }); - return true; - } - return false; - } - - function createContainer(options) { - $container = $('
') - .attr('id', options.containerId) - .addClass(options.positionClass) - .attr('aria-live', 'polite') - .attr('role', 'alert'); - - $container.appendTo($(options.target)); - return $container; - } - - function getDefaults() { - return { - tapToDismiss: true, - toastClass: 'toast', - containerId: 'toast-container', - debug: false, - - showMethod: 'fadeIn', //fadeIn, slideDown, and show are built into jQuery - showDuration: 300, - showEasing: 'swing', //swing and linear are built into jQuery - onShown: undefined, - hideMethod: 'fadeOut', - hideDuration: 1000, - hideEasing: 'swing', - onHidden: undefined, - closeMethod: false, - closeDuration: false, - closeEasing: false, - - extendedTimeOut: 1000, - iconClasses: { - error: 'toast-error', - info: 'toast-info', - success: 'toast-success', - warning: 'toast-warning' - }, - iconClass: 'toast-info', - positionClass: 'toast-top-right', - timeOut: 5000, // Set timeOut and extendedTimeOut to 0 to make it sticky - titleClass: 'toast-title', - messageClass: 'toast-message', - escapeHtml: false, - target: 'body', - closeHtml: '', - newestOnTop: true, - preventDuplicates: false, - progressBar: false - }; - } - - function publish(args) { - if (!listener) { return; } - listener(args); - } - - function notify(map) { - var options = getOptions(); - var iconClass = map.iconClass || options.iconClass; - - if (typeof (map.optionsOverride) !== 'undefined') { - options = $.extend(options, map.optionsOverride); - iconClass = map.optionsOverride.iconClass || iconClass; - } - - if (shouldExit(options, map)) { return; } - - toastId++; - - $container = getContainer(options, true); - - var intervalId = null; - var $toastElement = $('
'); - var $titleElement = $('
'); - var $messageElement = $('
'); - var $progressElement = $('
'); - var $closeElement = $(options.closeHtml); - var progressBar = { - intervalId: null, - hideEta: null, - maxHideTime: null - }; - var response = { - toastId: toastId, - state: 'visible', - startTime: new Date(), - options: options, - map: map - }; - - personalizeToast(); - - displayToast(); - - handleEvents(); - - publish(response); - - if (options.debug && console) { - console.log(response); - } - - return $toastElement; - - function escapeHtml(source) { - if (source == null) - source = ""; - - return new String(source) - .replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(//g, '>'); - } - - function personalizeToast() { - setIcon(); - setTitle(); - setMessage(); - setCloseButton(); - setProgressBar(); - setSequence(); - } - - function handleEvents() { - $toastElement.hover(stickAround, delayedHideToast); - if (!options.onclick && options.tapToDismiss) { - $toastElement.click(hideToast); - } - - if (options.closeButton && $closeElement) { - $closeElement.click(function (event) { - if (event.stopPropagation) { - event.stopPropagation(); - } else if (event.cancelBubble !== undefined && event.cancelBubble !== true) { - event.cancelBubble = true; - } - hideToast(true); - }); - } - - if (options.onclick) { - $toastElement.click(function (event) { - options.onclick(event); - hideToast(); - }); - } - } - - function displayToast() { - $toastElement.hide(); - - $toastElement[options.showMethod]( - {duration: options.showDuration, easing: options.showEasing, complete: options.onShown} - ); - - if (options.timeOut > 0) { - intervalId = setTimeout(hideToast, options.timeOut); - progressBar.maxHideTime = parseFloat(options.timeOut); - progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime; - if (options.progressBar) { - progressBar.intervalId = setInterval(updateProgress, 10); - } - } - } - - function setIcon() { - if (map.iconClass) { - $toastElement.addClass(options.toastClass).addClass(iconClass); - } - } - - function setSequence() { - if (options.newestOnTop) { - $container.prepend($toastElement); - } else { - $container.append($toastElement); - } - } - - function setTitle() { - if (map.title) { - $titleElement.append(!options.escapeHtml ? map.title : escapeHtml(map.title)).addClass(options.titleClass); - $toastElement.append($titleElement); - } - } - - function setMessage() { - if (map.message) { - $messageElement.append(!options.escapeHtml ? map.message : escapeHtml(map.message)).addClass(options.messageClass); - $toastElement.append($messageElement); - } - } - - function setCloseButton() { - if (options.closeButton) { - $closeElement.addClass('toast-close-button').attr('role', 'button'); - $toastElement.prepend($closeElement); - } - } - - function setProgressBar() { - if (options.progressBar) { - $progressElement.addClass('toast-progress'); - $toastElement.prepend($progressElement); - } - } - - function shouldExit(options, map) { - if (options.preventDuplicates) { - if (map.message === previousToast) { - return true; - } else { - previousToast = map.message; - } - } - return false; - } - - function hideToast(override) { - var method = override && options.closeMethod !== false ? options.closeMethod : options.hideMethod; - var duration = override && options.closeDuration !== false ? - options.closeDuration : options.hideDuration; - var easing = override && options.closeEasing !== false ? options.closeEasing : options.hideEasing; - if ($(':focus', $toastElement).length && !override) { - return; - } - clearTimeout(progressBar.intervalId); - return $toastElement[method]({ - duration: duration, - easing: easing, - complete: function () { - removeToast($toastElement); - if (options.onHidden && response.state !== 'hidden') { - options.onHidden(); - } - response.state = 'hidden'; - response.endTime = new Date(); - publish(response); - } - }); - } - - function delayedHideToast() { - if (options.timeOut > 0 || options.extendedTimeOut > 0) { - intervalId = setTimeout(hideToast, options.extendedTimeOut); - progressBar.maxHideTime = parseFloat(options.extendedTimeOut); - progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime; - } - } - - function stickAround() { - clearTimeout(intervalId); - progressBar.hideEta = 0; - $toastElement.stop(true, true)[options.showMethod]( - {duration: options.showDuration, easing: options.showEasing} - ); - } - - function updateProgress() { - var percentage = ((progressBar.hideEta - (new Date().getTime())) / progressBar.maxHideTime) * 100; - $progressElement.width(percentage + '%'); - } - } - - function getOptions() { - return $.extend({}, getDefaults(), toastr.options); - } - - function removeToast($toastElement) { - if (!$container) { $container = getContainer(); } - if ($toastElement.is(':visible')) { - return; - } - $toastElement.remove(); - $toastElement = null; - if ($container.children().length === 0) { - $container.remove(); - previousToast = undefined; - } - } - - })(); - }); -}(typeof define === 'function' && define.amd ? define : function (deps, factory) { - if (typeof module !== 'undefined' && module.exports) { //Node - module.exports = factory(require('jquery')); - } else { - window.toastr = factory(window.jQuery); - } -})); diff --git a/rhodecode/public/js/src/rhodecode/utils/notifications.js b/rhodecode/public/js/src/rhodecode/utils/notifications.js index 086b4d23..5bc334ed 100644 --- a/rhodecode/public/js/src/rhodecode/utils/notifications.js +++ b/rhodecode/public/js/src/rhodecode/utils/notifications.js @@ -1,29 +1,17 @@ "use strict"; -toastr.options = { - "closeButton": true, - "debug": false, - "newestOnTop": false, - "progressBar": false, - "positionClass": "toast-top-center", - "preventDuplicates": false, - "onclick": null, - "showDuration": "300", - "hideDuration": "300", - "timeOut": "0", - "extendedTimeOut": "0", - "showEasing": "swing", - "hideEasing": "linear", - "showMethod": "fadeIn", - "hideMethod": "fadeOut" -}; function notifySystem(data) { var notification = new Notification(data.message.level + ': ' + data.message.message); }; function notifyToaster(data){ - toastr[data.message.level](data.message.message); + var notifications = document.getElementById('notifications'); + notifications.push('toasts', + { level: data.message.level, + message: data.message.message + }); + notifications.open(); } function handleNotifications(data) { diff --git a/rhodecode/templates/admin/my_account/my_account_notifications.html b/rhodecode/templates/admin/my_account/my_account_notifications.html index 478be17b..bf6f2f0a 100644 --- a/rhodecode/templates/admin/my_account/my_account_notifications.html +++ b/rhodecode/templates/admin/my_account/my_account_notifications.html @@ -1,5 +1,5 @@ diff --git a/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.less b/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.less index 77348a45..4b02daea 100644 --- a/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.less +++ b/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.less @@ -1,3 +1,5 @@ +@import '../../../../css/variables'; + paper-toast{ width: 100%; min-width: 400px; @@ -27,13 +29,16 @@ paper-toast a{ min-width: 100px; font-weight: bold; text-transform: uppercase; - &.info, &.success { - color: #0ac878; + &.info{ + color: @alert4; + } + &.success { + color: @alert1; } &.error, &.danger { - color: #e85e4d; + color: @alert2; } &.warning { - color: #ffc854; + color: @alert3; } } diff --git a/rhodecode/templates/admin/my_account/my_account_notifications.html b/rhodecode/templates/admin/my_account/my_account_notifications.html index bf6f2f0a..5ce2659d 100644 --- a/rhodecode/templates/admin/my_account/my_account_notifications.html +++ b/rhodecode/templates/admin/my_account/my_account_notifications.html @@ -36,7 +36,7 @@
From 7f5a61ea4fc7aef720176ac497fb0e3e8222dc56 Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Mon, 29 Aug 2016 10:44:47 +0200 Subject: [PATCH 032/125] notifications: override material design colors with our colors --- rhodecode/public/css/polymer.less | 7 +++++++ .../admin/my_account/my_account_notifications.html | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/rhodecode/public/css/polymer.less b/rhodecode/public/css/polymer.less index 38d32687..d7b5ff71 100644 --- a/rhodecode/public/css/polymer.less +++ b/rhodecode/public/css/polymer.less @@ -7,3 +7,10 @@ @import 'variables'; @import 'type'; @import 'buttons'; + +:root { + --primary-color: @rcblue; + --light-primary-color: @rclightblue; + --dark-primary-color: @rcdarkblue; + --primary-text-color: @grey2; +} diff --git a/rhodecode/templates/admin/my_account/my_account_notifications.html b/rhodecode/templates/admin/my_account/my_account_notifications.html index 5ce2659d..d0974230 100644 --- a/rhodecode/templates/admin/my_account/my_account_notifications.html +++ b/rhodecode/templates/admin/my_account/my_account_notifications.html @@ -1,5 +1,5 @@ diff --git a/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.js b/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.js index 7455ece0..c9e8ed4d 100644 --- a/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.js +++ b/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.js @@ -20,6 +20,8 @@ Polymer({ }, dismissNotifications: function(){ this.$['p-toast'].close(); + }, + handleClosed: function(){ this.splice('toasts', 0); }, open: function(){ From 7a7102fedde96cfb33bbd51b0bd49618fa353b3f Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Fri, 9 Sep 2016 16:21:58 +0200 Subject: [PATCH 106/125] connection: remove unused code --- .../js/src/rhodecode/connection_controller.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/rhodecode/public/js/src/rhodecode/connection_controller.js b/rhodecode/public/js/src/rhodecode/connection_controller.js index f212f980..d5a5afbb 100644 --- a/rhodecode/public/js/src/rhodecode/connection_controller.js +++ b/rhodecode/public/js/src/rhodecode/connection_controller.js @@ -26,13 +26,6 @@ var registerViewChannels; channelsInfo: {}, urls: urls }; - this.channelNameParsers = []; - - this.addChannelNameParser = function (fn) { - if (this.channelNameParsers.indexOf(fn) === -1) { - this.channelNameParsers.push(fn); - } - }; this.listen = function () { if (window.WebSocket) { @@ -178,16 +171,6 @@ var registerViewChannels; {channel: key, state: this.state.channelsInfo[key]}); } } - /** - * checks current channel list in state and if channel is not present - * converts them into executable "commands" and pushes them on topics - */ - for (var i = 0; i < this.state.channels.length; i++) { - var channel = this.state.channels[i]; - for (var j = 0; j < this.channelNameParsers.length; j++) { - this.channelNameParsers[j](channel); - } - } }; this.run = function () { From a045ce12621faecbd6efbb088e2aaea3cb76b53a Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Fri, 9 Sep 2016 17:20:22 +0200 Subject: [PATCH 107/125] helpers: added route_path_or_none helper for routes that might be added by plugins --- rhodecode/lib/helpers.py | 7 +++++++ rhodecode/templates/admin/my_account/my_account.html | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py index e7984eb9..e96c3caf 100644 --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -1946,6 +1946,13 @@ def route_path(*args, **kwds): return req.route_path(*args, **kwds) +def route_path_or_none(*args, **kwargs): + try: + return route_path(*args, **kwargs) + except KeyError: + return None + + def static_url(*args, **kwds): """ Wrapper around pyramids `route_path` function. It is used to generate diff --git a/rhodecode/templates/admin/my_account/my_account.html b/rhodecode/templates/admin/my_account/my_account.html index 53e027e0..02a8e0b2 100644 --- a/rhodecode/templates/admin/my_account/my_account.html +++ b/rhodecode/templates/admin/my_account/my_account.html @@ -30,10 +30,10 @@
  • ${_('Password')}
  • ${_('Auth Tokens')}
  • ## TODO: Find a better integration of oauth views into navigation. - %try: -
  • ${_('OAuth Identities')}
  • - %except KeyError: - %endtry + <% my_account_oauth_url = h.route_path_or_none('my_account_oauth') %> + % if my_account_oauth_url: +
  • ${_('OAuth Identities')}
  • + % endif
  • ${_('My Emails')}
  • ${_('My Repositories')}
  • ${_('Watched')}
  • From 2a46a56fff528ad968b9295058a9f95a482bdfd9 Mon Sep 17 00:00:00 2001 From: lisaq Date: Mon, 12 Sep 2016 11:56:55 +0200 Subject: [PATCH 108/125] styling: fixing alignment of comment toggle button --- rhodecode/public/css/code-block.less | 2 +- rhodecode/public/css/diff.less | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/rhodecode/public/css/code-block.less b/rhodecode/public/css/code-block.less index 73145bee..7b6400cd 100644 --- a/rhodecode/public/css/code-block.less +++ b/rhodecode/public/css/code-block.less @@ -255,7 +255,7 @@ table.code-difftable { /** LINE NUMBERS **/ .lineno { - padding-left: 2px; + padding-left: 2px !important; padding-right: 2px; text-align: right; width: 32px; diff --git a/rhodecode/public/css/diff.less b/rhodecode/public/css/diff.less index c1ac9b42..6adb1752 100644 --- a/rhodecode/public/css/diff.less +++ b/rhodecode/public/css/diff.less @@ -32,10 +32,14 @@ div.diffblock.margined.comm tr { } .comment-toggle { - width: 20px; + position: relative; color: @rcblue; - .icon-comment{ - display: inline-block; + + .icon-comment { + position: absolute; + top: 2px; + left: 0; + z-index: 100; visibility: hidden; } From b6c110dc52eb21b053429bf05ad8ff1a13d889e3 Mon Sep 17 00:00:00 2001 From: Daniel Dourvaris Date: Wed, 17 Aug 2016 20:16:09 +0300 Subject: [PATCH 109/125] events: send pushed commit ids in order --- rhodecode/events/repo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rhodecode/events/repo.py b/rhodecode/events/repo.py index 877925e1..a7072c57 100644 --- a/rhodecode/events/repo.py +++ b/rhodecode/events/repo.py @@ -47,7 +47,7 @@ def _commits_as_dict(commit_ids, repos): if not commit_ids: return [] - needed_commits = set(commit_ids) + needed_commits = list(commit_ids) commits = [] reviewers = [] @@ -57,7 +57,7 @@ def _commits_as_dict(commit_ids, repos): vcs_repo = repo.scm_instance(cache=False) try: - for commit_id in list(needed_commits): + for commit_id in needed_commits: try: cs = vcs_repo.get_changeset(commit_id) except CommitDoesNotExistError: @@ -78,7 +78,7 @@ def _commits_as_dict(commit_ids, repos): repo.repo_name) commits.append(cs_data) - needed_commits.discard(commit_id) + needed_commits.remove(commit_id) except Exception as e: log.exception(e) From fd1d44b493960add52fb57592a4764887586ad22 Mon Sep 17 00:00:00 2001 From: lisaq Date: Mon, 12 Sep 2016 16:33:48 +0200 Subject: [PATCH 110/125] styling: fixing width on comment toggle button td --- rhodecode/public/css/diff.less | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rhodecode/public/css/diff.less b/rhodecode/public/css/diff.less index 6adb1752..71dea11e 100644 --- a/rhodecode/public/css/diff.less +++ b/rhodecode/public/css/diff.less @@ -33,6 +33,8 @@ div.diffblock.margined.comm tr { .comment-toggle { position: relative; + min-width: 20px; + width: 20px; color: @rcblue; .icon-comment { From 091418c182d0834ba40433e36a389df547d83418 Mon Sep 17 00:00:00 2001 From: Daniel Dourvaris Date: Mon, 12 Sep 2016 06:56:30 +0300 Subject: [PATCH 111/125] vcs: change way refs are retrieved for git so same name branch/tags and remotes can be supported, fixes #298 --- rhodecode/lib/vcs/backends/git/inmemory.py | 2 +- rhodecode/lib/vcs/backends/git/repository.py | 89 +++++++++++--------- rhodecode/tests/vcs/test_git.py | 22 ++++- 3 files changed, 69 insertions(+), 44 deletions(-) diff --git a/rhodecode/lib/vcs/backends/git/inmemory.py b/rhodecode/lib/vcs/backends/git/inmemory.py index 699e7b0e..435fa12e 100644 --- a/rhodecode/lib/vcs/backends/git/inmemory.py +++ b/rhodecode/lib/vcs/backends/git/inmemory.py @@ -98,7 +98,7 @@ class GitInMemoryCommit(base.BaseInMemoryCommit): self.repository._rebuild_cache(self.repository.commit_ids) # invalidate parsed refs after commit - self.repository._parsed_refs = self.repository._get_parsed_refs() + self.repository._refs = self.repository._get_refs() self.repository.branches = self.repository._get_branches() tip = self.repository.get_commit() self.reset() diff --git a/rhodecode/lib/vcs/backends/git/repository.py b/rhodecode/lib/vcs/backends/git/repository.py index ded3ea3a..7e4d4263 100644 --- a/rhodecode/lib/vcs/backends/git/repository.py +++ b/rhodecode/lib/vcs/backends/git/repository.py @@ -205,12 +205,6 @@ class GitRepository(BaseRepository): return [] return output.splitlines() - def _get_all_commit_ids2(self): - # alternate implementation - includes = [x[1][0] for x in self._parsed_refs.iteritems() - if x[1][1] != 'T'] - return [c.commit.id for c in self._remote.get_walker(include=includes)] - def _get_commit_id(self, commit_id_or_idx): def is_null(value): return len(value) == commit_id_or_idx.count('0') @@ -232,17 +226,23 @@ class GitRepository(BaseRepository): raise CommitDoesNotExistError(msg) elif is_bstr: - # get by branch/tag name - ref_id = self._parsed_refs.get(commit_id_or_idx) - if ref_id: # and ref_id[1] in ['H', 'RH', 'T']: - return ref_id[0] + # check full path ref, eg. refs/heads/master + ref_id = self._refs.get(commit_id_or_idx) + if ref_id: + return ref_id - tag_ids = self.tags.values() - # maybe it's a tag ? we don't have them in self.commit_ids - if commit_id_or_idx in tag_ids: - return commit_id_or_idx + # check branch name + branch_ids = self.branches.values() + ref_id = self._refs.get('refs/heads/%s' % commit_id_or_idx) + if ref_id: + return ref_id - elif (not SHA_PATTERN.match(commit_id_or_idx) or + # check tag name + ref_id = self._refs.get('refs/tags/%s' % commit_id_or_idx) + if ref_id: + return ref_id + + if (not SHA_PATTERN.match(commit_id_or_idx) or commit_id_or_idx not in self.commit_ids): msg = "Commit %s does not exist for %s" % ( commit_id_or_idx, self) @@ -289,20 +289,25 @@ class GitRepository(BaseRepository): description = self._remote.get_description() return safe_unicode(description or self.DEFAULT_DESCRIPTION) - def _get_refs_entry(self, value, reverse): + def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True): if self.is_empty(): - return {} + return OrderedDict() - def get_name(ctx): - return ctx[0] + result = [] + for ref, sha in self._refs.iteritems(): + if ref.startswith(prefix): + ref_name = ref + if strip_prefix: + ref_name = ref[len(prefix):] + result.append((safe_unicode(ref_name), sha)) - _branches = [ - (safe_unicode(x[0]), x[1][0]) - for x in self._parsed_refs.iteritems() if x[1][1] == value] - return OrderedDict(sorted(_branches, key=get_name, reverse=reverse)) + def get_name(entry): + return entry[0] + + return OrderedDict(sorted(result, key=get_name, reverse=reverse)) def _get_branches(self): - return self._get_refs_entry('H', False) + return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True) @LazyProperty def branches(self): @@ -324,10 +329,12 @@ class GitRepository(BaseRepository): return self._get_tags() def _get_tags(self): - return self._get_refs_entry('T', True) + return self._get_refs_entries( + prefix='refs/tags/', strip_prefix=True, reverse=True) def tag(self, name, user, commit_id=None, message=None, date=None, **kwargs): + # TODO: fix this method to apply annotated tags correct with message """ Creates and returns a tag for the given ``commit_id``. @@ -346,7 +353,7 @@ class GitRepository(BaseRepository): name, commit.raw_id) self._remote.set_refs('refs/tags/%s' % name, commit._commit['id']) - self._parsed_refs = self._get_parsed_refs() + self._refs = self._get_refs() self.tags = self._get_tags() return commit @@ -367,24 +374,28 @@ class GitRepository(BaseRepository): self._remote.get_refs_path(), 'refs', 'tags', name) try: os.remove(tagpath) - self._parsed_refs = self._get_parsed_refs() + self._refs = self._get_refs() self.tags = self._get_tags() except OSError as e: raise RepositoryError(e.strerror) - @LazyProperty - def _parsed_refs(self): - return self._get_parsed_refs() + def _get_refs(self): + return self._remote.get_refs() - def _get_parsed_refs(self): - # TODO: (oliver) who needs RH; branches? - # Remote Heads were commented out, as they may overwrite local branches - # See the TODO note in rhodecode.lib.vcs.remote.git:get_refs for more - # details. - keys = [('refs/heads/', 'H'), - #('refs/remotes/origin/', 'RH'), - ('refs/tags/', 'T')] - return self._remote.get_refs(keys=keys) + @LazyProperty + def _refs(self): + return self._get_refs() + + @property + def _ref_tree(self): + node = tree = {} + for ref, sha in self._refs.iteritems(): + path = ref.split('/') + for bit in path[:-1]: + node = node.setdefault(bit, {}) + node[path[-1]] = sha + node = tree + return tree def get_commit(self, commit_id=None, commit_idx=None, pre_load=None): """ diff --git a/rhodecode/tests/vcs/test_git.py b/rhodecode/tests/vcs/test_git.py index 2996f18a..f4283cc6 100644 --- a/rhodecode/tests/vcs/test_git.py +++ b/rhodecode/tests/vcs/test_git.py @@ -934,20 +934,34 @@ class TestGitCommit(object): 'vcs/nodes.py'] assert set(changed) == set([f.path for f in commit.changed]) - def test_unicode_refs(self): + def test_unicode_branch_refs(self): unicode_branches = { - 'unicode': ['6c0ce52b229aa978889e91b38777f800e85f330b', 'H'], - u'uniçö∂e': ['ürl', 'H'] + 'refs/heads/unicode': '6c0ce52b229aa978889e91b38777f800e85f330b', + u'refs/heads/uniçö∂e': 'ürl', } with mock.patch( ("rhodecode.lib.vcs.backends.git.repository" - ".GitRepository._parsed_refs"), + ".GitRepository._refs"), unicode_branches): branches = self.repo.branches assert 'unicode' in branches assert u'uniçö∂e' in branches + def test_unicode_tag_refs(self): + unicode_tags = { + 'refs/tags/unicode': '6c0ce52b229aa978889e91b38777f800e85f330b', + u'refs/tags/uniçö∂e': '6c0ce52b229aa978889e91b38777f800e85f330b', + } + with mock.patch( + ("rhodecode.lib.vcs.backends.git.repository" + ".GitRepository._refs"), + unicode_tags): + tags = self.repo.tags + + assert 'unicode' in tags + assert u'uniçö∂e' in tags + def test_commit_message_is_unicode(self): for commit in self.repo: assert type(commit.message) == unicode From ea0fdb0acd295b3d803a7002bcf1924efb0c5b67 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 12 Sep 2016 20:56:20 +0200 Subject: [PATCH 112/125] vcs middleware: Fix up TODO note Add more context to the existing TODO note. --- rhodecode/lib/middleware/vcs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rhodecode/lib/middleware/vcs.py b/rhodecode/lib/middleware/vcs.py index 9f143334..5ce5fd0b 100644 --- a/rhodecode/lib/middleware/vcs.py +++ b/rhodecode/lib/middleware/vcs.py @@ -181,7 +181,8 @@ class VCSMiddleware(object): repo_name, vcs_handler.basepath, vcs_handler.SCM): return HTTPNotFound()(environ, start_response) - # TODO(marcink): this is probably not needed anymore + # TODO: johbo: Needed for the Pyro4 backend and Mercurial only. + # Remove once we fully switched to the HTTP backend. environ['REPO_NAME'] = repo_name # register repo_name and it's config back to the handler From 6908a7915dc5026493ff6b23249d97864612a1e2 Mon Sep 17 00:00:00 2001 From: Daniel Dourvaris Date: Mon, 12 Sep 2016 06:44:51 +0300 Subject: [PATCH 113/125] ux: show multiple tags/branches in changelog/summary instead of truncating --- rhodecode/public/css/tables.less | 14 ++++++++------ rhodecode/templates/changelog/changelog.html | 4 ++-- .../changelog/changelog_summary_data.html | 6 +++--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/rhodecode/public/css/tables.less b/rhodecode/public/css/tables.less index 072b1909..4a794f26 100644 --- a/rhodecode/public/css/tables.less +++ b/rhodecode/public/css/tables.less @@ -117,7 +117,7 @@ table.dataTable { &.annotate{ padding-right: 0; - + div.annotatediv{ margin: 0 0.7em; } @@ -138,7 +138,7 @@ table.dataTable { &.td-journalaction { min-width: 300px; - .journal_action_params { + .journal_action_params { // waiting for feedback } } @@ -202,9 +202,11 @@ table.dataTable { &.td-tags { padding: .5em 1em .5em 0; + width: 140px; .tag { margin: 1px; + float: left; } } @@ -262,11 +264,11 @@ table.dataTable { width: 150px; height: 22px; overflow: hidden; - + .tag { display: inline-block; } - + &.truncate { height: 22px; max-height:2em; @@ -428,7 +430,7 @@ table.trending_language_tbl { } } -// Compare +// Compare table.compare_view_commits { margin-top: @space; @@ -486,7 +488,7 @@ table.compare_view_commits { td { padding-top: @space; } - + &:first-child td { padding-top: 0; } diff --git a/rhodecode/templates/changelog/changelog.html b/rhodecode/templates/changelog/changelog.html index 01d62afa..aeb6503b 100644 --- a/rhodecode/templates/changelog/changelog.html +++ b/rhodecode/templates/changelog/changelog.html @@ -168,8 +168,8 @@ ${self.gravatar_with_user(commit.author)}
    -
    +
    +
    ## branch %if commit.branch: diff --git a/rhodecode/templates/changelog/changelog_summary_data.html b/rhodecode/templates/changelog/changelog_summary_data.html index 9e9aa54c..a08f0ac9 100644 --- a/rhodecode/templates/changelog/changelog_summary_data.html +++ b/rhodecode/templates/changelog/changelog_summary_data.html @@ -55,8 +55,8 @@ ${base.gravatar_with_user(cs.author)}
    -
    +
    +
    %if h.is_hg(c.rhodecode_repo): %for book in cs.bookmarks: @@ -105,7 +105,7 @@ ${c.repo_commits.pager('$link_previous ~2~ $link_next')}
    %endif - + %if not h.is_svn(c.rhodecode_repo):
    ${_('Push new repo:')}
    From 73ba40da93e13798db9cb7293c181f9f2a3a500e Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Tue, 13 Sep 2016 12:25:42 +0200 Subject: [PATCH 114/125] frontend: introduce rhodecode-app for more complex cross element wiring --- grunt_config.json | 2 - rhodecode/channelstream/__init__.py | 4 +- .../channelstream-connection.html | 106 ++++ .../channelstream-connection.js | 502 ++++++++++++++++++ .../rhodecode-app/rhodecode-app.html | 15 + .../components/rhodecode-app/rhodecode-app.js | 127 +++++ .../rhodecode-toast/rhodecode-toast.js | 15 +- .../js/src/components/shared-components.html | 2 + .../js/src/rhodecode/connection_controller.js | 208 -------- .../js/src/rhodecode/utils/notifications.js | 47 -- rhodecode/public/js/topics_list.txt | 5 +- rhodecode/templates/base/root.html | 6 +- 12 files changed, 774 insertions(+), 265 deletions(-) create mode 100644 rhodecode/public/js/src/components/channelstream-connection/channelstream-connection.html create mode 100644 rhodecode/public/js/src/components/channelstream-connection/channelstream-connection.js create mode 100644 rhodecode/public/js/src/components/rhodecode-app/rhodecode-app.html create mode 100644 rhodecode/public/js/src/components/rhodecode-app/rhodecode-app.js delete mode 100644 rhodecode/public/js/src/rhodecode/connection_controller.js delete mode 100644 rhodecode/public/js/src/rhodecode/utils/notifications.js diff --git a/grunt_config.json b/grunt_config.json index 89b95e2a..13826f7b 100644 --- a/grunt_config.json +++ b/grunt_config.json @@ -66,7 +66,6 @@ "<%= dirs.js.src %>/rhodecode/utils/topics.js", "<%= dirs.js.src %>/rhodecode/widgets/multiselect.js", "<%= dirs.js.src %>/rhodecode/init.js", - "<%= dirs.js.src %>/rhodecode/connection_controller.js", "<%= dirs.js.src %>/rhodecode/codemirror.js", "<%= dirs.js.src %>/rhodecode/comments.js", "<%= dirs.js.src %>/rhodecode/constants.js", @@ -81,7 +80,6 @@ "<%= dirs.js.src %>/rhodecode/select2_widgets.js", "<%= dirs.js.src %>/rhodecode/tooltips.js", "<%= dirs.js.src %>/rhodecode/users.js", - "<%= dirs.js.src %>/rhodecode/utils/notifications.js", "<%= dirs.js.src %>/rhodecode/appenlight.js", "<%= dirs.js.src %>/rhodecode.js" ], diff --git a/rhodecode/channelstream/__init__.py b/rhodecode/channelstream/__init__.py index e08128bc..a7abefb0 100644 --- a/rhodecode/channelstream/__init__.py +++ b/rhodecode/channelstream/__init__.py @@ -29,7 +29,9 @@ from rhodecode.lib.ext_json import json def url_gen(request): urls = { 'connect': request.route_url('channelstream_connect'), - 'subscribe': request.route_url('channelstream_subscribe') + 'subscribe': request.route_url('channelstream_subscribe'), + 'longpoll': request.registry.settings.get('channelstream.longpoll_url', ''), + 'ws': request.registry.settings.get('channelstream.ws_url', '') } return json.dumps(urls) diff --git a/rhodecode/public/js/src/components/channelstream-connection/channelstream-connection.html b/rhodecode/public/js/src/components/channelstream-connection/channelstream-connection.html new file mode 100644 index 00000000..d7b43525 --- /dev/null +++ b/rhodecode/public/js/src/components/channelstream-connection/channelstream-connection.html @@ -0,0 +1,106 @@ + + + + + + + + diff --git a/rhodecode/public/js/src/components/channelstream-connection/channelstream-connection.js b/rhodecode/public/js/src/components/channelstream-connection/channelstream-connection.js new file mode 100644 index 00000000..76453e53 --- /dev/null +++ b/rhodecode/public/js/src/components/channelstream-connection/channelstream-connection.js @@ -0,0 +1,502 @@ +Polymer({ + is: 'channelstream-connection', + + /** + * Fired when `channels` array changes. + * + * @event channelstream-channels-changed + */ + + /** + * Fired when `connect()` method succeeds. + * + * @event channelstream-connected + */ + + /** + * Fired when `connect` fails. + * + * @event channelstream-connect-error + */ + + /** + * Fired when `disconnect()` succeeds. + * + * @event channelstream-disconnected + */ + + /** + * Fired when `message()` succeeds. + * + * @event channelstream-message-sent + */ + + /** + * Fired when `message()` fails. + * + * @event channelstream-message-error + */ + + /** + * Fired when `subscribe()` succeeds. + * + * @event channelstream-subscribed + */ + + /** + * Fired when `subscribe()` fails. + * + * @event channelstream-subscribe-error + */ + + /** + * Fired when `unsubscribe()` succeeds. + * + * @event channelstream-unsubscribed + */ + + /** + * Fired when `unsubscribe()` fails. + * + * @event channelstream-unsubscribe-error + */ + + /** + * Fired when listening connection receives a message. + * + * @event channelstream-listen-message + */ + + /** + * Fired when listening connection is opened. + * + * @event channelstream-listen-opened + */ + + /** + * Fired when listening connection is closed. + * + * @event channelstream-listen-closed + */ + + /** + * Fired when listening connection suffers an error. + * + * @event channelstream-listen-error + */ + + properties: { + isReady: Boolean, + /** List of channels user should be subscribed to. */ + channels: { + type: Array, + value: function () { + return [] + }, + notify: true + }, + /** Username of connecting user. */ + username: { + type: String, + value: 'Anonymous', + reflectToAttribute: true + }, + /** Connection identifier. */ + connectionId: { + type: String, + reflectToAttribute: true + }, + /** Websocket instance. */ + websocket: { + type: Object, + value: null + }, + /** Websocket connection url. */ + websocketUrl: { + type: String, + value: '' + }, + /** URL used in `connect()`. */ + connectUrl: { + type: String, + value: '' + }, + /** URL used in `disconnect()`. */ + disconnectUrl: { + type: String, + value: '' + }, + /** URL used in `subscribe()`. */ + subscribeUrl: { + type: String, + value: '' + }, + /** URL used in `unsubscribe()`. */ + unsubscribeUrl: { + type: String, + value: '' + }, + /** URL used in `message()`. */ + messageUrl: { + type: String, + value: '' + }, + /** Long-polling connection url. */ + longPollUrl: { + type: String, + value: '' + }, + /** Long-polling connection url. */ + shouldReconnect: { + type: Boolean, + value: true + }, + /** Should send heartbeats. */ + heartbeats: { + type: Boolean, + value: true + }, + /** How much should every retry interval increase (in milliseconds) */ + increaseBounceIv: { + type: Number, + value: 2000 + }, + _currentBounceIv: { + type: Number, + reflectToAttribute: true, + value: 0 + }, + /** Should use websockets or long-polling by default */ + useWebsocket: { + type: Boolean, + reflectToAttribute: true, + value: true + }, + connected: { + type: Boolean, + reflectToAttribute: true, + value: false + } + }, + + observers: [ + '_handleChannelsChange(channels.splices)' + ], + + listeners: { + 'channelstream-connected': 'startListening', + 'channelstream-connect-error': 'retryConnection', + }, + + /** + * Mutators hold functions that you can set locally to change the data + * that the client is sending to all endpoints + * you can call it like `elem.mutators('connect', yourFunc())` + * mutators will be executed in order they were pushed onto arrays + * + */ + mutators: { + connect: function () { + return [] + }(), + message: function () { + return [] + }(), + subscribe: function () { + return [] + }(), + unsubscribe: function () { + return [] + }(), + disconnect: function () { + return [] + }() + }, + ready: function () { + this.isReady = true; + }, + + /** + * Connects user and fetches connection id from the server. + * + */ + connect: function () { + var request = this.$['ajaxConnect']; + request.url = this.connectUrl; + request.body = { + username: this.username, + channels: this.channels + }; + for (var i = 0; i < this.mutators.connect.length; i++) { + this.mutators.connect[i](request); + } + request.generateRequest() + }, + /** + * Overwrite with custom function that will + */ + addMutator: function (type, func) { + this.mutators[type].push(func); + }, + /** + * Subscribes user to channels. + * + */ + subscribe: function (channels) { + var request = this.$['ajaxSubscribe']; + request.url = this.subscribeUrl; + request.body = { + channels: channels, + conn_id: this.connectionId + }; + for (var i = 0; i < this.mutators.subscribe.length; i++) { + this.mutators.subscribe[i](request); + } + if (request.body.channels.length) { + request.generateRequest(); + } + }, + /** + * Unsubscribes user from channels. + * + */ + unsubscribe: function (unsubscribe) { + var request = this.$['ajaxUnsubscribe']; + + request.url = this.unsubscribeUrl; + request.body = { + channels: unsubscribe, + conn_id: this.connectionId + }; + for (var i = 0; i < this.mutators.unsubscribe.length; i++) { + this.mutators.unsubscribe[i](request); + } + request.generateRequest() + }, + + /** + * calculates list of channels we should add user to based on difference + * between channels property and passed channel list + */ + calculateSubscribe: function (channels) { + var currentlySubscribed = this.channels; + var toSubscribe = []; + for (var i = 0; i < channels.length; i++) { + if (currentlySubscribed.indexOf(channels[i]) === -1) { + toSubscribe.push(channels[i]); + } + } + return toSubscribe + }, + /** + * calculates list of channels we should remove user from based difference + * between channels property and passed channel list + */ + calculateUnsubscribe: function (channels) { + var currentlySubscribed = this.channels; + var toUnsubscribe = []; + for (var i = 0; i < channels.length; i++) { + if (currentlySubscribed.indexOf(channels[i]) !== -1) { + toUnsubscribe.push(channels[i]); + } + } + return toUnsubscribe + }, + /** + * Marks the connection as expired. + * + */ + disconnect: function () { + var request = this.$['ajaxDisconnect']; + request.url = this.disconnectUrl; + request.params = { + conn_id: this.connectionId + }; + for (var i = 0; i < this.mutators.disconnect.length; i++) { + this.mutators.disconnect[i](request); + } + // mark connection as expired + request.generateRequest(); + // disconnect existing connection + this.closeConnection(); + }, + + /** + * Sends a message to the server. + * + */ + message: function (message) { + var request = this.$['ajaxMessage']; + request.url = this.messageUrl; + request.body = message; + for (var i = 0; i < this.mutators.message.length; i++) { + this.mutators.message[i](request) + } + request.generateRequest(); + }, + /** + * Opens "long lived" (websocket/longpoll) connection to the channelstream server. + * + */ + startListening: function (event) { + this.fire('start-listening', {}); + if (this.useWebsocket) { + this.useWebsocket = window.WebSocket ? true : false; + } + if (this.useWebsocket) { + this.openWebsocket(); + } + else { + this.openLongPoll(); + } + }, + /** + * Opens websocket connection. + * + */ + openWebsocket: function () { + var url = this.websocketUrl + '?conn_id=' + this.connectionId; + this.websocket = new WebSocket(url); + this.websocket.onopen = this._handleListenOpen.bind(this); + this.websocket.onclose = this._handleListenCloseEvent.bind(this); + this.websocket.onerror = this._handleListenErrorEvent.bind(this); + this.websocket.onmessage = this._handleListenMessageEvent.bind(this); + }, + /** + * Opens long-poll connection. + * + */ + openLongPoll: function () { + var request = this.$['ajaxListen']; + request.url = this.longPollUrl + '?conn_id=' + this.connectionId; + request.generateRequest() + }, + /** + * Retries `connect()` call while incrementing interval between tries up to 1 minute. + * + */ + retryConnection: function () { + if (!this.shouldReconnect) { + return; + } + if (this._currentBounceIv < 60000) { + this._currentBounceIv = this._currentBounceIv + this.increaseBounceIv; + } + else { + this._currentBounceIv = 60000; + } + setTimeout(this.connect.bind(this), this._currentBounceIv); + }, + /** + * Closes listening connection. + * + */ + closeConnection: function () { + var request = this.$['ajaxListen']; + if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { + this.websocket.onclose = null; + this.websocket.onerror = null; + this.websocket.close(); + } + if (request.loading) { + request.lastRequest.abort(); + } + this.connected = false; + }, + + _handleChannelsChange: function (event) { + // do not fire the event if set() didn't mutate anything + // is this a reliable way to do it? + if (!this.isReady || event === undefined) { + return + } + this.fire('channelstream-channels-changed', event) + }, + + _handleListenOpen: function (event) { + this.connected = true; + this.fire('channelstream-listen-opened', event); + this.createHeartBeats(); + }, + + createHeartBeats: function () { + if (typeof self._heartbeat === 'undefined' && this.websocket !== null + && this.heartbeats) { + self._heartbeat = setInterval(this._sendHeartBeat.bind(this), 10000); + } + }, + + _sendHeartBeat: function () { + if (this.websocket.readyState === WebSocket.OPEN && this.heartbeats) { + this.websocket.send(JSON.stringify({type: 'heartbeat'})); + } + }, + + _handleListenError: function (event) { + this.connected = false; + this.retryConnection(); + }, + _handleConnectError: function (event) { + this.connected = false; + this.fire('channelstream-connect-error', event.detail); + }, + + _handleListenMessageEvent: function (event) { + var data = null; + // comes from iron-ajax + if (event.detail) { + data = JSON.parse(event.detail.response) + // comes from websocket + setTimeout(this.openLongPoll.bind(this), 0); + } else { + data = JSON.parse(event.data) + } + this.fire('channelstream-listen-message', data); + + }, + + _handleListenCloseEvent: function (event) { + this.connected = false; + this.fire('channelstream-listen-closed', event.detail); + this.retryConnection(); + }, + + _handleListenErrorEvent: function (event) { + this.connected = false; + this.fire('channelstream-listen-error', {}) + }, + + _handleConnect: function (event) { + this.currentBounceIv = 0; + this.connectionId = event.detail.response.conn_id; + this.fire('channelstream-connected', event.detail.response); + }, + + _handleDisconnect: function (event) { + this.connected = false; + this.fire('channelstream-disconnected', {}); + }, + + _handleMessage: function (event) { + this.fire('channelstream-message-sent', event.detail.response); + }, + _handleMessageError: function (event) { + this.fire('channelstream-message-error', event.detail); + }, + + _handleSubscribe: function (event) { + this.fire('channelstream-subscribed', event.detail.response); + }, + + _handleSubscribeError: function (event) { + this.fire('channelstream-subscribe-error', event.detail); + }, + + _handleUnsubscribe: function (event) { + this.fire('channelstream-unsubscribed', event.detail.response); + }, + + _handleUnsubscribeError: function (event) { + this.fire('channelstream-unsubscribe-error', event.detail); + } +}); diff --git a/rhodecode/public/js/src/components/rhodecode-app/rhodecode-app.html b/rhodecode/public/js/src/components/rhodecode-app/rhodecode-app.html new file mode 100644 index 00000000..29c980a3 --- /dev/null +++ b/rhodecode/public/js/src/components/rhodecode-app/rhodecode-app.html @@ -0,0 +1,15 @@ + + + + + + + diff --git a/rhodecode/public/js/src/components/rhodecode-app/rhodecode-app.js b/rhodecode/public/js/src/components/rhodecode-app/rhodecode-app.js new file mode 100644 index 00000000..38db6402 --- /dev/null +++ b/rhodecode/public/js/src/components/rhodecode-app/rhodecode-app.js @@ -0,0 +1,127 @@ +ccLog = Logger.get('RhodeCodeApp'); +ccLog.setLevel(Logger.OFF); + +var rhodeCodeApp = Polymer({ + is: 'rhodecode-app', + created: function () { + ccLog.debug('rhodeCodeApp created'); + $.Topic('/notifications').subscribe(this.handleNotifications.bind(this)); + + $.Topic('/plugins/__REGISTER__').subscribe( + this.kickoffChannelstreamPlugin.bind(this) + ); + + $.Topic('/connection_controller/subscribe').subscribe( + this.subscribeToChannelTopic.bind(this)); + }, + + /** proxy to channelstream connection */ + getChannelStreamConnection: function () { + return this.$['channelstream-connection']; + }, + + handleNotifications: function (data) { + this.$['notifications'].handleNotification(data); + }, + + /** opens connection to ws server */ + kickoffChannelstreamPlugin: function (data) { + ccLog.debug('kickoffChannelstreamPlugin'); + var channels = ['broadcast']; + var addChannels = this.checkViewChannels(); + for (var i = 0; i < addChannels.length; i++) { + channels.push(addChannels[i]); + } + var channelstreamConnection = this.$['channelstream-connection']; + channelstreamConnection.connectUrl = CHANNELSTREAM_URLS.connect; + channelstreamConnection.subscribeUrl = CHANNELSTREAM_URLS.subscribe; + channelstreamConnection.websocketUrl = CHANNELSTREAM_URLS.ws + '/ws'; + channelstreamConnection.longPollUrl = CHANNELSTREAM_URLS.longpoll + '/listen'; + // some channels might already be registered by topic + for (var i = 0; i < channels.length; i++) { + channelstreamConnection.push('channels', channels[i]); + } + // append any additional channels registered in other plugins + $.Topic('/connection_controller/subscribe').processPrepared(); + channelstreamConnection.connect(); + }, + + checkViewChannels: function () { + var channels = [] + // subscribe to PR repo channel for PR's' + if (templateContext.pull_request_data.pull_request_id) { + var channelName = '/repo$' + templateContext.repo_name + '$/pr/' + + String(templateContext.pull_request_data.pull_request_id); + channels.push(channelName); + } + return channels; + }, + + /** subscribes users from channels in channelstream */ + subscribeToChannelTopic: function (channels) { + var channelstreamConnection = this.$['channelstream-connection']; + var toSubscribe = channelstreamConnection.calculateSubscribe(channels); + ccLog.debug('subscribeToChannelTopic', toSubscribe); + if (toSubscribe.length > 0) { + // if we are connected then subscribe + if (channelstreamConnection.connected) { + channelstreamConnection.subscribe(toSubscribe); + } + // not connected? just push channels onto the stack + else { + for (var i = 0; i < toSubscribe.length; i++) { + channelstreamConnection.push('channels', toSubscribe[i]); + } + } + } + }, + + /** publish received messages into correct topic */ + receivedMessage: function (event) { + for (var i = 0; i < event.detail.length; i++) { + var message = event.detail[i]; + if (message.message.topic) { + ccLog.debug('publishing', message.message.topic); + $.Topic(message.message.topic).publish(message); + } + else if (message.type === 'presence'){ + $.Topic('/connection_controller/presence').publish(message); + } + else { + ccLog.warn('unhandled message', message); + } + } + }, + + handleConnected: function (event) { + var channelstreamConnection = this.$['channelstream-connection']; + channelstreamConnection.set('channelsState', + event.detail.channels_info); + channelstreamConnection.set('userState', event.detail.state); + channelstreamConnection.set('channels', event.detail.channels); + this.propagageChannelsState(); + }, + handleSubscribed: function (event) { + var channelstreamConnection = this.$['channelstream-connection']; + var channelInfo = event.detail.channels_info; + var channelKeys = Object.keys(event.detail.channels_info); + for (var i = 0; i < channelKeys.length; i++) { + var key = channelKeys[i]; + channelstreamConnection.set(['channelsState', key], channelInfo[key]); + } + channelstreamConnection.set('channels', event.detail.channels); + this.propagageChannelsState(); + }, + /** propagates channel states on topics */ + propagageChannelsState: function (event) { + var channelstreamConnection = this.$['channelstream-connection']; + var channel_data = channelstreamConnection.channelsState; + var channels = channelstreamConnection.channels; + for (var i = 0; i < channels.length; i++) { + var key = channels[i]; + $.Topic('/connection_controller/channel_update').publish( + {channel: key, state: channel_data[key]} + ); + } + } +}); diff --git a/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.js b/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.js index c9e8ed4d..d468b3d8 100644 --- a/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.js +++ b/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.js @@ -11,10 +11,6 @@ Polymer({ observers: [ '_changedToasts(toasts.splices)' ], - ready: function(){ - - }, - _changedToasts: function(newValue, oldValue){ this.$['p-toast'].notifyResize(); }, @@ -27,5 +23,16 @@ Polymer({ open: function(){ this.$['p-toast'].open(); }, + handleNotification: function(data){ + if (!templateContext.rhodecode_user.notification_status && !data.message.force) { + // do not act if notifications are disabled + return + } + this.push('toasts',{ + level: data.message.level, + message: data.message.message + }); + this.open(); + }, _gettext: _gettext }); diff --git a/rhodecode/public/js/src/components/shared-components.html b/rhodecode/public/js/src/components/shared-components.html index 00d67cd8..b8f91b11 100644 --- a/rhodecode/public/js/src/components/shared-components.html +++ b/rhodecode/public/js/src/components/shared-components.html @@ -1,6 +1,8 @@ + + diff --git a/rhodecode/public/js/src/rhodecode/connection_controller.js b/rhodecode/public/js/src/rhodecode/connection_controller.js deleted file mode 100644 index d5a5afbb..00000000 --- a/rhodecode/public/js/src/rhodecode/connection_controller.js +++ /dev/null @@ -1,208 +0,0 @@ -"use strict"; -/** leak object to top level scope **/ -var ccLog = undefined; -// global code-mirror logger;, to enable run -// Logger.get('ConnectionController').setLevel(Logger.DEBUG) -ccLog = Logger.get('ConnectionController'); -ccLog.setLevel(Logger.OFF); - -var ConnectionController; -var connCtrlr; -var registerViewChannels; - -(function () { - ConnectionController = function (webappUrl, serverUrl, urls) { - var self = this; - - var channels = ['broadcast']; - this.state = { - open: false, - webappUrl: webappUrl, - serverUrl: serverUrl, - connId: null, - socket: null, - channels: channels, - heartbeat: null, - channelsInfo: {}, - urls: urls - }; - - this.listen = function () { - if (window.WebSocket) { - ccLog.debug('attempting to create socket'); - var socket_url = self.state.serverUrl + "/ws?conn_id=" + self.state.connId; - var socket_conf = { - url: socket_url, - handleAs: 'json', - headers: { - "Accept": "application/json", - "Content-Type": "application/json" - } - }; - self.state.socket = new WebSocket(socket_conf.url); - - self.state.socket.onopen = function (event) { - ccLog.debug('open event', event); - if (self.state.heartbeat === null) { - self.state.heartbeat = setInterval(function () { - if (self.state.socket.readyState === WebSocket.OPEN) { - self.state.socket.send('heartbeat'); - } - }, 10000) - } - }; - self.state.socket.onmessage = function (event) { - var data = $.parseJSON(event.data); - for (var i = 0; i < data.length; i++) { - if (data[i].message.topic) { - ccLog.debug('publishing', - data[i].message.topic, data[i]); - $.Topic(data[i].message.topic).publish(data[i]) - } - else { - ccLog.warn('unhandled message', data); - } - } - }; - self.state.socket.onclose = function (event) { - ccLog.debug('closed event', event); - setTimeout(function () { - self.connect(true); - }, 5000); - }; - - self.state.socket.onerror = function (event) { - ccLog.debug('error event', event); - }; - } - else { - ccLog.debug('attempting to create long polling connection'); - var poolUrl = self.state.serverUrl + "/listen?conn_id=" + self.state.connId; - self.state.socket = $.ajax({ - url: poolUrl - }).done(function (data) { - ccLog.debug('data', data); - var data = $.parseJSON(data); - for (var i = 0; i < data.length; i++) { - if (data[i].message.topic) { - ccLog.info('publishing', - data[i].message.topic, data[i]); - $.Topic(data[i].message.topic).publish(data[i]) - } - else { - ccLog.warn('unhandled message', data); - } - } - self.listen(); - }).fail(function () { - ccLog.debug('longpoll error'); - setTimeout(function () { - self.connect(true); - }, 5000); - }); - } - - }; - - this.connect = function (create_new_socket) { - var connReq = {'channels': self.state.channels}; - ccLog.debug('try obtaining connection info', connReq); - $.ajax({ - url: self.state.urls.connect, - type: "POST", - contentType: "application/json", - data: JSON.stringify(connReq), - dataType: "json" - }).done(function (data) { - ccLog.debug('Got connection:', data.conn_id); - self.state.channels = data.channels; - self.state.channelsInfo = data.channels_info; - self.state.connId = data.conn_id; - if (create_new_socket) { - self.listen(); - } - self.update(); - }).fail(function () { - setTimeout(function () { - self.connect(create_new_socket); - }, 5000); - }); - self.update(); - }; - - this.subscribeToChannels = function (channels) { - var new_channels = []; - for (var i = 0; i < channels.length; i++) { - var channel = channels[i]; - if (self.state.channels.indexOf(channel)) { - self.state.channels.push(channel); - new_channels.push(channel) - } - } - /** - * only execute the request if socket is present because subscribe - * can actually add channels before initial app connection - **/ - if (new_channels && self.state.socket !== null) { - var connReq = { - 'channels': self.state.channels, - 'conn_id': self.state.connId - }; - $.ajax({ - url: self.state.urls.subscribe, - type: "POST", - contentType: "application/json", - data: JSON.stringify(connReq), - dataType: "json" - }).done(function (data) { - self.state.channels = data.channels; - self.state.channelsInfo = data.channels_info; - self.update(); - }); - } - self.update(); - }; - - this.update = function () { - for (var key in this.state.channelsInfo) { - if (this.state.channelsInfo.hasOwnProperty(key)) { - // update channels with latest info - $.Topic('/connection_controller/channel_update').publish( - {channel: key, state: this.state.channelsInfo[key]}); - } - } - }; - - this.run = function () { - this.connect(true); - }; - - $.Topic('/connection_controller/subscribe').subscribe( - self.subscribeToChannels); - }; - - $.Topic('/plugins/__REGISTER__').subscribe(function (data) { - if (window.CHANNELSTREAM_SETTINGS && window.CHANNELSTREAM_SETTINGS.enabled) { - connCtrlr = new ConnectionController( - CHANNELSTREAM_SETTINGS.webapp_location, - CHANNELSTREAM_SETTINGS.ws_location, - CHANNELSTREAM_URLS - ); - registerViewChannels(); - - $(document).ready(function () { - connCtrlr.run(); - }); - } - }); - -registerViewChannels = function (){ - // subscribe to PR repo channel for PR's' - if (templateContext.pull_request_data.pull_request_id) { - var channelName = '/repo$' + templateContext.repo_name + '$/pr/' + - String(templateContext.pull_request_data.pull_request_id); - connCtrlr.state.channels.push(channelName); - } -} - -})(); diff --git a/rhodecode/public/js/src/rhodecode/utils/notifications.js b/rhodecode/public/js/src/rhodecode/utils/notifications.js deleted file mode 100644 index e468ad28..00000000 --- a/rhodecode/public/js/src/rhodecode/utils/notifications.js +++ /dev/null @@ -1,47 +0,0 @@ -"use strict"; - - -function notifySystem(data) { - var notification = new Notification(data.message.level + ': ' + data.message.message); -}; - -function notifyToaster(data){ - var notifications = document.getElementById('notifications'); - notifications.push('toasts', - { level: data.message.level, - message: data.message.message - }); - notifications.open(); -} - -function handleNotifications(data) { - if (!templateContext.rhodecode_user.notification_status && !data.message.force) { - // do not act if notifications are disabled - return - } - // use only js notifications for now - var onlyJS = true; - if (!("Notification" in window) || onlyJS) { - // use legacy notificartion - notifyToaster(data); - } - else { - // Let's check whether notification permissions have already been granted - if (Notification.permission === "granted") { - notifySystem(data); - } - // Otherwise, we need to ask the user for permission - else if (Notification.permission !== 'denied') { - Notification.requestPermission(function (permission) { - if (permission === "granted") { - notifySystem(data); - } - }); - } - else{ - notifyToaster(data); - } - } -}; - -$.Topic('/notifications').subscribe(handleNotifications); diff --git a/rhodecode/public/js/topics_list.txt b/rhodecode/public/js/topics_list.txt index 2c25850a..53e6f2b1 100644 --- a/rhodecode/public/js/topics_list.txt +++ b/rhodecode/public/js/topics_list.txt @@ -1,4 +1,7 @@ /plugins/__REGISTER__ - launched after the onDomReady() code from rhodecode.js is executed /ui/plugins/code/anchor_focus - launched when rc starts to scroll on load to anchor on PR/Codeview /ui/plugins/code/comment_form_built - launched when injectInlineForm() is executed and the form object is created -/notifications - shows new event notifications \ No newline at end of file +/notifications - shows new event notifications +/connection_controller/subscribe - subscribes user to new channels +/connection_controller/presence - receives presence change messages +/connection_controller/channel_update - receives channel states diff --git a/rhodecode/templates/base/root.html b/rhodecode/templates/base/root.html index 0e314da1..f5bc24a9 100644 --- a/rhodecode/templates/base/root.html +++ b/rhodecode/templates/base/root.html @@ -146,7 +146,9 @@ c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default <%def name="head_extra()"> ${self.head_extra()} ## extra stuff %if c.pre_code: @@ -174,6 +176,6 @@ c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default %if c.post_code: ${c.post_code|n} %endif - + From 08a1f1af346c02bb81c30e8338d3b7bc0fffcac2 Mon Sep 17 00:00:00 2001 From: Daniel Dourvaris Date: Tue, 13 Sep 2016 13:06:45 +0300 Subject: [PATCH 115/125] ux: remove position relative on diff td as it causes very slow rendering in browsers --- rhodecode/public/css/diff.less | 14 +++++++------- rhodecode/public/css/main.less | 1 - 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/rhodecode/public/css/diff.less b/rhodecode/public/css/diff.less index 71dea11e..47d35023 100644 --- a/rhodecode/public/css/diff.less +++ b/rhodecode/public/css/diff.less @@ -6,14 +6,15 @@ div.diffblock .code-header .changeset_header > div { // Line select and comment div.diffblock.margined.comm tr { td { - position: relative; + // IMPORTANT - never position:relative this as it causes insanely + // slow rendering } .add-comment-line { // Force td width for Firefox width: 20px; - - // TODO: anderson: fixing mouse-over bug. + + // TODO: anderson: fixing mouse-over bug. // why was it vertical-align baseline in first place?? vertical-align: top !important; // Force width and display for IE 9 @@ -23,9 +24,8 @@ div.diffblock.margined.comm tr { a { display: none; - position: absolute; - top: 2px; - left: 2px; + margin-top: 2px; + margin-left: 2px; color: @grey3; } } @@ -69,7 +69,7 @@ div.diffblock.margined.comm tr { &.commenting { &, del, ins { background-image: none !important; - background-color: lighten(@alert4, 10%) !important; + background-color: lighten(@alert4, 10%) !important; } } } diff --git a/rhodecode/public/css/main.less b/rhodecode/public/css/main.less index 83dd6987..47de5930 100644 --- a/rhodecode/public/css/main.less +++ b/rhodecode/public/css/main.less @@ -27,7 +27,6 @@ @import 'panels'; @import 'deform'; - //--- BASE ------------------// .noscript-error { top: 0; From 51ce2128e0b075ef0df250d157abb360b5c9faf1 Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Tue, 13 Sep 2016 17:31:44 +0200 Subject: [PATCH 116/125] frontend: use splitDelimitedHash when dealing with hash parsing --- rhodecode/public/js/src/rhodecode.js | 12 +++------- .../public/js/src/rhodecode/utils/string.js | 23 +++++++++++++++++++ rhodecode/templates/changeset/changeset.html | 6 ++--- .../pullrequests/pullrequest_show.html | 6 ++--- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/rhodecode/public/js/src/rhodecode.js b/rhodecode/public/js/src/rhodecode.js index e584289b..a46a2e8c 100644 --- a/rhodecode/public/js/src/rhodecode.js +++ b/rhodecode/public/js/src/rhodecode.js @@ -366,15 +366,9 @@ $(document).ready(function() { // At the time of development, Chrome didn't seem to support jquery's :target // element, so I had to scroll manually if (location.hash) { - var splitIx = location.hash.indexOf('/?/'); - if (splitIx !== -1){ - var loc = location.hash.slice(0, splitIx); - var remainder = location.hash.slice(splitIx + 2); - } - else{ - var loc = location.hash; - var remainder = null; - } + var result = splitDelimitedHash(location.hash); + var loc = result.loc; + var remainder = result.remainder; if (loc.length > 1){ var lineno = $(loc+'.lineno'); if (lineno.length > 0){ diff --git a/rhodecode/public/js/src/rhodecode/utils/string.js b/rhodecode/public/js/src/rhodecode/utils/string.js index bba1c17d..40a9dec3 100644 --- a/rhodecode/public/js/src/rhodecode/utils/string.js +++ b/rhodecode/public/js/src/rhodecode/utils/string.js @@ -73,6 +73,29 @@ String.prototype.capitalizeFirstLetter = function() { }; +/** + * Splits remainder + * + * @param input + */ +function splitDelimitedHash(input){ + var splitIx = input.indexOf('/?/'); + if (splitIx !== -1){ + var loc = input.slice(0, splitIx); + var remainder = input.slice(splitIx + 2); + } + else{ + var loc = input; + var remainder = null; + } + //fixes for some urls generated incorrectly + var result = loc.match('#+(.*)'); + if (result !== null){ + loc = '#' + result[1]; + } + return {loc:loc, remainder: remainder} +} + /** * Escape html characters in string */ diff --git a/rhodecode/templates/changeset/changeset.html b/rhodecode/templates/changeset/changeset.html index 1f2580a1..5523e1be 100644 --- a/rhodecode/templates/changeset/changeset.html +++ b/rhodecode/templates/changeset/changeset.html @@ -372,9 +372,9 @@ } }); - if (location.href.indexOf('#') != -1) { - var id = '#'+location.href.substring(location.href.indexOf('#') + 1).split('#'); - var line = $('html').find(id); + if (location.hash) { + var result = splitDelimitedHash(location.hash); + var line = $('html').find(result.loc); offsetScroll(line, 70); } diff --git a/rhodecode/templates/pullrequests/pullrequest_show.html b/rhodecode/templates/pullrequests/pullrequest_show.html index 6c3d011e..931c0c7c 100644 --- a/rhodecode/templates/pullrequests/pullrequest_show.html +++ b/rhodecode/templates/pullrequests/pullrequest_show.html @@ -412,9 +412,9 @@ %endif