##// END OF EJS Templates
repository-groups: use lazy loaded admin dashboard
marcink -
r3623:58c253a3 default
parent child Browse files
Show More
@@ -0,0 +1,54 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4
5 from alembic.migration import MigrationContext
6 from alembic.operations import Operations
7 from sqlalchemy import String, Column
8 from sqlalchemy.sql import text
9
10 from rhodecode.lib.dbmigrate.versions import _reset_base
11 from rhodecode.model import meta, init_model_encryption
12 from rhodecode.model.db import RepoGroup
13
14
15 log = logging.getLogger(__name__)
16
17
18 def upgrade(migrate_engine):
19 """
20 Upgrade operations go here.
21 Don't create your own engine; bind migrate_engine to your metadata
22 """
23 _reset_base(migrate_engine)
24 from rhodecode.lib.dbmigrate.schema import db_4_16_0_2
25
26 init_model_encryption(db_4_16_0_2)
27
28 context = MigrationContext.configure(migrate_engine.connect())
29 op = Operations(context)
30
31 repo_group = db_4_16_0_2.RepoGroup.__table__
32
33 with op.batch_alter_table(repo_group.name) as batch_op:
34 batch_op.add_column(
35 Column("repo_group_name_hash", String(1024), nullable=True, unique=False))
36
37 _generate_repo_group_name_hashes(db_4_16_0_2, op, meta.Session)
38
39
40 def downgrade(migrate_engine):
41 pass
42
43
44 def _generate_repo_group_name_hashes(models, op, session):
45 repo_groups = models.RepoGroup.get_all()
46 for repo_group in repo_groups:
47 print(repo_group.group_name)
48 hash_ = RepoGroup.hash_repo_group_name(repo_group.group_name)
49 params = {'hash': hash_, 'id': repo_group.group_id}
50 query = text(
51 'UPDATE groups SET repo_group_name_hash = :hash'
52 ' WHERE group_id = :id').bindparams(**params)
53 op.execute(query)
54 session().commit()
@@ -0,0 +1,39 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4
5 from alembic.migration import MigrationContext
6 from alembic.operations import Operations
7
8 from rhodecode.lib.dbmigrate.versions import _reset_base
9 from rhodecode.model import init_model_encryption
10
11
12 log = logging.getLogger(__name__)
13
14
15 def upgrade(migrate_engine):
16 """
17 Upgrade operations go here.
18 Don't create your own engine; bind migrate_engine to your metadata
19 """
20 _reset_base(migrate_engine)
21 from rhodecode.lib.dbmigrate.schema import db_4_16_0_2
22
23 init_model_encryption(db_4_16_0_2)
24
25 context = MigrationContext.configure(migrate_engine.connect())
26 op = Operations(context)
27
28 repo_group = db_4_16_0_2.RepoGroup.__table__
29
30 with op.batch_alter_table(repo_group.name) as batch_op:
31 batch_op.alter_column("repo_group_name_hash", nullable=False)
32
33
34 def downgrade(migrate_engine):
35 pass
36
37
38 def _generate_repo_group_name_hashes(models, op, session):
39 pass
@@ -45,7 +45,7 b' PYRAMID_SETTINGS = {}'
45 45 EXTENSIONS = {}
46 46
47 47 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
48 __dbversion__ = 95 # defines current db version for migrations
48 __dbversion__ = 97 # defines current db version for migrations
49 49 __platform__ = platform.system()
50 50 __license__ = 'AGPLv3, and Commercial License'
51 51 __author__ = 'RhodeCode GmbH'
@@ -424,6 +424,10 b' def admin_routes(config):'
424 424 pattern='/repo_groups')
425 425
426 426 config.add_route(
427 name='repo_groups_data',
428 pattern='/repo_groups_data')
429
430 config.add_route(
427 431 name='repo_group_new',
428 432 pattern='/repo_group/new')
429 433
@@ -23,11 +23,11 b' import pytest'
23 23
24 24 from rhodecode.apps._base import ADMIN_PREFIX
25 25 from rhodecode.lib import helpers as h
26 from rhodecode.model.db import Repository, UserRepoToPerm, User
26 from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.model.repo_group import RepoGroupModel
29 29 from rhodecode.tests import (
30 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH, TestController)
30 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
31 31 from rhodecode.tests.fixture import Fixture
32 32
33 33 fixture = Fixture()
@@ -38,6 +38,7 b' def route_path(name, params=None, **kwar'
38 38
39 39 base_url = {
40 40 'repo_groups': ADMIN_PREFIX + '/repo_groups',
41 'repo_groups_data': ADMIN_PREFIX + '/repo_groups_data',
41 42 'repo_group_new': ADMIN_PREFIX + '/repo_group/new',
42 43 'repo_group_create': ADMIN_PREFIX + '/repo_group/create',
43 44
@@ -59,13 +60,30 b' def _get_permission_for_user(user, repo)'
59 60
60 61 @pytest.mark.usefixtures("app")
61 62 class TestAdminRepositoryGroups(object):
63
62 64 def test_show_repo_groups(self, autologin_user):
63 response = self.app.get(route_path('repo_groups'))
64 response.mustcontain('data: []')
65 self.app.get(route_path('repo_groups'))
66
67 def test_show_repo_groups_data(self, autologin_user, xhr_header):
68 response = self.app.get(route_path(
69 'repo_groups_data'), extra_environ=xhr_header)
70
71 all_repo_groups = RepoGroup.query().count()
72 assert response.json['recordsTotal'] == all_repo_groups
65 73
66 def test_show_repo_groups_after_creating_group(self, autologin_user):
74 def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
75 response = self.app.get(route_path(
76 'repo_groups_data', params={'search[value]': 'empty_search'}),
77 extra_environ=xhr_header)
78
79 all_repo_groups = RepoGroup.query().count()
80 assert response.json['recordsTotal'] == all_repo_groups
81 assert response.json['recordsFiltered'] == 0
82
83 def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
67 84 fixture.create_repo_group('test_repo_group')
68 response = self.app.get(route_path('repo_groups'))
85 response = self.app.get(route_path(
86 'repo_groups_data'), extra_environ=xhr_header)
69 87 response.mustcontain('"name_raw": "test_repo_group"')
70 88 fixture.destroy_repo_group('test_repo_group')
71 89
@@ -17,7 +17,7 b''
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20 import datetime
21 21 import logging
22 22 import formencode
23 23 import formencode.htmlfill
@@ -30,16 +30,16 b' from pyramid.response import Response'
30 30 from rhodecode import events
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32
33 from rhodecode.lib.ext_json import json
34 33 from rhodecode.lib.auth import (
35 34 LoginRequired, CSRFRequired, NotAnonymous,
36 35 HasPermissionAny, HasRepoGroupPermissionAny)
37 36 from rhodecode.lib import helpers as h, audit_logger
38 from rhodecode.lib.utils2 import safe_int, safe_unicode
37 from rhodecode.lib.utils2 import safe_int, safe_unicode, datetime_to_time
39 38 from rhodecode.model.forms import RepoGroupForm
40 39 from rhodecode.model.repo_group import RepoGroupModel
41 40 from rhodecode.model.scm import RepoGroupList
42 from rhodecode.model.db import Session, RepoGroup
41 from rhodecode.model.db import (
42 or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
43 43
44 44 log = logging.getLogger(__name__)
45 45
@@ -88,22 +88,168 b' class AdminRepoGroupsView(BaseAppView, D'
88 88 return False
89 89 return False
90 90
91 # permission check in data loading of
92 # `repo_group_list_data` via RepoGroupList
91 93 @LoginRequired()
92 94 @NotAnonymous()
93 # perms check inside
94 95 @view_config(
95 96 route_name='repo_groups', request_method='GET',
96 97 renderer='rhodecode:templates/admin/repo_groups/repo_groups.mako')
97 98 def repo_group_list(self):
98 99 c = self.load_default_context()
100 return self._get_template_context(c)
99 101
100 repo_group_list = RepoGroup.get_all_repo_groups()
101 repo_group_list_acl = RepoGroupList(
102 repo_group_list, perm_set=['group.admin'])
103 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
104 repo_group_list=repo_group_list_acl, admin=True)
105 c.data = json.dumps(repo_group_data)
106 return self._get_template_context(c)
102 # permission check inside
103 @LoginRequired()
104 @NotAnonymous()
105 @view_config(
106 route_name='repo_groups_data', request_method='GET',
107 renderer='json_ext', xhr=True)
108 def repo_group_list_data(self):
109 self.load_default_context()
110 column_map = {
111 'name_raw': 'group_name_hash',
112 'desc': 'group_description',
113 'last_change_raw': 'updated_on',
114 'top_level_repos': 'repos_total',
115 'owner': 'user_username',
116 }
117 draw, start, limit = self._extract_chunk(self.request)
118 search_q, order_by, order_dir = self._extract_ordering(
119 self.request, column_map=column_map)
120
121 _render = self.request.get_partial_renderer(
122 'rhodecode:templates/data_table/_dt_elements.mako')
123 c = _render.get_call_context()
124
125 def quick_menu(repo_group_name):
126 return _render('quick_repo_group_menu', repo_group_name)
127
128 def repo_group_lnk(repo_group_name):
129 return _render('repo_group_name', repo_group_name)
130
131 def last_change(last_change):
132 if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
133 delta = datetime.timedelta(
134 seconds=(datetime.datetime.now() - datetime.datetime.utcnow()).seconds)
135 last_change = last_change + delta
136 return _render("last_change", last_change)
137
138 def desc(desc, personal):
139 return _render(
140 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
141
142 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
143 return _render(
144 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
145
146 def user_profile(username):
147 return _render('user_profile', username)
148
149 auth_repo_group_list = RepoGroupList(
150 RepoGroup.query().all(), perm_set=['group.admin'])
151
152 allowed_ids = [-1]
153 for repo_group in auth_repo_group_list:
154 allowed_ids.append(repo_group.group_id)
155
156 repo_groups_data_total_count = RepoGroup.query()\
157 .filter(or_(
158 # generate multiple IN to fix limitation problems
159 *in_filter_generator(RepoGroup.group_id, allowed_ids)
160 )) \
161 .count()
162
163 repo_groups_data_total_inactive_count = RepoGroup.query()\
164 .filter(RepoGroup.group_id.in_(allowed_ids))\
165 .count()
166
167 repo_count = count(Repository.repo_id)
168 base_q = Session.query(
169 RepoGroup.group_name,
170 RepoGroup.group_name_hash,
171 RepoGroup.group_description,
172 RepoGroup.group_id,
173 RepoGroup.personal,
174 RepoGroup.updated_on,
175 User,
176 repo_count.label('repos_count')
177 ) \
178 .filter(or_(
179 # generate multiple IN to fix limitation problems
180 *in_filter_generator(RepoGroup.group_id, allowed_ids)
181 )) \
182 .outerjoin(Repository) \
183 .join(User, User.user_id == RepoGroup.user_id) \
184 .group_by(RepoGroup, User)
185
186 if search_q:
187 like_expression = u'%{}%'.format(safe_unicode(search_q))
188 base_q = base_q.filter(or_(
189 RepoGroup.group_name.ilike(like_expression),
190 ))
191
192 repo_groups_data_total_filtered_count = base_q.count()
193 # the inactive isn't really used, but we still make it same as other data grids
194 # which use inactive (users,user groups)
195 repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
196
197 sort_defined = False
198 if order_by == 'group_name':
199 sort_col = func.lower(RepoGroup.group_name)
200 sort_defined = True
201 elif order_by == 'repos_total':
202 sort_col = repo_count
203 sort_defined = True
204 elif order_by == 'user_username':
205 sort_col = User.username
206 else:
207 sort_col = getattr(RepoGroup, order_by, None)
208
209 if sort_defined or sort_col:
210 if order_dir == 'asc':
211 sort_col = sort_col.asc()
212 else:
213 sort_col = sort_col.desc()
214
215 base_q = base_q.order_by(sort_col)
216 base_q = base_q.offset(start).limit(limit)
217
218 # authenticated access to user groups
219 auth_repo_group_list = base_q.all()
220
221 repo_groups_data = []
222 for repo_gr in auth_repo_group_list:
223 row = {
224 "menu": quick_menu(repo_gr.group_name),
225 "name": repo_group_lnk(repo_gr.group_name),
226 "name_raw": repo_gr.group_name,
227 "last_change": last_change(repo_gr.updated_on),
228 "last_change_raw": datetime_to_time(repo_gr.updated_on),
229
230 "last_changeset": "",
231 "last_changeset_raw": "",
232
233 "desc": desc(repo_gr.group_description, repo_gr.personal),
234 "owner": user_profile(repo_gr.User.username),
235 "top_level_repos": repo_gr.repos_count,
236 "action": repo_group_actions(
237 repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
238
239 }
240
241 repo_groups_data.append(row)
242
243 data = ({
244 'draw': draw,
245 'data': repo_groups_data,
246 'recordsTotal': repo_groups_data_total_count,
247 'recordsTotalInactive': repo_groups_data_total_inactive_count,
248 'recordsFiltered': repo_groups_data_total_filtered_count,
249 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
250 })
251
252 return data
107 253
108 254 @LoginRequired()
109 255 @NotAnonymous()
@@ -25,6 +25,7 b' Database Models for RhodeCode Enterprise'
25 25 import re
26 26 import os
27 27 import time
28 import string
28 29 import hashlib
29 30 import logging
30 31 import datetime
@@ -50,6 +51,7 b' from sqlalchemy.dialects.mysql import LO'
50 51 from zope.cachedescriptors.property import Lazy as LazyProperty
51 52 from pyramid import compat
52 53 from pyramid.threadlocal import get_current_request
54 from webhelpers.text import collapse, remove_formatting
53 55
54 56 from rhodecode.translation import _
55 57 from rhodecode.lib.vcs import get_vcs_instance
@@ -2469,7 +2471,8 b' class RepoGroup(Base, BaseModel):'
2469 2471 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2470 2472
2471 2473 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2472 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2474 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2475 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2473 2476 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2474 2477 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2475 2478 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
@@ -2492,6 +2495,15 b' class RepoGroup(Base, BaseModel):'
2492 2495 return u"<%s('id:%s:%s')>" % (
2493 2496 self.__class__.__name__, self.group_id, self.group_name)
2494 2497
2498 @hybrid_property
2499 def group_name(self):
2500 return self._group_name
2501
2502 @group_name.setter
2503 def group_name(self, value):
2504 self._group_name = value
2505 self.group_name_hash = self.hash_repo_group_name(value)
2506
2495 2507 @validates('group_parent_id')
2496 2508 def validate_group_parent_id(self, key, val):
2497 2509 """
@@ -2508,6 +2520,18 b' class RepoGroup(Base, BaseModel):'
2508 2520 return h.escape(self.group_description)
2509 2521
2510 2522 @classmethod
2523 def hash_repo_group_name(cls, repo_group_name):
2524 val = remove_formatting(repo_group_name)
2525 val = safe_str(val).lower()
2526 chars = []
2527 for c in val:
2528 if c not in string.ascii_letters:
2529 c = str(ord(c))
2530 chars.append(c)
2531
2532 return ''.join(chars)
2533
2534 @classmethod
2511 2535 def _generate_choice(cls, repo_group):
2512 2536 from webhelpers.html import literal as _literal
2513 2537 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
@@ -2770,6 +2794,13 b' class RepoGroup(Base, BaseModel):'
2770 2794 }
2771 2795 return data
2772 2796
2797 def get_dict(self):
2798 # Since we transformed `group_name` to a hybrid property, we need to
2799 # keep compatibility with the code which uses `group_name` field.
2800 result = super(RepoGroup, self).get_dict()
2801 result['group_name'] = result.pop('_group_name', None)
2802 return result
2803
2773 2804
2774 2805 class Permission(Base, BaseModel):
2775 2806 __tablename__ = 'permissions'
@@ -10,7 +10,7 b''
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
13 ${h.link_to(_('Admin'),h.route_path('admin_home'))} &raquo; <span id="repo_group_count">0</span> ${_('repository groups')}
13 ${h.link_to(_('Admin'),h.route_path('admin_home'))} &raquo; <span id="repo_group_count">0</span>
14 14 </%def>
15 15
16 16 <%def name="menu_bar_nav()">
@@ -36,15 +36,32 b''
36 36
37 37 <script>
38 38 $(document).ready(function() {
39
40 var get_datatable_count = function(){
41 var api = $('#group_list_table').dataTable().api();
42 $('#repo_group_count').text(api.page.info().recordsDisplay);
43 };
39 var $repoGroupsListTable = $('#group_list_table');
44 40
45 41 // repo group list
46 $('#group_list_table').DataTable({
47 data: ${c.data|n},
42 $repoGroupsListTable.DataTable({
43 processing: true,
44 serverSide: true,
45 ajax: {
46 "url": "${h.route_path('repo_groups_data')}",
47 "dataSrc": function (json) {
48 var filteredCount = json.recordsFiltered;
49 var filteredInactiveCount = json.recordsFilteredInactive;
50 var totalInactive = json.recordsTotalInactive;
51 var total = json.recordsTotal;
52
53 var _text = _gettext(
54 "{0} of {1} repository groups").format(
55 filteredCount, total);
56
57 if (total === filteredCount) {
58 _text = _gettext("{0} repository groups").format(total);
59 }
60 $('#repo_group_count').text(_text);
61 return json.data;
62 },
63 },
64
48 65 dom: 'rtp',
49 66 pageLength: ${c.visual.admin_grid_items},
50 67 order: [[ 0, "asc" ]],
@@ -62,36 +79,34 b''
62 79 { data: {"_": "owner",
63 80 "sort": "owner"}, title: "${_('Owner')}", className: "td-user" },
64 81 { data: {"_": "action",
65 "sort": "action"}, title: "${_('Action')}", className: "td-action" }
82 "sort": "action"}, title: "${_('Action')}", className: "td-action", orderable: false }
66 83 ],
67 84 language: {
68 85 paginate: DEFAULT_GRID_PAGINATION,
86 sProcessing: _gettext('loading...'),
69 87 emptyTable: _gettext("No repository groups available yet.")
70 88 },
71 "initComplete": function( settings, json ) {
72 get_datatable_count();
73 quick_repo_menu();
74 }
75 89 });
76 90
77 // update the counter when doing search
78 $('#group_list_table').on( 'search.dt', function (e,settings) {
79 get_datatable_count();
91 $repoGroupsListTable.on('xhr.dt', function(e, settings, json, xhr){
92 $repoGroupsListTable.css('opacity', 1);
93 });
94
95 $repoGroupsListTable.on('preXhr.dt', function(e, settings, data){
96 $repoGroupsListTable.css('opacity', 0.3);
80 97 });
81 98
82 // filter, filter both grids
83 $('#q_filter').on( 'keyup', function () {
99 // filter
100 $('#q_filter').on('keyup',
101 $.debounce(250, function() {
102 $repoGroupsListTable.DataTable().search(
103 $('#q_filter').val()
104 ).draw();
105 })
106 );
107 });
84 108
85 var repo_group_api = $('#group_list_table').dataTable().api();
86 repo_group_api
87 .columns(0)
88 .search(this.value)
89 .draw();
90 });
109 </script>
91 110
92 // refilter table if page load via back button
93 $("#q_filter").trigger('keyup');
94 });
95 </script>
96 111 </%def>
97 112
@@ -66,7 +66,7 b''
66 66 { data: {"_": "state",
67 67 "sort": "state"}, title: "${_('State')}", className: "td-tags td-state" },
68 68 { data: {"_": "action",
69 "sort": "action"}, title: "${_('Action')}", className: "td-action" }
69 "sort": "action"}, title: "${_('Action')}", className: "td-action", orderable: false }
70 70 ],
71 71 language: {
72 72 paginate: DEFAULT_GRID_PAGINATION,
@@ -34,6 +34,8 b' def route_path(name, params=None, **kwar'
34 34 ADMIN_PREFIX + '/repos',
35 35 'repo_groups':
36 36 ADMIN_PREFIX + '/repo_groups',
37 'repo_groups_data':
38 ADMIN_PREFIX + '/repo_groups_data',
37 39 'user_groups':
38 40 ADMIN_PREFIX + '/user_groups',
39 41 'user_groups_data':
@@ -67,8 +69,9 b' class TestAdminDelegatedUser(TestControl'
67 69 response = self.app.get(route_path('repos'), status=200)
68 70 response.mustcontain('data: []')
69 71
70 response = self.app.get(route_path('repo_groups'), status=200)
71 response.mustcontain('data: []')
72 response = self.app.get(route_path('repo_groups_data'),
73 status=200, extra_environ=xhr_header)
74 assert response.json['data'] == []
72 75
73 76 response = self.app.get(route_path('user_groups_data'),
74 77 status=200, extra_environ=xhr_header)
@@ -102,7 +105,8 b' class TestAdminDelegatedUser(TestControl'
102 105 response = self.app.get(route_path('repos'), status=200)
103 106 response.mustcontain('"name_raw": "{}"'.format(repo_name))
104 107
105 response = self.app.get(route_path('repo_groups'), status=200)
108 response = self.app.get(route_path('repo_groups_data'),
109 extra_environ=xhr_header, status=200)
106 110 response.mustcontain('"name_raw": "{}"'.format(repo_group_name))
107 111
108 112 response = self.app.get(route_path('user_groups_data'),
@@ -144,7 +148,8 b' class TestAdminDelegatedUser(TestControl'
144 148 response = self.app.get(route_path('repos'), status=200)
145 149 response.mustcontain('"name_raw": "{}"'.format(repo_name))
146 150
147 response = self.app.get(route_path('repo_groups'), status=200)
151 response = self.app.get(route_path('repo_groups_data'),
152 extra_environ=xhr_header, status=200)
148 153 response.mustcontain('"name_raw": "{}"'.format(repo_group_name))
149 154
150 155 response = self.app.get(route_path('user_groups_data'),
General Comments 0
You need to be logged in to leave comments. Login now