##// END OF EJS Templates
py3: drop .keys when we don't need them...
Mads Kiilerich -
r7956:bdb79ef2 default
parent child Browse files
Show More
@@ -1,157 +1,157 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.changelog
15 kallithea.controllers.changelog
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 changelog controller for Kallithea
18 changelog controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 21, 2010
22 :created_on: Apr 21, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30
30
31 from tg import request, session
31 from tg import request, session
32 from tg import tmpl_context as c
32 from tg import tmpl_context as c
33 from tg.i18n import ugettext as _
33 from tg.i18n import ugettext as _
34 from webob.exc import HTTPBadRequest, HTTPFound, HTTPNotFound
34 from webob.exc import HTTPBadRequest, HTTPFound, HTTPNotFound
35
35
36 import kallithea.lib.helpers as h
36 import kallithea.lib.helpers as h
37 from kallithea.config.routing import url
37 from kallithea.config.routing import url
38 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
38 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
39 from kallithea.lib.base import BaseRepoController, render
39 from kallithea.lib.base import BaseRepoController, render
40 from kallithea.lib.graphmod import graph_data
40 from kallithea.lib.graphmod import graph_data
41 from kallithea.lib.page import Page
41 from kallithea.lib.page import Page
42 from kallithea.lib.utils2 import safe_int
42 from kallithea.lib.utils2 import safe_int
43 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, ChangesetError, EmptyRepositoryError, NodeDoesNotExistError, RepositoryError
43 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, ChangesetError, EmptyRepositoryError, NodeDoesNotExistError, RepositoryError
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class ChangelogController(BaseRepoController):
49 class ChangelogController(BaseRepoController):
50
50
51 def _before(self, *args, **kwargs):
51 def _before(self, *args, **kwargs):
52 super(ChangelogController, self)._before(*args, **kwargs)
52 super(ChangelogController, self)._before(*args, **kwargs)
53 c.affected_files_cut_off = 60
53 c.affected_files_cut_off = 60
54
54
55 @staticmethod
55 @staticmethod
56 def __get_cs(rev, repo):
56 def __get_cs(rev, repo):
57 """
57 """
58 Safe way to get changeset. If error occur fail with error message.
58 Safe way to get changeset. If error occur fail with error message.
59
59
60 :param rev: revision to fetch
60 :param rev: revision to fetch
61 :param repo: repo instance
61 :param repo: repo instance
62 """
62 """
63
63
64 try:
64 try:
65 return c.db_repo_scm_instance.get_changeset(rev)
65 return c.db_repo_scm_instance.get_changeset(rev)
66 except EmptyRepositoryError as e:
66 except EmptyRepositoryError as e:
67 h.flash(_('There are no changesets yet'), category='error')
67 h.flash(_('There are no changesets yet'), category='error')
68 except RepositoryError as e:
68 except RepositoryError as e:
69 log.error(traceback.format_exc())
69 log.error(traceback.format_exc())
70 h.flash(e, category='error')
70 h.flash(e, category='error')
71 raise HTTPBadRequest()
71 raise HTTPBadRequest()
72
72
73 @LoginRequired(allow_default_user=True)
73 @LoginRequired(allow_default_user=True)
74 @HasRepoPermissionLevelDecorator('read')
74 @HasRepoPermissionLevelDecorator('read')
75 def index(self, repo_name, revision=None, f_path=None):
75 def index(self, repo_name, revision=None, f_path=None):
76 limit = 2000
76 limit = 2000
77 default = 100
77 default = 100
78 if request.GET.get('size'):
78 if request.GET.get('size'):
79 c.size = max(min(safe_int(request.GET.get('size')), limit), 1)
79 c.size = max(min(safe_int(request.GET.get('size')), limit), 1)
80 session['changelog_size'] = c.size
80 session['changelog_size'] = c.size
81 session.save()
81 session.save()
82 else:
82 else:
83 c.size = int(session.get('changelog_size', default))
83 c.size = int(session.get('changelog_size', default))
84 # min size must be 1
84 # min size must be 1
85 c.size = max(c.size, 1)
85 c.size = max(c.size, 1)
86 p = safe_int(request.GET.get('page'), 1)
86 p = safe_int(request.GET.get('page'), 1)
87 branch_name = request.GET.get('branch', None)
87 branch_name = request.GET.get('branch', None)
88 if (branch_name and
88 if (branch_name and
89 branch_name not in c.db_repo_scm_instance.branches and
89 branch_name not in c.db_repo_scm_instance.branches and
90 branch_name not in c.db_repo_scm_instance.closed_branches and
90 branch_name not in c.db_repo_scm_instance.closed_branches and
91 not revision
91 not revision
92 ):
92 ):
93 raise HTTPFound(location=url('changelog_file_home', repo_name=c.repo_name,
93 raise HTTPFound(location=url('changelog_file_home', repo_name=c.repo_name,
94 revision=branch_name, f_path=f_path or ''))
94 revision=branch_name, f_path=f_path or ''))
95
95
96 if revision == 'tip':
96 if revision == 'tip':
97 revision = None
97 revision = None
98
98
99 c.changelog_for_path = f_path
99 c.changelog_for_path = f_path
100 try:
100 try:
101
101
102 if f_path:
102 if f_path:
103 log.debug('generating changelog for path %s', f_path)
103 log.debug('generating changelog for path %s', f_path)
104 # get the history for the file !
104 # get the history for the file !
105 tip_cs = c.db_repo_scm_instance.get_changeset()
105 tip_cs = c.db_repo_scm_instance.get_changeset()
106 try:
106 try:
107 collection = tip_cs.get_file_history(f_path)
107 collection = tip_cs.get_file_history(f_path)
108 except (NodeDoesNotExistError, ChangesetError):
108 except (NodeDoesNotExistError, ChangesetError):
109 # this node is not present at tip !
109 # this node is not present at tip !
110 try:
110 try:
111 cs = self.__get_cs(revision, repo_name)
111 cs = self.__get_cs(revision, repo_name)
112 collection = cs.get_file_history(f_path)
112 collection = cs.get_file_history(f_path)
113 except RepositoryError as e:
113 except RepositoryError as e:
114 h.flash(e, category='warning')
114 h.flash(e, category='warning')
115 raise HTTPFound(location=h.url('changelog_home', repo_name=repo_name))
115 raise HTTPFound(location=h.url('changelog_home', repo_name=repo_name))
116 else:
116 else:
117 collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision,
117 collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision,
118 branch_name=branch_name, reverse=True)
118 branch_name=branch_name, reverse=True)
119 c.total_cs = len(collection)
119 c.total_cs = len(collection)
120
120
121 c.cs_pagination = Page(collection, page=p, item_count=c.total_cs, items_per_page=c.size,
121 c.cs_pagination = Page(collection, page=p, item_count=c.total_cs, items_per_page=c.size,
122 branch=branch_name)
122 branch=branch_name)
123
123
124 page_revisions = [x.raw_id for x in c.cs_pagination]
124 page_revisions = [x.raw_id for x in c.cs_pagination]
125 c.cs_comments = c.db_repo.get_comments(page_revisions)
125 c.cs_comments = c.db_repo.get_comments(page_revisions)
126 c.cs_statuses = c.db_repo.statuses(page_revisions)
126 c.cs_statuses = c.db_repo.statuses(page_revisions)
127 except EmptyRepositoryError as e:
127 except EmptyRepositoryError as e:
128 h.flash(e, category='warning')
128 h.flash(e, category='warning')
129 raise HTTPFound(location=url('summary_home', repo_name=c.repo_name))
129 raise HTTPFound(location=url('summary_home', repo_name=c.repo_name))
130 except (RepositoryError, ChangesetDoesNotExistError, Exception) as e:
130 except (RepositoryError, ChangesetDoesNotExistError, Exception) as e:
131 log.error(traceback.format_exc())
131 log.error(traceback.format_exc())
132 h.flash(e, category='error')
132 h.flash(e, category='error')
133 raise HTTPFound(location=url('changelog_home', repo_name=c.repo_name))
133 raise HTTPFound(location=url('changelog_home', repo_name=c.repo_name))
134
134
135 c.branch_name = branch_name
135 c.branch_name = branch_name
136 c.branch_filters = [('', _('None'))] + \
136 c.branch_filters = [('', _('None'))] + \
137 [(k, k) for k in c.db_repo_scm_instance.branches.keys()]
137 [(k, k) for k in c.db_repo_scm_instance.branches]
138 if c.db_repo_scm_instance.closed_branches:
138 if c.db_repo_scm_instance.closed_branches:
139 prefix = _('(closed)') + ' '
139 prefix = _('(closed)') + ' '
140 c.branch_filters += [('-', '-')] + \
140 c.branch_filters += [('-', '-')] + \
141 [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches.keys()]
141 [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches]
142 revs = []
142 revs = []
143 if not f_path:
143 if not f_path:
144 revs = [x.revision for x in c.cs_pagination]
144 revs = [x.revision for x in c.cs_pagination]
145 c.jsdata = graph_data(c.db_repo_scm_instance, revs)
145 c.jsdata = graph_data(c.db_repo_scm_instance, revs)
146
146
147 c.revision = revision # requested revision ref
147 c.revision = revision # requested revision ref
148 c.first_revision = c.cs_pagination[0] # pagination is never empty here!
148 c.first_revision = c.cs_pagination[0] # pagination is never empty here!
149 return render('changelog/changelog.html')
149 return render('changelog/changelog.html')
150
150
151 @LoginRequired(allow_default_user=True)
151 @LoginRequired(allow_default_user=True)
152 @HasRepoPermissionLevelDecorator('read')
152 @HasRepoPermissionLevelDecorator('read')
153 def changelog_details(self, cs):
153 def changelog_details(self, cs):
154 if request.environ.get('HTTP_X_PARTIAL_XHR'):
154 if request.environ.get('HTTP_X_PARTIAL_XHR'):
155 c.cs = c.db_repo_scm_instance.get_changeset(cs)
155 c.cs = c.db_repo_scm_instance.get_changeset(cs)
156 return render('changelog/changelog_details.html')
156 return render('changelog/changelog_details.html')
157 raise HTTPNotFound()
157 raise HTTPNotFound()
@@ -1,460 +1,460 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.indexers.daemon
15 kallithea.lib.indexers.daemon
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 A daemon will read from task table and run tasks
18 A daemon will read from task table and run tasks
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Jan 26, 2010
22 :created_on: Jan 26, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28
28
29 import logging
29 import logging
30 import os
30 import os
31 import sys
31 import sys
32 import traceback
32 import traceback
33 from os.path import dirname
33 from os.path import dirname
34 from shutil import rmtree
34 from shutil import rmtree
35 from time import mktime
35 from time import mktime
36
36
37 from whoosh.index import create_in, exists_in, open_dir
37 from whoosh.index import create_in, exists_in, open_dir
38 from whoosh.qparser import QueryParser
38 from whoosh.qparser import QueryParser
39
39
40 from kallithea.config.conf import INDEX_EXTENSIONS, INDEX_FILENAMES
40 from kallithea.config.conf import INDEX_EXTENSIONS, INDEX_FILENAMES
41 from kallithea.lib.indexers import CHGSET_IDX_NAME, CHGSETS_SCHEMA, IDX_NAME, SCHEMA
41 from kallithea.lib.indexers import CHGSET_IDX_NAME, CHGSETS_SCHEMA, IDX_NAME, SCHEMA
42 from kallithea.lib.utils2 import safe_str, safe_unicode
42 from kallithea.lib.utils2 import safe_str, safe_unicode
43 from kallithea.lib.vcs.exceptions import ChangesetError, NodeDoesNotExistError, RepositoryError
43 from kallithea.lib.vcs.exceptions import ChangesetError, NodeDoesNotExistError, RepositoryError
44 from kallithea.model.db import Repository
44 from kallithea.model.db import Repository
45 from kallithea.model.scm import ScmModel
45 from kallithea.model.scm import ScmModel
46
46
47
47
48 # Add location of top level folder to sys.path
48 # Add location of top level folder to sys.path
49 project_path = dirname(dirname(dirname(dirname(os.path.realpath(__file__)))))
49 project_path = dirname(dirname(dirname(dirname(os.path.realpath(__file__)))))
50 sys.path.append(project_path)
50 sys.path.append(project_path)
51
51
52
52
53
53
54
54
55 log = logging.getLogger('whoosh_indexer')
55 log = logging.getLogger('whoosh_indexer')
56
56
57
57
58 class WhooshIndexingDaemon(object):
58 class WhooshIndexingDaemon(object):
59 """
59 """
60 Daemon for atomic indexing jobs
60 Daemon for atomic indexing jobs
61 """
61 """
62
62
63 def __init__(self, indexname=IDX_NAME, index_location=None,
63 def __init__(self, indexname=IDX_NAME, index_location=None,
64 repo_location=None, repo_list=None,
64 repo_location=None, repo_list=None,
65 repo_update_list=None):
65 repo_update_list=None):
66 self.indexname = indexname
66 self.indexname = indexname
67
67
68 self.index_location = index_location
68 self.index_location = index_location
69 if not index_location:
69 if not index_location:
70 raise Exception('You have to provide index location')
70 raise Exception('You have to provide index location')
71
71
72 self.repo_location = repo_location
72 self.repo_location = repo_location
73 if not repo_location:
73 if not repo_location:
74 raise Exception('You have to provide repositories location')
74 raise Exception('You have to provide repositories location')
75
75
76 self.repo_paths = ScmModel().repo_scan(self.repo_location)
76 self.repo_paths = ScmModel().repo_scan(self.repo_location)
77
77
78 # filter repo list
78 # filter repo list
79 if repo_list:
79 if repo_list:
80 # Fix non-ascii repo names to unicode
80 # Fix non-ascii repo names to unicode
81 repo_list = set(safe_unicode(repo_name) for repo_name in repo_list)
81 repo_list = set(safe_unicode(repo_name) for repo_name in repo_list)
82 self.filtered_repo_paths = {}
82 self.filtered_repo_paths = {}
83 for repo_name, repo in self.repo_paths.items():
83 for repo_name, repo in self.repo_paths.items():
84 if repo_name in repo_list:
84 if repo_name in repo_list:
85 self.filtered_repo_paths[repo_name] = repo
85 self.filtered_repo_paths[repo_name] = repo
86
86
87 self.repo_paths = self.filtered_repo_paths
87 self.repo_paths = self.filtered_repo_paths
88
88
89 # filter update repo list
89 # filter update repo list
90 self.filtered_repo_update_paths = {}
90 self.filtered_repo_update_paths = {}
91 if repo_update_list:
91 if repo_update_list:
92 self.filtered_repo_update_paths = {}
92 self.filtered_repo_update_paths = {}
93 for repo_name, repo in self.repo_paths.items():
93 for repo_name, repo in self.repo_paths.items():
94 if repo_name in repo_update_list:
94 if repo_name in repo_update_list:
95 self.filtered_repo_update_paths[repo_name] = repo
95 self.filtered_repo_update_paths[repo_name] = repo
96 self.repo_paths = self.filtered_repo_update_paths
96 self.repo_paths = self.filtered_repo_update_paths
97
97
98 self.initial = True
98 self.initial = True
99 if not os.path.isdir(self.index_location):
99 if not os.path.isdir(self.index_location):
100 os.makedirs(self.index_location)
100 os.makedirs(self.index_location)
101 log.info('Cannot run incremental index since it does not '
101 log.info('Cannot run incremental index since it does not '
102 'yet exist - running full build')
102 'yet exist - running full build')
103 elif not exists_in(self.index_location, IDX_NAME):
103 elif not exists_in(self.index_location, IDX_NAME):
104 log.info('Running full index build, as the file content '
104 log.info('Running full index build, as the file content '
105 'index does not exist')
105 'index does not exist')
106 elif not exists_in(self.index_location, CHGSET_IDX_NAME):
106 elif not exists_in(self.index_location, CHGSET_IDX_NAME):
107 log.info('Running full index build, as the changeset '
107 log.info('Running full index build, as the changeset '
108 'index does not exist')
108 'index does not exist')
109 else:
109 else:
110 self.initial = False
110 self.initial = False
111
111
112 def _get_index_revision(self, repo):
112 def _get_index_revision(self, repo):
113 db_repo = Repository.get_by_repo_name(safe_unicode(repo.name))
113 db_repo = Repository.get_by_repo_name(safe_unicode(repo.name))
114 landing_rev = 'tip'
114 landing_rev = 'tip'
115 if db_repo:
115 if db_repo:
116 _rev_type, _rev = db_repo.landing_rev
116 _rev_type, _rev = db_repo.landing_rev
117 landing_rev = _rev
117 landing_rev = _rev
118 return landing_rev
118 return landing_rev
119
119
120 def _get_index_changeset(self, repo, index_rev=None):
120 def _get_index_changeset(self, repo, index_rev=None):
121 if not index_rev:
121 if not index_rev:
122 index_rev = self._get_index_revision(repo)
122 index_rev = self._get_index_revision(repo)
123 cs = repo.get_changeset(index_rev)
123 cs = repo.get_changeset(index_rev)
124 return cs
124 return cs
125
125
126 def get_paths(self, repo):
126 def get_paths(self, repo):
127 """
127 """
128 recursive walk in root dir and return a set of all path in that dir
128 recursive walk in root dir and return a set of all path in that dir
129 based on repository walk function
129 based on repository walk function
130 """
130 """
131 index_paths_ = set()
131 index_paths_ = set()
132 try:
132 try:
133 cs = self._get_index_changeset(repo)
133 cs = self._get_index_changeset(repo)
134 for _topnode, _dirs, files in cs.walk('/'):
134 for _topnode, _dirs, files in cs.walk('/'):
135 for f in files:
135 for f in files:
136 index_paths_.add(os.path.join(safe_str(repo.path), safe_str(f.path)))
136 index_paths_.add(os.path.join(safe_str(repo.path), safe_str(f.path)))
137
137
138 except RepositoryError:
138 except RepositoryError:
139 log.debug(traceback.format_exc())
139 log.debug(traceback.format_exc())
140 pass
140 pass
141 return index_paths_
141 return index_paths_
142
142
143 def get_node(self, repo, path, index_rev=None):
143 def get_node(self, repo, path, index_rev=None):
144 """
144 """
145 gets a filenode based on given full path. It operates on string for
145 gets a filenode based on given full path. It operates on string for
146 hg git compatibility.
146 hg git compatibility.
147
147
148 :param repo: scm repo instance
148 :param repo: scm repo instance
149 :param path: full path including root location
149 :param path: full path including root location
150 :return: FileNode
150 :return: FileNode
151 """
151 """
152 # FIXME: paths should be normalized ... or even better: don't include repo.path
152 # FIXME: paths should be normalized ... or even better: don't include repo.path
153 path = safe_str(path)
153 path = safe_str(path)
154 repo_path = safe_str(repo.path)
154 repo_path = safe_str(repo.path)
155 assert path.startswith(repo_path)
155 assert path.startswith(repo_path)
156 assert path[len(repo_path)] in (os.path.sep, os.path.altsep)
156 assert path[len(repo_path)] in (os.path.sep, os.path.altsep)
157 node_path = path[len(repo_path) + 1:]
157 node_path = path[len(repo_path) + 1:]
158 cs = self._get_index_changeset(repo, index_rev=index_rev)
158 cs = self._get_index_changeset(repo, index_rev=index_rev)
159 node = cs.get_node(node_path)
159 node = cs.get_node(node_path)
160 return node
160 return node
161
161
162 def is_indexable_node(self, node):
162 def is_indexable_node(self, node):
163 """
163 """
164 Just index the content of chosen files, skipping binary files
164 Just index the content of chosen files, skipping binary files
165 """
165 """
166 return (node.extension in INDEX_EXTENSIONS or node.name in INDEX_FILENAMES) and \
166 return (node.extension in INDEX_EXTENSIONS or node.name in INDEX_FILENAMES) and \
167 not node.is_binary
167 not node.is_binary
168
168
169 def get_node_mtime(self, node):
169 def get_node_mtime(self, node):
170 return mktime(node.last_changeset.date.timetuple())
170 return mktime(node.last_changeset.date.timetuple())
171
171
172 def add_doc(self, writer, path, repo, repo_name, index_rev=None):
172 def add_doc(self, writer, path, repo, repo_name, index_rev=None):
173 """
173 """
174 Adding doc to writer this function itself fetches data from
174 Adding doc to writer this function itself fetches data from
175 the instance of vcs backend
175 the instance of vcs backend
176 """
176 """
177 try:
177 try:
178 node = self.get_node(repo, path, index_rev)
178 node = self.get_node(repo, path, index_rev)
179 except (ChangesetError, NodeDoesNotExistError):
179 except (ChangesetError, NodeDoesNotExistError):
180 log.debug(" >> %s - not found in %s %s", path, repo, index_rev)
180 log.debug(" >> %s - not found in %s %s", path, repo, index_rev)
181 return 0, 0
181 return 0, 0
182
182
183 indexed = indexed_w_content = 0
183 indexed = indexed_w_content = 0
184 if self.is_indexable_node(node):
184 if self.is_indexable_node(node):
185 bytes_content = node.content
185 bytes_content = node.content
186 if b'\0' in bytes_content:
186 if b'\0' in bytes_content:
187 log.warning(' >> %s - no text content', path)
187 log.warning(' >> %s - no text content', path)
188 u_content = u''
188 u_content = u''
189 else:
189 else:
190 log.debug(' >> %s', path)
190 log.debug(' >> %s', path)
191 u_content = safe_unicode(bytes_content)
191 u_content = safe_unicode(bytes_content)
192 indexed_w_content += 1
192 indexed_w_content += 1
193
193
194 else:
194 else:
195 log.debug(' >> %s - not indexable', path)
195 log.debug(' >> %s - not indexable', path)
196 # just index file name without it's content
196 # just index file name without it's content
197 u_content = u''
197 u_content = u''
198 indexed += 1
198 indexed += 1
199
199
200 p = safe_unicode(path)
200 p = safe_unicode(path)
201 writer.add_document(
201 writer.add_document(
202 fileid=p,
202 fileid=p,
203 owner=unicode(repo.contact),
203 owner=unicode(repo.contact),
204 repository_rawname=safe_unicode(repo_name),
204 repository_rawname=safe_unicode(repo_name),
205 repository=safe_unicode(repo_name),
205 repository=safe_unicode(repo_name),
206 path=p,
206 path=p,
207 content=u_content,
207 content=u_content,
208 modtime=self.get_node_mtime(node),
208 modtime=self.get_node_mtime(node),
209 extension=node.extension
209 extension=node.extension
210 )
210 )
211 return indexed, indexed_w_content
211 return indexed, indexed_w_content
212
212
213 def index_changesets(self, writer, repo_name, repo, start_rev=None):
213 def index_changesets(self, writer, repo_name, repo, start_rev=None):
214 """
214 """
215 Add all changeset in the vcs repo starting at start_rev
215 Add all changeset in the vcs repo starting at start_rev
216 to the index writer
216 to the index writer
217
217
218 :param writer: the whoosh index writer to add to
218 :param writer: the whoosh index writer to add to
219 :param repo_name: name of the repository from whence the
219 :param repo_name: name of the repository from whence the
220 changeset originates including the repository group
220 changeset originates including the repository group
221 :param repo: the vcs repository instance to index changesets for,
221 :param repo: the vcs repository instance to index changesets for,
222 the presumption is the repo has changesets to index
222 the presumption is the repo has changesets to index
223 :param start_rev=None: the full sha id to start indexing from
223 :param start_rev=None: the full sha id to start indexing from
224 if start_rev is None then index from the first changeset in
224 if start_rev is None then index from the first changeset in
225 the repo
225 the repo
226 """
226 """
227
227
228 if start_rev is None:
228 if start_rev is None:
229 start_rev = repo[0].raw_id
229 start_rev = repo[0].raw_id
230
230
231 log.debug('Indexing changesets in %s, starting at rev %s',
231 log.debug('Indexing changesets in %s, starting at rev %s',
232 repo_name, start_rev)
232 repo_name, start_rev)
233
233
234 indexed = 0
234 indexed = 0
235 cs_iter = repo.get_changesets(start=start_rev)
235 cs_iter = repo.get_changesets(start=start_rev)
236 total = len(cs_iter)
236 total = len(cs_iter)
237 for cs in cs_iter:
237 for cs in cs_iter:
238 indexed += 1
238 indexed += 1
239 log.debug(' >> %s %s/%s', cs, indexed, total)
239 log.debug(' >> %s %s/%s', cs, indexed, total)
240 writer.add_document(
240 writer.add_document(
241 raw_id=unicode(cs.raw_id),
241 raw_id=unicode(cs.raw_id),
242 owner=unicode(repo.contact),
242 owner=unicode(repo.contact),
243 date=cs._timestamp,
243 date=cs._timestamp,
244 repository_rawname=safe_unicode(repo_name),
244 repository_rawname=safe_unicode(repo_name),
245 repository=safe_unicode(repo_name),
245 repository=safe_unicode(repo_name),
246 author=cs.author,
246 author=cs.author,
247 message=cs.message,
247 message=cs.message,
248 last=cs.last,
248 last=cs.last,
249 added=u' '.join([safe_unicode(node.path) for node in cs.added]).lower(),
249 added=u' '.join([safe_unicode(node.path) for node in cs.added]).lower(),
250 removed=u' '.join([safe_unicode(node.path) for node in cs.removed]).lower(),
250 removed=u' '.join([safe_unicode(node.path) for node in cs.removed]).lower(),
251 changed=u' '.join([safe_unicode(node.path) for node in cs.changed]).lower(),
251 changed=u' '.join([safe_unicode(node.path) for node in cs.changed]).lower(),
252 parents=u' '.join([cs.raw_id for cs in cs.parents]),
252 parents=u' '.join([cs.raw_id for cs in cs.parents]),
253 )
253 )
254
254
255 return indexed
255 return indexed
256
256
257 def index_files(self, file_idx_writer, repo_name, repo):
257 def index_files(self, file_idx_writer, repo_name, repo):
258 """
258 """
259 Index files for given repo_name
259 Index files for given repo_name
260
260
261 :param file_idx_writer: the whoosh index writer to add to
261 :param file_idx_writer: the whoosh index writer to add to
262 :param repo_name: name of the repository we're indexing
262 :param repo_name: name of the repository we're indexing
263 :param repo: instance of vcs repo
263 :param repo: instance of vcs repo
264 """
264 """
265 i_cnt = iwc_cnt = 0
265 i_cnt = iwc_cnt = 0
266 log.debug('Building file index for %s @revision:%s', repo_name,
266 log.debug('Building file index for %s @revision:%s', repo_name,
267 self._get_index_revision(repo))
267 self._get_index_revision(repo))
268 index_rev = self._get_index_revision(repo)
268 index_rev = self._get_index_revision(repo)
269 for idx_path in self.get_paths(repo):
269 for idx_path in self.get_paths(repo):
270 i, iwc = self.add_doc(file_idx_writer, idx_path, repo, repo_name, index_rev)
270 i, iwc = self.add_doc(file_idx_writer, idx_path, repo, repo_name, index_rev)
271 i_cnt += i
271 i_cnt += i
272 iwc_cnt += iwc
272 iwc_cnt += iwc
273
273
274 log.debug('added %s files %s with content for repo %s',
274 log.debug('added %s files %s with content for repo %s',
275 i_cnt + iwc_cnt, iwc_cnt, repo.path)
275 i_cnt + iwc_cnt, iwc_cnt, repo.path)
276 return i_cnt, iwc_cnt
276 return i_cnt, iwc_cnt
277
277
278 def update_changeset_index(self):
278 def update_changeset_index(self):
279 idx = open_dir(self.index_location, indexname=CHGSET_IDX_NAME)
279 idx = open_dir(self.index_location, indexname=CHGSET_IDX_NAME)
280
280
281 with idx.searcher() as searcher:
281 with idx.searcher() as searcher:
282 writer = idx.writer()
282 writer = idx.writer()
283 writer_is_dirty = False
283 writer_is_dirty = False
284 try:
284 try:
285 indexed_total = 0
285 indexed_total = 0
286 repo_name = None
286 repo_name = None
287 for repo_name, repo in sorted(self.repo_paths.items()):
287 for repo_name, repo in sorted(self.repo_paths.items()):
288 log.debug('Updating changeset index for repo %s', repo_name)
288 log.debug('Updating changeset index for repo %s', repo_name)
289 # skip indexing if there aren't any revs in the repo
289 # skip indexing if there aren't any revs in the repo
290 num_of_revs = len(repo)
290 num_of_revs = len(repo)
291 if num_of_revs < 1:
291 if num_of_revs < 1:
292 continue
292 continue
293
293
294 qp = QueryParser('repository', schema=CHGSETS_SCHEMA)
294 qp = QueryParser('repository', schema=CHGSETS_SCHEMA)
295 q = qp.parse(u"last:t AND %s" % repo_name)
295 q = qp.parse(u"last:t AND %s" % repo_name)
296
296
297 results = searcher.search(q)
297 results = searcher.search(q)
298
298
299 # default to scanning the entire repo
299 # default to scanning the entire repo
300 last_rev = 0
300 last_rev = 0
301 start_id = None
301 start_id = None
302
302
303 if len(results) > 0:
303 if len(results) > 0:
304 # assuming that there is only one result, if not this
304 # assuming that there is only one result, if not this
305 # may require a full re-index.
305 # may require a full re-index.
306 start_id = results[0]['raw_id']
306 start_id = results[0]['raw_id']
307 last_rev = repo.get_changeset(revision=start_id).revision
307 last_rev = repo.get_changeset(revision=start_id).revision
308
308
309 # there are new changesets to index or a new repo to index
309 # there are new changesets to index or a new repo to index
310 if last_rev == 0 or num_of_revs > last_rev + 1:
310 if last_rev == 0 or num_of_revs > last_rev + 1:
311 # delete the docs in the index for the previous
311 # delete the docs in the index for the previous
312 # last changeset(s)
312 # last changeset(s)
313 for hit in results:
313 for hit in results:
314 q = qp.parse(u"last:t AND %s AND raw_id:%s" %
314 q = qp.parse(u"last:t AND %s AND raw_id:%s" %
315 (repo_name, hit['raw_id']))
315 (repo_name, hit['raw_id']))
316 writer.delete_by_query(q)
316 writer.delete_by_query(q)
317
317
318 # index from the previous last changeset + all new ones
318 # index from the previous last changeset + all new ones
319 indexed_total += self.index_changesets(writer,
319 indexed_total += self.index_changesets(writer,
320 repo_name, repo, start_id)
320 repo_name, repo, start_id)
321 writer_is_dirty = True
321 writer_is_dirty = True
322 log.debug('indexed %s changesets for repo %s',
322 log.debug('indexed %s changesets for repo %s',
323 indexed_total, repo_name
323 indexed_total, repo_name
324 )
324 )
325 finally:
325 finally:
326 if writer_is_dirty:
326 if writer_is_dirty:
327 log.debug('>> COMMITING CHANGES TO CHANGESET INDEX<<')
327 log.debug('>> COMMITING CHANGES TO CHANGESET INDEX<<')
328 writer.commit(merge=True)
328 writer.commit(merge=True)
329 log.debug('>>> FINISHED REBUILDING CHANGESET INDEX <<<')
329 log.debug('>>> FINISHED REBUILDING CHANGESET INDEX <<<')
330 else:
330 else:
331 log.debug('>> NOTHING TO COMMIT TO CHANGESET INDEX<<')
331 log.debug('>> NOTHING TO COMMIT TO CHANGESET INDEX<<')
332
332
333 def update_file_index(self):
333 def update_file_index(self):
334 log.debug((u'STARTING INCREMENTAL INDEXING UPDATE FOR EXTENSIONS %s '
334 log.debug(u'STARTING INCREMENTAL INDEXING UPDATE FOR EXTENSIONS %s '
335 'AND REPOS %s') % (INDEX_EXTENSIONS, self.repo_paths.keys()))
335 'AND REPOS %s', INDEX_EXTENSIONS, ' and '.join(self.repo_paths))
336
336
337 idx = open_dir(self.index_location, indexname=self.indexname)
337 idx = open_dir(self.index_location, indexname=self.indexname)
338 # The set of all paths in the index
338 # The set of all paths in the index
339 indexed_paths = set()
339 indexed_paths = set()
340 # The set of all paths we need to re-index
340 # The set of all paths we need to re-index
341 to_index = set()
341 to_index = set()
342
342
343 writer = idx.writer()
343 writer = idx.writer()
344 writer_is_dirty = False
344 writer_is_dirty = False
345 try:
345 try:
346 with idx.reader() as reader:
346 with idx.reader() as reader:
347
347
348 # Loop over the stored fields in the index
348 # Loop over the stored fields in the index
349 for fields in reader.all_stored_fields():
349 for fields in reader.all_stored_fields():
350 indexed_path = fields['path']
350 indexed_path = fields['path']
351 indexed_repo_path = fields['repository']
351 indexed_repo_path = fields['repository']
352 indexed_paths.add(indexed_path)
352 indexed_paths.add(indexed_path)
353
353
354 if indexed_repo_path not in self.filtered_repo_update_paths:
354 if indexed_repo_path not in self.filtered_repo_update_paths:
355 continue
355 continue
356
356
357 repo = self.repo_paths[indexed_repo_path]
357 repo = self.repo_paths[indexed_repo_path]
358
358
359 try:
359 try:
360 node = self.get_node(repo, indexed_path)
360 node = self.get_node(repo, indexed_path)
361 # Check if this file was changed since it was indexed
361 # Check if this file was changed since it was indexed
362 indexed_time = fields['modtime']
362 indexed_time = fields['modtime']
363 mtime = self.get_node_mtime(node)
363 mtime = self.get_node_mtime(node)
364 if mtime > indexed_time:
364 if mtime > indexed_time:
365 # The file has changed, delete it and add it to
365 # The file has changed, delete it and add it to
366 # the list of files to reindex
366 # the list of files to reindex
367 log.debug(
367 log.debug(
368 'adding to reindex list %s mtime: %s vs %s',
368 'adding to reindex list %s mtime: %s vs %s',
369 indexed_path, mtime, indexed_time
369 indexed_path, mtime, indexed_time
370 )
370 )
371 writer.delete_by_term('fileid', indexed_path)
371 writer.delete_by_term('fileid', indexed_path)
372 writer_is_dirty = True
372 writer_is_dirty = True
373
373
374 to_index.add(indexed_path)
374 to_index.add(indexed_path)
375 except (ChangesetError, NodeDoesNotExistError):
375 except (ChangesetError, NodeDoesNotExistError):
376 # This file was deleted since it was indexed
376 # This file was deleted since it was indexed
377 log.debug('removing from index %s', indexed_path)
377 log.debug('removing from index %s', indexed_path)
378 writer.delete_by_term('path', indexed_path)
378 writer.delete_by_term('path', indexed_path)
379 writer_is_dirty = True
379 writer_is_dirty = True
380
380
381 # Loop over the files in the filesystem
381 # Loop over the files in the filesystem
382 # Assume we have a function that gathers the filenames of the
382 # Assume we have a function that gathers the filenames of the
383 # documents to be indexed
383 # documents to be indexed
384 ri_cnt_total = 0 # indexed
384 ri_cnt_total = 0 # indexed
385 riwc_cnt_total = 0 # indexed with content
385 riwc_cnt_total = 0 # indexed with content
386 for repo_name, repo in sorted(self.repo_paths.items()):
386 for repo_name, repo in sorted(self.repo_paths.items()):
387 log.debug('Updating file index for repo %s', repo_name)
387 log.debug('Updating file index for repo %s', repo_name)
388 # skip indexing if there aren't any revisions
388 # skip indexing if there aren't any revisions
389 if len(repo) < 1:
389 if len(repo) < 1:
390 continue
390 continue
391 ri_cnt = 0 # indexed
391 ri_cnt = 0 # indexed
392 riwc_cnt = 0 # indexed with content
392 riwc_cnt = 0 # indexed with content
393 for path in self.get_paths(repo):
393 for path in self.get_paths(repo):
394 path = safe_unicode(path)
394 path = safe_unicode(path)
395 if path in to_index or path not in indexed_paths:
395 if path in to_index or path not in indexed_paths:
396
396
397 # This is either a file that's changed, or a new file
397 # This is either a file that's changed, or a new file
398 # that wasn't indexed before. So index it!
398 # that wasn't indexed before. So index it!
399 i, iwc = self.add_doc(writer, path, repo, repo_name)
399 i, iwc = self.add_doc(writer, path, repo, repo_name)
400 writer_is_dirty = True
400 writer_is_dirty = True
401 ri_cnt += i
401 ri_cnt += i
402 ri_cnt_total += 1
402 ri_cnt_total += 1
403 riwc_cnt += iwc
403 riwc_cnt += iwc
404 riwc_cnt_total += iwc
404 riwc_cnt_total += iwc
405 log.debug('added %s files %s with content for repo %s',
405 log.debug('added %s files %s with content for repo %s',
406 ri_cnt + riwc_cnt, riwc_cnt, repo.path
406 ri_cnt + riwc_cnt, riwc_cnt, repo.path
407 )
407 )
408 log.debug('indexed %s files in total and %s with content',
408 log.debug('indexed %s files in total and %s with content',
409 ri_cnt_total, riwc_cnt_total
409 ri_cnt_total, riwc_cnt_total
410 )
410 )
411 finally:
411 finally:
412 if writer_is_dirty:
412 if writer_is_dirty:
413 log.debug('>> COMMITING CHANGES TO FILE INDEX <<')
413 log.debug('>> COMMITING CHANGES TO FILE INDEX <<')
414 writer.commit(merge=True)
414 writer.commit(merge=True)
415 log.debug('>>> FINISHED REBUILDING FILE INDEX <<<')
415 log.debug('>>> FINISHED REBUILDING FILE INDEX <<<')
416 else:
416 else:
417 log.debug('>> NOTHING TO COMMIT TO FILE INDEX <<')
417 log.debug('>> NOTHING TO COMMIT TO FILE INDEX <<')
418 writer.cancel()
418 writer.cancel()
419
419
420 def build_indexes(self):
420 def build_indexes(self):
421 if os.path.exists(self.index_location):
421 if os.path.exists(self.index_location):
422 log.debug('removing previous index')
422 log.debug('removing previous index')
423 rmtree(self.index_location)
423 rmtree(self.index_location)
424
424
425 if not os.path.exists(self.index_location):
425 if not os.path.exists(self.index_location):
426 os.mkdir(self.index_location)
426 os.mkdir(self.index_location)
427
427
428 chgset_idx = create_in(self.index_location, CHGSETS_SCHEMA,
428 chgset_idx = create_in(self.index_location, CHGSETS_SCHEMA,
429 indexname=CHGSET_IDX_NAME)
429 indexname=CHGSET_IDX_NAME)
430 chgset_idx_writer = chgset_idx.writer()
430 chgset_idx_writer = chgset_idx.writer()
431
431
432 file_idx = create_in(self.index_location, SCHEMA, indexname=IDX_NAME)
432 file_idx = create_in(self.index_location, SCHEMA, indexname=IDX_NAME)
433 file_idx_writer = file_idx.writer()
433 file_idx_writer = file_idx.writer()
434 log.debug('BUILDING INDEX FOR EXTENSIONS %s '
434 log.debug('BUILDING INDEX FOR EXTENSIONS %s '
435 'AND REPOS %s' % (INDEX_EXTENSIONS, self.repo_paths.keys()))
435 'AND REPOS %s', INDEX_EXTENSIONS, ' and '.join(self.repo_paths))
436
436
437 for repo_name, repo in sorted(self.repo_paths.items()):
437 for repo_name, repo in sorted(self.repo_paths.items()):
438 log.debug('Updating indices for repo %s', repo_name)
438 log.debug('Updating indices for repo %s', repo_name)
439 # skip indexing if there aren't any revisions
439 # skip indexing if there aren't any revisions
440 if len(repo) < 1:
440 if len(repo) < 1:
441 continue
441 continue
442
442
443 self.index_files(file_idx_writer, repo_name, repo)
443 self.index_files(file_idx_writer, repo_name, repo)
444 self.index_changesets(chgset_idx_writer, repo_name, repo)
444 self.index_changesets(chgset_idx_writer, repo_name, repo)
445
445
446 log.debug('>> COMMITING CHANGES <<')
446 log.debug('>> COMMITING CHANGES <<')
447 file_idx_writer.commit(merge=True)
447 file_idx_writer.commit(merge=True)
448 chgset_idx_writer.commit(merge=True)
448 chgset_idx_writer.commit(merge=True)
449 log.debug('>>> FINISHED BUILDING INDEX <<<')
449 log.debug('>>> FINISHED BUILDING INDEX <<<')
450
450
451 def update_indexes(self):
451 def update_indexes(self):
452 self.update_file_index()
452 self.update_file_index()
453 self.update_changeset_index()
453 self.update_changeset_index()
454
454
455 def run(self, full_index=False):
455 def run(self, full_index=False):
456 """Run daemon"""
456 """Run daemon"""
457 if full_index or self.initial:
457 if full_index or self.initial:
458 self.build_indexes()
458 self.build_indexes()
459 else:
459 else:
460 self.update_indexes()
460 self.update_indexes()
@@ -1,64 +1,63 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends
3 vcs.backends
4 ~~~~~~~~~~~~
4 ~~~~~~~~~~~~
5
5
6 Main package for scm backends
6 Main package for scm backends
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11 import os
11 import os
12 from pprint import pformat
13
12
14 from kallithea.lib.vcs.conf import settings
13 from kallithea.lib.vcs.conf import settings
15 from kallithea.lib.vcs.exceptions import VCSError
14 from kallithea.lib.vcs.exceptions import VCSError
16 from kallithea.lib.vcs.utils.helpers import get_scm
15 from kallithea.lib.vcs.utils.helpers import get_scm
17 from kallithea.lib.vcs.utils.imports import import_class
16 from kallithea.lib.vcs.utils.imports import import_class
18 from kallithea.lib.vcs.utils.paths import abspath
17 from kallithea.lib.vcs.utils.paths import abspath
19
18
20
19
21 def get_repo(path=None, alias=None, create=False):
20 def get_repo(path=None, alias=None, create=False):
22 """
21 """
23 Returns ``Repository`` object of type linked with given ``alias`` at
22 Returns ``Repository`` object of type linked with given ``alias`` at
24 the specified ``path``. If ``alias`` is not given it will try to guess it
23 the specified ``path``. If ``alias`` is not given it will try to guess it
25 using get_scm method
24 using get_scm method
26 """
25 """
27 if create:
26 if create:
28 if not (path or alias):
27 if not (path or alias):
29 raise TypeError("If create is specified, we need path and scm type")
28 raise TypeError("If create is specified, we need path and scm type")
30 return get_backend(alias)(path, create=True)
29 return get_backend(alias)(path, create=True)
31 if path is None:
30 if path is None:
32 path = abspath(os.path.curdir)
31 path = abspath(os.path.curdir)
33 try:
32 try:
34 scm, path = get_scm(path, search_up=True)
33 scm, path = get_scm(path, search_up=True)
35 path = abspath(path)
34 path = abspath(path)
36 alias = scm
35 alias = scm
37 except VCSError:
36 except VCSError:
38 raise VCSError("No scm found at %s" % path)
37 raise VCSError("No scm found at %s" % path)
39 if alias is None:
38 if alias is None:
40 alias = get_scm(path)[0]
39 alias = get_scm(path)[0]
41
40
42 backend = get_backend(alias)
41 backend = get_backend(alias)
43 repo = backend(path, create=create)
42 repo = backend(path, create=create)
44 return repo
43 return repo
45
44
46
45
47 def get_backend(alias):
46 def get_backend(alias):
48 """
47 """
49 Returns ``Repository`` class identified by the given alias or raises
48 Returns ``Repository`` class identified by the given alias or raises
50 VCSError if alias is not recognized or backend class cannot be imported.
49 VCSError if alias is not recognized or backend class cannot be imported.
51 """
50 """
52 if alias not in settings.BACKENDS:
51 if alias not in settings.BACKENDS:
53 raise VCSError("Given alias '%s' is not recognized! Allowed aliases:\n"
52 raise VCSError("Given alias '%s' is not recognized! Allowed aliases:\n"
54 "%s" % (alias, pformat(settings.BACKENDS.keys())))
53 "%s" % (alias, '", "'.join(settings.BACKENDS)))
55 backend_path = settings.BACKENDS[alias]
54 backend_path = settings.BACKENDS[alias]
56 klass = import_class(backend_path)
55 klass = import_class(backend_path)
57 return klass
56 return klass
58
57
59
58
60 def get_supported_backends():
59 def get_supported_backends():
61 """
60 """
62 Returns list of aliases of supported backends.
61 Returns list of aliases of supported backends.
63 """
62 """
64 return settings.BACKENDS.keys()
63 return settings.BACKENDS.keys()
@@ -1,204 +1,204 b''
1 """
1 """
2 termcolors.py
2 termcolors.py
3
3
4 Grabbed from Django (http://www.djangoproject.com)
4 Grabbed from Django (http://www.djangoproject.com)
5 """
5 """
6
6
7 color_names = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')
7 color_names = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')
8 foreground = dict([(color_names[x], '3%s' % x) for x in range(8)])
8 foreground = dict([(color_names[x], '3%s' % x) for x in range(8)])
9 background = dict([(color_names[x], '4%s' % x) for x in range(8)])
9 background = dict([(color_names[x], '4%s' % x) for x in range(8)])
10
10
11 RESET = '0'
11 RESET = '0'
12 opt_dict = {'bold': '1', 'underscore': '4', 'blink': '5', 'reverse': '7', 'conceal': '8'}
12 opt_dict = {'bold': '1', 'underscore': '4', 'blink': '5', 'reverse': '7', 'conceal': '8'}
13
13
14
14
15 def colorize(text='', opts=(), **kwargs):
15 def colorize(text='', opts=(), **kwargs):
16 """
16 """
17 Returns your text, enclosed in ANSI graphics codes.
17 Returns your text, enclosed in ANSI graphics codes.
18
18
19 Depends on the keyword arguments 'fg' and 'bg', and the contents of
19 Depends on the keyword arguments 'fg' and 'bg', and the contents of
20 the opts tuple/list.
20 the opts tuple/list.
21
21
22 Returns the RESET code if no parameters are given.
22 Returns the RESET code if no parameters are given.
23
23
24 Valid colors:
24 Valid colors:
25 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'
25 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'
26
26
27 Valid options:
27 Valid options:
28 'bold'
28 'bold'
29 'underscore'
29 'underscore'
30 'blink'
30 'blink'
31 'reverse'
31 'reverse'
32 'conceal'
32 'conceal'
33 'noreset' - string will not be auto-terminated with the RESET code
33 'noreset' - string will not be auto-terminated with the RESET code
34
34
35 Examples:
35 Examples:
36 colorize('hello', fg='red', bg='blue', opts=('blink',))
36 colorize('hello', fg='red', bg='blue', opts=('blink',))
37 colorize()
37 colorize()
38 colorize('goodbye', opts=('underscore',))
38 colorize('goodbye', opts=('underscore',))
39 print colorize('first line', fg='red', opts=('noreset',))
39 print colorize('first line', fg='red', opts=('noreset',))
40 print 'this should be red too'
40 print 'this should be red too'
41 print colorize('and so should this')
41 print colorize('and so should this')
42 print 'this should not be red'
42 print 'this should not be red'
43 """
43 """
44 code_list = []
44 code_list = []
45 if text == '' and len(opts) == 1 and opts[0] == 'reset':
45 if text == '' and len(opts) == 1 and opts[0] == 'reset':
46 return '\x1b[%sm' % RESET
46 return '\x1b[%sm' % RESET
47 for k, v in kwargs.iteritems():
47 for k, v in kwargs.iteritems():
48 if k == 'fg':
48 if k == 'fg':
49 code_list.append(foreground[v])
49 code_list.append(foreground[v])
50 elif k == 'bg':
50 elif k == 'bg':
51 code_list.append(background[v])
51 code_list.append(background[v])
52 for o in opts:
52 for o in opts:
53 if o in opt_dict:
53 if o in opt_dict:
54 code_list.append(opt_dict[o])
54 code_list.append(opt_dict[o])
55 if 'noreset' not in opts:
55 if 'noreset' not in opts:
56 text = text + '\x1b[%sm' % RESET
56 text = text + '\x1b[%sm' % RESET
57 return ('\x1b[%sm' % ';'.join(code_list)) + text
57 return ('\x1b[%sm' % ';'.join(code_list)) + text
58
58
59
59
60 def make_style(opts=(), **kwargs):
60 def make_style(opts=(), **kwargs):
61 """
61 """
62 Returns a function with default parameters for colorize()
62 Returns a function with default parameters for colorize()
63
63
64 Example:
64 Example:
65 bold_red = make_style(opts=('bold',), fg='red')
65 bold_red = make_style(opts=('bold',), fg='red')
66 print bold_red('hello')
66 print bold_red('hello')
67 KEYWORD = make_style(fg='yellow')
67 KEYWORD = make_style(fg='yellow')
68 COMMENT = make_style(fg='blue', opts=('bold',))
68 COMMENT = make_style(fg='blue', opts=('bold',))
69 """
69 """
70 return lambda text: colorize(text, opts, **kwargs)
70 return lambda text: colorize(text, opts, **kwargs)
71
71
72
72
73 NOCOLOR_PALETTE = 'nocolor'
73 NOCOLOR_PALETTE = 'nocolor'
74 DARK_PALETTE = 'dark'
74 DARK_PALETTE = 'dark'
75 LIGHT_PALETTE = 'light'
75 LIGHT_PALETTE = 'light'
76
76
77 PALETTES = {
77 PALETTES = {
78 NOCOLOR_PALETTE: {
78 NOCOLOR_PALETTE: {
79 'ERROR': {},
79 'ERROR': {},
80 'NOTICE': {},
80 'NOTICE': {},
81 'SQL_FIELD': {},
81 'SQL_FIELD': {},
82 'SQL_COLTYPE': {},
82 'SQL_COLTYPE': {},
83 'SQL_KEYWORD': {},
83 'SQL_KEYWORD': {},
84 'SQL_TABLE': {},
84 'SQL_TABLE': {},
85 'HTTP_INFO': {},
85 'HTTP_INFO': {},
86 'HTTP_SUCCESS': {},
86 'HTTP_SUCCESS': {},
87 'HTTP_REDIRECT': {},
87 'HTTP_REDIRECT': {},
88 'HTTP_NOT_MODIFIED': {},
88 'HTTP_NOT_MODIFIED': {},
89 'HTTP_BAD_REQUEST': {},
89 'HTTP_BAD_REQUEST': {},
90 'HTTP_NOT_FOUND': {},
90 'HTTP_NOT_FOUND': {},
91 'HTTP_SERVER_ERROR': {},
91 'HTTP_SERVER_ERROR': {},
92 },
92 },
93 DARK_PALETTE: {
93 DARK_PALETTE: {
94 'ERROR': { 'fg': 'red', 'opts': ('bold',) },
94 'ERROR': { 'fg': 'red', 'opts': ('bold',) },
95 'NOTICE': { 'fg': 'red' },
95 'NOTICE': { 'fg': 'red' },
96 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) },
96 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) },
97 'SQL_COLTYPE': { 'fg': 'green' },
97 'SQL_COLTYPE': { 'fg': 'green' },
98 'SQL_KEYWORD': { 'fg': 'yellow' },
98 'SQL_KEYWORD': { 'fg': 'yellow' },
99 'SQL_TABLE': { 'opts': ('bold',) },
99 'SQL_TABLE': { 'opts': ('bold',) },
100 'HTTP_INFO': { 'opts': ('bold',) },
100 'HTTP_INFO': { 'opts': ('bold',) },
101 'HTTP_SUCCESS': { },
101 'HTTP_SUCCESS': { },
102 'HTTP_REDIRECT': { 'fg': 'green' },
102 'HTTP_REDIRECT': { 'fg': 'green' },
103 'HTTP_NOT_MODIFIED': { 'fg': 'cyan' },
103 'HTTP_NOT_MODIFIED': { 'fg': 'cyan' },
104 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) },
104 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) },
105 'HTTP_NOT_FOUND': { 'fg': 'yellow' },
105 'HTTP_NOT_FOUND': { 'fg': 'yellow' },
106 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) },
106 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) },
107 },
107 },
108 LIGHT_PALETTE: {
108 LIGHT_PALETTE: {
109 'ERROR': { 'fg': 'red', 'opts': ('bold',) },
109 'ERROR': { 'fg': 'red', 'opts': ('bold',) },
110 'NOTICE': { 'fg': 'red' },
110 'NOTICE': { 'fg': 'red' },
111 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) },
111 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) },
112 'SQL_COLTYPE': { 'fg': 'green' },
112 'SQL_COLTYPE': { 'fg': 'green' },
113 'SQL_KEYWORD': { 'fg': 'blue' },
113 'SQL_KEYWORD': { 'fg': 'blue' },
114 'SQL_TABLE': { 'opts': ('bold',) },
114 'SQL_TABLE': { 'opts': ('bold',) },
115 'HTTP_INFO': { 'opts': ('bold',) },
115 'HTTP_INFO': { 'opts': ('bold',) },
116 'HTTP_SUCCESS': { },
116 'HTTP_SUCCESS': { },
117 'HTTP_REDIRECT': { 'fg': 'green', 'opts': ('bold',) },
117 'HTTP_REDIRECT': { 'fg': 'green', 'opts': ('bold',) },
118 'HTTP_NOT_MODIFIED': { 'fg': 'green' },
118 'HTTP_NOT_MODIFIED': { 'fg': 'green' },
119 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) },
119 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) },
120 'HTTP_NOT_FOUND': { 'fg': 'red' },
120 'HTTP_NOT_FOUND': { 'fg': 'red' },
121 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) },
121 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) },
122 }
122 }
123 }
123 }
124 DEFAULT_PALETTE = DARK_PALETTE
124 DEFAULT_PALETTE = DARK_PALETTE
125
125
126
126
127 def parse_color_setting(config_string):
127 def parse_color_setting(config_string):
128 """Parse a DJANGO_COLORS environment variable to produce the system palette
128 """Parse a DJANGO_COLORS environment variable to produce the system palette
129
129
130 The general form of a palette definition is:
130 The general form of a palette definition is:
131
131
132 "palette;role=fg;role=fg/bg;role=fg,option,option;role=fg/bg,option,option"
132 "palette;role=fg;role=fg/bg;role=fg,option,option;role=fg/bg,option,option"
133
133
134 where:
134 where:
135 palette is a named palette; one of 'light', 'dark', or 'nocolor'.
135 palette is a named palette; one of 'light', 'dark', or 'nocolor'.
136 role is a named style used by Django
136 role is a named style used by Django
137 fg is a background color.
137 fg is a background color.
138 bg is a background color.
138 bg is a background color.
139 option is a display options.
139 option is a display options.
140
140
141 Specifying a named palette is the same as manually specifying the individual
141 Specifying a named palette is the same as manually specifying the individual
142 definitions for each role. Any individual definitions following the palette
142 definitions for each role. Any individual definitions following the palette
143 definition will augment the base palette definition.
143 definition will augment the base palette definition.
144
144
145 Valid roles:
145 Valid roles:
146 'error', 'notice', 'sql_field', 'sql_coltype', 'sql_keyword', 'sql_table',
146 'error', 'notice', 'sql_field', 'sql_coltype', 'sql_keyword', 'sql_table',
147 'http_info', 'http_success', 'http_redirect', 'http_bad_request',
147 'http_info', 'http_success', 'http_redirect', 'http_bad_request',
148 'http_not_found', 'http_server_error'
148 'http_not_found', 'http_server_error'
149
149
150 Valid colors:
150 Valid colors:
151 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'
151 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'
152
152
153 Valid options:
153 Valid options:
154 'bold', 'underscore', 'blink', 'reverse', 'conceal'
154 'bold', 'underscore', 'blink', 'reverse', 'conceal'
155
155
156 """
156 """
157 if not config_string:
157 if not config_string:
158 return PALETTES[DEFAULT_PALETTE]
158 return PALETTES[DEFAULT_PALETTE]
159
159
160 # Split the color configuration into parts
160 # Split the color configuration into parts
161 parts = config_string.lower().split(';')
161 parts = config_string.lower().split(';')
162 palette = PALETTES[NOCOLOR_PALETTE].copy()
162 palette = PALETTES[NOCOLOR_PALETTE].copy()
163 for part in parts:
163 for part in parts:
164 if part in PALETTES:
164 if part in PALETTES:
165 # A default palette has been specified
165 # A default palette has been specified
166 palette.update(PALETTES[part])
166 palette.update(PALETTES[part])
167 elif '=' in part:
167 elif '=' in part:
168 # Process a palette defining string
168 # Process a palette defining string
169 definition = {}
169 definition = {}
170
170
171 # Break the definition into the role,
171 # Break the definition into the role,
172 # plus the list of specific instructions.
172 # plus the list of specific instructions.
173 # The role must be in upper case
173 # The role must be in upper case
174 role, instructions = part.split('=')
174 role, instructions = part.split('=')
175 role = role.upper()
175 role = role.upper()
176
176
177 styles = instructions.split(',')
177 styles = instructions.split(',')
178 styles.reverse()
178 styles.reverse()
179
179
180 # The first instruction can contain a slash
180 # The first instruction can contain a slash
181 # to break apart fg/bg.
181 # to break apart fg/bg.
182 colors = styles.pop().split('/')
182 colors = styles.pop().split('/')
183 colors.reverse()
183 colors.reverse()
184 fg = colors.pop()
184 fg = colors.pop()
185 if fg in color_names:
185 if fg in color_names:
186 definition['fg'] = fg
186 definition['fg'] = fg
187 if colors and colors[-1] in color_names:
187 if colors and colors[-1] in color_names:
188 definition['bg'] = colors[-1]
188 definition['bg'] = colors[-1]
189
189
190 # All remaining instructions are options
190 # All remaining instructions are options
191 opts = tuple(s for s in styles if s in opt_dict.keys())
191 opts = tuple(s for s in styles if s in opt_dict)
192 if opts:
192 if opts:
193 definition['opts'] = opts
193 definition['opts'] = opts
194
194
195 # The nocolor palette has all available roles.
195 # The nocolor palette has all available roles.
196 # Use that palette as the basis for determining
196 # Use that palette as the basis for determining
197 # if the role is valid.
197 # if the role is valid.
198 if role in PALETTES[NOCOLOR_PALETTE] and definition:
198 if role in PALETTES[NOCOLOR_PALETTE] and definition:
199 palette[role] = definition
199 palette[role] = definition
200
200
201 # If there are no colors specified, return the empty palette.
201 # If there are no colors specified, return the empty palette.
202 if palette == PALETTES[NOCOLOR_PALETTE]:
202 if palette == PALETTES[NOCOLOR_PALETTE]:
203 return None
203 return None
204 return palette
204 return palette
@@ -1,2550 +1,2551 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.model.db
15 kallithea.model.db
16 ~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~
17
17
18 Database Models for Kallithea
18 Database Models for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 08, 2010
22 :created_on: Apr 08, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import base64
28 import base64
29 import collections
29 import collections
30 import datetime
30 import datetime
31 import functools
31 import functools
32 import hashlib
32 import hashlib
33 import logging
33 import logging
34 import os
34 import os
35 import time
35 import time
36 import traceback
36 import traceback
37
37
38 import ipaddr
38 import ipaddr
39 import sqlalchemy
39 import sqlalchemy
40 from beaker.cache import cache_region, region_invalidate
40 from beaker.cache import cache_region, region_invalidate
41 from sqlalchemy import *
41 from sqlalchemy import *
42 from sqlalchemy.ext.hybrid import hybrid_property
42 from sqlalchemy.ext.hybrid import hybrid_property
43 from sqlalchemy.orm import class_mapper, joinedload, relationship, validates
43 from sqlalchemy.orm import class_mapper, joinedload, relationship, validates
44 from tg.i18n import lazy_ugettext as _
44 from tg.i18n import lazy_ugettext as _
45 from webob.exc import HTTPNotFound
45 from webob.exc import HTTPNotFound
46
46
47 import kallithea
47 import kallithea
48 from kallithea.lib.caching_query import FromCache
48 from kallithea.lib.caching_query import FromCache
49 from kallithea.lib.compat import json
49 from kallithea.lib.compat import json
50 from kallithea.lib.exceptions import DefaultUserException
50 from kallithea.lib.exceptions import DefaultUserException
51 from kallithea.lib.utils2 import Optional, aslist, get_changeset_safe, get_clone_url, remove_prefix, safe_int, safe_str, safe_unicode, str2bool, urlreadable
51 from kallithea.lib.utils2 import Optional, aslist, get_changeset_safe, get_clone_url, remove_prefix, safe_int, safe_str, safe_unicode, str2bool, urlreadable
52 from kallithea.lib.vcs import get_backend
52 from kallithea.lib.vcs import get_backend
53 from kallithea.lib.vcs.backends.base import EmptyChangeset
53 from kallithea.lib.vcs.backends.base import EmptyChangeset
54 from kallithea.lib.vcs.utils.helpers import get_scm
54 from kallithea.lib.vcs.utils.helpers import get_scm
55 from kallithea.lib.vcs.utils.lazy import LazyProperty
55 from kallithea.lib.vcs.utils.lazy import LazyProperty
56 from kallithea.model.meta import Base, Session
56 from kallithea.model.meta import Base, Session
57
57
58
58
59 URL_SEP = '/'
59 URL_SEP = '/'
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62 #==============================================================================
62 #==============================================================================
63 # BASE CLASSES
63 # BASE CLASSES
64 #==============================================================================
64 #==============================================================================
65
65
66 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
66 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
67
67
68
68
69 class BaseDbModel(object):
69 class BaseDbModel(object):
70 """
70 """
71 Base Model for all classes
71 Base Model for all classes
72 """
72 """
73
73
74 @classmethod
74 @classmethod
75 def _get_keys(cls):
75 def _get_keys(cls):
76 """return column names for this model """
76 """return column names for this model """
77 # Note: not a normal dict - iterator gives "users.firstname", but keys gives "firstname"
77 return class_mapper(cls).c.keys()
78 return class_mapper(cls).c.keys()
78
79
79 def get_dict(self):
80 def get_dict(self):
80 """
81 """
81 return dict with keys and values corresponding
82 return dict with keys and values corresponding
82 to this model data """
83 to this model data """
83
84
84 d = {}
85 d = {}
85 for k in self._get_keys():
86 for k in self._get_keys():
86 d[k] = getattr(self, k)
87 d[k] = getattr(self, k)
87
88
88 # also use __json__() if present to get additional fields
89 # also use __json__() if present to get additional fields
89 _json_attr = getattr(self, '__json__', None)
90 _json_attr = getattr(self, '__json__', None)
90 if _json_attr:
91 if _json_attr:
91 # update with attributes from __json__
92 # update with attributes from __json__
92 if callable(_json_attr):
93 if callable(_json_attr):
93 _json_attr = _json_attr()
94 _json_attr = _json_attr()
94 for k, val in _json_attr.iteritems():
95 for k, val in _json_attr.iteritems():
95 d[k] = val
96 d[k] = val
96 return d
97 return d
97
98
98 def get_appstruct(self):
99 def get_appstruct(self):
99 """return list with keys and values tuples corresponding
100 """return list with keys and values tuples corresponding
100 to this model data """
101 to this model data """
101
102
102 return [
103 return [
103 (k, getattr(self, k))
104 (k, getattr(self, k))
104 for k in self._get_keys()
105 for k in self._get_keys()
105 ]
106 ]
106
107
107 def populate_obj(self, populate_dict):
108 def populate_obj(self, populate_dict):
108 """populate model with data from given populate_dict"""
109 """populate model with data from given populate_dict"""
109
110
110 for k in self._get_keys():
111 for k in self._get_keys():
111 if k in populate_dict:
112 if k in populate_dict:
112 setattr(self, k, populate_dict[k])
113 setattr(self, k, populate_dict[k])
113
114
114 @classmethod
115 @classmethod
115 def query(cls):
116 def query(cls):
116 return Session().query(cls)
117 return Session().query(cls)
117
118
118 @classmethod
119 @classmethod
119 def get(cls, id_):
120 def get(cls, id_):
120 if id_:
121 if id_:
121 return cls.query().get(id_)
122 return cls.query().get(id_)
122
123
123 @classmethod
124 @classmethod
124 def guess_instance(cls, value, callback=None):
125 def guess_instance(cls, value, callback=None):
125 """Haphazardly attempt to convert `value` to a `cls` instance.
126 """Haphazardly attempt to convert `value` to a `cls` instance.
126
127
127 If `value` is None or already a `cls` instance, return it. If `value`
128 If `value` is None or already a `cls` instance, return it. If `value`
128 is a number (or looks like one if you squint just right), assume it's
129 is a number (or looks like one if you squint just right), assume it's
129 a database primary key and let SQLAlchemy sort things out. Otherwise,
130 a database primary key and let SQLAlchemy sort things out. Otherwise,
130 fall back to resolving it using `callback` (if specified); this could
131 fall back to resolving it using `callback` (if specified); this could
131 e.g. be a function that looks up instances by name (though that won't
132 e.g. be a function that looks up instances by name (though that won't
132 work if the name begins with a digit). Otherwise, raise Exception.
133 work if the name begins with a digit). Otherwise, raise Exception.
133 """
134 """
134
135
135 if value is None:
136 if value is None:
136 return None
137 return None
137 if isinstance(value, cls):
138 if isinstance(value, cls):
138 return value
139 return value
139 if isinstance(value, (int, long)):
140 if isinstance(value, (int, long)):
140 return cls.get(value)
141 return cls.get(value)
141 if isinstance(value, basestring) and value.isdigit():
142 if isinstance(value, basestring) and value.isdigit():
142 return cls.get(int(value))
143 return cls.get(int(value))
143 if callback is not None:
144 if callback is not None:
144 return callback(value)
145 return callback(value)
145
146
146 raise Exception(
147 raise Exception(
147 'given object must be int, long or Instance of %s '
148 'given object must be int, long or Instance of %s '
148 'got %s, no callback provided' % (cls, type(value))
149 'got %s, no callback provided' % (cls, type(value))
149 )
150 )
150
151
151 @classmethod
152 @classmethod
152 def get_or_404(cls, id_):
153 def get_or_404(cls, id_):
153 try:
154 try:
154 id_ = int(id_)
155 id_ = int(id_)
155 except (TypeError, ValueError):
156 except (TypeError, ValueError):
156 raise HTTPNotFound
157 raise HTTPNotFound
157
158
158 res = cls.query().get(id_)
159 res = cls.query().get(id_)
159 if res is None:
160 if res is None:
160 raise HTTPNotFound
161 raise HTTPNotFound
161 return res
162 return res
162
163
163 @classmethod
164 @classmethod
164 def delete(cls, id_):
165 def delete(cls, id_):
165 obj = cls.query().get(id_)
166 obj = cls.query().get(id_)
166 Session().delete(obj)
167 Session().delete(obj)
167
168
168 def __repr__(self):
169 def __repr__(self):
169 if hasattr(self, '__unicode__'):
170 if hasattr(self, '__unicode__'):
170 # python repr needs to return str
171 # python repr needs to return str
171 try:
172 try:
172 return safe_str(self.__unicode__())
173 return safe_str(self.__unicode__())
173 except UnicodeDecodeError:
174 except UnicodeDecodeError:
174 pass
175 pass
175 return '<DB:%s>' % (self.__class__.__name__)
176 return '<DB:%s>' % (self.__class__.__name__)
176
177
177
178
178 _table_args_default_dict = {'extend_existing': True,
179 _table_args_default_dict = {'extend_existing': True,
179 'mysql_engine': 'InnoDB',
180 'mysql_engine': 'InnoDB',
180 'mysql_charset': 'utf8',
181 'mysql_charset': 'utf8',
181 'sqlite_autoincrement': True,
182 'sqlite_autoincrement': True,
182 }
183 }
183
184
184 class Setting(Base, BaseDbModel):
185 class Setting(Base, BaseDbModel):
185 __tablename__ = 'settings'
186 __tablename__ = 'settings'
186 __table_args__ = (
187 __table_args__ = (
187 _table_args_default_dict,
188 _table_args_default_dict,
188 )
189 )
189
190
190 SETTINGS_TYPES = {
191 SETTINGS_TYPES = {
191 'str': safe_str,
192 'str': safe_str,
192 'int': safe_int,
193 'int': safe_int,
193 'unicode': safe_unicode,
194 'unicode': safe_unicode,
194 'bool': str2bool,
195 'bool': str2bool,
195 'list': functools.partial(aslist, sep=',')
196 'list': functools.partial(aslist, sep=',')
196 }
197 }
197 DEFAULT_UPDATE_URL = ''
198 DEFAULT_UPDATE_URL = ''
198
199
199 app_settings_id = Column(Integer(), primary_key=True)
200 app_settings_id = Column(Integer(), primary_key=True)
200 app_settings_name = Column(String(255), nullable=False, unique=True)
201 app_settings_name = Column(String(255), nullable=False, unique=True)
201 _app_settings_value = Column("app_settings_value", Unicode(4096), nullable=False)
202 _app_settings_value = Column("app_settings_value", Unicode(4096), nullable=False)
202 _app_settings_type = Column("app_settings_type", String(255), nullable=True) # FIXME: not nullable?
203 _app_settings_type = Column("app_settings_type", String(255), nullable=True) # FIXME: not nullable?
203
204
204 def __init__(self, key='', val='', type='unicode'):
205 def __init__(self, key='', val='', type='unicode'):
205 self.app_settings_name = key
206 self.app_settings_name = key
206 self.app_settings_value = val
207 self.app_settings_value = val
207 self.app_settings_type = type
208 self.app_settings_type = type
208
209
209 @validates('_app_settings_value')
210 @validates('_app_settings_value')
210 def validate_settings_value(self, key, val):
211 def validate_settings_value(self, key, val):
211 assert isinstance(val, unicode)
212 assert isinstance(val, unicode)
212 return val
213 return val
213
214
214 @hybrid_property
215 @hybrid_property
215 def app_settings_value(self):
216 def app_settings_value(self):
216 v = self._app_settings_value
217 v = self._app_settings_value
217 _type = self.app_settings_type
218 _type = self.app_settings_type
218 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
219 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
219 return converter(v)
220 return converter(v)
220
221
221 @app_settings_value.setter
222 @app_settings_value.setter
222 def app_settings_value(self, val):
223 def app_settings_value(self, val):
223 """
224 """
224 Setter that will always make sure we use unicode in app_settings_value
225 Setter that will always make sure we use unicode in app_settings_value
225
226
226 :param val:
227 :param val:
227 """
228 """
228 self._app_settings_value = safe_unicode(val)
229 self._app_settings_value = safe_unicode(val)
229
230
230 @hybrid_property
231 @hybrid_property
231 def app_settings_type(self):
232 def app_settings_type(self):
232 return self._app_settings_type
233 return self._app_settings_type
233
234
234 @app_settings_type.setter
235 @app_settings_type.setter
235 def app_settings_type(self, val):
236 def app_settings_type(self, val):
236 if val not in self.SETTINGS_TYPES:
237 if val not in self.SETTINGS_TYPES:
237 raise Exception('type must be one of %s got %s'
238 raise Exception('type must be one of %s got %s'
238 % (list(self.SETTINGS_TYPES), val))
239 % (list(self.SETTINGS_TYPES), val))
239 self._app_settings_type = val
240 self._app_settings_type = val
240
241
241 def __unicode__(self):
242 def __unicode__(self):
242 return u"<%s('%s:%s[%s]')>" % (
243 return u"<%s('%s:%s[%s]')>" % (
243 self.__class__.__name__,
244 self.__class__.__name__,
244 self.app_settings_name, self.app_settings_value, self.app_settings_type
245 self.app_settings_name, self.app_settings_value, self.app_settings_type
245 )
246 )
246
247
247 @classmethod
248 @classmethod
248 def get_by_name(cls, key):
249 def get_by_name(cls, key):
249 return cls.query() \
250 return cls.query() \
250 .filter(cls.app_settings_name == key).scalar()
251 .filter(cls.app_settings_name == key).scalar()
251
252
252 @classmethod
253 @classmethod
253 def get_by_name_or_create(cls, key, val='', type='unicode'):
254 def get_by_name_or_create(cls, key, val='', type='unicode'):
254 res = cls.get_by_name(key)
255 res = cls.get_by_name(key)
255 if res is None:
256 if res is None:
256 res = cls(key, val, type)
257 res = cls(key, val, type)
257 return res
258 return res
258
259
259 @classmethod
260 @classmethod
260 def create_or_update(cls, key, val=Optional(''), type=Optional('unicode')):
261 def create_or_update(cls, key, val=Optional(''), type=Optional('unicode')):
261 """
262 """
262 Creates or updates Kallithea setting. If updates are triggered, it will only
263 Creates or updates Kallithea setting. If updates are triggered, it will only
263 update parameters that are explicitly set. Optional instance will be skipped.
264 update parameters that are explicitly set. Optional instance will be skipped.
264
265
265 :param key:
266 :param key:
266 :param val:
267 :param val:
267 :param type:
268 :param type:
268 :return:
269 :return:
269 """
270 """
270 res = cls.get_by_name(key)
271 res = cls.get_by_name(key)
271 if res is None:
272 if res is None:
272 val = Optional.extract(val)
273 val = Optional.extract(val)
273 type = Optional.extract(type)
274 type = Optional.extract(type)
274 res = cls(key, val, type)
275 res = cls(key, val, type)
275 Session().add(res)
276 Session().add(res)
276 else:
277 else:
277 res.app_settings_name = key
278 res.app_settings_name = key
278 if not isinstance(val, Optional):
279 if not isinstance(val, Optional):
279 # update if set
280 # update if set
280 res.app_settings_value = val
281 res.app_settings_value = val
281 if not isinstance(type, Optional):
282 if not isinstance(type, Optional):
282 # update if set
283 # update if set
283 res.app_settings_type = type
284 res.app_settings_type = type
284 return res
285 return res
285
286
286 @classmethod
287 @classmethod
287 def get_app_settings(cls, cache=False):
288 def get_app_settings(cls, cache=False):
288
289
289 ret = cls.query()
290 ret = cls.query()
290
291
291 if cache:
292 if cache:
292 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
293 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
293
294
294 if ret is None:
295 if ret is None:
295 raise Exception('Could not get application settings !')
296 raise Exception('Could not get application settings !')
296 settings = {}
297 settings = {}
297 for each in ret:
298 for each in ret:
298 settings[each.app_settings_name] = \
299 settings[each.app_settings_name] = \
299 each.app_settings_value
300 each.app_settings_value
300
301
301 return settings
302 return settings
302
303
303 @classmethod
304 @classmethod
304 def get_auth_settings(cls, cache=False):
305 def get_auth_settings(cls, cache=False):
305 ret = cls.query() \
306 ret = cls.query() \
306 .filter(cls.app_settings_name.startswith('auth_')).all()
307 .filter(cls.app_settings_name.startswith('auth_')).all()
307 fd = {}
308 fd = {}
308 for row in ret:
309 for row in ret:
309 fd[row.app_settings_name] = row.app_settings_value
310 fd[row.app_settings_name] = row.app_settings_value
310 return fd
311 return fd
311
312
312 @classmethod
313 @classmethod
313 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
314 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
314 ret = cls.query() \
315 ret = cls.query() \
315 .filter(cls.app_settings_name.startswith('default_')).all()
316 .filter(cls.app_settings_name.startswith('default_')).all()
316 fd = {}
317 fd = {}
317 for row in ret:
318 for row in ret:
318 key = row.app_settings_name
319 key = row.app_settings_name
319 if strip_prefix:
320 if strip_prefix:
320 key = remove_prefix(key, prefix='default_')
321 key = remove_prefix(key, prefix='default_')
321 fd.update({key: row.app_settings_value})
322 fd.update({key: row.app_settings_value})
322
323
323 return fd
324 return fd
324
325
325 @classmethod
326 @classmethod
326 def get_server_info(cls):
327 def get_server_info(cls):
327 import pkg_resources
328 import pkg_resources
328 import platform
329 import platform
329 from kallithea.lib.utils import check_git_version
330 from kallithea.lib.utils import check_git_version
330 mods = [(p.project_name, p.version) for p in pkg_resources.working_set]
331 mods = [(p.project_name, p.version) for p in pkg_resources.working_set]
331 info = {
332 info = {
332 'modules': sorted(mods, key=lambda k: k[0].lower()),
333 'modules': sorted(mods, key=lambda k: k[0].lower()),
333 'py_version': platform.python_version(),
334 'py_version': platform.python_version(),
334 'platform': safe_unicode(platform.platform()),
335 'platform': safe_unicode(platform.platform()),
335 'kallithea_version': kallithea.__version__,
336 'kallithea_version': kallithea.__version__,
336 'git_version': safe_unicode(check_git_version()),
337 'git_version': safe_unicode(check_git_version()),
337 'git_path': kallithea.CONFIG.get('git_path')
338 'git_path': kallithea.CONFIG.get('git_path')
338 }
339 }
339 return info
340 return info
340
341
341
342
342 class Ui(Base, BaseDbModel):
343 class Ui(Base, BaseDbModel):
343 __tablename__ = 'ui'
344 __tablename__ = 'ui'
344 __table_args__ = (
345 __table_args__ = (
345 # FIXME: ui_key as key is wrong and should be removed when the corresponding
346 # FIXME: ui_key as key is wrong and should be removed when the corresponding
346 # Ui.get_by_key has been replaced by the composite key
347 # Ui.get_by_key has been replaced by the composite key
347 UniqueConstraint('ui_key'),
348 UniqueConstraint('ui_key'),
348 UniqueConstraint('ui_section', 'ui_key'),
349 UniqueConstraint('ui_section', 'ui_key'),
349 _table_args_default_dict,
350 _table_args_default_dict,
350 )
351 )
351
352
352 HOOK_UPDATE = 'changegroup.update'
353 HOOK_UPDATE = 'changegroup.update'
353 HOOK_REPO_SIZE = 'changegroup.repo_size'
354 HOOK_REPO_SIZE = 'changegroup.repo_size'
354
355
355 ui_id = Column(Integer(), primary_key=True)
356 ui_id = Column(Integer(), primary_key=True)
356 ui_section = Column(String(255), nullable=False)
357 ui_section = Column(String(255), nullable=False)
357 ui_key = Column(String(255), nullable=False)
358 ui_key = Column(String(255), nullable=False)
358 ui_value = Column(String(255), nullable=True) # FIXME: not nullable?
359 ui_value = Column(String(255), nullable=True) # FIXME: not nullable?
359 ui_active = Column(Boolean(), nullable=False, default=True)
360 ui_active = Column(Boolean(), nullable=False, default=True)
360
361
361 @classmethod
362 @classmethod
362 def get_by_key(cls, section, key):
363 def get_by_key(cls, section, key):
363 """ Return specified Ui object, or None if not found. """
364 """ Return specified Ui object, or None if not found. """
364 return cls.query().filter_by(ui_section=section, ui_key=key).scalar()
365 return cls.query().filter_by(ui_section=section, ui_key=key).scalar()
365
366
366 @classmethod
367 @classmethod
367 def get_or_create(cls, section, key):
368 def get_or_create(cls, section, key):
368 """ Return specified Ui object, creating it if necessary. """
369 """ Return specified Ui object, creating it if necessary. """
369 setting = cls.get_by_key(section, key)
370 setting = cls.get_by_key(section, key)
370 if setting is None:
371 if setting is None:
371 setting = cls(ui_section=section, ui_key=key)
372 setting = cls(ui_section=section, ui_key=key)
372 Session().add(setting)
373 Session().add(setting)
373 return setting
374 return setting
374
375
375 @classmethod
376 @classmethod
376 def get_builtin_hooks(cls):
377 def get_builtin_hooks(cls):
377 q = cls.query()
378 q = cls.query()
378 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
379 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
379 q = q.filter(cls.ui_section == 'hooks')
380 q = q.filter(cls.ui_section == 'hooks')
380 return q.all()
381 return q.all()
381
382
382 @classmethod
383 @classmethod
383 def get_custom_hooks(cls):
384 def get_custom_hooks(cls):
384 q = cls.query()
385 q = cls.query()
385 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
386 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
386 q = q.filter(cls.ui_section == 'hooks')
387 q = q.filter(cls.ui_section == 'hooks')
387 return q.all()
388 return q.all()
388
389
389 @classmethod
390 @classmethod
390 def get_repos_location(cls):
391 def get_repos_location(cls):
391 return cls.get_by_key('paths', '/').ui_value
392 return cls.get_by_key('paths', '/').ui_value
392
393
393 @classmethod
394 @classmethod
394 def create_or_update_hook(cls, key, val):
395 def create_or_update_hook(cls, key, val):
395 new_ui = cls.get_or_create('hooks', key)
396 new_ui = cls.get_or_create('hooks', key)
396 new_ui.ui_active = True
397 new_ui.ui_active = True
397 new_ui.ui_value = val
398 new_ui.ui_value = val
398
399
399 def __repr__(self):
400 def __repr__(self):
400 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
401 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
401 self.ui_key, self.ui_value)
402 self.ui_key, self.ui_value)
402
403
403
404
404 class User(Base, BaseDbModel):
405 class User(Base, BaseDbModel):
405 __tablename__ = 'users'
406 __tablename__ = 'users'
406 __table_args__ = (
407 __table_args__ = (
407 Index('u_username_idx', 'username'),
408 Index('u_username_idx', 'username'),
408 Index('u_email_idx', 'email'),
409 Index('u_email_idx', 'email'),
409 _table_args_default_dict,
410 _table_args_default_dict,
410 )
411 )
411
412
412 DEFAULT_USER = 'default'
413 DEFAULT_USER = 'default'
413 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
414 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
414 # The name of the default auth type in extern_type, 'internal' lives in auth_internal.py
415 # The name of the default auth type in extern_type, 'internal' lives in auth_internal.py
415 DEFAULT_AUTH_TYPE = 'internal'
416 DEFAULT_AUTH_TYPE = 'internal'
416
417
417 user_id = Column(Integer(), primary_key=True)
418 user_id = Column(Integer(), primary_key=True)
418 username = Column(String(255), nullable=False, unique=True)
419 username = Column(String(255), nullable=False, unique=True)
419 password = Column(String(255), nullable=False)
420 password = Column(String(255), nullable=False)
420 active = Column(Boolean(), nullable=False, default=True)
421 active = Column(Boolean(), nullable=False, default=True)
421 admin = Column(Boolean(), nullable=False, default=False)
422 admin = Column(Boolean(), nullable=False, default=False)
422 name = Column("firstname", Unicode(255), nullable=False)
423 name = Column("firstname", Unicode(255), nullable=False)
423 lastname = Column(Unicode(255), nullable=False)
424 lastname = Column(Unicode(255), nullable=False)
424 _email = Column("email", String(255), nullable=True, unique=True) # FIXME: not nullable?
425 _email = Column("email", String(255), nullable=True, unique=True) # FIXME: not nullable?
425 last_login = Column(DateTime(timezone=False), nullable=True)
426 last_login = Column(DateTime(timezone=False), nullable=True)
426 extern_type = Column(String(255), nullable=True) # FIXME: not nullable?
427 extern_type = Column(String(255), nullable=True) # FIXME: not nullable?
427 extern_name = Column(String(255), nullable=True) # FIXME: not nullable?
428 extern_name = Column(String(255), nullable=True) # FIXME: not nullable?
428 api_key = Column(String(255), nullable=False)
429 api_key = Column(String(255), nullable=False)
429 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
430 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
430 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
431 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
431
432
432 user_log = relationship('UserLog')
433 user_log = relationship('UserLog')
433 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
434 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
434
435
435 repositories = relationship('Repository')
436 repositories = relationship('Repository')
436 repo_groups = relationship('RepoGroup')
437 repo_groups = relationship('RepoGroup')
437 user_groups = relationship('UserGroup')
438 user_groups = relationship('UserGroup')
438 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
439 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
439 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
440 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
440
441
441 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
442 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
442 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
443 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
443
444
444 group_member = relationship('UserGroupMember', cascade='all')
445 group_member = relationship('UserGroupMember', cascade='all')
445
446
446 # comments created by this user
447 # comments created by this user
447 user_comments = relationship('ChangesetComment', cascade='all')
448 user_comments = relationship('ChangesetComment', cascade='all')
448 # extra emails for this user
449 # extra emails for this user
449 user_emails = relationship('UserEmailMap', cascade='all')
450 user_emails = relationship('UserEmailMap', cascade='all')
450 # extra API keys
451 # extra API keys
451 user_api_keys = relationship('UserApiKeys', cascade='all')
452 user_api_keys = relationship('UserApiKeys', cascade='all')
452 ssh_keys = relationship('UserSshKeys', cascade='all')
453 ssh_keys = relationship('UserSshKeys', cascade='all')
453
454
454 @hybrid_property
455 @hybrid_property
455 def email(self):
456 def email(self):
456 return self._email
457 return self._email
457
458
458 @email.setter
459 @email.setter
459 def email(self, val):
460 def email(self, val):
460 self._email = val.lower() if val else None
461 self._email = val.lower() if val else None
461
462
462 @property
463 @property
463 def firstname(self):
464 def firstname(self):
464 # alias for future
465 # alias for future
465 return self.name
466 return self.name
466
467
467 @property
468 @property
468 def emails(self):
469 def emails(self):
469 other = UserEmailMap.query().filter(UserEmailMap.user == self).all()
470 other = UserEmailMap.query().filter(UserEmailMap.user == self).all()
470 return [self.email] + [x.email for x in other]
471 return [self.email] + [x.email for x in other]
471
472
472 @property
473 @property
473 def api_keys(self):
474 def api_keys(self):
474 other = UserApiKeys.query().filter(UserApiKeys.user == self).all()
475 other = UserApiKeys.query().filter(UserApiKeys.user == self).all()
475 return [self.api_key] + [x.api_key for x in other]
476 return [self.api_key] + [x.api_key for x in other]
476
477
477 @property
478 @property
478 def ip_addresses(self):
479 def ip_addresses(self):
479 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
480 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
480 return [x.ip_addr for x in ret]
481 return [x.ip_addr for x in ret]
481
482
482 @property
483 @property
483 def full_name(self):
484 def full_name(self):
484 return '%s %s' % (self.firstname, self.lastname)
485 return '%s %s' % (self.firstname, self.lastname)
485
486
486 @property
487 @property
487 def full_name_or_username(self):
488 def full_name_or_username(self):
488 """
489 """
489 Show full name.
490 Show full name.
490 If full name is not set, fall back to username.
491 If full name is not set, fall back to username.
491 """
492 """
492 return ('%s %s' % (self.firstname, self.lastname)
493 return ('%s %s' % (self.firstname, self.lastname)
493 if (self.firstname and self.lastname) else self.username)
494 if (self.firstname and self.lastname) else self.username)
494
495
495 @property
496 @property
496 def full_name_and_username(self):
497 def full_name_and_username(self):
497 """
498 """
498 Show full name and username as 'Firstname Lastname (username)'.
499 Show full name and username as 'Firstname Lastname (username)'.
499 If full name is not set, fall back to username.
500 If full name is not set, fall back to username.
500 """
501 """
501 return ('%s %s (%s)' % (self.firstname, self.lastname, self.username)
502 return ('%s %s (%s)' % (self.firstname, self.lastname, self.username)
502 if (self.firstname and self.lastname) else self.username)
503 if (self.firstname and self.lastname) else self.username)
503
504
504 @property
505 @property
505 def full_contact(self):
506 def full_contact(self):
506 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
507 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
507
508
508 @property
509 @property
509 def short_contact(self):
510 def short_contact(self):
510 return '%s %s' % (self.firstname, self.lastname)
511 return '%s %s' % (self.firstname, self.lastname)
511
512
512 @property
513 @property
513 def is_admin(self):
514 def is_admin(self):
514 return self.admin
515 return self.admin
515
516
516 @hybrid_property
517 @hybrid_property
517 def is_default_user(self):
518 def is_default_user(self):
518 return self.username == User.DEFAULT_USER
519 return self.username == User.DEFAULT_USER
519
520
520 @hybrid_property
521 @hybrid_property
521 def user_data(self):
522 def user_data(self):
522 if not self._user_data:
523 if not self._user_data:
523 return {}
524 return {}
524
525
525 try:
526 try:
526 return json.loads(self._user_data)
527 return json.loads(self._user_data)
527 except TypeError:
528 except TypeError:
528 return {}
529 return {}
529
530
530 @user_data.setter
531 @user_data.setter
531 def user_data(self, val):
532 def user_data(self, val):
532 try:
533 try:
533 self._user_data = json.dumps(val)
534 self._user_data = json.dumps(val)
534 except Exception:
535 except Exception:
535 log.error(traceback.format_exc())
536 log.error(traceback.format_exc())
536
537
537 def __unicode__(self):
538 def __unicode__(self):
538 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
539 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
539 self.user_id, self.username)
540 self.user_id, self.username)
540
541
541 @classmethod
542 @classmethod
542 def guess_instance(cls, value):
543 def guess_instance(cls, value):
543 return super(User, cls).guess_instance(value, User.get_by_username)
544 return super(User, cls).guess_instance(value, User.get_by_username)
544
545
545 @classmethod
546 @classmethod
546 def get_or_404(cls, id_, allow_default=True):
547 def get_or_404(cls, id_, allow_default=True):
547 '''
548 '''
548 Overridden version of BaseDbModel.get_or_404, with an extra check on
549 Overridden version of BaseDbModel.get_or_404, with an extra check on
549 the default user.
550 the default user.
550 '''
551 '''
551 user = super(User, cls).get_or_404(id_)
552 user = super(User, cls).get_or_404(id_)
552 if not allow_default and user.is_default_user:
553 if not allow_default and user.is_default_user:
553 raise DefaultUserException()
554 raise DefaultUserException()
554 return user
555 return user
555
556
556 @classmethod
557 @classmethod
557 def get_by_username_or_email(cls, username_or_email, case_insensitive=False, cache=False):
558 def get_by_username_or_email(cls, username_or_email, case_insensitive=False, cache=False):
558 """
559 """
559 For anything that looks like an email address, look up by the email address (matching
560 For anything that looks like an email address, look up by the email address (matching
560 case insensitively).
561 case insensitively).
561 For anything else, try to look up by the user name.
562 For anything else, try to look up by the user name.
562
563
563 This assumes no normal username can have '@' symbol.
564 This assumes no normal username can have '@' symbol.
564 """
565 """
565 if '@' in username_or_email:
566 if '@' in username_or_email:
566 return User.get_by_email(username_or_email, cache=cache)
567 return User.get_by_email(username_or_email, cache=cache)
567 else:
568 else:
568 return User.get_by_username(username_or_email, case_insensitive=case_insensitive, cache=cache)
569 return User.get_by_username(username_or_email, case_insensitive=case_insensitive, cache=cache)
569
570
570 @classmethod
571 @classmethod
571 def get_by_username(cls, username, case_insensitive=False, cache=False):
572 def get_by_username(cls, username, case_insensitive=False, cache=False):
572 if case_insensitive:
573 if case_insensitive:
573 q = cls.query().filter(func.lower(cls.username) == func.lower(username))
574 q = cls.query().filter(func.lower(cls.username) == func.lower(username))
574 else:
575 else:
575 q = cls.query().filter(cls.username == username)
576 q = cls.query().filter(cls.username == username)
576
577
577 if cache:
578 if cache:
578 q = q.options(FromCache(
579 q = q.options(FromCache(
579 "sql_cache_short",
580 "sql_cache_short",
580 "get_user_%s" % _hash_key(username)
581 "get_user_%s" % _hash_key(username)
581 )
582 )
582 )
583 )
583 return q.scalar()
584 return q.scalar()
584
585
585 @classmethod
586 @classmethod
586 def get_by_api_key(cls, api_key, cache=False, fallback=True):
587 def get_by_api_key(cls, api_key, cache=False, fallback=True):
587 if len(api_key) != 40 or not api_key.isalnum():
588 if len(api_key) != 40 or not api_key.isalnum():
588 return None
589 return None
589
590
590 q = cls.query().filter(cls.api_key == api_key)
591 q = cls.query().filter(cls.api_key == api_key)
591
592
592 if cache:
593 if cache:
593 q = q.options(FromCache("sql_cache_short",
594 q = q.options(FromCache("sql_cache_short",
594 "get_api_key_%s" % api_key))
595 "get_api_key_%s" % api_key))
595 res = q.scalar()
596 res = q.scalar()
596
597
597 if fallback and not res:
598 if fallback and not res:
598 # fallback to additional keys
599 # fallback to additional keys
599 _res = UserApiKeys.query().filter_by(api_key=api_key, is_expired=False).first()
600 _res = UserApiKeys.query().filter_by(api_key=api_key, is_expired=False).first()
600 if _res:
601 if _res:
601 res = _res.user
602 res = _res.user
602 if res is None or not res.active or res.is_default_user:
603 if res is None or not res.active or res.is_default_user:
603 return None
604 return None
604 return res
605 return res
605
606
606 @classmethod
607 @classmethod
607 def get_by_email(cls, email, cache=False):
608 def get_by_email(cls, email, cache=False):
608 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
609 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
609
610
610 if cache:
611 if cache:
611 q = q.options(FromCache("sql_cache_short",
612 q = q.options(FromCache("sql_cache_short",
612 "get_email_key_%s" % email))
613 "get_email_key_%s" % email))
613
614
614 ret = q.scalar()
615 ret = q.scalar()
615 if ret is None:
616 if ret is None:
616 q = UserEmailMap.query()
617 q = UserEmailMap.query()
617 # try fetching in alternate email map
618 # try fetching in alternate email map
618 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
619 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
619 q = q.options(joinedload(UserEmailMap.user))
620 q = q.options(joinedload(UserEmailMap.user))
620 if cache:
621 if cache:
621 q = q.options(FromCache("sql_cache_short",
622 q = q.options(FromCache("sql_cache_short",
622 "get_email_map_key_%s" % email))
623 "get_email_map_key_%s" % email))
623 ret = getattr(q.scalar(), 'user', None)
624 ret = getattr(q.scalar(), 'user', None)
624
625
625 return ret
626 return ret
626
627
627 @classmethod
628 @classmethod
628 def get_from_cs_author(cls, author):
629 def get_from_cs_author(cls, author):
629 """
630 """
630 Tries to get User objects out of commit author string
631 Tries to get User objects out of commit author string
631
632
632 :param author:
633 :param author:
633 """
634 """
634 from kallithea.lib.helpers import email, author_name
635 from kallithea.lib.helpers import email, author_name
635 # Valid email in the attribute passed, see if they're in the system
636 # Valid email in the attribute passed, see if they're in the system
636 _email = email(author)
637 _email = email(author)
637 if _email:
638 if _email:
638 user = cls.get_by_email(_email)
639 user = cls.get_by_email(_email)
639 if user is not None:
640 if user is not None:
640 return user
641 return user
641 # Maybe we can match by username?
642 # Maybe we can match by username?
642 _author = author_name(author)
643 _author = author_name(author)
643 user = cls.get_by_username(_author, case_insensitive=True)
644 user = cls.get_by_username(_author, case_insensitive=True)
644 if user is not None:
645 if user is not None:
645 return user
646 return user
646
647
647 def update_lastlogin(self):
648 def update_lastlogin(self):
648 """Update user lastlogin"""
649 """Update user lastlogin"""
649 self.last_login = datetime.datetime.now()
650 self.last_login = datetime.datetime.now()
650 log.debug('updated user %s lastlogin', self.username)
651 log.debug('updated user %s lastlogin', self.username)
651
652
652 @classmethod
653 @classmethod
653 def get_first_admin(cls):
654 def get_first_admin(cls):
654 user = User.query().filter(User.admin == True).first()
655 user = User.query().filter(User.admin == True).first()
655 if user is None:
656 if user is None:
656 raise Exception('Missing administrative account!')
657 raise Exception('Missing administrative account!')
657 return user
658 return user
658
659
659 @classmethod
660 @classmethod
660 def get_default_user(cls, cache=False):
661 def get_default_user(cls, cache=False):
661 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
662 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
662 if user is None:
663 if user is None:
663 raise Exception('Missing default account!')
664 raise Exception('Missing default account!')
664 return user
665 return user
665
666
666 def get_api_data(self, details=False):
667 def get_api_data(self, details=False):
667 """
668 """
668 Common function for generating user related data for API
669 Common function for generating user related data for API
669 """
670 """
670 user = self
671 user = self
671 data = dict(
672 data = dict(
672 user_id=user.user_id,
673 user_id=user.user_id,
673 username=user.username,
674 username=user.username,
674 firstname=user.name,
675 firstname=user.name,
675 lastname=user.lastname,
676 lastname=user.lastname,
676 email=user.email,
677 email=user.email,
677 emails=user.emails,
678 emails=user.emails,
678 active=user.active,
679 active=user.active,
679 admin=user.admin,
680 admin=user.admin,
680 )
681 )
681 if details:
682 if details:
682 data.update(dict(
683 data.update(dict(
683 extern_type=user.extern_type,
684 extern_type=user.extern_type,
684 extern_name=user.extern_name,
685 extern_name=user.extern_name,
685 api_key=user.api_key,
686 api_key=user.api_key,
686 api_keys=user.api_keys,
687 api_keys=user.api_keys,
687 last_login=user.last_login,
688 last_login=user.last_login,
688 ip_addresses=user.ip_addresses
689 ip_addresses=user.ip_addresses
689 ))
690 ))
690 return data
691 return data
691
692
692 def __json__(self):
693 def __json__(self):
693 data = dict(
694 data = dict(
694 full_name=self.full_name,
695 full_name=self.full_name,
695 full_name_or_username=self.full_name_or_username,
696 full_name_or_username=self.full_name_or_username,
696 short_contact=self.short_contact,
697 short_contact=self.short_contact,
697 full_contact=self.full_contact
698 full_contact=self.full_contact
698 )
699 )
699 data.update(self.get_api_data())
700 data.update(self.get_api_data())
700 return data
701 return data
701
702
702
703
703 class UserApiKeys(Base, BaseDbModel):
704 class UserApiKeys(Base, BaseDbModel):
704 __tablename__ = 'user_api_keys'
705 __tablename__ = 'user_api_keys'
705 __table_args__ = (
706 __table_args__ = (
706 Index('uak_api_key_idx', 'api_key'),
707 Index('uak_api_key_idx', 'api_key'),
707 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
708 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
708 _table_args_default_dict,
709 _table_args_default_dict,
709 )
710 )
710
711
711 user_api_key_id = Column(Integer(), primary_key=True)
712 user_api_key_id = Column(Integer(), primary_key=True)
712 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
713 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
713 api_key = Column(String(255), nullable=False, unique=True)
714 api_key = Column(String(255), nullable=False, unique=True)
714 description = Column(UnicodeText(), nullable=False)
715 description = Column(UnicodeText(), nullable=False)
715 expires = Column(Float(53), nullable=False)
716 expires = Column(Float(53), nullable=False)
716 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
717 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
717
718
718 user = relationship('User')
719 user = relationship('User')
719
720
720 @hybrid_property
721 @hybrid_property
721 def is_expired(self):
722 def is_expired(self):
722 return (self.expires != -1) & (time.time() > self.expires)
723 return (self.expires != -1) & (time.time() > self.expires)
723
724
724
725
725 class UserEmailMap(Base, BaseDbModel):
726 class UserEmailMap(Base, BaseDbModel):
726 __tablename__ = 'user_email_map'
727 __tablename__ = 'user_email_map'
727 __table_args__ = (
728 __table_args__ = (
728 Index('uem_email_idx', 'email'),
729 Index('uem_email_idx', 'email'),
729 _table_args_default_dict,
730 _table_args_default_dict,
730 )
731 )
731
732
732 email_id = Column(Integer(), primary_key=True)
733 email_id = Column(Integer(), primary_key=True)
733 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
734 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
734 _email = Column("email", String(255), nullable=False, unique=True)
735 _email = Column("email", String(255), nullable=False, unique=True)
735 user = relationship('User')
736 user = relationship('User')
736
737
737 @validates('_email')
738 @validates('_email')
738 def validate_email(self, key, email):
739 def validate_email(self, key, email):
739 # check if this email is not main one
740 # check if this email is not main one
740 main_email = Session().query(User).filter(User.email == email).scalar()
741 main_email = Session().query(User).filter(User.email == email).scalar()
741 if main_email is not None:
742 if main_email is not None:
742 raise AttributeError('email %s is present is user table' % email)
743 raise AttributeError('email %s is present is user table' % email)
743 return email
744 return email
744
745
745 @hybrid_property
746 @hybrid_property
746 def email(self):
747 def email(self):
747 return self._email
748 return self._email
748
749
749 @email.setter
750 @email.setter
750 def email(self, val):
751 def email(self, val):
751 self._email = val.lower() if val else None
752 self._email = val.lower() if val else None
752
753
753
754
754 class UserIpMap(Base, BaseDbModel):
755 class UserIpMap(Base, BaseDbModel):
755 __tablename__ = 'user_ip_map'
756 __tablename__ = 'user_ip_map'
756 __table_args__ = (
757 __table_args__ = (
757 UniqueConstraint('user_id', 'ip_addr'),
758 UniqueConstraint('user_id', 'ip_addr'),
758 _table_args_default_dict,
759 _table_args_default_dict,
759 )
760 )
760
761
761 ip_id = Column(Integer(), primary_key=True)
762 ip_id = Column(Integer(), primary_key=True)
762 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
763 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
763 ip_addr = Column(String(255), nullable=False)
764 ip_addr = Column(String(255), nullable=False)
764 active = Column(Boolean(), nullable=False, default=True)
765 active = Column(Boolean(), nullable=False, default=True)
765 user = relationship('User')
766 user = relationship('User')
766
767
767 @classmethod
768 @classmethod
768 def _get_ip_range(cls, ip_addr):
769 def _get_ip_range(cls, ip_addr):
769 net = ipaddr.IPNetwork(address=ip_addr)
770 net = ipaddr.IPNetwork(address=ip_addr)
770 return [str(net.network), str(net.broadcast)]
771 return [str(net.network), str(net.broadcast)]
771
772
772 def __json__(self):
773 def __json__(self):
773 return dict(
774 return dict(
774 ip_addr=self.ip_addr,
775 ip_addr=self.ip_addr,
775 ip_range=self._get_ip_range(self.ip_addr)
776 ip_range=self._get_ip_range(self.ip_addr)
776 )
777 )
777
778
778 def __unicode__(self):
779 def __unicode__(self):
779 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
780 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
780 self.user_id, self.ip_addr)
781 self.user_id, self.ip_addr)
781
782
782
783
783 class UserLog(Base, BaseDbModel):
784 class UserLog(Base, BaseDbModel):
784 __tablename__ = 'user_logs'
785 __tablename__ = 'user_logs'
785 __table_args__ = (
786 __table_args__ = (
786 _table_args_default_dict,
787 _table_args_default_dict,
787 )
788 )
788
789
789 user_log_id = Column(Integer(), primary_key=True)
790 user_log_id = Column(Integer(), primary_key=True)
790 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
791 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
791 username = Column(String(255), nullable=False)
792 username = Column(String(255), nullable=False)
792 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
793 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
793 repository_name = Column(Unicode(255), nullable=False)
794 repository_name = Column(Unicode(255), nullable=False)
794 user_ip = Column(String(255), nullable=True)
795 user_ip = Column(String(255), nullable=True)
795 action = Column(UnicodeText(), nullable=False)
796 action = Column(UnicodeText(), nullable=False)
796 action_date = Column(DateTime(timezone=False), nullable=False)
797 action_date = Column(DateTime(timezone=False), nullable=False)
797
798
798 def __unicode__(self):
799 def __unicode__(self):
799 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
800 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
800 self.repository_name,
801 self.repository_name,
801 self.action)
802 self.action)
802
803
803 @property
804 @property
804 def action_as_day(self):
805 def action_as_day(self):
805 return datetime.date(*self.action_date.timetuple()[:3])
806 return datetime.date(*self.action_date.timetuple()[:3])
806
807
807 user = relationship('User')
808 user = relationship('User')
808 repository = relationship('Repository', cascade='')
809 repository = relationship('Repository', cascade='')
809
810
810
811
811 class UserGroup(Base, BaseDbModel):
812 class UserGroup(Base, BaseDbModel):
812 __tablename__ = 'users_groups'
813 __tablename__ = 'users_groups'
813 __table_args__ = (
814 __table_args__ = (
814 _table_args_default_dict,
815 _table_args_default_dict,
815 )
816 )
816
817
817 users_group_id = Column(Integer(), primary_key=True)
818 users_group_id = Column(Integer(), primary_key=True)
818 users_group_name = Column(Unicode(255), nullable=False, unique=True)
819 users_group_name = Column(Unicode(255), nullable=False, unique=True)
819 user_group_description = Column(Unicode(10000), nullable=True) # FIXME: not nullable?
820 user_group_description = Column(Unicode(10000), nullable=True) # FIXME: not nullable?
820 users_group_active = Column(Boolean(), nullable=False)
821 users_group_active = Column(Boolean(), nullable=False)
821 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
822 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
822 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
823 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
823 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
824 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
824
825
825 members = relationship('UserGroupMember', cascade="all, delete-orphan")
826 members = relationship('UserGroupMember', cascade="all, delete-orphan")
826 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
827 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
827 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
828 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
828 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
829 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
829 user_user_group_to_perm = relationship('UserUserGroupToPerm ', cascade='all')
830 user_user_group_to_perm = relationship('UserUserGroupToPerm ', cascade='all')
830 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
831 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
831
832
832 owner = relationship('User')
833 owner = relationship('User')
833
834
834 @hybrid_property
835 @hybrid_property
835 def group_data(self):
836 def group_data(self):
836 if not self._group_data:
837 if not self._group_data:
837 return {}
838 return {}
838
839
839 try:
840 try:
840 return json.loads(self._group_data)
841 return json.loads(self._group_data)
841 except TypeError:
842 except TypeError:
842 return {}
843 return {}
843
844
844 @group_data.setter
845 @group_data.setter
845 def group_data(self, val):
846 def group_data(self, val):
846 try:
847 try:
847 self._group_data = json.dumps(val)
848 self._group_data = json.dumps(val)
848 except Exception:
849 except Exception:
849 log.error(traceback.format_exc())
850 log.error(traceback.format_exc())
850
851
851 def __unicode__(self):
852 def __unicode__(self):
852 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
853 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
853 self.users_group_id,
854 self.users_group_id,
854 self.users_group_name)
855 self.users_group_name)
855
856
856 @classmethod
857 @classmethod
857 def guess_instance(cls, value):
858 def guess_instance(cls, value):
858 return super(UserGroup, cls).guess_instance(value, UserGroup.get_by_group_name)
859 return super(UserGroup, cls).guess_instance(value, UserGroup.get_by_group_name)
859
860
860 @classmethod
861 @classmethod
861 def get_by_group_name(cls, group_name, cache=False,
862 def get_by_group_name(cls, group_name, cache=False,
862 case_insensitive=False):
863 case_insensitive=False):
863 if case_insensitive:
864 if case_insensitive:
864 q = cls.query().filter(func.lower(cls.users_group_name) == func.lower(group_name))
865 q = cls.query().filter(func.lower(cls.users_group_name) == func.lower(group_name))
865 else:
866 else:
866 q = cls.query().filter(cls.users_group_name == group_name)
867 q = cls.query().filter(cls.users_group_name == group_name)
867 if cache:
868 if cache:
868 q = q.options(FromCache(
869 q = q.options(FromCache(
869 "sql_cache_short",
870 "sql_cache_short",
870 "get_group_%s" % _hash_key(group_name)
871 "get_group_%s" % _hash_key(group_name)
871 )
872 )
872 )
873 )
873 return q.scalar()
874 return q.scalar()
874
875
875 @classmethod
876 @classmethod
876 def get(cls, user_group_id, cache=False):
877 def get(cls, user_group_id, cache=False):
877 user_group = cls.query()
878 user_group = cls.query()
878 if cache:
879 if cache:
879 user_group = user_group.options(FromCache("sql_cache_short",
880 user_group = user_group.options(FromCache("sql_cache_short",
880 "get_users_group_%s" % user_group_id))
881 "get_users_group_%s" % user_group_id))
881 return user_group.get(user_group_id)
882 return user_group.get(user_group_id)
882
883
883 def get_api_data(self, with_members=True):
884 def get_api_data(self, with_members=True):
884 user_group = self
885 user_group = self
885
886
886 data = dict(
887 data = dict(
887 users_group_id=user_group.users_group_id,
888 users_group_id=user_group.users_group_id,
888 group_name=user_group.users_group_name,
889 group_name=user_group.users_group_name,
889 group_description=user_group.user_group_description,
890 group_description=user_group.user_group_description,
890 active=user_group.users_group_active,
891 active=user_group.users_group_active,
891 owner=user_group.owner.username,
892 owner=user_group.owner.username,
892 )
893 )
893 if with_members:
894 if with_members:
894 data['members'] = [
895 data['members'] = [
895 ugm.user.get_api_data()
896 ugm.user.get_api_data()
896 for ugm in user_group.members
897 for ugm in user_group.members
897 ]
898 ]
898
899
899 return data
900 return data
900
901
901
902
902 class UserGroupMember(Base, BaseDbModel):
903 class UserGroupMember(Base, BaseDbModel):
903 __tablename__ = 'users_groups_members'
904 __tablename__ = 'users_groups_members'
904 __table_args__ = (
905 __table_args__ = (
905 _table_args_default_dict,
906 _table_args_default_dict,
906 )
907 )
907
908
908 users_group_member_id = Column(Integer(), primary_key=True)
909 users_group_member_id = Column(Integer(), primary_key=True)
909 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
910 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
910 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
911 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
911
912
912 user = relationship('User')
913 user = relationship('User')
913 users_group = relationship('UserGroup')
914 users_group = relationship('UserGroup')
914
915
915 def __init__(self, gr_id='', u_id=''):
916 def __init__(self, gr_id='', u_id=''):
916 self.users_group_id = gr_id
917 self.users_group_id = gr_id
917 self.user_id = u_id
918 self.user_id = u_id
918
919
919
920
920 class RepositoryField(Base, BaseDbModel):
921 class RepositoryField(Base, BaseDbModel):
921 __tablename__ = 'repositories_fields'
922 __tablename__ = 'repositories_fields'
922 __table_args__ = (
923 __table_args__ = (
923 UniqueConstraint('repository_id', 'field_key'), # no-multi field
924 UniqueConstraint('repository_id', 'field_key'), # no-multi field
924 _table_args_default_dict,
925 _table_args_default_dict,
925 )
926 )
926
927
927 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
928 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
928
929
929 repo_field_id = Column(Integer(), primary_key=True)
930 repo_field_id = Column(Integer(), primary_key=True)
930 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
931 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
931 field_key = Column(String(250), nullable=False)
932 field_key = Column(String(250), nullable=False)
932 field_label = Column(String(1024), nullable=False)
933 field_label = Column(String(1024), nullable=False)
933 field_value = Column(String(10000), nullable=False)
934 field_value = Column(String(10000), nullable=False)
934 field_desc = Column(String(1024), nullable=False)
935 field_desc = Column(String(1024), nullable=False)
935 field_type = Column(String(255), nullable=False)
936 field_type = Column(String(255), nullable=False)
936 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
937 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
937
938
938 repository = relationship('Repository')
939 repository = relationship('Repository')
939
940
940 @property
941 @property
941 def field_key_prefixed(self):
942 def field_key_prefixed(self):
942 return 'ex_%s' % self.field_key
943 return 'ex_%s' % self.field_key
943
944
944 @classmethod
945 @classmethod
945 def un_prefix_key(cls, key):
946 def un_prefix_key(cls, key):
946 if key.startswith(cls.PREFIX):
947 if key.startswith(cls.PREFIX):
947 return key[len(cls.PREFIX):]
948 return key[len(cls.PREFIX):]
948 return key
949 return key
949
950
950 @classmethod
951 @classmethod
951 def get_by_key_name(cls, key, repo):
952 def get_by_key_name(cls, key, repo):
952 row = cls.query() \
953 row = cls.query() \
953 .filter(cls.repository == repo) \
954 .filter(cls.repository == repo) \
954 .filter(cls.field_key == key).scalar()
955 .filter(cls.field_key == key).scalar()
955 return row
956 return row
956
957
957
958
958 class Repository(Base, BaseDbModel):
959 class Repository(Base, BaseDbModel):
959 __tablename__ = 'repositories'
960 __tablename__ = 'repositories'
960 __table_args__ = (
961 __table_args__ = (
961 Index('r_repo_name_idx', 'repo_name'),
962 Index('r_repo_name_idx', 'repo_name'),
962 _table_args_default_dict,
963 _table_args_default_dict,
963 )
964 )
964
965
965 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
966 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
966 DEFAULT_CLONE_SSH = 'ssh://{system_user}@{hostname}/{repo}'
967 DEFAULT_CLONE_SSH = 'ssh://{system_user}@{hostname}/{repo}'
967
968
968 STATE_CREATED = u'repo_state_created'
969 STATE_CREATED = u'repo_state_created'
969 STATE_PENDING = u'repo_state_pending'
970 STATE_PENDING = u'repo_state_pending'
970 STATE_ERROR = u'repo_state_error'
971 STATE_ERROR = u'repo_state_error'
971
972
972 repo_id = Column(Integer(), primary_key=True)
973 repo_id = Column(Integer(), primary_key=True)
973 repo_name = Column(Unicode(255), nullable=False, unique=True)
974 repo_name = Column(Unicode(255), nullable=False, unique=True)
974 repo_state = Column(String(255), nullable=False)
975 repo_state = Column(String(255), nullable=False)
975
976
976 clone_uri = Column(String(255), nullable=True) # FIXME: not nullable?
977 clone_uri = Column(String(255), nullable=True) # FIXME: not nullable?
977 repo_type = Column(String(255), nullable=False) # 'hg' or 'git'
978 repo_type = Column(String(255), nullable=False) # 'hg' or 'git'
978 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
979 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
979 private = Column(Boolean(), nullable=False)
980 private = Column(Boolean(), nullable=False)
980 enable_statistics = Column("statistics", Boolean(), nullable=False, default=True)
981 enable_statistics = Column("statistics", Boolean(), nullable=False, default=True)
981 enable_downloads = Column("downloads", Boolean(), nullable=False, default=True)
982 enable_downloads = Column("downloads", Boolean(), nullable=False, default=True)
982 description = Column(Unicode(10000), nullable=False)
983 description = Column(Unicode(10000), nullable=False)
983 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
984 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
984 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
985 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
985 _landing_revision = Column("landing_revision", String(255), nullable=False)
986 _landing_revision = Column("landing_revision", String(255), nullable=False)
986 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
987 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
987
988
988 fork_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
989 fork_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
989 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=True)
990 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=True)
990
991
991 owner = relationship('User')
992 owner = relationship('User')
992 fork = relationship('Repository', remote_side=repo_id)
993 fork = relationship('Repository', remote_side=repo_id)
993 group = relationship('RepoGroup')
994 group = relationship('RepoGroup')
994 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
995 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
995 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
996 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
996 stats = relationship('Statistics', cascade='all', uselist=False)
997 stats = relationship('Statistics', cascade='all', uselist=False)
997
998
998 followers = relationship('UserFollowing',
999 followers = relationship('UserFollowing',
999 primaryjoin='UserFollowing.follows_repository_id==Repository.repo_id',
1000 primaryjoin='UserFollowing.follows_repository_id==Repository.repo_id',
1000 cascade='all')
1001 cascade='all')
1001 extra_fields = relationship('RepositoryField',
1002 extra_fields = relationship('RepositoryField',
1002 cascade="all, delete-orphan")
1003 cascade="all, delete-orphan")
1003
1004
1004 logs = relationship('UserLog')
1005 logs = relationship('UserLog')
1005 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
1006 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
1006
1007
1007 pull_requests_org = relationship('PullRequest',
1008 pull_requests_org = relationship('PullRequest',
1008 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
1009 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
1009 cascade="all, delete-orphan")
1010 cascade="all, delete-orphan")
1010
1011
1011 pull_requests_other = relationship('PullRequest',
1012 pull_requests_other = relationship('PullRequest',
1012 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
1013 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
1013 cascade="all, delete-orphan")
1014 cascade="all, delete-orphan")
1014
1015
1015 def __unicode__(self):
1016 def __unicode__(self):
1016 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1017 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1017 safe_unicode(self.repo_name))
1018 safe_unicode(self.repo_name))
1018
1019
1019 @hybrid_property
1020 @hybrid_property
1020 def landing_rev(self):
1021 def landing_rev(self):
1021 # always should return [rev_type, rev]
1022 # always should return [rev_type, rev]
1022 if self._landing_revision:
1023 if self._landing_revision:
1023 _rev_info = self._landing_revision.split(':')
1024 _rev_info = self._landing_revision.split(':')
1024 if len(_rev_info) < 2:
1025 if len(_rev_info) < 2:
1025 _rev_info.insert(0, 'rev')
1026 _rev_info.insert(0, 'rev')
1026 return [_rev_info[0], _rev_info[1]]
1027 return [_rev_info[0], _rev_info[1]]
1027 return [None, None]
1028 return [None, None]
1028
1029
1029 @landing_rev.setter
1030 @landing_rev.setter
1030 def landing_rev(self, val):
1031 def landing_rev(self, val):
1031 if ':' not in val:
1032 if ':' not in val:
1032 raise ValueError('value must be delimited with `:` and consist '
1033 raise ValueError('value must be delimited with `:` and consist '
1033 'of <rev_type>:<rev>, got %s instead' % val)
1034 'of <rev_type>:<rev>, got %s instead' % val)
1034 self._landing_revision = val
1035 self._landing_revision = val
1035
1036
1036 @hybrid_property
1037 @hybrid_property
1037 def changeset_cache(self):
1038 def changeset_cache(self):
1038 try:
1039 try:
1039 cs_cache = json.loads(self._changeset_cache) # might raise on bad data
1040 cs_cache = json.loads(self._changeset_cache) # might raise on bad data
1040 cs_cache['raw_id'] # verify data, raise exception on error
1041 cs_cache['raw_id'] # verify data, raise exception on error
1041 return cs_cache
1042 return cs_cache
1042 except (TypeError, KeyError, ValueError):
1043 except (TypeError, KeyError, ValueError):
1043 return EmptyChangeset().__json__()
1044 return EmptyChangeset().__json__()
1044
1045
1045 @changeset_cache.setter
1046 @changeset_cache.setter
1046 def changeset_cache(self, val):
1047 def changeset_cache(self, val):
1047 try:
1048 try:
1048 self._changeset_cache = json.dumps(val)
1049 self._changeset_cache = json.dumps(val)
1049 except Exception:
1050 except Exception:
1050 log.error(traceback.format_exc())
1051 log.error(traceback.format_exc())
1051
1052
1052 @classmethod
1053 @classmethod
1053 def query(cls, sorted=False):
1054 def query(cls, sorted=False):
1054 """Add Repository-specific helpers for common query constructs.
1055 """Add Repository-specific helpers for common query constructs.
1055
1056
1056 sorted: if True, apply the default ordering (name, case insensitive).
1057 sorted: if True, apply the default ordering (name, case insensitive).
1057 """
1058 """
1058 q = super(Repository, cls).query()
1059 q = super(Repository, cls).query()
1059
1060
1060 if sorted:
1061 if sorted:
1061 q = q.order_by(func.lower(Repository.repo_name))
1062 q = q.order_by(func.lower(Repository.repo_name))
1062
1063
1063 return q
1064 return q
1064
1065
1065 @classmethod
1066 @classmethod
1066 def url_sep(cls):
1067 def url_sep(cls):
1067 return URL_SEP
1068 return URL_SEP
1068
1069
1069 @classmethod
1070 @classmethod
1070 def normalize_repo_name(cls, repo_name):
1071 def normalize_repo_name(cls, repo_name):
1071 """
1072 """
1072 Normalizes os specific repo_name to the format internally stored inside
1073 Normalizes os specific repo_name to the format internally stored inside
1073 database using URL_SEP
1074 database using URL_SEP
1074
1075
1075 :param cls:
1076 :param cls:
1076 :param repo_name:
1077 :param repo_name:
1077 """
1078 """
1078 return cls.url_sep().join(repo_name.split(os.sep))
1079 return cls.url_sep().join(repo_name.split(os.sep))
1079
1080
1080 @classmethod
1081 @classmethod
1081 def guess_instance(cls, value):
1082 def guess_instance(cls, value):
1082 return super(Repository, cls).guess_instance(value, Repository.get_by_repo_name)
1083 return super(Repository, cls).guess_instance(value, Repository.get_by_repo_name)
1083
1084
1084 @classmethod
1085 @classmethod
1085 def get_by_repo_name(cls, repo_name, case_insensitive=False):
1086 def get_by_repo_name(cls, repo_name, case_insensitive=False):
1086 """Get the repo, defaulting to database case sensitivity.
1087 """Get the repo, defaulting to database case sensitivity.
1087 case_insensitive will be slower and should only be specified if necessary."""
1088 case_insensitive will be slower and should only be specified if necessary."""
1088 if case_insensitive:
1089 if case_insensitive:
1089 q = Session().query(cls).filter(func.lower(cls.repo_name) == func.lower(repo_name))
1090 q = Session().query(cls).filter(func.lower(cls.repo_name) == func.lower(repo_name))
1090 else:
1091 else:
1091 q = Session().query(cls).filter(cls.repo_name == repo_name)
1092 q = Session().query(cls).filter(cls.repo_name == repo_name)
1092 q = q.options(joinedload(Repository.fork)) \
1093 q = q.options(joinedload(Repository.fork)) \
1093 .options(joinedload(Repository.owner)) \
1094 .options(joinedload(Repository.owner)) \
1094 .options(joinedload(Repository.group))
1095 .options(joinedload(Repository.group))
1095 return q.scalar()
1096 return q.scalar()
1096
1097
1097 @classmethod
1098 @classmethod
1098 def get_by_full_path(cls, repo_full_path):
1099 def get_by_full_path(cls, repo_full_path):
1099 base_full_path = os.path.realpath(cls.base_path())
1100 base_full_path = os.path.realpath(cls.base_path())
1100 repo_full_path = os.path.realpath(repo_full_path)
1101 repo_full_path = os.path.realpath(repo_full_path)
1101 assert repo_full_path.startswith(base_full_path + os.path.sep)
1102 assert repo_full_path.startswith(base_full_path + os.path.sep)
1102 repo_name = repo_full_path[len(base_full_path) + 1:]
1103 repo_name = repo_full_path[len(base_full_path) + 1:]
1103 repo_name = cls.normalize_repo_name(repo_name)
1104 repo_name = cls.normalize_repo_name(repo_name)
1104 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1105 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1105
1106
1106 @classmethod
1107 @classmethod
1107 def get_repo_forks(cls, repo_id):
1108 def get_repo_forks(cls, repo_id):
1108 return cls.query().filter(Repository.fork_id == repo_id)
1109 return cls.query().filter(Repository.fork_id == repo_id)
1109
1110
1110 @classmethod
1111 @classmethod
1111 def base_path(cls):
1112 def base_path(cls):
1112 """
1113 """
1113 Returns base path where all repos are stored
1114 Returns base path where all repos are stored
1114
1115
1115 :param cls:
1116 :param cls:
1116 """
1117 """
1117 q = Session().query(Ui) \
1118 q = Session().query(Ui) \
1118 .filter(Ui.ui_key == cls.url_sep())
1119 .filter(Ui.ui_key == cls.url_sep())
1119 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1120 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1120 return q.one().ui_value
1121 return q.one().ui_value
1121
1122
1122 @property
1123 @property
1123 def forks(self):
1124 def forks(self):
1124 """
1125 """
1125 Return forks of this repo
1126 Return forks of this repo
1126 """
1127 """
1127 return Repository.get_repo_forks(self.repo_id)
1128 return Repository.get_repo_forks(self.repo_id)
1128
1129
1129 @property
1130 @property
1130 def parent(self):
1131 def parent(self):
1131 """
1132 """
1132 Returns fork parent
1133 Returns fork parent
1133 """
1134 """
1134 return self.fork
1135 return self.fork
1135
1136
1136 @property
1137 @property
1137 def just_name(self):
1138 def just_name(self):
1138 return self.repo_name.split(Repository.url_sep())[-1]
1139 return self.repo_name.split(Repository.url_sep())[-1]
1139
1140
1140 @property
1141 @property
1141 def groups_with_parents(self):
1142 def groups_with_parents(self):
1142 groups = []
1143 groups = []
1143 group = self.group
1144 group = self.group
1144 while group is not None:
1145 while group is not None:
1145 groups.append(group)
1146 groups.append(group)
1146 group = group.parent_group
1147 group = group.parent_group
1147 assert group not in groups, group # avoid recursion on bad db content
1148 assert group not in groups, group # avoid recursion on bad db content
1148 groups.reverse()
1149 groups.reverse()
1149 return groups
1150 return groups
1150
1151
1151 @LazyProperty
1152 @LazyProperty
1152 def repo_path(self):
1153 def repo_path(self):
1153 """
1154 """
1154 Returns base full path for that repository means where it actually
1155 Returns base full path for that repository means where it actually
1155 exists on a filesystem
1156 exists on a filesystem
1156 """
1157 """
1157 q = Session().query(Ui).filter(Ui.ui_key ==
1158 q = Session().query(Ui).filter(Ui.ui_key ==
1158 Repository.url_sep())
1159 Repository.url_sep())
1159 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1160 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1160 return q.one().ui_value
1161 return q.one().ui_value
1161
1162
1162 @property
1163 @property
1163 def repo_full_path(self):
1164 def repo_full_path(self):
1164 p = [self.repo_path]
1165 p = [self.repo_path]
1165 # we need to split the name by / since this is how we store the
1166 # we need to split the name by / since this is how we store the
1166 # names in the database, but that eventually needs to be converted
1167 # names in the database, but that eventually needs to be converted
1167 # into a valid system path
1168 # into a valid system path
1168 p += self.repo_name.split(Repository.url_sep())
1169 p += self.repo_name.split(Repository.url_sep())
1169 return os.path.join(*(safe_unicode(d) for d in p))
1170 return os.path.join(*(safe_unicode(d) for d in p))
1170
1171
1171 @property
1172 @property
1172 def cache_keys(self):
1173 def cache_keys(self):
1173 """
1174 """
1174 Returns associated cache keys for that repo
1175 Returns associated cache keys for that repo
1175 """
1176 """
1176 return CacheInvalidation.query() \
1177 return CacheInvalidation.query() \
1177 .filter(CacheInvalidation.cache_args == self.repo_name) \
1178 .filter(CacheInvalidation.cache_args == self.repo_name) \
1178 .order_by(CacheInvalidation.cache_key) \
1179 .order_by(CacheInvalidation.cache_key) \
1179 .all()
1180 .all()
1180
1181
1181 def get_new_name(self, repo_name):
1182 def get_new_name(self, repo_name):
1182 """
1183 """
1183 returns new full repository name based on assigned group and new new
1184 returns new full repository name based on assigned group and new new
1184
1185
1185 :param group_name:
1186 :param group_name:
1186 """
1187 """
1187 path_prefix = self.group.full_path_splitted if self.group else []
1188 path_prefix = self.group.full_path_splitted if self.group else []
1188 return Repository.url_sep().join(path_prefix + [repo_name])
1189 return Repository.url_sep().join(path_prefix + [repo_name])
1189
1190
1190 @property
1191 @property
1191 def _ui(self):
1192 def _ui(self):
1192 """
1193 """
1193 Creates an db based ui object for this repository
1194 Creates an db based ui object for this repository
1194 """
1195 """
1195 from kallithea.lib.utils import make_ui
1196 from kallithea.lib.utils import make_ui
1196 return make_ui()
1197 return make_ui()
1197
1198
1198 @classmethod
1199 @classmethod
1199 def is_valid(cls, repo_name):
1200 def is_valid(cls, repo_name):
1200 """
1201 """
1201 returns True if given repo name is a valid filesystem repository
1202 returns True if given repo name is a valid filesystem repository
1202
1203
1203 :param cls:
1204 :param cls:
1204 :param repo_name:
1205 :param repo_name:
1205 """
1206 """
1206 from kallithea.lib.utils import is_valid_repo
1207 from kallithea.lib.utils import is_valid_repo
1207
1208
1208 return is_valid_repo(repo_name, cls.base_path())
1209 return is_valid_repo(repo_name, cls.base_path())
1209
1210
1210 def get_api_data(self, with_revision_names=False,
1211 def get_api_data(self, with_revision_names=False,
1211 with_pullrequests=False):
1212 with_pullrequests=False):
1212 """
1213 """
1213 Common function for generating repo api data.
1214 Common function for generating repo api data.
1214 Optionally, also return tags, branches, bookmarks and PRs.
1215 Optionally, also return tags, branches, bookmarks and PRs.
1215 """
1216 """
1216 repo = self
1217 repo = self
1217 data = dict(
1218 data = dict(
1218 repo_id=repo.repo_id,
1219 repo_id=repo.repo_id,
1219 repo_name=repo.repo_name,
1220 repo_name=repo.repo_name,
1220 repo_type=repo.repo_type,
1221 repo_type=repo.repo_type,
1221 clone_uri=repo.clone_uri,
1222 clone_uri=repo.clone_uri,
1222 private=repo.private,
1223 private=repo.private,
1223 created_on=repo.created_on,
1224 created_on=repo.created_on,
1224 description=repo.description,
1225 description=repo.description,
1225 landing_rev=repo.landing_rev,
1226 landing_rev=repo.landing_rev,
1226 owner=repo.owner.username,
1227 owner=repo.owner.username,
1227 fork_of=repo.fork.repo_name if repo.fork else None,
1228 fork_of=repo.fork.repo_name if repo.fork else None,
1228 enable_statistics=repo.enable_statistics,
1229 enable_statistics=repo.enable_statistics,
1229 enable_downloads=repo.enable_downloads,
1230 enable_downloads=repo.enable_downloads,
1230 last_changeset=repo.changeset_cache,
1231 last_changeset=repo.changeset_cache,
1231 )
1232 )
1232 if with_revision_names:
1233 if with_revision_names:
1233 scm_repo = repo.scm_instance_no_cache()
1234 scm_repo = repo.scm_instance_no_cache()
1234 data.update(dict(
1235 data.update(dict(
1235 tags=scm_repo.tags,
1236 tags=scm_repo.tags,
1236 branches=scm_repo.branches,
1237 branches=scm_repo.branches,
1237 bookmarks=scm_repo.bookmarks,
1238 bookmarks=scm_repo.bookmarks,
1238 ))
1239 ))
1239 if with_pullrequests:
1240 if with_pullrequests:
1240 data['pull_requests'] = repo.pull_requests_other
1241 data['pull_requests'] = repo.pull_requests_other
1241 rc_config = Setting.get_app_settings()
1242 rc_config = Setting.get_app_settings()
1242 repository_fields = str2bool(rc_config.get('repository_fields'))
1243 repository_fields = str2bool(rc_config.get('repository_fields'))
1243 if repository_fields:
1244 if repository_fields:
1244 for f in self.extra_fields:
1245 for f in self.extra_fields:
1245 data[f.field_key_prefixed] = f.field_value
1246 data[f.field_key_prefixed] = f.field_value
1246
1247
1247 return data
1248 return data
1248
1249
1249 @property
1250 @property
1250 def last_db_change(self):
1251 def last_db_change(self):
1251 return self.updated_on
1252 return self.updated_on
1252
1253
1253 @property
1254 @property
1254 def clone_uri_hidden(self):
1255 def clone_uri_hidden(self):
1255 clone_uri = self.clone_uri
1256 clone_uri = self.clone_uri
1256 if clone_uri:
1257 if clone_uri:
1257 import urlobject
1258 import urlobject
1258 url_obj = urlobject.URLObject(self.clone_uri)
1259 url_obj = urlobject.URLObject(self.clone_uri)
1259 if url_obj.password:
1260 if url_obj.password:
1260 clone_uri = url_obj.with_password('*****')
1261 clone_uri = url_obj.with_password('*****')
1261 return clone_uri
1262 return clone_uri
1262
1263
1263 def clone_url(self, clone_uri_tmpl, with_id=False, username=None):
1264 def clone_url(self, clone_uri_tmpl, with_id=False, username=None):
1264 if '{repo}' not in clone_uri_tmpl and '_{repoid}' not in clone_uri_tmpl:
1265 if '{repo}' not in clone_uri_tmpl and '_{repoid}' not in clone_uri_tmpl:
1265 log.error("Configured clone_uri_tmpl %r has no '{repo}' or '_{repoid}' and cannot toggle to use repo id URLs", clone_uri_tmpl)
1266 log.error("Configured clone_uri_tmpl %r has no '{repo}' or '_{repoid}' and cannot toggle to use repo id URLs", clone_uri_tmpl)
1266 elif with_id:
1267 elif with_id:
1267 clone_uri_tmpl = clone_uri_tmpl.replace('{repo}', '_{repoid}')
1268 clone_uri_tmpl = clone_uri_tmpl.replace('{repo}', '_{repoid}')
1268 else:
1269 else:
1269 clone_uri_tmpl = clone_uri_tmpl.replace('_{repoid}', '{repo}')
1270 clone_uri_tmpl = clone_uri_tmpl.replace('_{repoid}', '{repo}')
1270
1271
1271 import kallithea.lib.helpers as h
1272 import kallithea.lib.helpers as h
1272 prefix_url = h.canonical_url('home')
1273 prefix_url = h.canonical_url('home')
1273
1274
1274 return get_clone_url(clone_uri_tmpl=clone_uri_tmpl,
1275 return get_clone_url(clone_uri_tmpl=clone_uri_tmpl,
1275 prefix_url=prefix_url,
1276 prefix_url=prefix_url,
1276 repo_name=self.repo_name,
1277 repo_name=self.repo_name,
1277 repo_id=self.repo_id,
1278 repo_id=self.repo_id,
1278 username=username)
1279 username=username)
1279
1280
1280 def set_state(self, state):
1281 def set_state(self, state):
1281 self.repo_state = state
1282 self.repo_state = state
1282
1283
1283 #==========================================================================
1284 #==========================================================================
1284 # SCM PROPERTIES
1285 # SCM PROPERTIES
1285 #==========================================================================
1286 #==========================================================================
1286
1287
1287 def get_changeset(self, rev=None):
1288 def get_changeset(self, rev=None):
1288 return get_changeset_safe(self.scm_instance, rev)
1289 return get_changeset_safe(self.scm_instance, rev)
1289
1290
1290 def get_landing_changeset(self):
1291 def get_landing_changeset(self):
1291 """
1292 """
1292 Returns landing changeset, or if that doesn't exist returns the tip
1293 Returns landing changeset, or if that doesn't exist returns the tip
1293 """
1294 """
1294 _rev_type, _rev = self.landing_rev
1295 _rev_type, _rev = self.landing_rev
1295 cs = self.get_changeset(_rev)
1296 cs = self.get_changeset(_rev)
1296 if isinstance(cs, EmptyChangeset):
1297 if isinstance(cs, EmptyChangeset):
1297 return self.get_changeset()
1298 return self.get_changeset()
1298 return cs
1299 return cs
1299
1300
1300 def update_changeset_cache(self, cs_cache=None):
1301 def update_changeset_cache(self, cs_cache=None):
1301 """
1302 """
1302 Update cache of last changeset for repository, keys should be::
1303 Update cache of last changeset for repository, keys should be::
1303
1304
1304 short_id
1305 short_id
1305 raw_id
1306 raw_id
1306 revision
1307 revision
1307 message
1308 message
1308 date
1309 date
1309 author
1310 author
1310
1311
1311 :param cs_cache:
1312 :param cs_cache:
1312 """
1313 """
1313 from kallithea.lib.vcs.backends.base import BaseChangeset
1314 from kallithea.lib.vcs.backends.base import BaseChangeset
1314 if cs_cache is None:
1315 if cs_cache is None:
1315 cs_cache = EmptyChangeset()
1316 cs_cache = EmptyChangeset()
1316 # use no-cache version here
1317 # use no-cache version here
1317 scm_repo = self.scm_instance_no_cache()
1318 scm_repo = self.scm_instance_no_cache()
1318 if scm_repo:
1319 if scm_repo:
1319 cs_cache = scm_repo.get_changeset()
1320 cs_cache = scm_repo.get_changeset()
1320
1321
1321 if isinstance(cs_cache, BaseChangeset):
1322 if isinstance(cs_cache, BaseChangeset):
1322 cs_cache = cs_cache.__json__()
1323 cs_cache = cs_cache.__json__()
1323
1324
1324 if (not self.changeset_cache or cs_cache['raw_id'] != self.changeset_cache['raw_id']):
1325 if (not self.changeset_cache or cs_cache['raw_id'] != self.changeset_cache['raw_id']):
1325 _default = datetime.datetime.fromtimestamp(0)
1326 _default = datetime.datetime.fromtimestamp(0)
1326 last_change = cs_cache.get('date') or _default
1327 last_change = cs_cache.get('date') or _default
1327 log.debug('updated repo %s with new cs cache %s',
1328 log.debug('updated repo %s with new cs cache %s',
1328 self.repo_name, cs_cache)
1329 self.repo_name, cs_cache)
1329 self.updated_on = last_change
1330 self.updated_on = last_change
1330 self.changeset_cache = cs_cache
1331 self.changeset_cache = cs_cache
1331 Session().commit()
1332 Session().commit()
1332 else:
1333 else:
1333 log.debug('changeset_cache for %s already up to date with %s',
1334 log.debug('changeset_cache for %s already up to date with %s',
1334 self.repo_name, cs_cache['raw_id'])
1335 self.repo_name, cs_cache['raw_id'])
1335
1336
1336 @property
1337 @property
1337 def tip(self):
1338 def tip(self):
1338 return self.get_changeset('tip')
1339 return self.get_changeset('tip')
1339
1340
1340 @property
1341 @property
1341 def author(self):
1342 def author(self):
1342 return self.tip.author
1343 return self.tip.author
1343
1344
1344 @property
1345 @property
1345 def last_change(self):
1346 def last_change(self):
1346 return self.scm_instance.last_change
1347 return self.scm_instance.last_change
1347
1348
1348 def get_comments(self, revisions=None):
1349 def get_comments(self, revisions=None):
1349 """
1350 """
1350 Returns comments for this repository grouped by revisions
1351 Returns comments for this repository grouped by revisions
1351
1352
1352 :param revisions: filter query by revisions only
1353 :param revisions: filter query by revisions only
1353 """
1354 """
1354 cmts = ChangesetComment.query() \
1355 cmts = ChangesetComment.query() \
1355 .filter(ChangesetComment.repo == self)
1356 .filter(ChangesetComment.repo == self)
1356 if revisions is not None:
1357 if revisions is not None:
1357 if not revisions:
1358 if not revisions:
1358 return {} # don't use sql 'in' on empty set
1359 return {} # don't use sql 'in' on empty set
1359 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1360 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1360 grouped = collections.defaultdict(list)
1361 grouped = collections.defaultdict(list)
1361 for cmt in cmts.all():
1362 for cmt in cmts.all():
1362 grouped[cmt.revision].append(cmt)
1363 grouped[cmt.revision].append(cmt)
1363 return grouped
1364 return grouped
1364
1365
1365 def statuses(self, revisions):
1366 def statuses(self, revisions):
1366 """
1367 """
1367 Returns statuses for this repository.
1368 Returns statuses for this repository.
1368 PRs without any votes do _not_ show up as unreviewed.
1369 PRs without any votes do _not_ show up as unreviewed.
1369
1370
1370 :param revisions: list of revisions to get statuses for
1371 :param revisions: list of revisions to get statuses for
1371 """
1372 """
1372 if not revisions:
1373 if not revisions:
1373 return {}
1374 return {}
1374
1375
1375 statuses = ChangesetStatus.query() \
1376 statuses = ChangesetStatus.query() \
1376 .filter(ChangesetStatus.repo == self) \
1377 .filter(ChangesetStatus.repo == self) \
1377 .filter(ChangesetStatus.version == 0) \
1378 .filter(ChangesetStatus.version == 0) \
1378 .filter(ChangesetStatus.revision.in_(revisions))
1379 .filter(ChangesetStatus.revision.in_(revisions))
1379
1380
1380 grouped = {}
1381 grouped = {}
1381 for stat in statuses.all():
1382 for stat in statuses.all():
1382 pr_id = pr_nice_id = pr_repo = None
1383 pr_id = pr_nice_id = pr_repo = None
1383 if stat.pull_request:
1384 if stat.pull_request:
1384 pr_id = stat.pull_request.pull_request_id
1385 pr_id = stat.pull_request.pull_request_id
1385 pr_nice_id = PullRequest.make_nice_id(pr_id)
1386 pr_nice_id = PullRequest.make_nice_id(pr_id)
1386 pr_repo = stat.pull_request.other_repo.repo_name
1387 pr_repo = stat.pull_request.other_repo.repo_name
1387 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1388 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1388 pr_id, pr_repo, pr_nice_id,
1389 pr_id, pr_repo, pr_nice_id,
1389 stat.author]
1390 stat.author]
1390 return grouped
1391 return grouped
1391
1392
1392 def _repo_size(self):
1393 def _repo_size(self):
1393 from kallithea.lib import helpers as h
1394 from kallithea.lib import helpers as h
1394 log.debug('calculating repository size...')
1395 log.debug('calculating repository size...')
1395 return h.format_byte_size(self.scm_instance.size)
1396 return h.format_byte_size(self.scm_instance.size)
1396
1397
1397 #==========================================================================
1398 #==========================================================================
1398 # SCM CACHE INSTANCE
1399 # SCM CACHE INSTANCE
1399 #==========================================================================
1400 #==========================================================================
1400
1401
1401 def set_invalidate(self):
1402 def set_invalidate(self):
1402 """
1403 """
1403 Mark caches of this repo as invalid.
1404 Mark caches of this repo as invalid.
1404 """
1405 """
1405 CacheInvalidation.set_invalidate(self.repo_name)
1406 CacheInvalidation.set_invalidate(self.repo_name)
1406
1407
1407 _scm_instance = None
1408 _scm_instance = None
1408
1409
1409 @property
1410 @property
1410 def scm_instance(self):
1411 def scm_instance(self):
1411 if self._scm_instance is None:
1412 if self._scm_instance is None:
1412 self._scm_instance = self.scm_instance_cached()
1413 self._scm_instance = self.scm_instance_cached()
1413 return self._scm_instance
1414 return self._scm_instance
1414
1415
1415 def scm_instance_cached(self, valid_cache_keys=None):
1416 def scm_instance_cached(self, valid_cache_keys=None):
1416 @cache_region('long_term', 'scm_instance_cached')
1417 @cache_region('long_term', 'scm_instance_cached')
1417 def _c(repo_name): # repo_name is just for the cache key
1418 def _c(repo_name): # repo_name is just for the cache key
1418 log.debug('Creating new %s scm_instance and populating cache', repo_name)
1419 log.debug('Creating new %s scm_instance and populating cache', repo_name)
1419 return self.scm_instance_no_cache()
1420 return self.scm_instance_no_cache()
1420 rn = self.repo_name
1421 rn = self.repo_name
1421
1422
1422 valid = CacheInvalidation.test_and_set_valid(rn, None, valid_cache_keys=valid_cache_keys)
1423 valid = CacheInvalidation.test_and_set_valid(rn, None, valid_cache_keys=valid_cache_keys)
1423 if not valid:
1424 if not valid:
1424 log.debug('Cache for %s invalidated, getting new object', rn)
1425 log.debug('Cache for %s invalidated, getting new object', rn)
1425 region_invalidate(_c, None, 'scm_instance_cached', rn)
1426 region_invalidate(_c, None, 'scm_instance_cached', rn)
1426 else:
1427 else:
1427 log.debug('Trying to get scm_instance of %s from cache', rn)
1428 log.debug('Trying to get scm_instance of %s from cache', rn)
1428 return _c(rn)
1429 return _c(rn)
1429
1430
1430 def scm_instance_no_cache(self):
1431 def scm_instance_no_cache(self):
1431 repo_full_path = safe_str(self.repo_full_path)
1432 repo_full_path = safe_str(self.repo_full_path)
1432 alias = get_scm(repo_full_path)[0]
1433 alias = get_scm(repo_full_path)[0]
1433 log.debug('Creating instance of %s repository from %s',
1434 log.debug('Creating instance of %s repository from %s',
1434 alias, self.repo_full_path)
1435 alias, self.repo_full_path)
1435 backend = get_backend(alias)
1436 backend = get_backend(alias)
1436
1437
1437 if alias == 'hg':
1438 if alias == 'hg':
1438 repo = backend(repo_full_path, create=False,
1439 repo = backend(repo_full_path, create=False,
1439 baseui=self._ui)
1440 baseui=self._ui)
1440 else:
1441 else:
1441 repo = backend(repo_full_path, create=False)
1442 repo = backend(repo_full_path, create=False)
1442
1443
1443 return repo
1444 return repo
1444
1445
1445 def __json__(self):
1446 def __json__(self):
1446 return dict(
1447 return dict(
1447 repo_id=self.repo_id,
1448 repo_id=self.repo_id,
1448 repo_name=self.repo_name,
1449 repo_name=self.repo_name,
1449 landing_rev=self.landing_rev,
1450 landing_rev=self.landing_rev,
1450 )
1451 )
1451
1452
1452
1453
1453 class RepoGroup(Base, BaseDbModel):
1454 class RepoGroup(Base, BaseDbModel):
1454 __tablename__ = 'groups'
1455 __tablename__ = 'groups'
1455 __table_args__ = (
1456 __table_args__ = (
1456 _table_args_default_dict,
1457 _table_args_default_dict,
1457 )
1458 )
1458
1459
1459 SEP = ' &raquo; '
1460 SEP = ' &raquo; '
1460
1461
1461 group_id = Column(Integer(), primary_key=True)
1462 group_id = Column(Integer(), primary_key=True)
1462 group_name = Column(Unicode(255), nullable=False, unique=True) # full path
1463 group_name = Column(Unicode(255), nullable=False, unique=True) # full path
1463 parent_group_id = Column('group_parent_id', Integer(), ForeignKey('groups.group_id'), nullable=True)
1464 parent_group_id = Column('group_parent_id', Integer(), ForeignKey('groups.group_id'), nullable=True)
1464 group_description = Column(Unicode(10000), nullable=False)
1465 group_description = Column(Unicode(10000), nullable=False)
1465 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1466 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1466 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1467 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1467
1468
1468 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1469 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1469 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1470 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1470 parent_group = relationship('RepoGroup', remote_side=group_id)
1471 parent_group = relationship('RepoGroup', remote_side=group_id)
1471 owner = relationship('User')
1472 owner = relationship('User')
1472
1473
1473 @classmethod
1474 @classmethod
1474 def query(cls, sorted=False):
1475 def query(cls, sorted=False):
1475 """Add RepoGroup-specific helpers for common query constructs.
1476 """Add RepoGroup-specific helpers for common query constructs.
1476
1477
1477 sorted: if True, apply the default ordering (name, case insensitive).
1478 sorted: if True, apply the default ordering (name, case insensitive).
1478 """
1479 """
1479 q = super(RepoGroup, cls).query()
1480 q = super(RepoGroup, cls).query()
1480
1481
1481 if sorted:
1482 if sorted:
1482 q = q.order_by(func.lower(RepoGroup.group_name))
1483 q = q.order_by(func.lower(RepoGroup.group_name))
1483
1484
1484 return q
1485 return q
1485
1486
1486 def __init__(self, group_name='', parent_group=None):
1487 def __init__(self, group_name='', parent_group=None):
1487 self.group_name = group_name
1488 self.group_name = group_name
1488 self.parent_group = parent_group
1489 self.parent_group = parent_group
1489
1490
1490 def __unicode__(self):
1491 def __unicode__(self):
1491 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
1492 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
1492 self.group_name)
1493 self.group_name)
1493
1494
1494 @classmethod
1495 @classmethod
1495 def _generate_choice(cls, repo_group):
1496 def _generate_choice(cls, repo_group):
1496 """Return tuple with group_id and name as html literal"""
1497 """Return tuple with group_id and name as html literal"""
1497 from webhelpers2.html import literal
1498 from webhelpers2.html import literal
1498 if repo_group is None:
1499 if repo_group is None:
1499 return (-1, u'-- %s --' % _('top level'))
1500 return (-1, u'-- %s --' % _('top level'))
1500 return repo_group.group_id, literal(cls.SEP.join(repo_group.full_path_splitted))
1501 return repo_group.group_id, literal(cls.SEP.join(repo_group.full_path_splitted))
1501
1502
1502 @classmethod
1503 @classmethod
1503 def groups_choices(cls, groups):
1504 def groups_choices(cls, groups):
1504 """Return tuples with group_id and name as html literal."""
1505 """Return tuples with group_id and name as html literal."""
1505 return sorted((cls._generate_choice(g) for g in groups),
1506 return sorted((cls._generate_choice(g) for g in groups),
1506 key=lambda c: c[1].split(cls.SEP))
1507 key=lambda c: c[1].split(cls.SEP))
1507
1508
1508 @classmethod
1509 @classmethod
1509 def url_sep(cls):
1510 def url_sep(cls):
1510 return URL_SEP
1511 return URL_SEP
1511
1512
1512 @classmethod
1513 @classmethod
1513 def guess_instance(cls, value):
1514 def guess_instance(cls, value):
1514 return super(RepoGroup, cls).guess_instance(value, RepoGroup.get_by_group_name)
1515 return super(RepoGroup, cls).guess_instance(value, RepoGroup.get_by_group_name)
1515
1516
1516 @classmethod
1517 @classmethod
1517 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1518 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1518 group_name = group_name.rstrip('/')
1519 group_name = group_name.rstrip('/')
1519 if case_insensitive:
1520 if case_insensitive:
1520 gr = cls.query() \
1521 gr = cls.query() \
1521 .filter(func.lower(cls.group_name) == func.lower(group_name))
1522 .filter(func.lower(cls.group_name) == func.lower(group_name))
1522 else:
1523 else:
1523 gr = cls.query() \
1524 gr = cls.query() \
1524 .filter(cls.group_name == group_name)
1525 .filter(cls.group_name == group_name)
1525 if cache:
1526 if cache:
1526 gr = gr.options(FromCache(
1527 gr = gr.options(FromCache(
1527 "sql_cache_short",
1528 "sql_cache_short",
1528 "get_group_%s" % _hash_key(group_name)
1529 "get_group_%s" % _hash_key(group_name)
1529 )
1530 )
1530 )
1531 )
1531 return gr.scalar()
1532 return gr.scalar()
1532
1533
1533 @property
1534 @property
1534 def parents(self):
1535 def parents(self):
1535 groups = []
1536 groups = []
1536 group = self.parent_group
1537 group = self.parent_group
1537 while group is not None:
1538 while group is not None:
1538 groups.append(group)
1539 groups.append(group)
1539 group = group.parent_group
1540 group = group.parent_group
1540 assert group not in groups, group # avoid recursion on bad db content
1541 assert group not in groups, group # avoid recursion on bad db content
1541 groups.reverse()
1542 groups.reverse()
1542 return groups
1543 return groups
1543
1544
1544 @property
1545 @property
1545 def children(self):
1546 def children(self):
1546 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1547 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1547
1548
1548 @property
1549 @property
1549 def name(self):
1550 def name(self):
1550 return self.group_name.split(RepoGroup.url_sep())[-1]
1551 return self.group_name.split(RepoGroup.url_sep())[-1]
1551
1552
1552 @property
1553 @property
1553 def full_path(self):
1554 def full_path(self):
1554 return self.group_name
1555 return self.group_name
1555
1556
1556 @property
1557 @property
1557 def full_path_splitted(self):
1558 def full_path_splitted(self):
1558 return self.group_name.split(RepoGroup.url_sep())
1559 return self.group_name.split(RepoGroup.url_sep())
1559
1560
1560 @property
1561 @property
1561 def repositories(self):
1562 def repositories(self):
1562 return Repository.query(sorted=True).filter_by(group=self)
1563 return Repository.query(sorted=True).filter_by(group=self)
1563
1564
1564 @property
1565 @property
1565 def repositories_recursive_count(self):
1566 def repositories_recursive_count(self):
1566 cnt = self.repositories.count()
1567 cnt = self.repositories.count()
1567
1568
1568 def children_count(group):
1569 def children_count(group):
1569 cnt = 0
1570 cnt = 0
1570 for child in group.children:
1571 for child in group.children:
1571 cnt += child.repositories.count()
1572 cnt += child.repositories.count()
1572 cnt += children_count(child)
1573 cnt += children_count(child)
1573 return cnt
1574 return cnt
1574
1575
1575 return cnt + children_count(self)
1576 return cnt + children_count(self)
1576
1577
1577 def _recursive_objects(self, include_repos=True):
1578 def _recursive_objects(self, include_repos=True):
1578 all_ = []
1579 all_ = []
1579
1580
1580 def _get_members(root_gr):
1581 def _get_members(root_gr):
1581 if include_repos:
1582 if include_repos:
1582 for r in root_gr.repositories:
1583 for r in root_gr.repositories:
1583 all_.append(r)
1584 all_.append(r)
1584 childs = root_gr.children.all()
1585 childs = root_gr.children.all()
1585 if childs:
1586 if childs:
1586 for gr in childs:
1587 for gr in childs:
1587 all_.append(gr)
1588 all_.append(gr)
1588 _get_members(gr)
1589 _get_members(gr)
1589
1590
1590 _get_members(self)
1591 _get_members(self)
1591 return [self] + all_
1592 return [self] + all_
1592
1593
1593 def recursive_groups_and_repos(self):
1594 def recursive_groups_and_repos(self):
1594 """
1595 """
1595 Recursive return all groups, with repositories in those groups
1596 Recursive return all groups, with repositories in those groups
1596 """
1597 """
1597 return self._recursive_objects()
1598 return self._recursive_objects()
1598
1599
1599 def recursive_groups(self):
1600 def recursive_groups(self):
1600 """
1601 """
1601 Returns all children groups for this group including children of children
1602 Returns all children groups for this group including children of children
1602 """
1603 """
1603 return self._recursive_objects(include_repos=False)
1604 return self._recursive_objects(include_repos=False)
1604
1605
1605 def get_new_name(self, group_name):
1606 def get_new_name(self, group_name):
1606 """
1607 """
1607 returns new full group name based on parent and new name
1608 returns new full group name based on parent and new name
1608
1609
1609 :param group_name:
1610 :param group_name:
1610 """
1611 """
1611 path_prefix = (self.parent_group.full_path_splitted if
1612 path_prefix = (self.parent_group.full_path_splitted if
1612 self.parent_group else [])
1613 self.parent_group else [])
1613 return RepoGroup.url_sep().join(path_prefix + [group_name])
1614 return RepoGroup.url_sep().join(path_prefix + [group_name])
1614
1615
1615 def get_api_data(self):
1616 def get_api_data(self):
1616 """
1617 """
1617 Common function for generating api data
1618 Common function for generating api data
1618
1619
1619 """
1620 """
1620 group = self
1621 group = self
1621 data = dict(
1622 data = dict(
1622 group_id=group.group_id,
1623 group_id=group.group_id,
1623 group_name=group.group_name,
1624 group_name=group.group_name,
1624 group_description=group.group_description,
1625 group_description=group.group_description,
1625 parent_group=group.parent_group.group_name if group.parent_group else None,
1626 parent_group=group.parent_group.group_name if group.parent_group else None,
1626 repositories=[x.repo_name for x in group.repositories],
1627 repositories=[x.repo_name for x in group.repositories],
1627 owner=group.owner.username
1628 owner=group.owner.username
1628 )
1629 )
1629 return data
1630 return data
1630
1631
1631
1632
1632 class Permission(Base, BaseDbModel):
1633 class Permission(Base, BaseDbModel):
1633 __tablename__ = 'permissions'
1634 __tablename__ = 'permissions'
1634 __table_args__ = (
1635 __table_args__ = (
1635 Index('p_perm_name_idx', 'permission_name'),
1636 Index('p_perm_name_idx', 'permission_name'),
1636 _table_args_default_dict,
1637 _table_args_default_dict,
1637 )
1638 )
1638
1639
1639 PERMS = (
1640 PERMS = (
1640 ('hg.admin', _('Kallithea Administrator')),
1641 ('hg.admin', _('Kallithea Administrator')),
1641
1642
1642 ('repository.none', _('Default user has no access to new repositories')),
1643 ('repository.none', _('Default user has no access to new repositories')),
1643 ('repository.read', _('Default user has read access to new repositories')),
1644 ('repository.read', _('Default user has read access to new repositories')),
1644 ('repository.write', _('Default user has write access to new repositories')),
1645 ('repository.write', _('Default user has write access to new repositories')),
1645 ('repository.admin', _('Default user has admin access to new repositories')),
1646 ('repository.admin', _('Default user has admin access to new repositories')),
1646
1647
1647 ('group.none', _('Default user has no access to new repository groups')),
1648 ('group.none', _('Default user has no access to new repository groups')),
1648 ('group.read', _('Default user has read access to new repository groups')),
1649 ('group.read', _('Default user has read access to new repository groups')),
1649 ('group.write', _('Default user has write access to new repository groups')),
1650 ('group.write', _('Default user has write access to new repository groups')),
1650 ('group.admin', _('Default user has admin access to new repository groups')),
1651 ('group.admin', _('Default user has admin access to new repository groups')),
1651
1652
1652 ('usergroup.none', _('Default user has no access to new user groups')),
1653 ('usergroup.none', _('Default user has no access to new user groups')),
1653 ('usergroup.read', _('Default user has read access to new user groups')),
1654 ('usergroup.read', _('Default user has read access to new user groups')),
1654 ('usergroup.write', _('Default user has write access to new user groups')),
1655 ('usergroup.write', _('Default user has write access to new user groups')),
1655 ('usergroup.admin', _('Default user has admin access to new user groups')),
1656 ('usergroup.admin', _('Default user has admin access to new user groups')),
1656
1657
1657 ('hg.repogroup.create.false', _('Only admins can create repository groups')),
1658 ('hg.repogroup.create.false', _('Only admins can create repository groups')),
1658 ('hg.repogroup.create.true', _('Non-admins can create repository groups')),
1659 ('hg.repogroup.create.true', _('Non-admins can create repository groups')),
1659
1660
1660 ('hg.usergroup.create.false', _('Only admins can create user groups')),
1661 ('hg.usergroup.create.false', _('Only admins can create user groups')),
1661 ('hg.usergroup.create.true', _('Non-admins can create user groups')),
1662 ('hg.usergroup.create.true', _('Non-admins can create user groups')),
1662
1663
1663 ('hg.create.none', _('Only admins can create top level repositories')),
1664 ('hg.create.none', _('Only admins can create top level repositories')),
1664 ('hg.create.repository', _('Non-admins can create top level repositories')),
1665 ('hg.create.repository', _('Non-admins can create top level repositories')),
1665
1666
1666 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
1667 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
1667 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
1668 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
1668
1669
1669 ('hg.fork.none', _('Only admins can fork repositories')),
1670 ('hg.fork.none', _('Only admins can fork repositories')),
1670 ('hg.fork.repository', _('Non-admins can fork repositories')),
1671 ('hg.fork.repository', _('Non-admins can fork repositories')),
1671
1672
1672 ('hg.register.none', _('Registration disabled')),
1673 ('hg.register.none', _('Registration disabled')),
1673 ('hg.register.manual_activate', _('User registration with manual account activation')),
1674 ('hg.register.manual_activate', _('User registration with manual account activation')),
1674 ('hg.register.auto_activate', _('User registration with automatic account activation')),
1675 ('hg.register.auto_activate', _('User registration with automatic account activation')),
1675
1676
1676 ('hg.extern_activate.manual', _('Manual activation of external account')),
1677 ('hg.extern_activate.manual', _('Manual activation of external account')),
1677 ('hg.extern_activate.auto', _('Automatic activation of external account')),
1678 ('hg.extern_activate.auto', _('Automatic activation of external account')),
1678 )
1679 )
1679
1680
1680 # definition of system default permissions for DEFAULT user
1681 # definition of system default permissions for DEFAULT user
1681 DEFAULT_USER_PERMISSIONS = (
1682 DEFAULT_USER_PERMISSIONS = (
1682 'repository.read',
1683 'repository.read',
1683 'group.read',
1684 'group.read',
1684 'usergroup.read',
1685 'usergroup.read',
1685 'hg.create.repository',
1686 'hg.create.repository',
1686 'hg.create.write_on_repogroup.true',
1687 'hg.create.write_on_repogroup.true',
1687 'hg.fork.repository',
1688 'hg.fork.repository',
1688 'hg.register.manual_activate',
1689 'hg.register.manual_activate',
1689 'hg.extern_activate.auto',
1690 'hg.extern_activate.auto',
1690 )
1691 )
1691
1692
1692 # defines which permissions are more important higher the more important
1693 # defines which permissions are more important higher the more important
1693 # Weight defines which permissions are more important.
1694 # Weight defines which permissions are more important.
1694 # The higher number the more important.
1695 # The higher number the more important.
1695 PERM_WEIGHTS = {
1696 PERM_WEIGHTS = {
1696 'repository.none': 0,
1697 'repository.none': 0,
1697 'repository.read': 1,
1698 'repository.read': 1,
1698 'repository.write': 3,
1699 'repository.write': 3,
1699 'repository.admin': 4,
1700 'repository.admin': 4,
1700
1701
1701 'group.none': 0,
1702 'group.none': 0,
1702 'group.read': 1,
1703 'group.read': 1,
1703 'group.write': 3,
1704 'group.write': 3,
1704 'group.admin': 4,
1705 'group.admin': 4,
1705
1706
1706 'usergroup.none': 0,
1707 'usergroup.none': 0,
1707 'usergroup.read': 1,
1708 'usergroup.read': 1,
1708 'usergroup.write': 3,
1709 'usergroup.write': 3,
1709 'usergroup.admin': 4,
1710 'usergroup.admin': 4,
1710
1711
1711 'hg.repogroup.create.false': 0,
1712 'hg.repogroup.create.false': 0,
1712 'hg.repogroup.create.true': 1,
1713 'hg.repogroup.create.true': 1,
1713
1714
1714 'hg.usergroup.create.false': 0,
1715 'hg.usergroup.create.false': 0,
1715 'hg.usergroup.create.true': 1,
1716 'hg.usergroup.create.true': 1,
1716
1717
1717 'hg.fork.none': 0,
1718 'hg.fork.none': 0,
1718 'hg.fork.repository': 1,
1719 'hg.fork.repository': 1,
1719
1720
1720 'hg.create.none': 0,
1721 'hg.create.none': 0,
1721 'hg.create.repository': 1,
1722 'hg.create.repository': 1,
1722
1723
1723 'hg.create.write_on_repogroup.false': 0,
1724 'hg.create.write_on_repogroup.false': 0,
1724 'hg.create.write_on_repogroup.true': 1,
1725 'hg.create.write_on_repogroup.true': 1,
1725
1726
1726 'hg.register.none': 0,
1727 'hg.register.none': 0,
1727 'hg.register.manual_activate': 1,
1728 'hg.register.manual_activate': 1,
1728 'hg.register.auto_activate': 2,
1729 'hg.register.auto_activate': 2,
1729
1730
1730 'hg.extern_activate.manual': 0,
1731 'hg.extern_activate.manual': 0,
1731 'hg.extern_activate.auto': 1,
1732 'hg.extern_activate.auto': 1,
1732 }
1733 }
1733
1734
1734 permission_id = Column(Integer(), primary_key=True)
1735 permission_id = Column(Integer(), primary_key=True)
1735 permission_name = Column(String(255), nullable=False)
1736 permission_name = Column(String(255), nullable=False)
1736
1737
1737 def __unicode__(self):
1738 def __unicode__(self):
1738 return u"<%s('%s:%s')>" % (
1739 return u"<%s('%s:%s')>" % (
1739 self.__class__.__name__, self.permission_id, self.permission_name
1740 self.__class__.__name__, self.permission_id, self.permission_name
1740 )
1741 )
1741
1742
1742 @classmethod
1743 @classmethod
1743 def guess_instance(cls, value):
1744 def guess_instance(cls, value):
1744 return super(Permission, cls).guess_instance(value, Permission.get_by_key)
1745 return super(Permission, cls).guess_instance(value, Permission.get_by_key)
1745
1746
1746 @classmethod
1747 @classmethod
1747 def get_by_key(cls, key):
1748 def get_by_key(cls, key):
1748 return cls.query().filter(cls.permission_name == key).scalar()
1749 return cls.query().filter(cls.permission_name == key).scalar()
1749
1750
1750 @classmethod
1751 @classmethod
1751 def get_default_perms(cls, default_user_id):
1752 def get_default_perms(cls, default_user_id):
1752 q = Session().query(UserRepoToPerm, Repository, cls) \
1753 q = Session().query(UserRepoToPerm, Repository, cls) \
1753 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id)) \
1754 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id)) \
1754 .join((cls, UserRepoToPerm.permission_id == cls.permission_id)) \
1755 .join((cls, UserRepoToPerm.permission_id == cls.permission_id)) \
1755 .filter(UserRepoToPerm.user_id == default_user_id)
1756 .filter(UserRepoToPerm.user_id == default_user_id)
1756
1757
1757 return q.all()
1758 return q.all()
1758
1759
1759 @classmethod
1760 @classmethod
1760 def get_default_group_perms(cls, default_user_id):
1761 def get_default_group_perms(cls, default_user_id):
1761 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls) \
1762 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls) \
1762 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id)) \
1763 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id)) \
1763 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id)) \
1764 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id)) \
1764 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1765 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1765
1766
1766 return q.all()
1767 return q.all()
1767
1768
1768 @classmethod
1769 @classmethod
1769 def get_default_user_group_perms(cls, default_user_id):
1770 def get_default_user_group_perms(cls, default_user_id):
1770 q = Session().query(UserUserGroupToPerm, UserGroup, cls) \
1771 q = Session().query(UserUserGroupToPerm, UserGroup, cls) \
1771 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id)) \
1772 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id)) \
1772 .join((cls, UserUserGroupToPerm.permission_id == cls.permission_id)) \
1773 .join((cls, UserUserGroupToPerm.permission_id == cls.permission_id)) \
1773 .filter(UserUserGroupToPerm.user_id == default_user_id)
1774 .filter(UserUserGroupToPerm.user_id == default_user_id)
1774
1775
1775 return q.all()
1776 return q.all()
1776
1777
1777
1778
1778 class UserRepoToPerm(Base, BaseDbModel):
1779 class UserRepoToPerm(Base, BaseDbModel):
1779 __tablename__ = 'repo_to_perm'
1780 __tablename__ = 'repo_to_perm'
1780 __table_args__ = (
1781 __table_args__ = (
1781 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1782 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1782 _table_args_default_dict,
1783 _table_args_default_dict,
1783 )
1784 )
1784
1785
1785 repo_to_perm_id = Column(Integer(), primary_key=True)
1786 repo_to_perm_id = Column(Integer(), primary_key=True)
1786 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1787 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1787 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1788 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1788 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1789 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1789
1790
1790 user = relationship('User')
1791 user = relationship('User')
1791 repository = relationship('Repository')
1792 repository = relationship('Repository')
1792 permission = relationship('Permission')
1793 permission = relationship('Permission')
1793
1794
1794 @classmethod
1795 @classmethod
1795 def create(cls, user, repository, permission):
1796 def create(cls, user, repository, permission):
1796 n = cls()
1797 n = cls()
1797 n.user = user
1798 n.user = user
1798 n.repository = repository
1799 n.repository = repository
1799 n.permission = permission
1800 n.permission = permission
1800 Session().add(n)
1801 Session().add(n)
1801 return n
1802 return n
1802
1803
1803 def __unicode__(self):
1804 def __unicode__(self):
1804 return u'<%s => %s >' % (self.user, self.repository)
1805 return u'<%s => %s >' % (self.user, self.repository)
1805
1806
1806
1807
1807 class UserUserGroupToPerm(Base, BaseDbModel):
1808 class UserUserGroupToPerm(Base, BaseDbModel):
1808 __tablename__ = 'user_user_group_to_perm'
1809 __tablename__ = 'user_user_group_to_perm'
1809 __table_args__ = (
1810 __table_args__ = (
1810 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
1811 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
1811 _table_args_default_dict,
1812 _table_args_default_dict,
1812 )
1813 )
1813
1814
1814 user_user_group_to_perm_id = Column(Integer(), primary_key=True)
1815 user_user_group_to_perm_id = Column(Integer(), primary_key=True)
1815 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1816 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1816 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1817 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1817 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1818 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1818
1819
1819 user = relationship('User')
1820 user = relationship('User')
1820 user_group = relationship('UserGroup')
1821 user_group = relationship('UserGroup')
1821 permission = relationship('Permission')
1822 permission = relationship('Permission')
1822
1823
1823 @classmethod
1824 @classmethod
1824 def create(cls, user, user_group, permission):
1825 def create(cls, user, user_group, permission):
1825 n = cls()
1826 n = cls()
1826 n.user = user
1827 n.user = user
1827 n.user_group = user_group
1828 n.user_group = user_group
1828 n.permission = permission
1829 n.permission = permission
1829 Session().add(n)
1830 Session().add(n)
1830 return n
1831 return n
1831
1832
1832 def __unicode__(self):
1833 def __unicode__(self):
1833 return u'<%s => %s >' % (self.user, self.user_group)
1834 return u'<%s => %s >' % (self.user, self.user_group)
1834
1835
1835
1836
1836 class UserToPerm(Base, BaseDbModel):
1837 class UserToPerm(Base, BaseDbModel):
1837 __tablename__ = 'user_to_perm'
1838 __tablename__ = 'user_to_perm'
1838 __table_args__ = (
1839 __table_args__ = (
1839 UniqueConstraint('user_id', 'permission_id'),
1840 UniqueConstraint('user_id', 'permission_id'),
1840 _table_args_default_dict,
1841 _table_args_default_dict,
1841 )
1842 )
1842
1843
1843 user_to_perm_id = Column(Integer(), primary_key=True)
1844 user_to_perm_id = Column(Integer(), primary_key=True)
1844 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1845 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1845 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1846 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1846
1847
1847 user = relationship('User')
1848 user = relationship('User')
1848 permission = relationship('Permission')
1849 permission = relationship('Permission')
1849
1850
1850 def __unicode__(self):
1851 def __unicode__(self):
1851 return u'<%s => %s >' % (self.user, self.permission)
1852 return u'<%s => %s >' % (self.user, self.permission)
1852
1853
1853
1854
1854 class UserGroupRepoToPerm(Base, BaseDbModel):
1855 class UserGroupRepoToPerm(Base, BaseDbModel):
1855 __tablename__ = 'users_group_repo_to_perm'
1856 __tablename__ = 'users_group_repo_to_perm'
1856 __table_args__ = (
1857 __table_args__ = (
1857 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1858 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1858 _table_args_default_dict,
1859 _table_args_default_dict,
1859 )
1860 )
1860
1861
1861 users_group_to_perm_id = Column(Integer(), primary_key=True)
1862 users_group_to_perm_id = Column(Integer(), primary_key=True)
1862 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1863 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1863 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1864 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1864 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1865 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1865
1866
1866 users_group = relationship('UserGroup')
1867 users_group = relationship('UserGroup')
1867 permission = relationship('Permission')
1868 permission = relationship('Permission')
1868 repository = relationship('Repository')
1869 repository = relationship('Repository')
1869
1870
1870 @classmethod
1871 @classmethod
1871 def create(cls, users_group, repository, permission):
1872 def create(cls, users_group, repository, permission):
1872 n = cls()
1873 n = cls()
1873 n.users_group = users_group
1874 n.users_group = users_group
1874 n.repository = repository
1875 n.repository = repository
1875 n.permission = permission
1876 n.permission = permission
1876 Session().add(n)
1877 Session().add(n)
1877 return n
1878 return n
1878
1879
1879 def __unicode__(self):
1880 def __unicode__(self):
1880 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
1881 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
1881
1882
1882
1883
1883 class UserGroupUserGroupToPerm(Base, BaseDbModel):
1884 class UserGroupUserGroupToPerm(Base, BaseDbModel):
1884 __tablename__ = 'user_group_user_group_to_perm'
1885 __tablename__ = 'user_group_user_group_to_perm'
1885 __table_args__ = (
1886 __table_args__ = (
1886 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
1887 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
1887 _table_args_default_dict,
1888 _table_args_default_dict,
1888 )
1889 )
1889
1890
1890 user_group_user_group_to_perm_id = Column(Integer(), primary_key=True)
1891 user_group_user_group_to_perm_id = Column(Integer(), primary_key=True)
1891 target_user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1892 target_user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1892 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1893 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1893 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1894 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1894
1895
1895 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
1896 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
1896 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
1897 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
1897 permission = relationship('Permission')
1898 permission = relationship('Permission')
1898
1899
1899 @classmethod
1900 @classmethod
1900 def create(cls, target_user_group, user_group, permission):
1901 def create(cls, target_user_group, user_group, permission):
1901 n = cls()
1902 n = cls()
1902 n.target_user_group = target_user_group
1903 n.target_user_group = target_user_group
1903 n.user_group = user_group
1904 n.user_group = user_group
1904 n.permission = permission
1905 n.permission = permission
1905 Session().add(n)
1906 Session().add(n)
1906 return n
1907 return n
1907
1908
1908 def __unicode__(self):
1909 def __unicode__(self):
1909 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
1910 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
1910
1911
1911
1912
1912 class UserGroupToPerm(Base, BaseDbModel):
1913 class UserGroupToPerm(Base, BaseDbModel):
1913 __tablename__ = 'users_group_to_perm'
1914 __tablename__ = 'users_group_to_perm'
1914 __table_args__ = (
1915 __table_args__ = (
1915 UniqueConstraint('users_group_id', 'permission_id',),
1916 UniqueConstraint('users_group_id', 'permission_id',),
1916 _table_args_default_dict,
1917 _table_args_default_dict,
1917 )
1918 )
1918
1919
1919 users_group_to_perm_id = Column(Integer(), primary_key=True)
1920 users_group_to_perm_id = Column(Integer(), primary_key=True)
1920 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1921 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1921 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1922 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1922
1923
1923 users_group = relationship('UserGroup')
1924 users_group = relationship('UserGroup')
1924 permission = relationship('Permission')
1925 permission = relationship('Permission')
1925
1926
1926
1927
1927 class UserRepoGroupToPerm(Base, BaseDbModel):
1928 class UserRepoGroupToPerm(Base, BaseDbModel):
1928 __tablename__ = 'user_repo_group_to_perm'
1929 __tablename__ = 'user_repo_group_to_perm'
1929 __table_args__ = (
1930 __table_args__ = (
1930 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1931 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1931 _table_args_default_dict,
1932 _table_args_default_dict,
1932 )
1933 )
1933
1934
1934 group_to_perm_id = Column(Integer(), primary_key=True)
1935 group_to_perm_id = Column(Integer(), primary_key=True)
1935 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1936 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1936 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1937 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1937 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1938 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1938
1939
1939 user = relationship('User')
1940 user = relationship('User')
1940 group = relationship('RepoGroup')
1941 group = relationship('RepoGroup')
1941 permission = relationship('Permission')
1942 permission = relationship('Permission')
1942
1943
1943 @classmethod
1944 @classmethod
1944 def create(cls, user, repository_group, permission):
1945 def create(cls, user, repository_group, permission):
1945 n = cls()
1946 n = cls()
1946 n.user = user
1947 n.user = user
1947 n.group = repository_group
1948 n.group = repository_group
1948 n.permission = permission
1949 n.permission = permission
1949 Session().add(n)
1950 Session().add(n)
1950 return n
1951 return n
1951
1952
1952
1953
1953 class UserGroupRepoGroupToPerm(Base, BaseDbModel):
1954 class UserGroupRepoGroupToPerm(Base, BaseDbModel):
1954 __tablename__ = 'users_group_repo_group_to_perm'
1955 __tablename__ = 'users_group_repo_group_to_perm'
1955 __table_args__ = (
1956 __table_args__ = (
1956 UniqueConstraint('users_group_id', 'group_id'),
1957 UniqueConstraint('users_group_id', 'group_id'),
1957 _table_args_default_dict,
1958 _table_args_default_dict,
1958 )
1959 )
1959
1960
1960 users_group_repo_group_to_perm_id = Column(Integer(), primary_key=True)
1961 users_group_repo_group_to_perm_id = Column(Integer(), primary_key=True)
1961 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1962 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1962 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1963 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1963 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1964 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1964
1965
1965 users_group = relationship('UserGroup')
1966 users_group = relationship('UserGroup')
1966 permission = relationship('Permission')
1967 permission = relationship('Permission')
1967 group = relationship('RepoGroup')
1968 group = relationship('RepoGroup')
1968
1969
1969 @classmethod
1970 @classmethod
1970 def create(cls, user_group, repository_group, permission):
1971 def create(cls, user_group, repository_group, permission):
1971 n = cls()
1972 n = cls()
1972 n.users_group = user_group
1973 n.users_group = user_group
1973 n.group = repository_group
1974 n.group = repository_group
1974 n.permission = permission
1975 n.permission = permission
1975 Session().add(n)
1976 Session().add(n)
1976 return n
1977 return n
1977
1978
1978
1979
1979 class Statistics(Base, BaseDbModel):
1980 class Statistics(Base, BaseDbModel):
1980 __tablename__ = 'statistics'
1981 __tablename__ = 'statistics'
1981 __table_args__ = (
1982 __table_args__ = (
1982 _table_args_default_dict,
1983 _table_args_default_dict,
1983 )
1984 )
1984
1985
1985 stat_id = Column(Integer(), primary_key=True)
1986 stat_id = Column(Integer(), primary_key=True)
1986 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True)
1987 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True)
1987 stat_on_revision = Column(Integer(), nullable=False)
1988 stat_on_revision = Column(Integer(), nullable=False)
1988 commit_activity = Column(LargeBinary(1000000), nullable=False) # JSON data
1989 commit_activity = Column(LargeBinary(1000000), nullable=False) # JSON data
1989 commit_activity_combined = Column(LargeBinary(), nullable=False) # JSON data
1990 commit_activity_combined = Column(LargeBinary(), nullable=False) # JSON data
1990 languages = Column(LargeBinary(1000000), nullable=False) # JSON data
1991 languages = Column(LargeBinary(1000000), nullable=False) # JSON data
1991
1992
1992 repository = relationship('Repository', single_parent=True)
1993 repository = relationship('Repository', single_parent=True)
1993
1994
1994
1995
1995 class UserFollowing(Base, BaseDbModel):
1996 class UserFollowing(Base, BaseDbModel):
1996 __tablename__ = 'user_followings'
1997 __tablename__ = 'user_followings'
1997 __table_args__ = (
1998 __table_args__ = (
1998 UniqueConstraint('user_id', 'follows_repository_id', name='uq_user_followings_user_repo'),
1999 UniqueConstraint('user_id', 'follows_repository_id', name='uq_user_followings_user_repo'),
1999 UniqueConstraint('user_id', 'follows_user_id', name='uq_user_followings_user_user'),
2000 UniqueConstraint('user_id', 'follows_user_id', name='uq_user_followings_user_user'),
2000 _table_args_default_dict,
2001 _table_args_default_dict,
2001 )
2002 )
2002
2003
2003 user_following_id = Column(Integer(), primary_key=True)
2004 user_following_id = Column(Integer(), primary_key=True)
2004 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2005 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2005 follows_repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
2006 follows_repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
2006 follows_user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
2007 follows_user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
2007 follows_from = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2008 follows_from = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2008
2009
2009 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2010 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2010
2011
2011 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2012 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2012 follows_repository = relationship('Repository', order_by=lambda: func.lower(Repository.repo_name))
2013 follows_repository = relationship('Repository', order_by=lambda: func.lower(Repository.repo_name))
2013
2014
2014 @classmethod
2015 @classmethod
2015 def get_repo_followers(cls, repo_id):
2016 def get_repo_followers(cls, repo_id):
2016 return cls.query().filter(cls.follows_repository_id == repo_id)
2017 return cls.query().filter(cls.follows_repository_id == repo_id)
2017
2018
2018
2019
2019 class CacheInvalidation(Base, BaseDbModel):
2020 class CacheInvalidation(Base, BaseDbModel):
2020 __tablename__ = 'cache_invalidation'
2021 __tablename__ = 'cache_invalidation'
2021 __table_args__ = (
2022 __table_args__ = (
2022 Index('key_idx', 'cache_key'),
2023 Index('key_idx', 'cache_key'),
2023 _table_args_default_dict,
2024 _table_args_default_dict,
2024 )
2025 )
2025
2026
2026 # cache_id, not used
2027 # cache_id, not used
2027 cache_id = Column(Integer(), primary_key=True)
2028 cache_id = Column(Integer(), primary_key=True)
2028 # cache_key as created by _get_cache_key
2029 # cache_key as created by _get_cache_key
2029 cache_key = Column(Unicode(255), nullable=False, unique=True)
2030 cache_key = Column(Unicode(255), nullable=False, unique=True)
2030 # cache_args is a repo_name
2031 # cache_args is a repo_name
2031 cache_args = Column(Unicode(255), nullable=False)
2032 cache_args = Column(Unicode(255), nullable=False)
2032 # instance sets cache_active True when it is caching, other instances set
2033 # instance sets cache_active True when it is caching, other instances set
2033 # cache_active to False to indicate that this cache is invalid
2034 # cache_active to False to indicate that this cache is invalid
2034 cache_active = Column(Boolean(), nullable=False, default=False)
2035 cache_active = Column(Boolean(), nullable=False, default=False)
2035
2036
2036 def __init__(self, cache_key, repo_name=''):
2037 def __init__(self, cache_key, repo_name=''):
2037 self.cache_key = cache_key
2038 self.cache_key = cache_key
2038 self.cache_args = repo_name
2039 self.cache_args = repo_name
2039 self.cache_active = False
2040 self.cache_active = False
2040
2041
2041 def __unicode__(self):
2042 def __unicode__(self):
2042 return u"<%s('%s:%s[%s]')>" % (
2043 return u"<%s('%s:%s[%s]')>" % (
2043 self.__class__.__name__,
2044 self.__class__.__name__,
2044 self.cache_id, self.cache_key, self.cache_active)
2045 self.cache_id, self.cache_key, self.cache_active)
2045
2046
2046 def _cache_key_partition(self):
2047 def _cache_key_partition(self):
2047 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2048 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2048 return prefix, repo_name, suffix
2049 return prefix, repo_name, suffix
2049
2050
2050 def get_prefix(self):
2051 def get_prefix(self):
2051 """
2052 """
2052 get prefix that might have been used in _get_cache_key to
2053 get prefix that might have been used in _get_cache_key to
2053 generate self.cache_key. Only used for informational purposes
2054 generate self.cache_key. Only used for informational purposes
2054 in repo_edit.html.
2055 in repo_edit.html.
2055 """
2056 """
2056 # prefix, repo_name, suffix
2057 # prefix, repo_name, suffix
2057 return self._cache_key_partition()[0]
2058 return self._cache_key_partition()[0]
2058
2059
2059 def get_suffix(self):
2060 def get_suffix(self):
2060 """
2061 """
2061 get suffix that might have been used in _get_cache_key to
2062 get suffix that might have been used in _get_cache_key to
2062 generate self.cache_key. Only used for informational purposes
2063 generate self.cache_key. Only used for informational purposes
2063 in repo_edit.html.
2064 in repo_edit.html.
2064 """
2065 """
2065 # prefix, repo_name, suffix
2066 # prefix, repo_name, suffix
2066 return self._cache_key_partition()[2]
2067 return self._cache_key_partition()[2]
2067
2068
2068 @classmethod
2069 @classmethod
2069 def clear_cache(cls):
2070 def clear_cache(cls):
2070 """
2071 """
2071 Delete all cache keys from database.
2072 Delete all cache keys from database.
2072 Should only be run when all instances are down and all entries thus stale.
2073 Should only be run when all instances are down and all entries thus stale.
2073 """
2074 """
2074 cls.query().delete()
2075 cls.query().delete()
2075 Session().commit()
2076 Session().commit()
2076
2077
2077 @classmethod
2078 @classmethod
2078 def _get_cache_key(cls, key):
2079 def _get_cache_key(cls, key):
2079 """
2080 """
2080 Wrapper for generating a unique cache key for this instance and "key".
2081 Wrapper for generating a unique cache key for this instance and "key".
2081 key must / will start with a repo_name which will be stored in .cache_args .
2082 key must / will start with a repo_name which will be stored in .cache_args .
2082 """
2083 """
2083 prefix = kallithea.CONFIG.get('instance_id', '')
2084 prefix = kallithea.CONFIG.get('instance_id', '')
2084 return "%s%s" % (prefix, key)
2085 return "%s%s" % (prefix, key)
2085
2086
2086 @classmethod
2087 @classmethod
2087 def set_invalidate(cls, repo_name):
2088 def set_invalidate(cls, repo_name):
2088 """
2089 """
2089 Mark all caches of a repo as invalid in the database.
2090 Mark all caches of a repo as invalid in the database.
2090 """
2091 """
2091 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
2092 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
2092 log.debug('for repo %s got %s invalidation objects',
2093 log.debug('for repo %s got %s invalidation objects',
2093 safe_str(repo_name), inv_objs)
2094 safe_str(repo_name), inv_objs)
2094
2095
2095 for inv_obj in inv_objs:
2096 for inv_obj in inv_objs:
2096 log.debug('marking %s key for invalidation based on repo_name=%s',
2097 log.debug('marking %s key for invalidation based on repo_name=%s',
2097 inv_obj, safe_str(repo_name))
2098 inv_obj, safe_str(repo_name))
2098 Session().delete(inv_obj)
2099 Session().delete(inv_obj)
2099 Session().commit()
2100 Session().commit()
2100
2101
2101 @classmethod
2102 @classmethod
2102 def test_and_set_valid(cls, repo_name, kind, valid_cache_keys=None):
2103 def test_and_set_valid(cls, repo_name, kind, valid_cache_keys=None):
2103 """
2104 """
2104 Mark this cache key as active and currently cached.
2105 Mark this cache key as active and currently cached.
2105 Return True if the existing cache registration still was valid.
2106 Return True if the existing cache registration still was valid.
2106 Return False to indicate that it had been invalidated and caches should be refreshed.
2107 Return False to indicate that it had been invalidated and caches should be refreshed.
2107 """
2108 """
2108
2109
2109 key = (repo_name + '_' + kind) if kind else repo_name
2110 key = (repo_name + '_' + kind) if kind else repo_name
2110 cache_key = cls._get_cache_key(key)
2111 cache_key = cls._get_cache_key(key)
2111
2112
2112 if valid_cache_keys and cache_key in valid_cache_keys:
2113 if valid_cache_keys and cache_key in valid_cache_keys:
2113 return True
2114 return True
2114
2115
2115 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2116 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2116 if inv_obj is None:
2117 if inv_obj is None:
2117 inv_obj = cls(cache_key, repo_name)
2118 inv_obj = cls(cache_key, repo_name)
2118 Session().add(inv_obj)
2119 Session().add(inv_obj)
2119 elif inv_obj.cache_active:
2120 elif inv_obj.cache_active:
2120 return True
2121 return True
2121 inv_obj.cache_active = True
2122 inv_obj.cache_active = True
2122 try:
2123 try:
2123 Session().commit()
2124 Session().commit()
2124 except sqlalchemy.exc.IntegrityError:
2125 except sqlalchemy.exc.IntegrityError:
2125 log.error('commit of CacheInvalidation failed - retrying')
2126 log.error('commit of CacheInvalidation failed - retrying')
2126 Session().rollback()
2127 Session().rollback()
2127 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2128 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2128 if inv_obj is None:
2129 if inv_obj is None:
2129 log.error('failed to create CacheInvalidation entry')
2130 log.error('failed to create CacheInvalidation entry')
2130 # TODO: fail badly?
2131 # TODO: fail badly?
2131 # else: TOCTOU - another thread added the key at the same time; no further action required
2132 # else: TOCTOU - another thread added the key at the same time; no further action required
2132 return False
2133 return False
2133
2134
2134 @classmethod
2135 @classmethod
2135 def get_valid_cache_keys(cls):
2136 def get_valid_cache_keys(cls):
2136 """
2137 """
2137 Return opaque object with information of which caches still are valid
2138 Return opaque object with information of which caches still are valid
2138 and can be used without checking for invalidation.
2139 and can be used without checking for invalidation.
2139 """
2140 """
2140 return set(inv_obj.cache_key for inv_obj in cls.query().filter(cls.cache_active).all())
2141 return set(inv_obj.cache_key for inv_obj in cls.query().filter(cls.cache_active).all())
2141
2142
2142
2143
2143 class ChangesetComment(Base, BaseDbModel):
2144 class ChangesetComment(Base, BaseDbModel):
2144 __tablename__ = 'changeset_comments'
2145 __tablename__ = 'changeset_comments'
2145 __table_args__ = (
2146 __table_args__ = (
2146 Index('cc_revision_idx', 'revision'),
2147 Index('cc_revision_idx', 'revision'),
2147 Index('cc_pull_request_id_idx', 'pull_request_id'),
2148 Index('cc_pull_request_id_idx', 'pull_request_id'),
2148 _table_args_default_dict,
2149 _table_args_default_dict,
2149 )
2150 )
2150
2151
2151 comment_id = Column(Integer(), primary_key=True)
2152 comment_id = Column(Integer(), primary_key=True)
2152 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2153 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2153 revision = Column(String(40), nullable=True)
2154 revision = Column(String(40), nullable=True)
2154 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2155 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2155 line_no = Column(Unicode(10), nullable=True)
2156 line_no = Column(Unicode(10), nullable=True)
2156 f_path = Column(Unicode(1000), nullable=True)
2157 f_path = Column(Unicode(1000), nullable=True)
2157 author_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2158 author_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2158 text = Column(UnicodeText(), nullable=False)
2159 text = Column(UnicodeText(), nullable=False)
2159 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2160 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2160 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2161 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2161
2162
2162 author = relationship('User')
2163 author = relationship('User')
2163 repo = relationship('Repository')
2164 repo = relationship('Repository')
2164 # status_change is frequently used directly in templates - make it a lazy
2165 # status_change is frequently used directly in templates - make it a lazy
2165 # join to avoid fetching each related ChangesetStatus on demand.
2166 # join to avoid fetching each related ChangesetStatus on demand.
2166 # There will only be one ChangesetStatus referencing each comment so the join will not explode.
2167 # There will only be one ChangesetStatus referencing each comment so the join will not explode.
2167 status_change = relationship('ChangesetStatus',
2168 status_change = relationship('ChangesetStatus',
2168 cascade="all, delete-orphan", lazy='joined')
2169 cascade="all, delete-orphan", lazy='joined')
2169 pull_request = relationship('PullRequest')
2170 pull_request = relationship('PullRequest')
2170
2171
2171 def url(self):
2172 def url(self):
2172 anchor = "comment-%s" % self.comment_id
2173 anchor = "comment-%s" % self.comment_id
2173 import kallithea.lib.helpers as h
2174 import kallithea.lib.helpers as h
2174 if self.revision:
2175 if self.revision:
2175 return h.url('changeset_home', repo_name=self.repo.repo_name, revision=self.revision, anchor=anchor)
2176 return h.url('changeset_home', repo_name=self.repo.repo_name, revision=self.revision, anchor=anchor)
2176 elif self.pull_request_id is not None:
2177 elif self.pull_request_id is not None:
2177 return self.pull_request.url(anchor=anchor)
2178 return self.pull_request.url(anchor=anchor)
2178
2179
2179 def __json__(self):
2180 def __json__(self):
2180 return dict(
2181 return dict(
2181 comment_id=self.comment_id,
2182 comment_id=self.comment_id,
2182 username=self.author.username,
2183 username=self.author.username,
2183 text=self.text,
2184 text=self.text,
2184 )
2185 )
2185
2186
2186 def deletable(self):
2187 def deletable(self):
2187 return self.created_on > datetime.datetime.now() - datetime.timedelta(minutes=5)
2188 return self.created_on > datetime.datetime.now() - datetime.timedelta(minutes=5)
2188
2189
2189
2190
2190 class ChangesetStatus(Base, BaseDbModel):
2191 class ChangesetStatus(Base, BaseDbModel):
2191 __tablename__ = 'changeset_statuses'
2192 __tablename__ = 'changeset_statuses'
2192 __table_args__ = (
2193 __table_args__ = (
2193 Index('cs_revision_idx', 'revision'),
2194 Index('cs_revision_idx', 'revision'),
2194 Index('cs_version_idx', 'version'),
2195 Index('cs_version_idx', 'version'),
2195 Index('cs_pull_request_id_idx', 'pull_request_id'),
2196 Index('cs_pull_request_id_idx', 'pull_request_id'),
2196 Index('cs_changeset_comment_id_idx', 'changeset_comment_id'),
2197 Index('cs_changeset_comment_id_idx', 'changeset_comment_id'),
2197 Index('cs_pull_request_id_user_id_version_idx', 'pull_request_id', 'user_id', 'version'),
2198 Index('cs_pull_request_id_user_id_version_idx', 'pull_request_id', 'user_id', 'version'),
2198 Index('cs_repo_id_pull_request_id_idx', 'repo_id', 'pull_request_id'),
2199 Index('cs_repo_id_pull_request_id_idx', 'repo_id', 'pull_request_id'),
2199 UniqueConstraint('repo_id', 'revision', 'version'),
2200 UniqueConstraint('repo_id', 'revision', 'version'),
2200 _table_args_default_dict,
2201 _table_args_default_dict,
2201 )
2202 )
2202
2203
2203 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2204 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2204 STATUS_APPROVED = 'approved'
2205 STATUS_APPROVED = 'approved'
2205 STATUS_REJECTED = 'rejected' # is shown as "Not approved" - TODO: change database content / scheme
2206 STATUS_REJECTED = 'rejected' # is shown as "Not approved" - TODO: change database content / scheme
2206 STATUS_UNDER_REVIEW = 'under_review'
2207 STATUS_UNDER_REVIEW = 'under_review'
2207
2208
2208 STATUSES = [
2209 STATUSES = [
2209 (STATUS_NOT_REVIEWED, _("Not reviewed")), # (no icon) and default
2210 (STATUS_NOT_REVIEWED, _("Not reviewed")), # (no icon) and default
2210 (STATUS_UNDER_REVIEW, _("Under review")),
2211 (STATUS_UNDER_REVIEW, _("Under review")),
2211 (STATUS_REJECTED, _("Not approved")),
2212 (STATUS_REJECTED, _("Not approved")),
2212 (STATUS_APPROVED, _("Approved")),
2213 (STATUS_APPROVED, _("Approved")),
2213 ]
2214 ]
2214 STATUSES_DICT = dict(STATUSES)
2215 STATUSES_DICT = dict(STATUSES)
2215
2216
2216 changeset_status_id = Column(Integer(), primary_key=True)
2217 changeset_status_id = Column(Integer(), primary_key=True)
2217 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2218 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2218 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2219 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2219 revision = Column(String(40), nullable=True)
2220 revision = Column(String(40), nullable=True)
2220 status = Column(String(128), nullable=False, default=DEFAULT)
2221 status = Column(String(128), nullable=False, default=DEFAULT)
2221 comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
2222 comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
2222 modified_at = Column(DateTime(), nullable=False, default=datetime.datetime.now)
2223 modified_at = Column(DateTime(), nullable=False, default=datetime.datetime.now)
2223 version = Column(Integer(), nullable=False, default=0)
2224 version = Column(Integer(), nullable=False, default=0)
2224 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2225 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2225
2226
2226 author = relationship('User')
2227 author = relationship('User')
2227 repo = relationship('Repository')
2228 repo = relationship('Repository')
2228 comment = relationship('ChangesetComment')
2229 comment = relationship('ChangesetComment')
2229 pull_request = relationship('PullRequest')
2230 pull_request = relationship('PullRequest')
2230
2231
2231 def __unicode__(self):
2232 def __unicode__(self):
2232 return u"<%s('%s:%s')>" % (
2233 return u"<%s('%s:%s')>" % (
2233 self.__class__.__name__,
2234 self.__class__.__name__,
2234 self.status, self.author
2235 self.status, self.author
2235 )
2236 )
2236
2237
2237 @classmethod
2238 @classmethod
2238 def get_status_lbl(cls, value):
2239 def get_status_lbl(cls, value):
2239 return cls.STATUSES_DICT.get(value)
2240 return cls.STATUSES_DICT.get(value)
2240
2241
2241 @property
2242 @property
2242 def status_lbl(self):
2243 def status_lbl(self):
2243 return ChangesetStatus.get_status_lbl(self.status)
2244 return ChangesetStatus.get_status_lbl(self.status)
2244
2245
2245 def __json__(self):
2246 def __json__(self):
2246 return dict(
2247 return dict(
2247 status=self.status,
2248 status=self.status,
2248 modified_at=self.modified_at.replace(microsecond=0),
2249 modified_at=self.modified_at.replace(microsecond=0),
2249 reviewer=self.author.username,
2250 reviewer=self.author.username,
2250 )
2251 )
2251
2252
2252
2253
2253 class PullRequest(Base, BaseDbModel):
2254 class PullRequest(Base, BaseDbModel):
2254 __tablename__ = 'pull_requests'
2255 __tablename__ = 'pull_requests'
2255 __table_args__ = (
2256 __table_args__ = (
2256 Index('pr_org_repo_id_idx', 'org_repo_id'),
2257 Index('pr_org_repo_id_idx', 'org_repo_id'),
2257 Index('pr_other_repo_id_idx', 'other_repo_id'),
2258 Index('pr_other_repo_id_idx', 'other_repo_id'),
2258 _table_args_default_dict,
2259 _table_args_default_dict,
2259 )
2260 )
2260
2261
2261 # values for .status
2262 # values for .status
2262 STATUS_NEW = u'new'
2263 STATUS_NEW = u'new'
2263 STATUS_CLOSED = u'closed'
2264 STATUS_CLOSED = u'closed'
2264
2265
2265 pull_request_id = Column(Integer(), primary_key=True)
2266 pull_request_id = Column(Integer(), primary_key=True)
2266 title = Column(Unicode(255), nullable=False)
2267 title = Column(Unicode(255), nullable=False)
2267 description = Column(UnicodeText(), nullable=False)
2268 description = Column(UnicodeText(), nullable=False)
2268 status = Column(Unicode(255), nullable=False, default=STATUS_NEW) # only for closedness, not approve/reject/etc
2269 status = Column(Unicode(255), nullable=False, default=STATUS_NEW) # only for closedness, not approve/reject/etc
2269 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2270 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2270 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2271 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2271 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2272 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2272 _revisions = Column('revisions', UnicodeText(), nullable=False)
2273 _revisions = Column('revisions', UnicodeText(), nullable=False)
2273 org_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2274 org_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2274 org_ref = Column(Unicode(255), nullable=False)
2275 org_ref = Column(Unicode(255), nullable=False)
2275 other_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2276 other_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2276 other_ref = Column(Unicode(255), nullable=False)
2277 other_ref = Column(Unicode(255), nullable=False)
2277
2278
2278 @hybrid_property
2279 @hybrid_property
2279 def revisions(self):
2280 def revisions(self):
2280 return self._revisions.split(':')
2281 return self._revisions.split(':')
2281
2282
2282 @revisions.setter
2283 @revisions.setter
2283 def revisions(self, val):
2284 def revisions(self, val):
2284 self._revisions = safe_unicode(':'.join(val))
2285 self._revisions = safe_unicode(':'.join(val))
2285
2286
2286 @property
2287 @property
2287 def org_ref_parts(self):
2288 def org_ref_parts(self):
2288 return self.org_ref.split(':')
2289 return self.org_ref.split(':')
2289
2290
2290 @property
2291 @property
2291 def other_ref_parts(self):
2292 def other_ref_parts(self):
2292 return self.other_ref.split(':')
2293 return self.other_ref.split(':')
2293
2294
2294 owner = relationship('User')
2295 owner = relationship('User')
2295 reviewers = relationship('PullRequestReviewer',
2296 reviewers = relationship('PullRequestReviewer',
2296 cascade="all, delete-orphan")
2297 cascade="all, delete-orphan")
2297 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
2298 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
2298 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
2299 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
2299 statuses = relationship('ChangesetStatus', order_by='ChangesetStatus.changeset_status_id')
2300 statuses = relationship('ChangesetStatus', order_by='ChangesetStatus.changeset_status_id')
2300 comments = relationship('ChangesetComment', order_by='ChangesetComment.comment_id',
2301 comments = relationship('ChangesetComment', order_by='ChangesetComment.comment_id',
2301 cascade="all, delete-orphan")
2302 cascade="all, delete-orphan")
2302
2303
2303 @classmethod
2304 @classmethod
2304 def query(cls, reviewer_id=None, include_closed=True, sorted=False):
2305 def query(cls, reviewer_id=None, include_closed=True, sorted=False):
2305 """Add PullRequest-specific helpers for common query constructs.
2306 """Add PullRequest-specific helpers for common query constructs.
2306
2307
2307 reviewer_id: only PRs with the specified user added as reviewer.
2308 reviewer_id: only PRs with the specified user added as reviewer.
2308
2309
2309 include_closed: if False, do not include closed PRs.
2310 include_closed: if False, do not include closed PRs.
2310
2311
2311 sorted: if True, apply the default ordering (newest first).
2312 sorted: if True, apply the default ordering (newest first).
2312 """
2313 """
2313 q = super(PullRequest, cls).query()
2314 q = super(PullRequest, cls).query()
2314
2315
2315 if reviewer_id is not None:
2316 if reviewer_id is not None:
2316 q = q.join(PullRequestReviewer).filter(PullRequestReviewer.user_id == reviewer_id)
2317 q = q.join(PullRequestReviewer).filter(PullRequestReviewer.user_id == reviewer_id)
2317
2318
2318 if not include_closed:
2319 if not include_closed:
2319 q = q.filter(PullRequest.status != PullRequest.STATUS_CLOSED)
2320 q = q.filter(PullRequest.status != PullRequest.STATUS_CLOSED)
2320
2321
2321 if sorted:
2322 if sorted:
2322 q = q.order_by(PullRequest.created_on.desc())
2323 q = q.order_by(PullRequest.created_on.desc())
2323
2324
2324 return q
2325 return q
2325
2326
2326 def get_reviewer_users(self):
2327 def get_reviewer_users(self):
2327 """Like .reviewers, but actually returning the users"""
2328 """Like .reviewers, but actually returning the users"""
2328 return User.query() \
2329 return User.query() \
2329 .join(PullRequestReviewer) \
2330 .join(PullRequestReviewer) \
2330 .filter(PullRequestReviewer.pull_request == self) \
2331 .filter(PullRequestReviewer.pull_request == self) \
2331 .order_by(PullRequestReviewer.pull_request_reviewers_id) \
2332 .order_by(PullRequestReviewer.pull_request_reviewers_id) \
2332 .all()
2333 .all()
2333
2334
2334 def is_closed(self):
2335 def is_closed(self):
2335 return self.status == self.STATUS_CLOSED
2336 return self.status == self.STATUS_CLOSED
2336
2337
2337 def user_review_status(self, user_id):
2338 def user_review_status(self, user_id):
2338 """Return the user's latest status votes on PR"""
2339 """Return the user's latest status votes on PR"""
2339 # note: no filtering on repo - that would be redundant
2340 # note: no filtering on repo - that would be redundant
2340 status = ChangesetStatus.query() \
2341 status = ChangesetStatus.query() \
2341 .filter(ChangesetStatus.pull_request == self) \
2342 .filter(ChangesetStatus.pull_request == self) \
2342 .filter(ChangesetStatus.user_id == user_id) \
2343 .filter(ChangesetStatus.user_id == user_id) \
2343 .order_by(ChangesetStatus.version) \
2344 .order_by(ChangesetStatus.version) \
2344 .first()
2345 .first()
2345 return str(status.status) if status else ''
2346 return str(status.status) if status else ''
2346
2347
2347 @classmethod
2348 @classmethod
2348 def make_nice_id(cls, pull_request_id):
2349 def make_nice_id(cls, pull_request_id):
2349 '''Return pull request id nicely formatted for displaying'''
2350 '''Return pull request id nicely formatted for displaying'''
2350 return '#%s' % pull_request_id
2351 return '#%s' % pull_request_id
2351
2352
2352 def nice_id(self):
2353 def nice_id(self):
2353 '''Return the id of this pull request, nicely formatted for displaying'''
2354 '''Return the id of this pull request, nicely formatted for displaying'''
2354 return self.make_nice_id(self.pull_request_id)
2355 return self.make_nice_id(self.pull_request_id)
2355
2356
2356 def get_api_data(self):
2357 def get_api_data(self):
2357 return self.__json__()
2358 return self.__json__()
2358
2359
2359 def __json__(self):
2360 def __json__(self):
2360 clone_uri_tmpl = kallithea.CONFIG.get('clone_uri_tmpl') or Repository.DEFAULT_CLONE_URI
2361 clone_uri_tmpl = kallithea.CONFIG.get('clone_uri_tmpl') or Repository.DEFAULT_CLONE_URI
2361 return dict(
2362 return dict(
2362 pull_request_id=self.pull_request_id,
2363 pull_request_id=self.pull_request_id,
2363 url=self.url(),
2364 url=self.url(),
2364 reviewers=self.reviewers,
2365 reviewers=self.reviewers,
2365 revisions=self.revisions,
2366 revisions=self.revisions,
2366 owner=self.owner.username,
2367 owner=self.owner.username,
2367 title=self.title,
2368 title=self.title,
2368 description=self.description,
2369 description=self.description,
2369 org_repo_url=self.org_repo.clone_url(clone_uri_tmpl=clone_uri_tmpl),
2370 org_repo_url=self.org_repo.clone_url(clone_uri_tmpl=clone_uri_tmpl),
2370 org_ref_parts=self.org_ref_parts,
2371 org_ref_parts=self.org_ref_parts,
2371 other_ref_parts=self.other_ref_parts,
2372 other_ref_parts=self.other_ref_parts,
2372 status=self.status,
2373 status=self.status,
2373 comments=self.comments,
2374 comments=self.comments,
2374 statuses=self.statuses,
2375 statuses=self.statuses,
2375 )
2376 )
2376
2377
2377 def url(self, **kwargs):
2378 def url(self, **kwargs):
2378 canonical = kwargs.pop('canonical', None)
2379 canonical = kwargs.pop('canonical', None)
2379 import kallithea.lib.helpers as h
2380 import kallithea.lib.helpers as h
2380 b = self.org_ref_parts[1]
2381 b = self.org_ref_parts[1]
2381 if b != self.other_ref_parts[1]:
2382 if b != self.other_ref_parts[1]:
2382 s = '/_/' + b
2383 s = '/_/' + b
2383 else:
2384 else:
2384 s = '/_/' + self.title
2385 s = '/_/' + self.title
2385 kwargs['extra'] = urlreadable(s)
2386 kwargs['extra'] = urlreadable(s)
2386 if canonical:
2387 if canonical:
2387 return h.canonical_url('pullrequest_show', repo_name=self.other_repo.repo_name,
2388 return h.canonical_url('pullrequest_show', repo_name=self.other_repo.repo_name,
2388 pull_request_id=self.pull_request_id, **kwargs)
2389 pull_request_id=self.pull_request_id, **kwargs)
2389 return h.url('pullrequest_show', repo_name=self.other_repo.repo_name,
2390 return h.url('pullrequest_show', repo_name=self.other_repo.repo_name,
2390 pull_request_id=self.pull_request_id, **kwargs)
2391 pull_request_id=self.pull_request_id, **kwargs)
2391
2392
2392
2393
2393 class PullRequestReviewer(Base, BaseDbModel):
2394 class PullRequestReviewer(Base, BaseDbModel):
2394 __tablename__ = 'pull_request_reviewers'
2395 __tablename__ = 'pull_request_reviewers'
2395 __table_args__ = (
2396 __table_args__ = (
2396 Index('pull_request_reviewers_user_id_idx', 'user_id'),
2397 Index('pull_request_reviewers_user_id_idx', 'user_id'),
2397 _table_args_default_dict,
2398 _table_args_default_dict,
2398 )
2399 )
2399
2400
2400 def __init__(self, user=None, pull_request=None):
2401 def __init__(self, user=None, pull_request=None):
2401 self.user = user
2402 self.user = user
2402 self.pull_request = pull_request
2403 self.pull_request = pull_request
2403
2404
2404 pull_request_reviewers_id = Column('pull_requests_reviewers_id', Integer(), primary_key=True)
2405 pull_request_reviewers_id = Column('pull_requests_reviewers_id', Integer(), primary_key=True)
2405 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
2406 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
2406 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2407 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2407
2408
2408 user = relationship('User')
2409 user = relationship('User')
2409 pull_request = relationship('PullRequest')
2410 pull_request = relationship('PullRequest')
2410
2411
2411 def __json__(self):
2412 def __json__(self):
2412 return dict(
2413 return dict(
2413 username=self.user.username if self.user else None,
2414 username=self.user.username if self.user else None,
2414 )
2415 )
2415
2416
2416
2417
2417 class Notification(object):
2418 class Notification(object):
2418 __tablename__ = 'notifications'
2419 __tablename__ = 'notifications'
2419
2420
2420 class UserNotification(object):
2421 class UserNotification(object):
2421 __tablename__ = 'user_to_notification'
2422 __tablename__ = 'user_to_notification'
2422
2423
2423
2424
2424 class Gist(Base, BaseDbModel):
2425 class Gist(Base, BaseDbModel):
2425 __tablename__ = 'gists'
2426 __tablename__ = 'gists'
2426 __table_args__ = (
2427 __table_args__ = (
2427 Index('g_gist_access_id_idx', 'gist_access_id'),
2428 Index('g_gist_access_id_idx', 'gist_access_id'),
2428 Index('g_created_on_idx', 'created_on'),
2429 Index('g_created_on_idx', 'created_on'),
2429 _table_args_default_dict,
2430 _table_args_default_dict,
2430 )
2431 )
2431
2432
2432 GIST_PUBLIC = u'public'
2433 GIST_PUBLIC = u'public'
2433 GIST_PRIVATE = u'private'
2434 GIST_PRIVATE = u'private'
2434 DEFAULT_FILENAME = u'gistfile1.txt'
2435 DEFAULT_FILENAME = u'gistfile1.txt'
2435
2436
2436 gist_id = Column(Integer(), primary_key=True)
2437 gist_id = Column(Integer(), primary_key=True)
2437 gist_access_id = Column(Unicode(250), nullable=False)
2438 gist_access_id = Column(Unicode(250), nullable=False)
2438 gist_description = Column(UnicodeText(), nullable=False)
2439 gist_description = Column(UnicodeText(), nullable=False)
2439 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2440 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2440 gist_expires = Column(Float(53), nullable=False)
2441 gist_expires = Column(Float(53), nullable=False)
2441 gist_type = Column(Unicode(128), nullable=False)
2442 gist_type = Column(Unicode(128), nullable=False)
2442 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2443 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2443 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2444 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2444
2445
2445 owner = relationship('User')
2446 owner = relationship('User')
2446
2447
2447 @hybrid_property
2448 @hybrid_property
2448 def is_expired(self):
2449 def is_expired(self):
2449 return (self.gist_expires != -1) & (time.time() > self.gist_expires)
2450 return (self.gist_expires != -1) & (time.time() > self.gist_expires)
2450
2451
2451 def __repr__(self):
2452 def __repr__(self):
2452 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
2453 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
2453
2454
2454 @classmethod
2455 @classmethod
2455 def guess_instance(cls, value):
2456 def guess_instance(cls, value):
2456 return super(Gist, cls).guess_instance(value, Gist.get_by_access_id)
2457 return super(Gist, cls).guess_instance(value, Gist.get_by_access_id)
2457
2458
2458 @classmethod
2459 @classmethod
2459 def get_or_404(cls, id_):
2460 def get_or_404(cls, id_):
2460 res = cls.query().filter(cls.gist_access_id == id_).scalar()
2461 res = cls.query().filter(cls.gist_access_id == id_).scalar()
2461 if res is None:
2462 if res is None:
2462 raise HTTPNotFound
2463 raise HTTPNotFound
2463 return res
2464 return res
2464
2465
2465 @classmethod
2466 @classmethod
2466 def get_by_access_id(cls, gist_access_id):
2467 def get_by_access_id(cls, gist_access_id):
2467 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
2468 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
2468
2469
2469 def gist_url(self):
2470 def gist_url(self):
2470 alias_url = kallithea.CONFIG.get('gist_alias_url')
2471 alias_url = kallithea.CONFIG.get('gist_alias_url')
2471 if alias_url:
2472 if alias_url:
2472 return alias_url.replace('{gistid}', self.gist_access_id)
2473 return alias_url.replace('{gistid}', self.gist_access_id)
2473
2474
2474 import kallithea.lib.helpers as h
2475 import kallithea.lib.helpers as h
2475 return h.canonical_url('gist', gist_id=self.gist_access_id)
2476 return h.canonical_url('gist', gist_id=self.gist_access_id)
2476
2477
2477 @classmethod
2478 @classmethod
2478 def base_path(cls):
2479 def base_path(cls):
2479 """
2480 """
2480 Returns base path where all gists are stored
2481 Returns base path where all gists are stored
2481
2482
2482 :param cls:
2483 :param cls:
2483 """
2484 """
2484 from kallithea.model.gist import GIST_STORE_LOC
2485 from kallithea.model.gist import GIST_STORE_LOC
2485 q = Session().query(Ui) \
2486 q = Session().query(Ui) \
2486 .filter(Ui.ui_key == URL_SEP)
2487 .filter(Ui.ui_key == URL_SEP)
2487 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2488 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2488 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
2489 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
2489
2490
2490 def get_api_data(self):
2491 def get_api_data(self):
2491 """
2492 """
2492 Common function for generating gist related data for API
2493 Common function for generating gist related data for API
2493 """
2494 """
2494 gist = self
2495 gist = self
2495 data = dict(
2496 data = dict(
2496 gist_id=gist.gist_id,
2497 gist_id=gist.gist_id,
2497 type=gist.gist_type,
2498 type=gist.gist_type,
2498 access_id=gist.gist_access_id,
2499 access_id=gist.gist_access_id,
2499 description=gist.gist_description,
2500 description=gist.gist_description,
2500 url=gist.gist_url(),
2501 url=gist.gist_url(),
2501 expires=gist.gist_expires,
2502 expires=gist.gist_expires,
2502 created_on=gist.created_on,
2503 created_on=gist.created_on,
2503 )
2504 )
2504 return data
2505 return data
2505
2506
2506 def __json__(self):
2507 def __json__(self):
2507 data = dict(
2508 data = dict(
2508 )
2509 )
2509 data.update(self.get_api_data())
2510 data.update(self.get_api_data())
2510 return data
2511 return data
2511 ## SCM functions
2512 ## SCM functions
2512
2513
2513 @property
2514 @property
2514 def scm_instance(self):
2515 def scm_instance(self):
2515 from kallithea.lib.vcs import get_repo
2516 from kallithea.lib.vcs import get_repo
2516 base_path = self.base_path()
2517 base_path = self.base_path()
2517 return get_repo(os.path.join(safe_str(base_path), safe_str(self.gist_access_id)))
2518 return get_repo(os.path.join(safe_str(base_path), safe_str(self.gist_access_id)))
2518
2519
2519
2520
2520 class UserSshKeys(Base, BaseDbModel):
2521 class UserSshKeys(Base, BaseDbModel):
2521 __tablename__ = 'user_ssh_keys'
2522 __tablename__ = 'user_ssh_keys'
2522 __table_args__ = (
2523 __table_args__ = (
2523 Index('usk_public_key_idx', 'public_key'),
2524 Index('usk_public_key_idx', 'public_key'),
2524 Index('usk_fingerprint_idx', 'fingerprint'),
2525 Index('usk_fingerprint_idx', 'fingerprint'),
2525 UniqueConstraint('fingerprint'),
2526 UniqueConstraint('fingerprint'),
2526 _table_args_default_dict
2527 _table_args_default_dict
2527 )
2528 )
2528 __mapper_args__ = {}
2529 __mapper_args__ = {}
2529
2530
2530 user_ssh_key_id = Column(Integer(), primary_key=True)
2531 user_ssh_key_id = Column(Integer(), primary_key=True)
2531 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2532 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2532 _public_key = Column('public_key', UnicodeText(), nullable=False)
2533 _public_key = Column('public_key', UnicodeText(), nullable=False)
2533 description = Column(UnicodeText(), nullable=False)
2534 description = Column(UnicodeText(), nullable=False)
2534 fingerprint = Column(String(255), nullable=False, unique=True)
2535 fingerprint = Column(String(255), nullable=False, unique=True)
2535 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2536 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2536 last_seen = Column(DateTime(timezone=False), nullable=True)
2537 last_seen = Column(DateTime(timezone=False), nullable=True)
2537
2538
2538 user = relationship('User')
2539 user = relationship('User')
2539
2540
2540 @property
2541 @property
2541 def public_key(self):
2542 def public_key(self):
2542 return self._public_key
2543 return self._public_key
2543
2544
2544 @public_key.setter
2545 @public_key.setter
2545 def public_key(self, full_key):
2546 def public_key(self, full_key):
2546 # the full public key is too long to be suitable as database key - instead,
2547 # the full public key is too long to be suitable as database key - instead,
2547 # use fingerprints similar to 'ssh-keygen -E sha256 -lf ~/.ssh/id_rsa.pub'
2548 # use fingerprints similar to 'ssh-keygen -E sha256 -lf ~/.ssh/id_rsa.pub'
2548 self._public_key = full_key
2549 self._public_key = full_key
2549 enc_key = full_key.split(" ")[1]
2550 enc_key = full_key.split(" ")[1]
2550 self.fingerprint = base64.b64encode(hashlib.sha256(base64.b64decode(enc_key)).digest()).replace(b'\n', b'').rstrip(b'=').decode()
2551 self.fingerprint = base64.b64encode(hashlib.sha256(base64.b64decode(enc_key)).digest()).replace(b'\n', b'').rstrip(b'=').decode()
@@ -1,230 +1,230 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.model.notification
15 kallithea.model.notification
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Model for notifications
18 Model for notifications
19
19
20
20
21 This file was forked by the Kallithea project in July 2014.
21 This file was forked by the Kallithea project in July 2014.
22 Original author and date, and relevant copyright and licensing information is below:
22 Original author and date, and relevant copyright and licensing information is below:
23 :created_on: Nov 20, 2011
23 :created_on: Nov 20, 2011
24 :author: marcink
24 :author: marcink
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :license: GPLv3, see LICENSE.md for more details.
26 :license: GPLv3, see LICENSE.md for more details.
27 """
27 """
28
28
29 import datetime
29 import datetime
30 import logging
30 import logging
31
31
32 from tg import app_globals
32 from tg import app_globals
33 from tg import tmpl_context as c
33 from tg import tmpl_context as c
34 from tg.i18n import ugettext as _
34 from tg.i18n import ugettext as _
35
35
36 import kallithea
36 import kallithea
37 from kallithea.lib import helpers as h
37 from kallithea.lib import helpers as h
38 from kallithea.model.db import User
38 from kallithea.model.db import User
39
39
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 class NotificationModel(object):
44 class NotificationModel(object):
45
45
46 TYPE_CHANGESET_COMMENT = u'cs_comment'
46 TYPE_CHANGESET_COMMENT = u'cs_comment'
47 TYPE_MESSAGE = u'message'
47 TYPE_MESSAGE = u'message'
48 TYPE_MENTION = u'mention' # not used
48 TYPE_MENTION = u'mention' # not used
49 TYPE_REGISTRATION = u'registration'
49 TYPE_REGISTRATION = u'registration'
50 TYPE_PULL_REQUEST = u'pull_request'
50 TYPE_PULL_REQUEST = u'pull_request'
51 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
51 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
52
52
53 def create(self, created_by, subject, body, recipients=None,
53 def create(self, created_by, subject, body, recipients=None,
54 type_=TYPE_MESSAGE, with_email=True,
54 type_=TYPE_MESSAGE, with_email=True,
55 email_kwargs=None, repo_name=None):
55 email_kwargs=None, repo_name=None):
56 """
56 """
57
57
58 Creates notification of given type
58 Creates notification of given type
59
59
60 :param created_by: int, str or User instance. User who created this
60 :param created_by: int, str or User instance. User who created this
61 notification
61 notification
62 :param subject:
62 :param subject:
63 :param body:
63 :param body:
64 :param recipients: list of int, str or User objects, when None
64 :param recipients: list of int, str or User objects, when None
65 is given send to all admins
65 is given send to all admins
66 :param type_: type of notification
66 :param type_: type of notification
67 :param with_email: send email with this notification
67 :param with_email: send email with this notification
68 :param email_kwargs: additional dict to pass as args to email template
68 :param email_kwargs: additional dict to pass as args to email template
69 """
69 """
70 from kallithea.lib.celerylib import tasks
70 from kallithea.lib.celerylib import tasks
71 email_kwargs = email_kwargs or {}
71 email_kwargs = email_kwargs or {}
72 if recipients and not getattr(recipients, '__iter__', False):
72 if recipients and not getattr(recipients, '__iter__', False):
73 raise Exception('recipients must be a list or iterable')
73 raise Exception('recipients must be a list or iterable')
74
74
75 created_by_obj = User.guess_instance(created_by)
75 created_by_obj = User.guess_instance(created_by)
76
76
77 recipients_objs = set()
77 recipients_objs = set()
78 if recipients:
78 if recipients:
79 for u in recipients:
79 for u in recipients:
80 obj = User.guess_instance(u)
80 obj = User.guess_instance(u)
81 if obj is not None:
81 if obj is not None:
82 recipients_objs.add(obj)
82 recipients_objs.add(obj)
83 else:
83 else:
84 # TODO: inform user that requested operation couldn't be completed
84 # TODO: inform user that requested operation couldn't be completed
85 log.error('cannot email unknown user %r', u)
85 log.error('cannot email unknown user %r', u)
86 log.debug('sending notifications %s to %s',
86 log.debug('sending notifications %s to %s',
87 type_, recipients_objs
87 type_, recipients_objs
88 )
88 )
89 elif recipients is None:
89 elif recipients is None:
90 # empty recipients means to all admins
90 # empty recipients means to all admins
91 recipients_objs = User.query().filter(User.admin == True).all()
91 recipients_objs = User.query().filter(User.admin == True).all()
92 log.debug('sending notifications %s to admins: %s',
92 log.debug('sending notifications %s to admins: %s',
93 type_, recipients_objs
93 type_, recipients_objs
94 )
94 )
95 #else: silently skip notification mails?
95 #else: silently skip notification mails?
96
96
97 if not with_email:
97 if not with_email:
98 return
98 return
99
99
100 headers = {}
100 headers = {}
101 headers['X-Kallithea-Notification-Type'] = type_
101 headers['X-Kallithea-Notification-Type'] = type_
102 if 'threading' in email_kwargs:
102 if 'threading' in email_kwargs:
103 headers['References'] = ' '.join('<%s>' % x for x in email_kwargs['threading'])
103 headers['References'] = ' '.join('<%s>' % x for x in email_kwargs['threading'])
104
104
105 # this is passed into template
105 # this is passed into template
106 created_on = h.fmt_date(datetime.datetime.now())
106 created_on = h.fmt_date(datetime.datetime.now())
107 html_kwargs = {
107 html_kwargs = {
108 'subject': subject,
108 'subject': subject,
109 'body': h.render_w_mentions(body, repo_name),
109 'body': h.render_w_mentions(body, repo_name),
110 'when': created_on,
110 'when': created_on,
111 'user': created_by_obj.username,
111 'user': created_by_obj.username,
112 }
112 }
113
113
114 txt_kwargs = {
114 txt_kwargs = {
115 'subject': subject,
115 'subject': subject,
116 'body': body,
116 'body': body,
117 'when': created_on,
117 'when': created_on,
118 'user': created_by_obj.username,
118 'user': created_by_obj.username,
119 }
119 }
120
120
121 html_kwargs.update(email_kwargs)
121 html_kwargs.update(email_kwargs)
122 txt_kwargs.update(email_kwargs)
122 txt_kwargs.update(email_kwargs)
123 email_subject = EmailNotificationModel() \
123 email_subject = EmailNotificationModel() \
124 .get_email_description(type_, **txt_kwargs)
124 .get_email_description(type_, **txt_kwargs)
125 email_txt_body = EmailNotificationModel() \
125 email_txt_body = EmailNotificationModel() \
126 .get_email_tmpl(type_, 'txt', **txt_kwargs)
126 .get_email_tmpl(type_, 'txt', **txt_kwargs)
127 email_html_body = EmailNotificationModel() \
127 email_html_body = EmailNotificationModel() \
128 .get_email_tmpl(type_, 'html', **html_kwargs)
128 .get_email_tmpl(type_, 'html', **html_kwargs)
129
129
130 # don't send email to person who created this comment
130 # don't send email to person who created this comment
131 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
131 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
132
132
133 # send email with notification to all other participants
133 # send email with notification to all other participants
134 for rec in rec_objs:
134 for rec in rec_objs:
135 tasks.send_email([rec.email], email_subject, email_txt_body,
135 tasks.send_email([rec.email], email_subject, email_txt_body,
136 email_html_body, headers, author=created_by_obj)
136 email_html_body, headers, author=created_by_obj)
137
137
138
138
139 class EmailNotificationModel(object):
139 class EmailNotificationModel(object):
140
140
141 TYPE_CHANGESET_COMMENT = NotificationModel.TYPE_CHANGESET_COMMENT
141 TYPE_CHANGESET_COMMENT = NotificationModel.TYPE_CHANGESET_COMMENT
142 TYPE_MESSAGE = NotificationModel.TYPE_MESSAGE # only used for testing
142 TYPE_MESSAGE = NotificationModel.TYPE_MESSAGE # only used for testing
143 # NotificationModel.TYPE_MENTION is not used
143 # NotificationModel.TYPE_MENTION is not used
144 TYPE_PASSWORD_RESET = 'password_link'
144 TYPE_PASSWORD_RESET = 'password_link'
145 TYPE_REGISTRATION = NotificationModel.TYPE_REGISTRATION
145 TYPE_REGISTRATION = NotificationModel.TYPE_REGISTRATION
146 TYPE_PULL_REQUEST = NotificationModel.TYPE_PULL_REQUEST
146 TYPE_PULL_REQUEST = NotificationModel.TYPE_PULL_REQUEST
147 TYPE_PULL_REQUEST_COMMENT = NotificationModel.TYPE_PULL_REQUEST_COMMENT
147 TYPE_PULL_REQUEST_COMMENT = NotificationModel.TYPE_PULL_REQUEST_COMMENT
148 TYPE_DEFAULT = 'default'
148 TYPE_DEFAULT = 'default'
149
149
150 def __init__(self):
150 def __init__(self):
151 super(EmailNotificationModel, self).__init__()
151 super(EmailNotificationModel, self).__init__()
152 self._template_root = kallithea.CONFIG['paths']['templates'][0]
152 self._template_root = kallithea.CONFIG['paths']['templates'][0]
153 self._tmpl_lookup = app_globals.mako_lookup
153 self._tmpl_lookup = app_globals.mako_lookup
154 self.email_types = {
154 self.email_types = {
155 self.TYPE_CHANGESET_COMMENT: 'changeset_comment',
155 self.TYPE_CHANGESET_COMMENT: 'changeset_comment',
156 self.TYPE_PASSWORD_RESET: 'password_reset',
156 self.TYPE_PASSWORD_RESET: 'password_reset',
157 self.TYPE_REGISTRATION: 'registration',
157 self.TYPE_REGISTRATION: 'registration',
158 self.TYPE_DEFAULT: 'default',
158 self.TYPE_DEFAULT: 'default',
159 self.TYPE_PULL_REQUEST: 'pull_request',
159 self.TYPE_PULL_REQUEST: 'pull_request',
160 self.TYPE_PULL_REQUEST_COMMENT: 'pull_request_comment',
160 self.TYPE_PULL_REQUEST_COMMENT: 'pull_request_comment',
161 }
161 }
162 self._subj_map = {
162 self._subj_map = {
163 self.TYPE_CHANGESET_COMMENT: _('[Comment] %(repo_name)s changeset %(short_id)s "%(message_short)s" on %(branch)s'),
163 self.TYPE_CHANGESET_COMMENT: _('[Comment] %(repo_name)s changeset %(short_id)s "%(message_short)s" on %(branch)s'),
164 self.TYPE_MESSAGE: 'Test Message',
164 self.TYPE_MESSAGE: 'Test Message',
165 # self.TYPE_PASSWORD_RESET
165 # self.TYPE_PASSWORD_RESET
166 self.TYPE_REGISTRATION: _('New user %(new_username)s registered'),
166 self.TYPE_REGISTRATION: _('New user %(new_username)s registered'),
167 # self.TYPE_DEFAULT
167 # self.TYPE_DEFAULT
168 self.TYPE_PULL_REQUEST: _('[Review] %(repo_name)s PR %(pr_nice_id)s "%(pr_title_short)s" from %(pr_source_branch)s by %(pr_owner_username)s'),
168 self.TYPE_PULL_REQUEST: _('[Review] %(repo_name)s PR %(pr_nice_id)s "%(pr_title_short)s" from %(pr_source_branch)s by %(pr_owner_username)s'),
169 self.TYPE_PULL_REQUEST_COMMENT: _('[Comment] %(repo_name)s PR %(pr_nice_id)s "%(pr_title_short)s" from %(pr_source_branch)s by %(pr_owner_username)s'),
169 self.TYPE_PULL_REQUEST_COMMENT: _('[Comment] %(repo_name)s PR %(pr_nice_id)s "%(pr_title_short)s" from %(pr_source_branch)s by %(pr_owner_username)s'),
170 }
170 }
171
171
172 def get_email_description(self, type_, **kwargs):
172 def get_email_description(self, type_, **kwargs):
173 """
173 """
174 return subject for email based on given type
174 return subject for email based on given type
175 """
175 """
176 tmpl = self._subj_map[type_]
176 tmpl = self._subj_map[type_]
177 try:
177 try:
178 subj = tmpl % kwargs
178 subj = tmpl % kwargs
179 except KeyError as e:
179 except KeyError as e:
180 log.error('error generating email subject for %r from %s: %s', type_, ','.join(self._subj_map.keys()), e)
180 log.error('error generating email subject for %r from %s: %s', type_, ', '.join(self._subj_map), e)
181 raise
181 raise
182 # gmail doesn't do proper threading but will ignore leading square
182 # gmail doesn't do proper threading but will ignore leading square
183 # bracket content ... so that is where we put status info
183 # bracket content ... so that is where we put status info
184 bracket_tags = []
184 bracket_tags = []
185 status_change = kwargs.get('status_change')
185 status_change = kwargs.get('status_change')
186 if status_change:
186 if status_change:
187 bracket_tags.append(unicode(status_change)) # apply unicode to evaluate LazyString before .join
187 bracket_tags.append(unicode(status_change)) # apply unicode to evaluate LazyString before .join
188 if kwargs.get('closing_pr'):
188 if kwargs.get('closing_pr'):
189 bracket_tags.append(_('Closing'))
189 bracket_tags.append(_('Closing'))
190 if bracket_tags:
190 if bracket_tags:
191 if subj.startswith('['):
191 if subj.startswith('['):
192 subj = '[' + ', '.join(bracket_tags) + ': ' + subj[1:]
192 subj = '[' + ', '.join(bracket_tags) + ': ' + subj[1:]
193 else:
193 else:
194 subj = '[' + ', '.join(bracket_tags) + '] ' + subj
194 subj = '[' + ', '.join(bracket_tags) + '] ' + subj
195 return subj
195 return subj
196
196
197 def get_email_tmpl(self, type_, content_type, **kwargs):
197 def get_email_tmpl(self, type_, content_type, **kwargs):
198 """
198 """
199 return generated template for email based on given type
199 return generated template for email based on given type
200 """
200 """
201
201
202 base = 'email_templates/' + self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT]) + '.' + content_type
202 base = 'email_templates/' + self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT]) + '.' + content_type
203 email_template = self._tmpl_lookup.get_template(base)
203 email_template = self._tmpl_lookup.get_template(base)
204 # translator and helpers inject
204 # translator and helpers inject
205 _kwargs = {'_': _,
205 _kwargs = {'_': _,
206 'h': h,
206 'h': h,
207 'c': c}
207 'c': c}
208 _kwargs.update(kwargs)
208 _kwargs.update(kwargs)
209 if content_type == 'html':
209 if content_type == 'html':
210 _kwargs.update({
210 _kwargs.update({
211 "color_text": "#202020",
211 "color_text": "#202020",
212 "color_emph": "#395fa0",
212 "color_emph": "#395fa0",
213 "color_link": "#395fa0",
213 "color_link": "#395fa0",
214 "color_border": "#ddd",
214 "color_border": "#ddd",
215 "color_background_grey": "#f9f9f9",
215 "color_background_grey": "#f9f9f9",
216 "color_button": "#395fa0",
216 "color_button": "#395fa0",
217 "monospace_style": "font-family:Lucida Console,Consolas,Monaco,Inconsolata,Liberation Mono,monospace",
217 "monospace_style": "font-family:Lucida Console,Consolas,Monaco,Inconsolata,Liberation Mono,monospace",
218 "sans_style": "font-family:Helvetica,Arial,sans-serif",
218 "sans_style": "font-family:Helvetica,Arial,sans-serif",
219 })
219 })
220 _kwargs.update({
220 _kwargs.update({
221 "default_style": "%(sans_style)s;font-weight:200;font-size:14px;line-height:17px;color:%(color_text)s" % _kwargs,
221 "default_style": "%(sans_style)s;font-weight:200;font-size:14px;line-height:17px;color:%(color_text)s" % _kwargs,
222 "comment_style": "%(monospace_style)s;white-space:pre-wrap" % _kwargs,
222 "comment_style": "%(monospace_style)s;white-space:pre-wrap" % _kwargs,
223 "data_style": "border:%(color_border)s 1px solid;background:%(color_background_grey)s" % _kwargs,
223 "data_style": "border:%(color_border)s 1px solid;background:%(color_background_grey)s" % _kwargs,
224 "emph_style": "font-weight:600;color:%(color_emph)s" % _kwargs,
224 "emph_style": "font-weight:600;color:%(color_emph)s" % _kwargs,
225 "link_style": "color:%(color_link)s;text-decoration:none" % _kwargs,
225 "link_style": "color:%(color_link)s;text-decoration:none" % _kwargs,
226 "link_text_style": "color:%(color_text)s;text-decoration:none;border:%(color_border)s 1px solid;background:%(color_background_grey)s" % _kwargs,
226 "link_text_style": "color:%(color_text)s;text-decoration:none;border:%(color_border)s 1px solid;background:%(color_background_grey)s" % _kwargs,
227 })
227 })
228
228
229 log.debug('rendering tmpl %s with kwargs %s', base, _kwargs)
229 log.debug('rendering tmpl %s with kwargs %s', base, _kwargs)
230 return email_template.render_unicode(**_kwargs)
230 return email_template.render_unicode(**_kwargs)
@@ -1,130 +1,130 b''
1 ## snippet for displaying permissions overview for users
1 ## snippet for displaying permissions overview for users
2 ## usage:
2 ## usage:
3 ## <%namespace name="p" file="/base/perms_summary.html"/>
3 ## <%namespace name="p" file="/base/perms_summary.html"/>
4 ## ${p.perms_summary(c.perm_user.permissions)}
4 ## ${p.perms_summary(c.perm_user.permissions)}
5
5
6 <%def name="perms_summary(permissions, show_all=False, actions=True)">
6 <%def name="perms_summary(permissions, show_all=False, actions=True)">
7 <div id="perms">
7 <div id="perms">
8 %for section in sorted(permissions.keys()):
8 %for section in sorted(permissions):
9 <div class="perms_section_head">
9 <div class="perms_section_head">
10 <h4>${section.replace("_"," ").capitalize()}</h4>
10 <h4>${section.replace("_"," ").capitalize()}</h4>
11 %if section != 'global':
11 %if section != 'global':
12 <div class="pull-right checkbox">
12 <div class="pull-right checkbox">
13 ${_('Show')}:
13 ${_('Show')}:
14 <label>${h.checkbox('perms_filter_none_%s' % section, 'none', 'checked', class_='perm_filter filter_%s' % section, **{'data-section':section, 'data-perm_type':'none'})}<span class="label label-none">${_('None')}</span></label>
14 <label>${h.checkbox('perms_filter_none_%s' % section, 'none', 'checked', class_='perm_filter filter_%s' % section, **{'data-section':section, 'data-perm_type':'none'})}<span class="label label-none">${_('None')}</span></label>
15 <label>${h.checkbox('perms_filter_read_%s' % section, 'read', 'checked', class_='perm_filter filter_%s' % section, **{'data-section':section, 'data-perm_type':'read'})}<span class="label label-read">${_('Read')}</span></label>
15 <label>${h.checkbox('perms_filter_read_%s' % section, 'read', 'checked', class_='perm_filter filter_%s' % section, **{'data-section':section, 'data-perm_type':'read'})}<span class="label label-read">${_('Read')}</span></label>
16 <label>${h.checkbox('perms_filter_write_%s' % section, 'write', 'checked', class_='perm_filter filter_%s' % section, **{'data-section':section, 'data-perm_type':'write'})}<span class="label label-write">${_('Write')}</span></label>
16 <label>${h.checkbox('perms_filter_write_%s' % section, 'write', 'checked', class_='perm_filter filter_%s' % section, **{'data-section':section, 'data-perm_type':'write'})}<span class="label label-write">${_('Write')}</span></label>
17 <label>${h.checkbox('perms_filter_admin_%s' % section, 'admin', 'checked', class_='perm_filter filter_%s' % section, **{'data-section':section, 'data-perm_type':'admin'})}<span class="label label-admin">${_('Admin')}</span></label>
17 <label>${h.checkbox('perms_filter_admin_%s' % section, 'admin', 'checked', class_='perm_filter filter_%s' % section, **{'data-section':section, 'data-perm_type':'admin'})}<span class="label label-admin">${_('Admin')}</span></label>
18 </div>
18 </div>
19 %endif
19 %endif
20 </div>
20 </div>
21 %if not permissions[section]:
21 %if not permissions[section]:
22 <span class="text-muted">${_('No permissions defined yet')}</span>
22 <span class="text-muted">${_('No permissions defined yet')}</span>
23 %else:
23 %else:
24 <div id='tbl_list_wrap_${section}'>
24 <div id='tbl_list_wrap_${section}'>
25 <table id="tbl_list_${section}" class="table">
25 <table id="tbl_list_${section}" class="table">
26 ## global permission box
26 ## global permission box
27 %if section == 'global':
27 %if section == 'global':
28 <thead>
28 <thead>
29 <tr>
29 <tr>
30 <th class="left col-xs-9">${_('Permission')}</th>
30 <th class="left col-xs-9">${_('Permission')}</th>
31 %if actions:
31 %if actions:
32 <th class="left col-xs-3">${_('Edit Permission')}</th>
32 <th class="left col-xs-3">${_('Edit Permission')}</th>
33 %endif
33 %endif
34 </tr>
34 </tr>
35 </thead>
35 </thead>
36 <tbody>
36 <tbody>
37 %for k in permissions[section]:
37 %for k in permissions[section]:
38 <tr>
38 <tr>
39 <td>
39 <td>
40 ${h.get_permission_name(k)}
40 ${h.get_permission_name(k)}
41 </td>
41 </td>
42 %if actions:
42 %if actions:
43 <td>
43 <td>
44 <a href="${h.url('admin_permissions')}">${_('Edit')}</a>
44 <a href="${h.url('admin_permissions')}">${_('Edit')}</a>
45 </td>
45 </td>
46 %endif
46 %endif
47 </tr>
47 </tr>
48 %endfor
48 %endfor
49 </tbody>
49 </tbody>
50 %else:
50 %else:
51 ## none/read/write/admin permissions on groups/repos etc
51 ## none/read/write/admin permissions on groups/repos etc
52 <thead>
52 <thead>
53 <tr>
53 <tr>
54 <th class="left col-xs-7">${_('Name')}</th>
54 <th class="left col-xs-7">${_('Name')}</th>
55 <th class="left col-xs-2">${_('Permission')}</th>
55 <th class="left col-xs-2">${_('Permission')}</th>
56 %if actions:
56 %if actions:
57 <th class="left col-xs-3">${_('Edit Permission')}</th>
57 <th class="left col-xs-3">${_('Edit Permission')}</th>
58 %endif
58 %endif
59 </tr>
59 </tr>
60 </thead>
60 </thead>
61 <tbody class="section_${section}">
61 <tbody class="section_${section}">
62 %for k, section_perm in sorted(permissions[section].items(), key=lambda s: {'none':0, 'read':1,'write':2,'admin':3}.get(s[1].split('.')[-1])):
62 %for k, section_perm in sorted(permissions[section].items(), key=lambda s: {'none':0, 'read':1,'write':2,'admin':3}.get(s[1].split('.')[-1])):
63 %if section_perm.split('.')[-1] != 'none' or show_all:
63 %if section_perm.split('.')[-1] != 'none' or show_all:
64 <tr class="perm_row ${'%s_%s' % (section, section_perm.split('.')[-1])}">
64 <tr class="perm_row ${'%s_%s' % (section, section_perm.split('.')[-1])}">
65 <td>
65 <td>
66 %if section == 'repositories':
66 %if section == 'repositories':
67 <a href="${h.url('summary_home',repo_name=k)}">${k}</a>
67 <a href="${h.url('summary_home',repo_name=k)}">${k}</a>
68 %elif section == 'repositories_groups':
68 %elif section == 'repositories_groups':
69 <a href="${h.url('repos_group_home',group_name=k)}">${k}</a>
69 <a href="${h.url('repos_group_home',group_name=k)}">${k}</a>
70 %elif section == 'user_groups':
70 %elif section == 'user_groups':
71 ##<a href="${h.url('edit_users_group',id=k)}">${k}</a>
71 ##<a href="${h.url('edit_users_group',id=k)}">${k}</a>
72 ${k}
72 ${k}
73 %endif
73 %endif
74 </td>
74 </td>
75 <td>
75 <td>
76 <span class="label label-${section_perm.split('.')[-1]}">${section_perm}</span>
76 <span class="label label-${section_perm.split('.')[-1]}">${section_perm}</span>
77 </td>
77 </td>
78 %if actions:
78 %if actions:
79 <td>
79 <td>
80 %if section == 'repositories':
80 %if section == 'repositories':
81 <a href="${h.url('edit_repo_perms',repo_name=k,anchor='permissions_manage')}">${_('Edit')}</a>
81 <a href="${h.url('edit_repo_perms',repo_name=k,anchor='permissions_manage')}">${_('Edit')}</a>
82 %elif section == 'repositories_groups':
82 %elif section == 'repositories_groups':
83 <a href="${h.url('edit_repo_group_perms',group_name=k,anchor='permissions_manage')}">${_('Edit')}</a>
83 <a href="${h.url('edit_repo_group_perms',group_name=k,anchor='permissions_manage')}">${_('Edit')}</a>
84 %elif section == 'user_groups':
84 %elif section == 'user_groups':
85 ##<a href="${h.url('edit_users_group',id=k)}">${_('Edit')}</a>
85 ##<a href="${h.url('edit_users_group',id=k)}">${_('Edit')}</a>
86 %endif
86 %endif
87 </td>
87 </td>
88 %endif
88 %endif
89 </tr>
89 </tr>
90 %endif
90 %endif
91 %endfor
91 %endfor
92 <tr id="empty_${section}" style="display: none"><td colspan="${3 if actions else 2}">${_('No permission defined')}</td></tr>
92 <tr id="empty_${section}" style="display: none"><td colspan="${3 if actions else 2}">${_('No permission defined')}</td></tr>
93 </tbody>
93 </tbody>
94 %endif
94 %endif
95 </table>
95 </table>
96 </div>
96 </div>
97 %endif
97 %endif
98 %endfor
98 %endfor
99 </div>
99 </div>
100 <script>
100 <script>
101 $(document).ready(function(){
101 $(document).ready(function(){
102 var show_empty = function(section){
102 var show_empty = function(section){
103 var visible = $('.section_{0} tr.perm_row:visible'.format(section)).length;
103 var visible = $('.section_{0} tr.perm_row:visible'.format(section)).length;
104 if(visible == 0){
104 if(visible == 0){
105 $('#empty_{0}'.format(section)).show();
105 $('#empty_{0}'.format(section)).show();
106 }
106 }
107 else{
107 else{
108 $('#empty_{0}'.format(section)).hide();
108 $('#empty_{0}'.format(section)).hide();
109 }
109 }
110 }
110 }
111 var update_show = function($checkbox){
111 var update_show = function($checkbox){
112 var section = $checkbox.data('section');
112 var section = $checkbox.data('section');
113
113
114 var elems = $('.filter_' + section).each(function(el){
114 var elems = $('.filter_' + section).each(function(el){
115 var perm_type = $checkbox.data('perm_type');
115 var perm_type = $checkbox.data('perm_type');
116 var checked = $checkbox.prop('checked');
116 var checked = $checkbox.prop('checked');
117 if(checked){
117 if(checked){
118 $('.'+section+'_'+perm_type).show();
118 $('.'+section+'_'+perm_type).show();
119 }
119 }
120 else{
120 else{
121 $('.'+section+'_'+perm_type).hide();
121 $('.'+section+'_'+perm_type).hide();
122 }
122 }
123 });
123 });
124 show_empty(section);
124 show_empty(section);
125 }
125 }
126 $('.perm_filter').on('change', function(){update_show($(this));});
126 $('.perm_filter').on('change', function(){update_show($(this));});
127 $('.perm_filter[value=none]').each(function(){this.checked = false; update_show($(this));});
127 $('.perm_filter[value=none]').each(function(){this.checked = false; update_show($(this));});
128 });
128 });
129 </script>
129 </script>
130 </%def>
130 </%def>
General Comments 0
You need to be logged in to leave comments. Login now