##// END OF EJS Templates
controllers: don't pass rendered templates in context variables...
Thomas De Schampheleire -
r4850:a4082453 default
parent child Browse files
Show More
@@ -1,148 +1,148 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.admin.admin
15 kallithea.controllers.admin.admin
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Controller for Admin panel of Kallithea
18 Controller for Admin panel of 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 7, 2010
22 :created_on: Apr 7, 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
30
31 from pylons import request, tmpl_context as c, url
31 from pylons import request, tmpl_context as c, url
32 from sqlalchemy.orm import joinedload
32 from sqlalchemy.orm import joinedload
33 from whoosh.qparser.default import QueryParser
33 from whoosh.qparser.default import QueryParser
34 from whoosh.qparser.dateparse import DateParserPlugin
34 from whoosh.qparser.dateparse import DateParserPlugin
35 from whoosh import query
35 from whoosh import query
36 from sqlalchemy.sql.expression import or_, and_, func
36 from sqlalchemy.sql.expression import or_, and_, func
37
37
38 from kallithea.model.db import UserLog
38 from kallithea.model.db import UserLog
39 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
39 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
40 from kallithea.lib.base import BaseController, render
40 from kallithea.lib.base import BaseController, render
41 from kallithea.lib.utils2 import safe_int, remove_prefix, remove_suffix
41 from kallithea.lib.utils2 import safe_int, remove_prefix, remove_suffix
42 from kallithea.lib.indexers import JOURNAL_SCHEMA
42 from kallithea.lib.indexers import JOURNAL_SCHEMA
43 from kallithea.lib.helpers import Page
43 from kallithea.lib.helpers import Page
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 def _journal_filter(user_log, search_term):
49 def _journal_filter(user_log, search_term):
50 """
50 """
51 Filters sqlalchemy user_log based on search_term with whoosh Query language
51 Filters sqlalchemy user_log based on search_term with whoosh Query language
52 http://packages.python.org/Whoosh/querylang.html
52 http://packages.python.org/Whoosh/querylang.html
53
53
54 :param user_log:
54 :param user_log:
55 :param search_term:
55 :param search_term:
56 """
56 """
57 log.debug('Initial search term: %r' % search_term)
57 log.debug('Initial search term: %r' % search_term)
58 qry = None
58 qry = None
59 if search_term:
59 if search_term:
60 qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
60 qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
61 qp.add_plugin(DateParserPlugin())
61 qp.add_plugin(DateParserPlugin())
62 qry = qp.parse(unicode(search_term))
62 qry = qp.parse(unicode(search_term))
63 log.debug('Filtering using parsed query %r' % qry)
63 log.debug('Filtering using parsed query %r' % qry)
64
64
65 def wildcard_handler(col, wc_term):
65 def wildcard_handler(col, wc_term):
66 if wc_term.startswith('*') and not wc_term.endswith('*'):
66 if wc_term.startswith('*') and not wc_term.endswith('*'):
67 #postfix == endswith
67 #postfix == endswith
68 wc_term = remove_prefix(wc_term, prefix='*')
68 wc_term = remove_prefix(wc_term, prefix='*')
69 return func.lower(col).endswith(wc_term)
69 return func.lower(col).endswith(wc_term)
70 elif wc_term.startswith('*') and wc_term.endswith('*'):
70 elif wc_term.startswith('*') and wc_term.endswith('*'):
71 #wildcard == ilike
71 #wildcard == ilike
72 wc_term = remove_prefix(wc_term, prefix='*')
72 wc_term = remove_prefix(wc_term, prefix='*')
73 wc_term = remove_suffix(wc_term, suffix='*')
73 wc_term = remove_suffix(wc_term, suffix='*')
74 return func.lower(col).contains(wc_term)
74 return func.lower(col).contains(wc_term)
75
75
76 def get_filterion(field, val, term):
76 def get_filterion(field, val, term):
77
77
78 if field == 'repository':
78 if field == 'repository':
79 field = getattr(UserLog, 'repository_name')
79 field = getattr(UserLog, 'repository_name')
80 elif field == 'ip':
80 elif field == 'ip':
81 field = getattr(UserLog, 'user_ip')
81 field = getattr(UserLog, 'user_ip')
82 elif field == 'date':
82 elif field == 'date':
83 field = getattr(UserLog, 'action_date')
83 field = getattr(UserLog, 'action_date')
84 elif field == 'username':
84 elif field == 'username':
85 field = getattr(UserLog, 'username')
85 field = getattr(UserLog, 'username')
86 else:
86 else:
87 field = getattr(UserLog, field)
87 field = getattr(UserLog, field)
88 log.debug('filter field: %s val=>%s' % (field, val))
88 log.debug('filter field: %s val=>%s' % (field, val))
89
89
90 #sql filtering
90 #sql filtering
91 if isinstance(term, query.Wildcard):
91 if isinstance(term, query.Wildcard):
92 return wildcard_handler(field, val)
92 return wildcard_handler(field, val)
93 elif isinstance(term, query.Prefix):
93 elif isinstance(term, query.Prefix):
94 return func.lower(field).startswith(func.lower(val))
94 return func.lower(field).startswith(func.lower(val))
95 elif isinstance(term, query.DateRange):
95 elif isinstance(term, query.DateRange):
96 return and_(field >= val[0], field <= val[1])
96 return and_(field >= val[0], field <= val[1])
97 return func.lower(field) == func.lower(val)
97 return func.lower(field) == func.lower(val)
98
98
99 if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard,
99 if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard,
100 query.DateRange)):
100 query.DateRange)):
101 if not isinstance(qry, query.And):
101 if not isinstance(qry, query.And):
102 qry = [qry]
102 qry = [qry]
103 for term in qry:
103 for term in qry:
104 field = term.fieldname
104 field = term.fieldname
105 val = (term.text if not isinstance(term, query.DateRange)
105 val = (term.text if not isinstance(term, query.DateRange)
106 else [term.startdate, term.enddate])
106 else [term.startdate, term.enddate])
107 user_log = user_log.filter(get_filterion(field, val, term))
107 user_log = user_log.filter(get_filterion(field, val, term))
108 elif isinstance(qry, query.Or):
108 elif isinstance(qry, query.Or):
109 filters = []
109 filters = []
110 for term in qry:
110 for term in qry:
111 field = term.fieldname
111 field = term.fieldname
112 val = (term.text if not isinstance(term, query.DateRange)
112 val = (term.text if not isinstance(term, query.DateRange)
113 else [term.startdate, term.enddate])
113 else [term.startdate, term.enddate])
114 filters.append(get_filterion(field, val, term))
114 filters.append(get_filterion(field, val, term))
115 user_log = user_log.filter(or_(*filters))
115 user_log = user_log.filter(or_(*filters))
116
116
117 return user_log
117 return user_log
118
118
119
119
120 class AdminController(BaseController):
120 class AdminController(BaseController):
121
121
122 @LoginRequired()
122 @LoginRequired()
123 def __before__(self):
123 def __before__(self):
124 super(AdminController, self).__before__()
124 super(AdminController, self).__before__()
125
125
126 @HasPermissionAllDecorator('hg.admin')
126 @HasPermissionAllDecorator('hg.admin')
127 def index(self):
127 def index(self):
128 users_log = UserLog.query()\
128 users_log = UserLog.query()\
129 .options(joinedload(UserLog.user))\
129 .options(joinedload(UserLog.user))\
130 .options(joinedload(UserLog.repository))
130 .options(joinedload(UserLog.repository))
131
131
132 #FILTERING
132 #FILTERING
133 c.search_term = request.GET.get('filter')
133 c.search_term = request.GET.get('filter')
134 users_log = _journal_filter(users_log, c.search_term)
134 users_log = _journal_filter(users_log, c.search_term)
135
135
136 users_log = users_log.order_by(UserLog.action_date.desc())
136 users_log = users_log.order_by(UserLog.action_date.desc())
137
137
138 p = safe_int(request.GET.get('page', 1), 1)
138 p = safe_int(request.GET.get('page', 1), 1)
139
139
140 def url_generator(**kw):
140 def url_generator(**kw):
141 return url.current(filter=c.search_term, **kw)
141 return url.current(filter=c.search_term, **kw)
142
142
143 c.users_log = Page(users_log, page=p, items_per_page=10, url=url_generator)
143 c.users_log = Page(users_log, page=p, items_per_page=10, url=url_generator)
144 c.log_data = render('admin/admin_log.html')
145
144
146 if request.environ.get('HTTP_X_PARTIAL_XHR'):
145 if request.environ.get('HTTP_X_PARTIAL_XHR'):
147 return c.log_data
146 return render('admin/admin_log.html')
147
148 return render('admin/admin.html')
148 return render('admin/admin.html')
@@ -1,61 +1,59 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.followers
15 kallithea.controllers.followers
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Followers controller for Kallithea
18 Followers 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 23, 2011
22 :created_on: Apr 23, 2011
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
29
30 from pylons import tmpl_context as c, request
30 from pylons import tmpl_context as c, request
31
31
32 from kallithea.lib.helpers import Page
32 from kallithea.lib.helpers import Page
33 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from kallithea.lib.base import BaseRepoController, render
34 from kallithea.lib.base import BaseRepoController, render
35 from kallithea.model.db import UserFollowing
35 from kallithea.model.db import UserFollowing
36 from kallithea.lib.utils2 import safe_int
36 from kallithea.lib.utils2 import safe_int
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 class FollowersController(BaseRepoController):
41 class FollowersController(BaseRepoController):
42
42
43 def __before__(self):
43 def __before__(self):
44 super(FollowersController, self).__before__()
44 super(FollowersController, self).__before__()
45
45
46 @LoginRequired()
46 @LoginRequired()
47 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
47 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
48 'repository.admin')
48 'repository.admin')
49 def followers(self, repo_name):
49 def followers(self, repo_name):
50 p = safe_int(request.GET.get('page', 1), 1)
50 p = safe_int(request.GET.get('page', 1), 1)
51 repo_id = c.db_repo.repo_id
51 repo_id = c.db_repo.repo_id
52 d = UserFollowing.get_repo_followers(repo_id)\
52 d = UserFollowing.get_repo_followers(repo_id)\
53 .order_by(UserFollowing.follows_from)
53 .order_by(UserFollowing.follows_from)
54 c.followers_pager = Page(d, page=p, items_per_page=20)
54 c.followers_pager = Page(d, page=p, items_per_page=20)
55
55
56 c.followers_data = render('/followers/followers_data.html')
57
58 if request.environ.get('HTTP_X_PARTIAL_XHR'):
56 if request.environ.get('HTTP_X_PARTIAL_XHR'):
59 return c.followers_data
57 return render('/followers/followers_data.html')
60
58
61 return render('/followers/followers.html')
59 return render('/followers/followers.html')
@@ -1,194 +1,192 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.forks
15 kallithea.controllers.forks
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 forks controller for Kallithea
18 forks 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 23, 2011
22 :created_on: Apr 23, 2011
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 formencode
29 import formencode
30 import traceback
30 import traceback
31 from formencode import htmlfill
31 from formencode import htmlfill
32
32
33 from pylons import tmpl_context as c, request, url
33 from pylons import tmpl_context as c, request, url
34 from pylons.controllers.util import redirect
34 from pylons.controllers.util import redirect
35 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36
36
37 import kallithea.lib.helpers as h
37 import kallithea.lib.helpers as h
38
38
39 from kallithea.lib.helpers import Page
39 from kallithea.lib.helpers import Page
40 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
40 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
41 NotAnonymous, HasRepoPermissionAny, HasPermissionAnyDecorator
41 NotAnonymous, HasRepoPermissionAny, HasPermissionAnyDecorator
42 from kallithea.lib.base import BaseRepoController, render
42 from kallithea.lib.base import BaseRepoController, render
43 from kallithea.model.db import Repository, RepoGroup, UserFollowing, User,\
43 from kallithea.model.db import Repository, RepoGroup, UserFollowing, User,\
44 Ui
44 Ui
45 from kallithea.model.repo import RepoModel
45 from kallithea.model.repo import RepoModel
46 from kallithea.model.forms import RepoForkForm
46 from kallithea.model.forms import RepoForkForm
47 from kallithea.model.scm import ScmModel, RepoGroupList
47 from kallithea.model.scm import ScmModel, RepoGroupList
48 from kallithea.lib.utils2 import safe_int
48 from kallithea.lib.utils2 import safe_int
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class ForksController(BaseRepoController):
53 class ForksController(BaseRepoController):
54
54
55 def __before__(self):
55 def __before__(self):
56 super(ForksController, self).__before__()
56 super(ForksController, self).__before__()
57
57
58 def __load_defaults(self):
58 def __load_defaults(self):
59 acl_groups = RepoGroupList(RepoGroup.query().all(),
59 acl_groups = RepoGroupList(RepoGroup.query().all(),
60 perm_set=['group.write', 'group.admin'])
60 perm_set=['group.write', 'group.admin'])
61 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
61 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
62 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
62 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
63 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
63 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
64 c.landing_revs_choices = choices
64 c.landing_revs_choices = choices
65 c.can_update = Ui.get_by_key(Ui.HOOK_UPDATE).ui_active
65 c.can_update = Ui.get_by_key(Ui.HOOK_UPDATE).ui_active
66
66
67 def __load_data(self, repo_name=None):
67 def __load_data(self, repo_name=None):
68 """
68 """
69 Load defaults settings for edit, and update
69 Load defaults settings for edit, and update
70
70
71 :param repo_name:
71 :param repo_name:
72 """
72 """
73 self.__load_defaults()
73 self.__load_defaults()
74
74
75 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
75 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
76 repo = db_repo.scm_instance
76 repo = db_repo.scm_instance
77
77
78 if c.repo_info is None:
78 if c.repo_info is None:
79 h.not_mapped_error(repo_name)
79 h.not_mapped_error(repo_name)
80 return redirect(url('repos'))
80 return redirect(url('repos'))
81
81
82 c.default_user_id = User.get_default_user().user_id
82 c.default_user_id = User.get_default_user().user_id
83 c.in_public_journal = UserFollowing.query()\
83 c.in_public_journal = UserFollowing.query()\
84 .filter(UserFollowing.user_id == c.default_user_id)\
84 .filter(UserFollowing.user_id == c.default_user_id)\
85 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
85 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
86
86
87 if c.repo_info.stats:
87 if c.repo_info.stats:
88 last_rev = c.repo_info.stats.stat_on_revision+1
88 last_rev = c.repo_info.stats.stat_on_revision+1
89 else:
89 else:
90 last_rev = 0
90 last_rev = 0
91 c.stats_revision = last_rev
91 c.stats_revision = last_rev
92
92
93 c.repo_last_rev = repo.count() if repo.revisions else 0
93 c.repo_last_rev = repo.count() if repo.revisions else 0
94
94
95 if last_rev == 0 or c.repo_last_rev == 0:
95 if last_rev == 0 or c.repo_last_rev == 0:
96 c.stats_percentage = 0
96 c.stats_percentage = 0
97 else:
97 else:
98 c.stats_percentage = '%.2f' % ((float((last_rev)) /
98 c.stats_percentage = '%.2f' % ((float((last_rev)) /
99 c.repo_last_rev) * 100)
99 c.repo_last_rev) * 100)
100
100
101 defaults = RepoModel()._get_defaults(repo_name)
101 defaults = RepoModel()._get_defaults(repo_name)
102 # alter the description to indicate a fork
102 # alter the description to indicate a fork
103 defaults['description'] = ('fork of repository: %s \n%s'
103 defaults['description'] = ('fork of repository: %s \n%s'
104 % (defaults['repo_name'],
104 % (defaults['repo_name'],
105 defaults['description']))
105 defaults['description']))
106 # add suffix to fork
106 # add suffix to fork
107 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
107 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
108
108
109 return defaults
109 return defaults
110
110
111 @LoginRequired()
111 @LoginRequired()
112 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
112 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
113 'repository.admin')
113 'repository.admin')
114 def forks(self, repo_name):
114 def forks(self, repo_name):
115 p = safe_int(request.GET.get('page', 1), 1)
115 p = safe_int(request.GET.get('page', 1), 1)
116 repo_id = c.db_repo.repo_id
116 repo_id = c.db_repo.repo_id
117 d = []
117 d = []
118 for r in Repository.get_repo_forks(repo_id):
118 for r in Repository.get_repo_forks(repo_id):
119 if not HasRepoPermissionAny(
119 if not HasRepoPermissionAny(
120 'repository.read', 'repository.write', 'repository.admin'
120 'repository.read', 'repository.write', 'repository.admin'
121 )(r.repo_name, 'get forks check'):
121 )(r.repo_name, 'get forks check'):
122 continue
122 continue
123 d.append(r)
123 d.append(r)
124 c.forks_pager = Page(d, page=p, items_per_page=20)
124 c.forks_pager = Page(d, page=p, items_per_page=20)
125
125
126 c.forks_data = render('/forks/forks_data.html')
127
128 if request.environ.get('HTTP_X_PARTIAL_XHR'):
126 if request.environ.get('HTTP_X_PARTIAL_XHR'):
129 return c.forks_data
127 return render('/forks/forks_data.html')
130
128
131 return render('/forks/forks.html')
129 return render('/forks/forks.html')
132
130
133 @LoginRequired()
131 @LoginRequired()
134 @NotAnonymous()
132 @NotAnonymous()
135 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
133 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
134 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
137 'repository.admin')
135 'repository.admin')
138 def fork(self, repo_name):
136 def fork(self, repo_name):
139 c.repo_info = Repository.get_by_repo_name(repo_name)
137 c.repo_info = Repository.get_by_repo_name(repo_name)
140 if not c.repo_info:
138 if not c.repo_info:
141 h.not_mapped_error(repo_name)
139 h.not_mapped_error(repo_name)
142 return redirect(url('home'))
140 return redirect(url('home'))
143
141
144 defaults = self.__load_data(repo_name)
142 defaults = self.__load_data(repo_name)
145
143
146 return htmlfill.render(
144 return htmlfill.render(
147 render('forks/fork.html'),
145 render('forks/fork.html'),
148 defaults=defaults,
146 defaults=defaults,
149 encoding="UTF-8",
147 encoding="UTF-8",
150 force_defaults=False
148 force_defaults=False
151 )
149 )
152
150
153 @LoginRequired()
151 @LoginRequired()
154 @NotAnonymous()
152 @NotAnonymous()
155 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
153 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
156 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
154 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
157 'repository.admin')
155 'repository.admin')
158 def fork_create(self, repo_name):
156 def fork_create(self, repo_name):
159 self.__load_defaults()
157 self.__load_defaults()
160 c.repo_info = Repository.get_by_repo_name(repo_name)
158 c.repo_info = Repository.get_by_repo_name(repo_name)
161 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
159 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
162 repo_groups=c.repo_groups_choices,
160 repo_groups=c.repo_groups_choices,
163 landing_revs=c.landing_revs_choices)()
161 landing_revs=c.landing_revs_choices)()
164 form_result = {}
162 form_result = {}
165 task_id = None
163 task_id = None
166 try:
164 try:
167 form_result = _form.to_python(dict(request.POST))
165 form_result = _form.to_python(dict(request.POST))
168
166
169 # an approximation that is better than nothing
167 # an approximation that is better than nothing
170 if not Ui.get_by_key(Ui.HOOK_UPDATE).ui_active:
168 if not Ui.get_by_key(Ui.HOOK_UPDATE).ui_active:
171 form_result['update_after_clone'] = False
169 form_result['update_after_clone'] = False
172
170
173 # create fork is done sometimes async on celery, db transaction
171 # create fork is done sometimes async on celery, db transaction
174 # management is handled there.
172 # management is handled there.
175 task = RepoModel().create_fork(form_result, self.authuser.user_id)
173 task = RepoModel().create_fork(form_result, self.authuser.user_id)
176 from celery.result import BaseAsyncResult
174 from celery.result import BaseAsyncResult
177 if isinstance(task, BaseAsyncResult):
175 if isinstance(task, BaseAsyncResult):
178 task_id = task.task_id
176 task_id = task.task_id
179 except formencode.Invalid, errors:
177 except formencode.Invalid, errors:
180 c.new_repo = errors.value['repo_name']
178 c.new_repo = errors.value['repo_name']
181 return htmlfill.render(
179 return htmlfill.render(
182 render('forks/fork.html'),
180 render('forks/fork.html'),
183 defaults=errors.value,
181 defaults=errors.value,
184 errors=errors.error_dict or {},
182 errors=errors.error_dict or {},
185 prefix_error=False,
183 prefix_error=False,
186 encoding="UTF-8")
184 encoding="UTF-8")
187 except Exception:
185 except Exception:
188 log.error(traceback.format_exc())
186 log.error(traceback.format_exc())
189 h.flash(_('An error occurred during repository forking %s') %
187 h.flash(_('An error occurred during repository forking %s') %
190 repo_name, category='error')
188 repo_name, category='error')
191
189
192 return redirect(h.url('repo_creating_home',
190 return redirect(h.url('repo_creating_home',
193 repo_name=form_result['repo_name_full'],
191 repo_name=form_result['repo_name_full'],
194 task_id=task_id))
192 task_id=task_id))
@@ -1,380 +1,379 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.journal
15 kallithea.controllers.journal
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Journal controller for pylons
18 Journal controller for pylons
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: Nov 21, 2010
22 :created_on: Nov 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
28
29 import logging
29 import logging
30 import traceback
30 import traceback
31 from itertools import groupby
31 from itertools import groupby
32
32
33 from sqlalchemy import or_
33 from sqlalchemy import or_
34 from sqlalchemy.orm import joinedload
34 from sqlalchemy.orm import joinedload
35 from sqlalchemy.sql.expression import func
35 from sqlalchemy.sql.expression import func
36
36
37 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
37 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
38
38
39 from webob.exc import HTTPBadRequest
39 from webob.exc import HTTPBadRequest
40 from pylons import request, tmpl_context as c, response, url
40 from pylons import request, tmpl_context as c, response, url
41 from pylons.i18n.translation import _
41 from pylons.i18n.translation import _
42
42
43 from kallithea.controllers.admin.admin import _journal_filter
43 from kallithea.controllers.admin.admin import _journal_filter
44 from kallithea.model.db import UserLog, UserFollowing, Repository, User
44 from kallithea.model.db import UserLog, UserFollowing, Repository, User
45 from kallithea.model.meta import Session
45 from kallithea.model.meta import Session
46 from kallithea.model.repo import RepoModel
46 from kallithea.model.repo import RepoModel
47 import kallithea.lib.helpers as h
47 import kallithea.lib.helpers as h
48 from kallithea.lib.helpers import Page
48 from kallithea.lib.helpers import Page
49 from kallithea.lib.auth import LoginRequired, NotAnonymous
49 from kallithea.lib.auth import LoginRequired, NotAnonymous
50 from kallithea.lib.base import BaseController, render
50 from kallithea.lib.base import BaseController, render
51 from kallithea.lib.utils2 import safe_int, AttributeDict
51 from kallithea.lib.utils2 import safe_int, AttributeDict
52 from kallithea.lib.compat import json
52 from kallithea.lib.compat import json
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class JournalController(BaseController):
57 class JournalController(BaseController):
58
58
59 def __before__(self):
59 def __before__(self):
60 super(JournalController, self).__before__()
60 super(JournalController, self).__before__()
61 self.language = 'en-us'
61 self.language = 'en-us'
62 self.ttl = "5"
62 self.ttl = "5"
63 self.feed_nr = 20
63 self.feed_nr = 20
64 c.search_term = request.GET.get('filter')
64 c.search_term = request.GET.get('filter')
65
65
66 def _get_daily_aggregate(self, journal):
66 def _get_daily_aggregate(self, journal):
67 groups = []
67 groups = []
68 for k, g in groupby(journal, lambda x: x.action_as_day):
68 for k, g in groupby(journal, lambda x: x.action_as_day):
69 user_group = []
69 user_group = []
70 #groupby username if it's a present value, else fallback to journal username
70 #groupby username if it's a present value, else fallback to journal username
71 for _unused, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
71 for _unused, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
72 l = list(g2)
72 l = list(g2)
73 user_group.append((l[0].user, l))
73 user_group.append((l[0].user, l))
74
74
75 groups.append((k, user_group,))
75 groups.append((k, user_group,))
76
76
77 return groups
77 return groups
78
78
79 def _get_journal_data(self, following_repos):
79 def _get_journal_data(self, following_repos):
80 repo_ids = [x.follows_repository.repo_id for x in following_repos
80 repo_ids = [x.follows_repository.repo_id for x in following_repos
81 if x.follows_repository is not None]
81 if x.follows_repository is not None]
82 user_ids = [x.follows_user.user_id for x in following_repos
82 user_ids = [x.follows_user.user_id for x in following_repos
83 if x.follows_user is not None]
83 if x.follows_user is not None]
84
84
85 filtering_criterion = None
85 filtering_criterion = None
86
86
87 if repo_ids and user_ids:
87 if repo_ids and user_ids:
88 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
88 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
89 UserLog.user_id.in_(user_ids))
89 UserLog.user_id.in_(user_ids))
90 if repo_ids and not user_ids:
90 if repo_ids and not user_ids:
91 filtering_criterion = UserLog.repository_id.in_(repo_ids)
91 filtering_criterion = UserLog.repository_id.in_(repo_ids)
92 if not repo_ids and user_ids:
92 if not repo_ids and user_ids:
93 filtering_criterion = UserLog.user_id.in_(user_ids)
93 filtering_criterion = UserLog.user_id.in_(user_ids)
94 if filtering_criterion is not None:
94 if filtering_criterion is not None:
95 journal = self.sa.query(UserLog)\
95 journal = self.sa.query(UserLog)\
96 .options(joinedload(UserLog.user))\
96 .options(joinedload(UserLog.user))\
97 .options(joinedload(UserLog.repository))
97 .options(joinedload(UserLog.repository))
98 #filter
98 #filter
99 journal = _journal_filter(journal, c.search_term)
99 journal = _journal_filter(journal, c.search_term)
100 journal = journal.filter(filtering_criterion)\
100 journal = journal.filter(filtering_criterion)\
101 .order_by(UserLog.action_date.desc())
101 .order_by(UserLog.action_date.desc())
102 else:
102 else:
103 journal = []
103 journal = []
104
104
105 return journal
105 return journal
106
106
107 def _atom_feed(self, repos, public=True):
107 def _atom_feed(self, repos, public=True):
108 journal = self._get_journal_data(repos)
108 journal = self._get_journal_data(repos)
109 if public:
109 if public:
110 _link = h.canonical_url('public_journal_atom')
110 _link = h.canonical_url('public_journal_atom')
111 _desc = '%s %s %s' % (c.site_name, _('public journal'),
111 _desc = '%s %s %s' % (c.site_name, _('public journal'),
112 'atom feed')
112 'atom feed')
113 else:
113 else:
114 _link = h.canonical_url('journal_atom')
114 _link = h.canonical_url('journal_atom')
115 _desc = '%s %s %s' % (c.site_name, _('journal'), 'atom feed')
115 _desc = '%s %s %s' % (c.site_name, _('journal'), 'atom feed')
116
116
117 feed = Atom1Feed(title=_desc,
117 feed = Atom1Feed(title=_desc,
118 link=_link,
118 link=_link,
119 description=_desc,
119 description=_desc,
120 language=self.language,
120 language=self.language,
121 ttl=self.ttl)
121 ttl=self.ttl)
122
122
123 for entry in journal[:self.feed_nr]:
123 for entry in journal[:self.feed_nr]:
124 user = entry.user
124 user = entry.user
125 if user is None:
125 if user is None:
126 #fix deleted users
126 #fix deleted users
127 user = AttributeDict({'short_contact': entry.username,
127 user = AttributeDict({'short_contact': entry.username,
128 'email': '',
128 'email': '',
129 'full_contact': ''})
129 'full_contact': ''})
130 action, action_extra, ico = h.action_parser(entry, feed=True)
130 action, action_extra, ico = h.action_parser(entry, feed=True)
131 title = "%s - %s %s" % (user.short_contact, action(),
131 title = "%s - %s %s" % (user.short_contact, action(),
132 entry.repository.repo_name)
132 entry.repository.repo_name)
133 desc = action_extra()
133 desc = action_extra()
134 _url = None
134 _url = None
135 if entry.repository is not None:
135 if entry.repository is not None:
136 _url = h.canonical_url('changelog_home',
136 _url = h.canonical_url('changelog_home',
137 repo_name=entry.repository.repo_name)
137 repo_name=entry.repository.repo_name)
138
138
139 feed.add_item(title=title,
139 feed.add_item(title=title,
140 pubdate=entry.action_date,
140 pubdate=entry.action_date,
141 link=_url or h.canonical_url(''),
141 link=_url or h.canonical_url(''),
142 author_email=user.email,
142 author_email=user.email,
143 author_name=user.full_contact,
143 author_name=user.full_contact,
144 description=desc)
144 description=desc)
145
145
146 response.content_type = feed.mime_type
146 response.content_type = feed.mime_type
147 return feed.writeString('utf-8')
147 return feed.writeString('utf-8')
148
148
149 def _rss_feed(self, repos, public=True):
149 def _rss_feed(self, repos, public=True):
150 journal = self._get_journal_data(repos)
150 journal = self._get_journal_data(repos)
151 if public:
151 if public:
152 _link = h.canonical_url('public_journal_atom')
152 _link = h.canonical_url('public_journal_atom')
153 _desc = '%s %s %s' % (c.site_name, _('public journal'),
153 _desc = '%s %s %s' % (c.site_name, _('public journal'),
154 'rss feed')
154 'rss feed')
155 else:
155 else:
156 _link = h.canonical_url('journal_atom')
156 _link = h.canonical_url('journal_atom')
157 _desc = '%s %s %s' % (c.site_name, _('journal'), 'rss feed')
157 _desc = '%s %s %s' % (c.site_name, _('journal'), 'rss feed')
158
158
159 feed = Rss201rev2Feed(title=_desc,
159 feed = Rss201rev2Feed(title=_desc,
160 link=_link,
160 link=_link,
161 description=_desc,
161 description=_desc,
162 language=self.language,
162 language=self.language,
163 ttl=self.ttl)
163 ttl=self.ttl)
164
164
165 for entry in journal[:self.feed_nr]:
165 for entry in journal[:self.feed_nr]:
166 user = entry.user
166 user = entry.user
167 if user is None:
167 if user is None:
168 #fix deleted users
168 #fix deleted users
169 user = AttributeDict({'short_contact': entry.username,
169 user = AttributeDict({'short_contact': entry.username,
170 'email': '',
170 'email': '',
171 'full_contact': ''})
171 'full_contact': ''})
172 action, action_extra, ico = h.action_parser(entry, feed=True)
172 action, action_extra, ico = h.action_parser(entry, feed=True)
173 title = "%s - %s %s" % (user.short_contact, action(),
173 title = "%s - %s %s" % (user.short_contact, action(),
174 entry.repository.repo_name)
174 entry.repository.repo_name)
175 desc = action_extra()
175 desc = action_extra()
176 _url = None
176 _url = None
177 if entry.repository is not None:
177 if entry.repository is not None:
178 _url = h.canonical_url('changelog_home',
178 _url = h.canonical_url('changelog_home',
179 repo_name=entry.repository.repo_name)
179 repo_name=entry.repository.repo_name)
180
180
181 feed.add_item(title=title,
181 feed.add_item(title=title,
182 pubdate=entry.action_date,
182 pubdate=entry.action_date,
183 link=_url or h.canonical_url(''),
183 link=_url or h.canonical_url(''),
184 author_email=user.email,
184 author_email=user.email,
185 author_name=user.full_contact,
185 author_name=user.full_contact,
186 description=desc)
186 description=desc)
187
187
188 response.content_type = feed.mime_type
188 response.content_type = feed.mime_type
189 return feed.writeString('utf-8')
189 return feed.writeString('utf-8')
190
190
191 @LoginRequired()
191 @LoginRequired()
192 @NotAnonymous()
192 @NotAnonymous()
193 def index(self):
193 def index(self):
194 # Return a rendered template
194 # Return a rendered template
195 p = safe_int(request.GET.get('page', 1), 1)
195 p = safe_int(request.GET.get('page', 1), 1)
196 c.user = User.get(self.authuser.user_id)
196 c.user = User.get(self.authuser.user_id)
197 c.following = self.sa.query(UserFollowing)\
197 c.following = self.sa.query(UserFollowing)\
198 .filter(UserFollowing.user_id == self.authuser.user_id)\
198 .filter(UserFollowing.user_id == self.authuser.user_id)\
199 .options(joinedload(UserFollowing.follows_repository))\
199 .options(joinedload(UserFollowing.follows_repository))\
200 .all()
200 .all()
201
201
202 journal = self._get_journal_data(c.following)
202 journal = self._get_journal_data(c.following)
203
203
204 def url_generator(**kw):
204 def url_generator(**kw):
205 return url.current(filter=c.search_term, **kw)
205 return url.current(filter=c.search_term, **kw)
206
206
207 c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
207 c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
208 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
208 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
209
209
210 c.journal_data = render('journal/journal_data.html')
211 if request.environ.get('HTTP_X_PARTIAL_XHR'):
210 if request.environ.get('HTTP_X_PARTIAL_XHR'):
212 return c.journal_data
211 return render('journal/journal_data.html')
213
212
214 repos_list = Session().query(Repository)\
213 repos_list = Session().query(Repository)\
215 .filter(Repository.user_id ==
214 .filter(Repository.user_id ==
216 self.authuser.user_id)\
215 self.authuser.user_id)\
217 .order_by(func.lower(Repository.repo_name)).all()
216 .order_by(func.lower(Repository.repo_name)).all()
218
217
219 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
218 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
220 admin=True)
219 admin=True)
221 #json used to render the grid
220 #json used to render the grid
222 c.data = json.dumps(repos_data)
221 c.data = json.dumps(repos_data)
223
222
224 watched_repos_data = []
223 watched_repos_data = []
225
224
226 ## watched repos
225 ## watched repos
227 _render = RepoModel._render_datatable
226 _render = RepoModel._render_datatable
228
227
229 def quick_menu(repo_name):
228 def quick_menu(repo_name):
230 return _render('quick_menu', repo_name)
229 return _render('quick_menu', repo_name)
231
230
232 def repo_lnk(name, rtype, rstate, private, fork_of):
231 def repo_lnk(name, rtype, rstate, private, fork_of):
233 return _render('repo_name', name, rtype, rstate, private, fork_of,
232 return _render('repo_name', name, rtype, rstate, private, fork_of,
234 short_name=False, admin=False)
233 short_name=False, admin=False)
235
234
236 def last_rev(repo_name, cs_cache):
235 def last_rev(repo_name, cs_cache):
237 return _render('revision', repo_name, cs_cache.get('revision'),
236 return _render('revision', repo_name, cs_cache.get('revision'),
238 cs_cache.get('raw_id'), cs_cache.get('author'),
237 cs_cache.get('raw_id'), cs_cache.get('author'),
239 cs_cache.get('message'))
238 cs_cache.get('message'))
240
239
241 def desc(desc):
240 def desc(desc):
242 from pylons import tmpl_context as c
241 from pylons import tmpl_context as c
243 if c.visual.stylify_metatags:
242 if c.visual.stylify_metatags:
244 return h.urlify_text(h.desc_stylize(h.truncate(desc, 60)))
243 return h.urlify_text(h.desc_stylize(h.truncate(desc, 60)))
245 else:
244 else:
246 return h.urlify_text(h.truncate(desc, 60))
245 return h.urlify_text(h.truncate(desc, 60))
247
246
248 def repo_actions(repo_name):
247 def repo_actions(repo_name):
249 return _render('repo_actions', repo_name)
248 return _render('repo_actions', repo_name)
250
249
251 def owner_actions(user_id, username):
250 def owner_actions(user_id, username):
252 return _render('user_name', user_id, username)
251 return _render('user_name', user_id, username)
253
252
254 def toogle_follow(repo_id):
253 def toogle_follow(repo_id):
255 return _render('toggle_follow', repo_id)
254 return _render('toggle_follow', repo_id)
256
255
257 for entry in c.following:
256 for entry in c.following:
258 repo = entry.follows_repository
257 repo = entry.follows_repository
259 cs_cache = repo.changeset_cache
258 cs_cache = repo.changeset_cache
260 row = {
259 row = {
261 "menu": quick_menu(repo.repo_name),
260 "menu": quick_menu(repo.repo_name),
262 "raw_name": repo.repo_name.lower(),
261 "raw_name": repo.repo_name.lower(),
263 "name": repo_lnk(repo.repo_name, repo.repo_type,
262 "name": repo_lnk(repo.repo_name, repo.repo_type,
264 repo.repo_state, repo.private, repo.fork),
263 repo.repo_state, repo.private, repo.fork),
265 "last_changeset": last_rev(repo.repo_name, cs_cache),
264 "last_changeset": last_rev(repo.repo_name, cs_cache),
266 "last_rev_raw": cs_cache.get('revision'),
265 "last_rev_raw": cs_cache.get('revision'),
267 "action": toogle_follow(repo.repo_id)
266 "action": toogle_follow(repo.repo_id)
268 }
267 }
269
268
270 watched_repos_data.append(row)
269 watched_repos_data.append(row)
271
270
272 c.watched_data = json.dumps({
271 c.watched_data = json.dumps({
273 "totalRecords": len(c.following),
272 "totalRecords": len(c.following),
274 "startIndex": 0,
273 "startIndex": 0,
275 "sort": "name",
274 "sort": "name",
276 "dir": "asc",
275 "dir": "asc",
277 "records": watched_repos_data
276 "records": watched_repos_data
278 })
277 })
279 return render('journal/journal.html')
278 return render('journal/journal.html')
280
279
281 @LoginRequired(api_access=True)
280 @LoginRequired(api_access=True)
282 @NotAnonymous()
281 @NotAnonymous()
283 def journal_atom(self):
282 def journal_atom(self):
284 """
283 """
285 Produce an atom-1.0 feed via feedgenerator module
284 Produce an atom-1.0 feed via feedgenerator module
286 """
285 """
287 following = self.sa.query(UserFollowing)\
286 following = self.sa.query(UserFollowing)\
288 .filter(UserFollowing.user_id == self.authuser.user_id)\
287 .filter(UserFollowing.user_id == self.authuser.user_id)\
289 .options(joinedload(UserFollowing.follows_repository))\
288 .options(joinedload(UserFollowing.follows_repository))\
290 .all()
289 .all()
291 return self._atom_feed(following, public=False)
290 return self._atom_feed(following, public=False)
292
291
293 @LoginRequired(api_access=True)
292 @LoginRequired(api_access=True)
294 @NotAnonymous()
293 @NotAnonymous()
295 def journal_rss(self):
294 def journal_rss(self):
296 """
295 """
297 Produce an rss feed via feedgenerator module
296 Produce an rss feed via feedgenerator module
298 """
297 """
299 following = self.sa.query(UserFollowing)\
298 following = self.sa.query(UserFollowing)\
300 .filter(UserFollowing.user_id == self.authuser.user_id)\
299 .filter(UserFollowing.user_id == self.authuser.user_id)\
301 .options(joinedload(UserFollowing.follows_repository))\
300 .options(joinedload(UserFollowing.follows_repository))\
302 .all()
301 .all()
303 return self._rss_feed(following, public=False)
302 return self._rss_feed(following, public=False)
304
303
305 @LoginRequired()
304 @LoginRequired()
306 @NotAnonymous()
305 @NotAnonymous()
307 def toggle_following(self):
306 def toggle_following(self):
308 cur_token = request.POST.get('auth_token')
307 cur_token = request.POST.get('auth_token')
309 token = h.get_token()
308 token = h.get_token()
310 if cur_token == token:
309 if cur_token == token:
311
310
312 user_id = request.POST.get('follows_user_id')
311 user_id = request.POST.get('follows_user_id')
313 if user_id:
312 if user_id:
314 try:
313 try:
315 self.scm_model.toggle_following_user(user_id,
314 self.scm_model.toggle_following_user(user_id,
316 self.authuser.user_id)
315 self.authuser.user_id)
317 Session.commit()
316 Session.commit()
318 return 'ok'
317 return 'ok'
319 except Exception:
318 except Exception:
320 log.error(traceback.format_exc())
319 log.error(traceback.format_exc())
321 raise HTTPBadRequest()
320 raise HTTPBadRequest()
322
321
323 repo_id = request.POST.get('follows_repo_id')
322 repo_id = request.POST.get('follows_repo_id')
324 if repo_id:
323 if repo_id:
325 try:
324 try:
326 self.scm_model.toggle_following_repo(repo_id,
325 self.scm_model.toggle_following_repo(repo_id,
327 self.authuser.user_id)
326 self.authuser.user_id)
328 Session.commit()
327 Session.commit()
329 return 'ok'
328 return 'ok'
330 except Exception:
329 except Exception:
331 log.error(traceback.format_exc())
330 log.error(traceback.format_exc())
332 raise HTTPBadRequest()
331 raise HTTPBadRequest()
333
332
334 log.debug('token mismatch %s vs %s' % (cur_token, token))
333 log.debug('token mismatch %s vs %s' % (cur_token, token))
335 raise HTTPBadRequest()
334 raise HTTPBadRequest()
336
335
337 @LoginRequired()
336 @LoginRequired()
338 def public_journal(self):
337 def public_journal(self):
339 # Return a rendered template
338 # Return a rendered template
340 p = safe_int(request.GET.get('page', 1), 1)
339 p = safe_int(request.GET.get('page', 1), 1)
341
340
342 c.following = self.sa.query(UserFollowing)\
341 c.following = self.sa.query(UserFollowing)\
343 .filter(UserFollowing.user_id == self.authuser.user_id)\
342 .filter(UserFollowing.user_id == self.authuser.user_id)\
344 .options(joinedload(UserFollowing.follows_repository))\
343 .options(joinedload(UserFollowing.follows_repository))\
345 .all()
344 .all()
346
345
347 journal = self._get_journal_data(c.following)
346 journal = self._get_journal_data(c.following)
348
347
349 c.journal_pager = Page(journal, page=p, items_per_page=20)
348 c.journal_pager = Page(journal, page=p, items_per_page=20)
350
349
351 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
350 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
352
351
353 c.journal_data = render('journal/journal_data.html')
354 if request.environ.get('HTTP_X_PARTIAL_XHR'):
352 if request.environ.get('HTTP_X_PARTIAL_XHR'):
355 return c.journal_data
353 return render('journal/journal_data.html')
354
356 return render('journal/public_journal.html')
355 return render('journal/public_journal.html')
357
356
358 @LoginRequired(api_access=True)
357 @LoginRequired(api_access=True)
359 def public_journal_atom(self):
358 def public_journal_atom(self):
360 """
359 """
361 Produce an atom-1.0 feed via feedgenerator module
360 Produce an atom-1.0 feed via feedgenerator module
362 """
361 """
363 c.following = self.sa.query(UserFollowing)\
362 c.following = self.sa.query(UserFollowing)\
364 .filter(UserFollowing.user_id == self.authuser.user_id)\
363 .filter(UserFollowing.user_id == self.authuser.user_id)\
365 .options(joinedload(UserFollowing.follows_repository))\
364 .options(joinedload(UserFollowing.follows_repository))\
366 .all()
365 .all()
367
366
368 return self._atom_feed(c.following)
367 return self._atom_feed(c.following)
369
368
370 @LoginRequired(api_access=True)
369 @LoginRequired(api_access=True)
371 def public_journal_rss(self):
370 def public_journal_rss(self):
372 """
371 """
373 Produce an rss2 feed via feedgenerator module
372 Produce an rss2 feed via feedgenerator module
374 """
373 """
375 c.following = self.sa.query(UserFollowing)\
374 c.following = self.sa.query(UserFollowing)\
376 .filter(UserFollowing.user_id == self.authuser.user_id)\
375 .filter(UserFollowing.user_id == self.authuser.user_id)\
377 .options(joinedload(UserFollowing.follows_repository))\
376 .options(joinedload(UserFollowing.follows_repository))\
378 .all()
377 .all()
379
378
380 return self._rss_feed(c.following)
379 return self._rss_feed(c.following)
@@ -1,772 +1,770 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.pullrequests
15 kallithea.controllers.pullrequests
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 pull requests controller for Kallithea for initializing pull requests
18 pull requests controller for Kallithea for initializing pull requests
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: May 7, 2012
22 :created_on: May 7, 2012
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 import formencode
30 import formencode
31 import re
31 import re
32
32
33 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
33 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
34
34
35 from pylons import request, tmpl_context as c, url
35 from pylons import request, tmpl_context as c, url
36 from pylons.controllers.util import redirect
36 from pylons.controllers.util import redirect
37 from pylons.i18n.translation import _
37 from pylons.i18n.translation import _
38
38
39 from kallithea.lib.compat import json
39 from kallithea.lib.compat import json
40 from kallithea.lib.base import BaseRepoController, render
40 from kallithea.lib.base import BaseRepoController, render
41 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
41 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
42 NotAnonymous
42 NotAnonymous
43 from kallithea.lib.helpers import Page
43 from kallithea.lib.helpers import Page
44 from kallithea.lib import helpers as h
44 from kallithea.lib import helpers as h
45 from kallithea.lib import diffs
45 from kallithea.lib import diffs
46 from kallithea.lib.utils import action_logger, jsonify
46 from kallithea.lib.utils import action_logger, jsonify
47 from kallithea.lib.vcs.utils import safe_str
47 from kallithea.lib.vcs.utils import safe_str
48 from kallithea.lib.vcs.exceptions import EmptyRepositoryError
48 from kallithea.lib.vcs.exceptions import EmptyRepositoryError
49 from kallithea.lib.diffs import LimitedDiffContainer
49 from kallithea.lib.diffs import LimitedDiffContainer
50 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment,\
50 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment,\
51 PullRequestReviewers
51 PullRequestReviewers
52 from kallithea.model.pull_request import PullRequestModel
52 from kallithea.model.pull_request import PullRequestModel
53 from kallithea.model.meta import Session
53 from kallithea.model.meta import Session
54 from kallithea.model.repo import RepoModel
54 from kallithea.model.repo import RepoModel
55 from kallithea.model.comment import ChangesetCommentsModel
55 from kallithea.model.comment import ChangesetCommentsModel
56 from kallithea.model.changeset_status import ChangesetStatusModel
56 from kallithea.model.changeset_status import ChangesetStatusModel
57 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
57 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
58 from kallithea.lib.utils2 import safe_int
58 from kallithea.lib.utils2 import safe_int
59 from kallithea.controllers.changeset import _ignorews_url,\
59 from kallithea.controllers.changeset import _ignorews_url,\
60 _context_url, get_line_ctx, get_ignore_ws
60 _context_url, get_line_ctx, get_ignore_ws
61 from kallithea.controllers.compare import CompareController
61 from kallithea.controllers.compare import CompareController
62 from kallithea.lib.graphmod import graph_data
62 from kallithea.lib.graphmod import graph_data
63
63
64 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
65
65
66
66
67 class PullrequestsController(BaseRepoController):
67 class PullrequestsController(BaseRepoController):
68
68
69 def __before__(self):
69 def __before__(self):
70 super(PullrequestsController, self).__before__()
70 super(PullrequestsController, self).__before__()
71 repo_model = RepoModel()
71 repo_model = RepoModel()
72 c.users_array = repo_model.get_users_js()
72 c.users_array = repo_model.get_users_js()
73 c.user_groups_array = repo_model.get_user_groups_js()
73 c.user_groups_array = repo_model.get_user_groups_js()
74
74
75 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
75 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
76 """return a structure with repo's interesting changesets, suitable for
76 """return a structure with repo's interesting changesets, suitable for
77 the selectors in pullrequest.html
77 the selectors in pullrequest.html
78
78
79 rev: a revision that must be in the list somehow and selected by default
79 rev: a revision that must be in the list somehow and selected by default
80 branch: a branch that must be in the list and selected by default - even if closed
80 branch: a branch that must be in the list and selected by default - even if closed
81 branch_rev: a revision of which peers should be preferred and available."""
81 branch_rev: a revision of which peers should be preferred and available."""
82 # list named branches that has been merged to this named branch - it should probably merge back
82 # list named branches that has been merged to this named branch - it should probably merge back
83 peers = []
83 peers = []
84
84
85 if rev:
85 if rev:
86 rev = safe_str(rev)
86 rev = safe_str(rev)
87
87
88 if branch:
88 if branch:
89 branch = safe_str(branch)
89 branch = safe_str(branch)
90
90
91 if branch_rev:
91 if branch_rev:
92 branch_rev = safe_str(branch_rev)
92 branch_rev = safe_str(branch_rev)
93 # a revset not restricting to merge() would be better
93 # a revset not restricting to merge() would be better
94 # (especially because it would get the branch point)
94 # (especially because it would get the branch point)
95 # ... but is currently too expensive
95 # ... but is currently too expensive
96 # including branches of children could be nice too
96 # including branches of children could be nice too
97 peerbranches = set()
97 peerbranches = set()
98 for i in repo._repo.revs(
98 for i in repo._repo.revs(
99 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
99 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
100 branch_rev, branch_rev):
100 branch_rev, branch_rev):
101 abranch = repo.get_changeset(i).branch
101 abranch = repo.get_changeset(i).branch
102 if abranch not in peerbranches:
102 if abranch not in peerbranches:
103 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
103 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
104 peers.append((n, abranch))
104 peers.append((n, abranch))
105 peerbranches.add(abranch)
105 peerbranches.add(abranch)
106
106
107 selected = None
107 selected = None
108 tiprev = repo.tags.get('tip')
108 tiprev = repo.tags.get('tip')
109 tipbranch = None
109 tipbranch = None
110
110
111 branches = []
111 branches = []
112 for abranch, branchrev in repo.branches.iteritems():
112 for abranch, branchrev in repo.branches.iteritems():
113 n = 'branch:%s:%s' % (abranch, branchrev)
113 n = 'branch:%s:%s' % (abranch, branchrev)
114 desc = abranch
114 desc = abranch
115 if branchrev == tiprev:
115 if branchrev == tiprev:
116 tipbranch = abranch
116 tipbranch = abranch
117 desc = '%s (current tip)' % desc
117 desc = '%s (current tip)' % desc
118 branches.append((n, desc))
118 branches.append((n, desc))
119 if rev == branchrev:
119 if rev == branchrev:
120 selected = n
120 selected = n
121 if branch == abranch:
121 if branch == abranch:
122 if not rev:
122 if not rev:
123 selected = n
123 selected = n
124 branch = None
124 branch = None
125 if branch: # branch not in list - it is probably closed
125 if branch: # branch not in list - it is probably closed
126 branchrev = repo.closed_branches.get(branch)
126 branchrev = repo.closed_branches.get(branch)
127 if branchrev:
127 if branchrev:
128 n = 'branch:%s:%s' % (branch, branchrev)
128 n = 'branch:%s:%s' % (branch, branchrev)
129 branches.append((n, _('%s (closed)') % branch))
129 branches.append((n, _('%s (closed)') % branch))
130 selected = n
130 selected = n
131 branch = None
131 branch = None
132 if branch:
132 if branch:
133 log.debug('branch %r not found in %s', branch, repo)
133 log.debug('branch %r not found in %s', branch, repo)
134
134
135 bookmarks = []
135 bookmarks = []
136 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
136 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
137 n = 'book:%s:%s' % (bookmark, bookmarkrev)
137 n = 'book:%s:%s' % (bookmark, bookmarkrev)
138 bookmarks.append((n, bookmark))
138 bookmarks.append((n, bookmark))
139 if rev == bookmarkrev:
139 if rev == bookmarkrev:
140 selected = n
140 selected = n
141
141
142 tags = []
142 tags = []
143 for tag, tagrev in repo.tags.iteritems():
143 for tag, tagrev in repo.tags.iteritems():
144 if tag == 'tip':
144 if tag == 'tip':
145 continue
145 continue
146 n = 'tag:%s:%s' % (tag, tagrev)
146 n = 'tag:%s:%s' % (tag, tagrev)
147 tags.append((n, tag))
147 tags.append((n, tag))
148 if rev == tagrev:
148 if rev == tagrev:
149 selected = n
149 selected = n
150
150
151 # prio 1: rev was selected as existing entry above
151 # prio 1: rev was selected as existing entry above
152
152
153 # prio 2: create special entry for rev; rev _must_ be used
153 # prio 2: create special entry for rev; rev _must_ be used
154 specials = []
154 specials = []
155 if rev and selected is None:
155 if rev and selected is None:
156 selected = 'rev:%s:%s' % (rev, rev)
156 selected = 'rev:%s:%s' % (rev, rev)
157 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
157 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
158
158
159 # prio 3: most recent peer branch
159 # prio 3: most recent peer branch
160 if peers and not selected:
160 if peers and not selected:
161 selected = peers[0][0]
161 selected = peers[0][0]
162
162
163 # prio 4: tip revision
163 # prio 4: tip revision
164 if not selected:
164 if not selected:
165 if h.is_hg(repo):
165 if h.is_hg(repo):
166 if tipbranch:
166 if tipbranch:
167 selected = 'branch:%s:%s' % (tipbranch, tiprev)
167 selected = 'branch:%s:%s' % (tipbranch, tiprev)
168 else:
168 else:
169 selected = 'tag:null:' + repo.EMPTY_CHANGESET
169 selected = 'tag:null:' + repo.EMPTY_CHANGESET
170 tags.append((selected, 'null'))
170 tags.append((selected, 'null'))
171 else:
171 else:
172 if 'master' in repo.branches:
172 if 'master' in repo.branches:
173 selected = 'branch:master:%s' % repo.branches['master']
173 selected = 'branch:master:%s' % repo.branches['master']
174 else:
174 else:
175 k, v = repo.branches.items()[0]
175 k, v = repo.branches.items()[0]
176 selected = 'branch:%s:%s' % (k, v)
176 selected = 'branch:%s:%s' % (k, v)
177
177
178 groups = [(specials, _("Special")),
178 groups = [(specials, _("Special")),
179 (peers, _("Peer branches")),
179 (peers, _("Peer branches")),
180 (bookmarks, _("Bookmarks")),
180 (bookmarks, _("Bookmarks")),
181 (branches, _("Branches")),
181 (branches, _("Branches")),
182 (tags, _("Tags")),
182 (tags, _("Tags")),
183 ]
183 ]
184 return [g for g in groups if g[0]], selected
184 return [g for g in groups if g[0]], selected
185
185
186 def _get_is_allowed_change_status(self, pull_request):
186 def _get_is_allowed_change_status(self, pull_request):
187 if pull_request.is_closed():
187 if pull_request.is_closed():
188 return False
188 return False
189
189
190 owner = self.authuser.user_id == pull_request.user_id
190 owner = self.authuser.user_id == pull_request.user_id
191 reviewer = self.authuser.user_id in [x.user_id for x in
191 reviewer = self.authuser.user_id in [x.user_id for x in
192 pull_request.reviewers]
192 pull_request.reviewers]
193 return self.authuser.admin or owner or reviewer
193 return self.authuser.admin or owner or reviewer
194
194
195 @LoginRequired()
195 @LoginRequired()
196 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
196 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
197 'repository.admin')
197 'repository.admin')
198 def show_all(self, repo_name):
198 def show_all(self, repo_name):
199 c.from_ = request.GET.get('from_') or ''
199 c.from_ = request.GET.get('from_') or ''
200 c.closed = request.GET.get('closed') or ''
200 c.closed = request.GET.get('closed') or ''
201 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
201 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
202 c.repo_name = repo_name
202 c.repo_name = repo_name
203 p = safe_int(request.GET.get('page', 1), 1)
203 p = safe_int(request.GET.get('page', 1), 1)
204
204
205 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
205 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
206
206
207 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
208
209 if request.environ.get('HTTP_X_PARTIAL_XHR'):
207 if request.environ.get('HTTP_X_PARTIAL_XHR'):
210 return c.pullrequest_data
208 return render('/pullrequests/pullrequest_data.html')
211
209
212 return render('/pullrequests/pullrequest_show_all.html')
210 return render('/pullrequests/pullrequest_show_all.html')
213
211
214 @LoginRequired()
212 @LoginRequired()
215 def show_my(self): # my_account_my_pullrequests
213 def show_my(self): # my_account_my_pullrequests
216 c.show_closed = request.GET.get('pr_show_closed')
214 c.show_closed = request.GET.get('pr_show_closed')
217 return render('/pullrequests/pullrequest_show_my.html')
215 return render('/pullrequests/pullrequest_show_my.html')
218
216
219 @NotAnonymous()
217 @NotAnonymous()
220 def show_my_data(self):
218 def show_my_data(self):
221 c.show_closed = request.GET.get('pr_show_closed')
219 c.show_closed = request.GET.get('pr_show_closed')
222
220
223 def _filter(pr):
221 def _filter(pr):
224 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
222 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
225 if not c.show_closed:
223 if not c.show_closed:
226 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
224 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
227 return s
225 return s
228
226
229 c.my_pull_requests = _filter(PullRequest.query()\
227 c.my_pull_requests = _filter(PullRequest.query()\
230 .filter(PullRequest.user_id ==
228 .filter(PullRequest.user_id ==
231 self.authuser.user_id)\
229 self.authuser.user_id)\
232 .all())
230 .all())
233
231
234 c.participate_in_pull_requests = _filter(PullRequest.query()\
232 c.participate_in_pull_requests = _filter(PullRequest.query()\
235 .join(PullRequestReviewers)\
233 .join(PullRequestReviewers)\
236 .filter(PullRequestReviewers.user_id ==
234 .filter(PullRequestReviewers.user_id ==
237 self.authuser.user_id)\
235 self.authuser.user_id)\
238 )
236 )
239
237
240 return render('/pullrequests/pullrequest_show_my_data.html')
238 return render('/pullrequests/pullrequest_show_my_data.html')
241
239
242 @LoginRequired()
240 @LoginRequired()
243 @NotAnonymous()
241 @NotAnonymous()
244 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
242 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
245 'repository.admin')
243 'repository.admin')
246 def index(self):
244 def index(self):
247 org_repo = c.db_repo
245 org_repo = c.db_repo
248 org_scm_instance = org_repo.scm_instance
246 org_scm_instance = org_repo.scm_instance
249 try:
247 try:
250 org_scm_instance.get_changeset()
248 org_scm_instance.get_changeset()
251 except EmptyRepositoryError, e:
249 except EmptyRepositoryError, e:
252 h.flash(h.literal(_('There are no changesets yet')),
250 h.flash(h.literal(_('There are no changesets yet')),
253 category='warning')
251 category='warning')
254 redirect(url('summary_home', repo_name=org_repo.repo_name))
252 redirect(url('summary_home', repo_name=org_repo.repo_name))
255
253
256 org_rev = request.GET.get('rev_end')
254 org_rev = request.GET.get('rev_end')
257 # rev_start is not directly useful - its parent could however be used
255 # rev_start is not directly useful - its parent could however be used
258 # as default for other and thus give a simple compare view
256 # as default for other and thus give a simple compare view
259 #other_rev = request.POST.get('rev_start')
257 #other_rev = request.POST.get('rev_start')
260 branch = request.GET.get('branch')
258 branch = request.GET.get('branch')
261
259
262 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
260 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
263 c.default_cs_repo = org_repo.repo_name
261 c.default_cs_repo = org_repo.repo_name
264 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
262 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
265
263
266 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
264 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
267 if default_cs_ref_type != 'branch':
265 if default_cs_ref_type != 'branch':
268 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
266 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
269
267
270 # add org repo to other so we can open pull request against peer branches on itself
268 # add org repo to other so we can open pull request against peer branches on itself
271 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
269 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
272
270
273 if org_repo.parent:
271 if org_repo.parent:
274 # add parent of this fork also and select it.
272 # add parent of this fork also and select it.
275 # use the same branch on destination as on source, if available.
273 # use the same branch on destination as on source, if available.
276 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
274 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
277 c.a_repo = org_repo.parent
275 c.a_repo = org_repo.parent
278 c.a_refs, c.default_a_ref = self._get_repo_refs(
276 c.a_refs, c.default_a_ref = self._get_repo_refs(
279 org_repo.parent.scm_instance, branch=default_cs_branch)
277 org_repo.parent.scm_instance, branch=default_cs_branch)
280
278
281 else:
279 else:
282 c.a_repo = org_repo
280 c.a_repo = org_repo
283 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance) # without rev and branch
281 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance) # without rev and branch
284
282
285 # gather forks and add to this list ... even though it is rare to
283 # gather forks and add to this list ... even though it is rare to
286 # request forks to pull from their parent
284 # request forks to pull from their parent
287 for fork in org_repo.forks:
285 for fork in org_repo.forks:
288 c.a_repos.append((fork.repo_name, fork.repo_name))
286 c.a_repos.append((fork.repo_name, fork.repo_name))
289
287
290 return render('/pullrequests/pullrequest.html')
288 return render('/pullrequests/pullrequest.html')
291
289
292 @LoginRequired()
290 @LoginRequired()
293 @NotAnonymous()
291 @NotAnonymous()
294 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
292 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
295 'repository.admin')
293 'repository.admin')
296 @jsonify
294 @jsonify
297 def repo_info(self, repo_name):
295 def repo_info(self, repo_name):
298 repo = RepoModel()._get_repo(repo_name)
296 repo = RepoModel()._get_repo(repo_name)
299 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
297 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
300 return {
298 return {
301 'description': repo.description.split('\n', 1)[0],
299 'description': repo.description.split('\n', 1)[0],
302 'selected_ref': selected_ref,
300 'selected_ref': selected_ref,
303 'refs': refs,
301 'refs': refs,
304 'user': dict(user_id=repo.user.user_id,
302 'user': dict(user_id=repo.user.user_id,
305 username=repo.user.username,
303 username=repo.user.username,
306 firstname=repo.user.firstname,
304 firstname=repo.user.firstname,
307 lastname=repo.user.lastname,
305 lastname=repo.user.lastname,
308 gravatar_link=h.gravatar_url(repo.user.email, 28),
306 gravatar_link=h.gravatar_url(repo.user.email, 28),
309 gravatar_size=14),
307 gravatar_size=14),
310 }
308 }
311
309
312 @LoginRequired()
310 @LoginRequired()
313 @NotAnonymous()
311 @NotAnonymous()
314 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
315 'repository.admin')
313 'repository.admin')
316 def create(self, repo_name):
314 def create(self, repo_name):
317 repo = RepoModel()._get_repo(repo_name)
315 repo = RepoModel()._get_repo(repo_name)
318 try:
316 try:
319 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
317 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
320 except formencode.Invalid, errors:
318 except formencode.Invalid, errors:
321 log.error(traceback.format_exc())
319 log.error(traceback.format_exc())
322 log.error(str(errors))
320 log.error(str(errors))
323 msg = _('Error creating pull request: %s') % errors.msg
321 msg = _('Error creating pull request: %s') % errors.msg
324 h.flash(msg, 'error')
322 h.flash(msg, 'error')
325 raise HTTPBadRequest
323 raise HTTPBadRequest
326
324
327 # heads up: org and other might seem backward here ...
325 # heads up: org and other might seem backward here ...
328 org_repo_name = _form['org_repo']
326 org_repo_name = _form['org_repo']
329 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
327 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
330 org_repo = RepoModel()._get_repo(org_repo_name)
328 org_repo = RepoModel()._get_repo(org_repo_name)
331 (org_ref_type,
329 (org_ref_type,
332 org_ref_name,
330 org_ref_name,
333 org_rev) = org_ref.split(':')
331 org_rev) = org_ref.split(':')
334 if org_ref_type == 'rev':
332 if org_ref_type == 'rev':
335 org_ref_type = 'branch'
333 org_ref_type = 'branch'
336 org_ref_name = org_repo.scm_instance.get_changeset(org_rev).branch
334 org_ref_name = org_repo.scm_instance.get_changeset(org_rev).branch
337 org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, org_ref_name)
335 org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, org_ref_name)
338
336
339 other_repo_name = _form['other_repo']
337 other_repo_name = _form['other_repo']
340 other_ref = _form['other_ref'] # will have symbolic name and head revision
338 other_ref = _form['other_ref'] # will have symbolic name and head revision
341 other_repo = RepoModel()._get_repo(other_repo_name)
339 other_repo = RepoModel()._get_repo(other_repo_name)
342 (other_ref_type,
340 (other_ref_type,
343 other_ref_name,
341 other_ref_name,
344 other_rev) = other_ref.split(':')
342 other_rev) = other_ref.split(':')
345
343
346 cs_ranges, _cs_ranges_not, ancestor_rev = \
344 cs_ranges, _cs_ranges_not, ancestor_rev = \
347 CompareController._get_changesets(org_repo.scm_instance.alias,
345 CompareController._get_changesets(org_repo.scm_instance.alias,
348 other_repo.scm_instance, other_rev, # org and other "swapped"
346 other_repo.scm_instance, other_rev, # org and other "swapped"
349 org_repo.scm_instance, org_rev,
347 org_repo.scm_instance, org_rev,
350 )
348 )
351 revisions = [cs.raw_id for cs in cs_ranges]
349 revisions = [cs.raw_id for cs in cs_ranges]
352
350
353 # hack: ancestor_rev is not an other_rev but we want to show the
351 # hack: ancestor_rev is not an other_rev but we want to show the
354 # requested destination and have the exact ancestor
352 # requested destination and have the exact ancestor
355 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
353 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
356
354
357 reviewers = _form['review_members']
355 reviewers = _form['review_members']
358
356
359 title = _form['pullrequest_title']
357 title = _form['pullrequest_title']
360 if not title:
358 if not title:
361 if org_repo_name == other_repo_name:
359 if org_repo_name == other_repo_name:
362 title = '%s to %s' % (h.short_ref(org_ref_type, org_ref_name),
360 title = '%s to %s' % (h.short_ref(org_ref_type, org_ref_name),
363 h.short_ref(other_ref_type, other_ref_name))
361 h.short_ref(other_ref_type, other_ref_name))
364 else:
362 else:
365 title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
363 title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
366 other_repo_name, h.short_ref(other_ref_type, other_ref_name))
364 other_repo_name, h.short_ref(other_ref_type, other_ref_name))
367 description = _form['pullrequest_desc'].strip() or _('No description')
365 description = _form['pullrequest_desc'].strip() or _('No description')
368 try:
366 try:
369 pull_request = PullRequestModel().create(
367 pull_request = PullRequestModel().create(
370 self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
368 self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
371 other_ref, revisions, reviewers, title, description
369 other_ref, revisions, reviewers, title, description
372 )
370 )
373 Session().commit()
371 Session().commit()
374 h.flash(_('Successfully opened new pull request'),
372 h.flash(_('Successfully opened new pull request'),
375 category='success')
373 category='success')
376 except Exception:
374 except Exception:
377 h.flash(_('Error occurred while creating pull request'),
375 h.flash(_('Error occurred while creating pull request'),
378 category='error')
376 category='error')
379 log.error(traceback.format_exc())
377 log.error(traceback.format_exc())
380 return redirect(url('pullrequest_home', repo_name=repo_name))
378 return redirect(url('pullrequest_home', repo_name=repo_name))
381
379
382 return redirect(pull_request.url())
380 return redirect(pull_request.url())
383
381
384 def create_update(self, old_pull_request, updaterev, title, description, reviewers_ids):
382 def create_update(self, old_pull_request, updaterev, title, description, reviewers_ids):
385 org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
383 org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
386 org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
384 org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
387 new_org_rev = self._get_ref_rev(org_repo, 'rev', updaterev)
385 new_org_rev = self._get_ref_rev(org_repo, 'rev', updaterev)
388
386
389 other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
387 other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
390 other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
388 other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
391 #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
389 #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
392 new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
390 new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
393
391
394 cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
392 cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
395 other_repo.scm_instance, new_other_rev, # org and other "swapped"
393 other_repo.scm_instance, new_other_rev, # org and other "swapped"
396 org_repo.scm_instance, new_org_rev)
394 org_repo.scm_instance, new_org_rev)
397
395
398 old_revisions = set(old_pull_request.revisions)
396 old_revisions = set(old_pull_request.revisions)
399 revisions = [cs.raw_id for cs in cs_ranges]
397 revisions = [cs.raw_id for cs in cs_ranges]
400 new_revisions = [r for r in revisions if r not in old_revisions]
398 new_revisions = [r for r in revisions if r not in old_revisions]
401 lost = old_revisions.difference(revisions)
399 lost = old_revisions.difference(revisions)
402
400
403 infos = ['This is an update of %s "%s".' %
401 infos = ['This is an update of %s "%s".' %
404 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
402 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
405 pull_request_id=old_pull_request.pull_request_id),
403 pull_request_id=old_pull_request.pull_request_id),
406 old_pull_request.title)]
404 old_pull_request.title)]
407
405
408 if lost:
406 if lost:
409 infos.append(_('Missing changesets since the previous pull request:'))
407 infos.append(_('Missing changesets since the previous pull request:'))
410 for r in old_pull_request.revisions:
408 for r in old_pull_request.revisions:
411 if r in lost:
409 if r in lost:
412 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
410 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
413 infos.append(' %s "%s"' % (h.short_id(r), rev_desc))
411 infos.append(' %s "%s"' % (h.short_id(r), rev_desc))
414
412
415 if new_revisions:
413 if new_revisions:
416 infos.append(_('New changesets on %s %s since the previous pull request:') % (org_ref_type, org_ref_name))
414 infos.append(_('New changesets on %s %s since the previous pull request:') % (org_ref_type, org_ref_name))
417 for r in reversed(revisions):
415 for r in reversed(revisions):
418 if r in new_revisions:
416 if r in new_revisions:
419 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
417 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
420 infos.append(' %s %s' % (h.short_id(r), h.shorter(rev_desc, 80)))
418 infos.append(' %s %s' % (h.short_id(r), h.shorter(rev_desc, 80)))
421
419
422 if ancestor_rev == other_rev:
420 if ancestor_rev == other_rev:
423 infos.append(_("Ancestor didn't change - show diff since previous version:"))
421 infos.append(_("Ancestor didn't change - show diff since previous version:"))
424 infos.append(h.canonical_url('compare_url',
422 infos.append(h.canonical_url('compare_url',
425 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
423 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
426 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
424 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
427 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
425 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
428 )) # note: linear diff, merge or not doesn't matter
426 )) # note: linear diff, merge or not doesn't matter
429 else:
427 else:
430 infos.append(_('This pull request is based on another %s revision and there is no simple diff.') % other_ref_name)
428 infos.append(_('This pull request is based on another %s revision and there is no simple diff.') % other_ref_name)
431 else:
429 else:
432 infos.append(_('No changes found on %s %s since previous version.') % (org_ref_type, org_ref_name))
430 infos.append(_('No changes found on %s %s since previous version.') % (org_ref_type, org_ref_name))
433 # TODO: fail?
431 # TODO: fail?
434
432
435 # hack: ancestor_rev is not an other_ref but we want to show the
433 # hack: ancestor_rev is not an other_ref but we want to show the
436 # requested destination and have the exact ancestor
434 # requested destination and have the exact ancestor
437 new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
435 new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
438 new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
436 new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
439
437
440 try:
438 try:
441 title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', title).groups()
439 title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', title).groups()
442 v = int(old_v) + 1
440 v = int(old_v) + 1
443 except (AttributeError, ValueError):
441 except (AttributeError, ValueError):
444 v = 2
442 v = 2
445 title = '%s (v%s)' % (title.strip(), v)
443 title = '%s (v%s)' % (title.strip(), v)
446
444
447 # using a mail-like separator, insert new update info at the top of the list
445 # using a mail-like separator, insert new update info at the top of the list
448 descriptions = description.replace('\r\n', '\n').split('\n-- \n', 1)
446 descriptions = description.replace('\r\n', '\n').split('\n-- \n', 1)
449 description = descriptions[0].strip() + '\n\n-- \n' + '\n'.join(infos)
447 description = descriptions[0].strip() + '\n\n-- \n' + '\n'.join(infos)
450 if len(descriptions) > 1:
448 if len(descriptions) > 1:
451 description += '\n\n' + descriptions[1].strip()
449 description += '\n\n' + descriptions[1].strip()
452
450
453 try:
451 try:
454 pull_request = PullRequestModel().create(
452 pull_request = PullRequestModel().create(
455 self.authuser.user_id,
453 self.authuser.user_id,
456 old_pull_request.org_repo.repo_name, new_org_ref,
454 old_pull_request.org_repo.repo_name, new_org_ref,
457 old_pull_request.other_repo.repo_name, new_other_ref,
455 old_pull_request.other_repo.repo_name, new_other_ref,
458 revisions, reviewers_ids, title, description
456 revisions, reviewers_ids, title, description
459 )
457 )
460 except Exception:
458 except Exception:
461 h.flash(_('Error occurred while creating pull request'),
459 h.flash(_('Error occurred while creating pull request'),
462 category='error')
460 category='error')
463 log.error(traceback.format_exc())
461 log.error(traceback.format_exc())
464 return redirect(old_pull_request.url())
462 return redirect(old_pull_request.url())
465
463
466 ChangesetCommentsModel().create(
464 ChangesetCommentsModel().create(
467 text=_('Closed, replaced by %s .') % pull_request.url(canonical=True),
465 text=_('Closed, replaced by %s .') % pull_request.url(canonical=True),
468 repo=old_pull_request.other_repo.repo_id,
466 repo=old_pull_request.other_repo.repo_id,
469 user=c.authuser.user_id,
467 user=c.authuser.user_id,
470 pull_request=old_pull_request.pull_request_id,
468 pull_request=old_pull_request.pull_request_id,
471 closing_pr=True)
469 closing_pr=True)
472 PullRequestModel().close_pull_request(old_pull_request.pull_request_id)
470 PullRequestModel().close_pull_request(old_pull_request.pull_request_id)
473
471
474 Session().commit()
472 Session().commit()
475 h.flash(_('Pull request update created'),
473 h.flash(_('Pull request update created'),
476 category='success')
474 category='success')
477
475
478 return redirect(pull_request.url())
476 return redirect(pull_request.url())
479
477
480 # pullrequest_post for PR editing
478 # pullrequest_post for PR editing
481 @LoginRequired()
479 @LoginRequired()
482 @NotAnonymous()
480 @NotAnonymous()
483 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
481 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
484 'repository.admin')
482 'repository.admin')
485 def post(self, repo_name, pull_request_id):
483 def post(self, repo_name, pull_request_id):
486 pull_request = PullRequest.get_or_404(pull_request_id)
484 pull_request = PullRequest.get_or_404(pull_request_id)
487 if pull_request.is_closed():
485 if pull_request.is_closed():
488 raise HTTPForbidden()
486 raise HTTPForbidden()
489 assert pull_request.other_repo.repo_name == repo_name
487 assert pull_request.other_repo.repo_name == repo_name
490 #only owner or admin can update it
488 #only owner or admin can update it
491 owner = pull_request.author.user_id == c.authuser.user_id
489 owner = pull_request.author.user_id == c.authuser.user_id
492 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
490 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
493 if not (h.HasPermissionAny('hg.admin') or repo_admin or owner):
491 if not (h.HasPermissionAny('hg.admin') or repo_admin or owner):
494 raise HTTPForbidden()
492 raise HTTPForbidden()
495
493
496 _form = PullRequestPostForm()().to_python(request.POST)
494 _form = PullRequestPostForm()().to_python(request.POST)
497 reviewers_ids = [int(s) for s in _form['review_members']]
495 reviewers_ids = [int(s) for s in _form['review_members']]
498
496
499 if _form['updaterev']:
497 if _form['updaterev']:
500 return self.create_update(pull_request,
498 return self.create_update(pull_request,
501 _form['updaterev'],
499 _form['updaterev'],
502 _form['pullrequest_title'],
500 _form['pullrequest_title'],
503 _form['pullrequest_desc'],
501 _form['pullrequest_desc'],
504 reviewers_ids)
502 reviewers_ids)
505
503
506 old_description = pull_request.description
504 old_description = pull_request.description
507 pull_request.title = _form['pullrequest_title']
505 pull_request.title = _form['pullrequest_title']
508 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
506 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
509 PullRequestModel().mention_from_description(pull_request, old_description)
507 PullRequestModel().mention_from_description(pull_request, old_description)
510
508
511 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
509 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
512
510
513 Session().commit()
511 Session().commit()
514 h.flash(_('Pull request updated'), category='success')
512 h.flash(_('Pull request updated'), category='success')
515
513
516 return redirect(pull_request.url())
514 return redirect(pull_request.url())
517
515
518 @LoginRequired()
516 @LoginRequired()
519 @NotAnonymous()
517 @NotAnonymous()
520 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
518 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
521 'repository.admin')
519 'repository.admin')
522 @jsonify
520 @jsonify
523 def delete(self, repo_name, pull_request_id):
521 def delete(self, repo_name, pull_request_id):
524 pull_request = PullRequest.get_or_404(pull_request_id)
522 pull_request = PullRequest.get_or_404(pull_request_id)
525 #only owner can delete it !
523 #only owner can delete it !
526 if pull_request.author.user_id == c.authuser.user_id:
524 if pull_request.author.user_id == c.authuser.user_id:
527 PullRequestModel().delete(pull_request)
525 PullRequestModel().delete(pull_request)
528 Session().commit()
526 Session().commit()
529 h.flash(_('Successfully deleted pull request'),
527 h.flash(_('Successfully deleted pull request'),
530 category='success')
528 category='success')
531 return redirect(url('my_pullrequests'))
529 return redirect(url('my_pullrequests'))
532 raise HTTPForbidden()
530 raise HTTPForbidden()
533
531
534 @LoginRequired()
532 @LoginRequired()
535 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
533 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
536 'repository.admin')
534 'repository.admin')
537 def show(self, repo_name, pull_request_id, extra=None):
535 def show(self, repo_name, pull_request_id, extra=None):
538 repo_model = RepoModel()
536 repo_model = RepoModel()
539 c.users_array = repo_model.get_users_js()
537 c.users_array = repo_model.get_users_js()
540 c.user_groups_array = repo_model.get_user_groups_js()
538 c.user_groups_array = repo_model.get_user_groups_js()
541 c.pull_request = PullRequest.get_or_404(pull_request_id)
539 c.pull_request = PullRequest.get_or_404(pull_request_id)
542 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
540 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
543 cc_model = ChangesetCommentsModel()
541 cc_model = ChangesetCommentsModel()
544 cs_model = ChangesetStatusModel()
542 cs_model = ChangesetStatusModel()
545
543
546 # pull_requests repo_name we opened it against
544 # pull_requests repo_name we opened it against
547 # ie. other_repo must match
545 # ie. other_repo must match
548 if repo_name != c.pull_request.other_repo.repo_name:
546 if repo_name != c.pull_request.other_repo.repo_name:
549 raise HTTPNotFound
547 raise HTTPNotFound
550
548
551 # load compare data into template context
549 # load compare data into template context
552 c.cs_repo = c.pull_request.org_repo
550 c.cs_repo = c.pull_request.org_repo
553 (c.cs_ref_type,
551 (c.cs_ref_type,
554 c.cs_ref_name,
552 c.cs_ref_name,
555 c.cs_rev) = c.pull_request.org_ref.split(':')
553 c.cs_rev) = c.pull_request.org_ref.split(':')
556
554
557 c.a_repo = c.pull_request.other_repo
555 c.a_repo = c.pull_request.other_repo
558 (c.a_ref_type,
556 (c.a_ref_type,
559 c.a_ref_name,
557 c.a_ref_name,
560 c.a_rev) = c.pull_request.other_ref.split(':') # other_rev is ancestor
558 c.a_rev) = c.pull_request.other_ref.split(':') # other_rev is ancestor
561
559
562 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
560 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
563 c.cs_repo = c.cs_repo
561 c.cs_repo = c.cs_repo
564 c.cs_ranges = [org_scm_instance.get_changeset(x) for x in c.pull_request.revisions]
562 c.cs_ranges = [org_scm_instance.get_changeset(x) for x in c.pull_request.revisions]
565 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
563 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
566 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
564 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
567 c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
565 c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
568
566
569 avail_revs = set()
567 avail_revs = set()
570 avail_show = []
568 avail_show = []
571 c.cs_branch_name = c.cs_ref_name
569 c.cs_branch_name = c.cs_ref_name
572 other_scm_instance = c.a_repo.scm_instance
570 other_scm_instance = c.a_repo.scm_instance
573 c.update_msg = ""
571 c.update_msg = ""
574 c.update_msg_other = ""
572 c.update_msg_other = ""
575 if org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
573 if org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
576 if c.cs_ref_type != 'branch':
574 if c.cs_ref_type != 'branch':
577 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
575 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
578 c.a_branch_name = c.a_ref_name
576 c.a_branch_name = c.a_ref_name
579 if c.a_ref_type != 'branch':
577 if c.a_ref_type != 'branch':
580 try:
578 try:
581 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
579 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
582 except EmptyRepositoryError:
580 except EmptyRepositoryError:
583 c.a_branch_name = 'null' # not a branch name ... but close enough
581 c.a_branch_name = 'null' # not a branch name ... but close enough
584 # candidates: descendants of old head that are on the right branch
582 # candidates: descendants of old head that are on the right branch
585 # and not are the old head itself ...
583 # and not are the old head itself ...
586 # and nothing at all if old head is a descendent of target ref name
584 # and nothing at all if old head is a descendent of target ref name
587 if other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
585 if other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
588 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
586 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
589 elif c.pull_request.is_closed():
587 elif c.pull_request.is_closed():
590 c.update_msg = _('This pull request has been closed and can not be updated.')
588 c.update_msg = _('This pull request has been closed and can not be updated.')
591 else: # look for descendants of PR head on source branch in org repo
589 else: # look for descendants of PR head on source branch in org repo
592 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
590 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
593 revs[0], c.cs_branch_name)
591 revs[0], c.cs_branch_name)
594 if len(avail_revs) > 1: # more than just revs[0]
592 if len(avail_revs) > 1: # more than just revs[0]
595 # also show changesets that not are descendants but would be merged in
593 # also show changesets that not are descendants but would be merged in
596 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
594 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
597 show = set(org_scm_instance._repo.revs('::%ld & !::%s & !::%s',
595 show = set(org_scm_instance._repo.revs('::%ld & !::%s & !::%s',
598 avail_revs, revs[0], targethead))
596 avail_revs, revs[0], targethead))
599 c.update_msg = _('This pull request can be updated with changes on %s:') % c.cs_branch_name
597 c.update_msg = _('This pull request can be updated with changes on %s:') % c.cs_branch_name
600 else:
598 else:
601 show = set()
599 show = set()
602 c.update_msg = _('No changesets found for updating this pull request.')
600 c.update_msg = _('No changesets found for updating this pull request.')
603
601
604 # TODO: handle branch heads that not are tip-most
602 # TODO: handle branch heads that not are tip-most
605 brevs = org_scm_instance._repo.revs('%s - %ld', c.cs_branch_name, avail_revs)
603 brevs = org_scm_instance._repo.revs('%s - %ld', c.cs_branch_name, avail_revs)
606 if brevs:
604 if brevs:
607 # also show changesets that are on branch but neither ancestors nor descendants
605 # also show changesets that are on branch but neither ancestors nor descendants
608 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
606 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
609 show.add(revs[0]) # make sure graph shows this so we can see how they relate
607 show.add(revs[0]) # make sure graph shows this so we can see how they relate
610 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
608 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
611 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
609 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
612
610
613 avail_show = sorted(show, reverse=True)
611 avail_show = sorted(show, reverse=True)
614
612
615 elif org_scm_instance.alias == 'git':
613 elif org_scm_instance.alias == 'git':
616 c.update_msg = _("Git pull requests don't support updates yet.")
614 c.update_msg = _("Git pull requests don't support updates yet.")
617
615
618 c.avail_revs = avail_revs
616 c.avail_revs = avail_revs
619 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
617 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
620 c.avail_jsdata = json.dumps(graph_data(org_scm_instance, avail_show))
618 c.avail_jsdata = json.dumps(graph_data(org_scm_instance, avail_show))
621
619
622 raw_ids = [x.raw_id for x in c.cs_ranges]
620 raw_ids = [x.raw_id for x in c.cs_ranges]
623 c.cs_comments = c.cs_repo.get_comments(raw_ids)
621 c.cs_comments = c.cs_repo.get_comments(raw_ids)
624 c.statuses = c.cs_repo.statuses(raw_ids)
622 c.statuses = c.cs_repo.statuses(raw_ids)
625
623
626 ignore_whitespace = request.GET.get('ignorews') == '1'
624 ignore_whitespace = request.GET.get('ignorews') == '1'
627 line_context = request.GET.get('context', 3)
625 line_context = request.GET.get('context', 3)
628 c.ignorews_url = _ignorews_url
626 c.ignorews_url = _ignorews_url
629 c.context_url = _context_url
627 c.context_url = _context_url
630 c.fulldiff = request.GET.get('fulldiff')
628 c.fulldiff = request.GET.get('fulldiff')
631 diff_limit = self.cut_off_limit if not c.fulldiff else None
629 diff_limit = self.cut_off_limit if not c.fulldiff else None
632
630
633 # we swap org/other ref since we run a simple diff on one repo
631 # we swap org/other ref since we run a simple diff on one repo
634 log.debug('running diff between %s and %s in %s'
632 log.debug('running diff between %s and %s in %s'
635 % (c.a_rev, c.cs_rev, org_scm_instance.path))
633 % (c.a_rev, c.cs_rev, org_scm_instance.path))
636 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
634 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
637 ignore_whitespace=ignore_whitespace,
635 ignore_whitespace=ignore_whitespace,
638 context=line_context)
636 context=line_context)
639
637
640 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
638 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
641 diff_limit=diff_limit)
639 diff_limit=diff_limit)
642 _parsed = diff_processor.prepare()
640 _parsed = diff_processor.prepare()
643
641
644 c.limited_diff = False
642 c.limited_diff = False
645 if isinstance(_parsed, LimitedDiffContainer):
643 if isinstance(_parsed, LimitedDiffContainer):
646 c.limited_diff = True
644 c.limited_diff = True
647
645
648 c.files = []
646 c.files = []
649 c.changes = {}
647 c.changes = {}
650 c.lines_added = 0
648 c.lines_added = 0
651 c.lines_deleted = 0
649 c.lines_deleted = 0
652
650
653 for f in _parsed:
651 for f in _parsed:
654 st = f['stats']
652 st = f['stats']
655 c.lines_added += st['added']
653 c.lines_added += st['added']
656 c.lines_deleted += st['deleted']
654 c.lines_deleted += st['deleted']
657 fid = h.FID('', f['filename'])
655 fid = h.FID('', f['filename'])
658 c.files.append([fid, f['operation'], f['filename'], f['stats']])
656 c.files.append([fid, f['operation'], f['filename'], f['stats']])
659 htmldiff = diff_processor.as_html(enable_comments=True,
657 htmldiff = diff_processor.as_html(enable_comments=True,
660 parsed_lines=[f])
658 parsed_lines=[f])
661 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
659 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
662
660
663 # inline comments
661 # inline comments
664 c.inline_cnt = 0
662 c.inline_cnt = 0
665 c.inline_comments = cc_model.get_inline_comments(
663 c.inline_comments = cc_model.get_inline_comments(
666 c.db_repo.repo_id,
664 c.db_repo.repo_id,
667 pull_request=pull_request_id)
665 pull_request=pull_request_id)
668 # count inline comments
666 # count inline comments
669 for __, lines in c.inline_comments:
667 for __, lines in c.inline_comments:
670 for comments in lines.values():
668 for comments in lines.values():
671 c.inline_cnt += len(comments)
669 c.inline_cnt += len(comments)
672 # comments
670 # comments
673 c.comments = cc_model.get_comments(c.db_repo.repo_id,
671 c.comments = cc_model.get_comments(c.db_repo.repo_id,
674 pull_request=pull_request_id)
672 pull_request=pull_request_id)
675
673
676 # (badly named) pull-request status calculation based on reviewer votes
674 # (badly named) pull-request status calculation based on reviewer votes
677 (c.pull_request_reviewers,
675 (c.pull_request_reviewers,
678 c.pull_request_pending_reviewers,
676 c.pull_request_pending_reviewers,
679 c.current_voting_result,
677 c.current_voting_result,
680 ) = cs_model.calculate_pull_request_result(c.pull_request)
678 ) = cs_model.calculate_pull_request_result(c.pull_request)
681 c.changeset_statuses = ChangesetStatus.STATUSES
679 c.changeset_statuses = ChangesetStatus.STATUSES
682
680
683 c.as_form = False
681 c.as_form = False
684 c.ancestor = None # there is one - but right here we don't know which
682 c.ancestor = None # there is one - but right here we don't know which
685 return render('/pullrequests/pullrequest_show.html')
683 return render('/pullrequests/pullrequest_show.html')
686
684
687 @LoginRequired()
685 @LoginRequired()
688 @NotAnonymous()
686 @NotAnonymous()
689 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
687 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
690 'repository.admin')
688 'repository.admin')
691 @jsonify
689 @jsonify
692 def comment(self, repo_name, pull_request_id):
690 def comment(self, repo_name, pull_request_id):
693 pull_request = PullRequest.get_or_404(pull_request_id)
691 pull_request = PullRequest.get_or_404(pull_request_id)
694
692
695 status = 0
693 status = 0
696 close_pr = False
694 close_pr = False
697 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
695 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
698 if allowed_to_change_status:
696 if allowed_to_change_status:
699 status = request.POST.get('changeset_status')
697 status = request.POST.get('changeset_status')
700 close_pr = request.POST.get('save_close')
698 close_pr = request.POST.get('save_close')
701 text = request.POST.get('text', '').strip() or _('No comments.')
699 text = request.POST.get('text', '').strip() or _('No comments.')
702 if close_pr:
700 if close_pr:
703 text = _('Closing.') + '\n' + text
701 text = _('Closing.') + '\n' + text
704
702
705 comm = ChangesetCommentsModel().create(
703 comm = ChangesetCommentsModel().create(
706 text=text,
704 text=text,
707 repo=c.db_repo.repo_id,
705 repo=c.db_repo.repo_id,
708 user=c.authuser.user_id,
706 user=c.authuser.user_id,
709 pull_request=pull_request_id,
707 pull_request=pull_request_id,
710 f_path=request.POST.get('f_path'),
708 f_path=request.POST.get('f_path'),
711 line_no=request.POST.get('line'),
709 line_no=request.POST.get('line'),
712 status_change=(ChangesetStatus.get_status_lbl(status)
710 status_change=(ChangesetStatus.get_status_lbl(status)
713 if status and allowed_to_change_status else None),
711 if status and allowed_to_change_status else None),
714 closing_pr=close_pr
712 closing_pr=close_pr
715 )
713 )
716
714
717 action_logger(self.authuser,
715 action_logger(self.authuser,
718 'user_commented_pull_request:%s' % pull_request_id,
716 'user_commented_pull_request:%s' % pull_request_id,
719 c.db_repo, self.ip_addr, self.sa)
717 c.db_repo, self.ip_addr, self.sa)
720
718
721 if allowed_to_change_status:
719 if allowed_to_change_status:
722 # get status if set !
720 # get status if set !
723 if status:
721 if status:
724 ChangesetStatusModel().set_status(
722 ChangesetStatusModel().set_status(
725 c.db_repo.repo_id,
723 c.db_repo.repo_id,
726 status,
724 status,
727 c.authuser.user_id,
725 c.authuser.user_id,
728 comm,
726 comm,
729 pull_request=pull_request_id
727 pull_request=pull_request_id
730 )
728 )
731
729
732 if close_pr:
730 if close_pr:
733 PullRequestModel().close_pull_request(pull_request_id)
731 PullRequestModel().close_pull_request(pull_request_id)
734 action_logger(self.authuser,
732 action_logger(self.authuser,
735 'user_closed_pull_request:%s' % pull_request_id,
733 'user_closed_pull_request:%s' % pull_request_id,
736 c.db_repo, self.ip_addr, self.sa)
734 c.db_repo, self.ip_addr, self.sa)
737
735
738 Session().commit()
736 Session().commit()
739
737
740 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
738 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
741 return redirect(pull_request.url())
739 return redirect(pull_request.url())
742
740
743 data = {
741 data = {
744 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
742 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
745 }
743 }
746 if comm:
744 if comm:
747 c.co = comm
745 c.co = comm
748 data.update(comm.get_dict())
746 data.update(comm.get_dict())
749 data.update({'rendered_text':
747 data.update({'rendered_text':
750 render('changeset/changeset_comment_block.html')})
748 render('changeset/changeset_comment_block.html')})
751
749
752 return data
750 return data
753
751
754 @LoginRequired()
752 @LoginRequired()
755 @NotAnonymous()
753 @NotAnonymous()
756 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
754 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
757 'repository.admin')
755 'repository.admin')
758 @jsonify
756 @jsonify
759 def delete_comment(self, repo_name, comment_id):
757 def delete_comment(self, repo_name, comment_id):
760 co = ChangesetComment.get(comment_id)
758 co = ChangesetComment.get(comment_id)
761 if co.pull_request.is_closed():
759 if co.pull_request.is_closed():
762 #don't allow deleting comments on closed pull request
760 #don't allow deleting comments on closed pull request
763 raise HTTPForbidden()
761 raise HTTPForbidden()
764
762
765 owner = co.author.user_id == c.authuser.user_id
763 owner = co.author.user_id == c.authuser.user_id
766 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
764 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
767 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
765 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
768 ChangesetCommentsModel().delete(comment=co)
766 ChangesetCommentsModel().delete(comment=co)
769 Session().commit()
767 Session().commit()
770 return True
768 return True
771 else:
769 else:
772 raise HTTPForbidden()
770 raise HTTPForbidden()
@@ -1,57 +1,57 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3
3
4 <%block name="title">
4 <%block name="title">
5 ${_('Admin Journal')}
5 ${_('Admin Journal')}
6 </%block>
6 </%block>
7
7
8 <%def name="breadcrumbs_links()">
8 <%def name="breadcrumbs_links()">
9 <form id="filter_form">
9 <form id="filter_form">
10 <input class="q_filter_box ${'' if c.search_term else 'initial'}" id="j_filter" size="15" type="text" name="filter" value="${c.search_term or _('journal filter...')}"/>
10 <input class="q_filter_box ${'' if c.search_term else 'initial'}" id="j_filter" size="15" type="text" name="filter" value="${c.search_term or _('journal filter...')}"/>
11 <span class="tooltip" title="${h.tooltip(h.journal_filter_help())}">?</span>
11 <span class="tooltip" title="${h.tooltip(h.journal_filter_help())}">?</span>
12 <input type='submit' value="${_('Filter')}" class="btn btn-mini" style="padding:0px 2px 0px 2px;margin:0px"/>
12 <input type='submit' value="${_('Filter')}" class="btn btn-mini" style="padding:0px 2px 0px 2px;margin:0px"/>
13 ${_('Admin Journal')} - ${ungettext('%s Entry', '%s Entries', c.users_log.item_count) % (c.users_log.item_count)}
13 ${_('Admin Journal')} - ${ungettext('%s Entry', '%s Entries', c.users_log.item_count) % (c.users_log.item_count)}
14 </form>
14 </form>
15 ${h.end_form()}
15 ${h.end_form()}
16 </%def>
16 </%def>
17
17
18 <%block name="header_menu">
18 <%block name="header_menu">
19 ${self.menu('admin')}
19 ${self.menu('admin')}
20 </%block>
20 </%block>
21 <%def name="main()">
21 <%def name="main()">
22 <div class="box">
22 <div class="box">
23 <!-- box / title -->
23 <!-- box / title -->
24 <div class="title">
24 <div class="title">
25 ${self.breadcrumbs()}
25 ${self.breadcrumbs()}
26 </div>
26 </div>
27 <!-- end box / title -->
27 <!-- end box / title -->
28 <div class="table">
28 <div class="table">
29 <div id="user_log">
29 <div id="user_log">
30 ${c.log_data}
30 <%include file='admin_log.html'/>
31 </div>
31 </div>
32 </div>
32 </div>
33 </div>
33 </div>
34
34
35 <script>
35 <script>
36 $(document).ready(function() {
36 $(document).ready(function() {
37 $('#j_filter').click(function(){
37 $('#j_filter').click(function(){
38 var $jfilter = $('#j_filter');
38 var $jfilter = $('#j_filter');
39 if($jfilter.hasClass('initial')){
39 if($jfilter.hasClass('initial')){
40 $jfilter.val('');
40 $jfilter.val('');
41 }
41 }
42 });
42 });
43 var fix_j_filter_width = function(len){
43 var fix_j_filter_width = function(len){
44 $('#j_filter').css('width', Math.max(80, len*6.50)+'px');
44 $('#j_filter').css('width', Math.max(80, len*6.50)+'px');
45 };
45 };
46 $('#j_filter').keyup(function () {
46 $('#j_filter').keyup(function () {
47 fix_j_filter_width($('#j_filter').val().length);
47 fix_j_filter_width($('#j_filter').val().length);
48 });
48 });
49 $('#filter_form').submit(function (e) {
49 $('#filter_form').submit(function (e) {
50 e.preventDefault();
50 e.preventDefault();
51 var val = $('#j_filter').val();
51 var val = $('#j_filter').val();
52 window.location = "${url.current(filter='__FILTER__')}".replace('__FILTER__',val);
52 window.location = "${url.current(filter='__FILTER__')}".replace('__FILTER__',val);
53 });
53 });
54 fix_j_filter_width($('#j_filter').val().length);
54 fix_j_filter_width($('#j_filter').val().length);
55 });
55 });
56 </script>
56 </script>
57 </%def>
57 </%def>
@@ -1,29 +1,29 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3
3
4 <%block name="title">
4 <%block name="title">
5 ${_('%s Followers') % c.repo_name}
5 ${_('%s Followers') % c.repo_name}
6 </%block>
6 </%block>
7
7
8 <%def name="breadcrumbs_links()">
8 <%def name="breadcrumbs_links()">
9 ${_('Followers')}
9 ${_('Followers')}
10 </%def>
10 </%def>
11
11
12 <%block name="header_menu">
12 <%block name="header_menu">
13 ${self.menu('repositories')}
13 ${self.menu('repositories')}
14 </%block>
14 </%block>
15 <%def name="main()">
15 <%def name="main()">
16 ${self.repo_context_bar('followers')}
16 ${self.repo_context_bar('followers')}
17 <div class="box">
17 <div class="box">
18 <!-- box / title -->
18 <!-- box / title -->
19 <div class="title">
19 <div class="title">
20 ${self.breadcrumbs()}
20 ${self.breadcrumbs()}
21 </div>
21 </div>
22 <!-- end box / title -->
22 <!-- end box / title -->
23 <div class="table">
23 <div class="table">
24 <div id="followers">
24 <div id="followers">
25 ${c.followers_data}
25 <%include file='followers_data.html'/>
26 </div>
26 </div>
27 </div>
27 </div>
28 </div>
28 </div>
29 </%def>
29 </%def>
@@ -1,30 +1,30 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3
3
4 <%block name="title">
4 <%block name="title">
5 ${_('%s Forks') % c.repo_name}
5 ${_('%s Forks') % c.repo_name}
6 </%block>
6 </%block>
7
7
8 <%def name="breadcrumbs_links()">
8 <%def name="breadcrumbs_links()">
9 ${_('Forks')}
9 ${_('Forks')}
10 </%def>
10 </%def>
11
11
12 <%block name="header_menu">
12 <%block name="header_menu">
13 ${self.menu('repositories')}
13 ${self.menu('repositories')}
14 </%block>
14 </%block>
15
15
16 <%def name="main()">
16 <%def name="main()">
17 ${self.repo_context_bar('showforks')}
17 ${self.repo_context_bar('showforks')}
18 <div class="box">
18 <div class="box">
19 <!-- box / title -->
19 <!-- box / title -->
20 <div class="title">
20 <div class="title">
21 ${self.breadcrumbs()}
21 ${self.breadcrumbs()}
22 </div>
22 </div>
23 <!-- end box / title -->
23 <!-- end box / title -->
24 <div class="table">
24 <div class="table">
25 <div id="forks">
25 <div id="forks">
26 ${c.forks_data}
26 <%include file='forks_data.html'/>
27 </div>
27 </div>
28 </div>
28 </div>
29 </div>
29 </div>
30 </%def>
30 </%def>
@@ -1,336 +1,338 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3 <%block name="title">
3 <%block name="title">
4 ${_('Journal')}
4 ${_('Journal')}
5 </%block>
5 </%block>
6 <%def name="breadcrumbs()">
6 <%def name="breadcrumbs()">
7 <h5>
7 <h5>
8 <form id="filter_form">
8 <form id="filter_form">
9 <input class="q_filter_box ${'' if c.search_term else 'initial'}" id="j_filter" size="15" type="text" name="filter" value="${c.search_term or _('quick filter...')}"/>
9 <input class="q_filter_box ${'' if c.search_term else 'initial'}" id="j_filter" size="15" type="text" name="filter" value="${c.search_term or _('quick filter...')}"/>
10 <span class="tooltip" title="${h.tooltip(h.journal_filter_help())}">?</span>
10 <span class="tooltip" title="${h.tooltip(h.journal_filter_help())}">?</span>
11 <input type='submit' value="${_('Filter')}" class="btn btn-small" style="padding:0px 2px 0px 2px;margin:0px"/>
11 <input type='submit' value="${_('Filter')}" class="btn btn-small" style="padding:0px 2px 0px 2px;margin:0px"/>
12 ${_('Journal')} - ${ungettext('%s Entry', '%s Entries', c.journal_pager.item_count) % (c.journal_pager.item_count)}
12 ${_('Journal')} - ${ungettext('%s Entry', '%s Entries', c.journal_pager.item_count) % (c.journal_pager.item_count)}
13 </form>
13 </form>
14 ${h.end_form()}
14 ${h.end_form()}
15 </h5>
15 </h5>
16 </%def>
16 </%def>
17 <%block name="header_menu">
17 <%block name="header_menu">
18 ${self.menu('journal')}
18 ${self.menu('journal')}
19 </%block>
19 </%block>
20 <%block name="head_extra">
20 <%block name="head_extra">
21 <link href="${h.url('journal_atom', api_key=c.authuser.api_key)}" rel="alternate" title="${_('ATOM journal feed')}" type="application/atom+xml" />
21 <link href="${h.url('journal_atom', api_key=c.authuser.api_key)}" rel="alternate" title="${_('ATOM journal feed')}" type="application/atom+xml" />
22 <link href="${h.url('journal_rss', api_key=c.authuser.api_key)}" rel="alternate" title="${_('RSS journal feed')}" type="application/rss+xml" />
22 <link href="${h.url('journal_rss', api_key=c.authuser.api_key)}" rel="alternate" title="${_('RSS journal feed')}" type="application/rss+xml" />
23 </%block>
23 </%block>
24
24
25 <%def name="main()">
25 <%def name="main()">
26 <div class="box box-left">
26 <div class="box box-left">
27 <!-- box / title -->
27 <!-- box / title -->
28 <div class="title">
28 <div class="title">
29 ${self.breadcrumbs()}
29 ${self.breadcrumbs()}
30 <ul class="links icon-only-links">
30 <ul class="links icon-only-links">
31 <li>
31 <li>
32 <span><a id="refresh" href="${h.url('journal')}"><i class="icon-arrows-cw"></i></a></span>
32 <span><a id="refresh" href="${h.url('journal')}"><i class="icon-arrows-cw"></i></a></span>
33 </li>
33 </li>
34 <li>
34 <li>
35 <span><a href="${h.url('journal_atom', api_key=c.authuser.api_key)}"><i class="icon-rss-squared"></i></a></span>
35 <span><a href="${h.url('journal_atom', api_key=c.authuser.api_key)}"><i class="icon-rss-squared"></i></a></span>
36 </li>
36 </li>
37 </ul>
37 </ul>
38 </div>
38 </div>
39 <div id="journal">${c.journal_data}</div>
39 <div id="journal">
40 <%include file='journal_data.html'/>
41 </div>
40 </div>
42 </div>
41 <div class="box box-right">
43 <div class="box box-right">
42 <!-- box / title -->
44 <!-- box / title -->
43
45
44 <div class="title">
46 <div class="title">
45 <h5>
47 <h5>
46 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value="" style="display: none"/>
48 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value="" style="display: none"/>
47 <input class="q_filter_box" id="q_filter_watched" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value="" style="display: none"/>
49 <input class="q_filter_box" id="q_filter_watched" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value="" style="display: none"/>
48 </h5>
50 </h5>
49 <ul class="links nav nav-tabs">
51 <ul class="links nav nav-tabs">
50 <li class="active" id="show_watched_li">
52 <li class="active" id="show_watched_li">
51 <a id="show_watched" href="#watched"><i class="icon-eye"></i> ${_('Watched')}</a>
53 <a id="show_watched" href="#watched"><i class="icon-eye"></i> ${_('Watched')}</a>
52 </li>
54 </li>
53 <li id="show_my_li">
55 <li id="show_my_li">
54 <a id="show_my" href="#my"><i class="icon-database"></i> ${_('My Repos')}</a>
56 <a id="show_my" href="#my"><i class="icon-database"></i> ${_('My Repos')}</a>
55 </li>
57 </li>
56 </ul>
58 </ul>
57 </div>
59 </div>
58
60
59 <!-- end box / title -->
61 <!-- end box / title -->
60 <div id="my_container" style="display:none">
62 <div id="my_container" style="display:none">
61 <div class="table-grid table yui-skin-sam" id="repos_list_wrap"></div>
63 <div class="table-grid table yui-skin-sam" id="repos_list_wrap"></div>
62 <div id="user-paginator" style="padding: 0px 0px 0px 20px"></div>
64 <div id="user-paginator" style="padding: 0px 0px 0px 20px"></div>
63 </div>
65 </div>
64
66
65 <div id="watched_container">
67 <div id="watched_container">
66 <div class="table-grid table yui-skin-sam" id="watched_repos_list_wrap"></div>
68 <div class="table-grid table yui-skin-sam" id="watched_repos_list_wrap"></div>
67 <div id="watched-user-paginator" style="padding: 0px 0px 0px 20px"></div>
69 <div id="watched-user-paginator" style="padding: 0px 0px 0px 20px"></div>
68 </div>
70 </div>
69 </div>
71 </div>
70
72
71 <script type="text/javascript">
73 <script type="text/javascript">
72
74
73 $('#j_filter').click(function(){
75 $('#j_filter').click(function(){
74 var $jfilter = $('#j_filter');
76 var $jfilter = $('#j_filter');
75 if($jfilter.hasClass('initial')){
77 if($jfilter.hasClass('initial')){
76 $jfilter.val('');
78 $jfilter.val('');
77 }
79 }
78 });
80 });
79 var fix_j_filter_width = function(len){
81 var fix_j_filter_width = function(len){
80 $('#j_filter').css('width', Math.max(80, len*6.50)+'px');
82 $('#j_filter').css('width', Math.max(80, len*6.50)+'px');
81 };
83 };
82 $('#j_filter').keyup(function(){
84 $('#j_filter').keyup(function(){
83 fix_j_filter_width($('#j_filter').val().length);
85 fix_j_filter_width($('#j_filter').val().length);
84 });
86 });
85 $('#filter_form').submit(function(e){
87 $('#filter_form').submit(function(e){
86 e.preventDefault();
88 e.preventDefault();
87 var val = $('#j_filter').val();
89 var val = $('#j_filter').val();
88 window.location = "${url.current(filter='__FILTER__')}".replace('__FILTER__',val);
90 window.location = "${url.current(filter='__FILTER__')}".replace('__FILTER__',val);
89 });
91 });
90 fix_j_filter_width($('#j_filter').val().length);
92 fix_j_filter_width($('#j_filter').val().length);
91
93
92 $('#refresh').click(function(e){
94 $('#refresh').click(function(e){
93 asynchtml("${h.url.current(filter=c.search_term)}", $("#journal"), function(){
95 asynchtml("${h.url.current(filter=c.search_term)}", $("#journal"), function(){
94 show_more_event();
96 show_more_event();
95 tooltip_activate();
97 tooltip_activate();
96 show_changeset_tooltip();
98 show_changeset_tooltip();
97 });
99 });
98 e.preventDefault();
100 e.preventDefault();
99 });
101 });
100
102
101 var show_my = function(e){
103 var show_my = function(e){
102 $('#watched_container').hide();
104 $('#watched_container').hide();
103 $('#my_container').show();
105 $('#my_container').show();
104 $('#q_filter').show();
106 $('#q_filter').show();
105 $('#q_filter_watched').hide();
107 $('#q_filter_watched').hide();
106
108
107 $('#show_my_li').addClass('active');
109 $('#show_my_li').addClass('active');
108 $('#show_watched_li').removeClass('active');
110 $('#show_watched_li').removeClass('active');
109 if(!$('#show_my').hasClass('loaded')){
111 if(!$('#show_my').hasClass('loaded')){
110 table_renderer(${c.data |n});
112 table_renderer(${c.data |n});
111 $('#show_my').addClass('loaded');
113 $('#show_my').addClass('loaded');
112 }
114 }
113 };
115 };
114 $('#show_my').click(function(){
116 $('#show_my').click(function(){
115 show_my();
117 show_my();
116 });
118 });
117 var show_watched = function(){
119 var show_watched = function(){
118 $('#my_container').hide();
120 $('#my_container').hide();
119 $('#watched_container').show();
121 $('#watched_container').show();
120 $('#q_filter_watched').show();
122 $('#q_filter_watched').show();
121 $('#q_filter').hide();
123 $('#q_filter').hide();
122
124
123 $('#show_watched_li').addClass('active');
125 $('#show_watched_li').addClass('active');
124 $('#show_my_li').removeClass('active');
126 $('#show_my_li').removeClass('active');
125 if(!$('#show_watched').hasClass('loaded')){
127 if(!$('#show_watched').hasClass('loaded')){
126 watched_renderer(${c.watched_data |n});
128 watched_renderer(${c.watched_data |n});
127 $('#show_watched').addClass('loaded');
129 $('#show_watched').addClass('loaded');
128 }
130 }
129 };
131 };
130 $('#show_watched').click(function(){
132 $('#show_watched').click(function(){
131 show_watched();
133 show_watched();
132 });
134 });
133 //init watched
135 //init watched
134 show_watched();
136 show_watched();
135
137
136 var tabs = {
138 var tabs = {
137 'watched': show_watched,
139 'watched': show_watched,
138 'my': show_my
140 'my': show_my
139 }
141 }
140 var url = location.href.split('#');
142 var url = location.href.split('#');
141 if (url[1]) {
143 if (url[1]) {
142 //We have a hash
144 //We have a hash
143 var tabHash = url[1];
145 var tabHash = url[1];
144 var func = tabs[tabHash]
146 var func = tabs[tabHash]
145 if (func){
147 if (func){
146 func();
148 func();
147 }
149 }
148 }
150 }
149 function watched_renderer(data){
151 function watched_renderer(data){
150 var myDataSource = new YAHOO.util.DataSource(data);
152 var myDataSource = new YAHOO.util.DataSource(data);
151 myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
153 myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
152
154
153 myDataSource.responseSchema = {
155 myDataSource.responseSchema = {
154 resultsList: "records",
156 resultsList: "records",
155 fields: [
157 fields: [
156 {key:"menu"},
158 {key:"menu"},
157 {key:"raw_name"},
159 {key:"raw_name"},
158 {key:"name"},
160 {key:"name"},
159 {key:"last_changeset"},
161 {key:"last_changeset"},
160 {key:"last_rev_raw"},
162 {key:"last_rev_raw"},
161 {key:"action"}
163 {key:"action"}
162 ]
164 ]
163 };
165 };
164 myDataSource.doBeforeCallback = function(req,raw,res,cb) {
166 myDataSource.doBeforeCallback = function(req,raw,res,cb) {
165 // This is the filter function
167 // This is the filter function
166 var data = res.results || [],
168 var data = res.results || [],
167 filtered = [],
169 filtered = [],
168 i,l;
170 i,l;
169
171
170 if (req) {
172 if (req) {
171 req = req.toLowerCase();
173 req = req.toLowerCase();
172 for (i = 0; i<data.length; i++) {
174 for (i = 0; i<data.length; i++) {
173 var pos = data[i].raw_name.toLowerCase().indexOf(req)
175 var pos = data[i].raw_name.toLowerCase().indexOf(req)
174 if (pos != -1) {
176 if (pos != -1) {
175 filtered.push(data[i]);
177 filtered.push(data[i]);
176 }
178 }
177 }
179 }
178 res.results = filtered;
180 res.results = filtered;
179 }
181 }
180 return res;
182 return res;
181 }
183 }
182 // main table sorting
184 // main table sorting
183 var myColumnDefs = [
185 var myColumnDefs = [
184 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
186 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
185 {key:"name",label:"${_('Name')}",sortable:true,
187 {key:"name",label:"${_('Name')}",sortable:true,
186 sortOptions: { sortFunction: nameSort }},
188 sortOptions: { sortFunction: nameSort }},
187 {key:"last_changeset",label:"${_('Tip')}",sortable:true,
189 {key:"last_changeset",label:"${_('Tip')}",sortable:true,
188 sortOptions: { sortFunction: revisionSort }},
190 sortOptions: { sortFunction: revisionSort }},
189 {key:"action",label:"${_('Action')}",sortable:false}
191 {key:"action",label:"${_('Action')}",sortable:false}
190 ];
192 ];
191
193
192 var myDataTable = new YAHOO.widget.DataTable("watched_repos_list_wrap", myColumnDefs, myDataSource,{
194 var myDataTable = new YAHOO.widget.DataTable("watched_repos_list_wrap", myColumnDefs, myDataSource,{
193 sortedBy:{key:"name",dir:"asc"},
195 sortedBy:{key:"name",dir:"asc"},
194 paginator: YUI_paginator(25, ['watched-user-paginator']),
196 paginator: YUI_paginator(25, ['watched-user-paginator']),
195
197
196 MSG_SORTASC:"${_('Click to sort ascending')}",
198 MSG_SORTASC:"${_('Click to sort ascending')}",
197 MSG_SORTDESC:"${_('Click to sort descending')}",
199 MSG_SORTDESC:"${_('Click to sort descending')}",
198 MSG_EMPTY:"${_('No records found.')}",
200 MSG_EMPTY:"${_('No records found.')}",
199 MSG_ERROR:"${_('Data error.')}",
201 MSG_ERROR:"${_('Data error.')}",
200 MSG_LOADING:"${_('Loading...')}"
202 MSG_LOADING:"${_('Loading...')}"
201 }
203 }
202 );
204 );
203 myDataTable.subscribe('postRenderEvent',function(oArgs) {
205 myDataTable.subscribe('postRenderEvent',function(oArgs) {
204 tooltip_activate();
206 tooltip_activate();
205 quick_repo_menu();
207 quick_repo_menu();
206 });
208 });
207
209
208 var filterTimeout = null;
210 var filterTimeout = null;
209
211
210 updateFilter = function () {
212 updateFilter = function () {
211 // Reset timeout
213 // Reset timeout
212 filterTimeout = null;
214 filterTimeout = null;
213
215
214 // Reset sort
216 // Reset sort
215 var state = myDataTable.getState();
217 var state = myDataTable.getState();
216 state.sortedBy = {key:'name', dir:YAHOO.widget.DataTable.CLASS_ASC};
218 state.sortedBy = {key:'name', dir:YAHOO.widget.DataTable.CLASS_ASC};
217
219
218 // Get filtered data
220 // Get filtered data
219 myDataSource.sendRequest(YUD.get('q_filter_watched').value,{
221 myDataSource.sendRequest(YUD.get('q_filter_watched').value,{
220 success : myDataTable.onDataReturnInitializeTable,
222 success : myDataTable.onDataReturnInitializeTable,
221 failure : myDataTable.onDataReturnInitializeTable,
223 failure : myDataTable.onDataReturnInitializeTable,
222 scope : myDataTable,
224 scope : myDataTable,
223 argument: state
225 argument: state
224 });
226 });
225
227
226 };
228 };
227 $('#q_filter_watched').click(function(){
229 $('#q_filter_watched').click(function(){
228 if(!$('#q_filter_watched').hasClass('loaded')) {
230 if(!$('#q_filter_watched').hasClass('loaded')) {
229 //TODO: load here full list later to do search within groups
231 //TODO: load here full list later to do search within groups
230 $('#q_filter_watched').css('loaded');
232 $('#q_filter_watched').css('loaded');
231 }
233 }
232 });
234 });
233
235
234 $('#q_filter_watched').keyup(function(){
236 $('#q_filter_watched').keyup(function(){
235 clearTimeout(filterTimeout);
237 clearTimeout(filterTimeout);
236 filterTimeout = setTimeout(updateFilter,600);
238 filterTimeout = setTimeout(updateFilter,600);
237 });
239 });
238 }
240 }
239
241
240 function table_renderer(data){
242 function table_renderer(data){
241 var myDataSource = new YAHOO.util.DataSource(data);
243 var myDataSource = new YAHOO.util.DataSource(data);
242 myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
244 myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
243
245
244 myDataSource.responseSchema = {
246 myDataSource.responseSchema = {
245 resultsList: "records",
247 resultsList: "records",
246 fields: [
248 fields: [
247 {key:"menu"},
249 {key:"menu"},
248 {key:"raw_name"},
250 {key:"raw_name"},
249 {key:"name"},
251 {key:"name"},
250 {key:"last_changeset"},
252 {key:"last_changeset"},
251 {key:"last_rev_raw"},
253 {key:"last_rev_raw"},
252 {key:"action"}
254 {key:"action"}
253 ]
255 ]
254 };
256 };
255 myDataSource.doBeforeCallback = function(req,raw,res,cb) {
257 myDataSource.doBeforeCallback = function(req,raw,res,cb) {
256 // This is the filter function
258 // This is the filter function
257 var data = res.results || [],
259 var data = res.results || [],
258 filtered = [],
260 filtered = [],
259 i,l;
261 i,l;
260
262
261 if (req) {
263 if (req) {
262 req = req.toLowerCase();
264 req = req.toLowerCase();
263 for (i = 0; i<data.length; i++) {
265 for (i = 0; i<data.length; i++) {
264 var pos = data[i].raw_name.toLowerCase().indexOf(req)
266 var pos = data[i].raw_name.toLowerCase().indexOf(req)
265 if (pos != -1) {
267 if (pos != -1) {
266 filtered.push(data[i]);
268 filtered.push(data[i]);
267 }
269 }
268 }
270 }
269 res.results = filtered;
271 res.results = filtered;
270 }
272 }
271 return res;
273 return res;
272 }
274 }
273 // main table sorting
275 // main table sorting
274 var myColumnDefs = [
276 var myColumnDefs = [
275 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
277 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
276 {key:"name",label:"${_('Name')}",sortable:true,
278 {key:"name",label:"${_('Name')}",sortable:true,
277 sortOptions: { sortFunction: nameSort }},
279 sortOptions: { sortFunction: nameSort }},
278 {key:"last_changeset",label:"${_('Tip')}",sortable:true,
280 {key:"last_changeset",label:"${_('Tip')}",sortable:true,
279 sortOptions: { sortFunction: revisionSort }},
281 sortOptions: { sortFunction: revisionSort }},
280 {key:"action",label:"${_('Action')}",sortable:false}
282 {key:"action",label:"${_('Action')}",sortable:false}
281 ];
283 ];
282
284
283 var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,{
285 var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,{
284 sortedBy:{key:"name",dir:"asc"},
286 sortedBy:{key:"name",dir:"asc"},
285 paginator: YUI_paginator(25, ['user-paginator']),
287 paginator: YUI_paginator(25, ['user-paginator']),
286
288
287 MSG_SORTASC:"${_('Click to sort ascending')}",
289 MSG_SORTASC:"${_('Click to sort ascending')}",
288 MSG_SORTDESC:"${_('Click to sort descending')}",
290 MSG_SORTDESC:"${_('Click to sort descending')}",
289 MSG_EMPTY:"${_('No records found.')}",
291 MSG_EMPTY:"${_('No records found.')}",
290 MSG_ERROR:"${_('Data error.')}",
292 MSG_ERROR:"${_('Data error.')}",
291 MSG_LOADING:"${_('Loading...')}"
293 MSG_LOADING:"${_('Loading...')}"
292 }
294 }
293 );
295 );
294 myDataTable.subscribe('postRenderEvent',function(oArgs) {
296 myDataTable.subscribe('postRenderEvent',function(oArgs) {
295 tooltip_activate();
297 tooltip_activate();
296 quick_repo_menu();
298 quick_repo_menu();
297 });
299 });
298
300
299 var filterTimeout = null;
301 var filterTimeout = null;
300
302
301 updateFilter = function () {
303 updateFilter = function () {
302 // Reset timeout
304 // Reset timeout
303 filterTimeout = null;
305 filterTimeout = null;
304
306
305 // Reset sort
307 // Reset sort
306 var state = myDataTable.getState();
308 var state = myDataTable.getState();
307 state.sortedBy = {key:'name', dir:YAHOO.widget.DataTable.CLASS_ASC};
309 state.sortedBy = {key:'name', dir:YAHOO.widget.DataTable.CLASS_ASC};
308
310
309 // Get filtered data
311 // Get filtered data
310 myDataSource.sendRequest(YUD.get('q_filter').value,{
312 myDataSource.sendRequest(YUD.get('q_filter').value,{
311 success : myDataTable.onDataReturnInitializeTable,
313 success : myDataTable.onDataReturnInitializeTable,
312 failure : myDataTable.onDataReturnInitializeTable,
314 failure : myDataTable.onDataReturnInitializeTable,
313 scope : myDataTable,
315 scope : myDataTable,
314 argument: state
316 argument: state
315 });
317 });
316
318
317 };
319 };
318 $('#q_filter').click(function(){
320 $('#q_filter').click(function(){
319 if(!$('#q_filter').hasClass('loaded')){
321 if(!$('#q_filter').hasClass('loaded')){
320 //TODO: load here full list later to do search within groups
322 //TODO: load here full list later to do search within groups
321 $('#q_filter').addClass('loaded');
323 $('#q_filter').addClass('loaded');
322 }
324 }
323 });
325 });
324
326
325 $('#q_filter').keyup(function(){
327 $('#q_filter').keyup(function(){
326 clearTimeout(filterTimeout);
328 clearTimeout(filterTimeout);
327 filterTimeout = setTimeout(updateFilter,600);
329 filterTimeout = setTimeout(updateFilter,600);
328 });
330 });
329
331
330 if($('#q_filter').val()) {
332 if($('#q_filter').val()) {
331 updateFilter();
333 updateFilter();
332 }
334 }
333 }
335 }
334
336
335 </script>
337 </script>
336 </%def>
338 </%def>
@@ -1,34 +1,36 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3 <%block name="title">
3 <%block name="title">
4 ${_('Public Journal')}
4 ${_('Public Journal')}
5 </%block>
5 </%block>
6 <%def name="breadcrumbs()">
6 <%def name="breadcrumbs()">
7 ${c.site_name}
7 ${c.site_name}
8 </%def>
8 </%def>
9 <%block name="header_menu">
9 <%block name="header_menu">
10 ${self.menu('journal')}
10 ${self.menu('journal')}
11 </%block>
11 </%block>
12 <%block name="head_extra">
12 <%block name="head_extra">
13 <link href="${h.url('public_journal_atom')}" rel="alternate" title="${_('ATOM public journal feed')}" type="application/atom+xml" />
13 <link href="${h.url('public_journal_atom')}" rel="alternate" title="${_('ATOM public journal feed')}" type="application/atom+xml" />
14 <link href="${h.url('public_journal_rss')}" rel="alternate" title="${_('RSS public journal feed')}" type="application/rss+xml" />
14 <link href="${h.url('public_journal_rss')}" rel="alternate" title="${_('RSS public journal feed')}" type="application/rss+xml" />
15 </%block>
15 </%block>
16 <%def name="main()">
16 <%def name="main()">
17
17
18 <div class="box">
18 <div class="box">
19 <!-- box / title -->
19 <!-- box / title -->
20 <div class="title">
20 <div class="title">
21 <h5>${_('Public Journal')}</h5>
21 <h5>${_('Public Journal')}</h5>
22 <ul class="links">
22 <ul class="links">
23 <li>
23 <li>
24 <span>
24 <span>
25 <a href="${h.url('public_journal_atom')}"> <i class="icon-rss-squared" style="color: #fa9b39"></i></a>
25 <a href="${h.url('public_journal_atom')}"> <i class="icon-rss-squared" style="color: #fa9b39"></i></a>
26 </span>
26 </span>
27 </li>
27 </li>
28 </ul>
28 </ul>
29 </div>
29 </div>
30
30
31 <div id="journal">${c.journal_data}</div>
31 <div id="journal">
32 <%include file='journal_data.html'/>
33 </div>
32 </div>
34 </div>
33
35
34 </%def>
36 </%def>
@@ -1,57 +1,57 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%block name="title">
3 <%block name="title">
4 ${_('%s Pull Requests') % c.repo_name}
4 ${_('%s Pull Requests') % c.repo_name}
5 </%block>
5 </%block>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 %if c.from_:
8 %if c.from_:
9 ${_("Pull Requests from %s'") % c.repo_name}
9 ${_("Pull Requests from %s'") % c.repo_name}
10 %else:
10 %else:
11 ${_("Pull Requests to '%s'") % c.repo_name}
11 ${_("Pull Requests to '%s'") % c.repo_name}
12 %endif
12 %endif
13 </%def>
13 </%def>
14
14
15 <%block name="header_menu">
15 <%block name="header_menu">
16 ${self.menu('repositories')}
16 ${self.menu('repositories')}
17 </%block>
17 </%block>
18
18
19 <%def name="main()">
19 <%def name="main()">
20 ${self.repo_context_bar('showpullrequest')}
20 ${self.repo_context_bar('showpullrequest')}
21
21
22 <div class="box">
22 <div class="box">
23 <!-- box / title -->
23 <!-- box / title -->
24 <div class="title">
24 <div class="title">
25 ${self.breadcrumbs()}
25 ${self.breadcrumbs()}
26 <ul class="links">
26 <ul class="links">
27 <li>
27 <li>
28 %if c.authuser.username != 'default':
28 %if c.authuser.username != 'default':
29 <span>
29 <span>
30 <a id="open_new_pr" class="btn btn-small btn-success" href="${h.url('pullrequest_home',repo_name=c.repo_name)}"><i class="icon-plus"></i> ${_('Open New Pull Request')}</a>
30 <a id="open_new_pr" class="btn btn-small btn-success" href="${h.url('pullrequest_home',repo_name=c.repo_name)}"><i class="icon-plus"></i> ${_('Open New Pull Request')}</a>
31 </span>
31 </span>
32 %endif
32 %endif
33 <span>
33 <span>
34 %if c.from_:
34 %if c.from_:
35 <a class="btn btn-small" href="${h.url('pullrequest_show_all',repo_name=c.repo_name,closed=c.closed)}"><i class="icon-git-compare"></i> ${_('Show Pull Requests to %s') % c.repo_name}</a>
35 <a class="btn btn-small" href="${h.url('pullrequest_show_all',repo_name=c.repo_name,closed=c.closed)}"><i class="icon-git-compare"></i> ${_('Show Pull Requests to %s') % c.repo_name}</a>
36 %else:
36 %else:
37 <a class="btn btn-small" href="${h.url('pullrequest_show_all',repo_name=c.repo_name,closed=c.closed,from_=1)}"><i class="icon-git-compare"></i> ${_("Show Pull Requests from '%s'") % c.repo_name}</a>
37 <a class="btn btn-small" href="${h.url('pullrequest_show_all',repo_name=c.repo_name,closed=c.closed,from_=1)}"><i class="icon-git-compare"></i> ${_("Show Pull Requests from '%s'") % c.repo_name}</a>
38 %endif
38 %endif
39 </span>
39 </span>
40 </li>
40 </li>
41 </ul>
41 </ul>
42 </div>
42 </div>
43
43
44 <div style="margin: 0 20px">
44 <div style="margin: 0 20px">
45 <div>
45 <div>
46 %if c.closed:
46 %if c.closed:
47 ${h.link_to(_('Hide closed pull requests (only show open pull requests)'), h.url('pullrequest_show_all',repo_name=c.repo_name,from_=c.from_))}
47 ${h.link_to(_('Hide closed pull requests (only show open pull requests)'), h.url('pullrequest_show_all',repo_name=c.repo_name,from_=c.from_))}
48 %else:
48 %else:
49 ${h.link_to(_('Show closed pull requests (in addition to open pull requests)'), h.url('pullrequest_show_all',repo_name=c.repo_name,from_=c.from_,closed=1))}
49 ${h.link_to(_('Show closed pull requests (in addition to open pull requests)'), h.url('pullrequest_show_all',repo_name=c.repo_name,from_=c.from_,closed=1))}
50 %endif
50 %endif
51 </div>
51 </div>
52 </div>
52 </div>
53
53
54 ${c.pullrequest_data}
54 <%include file='pullrequest_data.html'/>
55
55
56 </div>
56 </div>
57 </%def>
57 </%def>
General Comments 0
You need to be logged in to leave comments. Login now