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