diff --git a/pkgs/python-packages.nix b/pkgs/python-packages.nix --- a/pkgs/python-packages.nix +++ b/pkgs/python-packages.nix @@ -38,6 +38,19 @@ license = [ pkgs.lib.licenses.mit ]; }; }; + Chameleon = super.buildPythonPackage { + name = "Chameleon-2.24"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/5a/9e/637379ffa13c5172b5c0e704833ffea6bf51cec7567f93fd6e903d53ed74/Chameleon-2.24.tar.gz"; + md5 = "1b01f1f6533a8a11d0d2f2366dec5342"; + }; + meta = { + license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; + }; + }; Fabric = super.buildPythonPackage { name = "Fabric-1.10.0"; buildInputs = with self; []; @@ -558,6 +571,20 @@ license = [ pkgs.lib.licenses.bsdOriginal ]; }; }; + deform = super.buildPythonPackage { + name = "deform-2.0a3.dev0"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; [Chameleon colander iso8601 peppercorn translationstring zope.deprecation]; + src = fetchgit { + url = "https://github.com/Pylons/deform"; + rev = "08fb9de077c76951f6e70e28d4bf060a209d3d39"; + sha256 = "0nmhajc4pfgp4lbwhs5szqfzswpij1qyr69m7qkyhncl2g2d759r"; + }; + meta = { + license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; + }; + }; docutils = super.buildPythonPackage { name = "docutils-0.12"; buildInputs = with self; []; @@ -1355,7 +1382,7 @@ name = "rhodecode-enterprise-ce-4.3.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 colander decorator docutils 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]; + 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 colander decorator deform docutils 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]; src = ./.; meta = { license = [ { fullName = "AGPLv3, and Commercial License"; } ]; diff --git a/requirements.txt b/requirements.txt --- a/requirements.txt +++ b/requirements.txt @@ -60,6 +60,7 @@ cov-core==1.15.0 coverage==3.7.1 cssselect==0.9.1 decorator==3.4.2 +git+https://github.com/Pylons/deform@08fb9de077c76951f6e70e28d4bf060a209d3d39#egg=deform docutils==0.12 dogpile.cache==0.6.1 dogpile.core==0.4.1 diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -316,9 +316,10 @@ def includeme_first(config): config.add_route('favicon', '/favicon.ico') config.add_static_view( + '_static/deform', 'deform:static') + config.add_static_view( '_static', path='rhodecode:public', cache_max_age=3600 * 24) - def wrap_app_in_wsgi_middlewares(pyramid_app, config): """ Apply outer WSGI middlewares around the application. diff --git a/rhodecode/integrations/__init__.py b/rhodecode/integrations/__init__.py --- a/rhodecode/integrations/__init__.py +++ b/rhodecode/integrations/__init__.py @@ -19,6 +19,7 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import logging + from rhodecode.integrations.registry import IntegrationTypeRegistry from rhodecode.integrations.types import webhook, slack diff --git a/rhodecode/integrations/schema.py b/rhodecode/integrations/schema.py --- a/rhodecode/integrations/schema.py +++ b/rhodecode/integrations/schema.py @@ -35,7 +35,6 @@ class IntegrationSettingsSchemaBase(cola description=lazy_ugettext('Enable or disable this integration.'), missing=False, title=lazy_ugettext('Enabled'), - widget='bool', ) name = colander.SchemaNode( @@ -43,6 +42,4 @@ class IntegrationSettingsSchemaBase(cola description=lazy_ugettext('Short name for this integration.'), missing=colander.required, title=lazy_ugettext('Integration name'), - widget='string', ) - diff --git a/rhodecode/integrations/types/base.py b/rhodecode/integrations/types/base.py --- a/rhodecode/integrations/types/base.py +++ b/rhodecode/integrations/types/base.py @@ -31,8 +31,7 @@ class IntegrationTypeBase(object): self.settings = settings - @classmethod - def settings_schema(cls): + def settings_schema(self): """ A colander schema of settings for the integration type diff --git a/rhodecode/integrations/types/slack.py b/rhodecode/integrations/types/slack.py --- a/rhodecode/integrations/types/slack.py +++ b/rhodecode/integrations/types/slack.py @@ -19,7 +19,7 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ from __future__ import unicode_literals - +import deform import re import logging import requests @@ -48,10 +48,11 @@ class SlackSettingsSchema(IntegrationSet '' 'slack app manager')), default='', - placeholder='https://hooks.slack.com/services/...', preparer=strip_whitespace, validator=colander.url, - widget='string' + widget=deform.widget.TextInputWidget( + placeholder='https://hooks.slack.com/services/...', + ), ) username = colander.SchemaNode( colander.String(), @@ -59,8 +60,9 @@ class SlackSettingsSchema(IntegrationSet description=lazy_ugettext('Username to show notifications coming from.'), missing='Rhodecode', preparer=strip_whitespace, - widget='string', - placeholder='Rhodecode' + widget=deform.widget.TextInputWidget( + placeholder='Rhodecode' + ), ) channel = colander.SchemaNode( colander.String(), @@ -68,8 +70,9 @@ class SlackSettingsSchema(IntegrationSet description=lazy_ugettext('Channel to send notifications to.'), missing='', preparer=strip_whitespace, - widget='string', - placeholder='#general' + widget=deform.widget.TextInputWidget( + placeholder='#general' + ), ) icon_emoji = colander.SchemaNode( colander.String(), @@ -77,8 +80,9 @@ class SlackSettingsSchema(IntegrationSet description=lazy_ugettext('Emoji to use eg. :studio_microphone:'), missing='', preparer=strip_whitespace, - widget='string', - placeholder=':studio_microphone:' + widget=deform.widget.TextInputWidget( + placeholder=':studio_microphone:' + ), ) @@ -144,16 +148,19 @@ class SlackIntegrationType(IntegrationTy run_task(post_text_to_slack, self.settings, text) - @classmethod - def settings_schema(cls): + def settings_schema(self): schema = SlackSettingsSchema() schema.add(colander.SchemaNode( colander.Set(), - widget='checkbox_list', - choices=sorted([e.name for e in cls.valid_events]), + widget=deform.widget.CheckboxChoiceWidget( + values=sorted( + [(e.name, e.display_name) for e in self.valid_events] + ) + ), description="Events activated for this integration", name='events' )) + return schema def format_pull_request_comment_event(self, event, data): diff --git a/rhodecode/integrations/types/webhook.py b/rhodecode/integrations/types/webhook.py --- a/rhodecode/integrations/types/webhook.py +++ b/rhodecode/integrations/types/webhook.py @@ -20,6 +20,7 @@ from __future__ import unicode_literals +import deform import logging import requests import colander @@ -41,16 +42,18 @@ class WebhookSettingsSchema(IntegrationS description=lazy_ugettext('URL of the webhook to receive POST event.'), default='', validator=colander.url, - placeholder='https://www.example.com/webhook', - widget='string' + widget=deform.widget.TextInputWidget( + placeholder='https://www.example.com/webhook' + ), ) secret_token = colander.SchemaNode( colander.String(), title=lazy_ugettext('Secret Token'), description=lazy_ugettext('String used to validate received payloads.'), default='', - placeholder='secret_token', - widget='string' + widget=deform.widget.TextInputWidget( + placeholder='secret_token' + ), ) @@ -68,13 +71,15 @@ class WebhookIntegrationType(Integration events.RepoCreateEvent, ] - @classmethod - def settings_schema(cls): + def settings_schema(self): schema = WebhookSettingsSchema() schema.add(colander.SchemaNode( colander.Set(), - widget='checkbox_list', - choices=sorted([e.name for e in cls.valid_events]), + widget=deform.widget.CheckboxChoiceWidget( + values=sorted( + [(e.name, e.display_name) for e in self.valid_events] + ) + ), description="Events activated for this integration", name='events' )) diff --git a/rhodecode/integrations/views.py b/rhodecode/integrations/views.py --- a/rhodecode/integrations/views.py +++ b/rhodecode/integrations/views.py @@ -21,6 +21,7 @@ import colander import logging import pylons +import deform from pyramid.httpexceptions import HTTPFound, HTTPForbidden from pyramid.renderers import render @@ -101,30 +102,41 @@ class IntegrationSettingsViewBase(object return c def _form_schema(self): - return self.IntegrationType.settings_schema() + if self.integration: + settings = self.integration.settings + else: + settings = {} + return self.IntegrationType(settings=settings).settings_schema() - def settings_get(self, defaults=None, errors=None): + def settings_get(self, defaults=None, errors=None, form=None): """ View that displays the plugin settings as a form. """ defaults = defaults or {} errors = errors or {} - schema = self._form_schema() - - if not defaults: - if self.integration: - defaults['enabled'] = self.integration.enabled - defaults['name'] = self.integration.name + if self.integration: + defaults = self.integration.settings or {} + defaults['name'] = self.integration.name + defaults['enabled'] = self.integration.enabled + else: + if self.repo: + scope = self.repo.repo_name else: - if self.repo: - scope = self.repo.repo_name - else: - scope = _('Global') + scope = _('Global') + + defaults['name'] = '{} {} integration'.format(scope, + self.IntegrationType.display_name) + defaults['enabled'] = True - defaults['name'] = '{} {} integration'.format(scope, - self.IntegrationType.display_name) - defaults['enabled'] = True + schema = self._form_schema().bind(request=self.request) + + if self.integration: + buttons = ('submit', 'delete') + else: + buttons = ('submit',) + + form = form or deform.Form(schema, appstruct=defaults, buttons=buttons) for node in schema: setting = self.settings.get(node.name) @@ -135,6 +147,7 @@ class IntegrationSettingsViewBase(object defaults.setdefault(node.name, node.default) template_context = { + 'form': form, 'defaults': defaults, 'errors': errors, 'schema': schema, @@ -166,7 +179,9 @@ class IntegrationSettingsViewBase(object redirect_to = self.request.route_url('global_integrations_home') raise HTTPFound(redirect_to) - schema = self._form_schema() + schema = self._form_schema().bind(request=self.request) + + form = deform.Form(schema, buttons=('submit', 'delete')) params = {} for node in schema.children: @@ -177,15 +192,15 @@ class IntegrationSettingsViewBase(object if val: params[node.name] = val + controls = self.request.POST.items() try: - valid_data = schema.deserialize(params) - except colander.Invalid as e: - # Display error message and display form again. + valid_data = form.validate(controls) + except deform.ValidationFailure as e: self.request.session.flash( - _('Errors exist when saving plugin settings. ' + _('Errors exist when saving integration settings. ' 'Please check the form inputs.'), queue='error') - return self.settings_get(errors=e.asdict(), defaults=params) + return self.settings_get(errors={}, defaults=params, form=e) if not self.integration: self.integration = Integration() @@ -230,7 +245,6 @@ class IntegrationSettingsViewBase(object template_context = { 'current_IntegrationType': self.IntegrationType, 'current_integrations': current_integrations, - 'current_integration': 'none', 'available_integrations': integration_type_registry, 'c': self._template_c_context() } diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -950,7 +950,8 @@ def bool2icon(value): #============================================================================== from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \ HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \ -HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token +HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \ +csrf_token_key #============================================================================== @@ -1877,11 +1878,15 @@ def secure_form(url, method="POST", mult """ from webhelpers.pylonslib.secure_form import insecure_form - from rhodecode.lib.auth import get_csrf_token, csrf_token_key form = insecure_form(url, method, multipart, **attrs) - token = HTML.div(hidden(csrf_token_key, get_csrf_token()), style="display: none;") + token = csrf_input() return literal("%s\n%s" % (form, token)) +def csrf_input(): + return literal( + ''.format( + csrf_token_key, csrf_token_key, get_csrf_token())) + def dropdownmenu(name, selected, options, enable_filter=False, **attrs): select_html = select(name, selected, options, **attrs) select2 = """ @@ -1937,6 +1942,16 @@ def route_path(*args, **kwds): return req.route_path(*args, **kwds) +def static_url(*args, **kwds): + """ + Wrapper around pyramids `route_path` function. It is used to generate + URLs from within pylons views or templates. This will be removed when + pyramid migration if finished. + """ + req = get_current_request() + return req.static_url(*args, **kwds) + + def resource_path(*args, **kwds): """ Wrapper around pyramids `route_path` function. It is used to generate diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -41,6 +41,16 @@ for SELECT use formencode.All(OneOf(list """ +import deform +from pkg_resources import resource_filename + +deform_templates = resource_filename('deform', 'templates') +rhodecode_templates = resource_filename('rhodecode', 'templates/forms') +search_path = (rhodecode_templates, deform_templates) + +deform.Form.set_zpt_renderer(search_path) + + import logging import formencode diff --git a/rhodecode/public/css/deform.less b/rhodecode/public/css/deform.less new file mode 100644 --- /dev/null +++ b/rhodecode/public/css/deform.less @@ -0,0 +1,91 @@ +.deform { + + * { + box-sizing: border-box; + } + + .required:after { + color: #e32; + content: '*'; + display:inline; + } + + .control-label { + width: 200px; + float: left; + } + .control-inputs { + width: 400px; + float: left; + } + .form-group .radio, .form-group .checkbox { + position: relative; + display: block; + /* margin-bottom: 10px; */ + } + + .form-group { + clear: left; + } + + .form-control { + width: 100%; + } + + .error-block { + color: red; + } + + .deform-seq-container .control-inputs { + width: 100%; + } + + .deform-seq-container .deform-seq-item-handle { + width: 8.3%; + float: left; + } + + .deform-seq-container .deform-seq-item-group { + width: 91.6%; + float: left; + } + + .form-control { + input { + height: 40px; + } + input[type=checkbox], input[type=radio] { + height: auto; + } + select { + height: 40px; + } + } + + .form-control.select2-container { height: 40px; } + + .deform-two-field-sequence .deform-seq-container .deform-seq-item label { + display: none; + } + .deform-two-field-sequence .deform-seq-container .deform-seq-item:first-child label { + display: block; + } + .deform-two-field-sequence .deform-seq-container .deform-seq-item .panel-heading { + display: none; + } + .deform-two-field-sequence .deform-seq-container .deform-seq-item.form-group { + background: red; + } + .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; + } + .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel { + padding: 0; + margin: 5px 0; + border: none; + } + .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel > .panel-body { + padding: 0; + } + +} diff --git a/rhodecode/public/css/main.less b/rhodecode/public/css/main.less --- a/rhodecode/public/css/main.less +++ b/rhodecode/public/css/main.less @@ -25,6 +25,7 @@ @import 'comments'; @import 'panels-bootstrap'; @import 'panels'; +@import 'deform'; //--- BASE ------------------// @@ -141,7 +142,7 @@ input.inline[type="file"] { h1 { color: @grey2; } - + .error-branding { font-family: @text-semibold; color: @grey4; @@ -1022,9 +1023,9 @@ label { padding: .9em; color: @grey3; background-color: @grey7; - border-right: @border-thickness solid @border-default-color; - border-bottom: @border-thickness solid @border-default-color; - border-left: @border-thickness solid @border-default-color; + border-right: @border-thickness solid @border-default-color; + border-bottom: @border-thickness solid @border-default-color; + border-left: @border-thickness solid @border-default-color; } #repo_vcs_settings { @@ -1953,7 +1954,7 @@ div.search-feedback-items { padding:0px 0px 0px 96px; } -div.search-code-body { +div.search-code-body { background-color: #ffffff; padding: 5px 0 5px 10px; pre { .match { background-color: #faffa6;} diff --git a/rhodecode/templates/admin/integrations/edit.html b/rhodecode/templates/admin/integrations/edit.html --- a/rhodecode/templates/admin/integrations/edit.html +++ b/rhodecode/templates/admin/integrations/edit.html @@ -27,8 +27,6 @@ ${integration.name} %endif %def> - -
${node.description}
-+ There was a problem with this section +
+${errormsg}
+