# HG changeset patch # User Daniel Dourvaris # Date 2017-07-07 10:05:30 # Node ID 485023e66b4a20abf2f10257747ded6ee34603aa # Parent ed6eb7e0bdf81b77521ec4c1375185d619b446f7 gists: migrated gists controller to pyramid view. diff --git a/rhodecode/api/tests/test_get_gist.py b/rhodecode/api/tests/test_get_gist.py --- a/rhodecode/api/tests/test_get_gist.py +++ b/rhodecode/api/tests/test_get_gist.py @@ -28,7 +28,7 @@ from rhodecode.api.tests.utils import ( @pytest.mark.usefixtures("testuser_api", "app") class TestApiGetGist(object): - def test_api_get_gist(self, gist_util, http_host_stub): + def test_api_get_gist(self, gist_util, http_host_only_stub): gist = gist_util.create_gist() gist_id = gist.gist_access_id gist_created_on = gist.created_on @@ -45,14 +45,14 @@ class TestApiGetGist(object): 'expires': -1.0, 'gist_id': int(gist_id), 'type': 'public', - 'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,), + 'url': 'http://%s/_admin/gists/%s' % (http_host_only_stub, gist_id,), 'acl_level': Gist.ACL_LEVEL_PUBLIC, 'content': None, } assert_ok(id_, expected, given=response.body) - def test_api_get_gist_with_content(self, gist_util, http_host_stub): + def test_api_get_gist_with_content(self, gist_util, http_host_only_stub): mapping = { u'filename1.txt': {'content': u'hello world'}, u'filename1ą.txt': {'content': u'hello worldę'} @@ -73,7 +73,7 @@ class TestApiGetGist(object): 'expires': -1.0, 'gist_id': int(gist_id), 'type': 'public', - 'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,), + 'url': 'http://%s/_admin/gists/%s' % (http_host_only_stub, gist_id,), 'acl_level': Gist.ACL_LEVEL_PUBLIC, 'content': { u'filename1.txt': u'hello world', diff --git a/rhodecode/apps/gist/__init__.py b/rhodecode/apps/gist/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/gist/__init__.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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/ +from rhodecode.apps._base import ADMIN_PREFIX + + +def admin_routes(config): + config.add_route( + name='gists_show', pattern='/gists') + config.add_route( + name='gists_new', pattern='/gists/new') + config.add_route( + name='gists_create', pattern='/gists/create') + + config.add_route( + name='gist_show', pattern='/gists/{gist_id}') + + config.add_route( + name='gist_delete', pattern='/gists/{gist_id}/delete') + + config.add_route( + name='gist_edit', pattern='/gists/{gist_id}/edit') + + config.add_route( + name='gist_edit_check_revision', + pattern='/gists/{gist_id}/edit/check_revision') + + config.add_route( + name='gist_update', pattern='/gists/{gist_id}/update') + + config.add_route( + name='gist_show_rev', + pattern='/gists/{gist_id}/{revision}') + config.add_route( + name='gist_show_formatted', + pattern='/gists/{gist_id}/{revision}/{format}') + + config.add_route( + name='gist_show_formatted_path', + pattern='/gists/{gist_id}/{revision}/{format}/{f_path:.*}') + + +def includeme(config): + config.include(admin_routes, route_prefix=ADMIN_PREFIX) + # Scan module for configuration decorators. + config.scan() diff --git a/rhodecode/apps/gist/tests/__init__.py b/rhodecode/apps/gist/tests/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/gist/tests/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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/ + diff --git a/rhodecode/tests/functional/test_admin_gists.py b/rhodecode/apps/gist/tests/test_admin_gists.py rename from rhodecode/tests/functional/test_admin_gists.py rename to rhodecode/apps/gist/tests/test_admin_gists.py --- a/rhodecode/tests/functional/test_admin_gists.py +++ b/rhodecode/apps/gist/tests/test_admin_gists.py @@ -27,7 +27,31 @@ from rhodecode.model.gist import GistMod from rhodecode.model.meta import Session from rhodecode.tests import ( TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, - TestController, assert_session_flash, url) + TestController, assert_session_flash) + + +def route_path(name, params=None, **kwargs): + import urllib + from rhodecode.apps._base import ADMIN_PREFIX + + base_url = { + 'gists_show': ADMIN_PREFIX + '/gists', + 'gists_new': ADMIN_PREFIX + '/gists/new', + 'gists_create': ADMIN_PREFIX + '/gists/create', + 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}', + 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete', + 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit', + 'gist_edit_check_revision': ADMIN_PREFIX + '/gists/{gist_id}/edit/check_revision', + 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update', + 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/{revision}', + 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/{revision}/{format}', + 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/{revision}/{format}/{f_path}', + + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url class GistUtility(object): @@ -70,7 +94,7 @@ class TestGistsController(TestController def test_index_empty(self, create_gist): self.log_user() - response = self.app.get(url('gists')) + response = self.app.get(route_path('gists_show')) response.mustcontain('data: [],') def test_index(self, create_gist): @@ -79,7 +103,7 @@ class TestGistsController(TestController g2 = create_gist('gist2', lifetime=1400) g3 = create_gist('gist3', description='gist3-desc') g4 = create_gist('gist4', gist_type='private').gist_access_id - response = self.app.get(url('gists')) + response = self.app.get(route_path('gists_show')) response.mustcontain('gist: %s' % g1.gist_access_id) response.mustcontain('gist: %s' % g2.gist_access_id) @@ -95,7 +119,7 @@ class TestGistsController(TestController def test_index_private_gists(self, create_gist): self.log_user() gist = create_gist('gist5', gist_type='private') - response = self.app.get(url('gists', private=1)) + response = self.app.get(route_path('gists_show', params=dict(private=1))) # and privates response.mustcontain('gist: %s' % gist.gist_access_id) @@ -107,7 +131,7 @@ class TestGistsController(TestController create_gist('gist3', description='gist3-desc') create_gist('gist4', gist_type='private') - response = self.app.get(url('gists', all=1)) + response = self.app.get(route_path('gists_show', params=dict(all=1))) assert len(GistModel.get_all()) == 4 # and privates @@ -120,7 +144,7 @@ class TestGistsController(TestController create_gist('gist3', gist_type='private') create_gist('gist4', gist_type='private') - response = self.app.get(url('gists', all=1)) + response = self.app.get(route_path('gists_show', params=dict(all=1))) assert len(GistModel.get_all()) == 3 # since we don't have access to private in this view, we @@ -131,7 +155,7 @@ class TestGistsController(TestController def test_create(self): self.log_user() response = self.app.post( - url('gists'), + route_path('gists_create'), params={'lifetime': -1, 'content': 'gist test', 'filename': 'foo', @@ -146,7 +170,7 @@ class TestGistsController(TestController def test_create_with_path_with_dirs(self): self.log_user() response = self.app.post( - url('gists'), + route_path('gists_create'), params={'lifetime': -1, 'content': 'gist test', 'filename': '/home/foo', @@ -163,12 +187,13 @@ class TestGistsController(TestController Session().add(gist) Session().commit() - self.app.get(url('gist', gist_id=gist.gist_access_id), status=404) + self.app.get(route_path('gist_show', gist_id=gist.gist_access_id), + status=404) def test_create_private(self): self.log_user() response = self.app.post( - url('gists'), + route_path('gists_create'), params={'lifetime': -1, 'content': 'private gist test', 'filename': 'private-foo', @@ -187,7 +212,7 @@ class TestGistsController(TestController def test_create_private_acl_private(self): self.log_user() response = self.app.post( - url('gists'), + route_path('gists_create'), params={'lifetime': -1, 'content': 'private gist test', 'filename': 'private-foo', @@ -206,7 +231,7 @@ class TestGistsController(TestController def test_create_with_description(self): self.log_user() response = self.app.post( - url('gists'), + route_path('gists_create'), params={'lifetime': -1, 'content': 'gist test', 'filename': 'foo-desc', @@ -231,7 +256,8 @@ class TestGistsController(TestController 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC, 'csrf_token': self.csrf_token } - response = self.app.post(url('gists'), params=params, status=302) + response = self.app.post( + route_path('gists_create'), params=params, status=302) self.logout_user() response = response.follow() response.mustcontain('added file: foo-desc') @@ -240,35 +266,36 @@ class TestGistsController(TestController def test_new(self): self.log_user() - self.app.get(url('new_gist')) + self.app.get(route_path('gists_new')) def test_delete(self, create_gist): self.log_user() gist = create_gist('delete-me') response = self.app.post( - url('gist', gist_id=gist.gist_id), - params={'_method': 'delete', 'csrf_token': self.csrf_token}) + route_path('gist_delete', gist_id=gist.gist_id), + params={'csrf_token': self.csrf_token}) assert_session_flash(response, 'Deleted gist %s' % gist.gist_id) def test_delete_normal_user_his_gist(self, create_gist): self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN) + response = self.app.post( - url('gist', gist_id=gist.gist_id), - params={'_method': 'delete', 'csrf_token': self.csrf_token}) + route_path('gist_delete', gist_id=gist.gist_id), + params={'csrf_token': self.csrf_token}) assert_session_flash(response, 'Deleted gist %s' % gist.gist_id) def test_delete_normal_user_not_his_own_gist(self, create_gist): self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) - gist = create_gist('delete-me') + gist = create_gist('delete-me-2') + self.app.post( - url('gist', gist_id=gist.gist_id), - params={'_method': 'delete', 'csrf_token': self.csrf_token}, - status=403) + route_path('gist_delete', gist_id=gist.gist_id), + params={'csrf_token': self.csrf_token}, status=404) def test_show(self, create_gist): gist = create_gist('gist-show-me') - response = self.app.get(url('gist', gist_id=gist.gist_access_id)) + response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id)) response.mustcontain('added file: gist-show-me<') @@ -283,16 +310,19 @@ class TestGistsController(TestController with mock.patch( 'rhodecode.lib.vcs.settings.ALIASES', ['git']): gist = create_gist('gist-show-me-again') - self.app.get(url('gist', gist_id=gist.gist_access_id), status=200) + self.app.get( + route_path('gist_show', gist_id=gist.gist_access_id), status=200) def test_show_acl_private(self, create_gist): gist = create_gist('gist-show-me-only-when-im-logged-in', acl_level=Gist.ACL_LEVEL_PRIVATE) - self.app.get(url('gist', gist_id=gist.gist_access_id), status=404) + self.app.get( + route_path('gist_show', gist_id=gist.gist_access_id), status=404) # now we log-in we should see thi gist self.log_user() - response = self.app.get(url('gist', gist_id=gist.gist_access_id)) + response = self.app.get( + route_path('gist_show', gist_id=gist.gist_access_id)) response.mustcontain('added file: gist-show-me-only-when-im-logged-in') assert_response = response.assert_response() @@ -303,36 +333,42 @@ class TestGistsController(TestController def test_show_as_raw(self, create_gist): gist = create_gist('gist-show-me', content='GIST CONTENT') - response = self.app.get(url('formatted_gist', - gist_id=gist.gist_access_id, format='raw')) + response = self.app.get( + route_path('gist_show_formatted', + gist_id=gist.gist_access_id, revision='tip', + format='raw')) assert response.body == 'GIST CONTENT' def test_show_as_raw_individual_file(self, create_gist): gist = create_gist('gist-show-me-raw', content='GIST BODY') - response = self.app.get(url('formatted_gist_file', - gist_id=gist.gist_access_id, format='raw', - revision='tip', f_path='gist-show-me-raw')) + response = self.app.get( + route_path('gist_show_formatted_path', + gist_id=gist.gist_access_id, format='raw', + revision='tip', f_path='gist-show-me-raw')) assert response.body == 'GIST BODY' def test_edit_page(self, create_gist): self.log_user() gist = create_gist('gist-for-edit', content='GIST EDIT BODY') - response = self.app.get(url('edit_gist', gist_id=gist.gist_access_id)) + response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id)) response.mustcontain('GIST EDIT BODY') def test_edit_page_non_logged_user(self, create_gist): gist = create_gist('gist-for-edit', content='GIST EDIT BODY') - self.app.get(url('edit_gist', gist_id=gist.gist_access_id), status=302) + self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id), + status=302) def test_edit_normal_user_his_gist(self, create_gist): self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN) - self.app.get(url('edit_gist', gist_id=gist.gist_access_id, status=200)) + self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id, + status=200)) def test_edit_normal_user_not_his_own_gist(self, create_gist): self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) gist = create_gist('delete-me') - self.app.get(url('edit_gist', gist_id=gist.gist_access_id), status=403) + self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id), + status=404) def test_user_first_name_is_escaped(self, user_util, create_gist): xss_atack_string = '">' @@ -341,7 +377,7 @@ class TestGistsController(TestController user = user_util.create_user( firstname=xss_atack_string, password=password) create_gist('gist', gist_type='public', owner=user.username) - response = self.app.get(url('gists')) + response = self.app.get(route_path('gists_show')) response.mustcontain(xss_escaped_string) def test_user_last_name_is_escaped(self, user_util, create_gist): @@ -351,5 +387,5 @@ class TestGistsController(TestController user = user_util.create_user( lastname=xss_atack_string, password=password) create_gist('gist', gist_type='public', owner=user.username) - response = self.app.get(url('gists')) + response = self.app.get(route_path('gists_show')) response.mustcontain(xss_escaped_string) diff --git a/rhodecode/apps/gist/views.py b/rhodecode/apps/gist/views.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/gist/views.py @@ -0,0 +1,412 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2013-2017 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 time +import logging + +import formencode +import peppercorn + +from pyramid.httpexceptions import HTTPNotFound, HTTPForbidden, HTTPFound +from pyramid.view import view_config +from pyramid.renderers import render +from pyramid.response import Response + +from rhodecode.apps._base import BaseAppView +from rhodecode.lib import helpers as h +from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired +from rhodecode.lib.utils2 import time_to_datetime +from rhodecode.lib.ext_json import json +from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError +from rhodecode.model.gist import GistModel +from rhodecode.model.meta import Session +from rhodecode.model.db import Gist, User, or_ +from rhodecode.model import validation_schema +from rhodecode.model.validation_schema.schemas import gist_schema + + +log = logging.getLogger(__name__) + + +class GistView(BaseAppView): + + def load_default_context(self): + _ = self.request.translate + c = self._get_local_tmpl_context() + c.user = c.auth_user.get_instance() + + c.lifetime_values = [ + (-1, _('forever')), + (5, _('5 minutes')), + (60, _('1 hour')), + (60 * 24, _('1 day')), + (60 * 24 * 30, _('1 month')), + ] + + c.lifetime_options = [(c.lifetime_values, _("Lifetime"))] + c.acl_options = [ + (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")), + (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users")) + ] + + self._register_global_c(c) + return c + + @LoginRequired() + @view_config( + route_name='gists_show', request_method='GET', + renderer='rhodecode:templates/admin/gists/index.mako') + def gist_show_all(self): + c = self.load_default_context() + + not_default_user = self._rhodecode_user.username != User.DEFAULT_USER + c.show_private = self.request.GET.get('private') and not_default_user + c.show_public = self.request.GET.get('public') and not_default_user + c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin + + gists = _gists = Gist().query()\ + .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\ + .order_by(Gist.created_on.desc()) + + c.active = 'public' + # MY private + if c.show_private and not c.show_public: + gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\ + .filter(Gist.gist_owner == self._rhodecode_user.user_id) + c.active = 'my_private' + # MY public + elif c.show_public and not c.show_private: + gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\ + .filter(Gist.gist_owner == self._rhodecode_user.user_id) + c.active = 'my_public' + # MY public+private + elif c.show_private and c.show_public: + gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC, + Gist.gist_type == Gist.GIST_PRIVATE))\ + .filter(Gist.gist_owner == self._rhodecode_user.user_id) + c.active = 'my_all' + # Show all by super-admin + elif c.show_all: + c.active = 'all' + gists = _gists + + # default show ALL public gists + if not c.show_public and not c.show_private and not c.show_all: + gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC) + c.active = 'public' + + from rhodecode.lib.utils import PartialRenderer + _render = PartialRenderer('data_table/_dt_elements.mako') + + data = [] + + for gist in gists: + data.append({ + 'created_on': _render('gist_created', gist.created_on), + 'created_on_raw': gist.created_on, + 'type': _render('gist_type', gist.gist_type), + 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact), + 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires), + 'author_raw': h.escape(gist.owner.full_contact), + 'expires': _render('gist_expires', gist.gist_expires), + 'description': _render('gist_description', gist.gist_description) + }) + c.data = json.dumps(data) + + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='gists_new', request_method='GET', + renderer='rhodecode:templates/admin/gists/new.mako') + def gist_new(self): + c = self.load_default_context() + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + @view_config( + route_name='gists_create', request_method='POST', + renderer='rhodecode:templates/admin/gists/new.mako') + def gist_create(self): + _ = self.request.translate + c = self.load_default_context() + + data = dict(self.request.POST) + data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME + data['nodes'] = [{ + 'filename': data['filename'], + 'content': data.get('content'), + 'mimetype': data.get('mimetype') # None is autodetect + }] + + data['gist_type'] = ( + Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE) + data['gist_acl_level'] = ( + data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE) + + schema = gist_schema.GistSchema().bind( + lifetime_options=[x[0] for x in c.lifetime_values]) + + try: + + schema_data = schema.deserialize(data) + # convert to safer format with just KEYs so we sure no duplicates + schema_data['nodes'] = gist_schema.sequence_to_nodes( + schema_data['nodes']) + + gist = GistModel().create( + gist_id=schema_data['gistid'], # custom access id not real ID + description=schema_data['description'], + owner=self._rhodecode_user.user_id, + gist_mapping=schema_data['nodes'], + gist_type=schema_data['gist_type'], + lifetime=schema_data['lifetime'], + gist_acl_level=schema_data['gist_acl_level'] + ) + Session().commit() + new_gist_id = gist.gist_access_id + except validation_schema.Invalid as errors: + defaults = data + errors = errors.asdict() + + if 'nodes.0.content' in errors: + errors['content'] = errors['nodes.0.content'] + del errors['nodes.0.content'] + if 'nodes.0.filename' in errors: + errors['filename'] = errors['nodes.0.filename'] + del errors['nodes.0.filename'] + + data = render('rhodecode:templates/admin/gists/new.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + errors=errors, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + except Exception: + log.exception("Exception while trying to create a gist") + h.flash(_('Error occurred during gist creation'), category='error') + raise HTTPFound(h.route_url('gists_new')) + raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id)) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + @view_config( + route_name='gist_delete', request_method='POST') + def gist_delete(self): + _ = self.request.translate + gist_id = self.request.matchdict['gist_id'] + + c = self.load_default_context() + c.gist = Gist.get_or_404(gist_id) + + owner = c.gist.gist_owner == self._rhodecode_user.user_id + if not (h.HasPermissionAny('hg.admin')() or owner): + log.warning('Deletion of Gist was forbidden ' + 'by unauthorized user: `%s`', self._rhodecode_user) + raise HTTPNotFound() + + GistModel().delete(c.gist) + Session().commit() + h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success') + + raise HTTPFound(h.route_url('gists_show')) + + def _get_gist(self, gist_id): + + gist = Gist.get_or_404(gist_id) + + # Check if this gist is expired + if gist.gist_expires != -1: + if time.time() > gist.gist_expires: + log.error( + 'Gist expired at %s', time_to_datetime(gist.gist_expires)) + raise HTTPNotFound() + + # check if this gist requires a login + is_default_user = self._rhodecode_user.username == User.DEFAULT_USER + if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user: + log.error("Anonymous user %s tried to access protected gist `%s`", + self._rhodecode_user, gist_id) + raise HTTPNotFound() + return gist + + @LoginRequired() + @view_config( + route_name='gist_show', request_method='GET', + renderer='rhodecode:templates/admin/gists/show.mako') + @view_config( + route_name='gist_show_rev', request_method='GET', + renderer='rhodecode:templates/admin/gists/show.mako') + @view_config( + route_name='gist_show_formatted', request_method='GET', + renderer=None) + @view_config( + route_name='gist_show_formatted_path', request_method='GET', + renderer=None) + def show(self): + gist_id = self.request.matchdict['gist_id'] + + # TODO(marcink): expose those via matching dict + revision = self.request.matchdict.get('revision', 'tip') + f_path = self.request.matchdict.get('f_path', None) + return_format = self.request.matchdict.get('format') + + c = self.load_default_context() + c.gist = self._get_gist(gist_id) + c.render = not self.request.GET.get('no-render', False) + + try: + c.file_last_commit, c.files = GistModel().get_gist_files( + gist_id, revision=revision) + except VCSError: + log.exception("Exception in gist show") + raise HTTPNotFound() + + if return_format == 'raw': + content = '\n\n'.join([f.content for f in c.files + if (f_path is None or f.path == f_path)]) + response = Response(content) + response.content_type = 'text/plain' + return response + + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='gist_edit', request_method='GET', + renderer='rhodecode:templates/admin/gists/edit.mako') + def gist_edit(self): + _ = self.request.translate + gist_id = self.request.matchdict['gist_id'] + c = self.load_default_context() + c.gist = self._get_gist(gist_id) + + owner = c.gist.gist_owner == self._rhodecode_user.user_id + if not (h.HasPermissionAny('hg.admin')() or owner): + raise HTTPNotFound() + + try: + c.file_last_commit, c.files = GistModel().get_gist_files(gist_id) + except VCSError: + log.exception("Exception in gist edit") + raise HTTPNotFound() + + if c.gist.gist_expires == -1: + expiry = _('never') + else: + # this cannot use timeago, since it's used in select2 as a value + expiry = h.age(h.time_to_datetime(c.gist.gist_expires)) + + c.lifetime_values.append( + (0, _('%(expiry)s - current value') % {'expiry': _(expiry)}) + ) + + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + @view_config( + route_name='gist_update', request_method='POST', + renderer='rhodecode:templates/admin/gists/edit.mako') + def gist_update(self): + _ = self.request.translate + gist_id = self.request.matchdict['gist_id'] + c = self.load_default_context() + c.gist = self._get_gist(gist_id) + + owner = c.gist.gist_owner == self._rhodecode_user.user_id + if not (h.HasPermissionAny('hg.admin')() or owner): + raise HTTPNotFound() + + data = peppercorn.parse(self.request.POST.items()) + + schema = gist_schema.GistSchema() + schema = schema.bind( + # '0' is special value to leave lifetime untouched + lifetime_options=[x[0] for x in c.lifetime_values] + [0], + ) + + try: + schema_data = schema.deserialize(data) + # convert to safer format with just KEYs so we sure no duplicates + schema_data['nodes'] = gist_schema.sequence_to_nodes( + schema_data['nodes']) + + GistModel().update( + gist=c.gist, + description=schema_data['description'], + owner=c.gist.owner, + gist_mapping=schema_data['nodes'], + lifetime=schema_data['lifetime'], + gist_acl_level=schema_data['gist_acl_level'] + ) + + Session().commit() + h.flash(_('Successfully updated gist content'), category='success') + except NodeNotChangedError: + # raised if nothing was changed in repo itself. We anyway then + # store only DB stuff for gist + Session().commit() + h.flash(_('Successfully updated gist data'), category='success') + except validation_schema.Invalid as errors: + errors = errors.asdict() + h.flash(_('Error occurred during update of gist {}: {}').format( + gist_id, errors), category='error') + except Exception: + log.exception("Exception in gist edit") + h.flash(_('Error occurred during update of gist %s') % gist_id, + category='error') + + raise HTTPFound(h.route_url('gist_show', gist_id=gist_id)) + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='gist_edit_check_revision', request_method='GET', + renderer='json_ext') + def gist_edit_check_revision(self): + _ = self.request.translate + gist_id = self.request.matchdict['gist_id'] + c = self.load_default_context() + c.gist = self._get_gist(gist_id) + + last_rev = c.gist.scm_instance().get_commit() + success = True + revision = self.request.GET.get('revision') + + if revision != last_rev.raw_id: + log.error('Last revision %s is different then submitted %s' + % (revision, last_rev)) + # our gist has newer version than we + success = False + + return {'success': success} diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -303,6 +303,7 @@ def includeme(config): config.include('rhodecode.apps.user_profile') config.include('rhodecode.apps.my_account') config.include('rhodecode.apps.svn_support') + config.include('rhodecode.apps.gist') config.include('rhodecode.tweens') config.include('rhodecode.api') diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -489,39 +489,6 @@ def make_map(config): m.connect('notification', '/notifications/{notification_id}', action='show', conditions={'method': ['GET']}) - # ADMIN GIST - with rmap.submapper(path_prefix=ADMIN_PREFIX, - controller='admin/gists') as m: - m.connect('gists', '/gists', - action='create', conditions={'method': ['POST']}) - m.connect('gists', '/gists', jsroute=True, - action='index', conditions={'method': ['GET']}) - m.connect('new_gist', '/gists/new', jsroute=True, - action='new', conditions={'method': ['GET']}) - - m.connect('/gists/{gist_id}', - action='delete', conditions={'method': ['DELETE']}) - m.connect('edit_gist', '/gists/{gist_id}/edit', - action='edit_form', conditions={'method': ['GET']}) - m.connect('edit_gist', '/gists/{gist_id}/edit', - action='edit', conditions={'method': ['POST']}) - m.connect( - 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision', - action='check_revision', conditions={'method': ['GET']}) - - m.connect('gist', '/gists/{gist_id}', - action='show', conditions={'method': ['GET']}) - m.connect('gist_rev', '/gists/{gist_id}/{revision}', - revision='tip', - action='show', conditions={'method': ['GET']}) - m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}', - revision='tip', - action='show', conditions={'method': ['GET']}) - m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}', - revision='tip', - action='show', conditions={'method': ['GET']}, - requirements=URL_NAME_REQUIREMENTS) - # USER JOURNAL rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,), controller='journal', action='index') diff --git a/rhodecode/controllers/admin/gists.py b/rhodecode/controllers/admin/gists.py deleted file mode 100644 --- a/rhodecode/controllers/admin/gists.py +++ /dev/null @@ -1,369 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2013-2017 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/ - - -""" -gist controller for RhodeCode -""" - -import time -import logging - -import formencode -import peppercorn - -from pylons import request, response, tmpl_context as c, url -from pylons.controllers.util import redirect -from pylons.i18n.translation import _ -from webob.exc import HTTPNotFound, HTTPForbidden -from sqlalchemy.sql.expression import or_ - - -from rhodecode.model.gist import GistModel -from rhodecode.model.meta import Session -from rhodecode.model.db import Gist, User -from rhodecode.lib import auth -from rhodecode.lib import helpers as h -from rhodecode.lib.base import BaseController, render -from rhodecode.lib.auth import LoginRequired, NotAnonymous -from rhodecode.lib.utils import jsonify -from rhodecode.lib.utils2 import time_to_datetime -from rhodecode.lib.ext_json import json -from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError -from rhodecode.model import validation_schema -from rhodecode.model.validation_schema.schemas import gist_schema - - -log = logging.getLogger(__name__) - - -class GistsController(BaseController): - """REST Controller styled on the Atom Publishing Protocol""" - - def __load_defaults(self, extra_values=None): - c.lifetime_values = [ - (-1, _('forever')), - (5, _('5 minutes')), - (60, _('1 hour')), - (60 * 24, _('1 day')), - (60 * 24 * 30, _('1 month')), - ] - if extra_values: - c.lifetime_values.append(extra_values) - c.lifetime_options = [(c.lifetime_values, _("Lifetime"))] - c.acl_options = [ - (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")), - (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users")) - ] - - @LoginRequired() - def index(self): - """GET /admin/gists: All items in the collection""" - # url('gists') - not_default_user = c.rhodecode_user.username != User.DEFAULT_USER - c.show_private = request.GET.get('private') and not_default_user - c.show_public = request.GET.get('public') and not_default_user - c.show_all = request.GET.get('all') and c.rhodecode_user.admin - - gists = _gists = Gist().query()\ - .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\ - .order_by(Gist.created_on.desc()) - - c.active = 'public' - # MY private - if c.show_private and not c.show_public: - gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\ - .filter(Gist.gist_owner == c.rhodecode_user.user_id) - c.active = 'my_private' - # MY public - elif c.show_public and not c.show_private: - gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\ - .filter(Gist.gist_owner == c.rhodecode_user.user_id) - c.active = 'my_public' - # MY public+private - elif c.show_private and c.show_public: - gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC, - Gist.gist_type == Gist.GIST_PRIVATE))\ - .filter(Gist.gist_owner == c.rhodecode_user.user_id) - c.active = 'my_all' - # Show all by super-admin - elif c.show_all: - c.active = 'all' - gists = _gists - - # default show ALL public gists - if not c.show_public and not c.show_private and not c.show_all: - gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC) - c.active = 'public' - - from rhodecode.lib.utils import PartialRenderer - _render = PartialRenderer('data_table/_dt_elements.mako') - - data = [] - - for gist in gists: - data.append({ - 'created_on': _render('gist_created', gist.created_on), - 'created_on_raw': gist.created_on, - 'type': _render('gist_type', gist.gist_type), - 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact), - 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires), - 'author_raw': h.escape(gist.owner.full_contact), - 'expires': _render('gist_expires', gist.gist_expires), - 'description': _render('gist_description', gist.gist_description) - }) - c.data = json.dumps(data) - return render('admin/gists/index.mako') - - @LoginRequired() - @NotAnonymous() - @auth.CSRFRequired() - def create(self): - """POST /admin/gists: Create a new item""" - # url('gists') - self.__load_defaults() - - data = dict(request.POST) - data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME - data['nodes'] = [{ - 'filename': data['filename'], - 'content': data.get('content'), - 'mimetype': data.get('mimetype') # None is autodetect - }] - - data['gist_type'] = ( - Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE) - data['gist_acl_level'] = ( - data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE) - - schema = gist_schema.GistSchema().bind( - lifetime_options=[x[0] for x in c.lifetime_values]) - - try: - - schema_data = schema.deserialize(data) - # convert to safer format with just KEYs so we sure no duplicates - schema_data['nodes'] = gist_schema.sequence_to_nodes( - schema_data['nodes']) - - gist = GistModel().create( - gist_id=schema_data['gistid'], # custom access id not real ID - description=schema_data['description'], - owner=c.rhodecode_user.user_id, - gist_mapping=schema_data['nodes'], - gist_type=schema_data['gist_type'], - lifetime=schema_data['lifetime'], - gist_acl_level=schema_data['gist_acl_level'] - ) - Session().commit() - new_gist_id = gist.gist_access_id - except validation_schema.Invalid as errors: - defaults = data - errors = errors.asdict() - - if 'nodes.0.content' in errors: - errors['content'] = errors['nodes.0.content'] - del errors['nodes.0.content'] - if 'nodes.0.filename' in errors: - errors['filename'] = errors['nodes.0.filename'] - del errors['nodes.0.filename'] - - return formencode.htmlfill.render( - render('admin/gists/new.mako'), - defaults=defaults, - errors=errors, - prefix_error=False, - encoding="UTF-8", - force_defaults=False - ) - - except Exception: - log.exception("Exception while trying to create a gist") - h.flash(_('Error occurred during gist creation'), category='error') - return redirect(url('new_gist')) - return redirect(url('gist', gist_id=new_gist_id)) - - @LoginRequired() - @NotAnonymous() - def new(self): - """GET /admin/gists/new: Form to create a new item""" - # url('new_gist') - self.__load_defaults() - return render('admin/gists/new.mako') - - @LoginRequired() - @NotAnonymous() - @auth.CSRFRequired() - def delete(self, gist_id): - """DELETE /admin/gists/gist_id: Delete an existing item""" - # Forms posted to this method should contain a hidden field: - # - # Or using helpers: - # h.form(url('gist', gist_id=ID), - # method='delete') - # url('gist', gist_id=ID) - c.gist = Gist.get_or_404(gist_id) - - owner = c.gist.gist_owner == c.rhodecode_user.user_id - if not (h.HasPermissionAny('hg.admin')() or owner): - raise HTTPForbidden() - - GistModel().delete(c.gist) - Session().commit() - h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success') - - return redirect(url('gists')) - - def _add_gist_to_context(self, gist_id): - c.gist = Gist.get_or_404(gist_id) - - # Check if this gist is expired - if c.gist.gist_expires != -1: - if time.time() > c.gist.gist_expires: - log.error( - 'Gist expired at %s', time_to_datetime(c.gist.gist_expires)) - raise HTTPNotFound() - - # check if this gist requires a login - is_default_user = c.rhodecode_user.username == User.DEFAULT_USER - if c.gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user: - log.error("Anonymous user %s tried to access protected gist `%s`", - c.rhodecode_user, gist_id) - raise HTTPNotFound() - - @LoginRequired() - def show(self, gist_id, revision='tip', format='html', f_path=None): - """GET /admin/gists/gist_id: Show a specific item""" - # url('gist', gist_id=ID) - self._add_gist_to_context(gist_id) - c.render = not request.GET.get('no-render', False) - - try: - c.file_last_commit, c.files = GistModel().get_gist_files( - gist_id, revision=revision) - except VCSError: - log.exception("Exception in gist show") - raise HTTPNotFound() - if format == 'raw': - content = '\n\n'.join([f.content for f in c.files - if (f_path is None or f.path == f_path)]) - response.content_type = 'text/plain' - return content - return render('admin/gists/show.mako') - - @LoginRequired() - @NotAnonymous() - @auth.CSRFRequired() - def edit(self, gist_id): - self.__load_defaults() - self._add_gist_to_context(gist_id) - - owner = c.gist.gist_owner == c.rhodecode_user.user_id - if not (h.HasPermissionAny('hg.admin')() or owner): - raise HTTPForbidden() - - data = peppercorn.parse(request.POST.items()) - - schema = gist_schema.GistSchema() - schema = schema.bind( - # '0' is special value to leave lifetime untouched - lifetime_options=[x[0] for x in c.lifetime_values] + [0], - ) - - try: - schema_data = schema.deserialize(data) - # convert to safer format with just KEYs so we sure no duplicates - schema_data['nodes'] = gist_schema.sequence_to_nodes( - schema_data['nodes']) - - GistModel().update( - gist=c.gist, - description=schema_data['description'], - owner=c.gist.owner, - gist_mapping=schema_data['nodes'], - lifetime=schema_data['lifetime'], - gist_acl_level=schema_data['gist_acl_level'] - ) - - Session().commit() - h.flash(_('Successfully updated gist content'), category='success') - except NodeNotChangedError: - # raised if nothing was changed in repo itself. We anyway then - # store only DB stuff for gist - Session().commit() - h.flash(_('Successfully updated gist data'), category='success') - except validation_schema.Invalid as errors: - errors = errors.asdict() - h.flash(_('Error occurred during update of gist {}: {}').format( - gist_id, errors), category='error') - except Exception: - log.exception("Exception in gist edit") - h.flash(_('Error occurred during update of gist %s') % gist_id, - category='error') - - return redirect(url('gist', gist_id=gist_id)) - - @LoginRequired() - @NotAnonymous() - def edit_form(self, gist_id): - translate = _ = c.pyramid_request.translate - - """GET /admin/gists/gist_id/edit: Form to edit an existing item""" - # url('edit_gist', gist_id=ID) - self._add_gist_to_context(gist_id) - - owner = c.gist.gist_owner == c.rhodecode_user.user_id - if not (h.HasPermissionAny('hg.admin')() or owner): - raise HTTPForbidden() - - try: - c.file_last_commit, c.files = GistModel().get_gist_files(gist_id) - except VCSError: - log.exception("Exception in gist edit") - raise HTTPNotFound() - - if c.gist.gist_expires == -1: - expiry = _('never') - else: - # this cannot use timeago, since it's used in select2 as a value - expiry = h.age(h.time_to_datetime(c.gist.gist_expires)) - - expiry = translate(expiry) - self.__load_defaults( - extra_values=(0, _('%(expiry)s - current value') % {'expiry': expiry})) - return render('admin/gists/edit.mako') - - @LoginRequired() - @NotAnonymous() - @jsonify - def check_revision(self, gist_id): - c.gist = Gist.get_or_404(gist_id) - last_rev = c.gist.scm_instance().get_commit() - success = True - revision = request.GET.get('revision') - - ##TODO: maybe move this to model ? - if revision != last_rev.raw_id: - log.error('Last revision %s is different then submitted %s' - % (revision, last_rev)) - # our gist has newer version than we - success = False - - return {'success': success} diff --git a/rhodecode/lib/middleware/csrf.py b/rhodecode/lib/middleware/csrf.py --- a/rhodecode/lib/middleware/csrf.py +++ b/rhodecode/lib/middleware/csrf.py @@ -67,15 +67,25 @@ class CSRFDetector(object): '/error/document', )) + _SKIP_PATTERN = frozenset(( + '/_admin/gists/', + )) + def __init__(self, app): self._app = app def __call__(self, environ, start_response): if environ['REQUEST_METHOD'].upper() not in ('GET', 'POST'): raise Exception(self._PUT_DELETE_MESSAGE) + token_expected = environ['PATH_INFO'] not in self._PATHS_WITHOUT_TOKEN + allowed = True + for pattern in self._SKIP_PATTERN: + if environ['PATH_INFO'].startswith(pattern): + allowed = False + break if (environ['REQUEST_METHOD'] == 'POST' and - environ['PATH_INFO'] not in self._PATHS_WITHOUT_TOKEN and + token_expected and allowed and routes.middleware.is_form_post(environ)): body = environ['wsgi.input'] if body.seekable(): diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -3788,14 +3788,8 @@ class Gist(Base, BaseModel): return cls.query().filter(cls.gist_access_id == gist_access_id).scalar() def gist_url(self): - import rhodecode - from pylons import url - - 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) + from rhodecode.model.gist import GistModel + return GistModel().get_url(self) @classmethod def base_path(cls): diff --git a/rhodecode/model/gist.py b/rhodecode/model/gist.py --- a/rhodecode/model/gist.py +++ b/rhodecode/model/gist.py @@ -28,6 +28,8 @@ import logging import traceback import shutil +from pyramid.threadlocal import get_current_request + from rhodecode.lib.utils2 import ( safe_unicode, unique_id, safe_int, time_to_datetime, AttributeDict) from rhodecode.lib.ext_json import json @@ -233,3 +235,16 @@ class GistModel(BaseModel): ) return gist + + def get_url(self, gist, request=None): + import rhodecode + + if not request: + request = get_current_request() + + alias_url = rhodecode.CONFIG.get('gist_alias_url') + if alias_url: + return alias_url.replace('{gistid}', gist.gist_access_id) + + return request.route_url('gist_show', gist_id=gist.gist_access_id) + diff --git a/rhodecode/public/js/rhodecode/base/keyboard-bindings.js b/rhodecode/public/js/rhodecode/base/keyboard-bindings.js --- a/rhodecode/public/js/rhodecode/base/keyboard-bindings.js +++ b/rhodecode/public/js/rhodecode/base/keyboard-bindings.js @@ -46,13 +46,13 @@ function setRCMouseBindings(repoName, re window.location = pyroutes.url('home'); }); Mousetrap.bind(['g g'], function(e) { - window.location = pyroutes.url('gists', {'private': 1}); + window.location = pyroutes.url('gists_show', {'private': 1}); }); Mousetrap.bind(['g G'], function(e) { - window.location = pyroutes.url('gists', {'public': 1}); + window.location = pyroutes.url('gists_show', {'public': 1}); }); Mousetrap.bind(['n g'], function(e) { - window.location = pyroutes.url('new_gist'); + window.location = pyroutes.url('gists_new'); }); Mousetrap.bind(['n r'], function(e) { window.location = pyroutes.url('new_repo'); diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -15,8 +15,6 @@ function registerRCRoutes() { pyroutes.register('new_repo', '/_admin/create_repository', []); pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']); pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']); - pyroutes.register('gists', '/_admin/gists', []); - pyroutes.register('new_gist', '/_admin/gists/new', []); pyroutes.register('toggle_following', '/_admin/toggle_following', []); pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']); pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']); @@ -152,5 +150,16 @@ function registerRCRoutes() { pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []); pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []); pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []); + pyroutes.register('gists_show', '/_admin/gists', []); + pyroutes.register('gists_new', '/_admin/gists/new', []); + pyroutes.register('gists_create', '/_admin/gists/create', []); + pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']); + pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']); + pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']); + pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']); + pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']); + pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']); + pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']); + pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']); pyroutes.register('apiv2', '/_admin/api', []); } diff --git a/rhodecode/templates/admin/gists/edit.mako b/rhodecode/templates/admin/gists/edit.mako --- a/rhodecode/templates/admin/gists/edit.mako +++ b/rhodecode/templates/admin/gists/edit.mako @@ -26,7 +26,7 @@
- ${h.secure_form(h.url('edit_gist', gist_id=c.gist.gist_access_id), method='post', id='eform')} + ${h.secure_form(h.route_path('gist_update', gist_id=c.gist.gist_access_id), id='eform', method='POST')}
diff --git a/rhodecode/templates/admin/gists/show.mako b/rhodecode/templates/admin/gists/show.mako --- a/rhodecode/templates/admin/gists/show.mako +++ b/rhodecode/templates/admin/gists/show.mako @@ -18,7 +18,6 @@ <%def name="breadcrumbs_links()"> ${_('Gist')} · ${c.gist.gist_access_id} - / ${_('URL')}: ${c.gist.gist_url()} <%def name="menu_bar_nav()"> @@ -33,11 +32,12 @@ %if c.rhodecode_user.username != h.DEFAULT_USER: %endif
+ ${c.gist.gist_url()}
@@ -45,7 +45,7 @@
%if h.HasPermissionAny('hg.admin')() or c.gist.gist_owner == c.rhodecode_user.user_id:
- ${h.secure_form(url('gist', gist_id=c.gist.gist_access_id),method='delete')} + ${h.secure_form(h.route_path('gist_delete', gist_id=c.gist.gist_access_id), method='POST')} ${h.submit('remove_gist', _('Delete'),class_="btn btn-mini btn-danger",onclick="return confirm('"+_('Confirm to delete this Gist')+"');")} ${h.end_form()}
@@ -53,9 +53,9 @@
## only owner should see that %if h.HasPermissionAny('hg.admin')() or c.gist.gist_owner == c.rhodecode_user.user_id: - ${h.link_to(_('Edit'),h.url('edit_gist', gist_id=c.gist.gist_access_id),class_="btn btn-mini")} + ${h.link_to(_('Edit'), h.route_path('gist_edit', gist_id=c.gist.gist_access_id), class_="btn btn-mini")} %endif - ${h.link_to(_('Show as Raw'),h.url('formatted_gist', gist_id=c.gist.gist_access_id, format='raw'),class_="btn btn-mini")} + ${h.link_to(_('Show as Raw'), h.route_path('gist_show_formatted', gist_id=c.gist.gist_access_id, revision='tip', format='raw'), class_="btn btn-mini")}
%if c.gist.gist_type != 'public': @@ -78,19 +78,21 @@
-
${h.urlify_commit_message(c.file_last_commit.message,c.repo_name)}
+
${h.urlify_commit_message(c.file_last_commit.message, None)}
## iterate over the files % for file in c.files: <% renderer = c.render and h.renderer_from_filename(file.path, exclude=['.txt', '.TXT'])%> - +
+ -->
%if renderer: ${h.render(file.content, renderer=renderer)} diff --git a/rhodecode/templates/base/base.mako b/rhodecode/templates/base/base.mako --- a/rhodecode/templates/base/base.mako +++ b/rhodecode/templates/base/base.mako @@ -397,7 +397,7 @@ %endif
  • - +
  • diff --git a/rhodecode/templates/data_table/_dt_elements.mako b/rhodecode/templates/data_table/_dt_elements.mako --- a/rhodecode/templates/data_table/_dt_elements.mako +++ b/rhodecode/templates/data_table/_dt_elements.mako @@ -241,7 +241,7 @@ <%def name="gist_access_id(gist_access_id, full_contact)">