##// END OF EJS Templates
controllers: simplify request.GET.get for safe_int...
Mads Kiilerich -
r5992:fb64046d 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)
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
144
145 if request.environ.get('HTTP_X_PARTIAL_XHR'):
145 if request.environ.get('HTTP_X_PARTIAL_XHR'):
146 return render('admin/admin_log.html')
146 return render('admin/admin_log.html')
147
147
148 return render('admin/admin.html')
148 return render('admin/admin.html')
@@ -1,292 +1,292 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.gists
15 kallithea.controllers.admin.gists
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 gist controller for Kallithea
18 gist 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: May 9, 2013
22 :created_on: May 9, 2013
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 time
28 import time
29 import logging
29 import logging
30 import traceback
30 import traceback
31 import formencode.htmlfill
31 import formencode.htmlfill
32
32
33 from pylons import request, response, tmpl_context as c, url
33 from pylons import request, response, tmpl_context as c, url
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden
35 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden
36
36
37 from kallithea.model.forms import GistForm
37 from kallithea.model.forms import GistForm
38 from kallithea.model.gist import GistModel
38 from kallithea.model.gist import GistModel
39 from kallithea.model.meta import Session
39 from kallithea.model.meta import Session
40 from kallithea.model.db import Gist, User
40 from kallithea.model.db import Gist, User
41 from kallithea.lib import helpers as h
41 from kallithea.lib import helpers as h
42 from kallithea.lib.base import BaseController, render
42 from kallithea.lib.base import BaseController, render
43 from kallithea.lib.auth import LoginRequired, NotAnonymous
43 from kallithea.lib.auth import LoginRequired, NotAnonymous
44 from kallithea.lib.utils import jsonify
44 from kallithea.lib.utils import jsonify
45 from kallithea.lib.utils2 import safe_int, safe_unicode, time_to_datetime
45 from kallithea.lib.utils2 import safe_int, safe_unicode, time_to_datetime
46 from kallithea.lib.helpers import Page
46 from kallithea.lib.helpers import Page
47 from sqlalchemy.sql.expression import or_
47 from sqlalchemy.sql.expression import or_
48 from kallithea.lib.vcs.exceptions import VCSError, NodeNotChangedError
48 from kallithea.lib.vcs.exceptions import VCSError, NodeNotChangedError
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class GistsController(BaseController):
53 class GistsController(BaseController):
54 """REST Controller styled on the Atom Publishing Protocol"""
54 """REST Controller styled on the Atom Publishing Protocol"""
55
55
56 def __load_defaults(self, extra_values=None):
56 def __load_defaults(self, extra_values=None):
57 c.lifetime_values = [
57 c.lifetime_values = [
58 (str(-1), _('Forever')),
58 (str(-1), _('Forever')),
59 (str(5), _('5 minutes')),
59 (str(5), _('5 minutes')),
60 (str(60), _('1 hour')),
60 (str(60), _('1 hour')),
61 (str(60 * 24), _('1 day')),
61 (str(60 * 24), _('1 day')),
62 (str(60 * 24 * 30), _('1 month')),
62 (str(60 * 24 * 30), _('1 month')),
63 ]
63 ]
64 if extra_values:
64 if extra_values:
65 c.lifetime_values.append(extra_values)
65 c.lifetime_values.append(extra_values)
66 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
66 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
67
67
68 @LoginRequired()
68 @LoginRequired()
69 def index(self):
69 def index(self):
70 """GET /admin/gists: All items in the collection"""
70 """GET /admin/gists: All items in the collection"""
71 # url('gists')
71 # url('gists')
72 not_default_user = not c.authuser.is_default_user
72 not_default_user = not c.authuser.is_default_user
73 c.show_private = request.GET.get('private') and not_default_user
73 c.show_private = request.GET.get('private') and not_default_user
74 c.show_public = request.GET.get('public') and not_default_user
74 c.show_public = request.GET.get('public') and not_default_user
75
75
76 gists = Gist().query() \
76 gists = Gist().query() \
77 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time())) \
77 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time())) \
78 .order_by(Gist.created_on.desc())
78 .order_by(Gist.created_on.desc())
79
79
80 # MY private
80 # MY private
81 if c.show_private and not c.show_public:
81 if c.show_private and not c.show_public:
82 gists = gists.filter(Gist.gist_type == Gist.GIST_PRIVATE) \
82 gists = gists.filter(Gist.gist_type == Gist.GIST_PRIVATE) \
83 .filter(Gist.gist_owner == c.authuser.user_id)
83 .filter(Gist.gist_owner == c.authuser.user_id)
84 # MY public
84 # MY public
85 elif c.show_public and not c.show_private:
85 elif c.show_public and not c.show_private:
86 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC) \
86 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC) \
87 .filter(Gist.gist_owner == c.authuser.user_id)
87 .filter(Gist.gist_owner == c.authuser.user_id)
88
88
89 # MY public+private
89 # MY public+private
90 elif c.show_private and c.show_public:
90 elif c.show_private and c.show_public:
91 gists = gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
91 gists = gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
92 Gist.gist_type == Gist.GIST_PRIVATE)) \
92 Gist.gist_type == Gist.GIST_PRIVATE)) \
93 .filter(Gist.gist_owner == c.authuser.user_id)
93 .filter(Gist.gist_owner == c.authuser.user_id)
94
94
95 # default show ALL public gists
95 # default show ALL public gists
96 if not c.show_public and not c.show_private:
96 if not c.show_public and not c.show_private:
97 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
97 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
98
98
99 c.gists = gists
99 c.gists = gists
100 p = safe_int(request.GET.get('page', 1), 1)
100 p = safe_int(request.GET.get('page'), 1)
101 c.gists_pager = Page(c.gists, page=p, items_per_page=10)
101 c.gists_pager = Page(c.gists, page=p, items_per_page=10)
102 return render('admin/gists/index.html')
102 return render('admin/gists/index.html')
103
103
104 @LoginRequired()
104 @LoginRequired()
105 @NotAnonymous()
105 @NotAnonymous()
106 def create(self):
106 def create(self):
107 """POST /admin/gists: Create a new item"""
107 """POST /admin/gists: Create a new item"""
108 # url('gists')
108 # url('gists')
109 self.__load_defaults()
109 self.__load_defaults()
110 gist_form = GistForm([x[0] for x in c.lifetime_values])()
110 gist_form = GistForm([x[0] for x in c.lifetime_values])()
111 try:
111 try:
112 form_result = gist_form.to_python(dict(request.POST))
112 form_result = gist_form.to_python(dict(request.POST))
113 #TODO: multiple files support, from the form
113 #TODO: multiple files support, from the form
114 filename = form_result['filename'] or Gist.DEFAULT_FILENAME
114 filename = form_result['filename'] or Gist.DEFAULT_FILENAME
115 nodes = {
115 nodes = {
116 filename: {
116 filename: {
117 'content': form_result['content'],
117 'content': form_result['content'],
118 'lexer': form_result['mimetype'] # None is autodetect
118 'lexer': form_result['mimetype'] # None is autodetect
119 }
119 }
120 }
120 }
121 _public = form_result['public']
121 _public = form_result['public']
122 gist_type = Gist.GIST_PUBLIC if _public else Gist.GIST_PRIVATE
122 gist_type = Gist.GIST_PUBLIC if _public else Gist.GIST_PRIVATE
123 gist = GistModel().create(
123 gist = GistModel().create(
124 description=form_result['description'],
124 description=form_result['description'],
125 owner=c.authuser.user_id,
125 owner=c.authuser.user_id,
126 gist_mapping=nodes,
126 gist_mapping=nodes,
127 gist_type=gist_type,
127 gist_type=gist_type,
128 lifetime=form_result['lifetime']
128 lifetime=form_result['lifetime']
129 )
129 )
130 Session().commit()
130 Session().commit()
131 new_gist_id = gist.gist_access_id
131 new_gist_id = gist.gist_access_id
132 except formencode.Invalid as errors:
132 except formencode.Invalid as errors:
133 defaults = errors.value
133 defaults = errors.value
134
134
135 return formencode.htmlfill.render(
135 return formencode.htmlfill.render(
136 render('admin/gists/new.html'),
136 render('admin/gists/new.html'),
137 defaults=defaults,
137 defaults=defaults,
138 errors=errors.error_dict or {},
138 errors=errors.error_dict or {},
139 prefix_error=False,
139 prefix_error=False,
140 encoding="UTF-8",
140 encoding="UTF-8",
141 force_defaults=False)
141 force_defaults=False)
142
142
143 except Exception as e:
143 except Exception as e:
144 log.error(traceback.format_exc())
144 log.error(traceback.format_exc())
145 h.flash(_('Error occurred during gist creation'), category='error')
145 h.flash(_('Error occurred during gist creation'), category='error')
146 raise HTTPFound(location=url('new_gist'))
146 raise HTTPFound(location=url('new_gist'))
147 raise HTTPFound(location=url('gist', gist_id=new_gist_id))
147 raise HTTPFound(location=url('gist', gist_id=new_gist_id))
148
148
149 @LoginRequired()
149 @LoginRequired()
150 @NotAnonymous()
150 @NotAnonymous()
151 def new(self, format='html'):
151 def new(self, format='html'):
152 """GET /admin/gists/new: Form to create a new item"""
152 """GET /admin/gists/new: Form to create a new item"""
153 # url('new_gist')
153 # url('new_gist')
154 self.__load_defaults()
154 self.__load_defaults()
155 return render('admin/gists/new.html')
155 return render('admin/gists/new.html')
156
156
157 @LoginRequired()
157 @LoginRequired()
158 @NotAnonymous()
158 @NotAnonymous()
159 def update(self, gist_id):
159 def update(self, gist_id):
160 """PUT /admin/gists/gist_id: Update an existing item"""
160 """PUT /admin/gists/gist_id: Update an existing item"""
161 # Forms posted to this method should contain a hidden field:
161 # Forms posted to this method should contain a hidden field:
162 # <input type="hidden" name="_method" value="PUT" />
162 # <input type="hidden" name="_method" value="PUT" />
163 # Or using helpers:
163 # Or using helpers:
164 # h.form(url('gist', gist_id=ID),
164 # h.form(url('gist', gist_id=ID),
165 # method='put')
165 # method='put')
166 # url('gist', gist_id=ID)
166 # url('gist', gist_id=ID)
167
167
168 @LoginRequired()
168 @LoginRequired()
169 @NotAnonymous()
169 @NotAnonymous()
170 def delete(self, gist_id):
170 def delete(self, gist_id):
171 """DELETE /admin/gists/gist_id: Delete an existing item"""
171 """DELETE /admin/gists/gist_id: Delete an existing item"""
172 # Forms posted to this method should contain a hidden field:
172 # Forms posted to this method should contain a hidden field:
173 # <input type="hidden" name="_method" value="DELETE" />
173 # <input type="hidden" name="_method" value="DELETE" />
174 # Or using helpers:
174 # Or using helpers:
175 # h.form(url('gist', gist_id=ID),
175 # h.form(url('gist', gist_id=ID),
176 # method='delete')
176 # method='delete')
177 # url('gist', gist_id=ID)
177 # url('gist', gist_id=ID)
178 gist = GistModel().get_gist(gist_id)
178 gist = GistModel().get_gist(gist_id)
179 owner = gist.gist_owner == c.authuser.user_id
179 owner = gist.gist_owner == c.authuser.user_id
180 if h.HasPermissionAny('hg.admin')() or owner:
180 if h.HasPermissionAny('hg.admin')() or owner:
181 GistModel().delete(gist)
181 GistModel().delete(gist)
182 Session().commit()
182 Session().commit()
183 h.flash(_('Deleted gist %s') % gist.gist_access_id, category='success')
183 h.flash(_('Deleted gist %s') % gist.gist_access_id, category='success')
184 else:
184 else:
185 raise HTTPForbidden()
185 raise HTTPForbidden()
186
186
187 raise HTTPFound(location=url('gists'))
187 raise HTTPFound(location=url('gists'))
188
188
189 @LoginRequired()
189 @LoginRequired()
190 def show(self, gist_id, revision='tip', format='html', f_path=None):
190 def show(self, gist_id, revision='tip', format='html', f_path=None):
191 """GET /admin/gists/gist_id: Show a specific item"""
191 """GET /admin/gists/gist_id: Show a specific item"""
192 # url('gist', gist_id=ID)
192 # url('gist', gist_id=ID)
193 c.gist = Gist.get_or_404(gist_id)
193 c.gist = Gist.get_or_404(gist_id)
194
194
195 #check if this gist is not expired
195 #check if this gist is not expired
196 if c.gist.gist_expires != -1:
196 if c.gist.gist_expires != -1:
197 if time.time() > c.gist.gist_expires:
197 if time.time() > c.gist.gist_expires:
198 log.error('Gist expired at %s',
198 log.error('Gist expired at %s',
199 time_to_datetime(c.gist.gist_expires))
199 time_to_datetime(c.gist.gist_expires))
200 raise HTTPNotFound()
200 raise HTTPNotFound()
201 try:
201 try:
202 c.file_changeset, c.files = GistModel().get_gist_files(gist_id,
202 c.file_changeset, c.files = GistModel().get_gist_files(gist_id,
203 revision=revision)
203 revision=revision)
204 except VCSError:
204 except VCSError:
205 log.error(traceback.format_exc())
205 log.error(traceback.format_exc())
206 raise HTTPNotFound()
206 raise HTTPNotFound()
207 if format == 'raw':
207 if format == 'raw':
208 content = '\n\n'.join([f.content for f in c.files if (f_path is None or safe_unicode(f.path) == f_path)])
208 content = '\n\n'.join([f.content for f in c.files if (f_path is None or safe_unicode(f.path) == f_path)])
209 response.content_type = 'text/plain'
209 response.content_type = 'text/plain'
210 return content
210 return content
211 return render('admin/gists/show.html')
211 return render('admin/gists/show.html')
212
212
213 @LoginRequired()
213 @LoginRequired()
214 @NotAnonymous()
214 @NotAnonymous()
215 def edit(self, gist_id, format='html'):
215 def edit(self, gist_id, format='html'):
216 """GET /admin/gists/gist_id/edit: Form to edit an existing item"""
216 """GET /admin/gists/gist_id/edit: Form to edit an existing item"""
217 # url('edit_gist', gist_id=ID)
217 # url('edit_gist', gist_id=ID)
218 c.gist = Gist.get_or_404(gist_id)
218 c.gist = Gist.get_or_404(gist_id)
219
219
220 #check if this gist is not expired
220 #check if this gist is not expired
221 if c.gist.gist_expires != -1:
221 if c.gist.gist_expires != -1:
222 if time.time() > c.gist.gist_expires:
222 if time.time() > c.gist.gist_expires:
223 log.error('Gist expired at %s',
223 log.error('Gist expired at %s',
224 time_to_datetime(c.gist.gist_expires))
224 time_to_datetime(c.gist.gist_expires))
225 raise HTTPNotFound()
225 raise HTTPNotFound()
226 try:
226 try:
227 c.file_changeset, c.files = GistModel().get_gist_files(gist_id)
227 c.file_changeset, c.files = GistModel().get_gist_files(gist_id)
228 except VCSError:
228 except VCSError:
229 log.error(traceback.format_exc())
229 log.error(traceback.format_exc())
230 raise HTTPNotFound()
230 raise HTTPNotFound()
231
231
232 self.__load_defaults(extra_values=('0', _('Unmodified')))
232 self.__load_defaults(extra_values=('0', _('Unmodified')))
233 rendered = render('admin/gists/edit.html')
233 rendered = render('admin/gists/edit.html')
234
234
235 if request.POST:
235 if request.POST:
236 rpost = request.POST
236 rpost = request.POST
237 nodes = {}
237 nodes = {}
238 for org_filename, filename, mimetype, content in zip(
238 for org_filename, filename, mimetype, content in zip(
239 rpost.getall('org_files'),
239 rpost.getall('org_files'),
240 rpost.getall('files'),
240 rpost.getall('files'),
241 rpost.getall('mimetypes'),
241 rpost.getall('mimetypes'),
242 rpost.getall('contents')):
242 rpost.getall('contents')):
243
243
244 nodes[org_filename] = {
244 nodes[org_filename] = {
245 'org_filename': org_filename,
245 'org_filename': org_filename,
246 'filename': filename,
246 'filename': filename,
247 'content': content,
247 'content': content,
248 'lexer': mimetype,
248 'lexer': mimetype,
249 }
249 }
250 try:
250 try:
251 GistModel().update(
251 GistModel().update(
252 gist=c.gist,
252 gist=c.gist,
253 description=rpost['description'],
253 description=rpost['description'],
254 owner=c.gist.owner,
254 owner=c.gist.owner,
255 gist_mapping=nodes,
255 gist_mapping=nodes,
256 gist_type=c.gist.gist_type,
256 gist_type=c.gist.gist_type,
257 lifetime=rpost['lifetime']
257 lifetime=rpost['lifetime']
258 )
258 )
259
259
260 Session().commit()
260 Session().commit()
261 h.flash(_('Successfully updated gist content'), category='success')
261 h.flash(_('Successfully updated gist content'), category='success')
262 except NodeNotChangedError:
262 except NodeNotChangedError:
263 # raised if nothing was changed in repo itself. We anyway then
263 # raised if nothing was changed in repo itself. We anyway then
264 # store only DB stuff for gist
264 # store only DB stuff for gist
265 Session().commit()
265 Session().commit()
266 h.flash(_('Successfully updated gist data'), category='success')
266 h.flash(_('Successfully updated gist data'), category='success')
267 except Exception:
267 except Exception:
268 log.error(traceback.format_exc())
268 log.error(traceback.format_exc())
269 h.flash(_('Error occurred during update of gist %s') % gist_id,
269 h.flash(_('Error occurred during update of gist %s') % gist_id,
270 category='error')
270 category='error')
271
271
272 raise HTTPFound(location=url('gist', gist_id=gist_id))
272 raise HTTPFound(location=url('gist', gist_id=gist_id))
273
273
274 return rendered
274 return rendered
275
275
276 @LoginRequired()
276 @LoginRequired()
277 @NotAnonymous()
277 @NotAnonymous()
278 @jsonify
278 @jsonify
279 def check_revision(self, gist_id):
279 def check_revision(self, gist_id):
280 c.gist = Gist.get_or_404(gist_id)
280 c.gist = Gist.get_or_404(gist_id)
281 last_rev = c.gist.scm_instance.get_changeset()
281 last_rev = c.gist.scm_instance.get_changeset()
282 success = True
282 success = True
283 revision = request.POST.get('revision')
283 revision = request.POST.get('revision')
284
284
285 ##TODO: maybe move this to model ?
285 ##TODO: maybe move this to model ?
286 if revision != last_rev.raw_id:
286 if revision != last_rev.raw_id:
287 log.error('Last revision %s is different than submitted %s',
287 log.error('Last revision %s is different than submitted %s',
288 revision, last_rev)
288 revision, last_rev)
289 # our gist has newer version than we
289 # our gist has newer version than we
290 success = False
290 success = False
291
291
292 return {'success': success}
292 return {'success': success}
@@ -1,170 +1,170 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.notifications
15 kallithea.controllers.admin.notifications
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 notifications controller for Kallithea
18 notifications 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: Nov 23, 2010
22 :created_on: Nov 23, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30
30
31 from pylons import request
31 from pylons import request
32 from pylons import tmpl_context as c
32 from pylons import tmpl_context as c
33 from webob.exc import HTTPBadRequest, HTTPForbidden
33 from webob.exc import HTTPBadRequest, HTTPForbidden
34
34
35 from kallithea.model.db import Notification
35 from kallithea.model.db import Notification
36 from kallithea.model.notification import NotificationModel
36 from kallithea.model.notification import NotificationModel
37 from kallithea.model.meta import Session
37 from kallithea.model.meta import Session
38 from kallithea.lib.auth import LoginRequired, NotAnonymous
38 from kallithea.lib.auth import LoginRequired, NotAnonymous
39 from kallithea.lib.base import BaseController, render
39 from kallithea.lib.base import BaseController, render
40 from kallithea.lib import helpers as h
40 from kallithea.lib import helpers as h
41 from kallithea.lib.helpers import Page
41 from kallithea.lib.helpers import Page
42 from kallithea.lib.utils2 import safe_int
42 from kallithea.lib.utils2 import safe_int
43
43
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class NotificationsController(BaseController):
48 class NotificationsController(BaseController):
49 """REST Controller styled on the Atom Publishing Protocol"""
49 """REST Controller styled on the Atom Publishing Protocol"""
50 # To properly map this controller, ensure your config/routing.py
50 # To properly map this controller, ensure your config/routing.py
51 # file has a resource setup:
51 # file has a resource setup:
52 # map.resource('notification', 'notifications', controller='_admin/notifications',
52 # map.resource('notification', 'notifications', controller='_admin/notifications',
53 # path_prefix='/_admin', name_prefix='_admin_')
53 # path_prefix='/_admin', name_prefix='_admin_')
54
54
55 @LoginRequired()
55 @LoginRequired()
56 @NotAnonymous()
56 @NotAnonymous()
57 def __before__(self):
57 def __before__(self):
58 super(NotificationsController, self).__before__()
58 super(NotificationsController, self).__before__()
59
59
60 def index(self, format='html'):
60 def index(self, format='html'):
61 """GET /_admin/notifications: All items in the collection"""
61 """GET /_admin/notifications: All items in the collection"""
62 # url('notifications')
62 # url('notifications')
63 c.user = self.authuser
63 c.user = self.authuser
64 notif = NotificationModel().query_for_user(self.authuser.user_id,
64 notif = NotificationModel().query_for_user(self.authuser.user_id,
65 filter_=request.GET.getall('type'))
65 filter_=request.GET.getall('type'))
66
66
67 p = safe_int(request.GET.get('page', 1), 1)
67 p = safe_int(request.GET.get('page'), 1)
68 c.notifications = Page(notif, page=p, items_per_page=10)
68 c.notifications = Page(notif, page=p, items_per_page=10)
69 c.pull_request_type = Notification.TYPE_PULL_REQUEST
69 c.pull_request_type = Notification.TYPE_PULL_REQUEST
70 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
70 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
71 Notification.TYPE_PULL_REQUEST_COMMENT]
71 Notification.TYPE_PULL_REQUEST_COMMENT]
72
72
73 _current_filter = request.GET.getall('type')
73 _current_filter = request.GET.getall('type')
74 c.current_filter = 'all'
74 c.current_filter = 'all'
75 if _current_filter == [c.pull_request_type]:
75 if _current_filter == [c.pull_request_type]:
76 c.current_filter = 'pull_request'
76 c.current_filter = 'pull_request'
77 elif _current_filter == c.comment_type:
77 elif _current_filter == c.comment_type:
78 c.current_filter = 'comment'
78 c.current_filter = 'comment'
79
79
80 return render('admin/notifications/notifications.html')
80 return render('admin/notifications/notifications.html')
81
81
82 def mark_all_read(self):
82 def mark_all_read(self):
83 if request.environ.get('HTTP_X_PARTIAL_XHR'):
83 if request.environ.get('HTTP_X_PARTIAL_XHR'):
84 nm = NotificationModel()
84 nm = NotificationModel()
85 # mark all read
85 # mark all read
86 nm.mark_all_read_for_user(self.authuser.user_id,
86 nm.mark_all_read_for_user(self.authuser.user_id,
87 filter_=request.GET.getall('type'))
87 filter_=request.GET.getall('type'))
88 Session().commit()
88 Session().commit()
89 c.user = self.authuser
89 c.user = self.authuser
90 notif = nm.query_for_user(self.authuser.user_id,
90 notif = nm.query_for_user(self.authuser.user_id,
91 filter_=request.GET.getall('type'))
91 filter_=request.GET.getall('type'))
92 c.notifications = Page(notif, page=1, items_per_page=10)
92 c.notifications = Page(notif, page=1, items_per_page=10)
93 return render('admin/notifications/notifications_data.html')
93 return render('admin/notifications/notifications_data.html')
94
94
95 def create(self):
95 def create(self):
96 """POST /_admin/notifications: Create a new item"""
96 """POST /_admin/notifications: Create a new item"""
97 # url('notifications')
97 # url('notifications')
98
98
99 def new(self, format='html'):
99 def new(self, format='html'):
100 """GET /_admin/notifications/new: Form to create a new item"""
100 """GET /_admin/notifications/new: Form to create a new item"""
101 # url('new_notification')
101 # url('new_notification')
102
102
103 def update(self, notification_id):
103 def update(self, notification_id):
104 """PUT /_admin/notifications/id: Update an existing item"""
104 """PUT /_admin/notifications/id: Update an existing item"""
105 # Forms posted to this method should contain a hidden field:
105 # Forms posted to this method should contain a hidden field:
106 # <input type="hidden" name="_method" value="PUT" />
106 # <input type="hidden" name="_method" value="PUT" />
107 # Or using helpers:
107 # Or using helpers:
108 # h.form(url('notification', notification_id=ID),
108 # h.form(url('notification', notification_id=ID),
109 # method='put')
109 # method='put')
110 # url('notification', notification_id=ID)
110 # url('notification', notification_id=ID)
111 try:
111 try:
112 no = Notification.get(notification_id)
112 no = Notification.get(notification_id)
113 owner = all(un.user.user_id == c.authuser.user_id
113 owner = all(un.user.user_id == c.authuser.user_id
114 for un in no.notifications_to_users)
114 for un in no.notifications_to_users)
115 if h.HasPermissionAny('hg.admin')() or owner:
115 if h.HasPermissionAny('hg.admin')() or owner:
116 # deletes only notification2user
116 # deletes only notification2user
117 NotificationModel().mark_read(c.authuser.user_id, no)
117 NotificationModel().mark_read(c.authuser.user_id, no)
118 Session().commit()
118 Session().commit()
119 return 'ok'
119 return 'ok'
120 except Exception:
120 except Exception:
121 Session().rollback()
121 Session().rollback()
122 log.error(traceback.format_exc())
122 log.error(traceback.format_exc())
123 raise HTTPBadRequest()
123 raise HTTPBadRequest()
124
124
125 def delete(self, notification_id):
125 def delete(self, notification_id):
126 """DELETE /_admin/notifications/id: Delete an existing item"""
126 """DELETE /_admin/notifications/id: Delete an existing item"""
127 # Forms posted to this method should contain a hidden field:
127 # Forms posted to this method should contain a hidden field:
128 # <input type="hidden" name="_method" value="DELETE" />
128 # <input type="hidden" name="_method" value="DELETE" />
129 # Or using helpers:
129 # Or using helpers:
130 # h.form(url('notification', notification_id=ID),
130 # h.form(url('notification', notification_id=ID),
131 # method='delete')
131 # method='delete')
132 # url('notification', notification_id=ID)
132 # url('notification', notification_id=ID)
133 try:
133 try:
134 no = Notification.get(notification_id)
134 no = Notification.get(notification_id)
135 owner = any(un.user.user_id == c.authuser.user_id
135 owner = any(un.user.user_id == c.authuser.user_id
136 for un in no.notifications_to_users)
136 for un in no.notifications_to_users)
137 if h.HasPermissionAny('hg.admin')() or owner:
137 if h.HasPermissionAny('hg.admin')() or owner:
138 # deletes only notification2user
138 # deletes only notification2user
139 NotificationModel().delete(c.authuser.user_id, no)
139 NotificationModel().delete(c.authuser.user_id, no)
140 Session().commit()
140 Session().commit()
141 return 'ok'
141 return 'ok'
142 except Exception:
142 except Exception:
143 Session().rollback()
143 Session().rollback()
144 log.error(traceback.format_exc())
144 log.error(traceback.format_exc())
145 raise HTTPBadRequest()
145 raise HTTPBadRequest()
146
146
147 def show(self, notification_id, format='html'):
147 def show(self, notification_id, format='html'):
148 """GET /_admin/notifications/id: Show a specific item"""
148 """GET /_admin/notifications/id: Show a specific item"""
149 # url('notification', notification_id=ID)
149 # url('notification', notification_id=ID)
150 notification = Notification.get_or_404(notification_id)
150 notification = Notification.get_or_404(notification_id)
151
151
152 unotification = NotificationModel() \
152 unotification = NotificationModel() \
153 .get_user_notification(self.authuser.user_id, notification)
153 .get_user_notification(self.authuser.user_id, notification)
154
154
155 # if this association to user is not valid, we don't want to show
155 # if this association to user is not valid, we don't want to show
156 # this message
156 # this message
157 if unotification is None:
157 if unotification is None:
158 raise HTTPForbidden()
158 raise HTTPForbidden()
159
159
160 if not unotification.read:
160 if not unotification.read:
161 unotification.mark_as_read()
161 unotification.mark_as_read()
162 Session().commit()
162 Session().commit()
163
163
164 c.notification = notification
164 c.notification = notification
165 c.user = self.authuser
165 c.user = self.authuser
166 return render('admin/notifications/show_notification.html')
166 return render('admin/notifications/show_notification.html')
167
167
168 def edit(self, notification_id, format='html'):
168 def edit(self, notification_id, format='html'):
169 """GET /_admin/notifications/id/edit: Form to edit an existing item"""
169 """GET /_admin/notifications/id/edit: Form to edit an existing item"""
170 # url('edit_notification', notification_id=ID)
170 # url('edit_notification', notification_id=ID)
@@ -1,196 +1,196 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.changelog
15 kallithea.controllers.changelog
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 changelog controller for Kallithea
18 changelog controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 21, 2010
22 :created_on: Apr 21, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30
30
31 from pylons import request, url, session, tmpl_context as c
31 from pylons import request, url, session, tmpl_context as c
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from webob.exc import HTTPFound, HTTPNotFound, HTTPBadRequest
33 from webob.exc import HTTPFound, HTTPNotFound, HTTPBadRequest
34
34
35 import kallithea.lib.helpers as h
35 import kallithea.lib.helpers as h
36 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
36 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 from kallithea.lib.base import BaseRepoController, render
37 from kallithea.lib.base import BaseRepoController, render
38 from kallithea.lib.helpers import RepoPage
38 from kallithea.lib.helpers import RepoPage
39 from kallithea.lib.compat import json
39 from kallithea.lib.compat import json
40 from kallithea.lib.graphmod import graph_data
40 from kallithea.lib.graphmod import graph_data
41 from kallithea.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
41 from kallithea.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
42 ChangesetError, NodeDoesNotExistError, EmptyRepositoryError
42 ChangesetError, NodeDoesNotExistError, EmptyRepositoryError
43 from kallithea.lib.utils2 import safe_int, safe_str
43 from kallithea.lib.utils2 import safe_int, safe_str
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 def _load_changelog_summary():
49 def _load_changelog_summary():
50 p = safe_int(request.GET.get('page'), 1)
50 p = safe_int(request.GET.get('page'), 1)
51 size = safe_int(request.GET.get('size'), 10)
51 size = safe_int(request.GET.get('size'), 10)
52
52
53 def url_generator(**kw):
53 def url_generator(**kw):
54 return url('changelog_summary_home',
54 return url('changelog_summary_home',
55 repo_name=c.db_repo.repo_name, size=size, **kw)
55 repo_name=c.db_repo.repo_name, size=size, **kw)
56
56
57 collection = c.db_repo_scm_instance
57 collection = c.db_repo_scm_instance
58
58
59 c.repo_changesets = RepoPage(collection, page=p,
59 c.repo_changesets = RepoPage(collection, page=p,
60 items_per_page=size,
60 items_per_page=size,
61 url=url_generator)
61 url=url_generator)
62 page_revisions = [x.raw_id for x in list(c.repo_changesets)]
62 page_revisions = [x.raw_id for x in list(c.repo_changesets)]
63 c.comments = c.db_repo.get_comments(page_revisions)
63 c.comments = c.db_repo.get_comments(page_revisions)
64 c.statuses = c.db_repo.statuses(page_revisions)
64 c.statuses = c.db_repo.statuses(page_revisions)
65
65
66
66
67 class ChangelogController(BaseRepoController):
67 class ChangelogController(BaseRepoController):
68
68
69 def __before__(self):
69 def __before__(self):
70 super(ChangelogController, self).__before__()
70 super(ChangelogController, self).__before__()
71 c.affected_files_cut_off = 60
71 c.affected_files_cut_off = 60
72
72
73 @staticmethod
73 @staticmethod
74 def __get_cs(rev, repo):
74 def __get_cs(rev, repo):
75 """
75 """
76 Safe way to get changeset. If error occur fail with error message.
76 Safe way to get changeset. If error occur fail with error message.
77
77
78 :param rev: revision to fetch
78 :param rev: revision to fetch
79 :param repo: repo instance
79 :param repo: repo instance
80 """
80 """
81
81
82 try:
82 try:
83 return c.db_repo_scm_instance.get_changeset(rev)
83 return c.db_repo_scm_instance.get_changeset(rev)
84 except EmptyRepositoryError as e:
84 except EmptyRepositoryError as e:
85 h.flash(h.literal(_('There are no changesets yet')),
85 h.flash(h.literal(_('There are no changesets yet')),
86 category='error')
86 category='error')
87 except RepositoryError as e:
87 except RepositoryError as e:
88 log.error(traceback.format_exc())
88 log.error(traceback.format_exc())
89 h.flash(safe_str(e), category='error')
89 h.flash(safe_str(e), category='error')
90 raise HTTPBadRequest()
90 raise HTTPBadRequest()
91
91
92 @LoginRequired()
92 @LoginRequired()
93 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
93 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
94 'repository.admin')
94 'repository.admin')
95 def index(self, repo_name, revision=None, f_path=None):
95 def index(self, repo_name, revision=None, f_path=None):
96 # Fix URL after page size form submission via GET
96 # Fix URL after page size form submission via GET
97 # TODO: Somehow just don't send this extra junk in the GET URL
97 # TODO: Somehow just don't send this extra junk in the GET URL
98 if request.GET.get('set'):
98 if request.GET.get('set'):
99 request.GET.pop('set', None)
99 request.GET.pop('set', None)
100 if revision is None:
100 if revision is None:
101 raise HTTPFound(location=url('changelog_home', repo_name=repo_name, **request.GET))
101 raise HTTPFound(location=url('changelog_home', repo_name=repo_name, **request.GET))
102 raise HTTPFound(location=url('changelog_file_home', repo_name=repo_name, revision=revision, f_path=f_path, **request.GET))
102 raise HTTPFound(location=url('changelog_file_home', repo_name=repo_name, revision=revision, f_path=f_path, **request.GET))
103
103
104 limit = 2000
104 limit = 2000
105 default = 100
105 default = 100
106 if request.GET.get('size'):
106 if request.GET.get('size'):
107 c.size = max(min(safe_int(request.GET.get('size')), limit), 1)
107 c.size = max(min(safe_int(request.GET.get('size')), limit), 1)
108 session['changelog_size'] = c.size
108 session['changelog_size'] = c.size
109 session.save()
109 session.save()
110 else:
110 else:
111 c.size = int(session.get('changelog_size', default))
111 c.size = int(session.get('changelog_size', default))
112 # min size must be 1
112 # min size must be 1
113 c.size = max(c.size, 1)
113 c.size = max(c.size, 1)
114 p = safe_int(request.GET.get('page', 1), 1)
114 p = safe_int(request.GET.get('page'), 1)
115 branch_name = request.GET.get('branch', None)
115 branch_name = request.GET.get('branch', None)
116 if (branch_name and
116 if (branch_name and
117 branch_name not in c.db_repo_scm_instance.branches and
117 branch_name not in c.db_repo_scm_instance.branches and
118 branch_name not in c.db_repo_scm_instance.closed_branches and
118 branch_name not in c.db_repo_scm_instance.closed_branches and
119 not revision):
119 not revision):
120 raise HTTPFound(location=url('changelog_file_home', repo_name=c.repo_name,
120 raise HTTPFound(location=url('changelog_file_home', repo_name=c.repo_name,
121 revision=branch_name, f_path=f_path or ''))
121 revision=branch_name, f_path=f_path or ''))
122
122
123 if revision == 'tip':
123 if revision == 'tip':
124 revision = None
124 revision = None
125
125
126 c.changelog_for_path = f_path
126 c.changelog_for_path = f_path
127 try:
127 try:
128
128
129 if f_path:
129 if f_path:
130 log.debug('generating changelog for path %s', f_path)
130 log.debug('generating changelog for path %s', f_path)
131 # get the history for the file !
131 # get the history for the file !
132 tip_cs = c.db_repo_scm_instance.get_changeset()
132 tip_cs = c.db_repo_scm_instance.get_changeset()
133 try:
133 try:
134 collection = tip_cs.get_file_history(f_path)
134 collection = tip_cs.get_file_history(f_path)
135 except (NodeDoesNotExistError, ChangesetError):
135 except (NodeDoesNotExistError, ChangesetError):
136 #this node is not present at tip !
136 #this node is not present at tip !
137 try:
137 try:
138 cs = self.__get_cs(revision, repo_name)
138 cs = self.__get_cs(revision, repo_name)
139 collection = cs.get_file_history(f_path)
139 collection = cs.get_file_history(f_path)
140 except RepositoryError as e:
140 except RepositoryError as e:
141 h.flash(safe_str(e), category='warning')
141 h.flash(safe_str(e), category='warning')
142 raise HTTPFound(location=h.url('changelog_home', repo_name=repo_name))
142 raise HTTPFound(location=h.url('changelog_home', repo_name=repo_name))
143 collection = list(reversed(collection))
143 collection = list(reversed(collection))
144 else:
144 else:
145 collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision,
145 collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision,
146 branch_name=branch_name)
146 branch_name=branch_name)
147 c.total_cs = len(collection)
147 c.total_cs = len(collection)
148
148
149 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
149 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
150 items_per_page=c.size, branch=branch_name,)
150 items_per_page=c.size, branch=branch_name,)
151
151
152 page_revisions = [x.raw_id for x in c.pagination]
152 page_revisions = [x.raw_id for x in c.pagination]
153 c.comments = c.db_repo.get_comments(page_revisions)
153 c.comments = c.db_repo.get_comments(page_revisions)
154 c.statuses = c.db_repo.statuses(page_revisions)
154 c.statuses = c.db_repo.statuses(page_revisions)
155 except EmptyRepositoryError as e:
155 except EmptyRepositoryError as e:
156 h.flash(safe_str(e), category='warning')
156 h.flash(safe_str(e), category='warning')
157 raise HTTPFound(location=url('summary_home', repo_name=c.repo_name))
157 raise HTTPFound(location=url('summary_home', repo_name=c.repo_name))
158 except (RepositoryError, ChangesetDoesNotExistError, Exception) as e:
158 except (RepositoryError, ChangesetDoesNotExistError, Exception) as e:
159 log.error(traceback.format_exc())
159 log.error(traceback.format_exc())
160 h.flash(safe_str(e), category='error')
160 h.flash(safe_str(e), category='error')
161 raise HTTPFound(location=url('changelog_home', repo_name=c.repo_name))
161 raise HTTPFound(location=url('changelog_home', repo_name=c.repo_name))
162
162
163 c.branch_name = branch_name
163 c.branch_name = branch_name
164 c.branch_filters = [('', _('None'))] + \
164 c.branch_filters = [('', _('None'))] + \
165 [(k, k) for k in c.db_repo_scm_instance.branches.keys()]
165 [(k, k) for k in c.db_repo_scm_instance.branches.keys()]
166 if c.db_repo_scm_instance.closed_branches:
166 if c.db_repo_scm_instance.closed_branches:
167 prefix = _('(closed)') + ' '
167 prefix = _('(closed)') + ' '
168 c.branch_filters += [('-', '-')] + \
168 c.branch_filters += [('-', '-')] + \
169 [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches.keys()]
169 [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches.keys()]
170 revs = []
170 revs = []
171 if not f_path:
171 if not f_path:
172 revs = [x.revision for x in c.pagination]
172 revs = [x.revision for x in c.pagination]
173 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
173 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
174
174
175 c.revision = revision # requested revision ref
175 c.revision = revision # requested revision ref
176 c.first_revision = c.pagination[0] # pagination is never empty here!
176 c.first_revision = c.pagination[0] # pagination is never empty here!
177 return render('changelog/changelog.html')
177 return render('changelog/changelog.html')
178
178
179 @LoginRequired()
179 @LoginRequired()
180 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
180 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
181 'repository.admin')
181 'repository.admin')
182 def changelog_details(self, cs):
182 def changelog_details(self, cs):
183 if request.environ.get('HTTP_X_PARTIAL_XHR'):
183 if request.environ.get('HTTP_X_PARTIAL_XHR'):
184 c.cs = c.db_repo_scm_instance.get_changeset(cs)
184 c.cs = c.db_repo_scm_instance.get_changeset(cs)
185 return render('changelog/changelog_details.html')
185 return render('changelog/changelog_details.html')
186 raise HTTPNotFound()
186 raise HTTPNotFound()
187
187
188 @LoginRequired()
188 @LoginRequired()
189 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
189 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
190 'repository.admin')
190 'repository.admin')
191 def changelog_summary(self, repo_name):
191 def changelog_summary(self, repo_name):
192 if request.environ.get('HTTP_X_PARTIAL_XHR'):
192 if request.environ.get('HTTP_X_PARTIAL_XHR'):
193 _load_changelog_summary()
193 _load_changelog_summary()
194
194
195 return render('changelog/changelog_summary_data.html')
195 return render('changelog/changelog_summary_data.html')
196 raise HTTPNotFound()
196 raise HTTPNotFound()
@@ -1,59 +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)
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 if request.environ.get('HTTP_X_PARTIAL_XHR'):
56 if request.environ.get('HTTP_X_PARTIAL_XHR'):
57 return render('/followers/followers_data.html')
57 return render('/followers/followers_data.html')
58
58
59 return render('/followers/followers.html')
59 return render('/followers/followers.html')
@@ -1,191 +1,191 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.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound
35 from webob.exc import HTTPFound
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, HasPermissionAny
41 NotAnonymous, HasRepoPermissionAny, HasPermissionAnyDecorator, HasPermissionAny
42 from kallithea.lib.base import BaseRepoController, render
42 from kallithea.lib.base import BaseRepoController, render
43 from kallithea.model.db import Repository, UserFollowing, User, Ui
43 from kallithea.model.db import Repository, UserFollowing, User, Ui
44 from kallithea.model.repo import RepoModel
44 from kallithea.model.repo import RepoModel
45 from kallithea.model.forms import RepoForkForm
45 from kallithea.model.forms import RepoForkForm
46 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices
46 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices
47 from kallithea.lib.utils2 import safe_int
47 from kallithea.lib.utils2 import safe_int
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 class ForksController(BaseRepoController):
52 class ForksController(BaseRepoController):
53
53
54 def __before__(self):
54 def __before__(self):
55 super(ForksController, self).__before__()
55 super(ForksController, self).__before__()
56
56
57 def __load_defaults(self):
57 def __load_defaults(self):
58 repo_group_perms = ['group.admin']
58 repo_group_perms = ['group.admin']
59 if HasPermissionAny('hg.create.write_on_repogroup.true')():
59 if HasPermissionAny('hg.create.write_on_repogroup.true')():
60 repo_group_perms.append('group.write')
60 repo_group_perms.append('group.write')
61 c.repo_groups = AvailableRepoGroupChoices(['hg.create.repository'], repo_group_perms)
61 c.repo_groups = AvailableRepoGroupChoices(['hg.create.repository'], repo_group_perms)
62
62
63 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs()
63 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs()
64
64
65 c.can_update = Ui.get_by_key('hooks', Ui.HOOK_UPDATE).ui_active
65 c.can_update = Ui.get_by_key('hooks', 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 raise HTTPFound(location=url('repos'))
80 raise HTTPFound(location=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)
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 if request.environ.get('HTTP_X_PARTIAL_XHR'):
126 if request.environ.get('HTTP_X_PARTIAL_XHR'):
127 return render('/forks/forks_data.html')
127 return render('/forks/forks_data.html')
128
128
129 return render('/forks/forks.html')
129 return render('/forks/forks.html')
130
130
131 @LoginRequired()
131 @LoginRequired()
132 @NotAnonymous()
132 @NotAnonymous()
133 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
133 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
134 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
134 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
135 'repository.admin')
135 'repository.admin')
136 def fork(self, repo_name):
136 def fork(self, repo_name):
137 c.repo_info = Repository.get_by_repo_name(repo_name)
137 c.repo_info = Repository.get_by_repo_name(repo_name)
138 if not c.repo_info:
138 if not c.repo_info:
139 h.not_mapped_error(repo_name)
139 h.not_mapped_error(repo_name)
140 raise HTTPFound(location=url('home'))
140 raise HTTPFound(location=url('home'))
141
141
142 defaults = self.__load_data(repo_name)
142 defaults = self.__load_data(repo_name)
143
143
144 return htmlfill.render(
144 return htmlfill.render(
145 render('forks/fork.html'),
145 render('forks/fork.html'),
146 defaults=defaults,
146 defaults=defaults,
147 encoding="UTF-8",
147 encoding="UTF-8",
148 force_defaults=False)
148 force_defaults=False)
149
149
150 @LoginRequired()
150 @LoginRequired()
151 @NotAnonymous()
151 @NotAnonymous()
152 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
152 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
153 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
153 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
154 'repository.admin')
154 'repository.admin')
155 def fork_create(self, repo_name):
155 def fork_create(self, repo_name):
156 self.__load_defaults()
156 self.__load_defaults()
157 c.repo_info = Repository.get_by_repo_name(repo_name)
157 c.repo_info = Repository.get_by_repo_name(repo_name)
158 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
158 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
159 repo_groups=c.repo_groups,
159 repo_groups=c.repo_groups,
160 landing_revs=c.landing_revs_choices)()
160 landing_revs=c.landing_revs_choices)()
161 form_result = {}
161 form_result = {}
162 task_id = None
162 task_id = None
163 try:
163 try:
164 form_result = _form.to_python(dict(request.POST))
164 form_result = _form.to_python(dict(request.POST))
165
165
166 # an approximation that is better than nothing
166 # an approximation that is better than nothing
167 if not Ui.get_by_key('hooks', Ui.HOOK_UPDATE).ui_active:
167 if not Ui.get_by_key('hooks', Ui.HOOK_UPDATE).ui_active:
168 form_result['update_after_clone'] = False
168 form_result['update_after_clone'] = False
169
169
170 # create fork is done sometimes async on celery, db transaction
170 # create fork is done sometimes async on celery, db transaction
171 # management is handled there.
171 # management is handled there.
172 task = RepoModel().create_fork(form_result, self.authuser.user_id)
172 task = RepoModel().create_fork(form_result, self.authuser.user_id)
173 from celery.result import BaseAsyncResult
173 from celery.result import BaseAsyncResult
174 if isinstance(task, BaseAsyncResult):
174 if isinstance(task, BaseAsyncResult):
175 task_id = task.task_id
175 task_id = task.task_id
176 except formencode.Invalid as errors:
176 except formencode.Invalid as errors:
177 return htmlfill.render(
177 return htmlfill.render(
178 render('forks/fork.html'),
178 render('forks/fork.html'),
179 defaults=errors.value,
179 defaults=errors.value,
180 errors=errors.error_dict or {},
180 errors=errors.error_dict or {},
181 prefix_error=False,
181 prefix_error=False,
182 encoding="UTF-8",
182 encoding="UTF-8",
183 force_defaults=False)
183 force_defaults=False)
184 except Exception:
184 except Exception:
185 log.error(traceback.format_exc())
185 log.error(traceback.format_exc())
186 h.flash(_('An error occurred during repository forking %s') %
186 h.flash(_('An error occurred during repository forking %s') %
187 repo_name, category='error')
187 repo_name, category='error')
188
188
189 raise HTTPFound(location=h.url('repo_creating_home',
189 raise HTTPFound(location=h.url('repo_creating_home',
190 repo_name=form_result['repo_name_full'],
190 repo_name=form_result['repo_name_full'],
191 task_id=task_id))
191 task_id=task_id))
@@ -1,371 +1,371 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)
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_aggregate = self._get_daily_aggregate(c.journal_pager)
208 c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager)
209
209
210 if request.environ.get('HTTP_X_PARTIAL_XHR'):
210 if request.environ.get('HTTP_X_PARTIAL_XHR'):
211 return render('journal/journal_data.html')
211 return render('journal/journal_data.html')
212
212
213 repos_list = Session().query(Repository) \
213 repos_list = Session().query(Repository) \
214 .filter(Repository.user_id ==
214 .filter(Repository.user_id ==
215 self.authuser.user_id) \
215 self.authuser.user_id) \
216 .order_by(func.lower(Repository.repo_name)).all()
216 .order_by(func.lower(Repository.repo_name)).all()
217
217
218 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
218 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
219 admin=True)
219 admin=True)
220 #json used to render the grid
220 #json used to render the grid
221 c.data = json.dumps(repos_data)
221 c.data = json.dumps(repos_data)
222
222
223 watched_repos_data = []
223 watched_repos_data = []
224
224
225 ## watched repos
225 ## watched repos
226 _render = RepoModel._render_datatable
226 _render = RepoModel._render_datatable
227
227
228 def quick_menu(repo_name):
228 def quick_menu(repo_name):
229 return _render('quick_menu', repo_name)
229 return _render('quick_menu', repo_name)
230
230
231 def repo_lnk(name, rtype, rstate, private, fork_of):
231 def repo_lnk(name, rtype, rstate, private, fork_of):
232 return _render('repo_name', name, rtype, rstate, private, fork_of,
232 return _render('repo_name', name, rtype, rstate, private, fork_of,
233 short_name=False, admin=False)
233 short_name=False, admin=False)
234
234
235 def last_rev(repo_name, cs_cache):
235 def last_rev(repo_name, cs_cache):
236 return _render('revision', repo_name, cs_cache.get('revision'),
236 return _render('revision', repo_name, cs_cache.get('revision'),
237 cs_cache.get('raw_id'), cs_cache.get('author'),
237 cs_cache.get('raw_id'), cs_cache.get('author'),
238 cs_cache.get('message'))
238 cs_cache.get('message'))
239
239
240 def desc(desc):
240 def desc(desc):
241 from pylons import tmpl_context as c
241 from pylons import tmpl_context as c
242 return h.urlify_text(desc, truncate=60, stylize=c.visual.stylify_metatags)
242 return h.urlify_text(desc, truncate=60, stylize=c.visual.stylify_metatags)
243
243
244 def repo_actions(repo_name):
244 def repo_actions(repo_name):
245 return _render('repo_actions', repo_name)
245 return _render('repo_actions', repo_name)
246
246
247 def owner_actions(user_id, username):
247 def owner_actions(user_id, username):
248 return _render('user_name', user_id, username)
248 return _render('user_name', user_id, username)
249
249
250 def toogle_follow(repo_id):
250 def toogle_follow(repo_id):
251 return _render('toggle_follow', repo_id)
251 return _render('toggle_follow', repo_id)
252
252
253 for entry in c.following:
253 for entry in c.following:
254 repo = entry.follows_repository
254 repo = entry.follows_repository
255 cs_cache = repo.changeset_cache
255 cs_cache = repo.changeset_cache
256 row = {
256 row = {
257 "menu": quick_menu(repo.repo_name),
257 "menu": quick_menu(repo.repo_name),
258 "raw_name": repo.repo_name,
258 "raw_name": repo.repo_name,
259 "name": repo_lnk(repo.repo_name, repo.repo_type,
259 "name": repo_lnk(repo.repo_name, repo.repo_type,
260 repo.repo_state, repo.private, repo.fork),
260 repo.repo_state, repo.private, repo.fork),
261 "last_changeset": last_rev(repo.repo_name, cs_cache),
261 "last_changeset": last_rev(repo.repo_name, cs_cache),
262 "last_rev_raw": cs_cache.get('revision'),
262 "last_rev_raw": cs_cache.get('revision'),
263 "action": toogle_follow(repo.repo_id)
263 "action": toogle_follow(repo.repo_id)
264 }
264 }
265
265
266 watched_repos_data.append(row)
266 watched_repos_data.append(row)
267
267
268 c.watched_data = json.dumps({
268 c.watched_data = json.dumps({
269 "totalRecords": len(c.following),
269 "totalRecords": len(c.following),
270 "startIndex": 0,
270 "startIndex": 0,
271 "sort": "name",
271 "sort": "name",
272 "dir": "asc",
272 "dir": "asc",
273 "records": watched_repos_data
273 "records": watched_repos_data
274 })
274 })
275 return render('journal/journal.html')
275 return render('journal/journal.html')
276
276
277 @LoginRequired(api_access=True)
277 @LoginRequired(api_access=True)
278 @NotAnonymous()
278 @NotAnonymous()
279 def journal_atom(self):
279 def journal_atom(self):
280 """
280 """
281 Produce an atom-1.0 feed via feedgenerator module
281 Produce an atom-1.0 feed via feedgenerator module
282 """
282 """
283 following = self.sa.query(UserFollowing) \
283 following = self.sa.query(UserFollowing) \
284 .filter(UserFollowing.user_id == self.authuser.user_id) \
284 .filter(UserFollowing.user_id == self.authuser.user_id) \
285 .options(joinedload(UserFollowing.follows_repository)) \
285 .options(joinedload(UserFollowing.follows_repository)) \
286 .all()
286 .all()
287 return self._atom_feed(following, public=False)
287 return self._atom_feed(following, public=False)
288
288
289 @LoginRequired(api_access=True)
289 @LoginRequired(api_access=True)
290 @NotAnonymous()
290 @NotAnonymous()
291 def journal_rss(self):
291 def journal_rss(self):
292 """
292 """
293 Produce an rss feed via feedgenerator module
293 Produce an rss feed via feedgenerator module
294 """
294 """
295 following = self.sa.query(UserFollowing) \
295 following = self.sa.query(UserFollowing) \
296 .filter(UserFollowing.user_id == self.authuser.user_id) \
296 .filter(UserFollowing.user_id == self.authuser.user_id) \
297 .options(joinedload(UserFollowing.follows_repository)) \
297 .options(joinedload(UserFollowing.follows_repository)) \
298 .all()
298 .all()
299 return self._rss_feed(following, public=False)
299 return self._rss_feed(following, public=False)
300
300
301 @LoginRequired()
301 @LoginRequired()
302 @NotAnonymous()
302 @NotAnonymous()
303 def toggle_following(self):
303 def toggle_following(self):
304 user_id = request.POST.get('follows_user_id')
304 user_id = request.POST.get('follows_user_id')
305 if user_id:
305 if user_id:
306 try:
306 try:
307 self.scm_model.toggle_following_user(user_id,
307 self.scm_model.toggle_following_user(user_id,
308 self.authuser.user_id)
308 self.authuser.user_id)
309 Session.commit()
309 Session.commit()
310 return 'ok'
310 return 'ok'
311 except Exception:
311 except Exception:
312 log.error(traceback.format_exc())
312 log.error(traceback.format_exc())
313 raise HTTPBadRequest()
313 raise HTTPBadRequest()
314
314
315 repo_id = request.POST.get('follows_repo_id')
315 repo_id = request.POST.get('follows_repo_id')
316 if repo_id:
316 if repo_id:
317 try:
317 try:
318 self.scm_model.toggle_following_repo(repo_id,
318 self.scm_model.toggle_following_repo(repo_id,
319 self.authuser.user_id)
319 self.authuser.user_id)
320 Session.commit()
320 Session.commit()
321 return 'ok'
321 return 'ok'
322 except Exception:
322 except Exception:
323 log.error(traceback.format_exc())
323 log.error(traceback.format_exc())
324 raise HTTPBadRequest()
324 raise HTTPBadRequest()
325
325
326 raise HTTPBadRequest()
326 raise HTTPBadRequest()
327
327
328 @LoginRequired()
328 @LoginRequired()
329 def public_journal(self):
329 def public_journal(self):
330 # Return a rendered template
330 # Return a rendered template
331 p = safe_int(request.GET.get('page', 1), 1)
331 p = safe_int(request.GET.get('page'), 1)
332
332
333 c.following = self.sa.query(UserFollowing) \
333 c.following = self.sa.query(UserFollowing) \
334 .filter(UserFollowing.user_id == self.authuser.user_id) \
334 .filter(UserFollowing.user_id == self.authuser.user_id) \
335 .options(joinedload(UserFollowing.follows_repository)) \
335 .options(joinedload(UserFollowing.follows_repository)) \
336 .all()
336 .all()
337
337
338 journal = self._get_journal_data(c.following)
338 journal = self._get_journal_data(c.following)
339
339
340 c.journal_pager = Page(journal, page=p, items_per_page=20)
340 c.journal_pager = Page(journal, page=p, items_per_page=20)
341
341
342 c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager)
342 c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager)
343
343
344 if request.environ.get('HTTP_X_PARTIAL_XHR'):
344 if request.environ.get('HTTP_X_PARTIAL_XHR'):
345 return render('journal/journal_data.html')
345 return render('journal/journal_data.html')
346
346
347 return render('journal/public_journal.html')
347 return render('journal/public_journal.html')
348
348
349 @LoginRequired(api_access=True)
349 @LoginRequired(api_access=True)
350 def public_journal_atom(self):
350 def public_journal_atom(self):
351 """
351 """
352 Produce an atom-1.0 feed via feedgenerator module
352 Produce an atom-1.0 feed via feedgenerator module
353 """
353 """
354 c.following = self.sa.query(UserFollowing) \
354 c.following = self.sa.query(UserFollowing) \
355 .filter(UserFollowing.user_id == self.authuser.user_id) \
355 .filter(UserFollowing.user_id == self.authuser.user_id) \
356 .options(joinedload(UserFollowing.follows_repository)) \
356 .options(joinedload(UserFollowing.follows_repository)) \
357 .all()
357 .all()
358
358
359 return self._atom_feed(c.following)
359 return self._atom_feed(c.following)
360
360
361 @LoginRequired(api_access=True)
361 @LoginRequired(api_access=True)
362 def public_journal_rss(self):
362 def public_journal_rss(self):
363 """
363 """
364 Produce an rss2 feed via feedgenerator module
364 Produce an rss2 feed via feedgenerator module
365 """
365 """
366 c.following = self.sa.query(UserFollowing) \
366 c.following = self.sa.query(UserFollowing) \
367 .filter(UserFollowing.user_id == self.authuser.user_id) \
367 .filter(UserFollowing.user_id == self.authuser.user_id) \
368 .options(joinedload(UserFollowing.follows_repository)) \
368 .options(joinedload(UserFollowing.follows_repository)) \
369 .all()
369 .all()
370
370
371 return self._rss_feed(c.following)
371 return self._rss_feed(c.following)
@@ -1,830 +1,830 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 pylons import request, tmpl_context as c, url
33 from pylons import request, tmpl_context as c, url
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest
35 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest
36
36
37 from kallithea.lib.vcs.utils.hgcompat import unionrepo
37 from kallithea.lib.vcs.utils.hgcompat import unionrepo
38 from kallithea.lib.compat import json
38 from kallithea.lib.compat import json
39 from kallithea.lib.base import BaseRepoController, render
39 from kallithea.lib.base import BaseRepoController, render
40 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
40 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
41 NotAnonymous
41 NotAnonymous
42 from kallithea.lib.helpers import Page
42 from kallithea.lib.helpers import Page
43 from kallithea.lib import helpers as h
43 from kallithea.lib import helpers as h
44 from kallithea.lib import diffs
44 from kallithea.lib import diffs
45 from kallithea.lib.exceptions import UserInvalidException
45 from kallithea.lib.exceptions import UserInvalidException
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, ChangesetDoesNotExistError
48 from kallithea.lib.vcs.exceptions import EmptyRepositoryError, ChangesetDoesNotExistError
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, User
51 PullRequestReviewers, User
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, _context_url, \
59 from kallithea.controllers.changeset import _ignorews_url, _context_url, \
60 create_comment
60 create_comment
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 _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
69 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
70 """return a structure with repo's interesting changesets, suitable for
70 """return a structure with repo's interesting changesets, suitable for
71 the selectors in pullrequest.html
71 the selectors in pullrequest.html
72
72
73 rev: a revision that must be in the list somehow and selected by default
73 rev: a revision that must be in the list somehow and selected by default
74 branch: a branch that must be in the list and selected by default - even if closed
74 branch: a branch that must be in the list and selected by default - even if closed
75 branch_rev: a revision of which peers should be preferred and available."""
75 branch_rev: a revision of which peers should be preferred and available."""
76 # list named branches that has been merged to this named branch - it should probably merge back
76 # list named branches that has been merged to this named branch - it should probably merge back
77 peers = []
77 peers = []
78
78
79 if rev:
79 if rev:
80 rev = safe_str(rev)
80 rev = safe_str(rev)
81
81
82 if branch:
82 if branch:
83 branch = safe_str(branch)
83 branch = safe_str(branch)
84
84
85 if branch_rev:
85 if branch_rev:
86 branch_rev = safe_str(branch_rev)
86 branch_rev = safe_str(branch_rev)
87 # a revset not restricting to merge() would be better
87 # a revset not restricting to merge() would be better
88 # (especially because it would get the branch point)
88 # (especially because it would get the branch point)
89 # ... but is currently too expensive
89 # ... but is currently too expensive
90 # including branches of children could be nice too
90 # including branches of children could be nice too
91 peerbranches = set()
91 peerbranches = set()
92 for i in repo._repo.revs(
92 for i in repo._repo.revs(
93 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
93 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
94 branch_rev, branch_rev):
94 branch_rev, branch_rev):
95 abranch = repo.get_changeset(i).branch
95 abranch = repo.get_changeset(i).branch
96 if abranch not in peerbranches:
96 if abranch not in peerbranches:
97 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
97 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
98 peers.append((n, abranch))
98 peers.append((n, abranch))
99 peerbranches.add(abranch)
99 peerbranches.add(abranch)
100
100
101 selected = None
101 selected = None
102 tiprev = repo.tags.get('tip')
102 tiprev = repo.tags.get('tip')
103 tipbranch = None
103 tipbranch = None
104
104
105 branches = []
105 branches = []
106 for abranch, branchrev in repo.branches.iteritems():
106 for abranch, branchrev in repo.branches.iteritems():
107 n = 'branch:%s:%s' % (abranch, branchrev)
107 n = 'branch:%s:%s' % (abranch, branchrev)
108 desc = abranch
108 desc = abranch
109 if branchrev == tiprev:
109 if branchrev == tiprev:
110 tipbranch = abranch
110 tipbranch = abranch
111 desc = '%s (current tip)' % desc
111 desc = '%s (current tip)' % desc
112 branches.append((n, desc))
112 branches.append((n, desc))
113 if rev == branchrev:
113 if rev == branchrev:
114 selected = n
114 selected = n
115 if branch == abranch:
115 if branch == abranch:
116 if not rev:
116 if not rev:
117 selected = n
117 selected = n
118 branch = None
118 branch = None
119 if branch: # branch not in list - it is probably closed
119 if branch: # branch not in list - it is probably closed
120 branchrev = repo.closed_branches.get(branch)
120 branchrev = repo.closed_branches.get(branch)
121 if branchrev:
121 if branchrev:
122 n = 'branch:%s:%s' % (branch, branchrev)
122 n = 'branch:%s:%s' % (branch, branchrev)
123 branches.append((n, _('%s (closed)') % branch))
123 branches.append((n, _('%s (closed)') % branch))
124 selected = n
124 selected = n
125 branch = None
125 branch = None
126 if branch:
126 if branch:
127 log.debug('branch %r not found in %s', branch, repo)
127 log.debug('branch %r not found in %s', branch, repo)
128
128
129 bookmarks = []
129 bookmarks = []
130 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
130 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
131 n = 'book:%s:%s' % (bookmark, bookmarkrev)
131 n = 'book:%s:%s' % (bookmark, bookmarkrev)
132 bookmarks.append((n, bookmark))
132 bookmarks.append((n, bookmark))
133 if rev == bookmarkrev:
133 if rev == bookmarkrev:
134 selected = n
134 selected = n
135
135
136 tags = []
136 tags = []
137 for tag, tagrev in repo.tags.iteritems():
137 for tag, tagrev in repo.tags.iteritems():
138 if tag == 'tip':
138 if tag == 'tip':
139 continue
139 continue
140 n = 'tag:%s:%s' % (tag, tagrev)
140 n = 'tag:%s:%s' % (tag, tagrev)
141 tags.append((n, tag))
141 tags.append((n, tag))
142 if rev == tagrev:
142 if rev == tagrev:
143 selected = n
143 selected = n
144
144
145 # prio 1: rev was selected as existing entry above
145 # prio 1: rev was selected as existing entry above
146
146
147 # prio 2: create special entry for rev; rev _must_ be used
147 # prio 2: create special entry for rev; rev _must_ be used
148 specials = []
148 specials = []
149 if rev and selected is None:
149 if rev and selected is None:
150 selected = 'rev:%s:%s' % (rev, rev)
150 selected = 'rev:%s:%s' % (rev, rev)
151 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
151 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
152
152
153 # prio 3: most recent peer branch
153 # prio 3: most recent peer branch
154 if peers and not selected:
154 if peers and not selected:
155 selected = peers[0][0]
155 selected = peers[0][0]
156
156
157 # prio 4: tip revision
157 # prio 4: tip revision
158 if not selected:
158 if not selected:
159 if h.is_hg(repo):
159 if h.is_hg(repo):
160 if tipbranch:
160 if tipbranch:
161 selected = 'branch:%s:%s' % (tipbranch, tiprev)
161 selected = 'branch:%s:%s' % (tipbranch, tiprev)
162 else:
162 else:
163 selected = 'tag:null:' + repo.EMPTY_CHANGESET
163 selected = 'tag:null:' + repo.EMPTY_CHANGESET
164 tags.append((selected, 'null'))
164 tags.append((selected, 'null'))
165 else:
165 else:
166 if 'master' in repo.branches:
166 if 'master' in repo.branches:
167 selected = 'branch:master:%s' % repo.branches['master']
167 selected = 'branch:master:%s' % repo.branches['master']
168 else:
168 else:
169 k, v = repo.branches.items()[0]
169 k, v = repo.branches.items()[0]
170 selected = 'branch:%s:%s' % (k, v)
170 selected = 'branch:%s:%s' % (k, v)
171
171
172 groups = [(specials, _("Special")),
172 groups = [(specials, _("Special")),
173 (peers, _("Peer branches")),
173 (peers, _("Peer branches")),
174 (bookmarks, _("Bookmarks")),
174 (bookmarks, _("Bookmarks")),
175 (branches, _("Branches")),
175 (branches, _("Branches")),
176 (tags, _("Tags")),
176 (tags, _("Tags")),
177 ]
177 ]
178 return [g for g in groups if g[0]], selected
178 return [g for g in groups if g[0]], selected
179
179
180 def _get_is_allowed_change_status(self, pull_request):
180 def _get_is_allowed_change_status(self, pull_request):
181 if pull_request.is_closed():
181 if pull_request.is_closed():
182 return False
182 return False
183
183
184 owner = self.authuser.user_id == pull_request.user_id
184 owner = self.authuser.user_id == pull_request.user_id
185 reviewer = PullRequestReviewers.query() \
185 reviewer = PullRequestReviewers.query() \
186 .filter(PullRequestReviewers.pull_request == pull_request) \
186 .filter(PullRequestReviewers.pull_request == pull_request) \
187 .filter(PullRequestReviewers.user_id == self.authuser.user_id) \
187 .filter(PullRequestReviewers.user_id == self.authuser.user_id) \
188 .count() != 0
188 .count() != 0
189
189
190 return self.authuser.admin or owner or reviewer
190 return self.authuser.admin or owner or reviewer
191
191
192 @LoginRequired()
192 @LoginRequired()
193 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
193 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
194 'repository.admin')
194 'repository.admin')
195 def show_all(self, repo_name):
195 def show_all(self, repo_name):
196 c.from_ = request.GET.get('from_') or ''
196 c.from_ = request.GET.get('from_') or ''
197 c.closed = request.GET.get('closed') or ''
197 c.closed = request.GET.get('closed') or ''
198 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
198 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
199 c.repo_name = repo_name
199 c.repo_name = repo_name
200 p = safe_int(request.GET.get('page', 1), 1)
200 p = safe_int(request.GET.get('page'), 1)
201
201
202 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)
202 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)
203
203
204 return render('/pullrequests/pullrequest_show_all.html')
204 return render('/pullrequests/pullrequest_show_all.html')
205
205
206 @LoginRequired()
206 @LoginRequired()
207 @NotAnonymous()
207 @NotAnonymous()
208 def show_my(self):
208 def show_my(self):
209 c.closed = request.GET.get('closed') or ''
209 c.closed = request.GET.get('closed') or ''
210
210
211 def _filter(pr):
211 def _filter(pr):
212 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
212 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
213 if not c.closed:
213 if not c.closed:
214 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
214 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
215 return s
215 return s
216
216
217 c.my_pull_requests = _filter(PullRequest.query() \
217 c.my_pull_requests = _filter(PullRequest.query() \
218 .filter(PullRequest.user_id ==
218 .filter(PullRequest.user_id ==
219 self.authuser.user_id) \
219 self.authuser.user_id) \
220 .all())
220 .all())
221
221
222 c.participate_in_pull_requests = []
222 c.participate_in_pull_requests = []
223 c.participate_in_pull_requests_todo = []
223 c.participate_in_pull_requests_todo = []
224 done_status = set([ChangesetStatus.STATUS_APPROVED, ChangesetStatus.STATUS_REJECTED])
224 done_status = set([ChangesetStatus.STATUS_APPROVED, ChangesetStatus.STATUS_REJECTED])
225 for pr in _filter(PullRequest.query()
225 for pr in _filter(PullRequest.query()
226 .join(PullRequestReviewers)
226 .join(PullRequestReviewers)
227 .filter(PullRequestReviewers.user_id ==
227 .filter(PullRequestReviewers.user_id ==
228 self.authuser.user_id)
228 self.authuser.user_id)
229 ):
229 ):
230 status = pr.user_review_status(c.authuser.user_id) # very inefficient!!!
230 status = pr.user_review_status(c.authuser.user_id) # very inefficient!!!
231 if status in done_status:
231 if status in done_status:
232 c.participate_in_pull_requests.append(pr)
232 c.participate_in_pull_requests.append(pr)
233 else:
233 else:
234 c.participate_in_pull_requests_todo.append(pr)
234 c.participate_in_pull_requests_todo.append(pr)
235
235
236 return render('/pullrequests/pullrequest_show_my.html')
236 return render('/pullrequests/pullrequest_show_my.html')
237
237
238 @LoginRequired()
238 @LoginRequired()
239 @NotAnonymous()
239 @NotAnonymous()
240 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
240 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
241 'repository.admin')
241 'repository.admin')
242 def index(self):
242 def index(self):
243 org_repo = c.db_repo
243 org_repo = c.db_repo
244 org_scm_instance = org_repo.scm_instance
244 org_scm_instance = org_repo.scm_instance
245 try:
245 try:
246 org_scm_instance.get_changeset()
246 org_scm_instance.get_changeset()
247 except EmptyRepositoryError as e:
247 except EmptyRepositoryError as e:
248 h.flash(h.literal(_('There are no changesets yet')),
248 h.flash(h.literal(_('There are no changesets yet')),
249 category='warning')
249 category='warning')
250 raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name))
250 raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name))
251
251
252 org_rev = request.GET.get('rev_end')
252 org_rev = request.GET.get('rev_end')
253 # rev_start is not directly useful - its parent could however be used
253 # rev_start is not directly useful - its parent could however be used
254 # as default for other and thus give a simple compare view
254 # as default for other and thus give a simple compare view
255 rev_start = request.GET.get('rev_start')
255 rev_start = request.GET.get('rev_start')
256 other_rev = None
256 other_rev = None
257 if rev_start:
257 if rev_start:
258 starters = org_repo.get_changeset(rev_start).parents
258 starters = org_repo.get_changeset(rev_start).parents
259 if starters:
259 if starters:
260 other_rev = starters[0].raw_id
260 other_rev = starters[0].raw_id
261 else:
261 else:
262 other_rev = org_repo.scm_instance.EMPTY_CHANGESET
262 other_rev = org_repo.scm_instance.EMPTY_CHANGESET
263 branch = request.GET.get('branch')
263 branch = request.GET.get('branch')
264
264
265 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
265 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
266 c.default_cs_repo = org_repo.repo_name
266 c.default_cs_repo = org_repo.repo_name
267 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
267 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
268
268
269 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
269 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
270 if default_cs_ref_type != 'branch':
270 if default_cs_ref_type != 'branch':
271 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
271 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
272
272
273 # add org repo to other so we can open pull request against peer branches on itself
273 # add org repo to other so we can open pull request against peer branches on itself
274 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
274 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
275
275
276 if org_repo.parent:
276 if org_repo.parent:
277 # add parent of this fork also and select it.
277 # add parent of this fork also and select it.
278 # use the same branch on destination as on source, if available.
278 # use the same branch on destination as on source, if available.
279 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
279 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
280 c.a_repo = org_repo.parent
280 c.a_repo = org_repo.parent
281 c.a_refs, c.default_a_ref = self._get_repo_refs(
281 c.a_refs, c.default_a_ref = self._get_repo_refs(
282 org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)
282 org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)
283
283
284 else:
284 else:
285 c.a_repo = org_repo
285 c.a_repo = org_repo
286 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)
286 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)
287
287
288 # gather forks and add to this list ... even though it is rare to
288 # gather forks and add to this list ... even though it is rare to
289 # request forks to pull from their parent
289 # request forks to pull from their parent
290 for fork in org_repo.forks:
290 for fork in org_repo.forks:
291 c.a_repos.append((fork.repo_name, fork.repo_name))
291 c.a_repos.append((fork.repo_name, fork.repo_name))
292
292
293 return render('/pullrequests/pullrequest.html')
293 return render('/pullrequests/pullrequest.html')
294
294
295 @LoginRequired()
295 @LoginRequired()
296 @NotAnonymous()
296 @NotAnonymous()
297 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
298 'repository.admin')
298 'repository.admin')
299 @jsonify
299 @jsonify
300 def repo_info(self, repo_name):
300 def repo_info(self, repo_name):
301 repo = RepoModel()._get_repo(repo_name)
301 repo = RepoModel()._get_repo(repo_name)
302 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
302 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
303 return {
303 return {
304 'description': repo.description.split('\n', 1)[0],
304 'description': repo.description.split('\n', 1)[0],
305 'selected_ref': selected_ref,
305 'selected_ref': selected_ref,
306 'refs': refs,
306 'refs': refs,
307 }
307 }
308
308
309 @LoginRequired()
309 @LoginRequired()
310 @NotAnonymous()
310 @NotAnonymous()
311 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
311 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
312 'repository.admin')
312 'repository.admin')
313 def create(self, repo_name):
313 def create(self, repo_name):
314 repo = RepoModel()._get_repo(repo_name)
314 repo = RepoModel()._get_repo(repo_name)
315 try:
315 try:
316 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
316 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
317 except formencode.Invalid as errors:
317 except formencode.Invalid as errors:
318 log.error(traceback.format_exc())
318 log.error(traceback.format_exc())
319 log.error(str(errors))
319 log.error(str(errors))
320 msg = _('Error creating pull request: %s') % errors.msg
320 msg = _('Error creating pull request: %s') % errors.msg
321 h.flash(msg, 'error')
321 h.flash(msg, 'error')
322 raise HTTPBadRequest
322 raise HTTPBadRequest
323
323
324 # heads up: org and other might seem backward here ...
324 # heads up: org and other might seem backward here ...
325 org_repo_name = _form['org_repo']
325 org_repo_name = _form['org_repo']
326 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
326 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
327 org_repo = RepoModel()._get_repo(org_repo_name)
327 org_repo = RepoModel()._get_repo(org_repo_name)
328 (org_ref_type,
328 (org_ref_type,
329 org_ref_name,
329 org_ref_name,
330 org_rev) = org_ref.split(':')
330 org_rev) = org_ref.split(':')
331 if org_ref_type == 'rev':
331 if org_ref_type == 'rev':
332 org_ref_type = 'branch'
332 org_ref_type = 'branch'
333 cs = org_repo.scm_instance.get_changeset(org_rev)
333 cs = org_repo.scm_instance.get_changeset(org_rev)
334 org_ref = '%s:%s:%s' % (org_ref_type, cs.branch, cs.raw_id)
334 org_ref = '%s:%s:%s' % (org_ref_type, cs.branch, cs.raw_id)
335
335
336 other_repo_name = _form['other_repo']
336 other_repo_name = _form['other_repo']
337 other_ref = _form['other_ref'] # will have symbolic name and head revision
337 other_ref = _form['other_ref'] # will have symbolic name and head revision
338 other_repo = RepoModel()._get_repo(other_repo_name)
338 other_repo = RepoModel()._get_repo(other_repo_name)
339 (other_ref_type,
339 (other_ref_type,
340 other_ref_name,
340 other_ref_name,
341 other_rev) = other_ref.split(':')
341 other_rev) = other_ref.split(':')
342
342
343 cs_ranges, _cs_ranges_not, ancestor_rev = \
343 cs_ranges, _cs_ranges_not, ancestor_rev = \
344 CompareController._get_changesets(org_repo.scm_instance.alias,
344 CompareController._get_changesets(org_repo.scm_instance.alias,
345 other_repo.scm_instance, other_rev, # org and other "swapped"
345 other_repo.scm_instance, other_rev, # org and other "swapped"
346 org_repo.scm_instance, org_rev,
346 org_repo.scm_instance, org_rev,
347 )
347 )
348 if ancestor_rev is None:
348 if ancestor_rev is None:
349 ancestor_rev = org_repo.scm_instance.EMPTY_CHANGESET
349 ancestor_rev = org_repo.scm_instance.EMPTY_CHANGESET
350 revisions = [cs_.raw_id for cs_ in cs_ranges]
350 revisions = [cs_.raw_id for cs_ in cs_ranges]
351
351
352 # hack: ancestor_rev is not an other_rev but we want to show the
352 # hack: ancestor_rev is not an other_rev but we want to show the
353 # requested destination and have the exact ancestor
353 # requested destination and have the exact ancestor
354 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
354 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
355
355
356 reviewers = _form['review_members']
356 reviewers = _form['review_members']
357
357
358 title = _form['pullrequest_title']
358 title = _form['pullrequest_title']
359 if not title:
359 if not title:
360 if org_repo_name == other_repo_name:
360 if org_repo_name == other_repo_name:
361 title = '%s to %s' % (h.short_ref(org_ref_type, org_ref_name),
361 title = '%s to %s' % (h.short_ref(org_ref_type, org_ref_name),
362 h.short_ref(other_ref_type, other_ref_name))
362 h.short_ref(other_ref_type, other_ref_name))
363 else:
363 else:
364 title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
364 title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
365 other_repo_name, h.short_ref(other_ref_type, other_ref_name))
365 other_repo_name, h.short_ref(other_ref_type, other_ref_name))
366 description = _form['pullrequest_desc'].strip() or _('No description')
366 description = _form['pullrequest_desc'].strip() or _('No description')
367 try:
367 try:
368 pull_request = PullRequestModel().create(
368 pull_request = PullRequestModel().create(
369 self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
369 self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
370 other_ref, revisions, reviewers, title, description
370 other_ref, revisions, reviewers, title, description
371 )
371 )
372 Session().commit()
372 Session().commit()
373 h.flash(_('Successfully opened new pull request'),
373 h.flash(_('Successfully opened new pull request'),
374 category='success')
374 category='success')
375 except UserInvalidException as u:
375 except UserInvalidException as u:
376 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
376 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
377 raise HTTPBadRequest()
377 raise HTTPBadRequest()
378 except Exception:
378 except Exception:
379 h.flash(_('Error occurred while creating pull request'),
379 h.flash(_('Error occurred while creating pull request'),
380 category='error')
380 category='error')
381 log.error(traceback.format_exc())
381 log.error(traceback.format_exc())
382 raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))
382 raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))
383
383
384 raise HTTPFound(location=pull_request.url())
384 raise HTTPFound(location=pull_request.url())
385
385
386 def create_new_iteration(self, old_pull_request, new_rev, title, description, reviewers_ids):
386 def create_new_iteration(self, old_pull_request, new_rev, title, description, reviewers_ids):
387 org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
387 org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
388 org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
388 org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
389 new_org_rev = self._get_ref_rev(org_repo, 'rev', new_rev)
389 new_org_rev = self._get_ref_rev(org_repo, 'rev', new_rev)
390
390
391 other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
391 other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
392 other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
392 other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
393 #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
393 #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
394 new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
394 new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
395
395
396 cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
396 cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
397 other_repo.scm_instance, new_other_rev, # org and other "swapped"
397 other_repo.scm_instance, new_other_rev, # org and other "swapped"
398 org_repo.scm_instance, new_org_rev)
398 org_repo.scm_instance, new_org_rev)
399
399
400 old_revisions = set(old_pull_request.revisions)
400 old_revisions = set(old_pull_request.revisions)
401 revisions = [cs.raw_id for cs in cs_ranges]
401 revisions = [cs.raw_id for cs in cs_ranges]
402 new_revisions = [r for r in revisions if r not in old_revisions]
402 new_revisions = [r for r in revisions if r not in old_revisions]
403 lost = old_revisions.difference(revisions)
403 lost = old_revisions.difference(revisions)
404
404
405 infos = ['This is a new iteration of %s "%s".' %
405 infos = ['This is a new iteration of %s "%s".' %
406 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
406 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
407 pull_request_id=old_pull_request.pull_request_id),
407 pull_request_id=old_pull_request.pull_request_id),
408 old_pull_request.title)]
408 old_pull_request.title)]
409
409
410 if lost:
410 if lost:
411 infos.append(_('Missing changesets since the previous iteration:'))
411 infos.append(_('Missing changesets since the previous iteration:'))
412 for r in old_pull_request.revisions:
412 for r in old_pull_request.revisions:
413 if r in lost:
413 if r in lost:
414 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
414 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
415 infos.append(' %s "%s"' % (h.short_id(r), rev_desc))
415 infos.append(' %s "%s"' % (h.short_id(r), rev_desc))
416
416
417 if new_revisions:
417 if new_revisions:
418 infos.append(_('New changesets on %s %s since the previous iteration:') % (org_ref_type, org_ref_name))
418 infos.append(_('New changesets on %s %s since the previous iteration:') % (org_ref_type, org_ref_name))
419 for r in reversed(revisions):
419 for r in reversed(revisions):
420 if r in new_revisions:
420 if r in new_revisions:
421 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
421 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
422 infos.append(' %s %s' % (h.short_id(r), h.shorter(rev_desc, 80)))
422 infos.append(' %s %s' % (h.short_id(r), h.shorter(rev_desc, 80)))
423
423
424 if ancestor_rev == other_rev:
424 if ancestor_rev == other_rev:
425 infos.append(_("Ancestor didn't change - diff since previous iteration:"))
425 infos.append(_("Ancestor didn't change - diff since previous iteration:"))
426 infos.append(h.canonical_url('compare_url',
426 infos.append(h.canonical_url('compare_url',
427 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
427 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
428 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
428 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
429 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
429 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
430 )) # note: linear diff, merge or not doesn't matter
430 )) # note: linear diff, merge or not doesn't matter
431 else:
431 else:
432 infos.append(_('This iteration is based on another %s revision and there is no simple diff.') % other_ref_name)
432 infos.append(_('This iteration is based on another %s revision and there is no simple diff.') % other_ref_name)
433 else:
433 else:
434 infos.append(_('No changes found on %s %s since previous iteration.') % (org_ref_type, org_ref_name))
434 infos.append(_('No changes found on %s %s since previous iteration.') % (org_ref_type, org_ref_name))
435 # TODO: fail?
435 # TODO: fail?
436
436
437 # hack: ancestor_rev is not an other_ref but we want to show the
437 # hack: ancestor_rev is not an other_ref but we want to show the
438 # requested destination and have the exact ancestor
438 # requested destination and have the exact ancestor
439 new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
439 new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
440 new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
440 new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
441
441
442 try:
442 try:
443 title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', title).groups()
443 title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', title).groups()
444 v = int(old_v) + 1
444 v = int(old_v) + 1
445 except (AttributeError, ValueError):
445 except (AttributeError, ValueError):
446 v = 2
446 v = 2
447 title = '%s (v%s)' % (title.strip(), v)
447 title = '%s (v%s)' % (title.strip(), v)
448
448
449 # using a mail-like separator, insert new iteration info in description with latest first
449 # using a mail-like separator, insert new iteration info in description with latest first
450 descriptions = description.replace('\r\n', '\n').split('\n-- \n', 1)
450 descriptions = description.replace('\r\n', '\n').split('\n-- \n', 1)
451 description = descriptions[0].strip() + '\n\n-- \n' + '\n'.join(infos)
451 description = descriptions[0].strip() + '\n\n-- \n' + '\n'.join(infos)
452 if len(descriptions) > 1:
452 if len(descriptions) > 1:
453 description += '\n\n' + descriptions[1].strip()
453 description += '\n\n' + descriptions[1].strip()
454
454
455 try:
455 try:
456 pull_request = PullRequestModel().create(
456 pull_request = PullRequestModel().create(
457 self.authuser.user_id,
457 self.authuser.user_id,
458 old_pull_request.org_repo.repo_name, new_org_ref,
458 old_pull_request.org_repo.repo_name, new_org_ref,
459 old_pull_request.other_repo.repo_name, new_other_ref,
459 old_pull_request.other_repo.repo_name, new_other_ref,
460 revisions, reviewers_ids, title, description
460 revisions, reviewers_ids, title, description
461 )
461 )
462 except UserInvalidException as u:
462 except UserInvalidException as u:
463 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
463 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
464 raise HTTPBadRequest()
464 raise HTTPBadRequest()
465 except Exception:
465 except Exception:
466 h.flash(_('Error occurred while creating pull request'),
466 h.flash(_('Error occurred while creating pull request'),
467 category='error')
467 category='error')
468 log.error(traceback.format_exc())
468 log.error(traceback.format_exc())
469 raise HTTPFound(location=old_pull_request.url())
469 raise HTTPFound(location=old_pull_request.url())
470
470
471 ChangesetCommentsModel().create(
471 ChangesetCommentsModel().create(
472 text=_('Closed, next iteration: %s .') % pull_request.url(canonical=True),
472 text=_('Closed, next iteration: %s .') % pull_request.url(canonical=True),
473 repo=old_pull_request.other_repo.repo_id,
473 repo=old_pull_request.other_repo.repo_id,
474 user=c.authuser.user_id,
474 user=c.authuser.user_id,
475 pull_request=old_pull_request.pull_request_id,
475 pull_request=old_pull_request.pull_request_id,
476 closing_pr=True)
476 closing_pr=True)
477 PullRequestModel().close_pull_request(old_pull_request.pull_request_id)
477 PullRequestModel().close_pull_request(old_pull_request.pull_request_id)
478
478
479 Session().commit()
479 Session().commit()
480 h.flash(_('New pull request iteration created'),
480 h.flash(_('New pull request iteration created'),
481 category='success')
481 category='success')
482
482
483 raise HTTPFound(location=pull_request.url())
483 raise HTTPFound(location=pull_request.url())
484
484
485 # pullrequest_post for PR editing
485 # pullrequest_post for PR editing
486 @LoginRequired()
486 @LoginRequired()
487 @NotAnonymous()
487 @NotAnonymous()
488 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
488 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
489 'repository.admin')
489 'repository.admin')
490 def post(self, repo_name, pull_request_id):
490 def post(self, repo_name, pull_request_id):
491 pull_request = PullRequest.get_or_404(pull_request_id)
491 pull_request = PullRequest.get_or_404(pull_request_id)
492 if pull_request.is_closed():
492 if pull_request.is_closed():
493 raise HTTPForbidden()
493 raise HTTPForbidden()
494 assert pull_request.other_repo.repo_name == repo_name
494 assert pull_request.other_repo.repo_name == repo_name
495 #only owner or admin can update it
495 #only owner or admin can update it
496 owner = pull_request.owner.user_id == c.authuser.user_id
496 owner = pull_request.owner.user_id == c.authuser.user_id
497 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
497 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
498 if not (h.HasPermissionAny('hg.admin')() or repo_admin or owner):
498 if not (h.HasPermissionAny('hg.admin')() or repo_admin or owner):
499 raise HTTPForbidden()
499 raise HTTPForbidden()
500
500
501 _form = PullRequestPostForm()().to_python(request.POST)
501 _form = PullRequestPostForm()().to_python(request.POST)
502 reviewers_ids = [int(s) for s in _form['review_members']]
502 reviewers_ids = [int(s) for s in _form['review_members']]
503
503
504 if _form['updaterev']:
504 if _form['updaterev']:
505 return self.create_new_iteration(pull_request,
505 return self.create_new_iteration(pull_request,
506 _form['updaterev'],
506 _form['updaterev'],
507 _form['pullrequest_title'],
507 _form['pullrequest_title'],
508 _form['pullrequest_desc'],
508 _form['pullrequest_desc'],
509 reviewers_ids)
509 reviewers_ids)
510
510
511 old_description = pull_request.description
511 old_description = pull_request.description
512 pull_request.title = _form['pullrequest_title']
512 pull_request.title = _form['pullrequest_title']
513 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
513 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
514 pull_request.owner = User.get_by_username(_form['owner'])
514 pull_request.owner = User.get_by_username(_form['owner'])
515 user = User.get(c.authuser.user_id)
515 user = User.get(c.authuser.user_id)
516 try:
516 try:
517 PullRequestModel().mention_from_description(user, pull_request, old_description)
517 PullRequestModel().mention_from_description(user, pull_request, old_description)
518 PullRequestModel().update_reviewers(user, pull_request_id, reviewers_ids)
518 PullRequestModel().update_reviewers(user, pull_request_id, reviewers_ids)
519 except UserInvalidException as u:
519 except UserInvalidException as u:
520 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
520 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
521 raise HTTPBadRequest()
521 raise HTTPBadRequest()
522
522
523 Session().commit()
523 Session().commit()
524 h.flash(_('Pull request updated'), category='success')
524 h.flash(_('Pull request updated'), category='success')
525
525
526 raise HTTPFound(location=pull_request.url())
526 raise HTTPFound(location=pull_request.url())
527
527
528 @LoginRequired()
528 @LoginRequired()
529 @NotAnonymous()
529 @NotAnonymous()
530 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
530 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
531 'repository.admin')
531 'repository.admin')
532 @jsonify
532 @jsonify
533 def delete(self, repo_name, pull_request_id):
533 def delete(self, repo_name, pull_request_id):
534 pull_request = PullRequest.get_or_404(pull_request_id)
534 pull_request = PullRequest.get_or_404(pull_request_id)
535 #only owner can delete it !
535 #only owner can delete it !
536 if pull_request.owner.user_id == c.authuser.user_id:
536 if pull_request.owner.user_id == c.authuser.user_id:
537 PullRequestModel().delete(pull_request)
537 PullRequestModel().delete(pull_request)
538 Session().commit()
538 Session().commit()
539 h.flash(_('Successfully deleted pull request'),
539 h.flash(_('Successfully deleted pull request'),
540 category='success')
540 category='success')
541 raise HTTPFound(location=url('my_pullrequests'))
541 raise HTTPFound(location=url('my_pullrequests'))
542 raise HTTPForbidden()
542 raise HTTPForbidden()
543
543
544 @LoginRequired()
544 @LoginRequired()
545 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
545 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
546 'repository.admin')
546 'repository.admin')
547 def show(self, repo_name, pull_request_id, extra=None):
547 def show(self, repo_name, pull_request_id, extra=None):
548 repo_model = RepoModel()
548 repo_model = RepoModel()
549 c.users_array = repo_model.get_users_js()
549 c.users_array = repo_model.get_users_js()
550 c.user_groups_array = repo_model.get_user_groups_js()
550 c.user_groups_array = repo_model.get_user_groups_js()
551 c.pull_request = PullRequest.get_or_404(pull_request_id)
551 c.pull_request = PullRequest.get_or_404(pull_request_id)
552 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
552 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
553 cc_model = ChangesetCommentsModel()
553 cc_model = ChangesetCommentsModel()
554 cs_model = ChangesetStatusModel()
554 cs_model = ChangesetStatusModel()
555
555
556 # pull_requests repo_name we opened it against
556 # pull_requests repo_name we opened it against
557 # ie. other_repo must match
557 # ie. other_repo must match
558 if repo_name != c.pull_request.other_repo.repo_name:
558 if repo_name != c.pull_request.other_repo.repo_name:
559 raise HTTPNotFound
559 raise HTTPNotFound
560
560
561 # load compare data into template context
561 # load compare data into template context
562 c.cs_repo = c.pull_request.org_repo
562 c.cs_repo = c.pull_request.org_repo
563 (c.cs_ref_type,
563 (c.cs_ref_type,
564 c.cs_ref_name,
564 c.cs_ref_name,
565 c.cs_rev) = c.pull_request.org_ref.split(':')
565 c.cs_rev) = c.pull_request.org_ref.split(':')
566
566
567 c.a_repo = c.pull_request.other_repo
567 c.a_repo = c.pull_request.other_repo
568 (c.a_ref_type,
568 (c.a_ref_type,
569 c.a_ref_name,
569 c.a_ref_name,
570 c.a_rev) = c.pull_request.other_ref.split(':') # other_rev is ancestor
570 c.a_rev) = c.pull_request.other_ref.split(':') # other_rev is ancestor
571
571
572 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
572 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
573 c.cs_repo = c.cs_repo
573 c.cs_repo = c.cs_repo
574 try:
574 try:
575 c.cs_ranges = [org_scm_instance.get_changeset(x)
575 c.cs_ranges = [org_scm_instance.get_changeset(x)
576 for x in c.pull_request.revisions]
576 for x in c.pull_request.revisions]
577 except ChangesetDoesNotExistError:
577 except ChangesetDoesNotExistError:
578 c.cs_ranges = []
578 c.cs_ranges = []
579 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
579 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
580 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
580 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
581 c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
581 c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
582
582
583 c.is_range = False
583 c.is_range = False
584 if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
584 if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
585 cs_a = org_scm_instance.get_changeset(c.a_rev)
585 cs_a = org_scm_instance.get_changeset(c.a_rev)
586 root_parents = c.cs_ranges[0].parents
586 root_parents = c.cs_ranges[0].parents
587 c.is_range = cs_a in root_parents
587 c.is_range = cs_a in root_parents
588 #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
588 #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
589
589
590 avail_revs = set()
590 avail_revs = set()
591 avail_show = []
591 avail_show = []
592 c.cs_branch_name = c.cs_ref_name
592 c.cs_branch_name = c.cs_ref_name
593 other_scm_instance = c.a_repo.scm_instance
593 other_scm_instance = c.a_repo.scm_instance
594 c.update_msg = ""
594 c.update_msg = ""
595 c.update_msg_other = ""
595 c.update_msg_other = ""
596 try:
596 try:
597 if org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
597 if org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
598 if c.cs_ref_type != 'branch':
598 if c.cs_ref_type != 'branch':
599 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
599 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
600 c.a_branch_name = c.a_ref_name
600 c.a_branch_name = c.a_ref_name
601 if c.a_ref_type != 'branch':
601 if c.a_ref_type != 'branch':
602 try:
602 try:
603 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
603 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
604 except EmptyRepositoryError:
604 except EmptyRepositoryError:
605 c.a_branch_name = 'null' # not a branch name ... but close enough
605 c.a_branch_name = 'null' # not a branch name ... but close enough
606 # candidates: descendants of old head that are on the right branch
606 # candidates: descendants of old head that are on the right branch
607 # and not are the old head itself ...
607 # and not are the old head itself ...
608 # and nothing at all if old head is a descendant of target ref name
608 # and nothing at all if old head is a descendant of target ref name
609 if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
609 if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
610 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
610 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
611 elif c.pull_request.is_closed():
611 elif c.pull_request.is_closed():
612 c.update_msg = _('This pull request has been closed and can not be updated.')
612 c.update_msg = _('This pull request has been closed and can not be updated.')
613 else: # look for descendants of PR head on source branch in org repo
613 else: # look for descendants of PR head on source branch in org repo
614 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
614 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
615 revs[0], c.cs_branch_name)
615 revs[0], c.cs_branch_name)
616 if len(avail_revs) > 1: # more than just revs[0]
616 if len(avail_revs) > 1: # more than just revs[0]
617 # also show changesets that not are descendants but would be merged in
617 # also show changesets that not are descendants but would be merged in
618 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
618 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
619 if org_scm_instance.path != other_scm_instance.path:
619 if org_scm_instance.path != other_scm_instance.path:
620 # Note: org_scm_instance.path must come first so all
620 # Note: org_scm_instance.path must come first so all
621 # valid revision numbers are 100% org_scm compatible
621 # valid revision numbers are 100% org_scm compatible
622 # - both for avail_revs and for revset results
622 # - both for avail_revs and for revset results
623 hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
623 hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
624 org_scm_instance.path,
624 org_scm_instance.path,
625 other_scm_instance.path)
625 other_scm_instance.path)
626 else:
626 else:
627 hgrepo = org_scm_instance._repo
627 hgrepo = org_scm_instance._repo
628 show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s',
628 show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s',
629 avail_revs, revs[0], targethead))
629 avail_revs, revs[0], targethead))
630 c.update_msg = _('The following changes are available on %s:') % c.cs_branch_name
630 c.update_msg = _('The following changes are available on %s:') % c.cs_branch_name
631 else:
631 else:
632 show = set()
632 show = set()
633 avail_revs = set() # drop revs[0]
633 avail_revs = set() # drop revs[0]
634 c.update_msg = _('No changesets found for iterating on this pull request.')
634 c.update_msg = _('No changesets found for iterating on this pull request.')
635
635
636 # TODO: handle branch heads that not are tip-most
636 # TODO: handle branch heads that not are tip-most
637 brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0])
637 brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0])
638 if brevs:
638 if brevs:
639 # also show changesets that are on branch but neither ancestors nor descendants
639 # also show changesets that are on branch but neither ancestors nor descendants
640 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
640 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
641 show.add(revs[0]) # make sure graph shows this so we can see how they relate
641 show.add(revs[0]) # make sure graph shows this so we can see how they relate
642 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
642 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
643 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
643 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
644
644
645 avail_show = sorted(show, reverse=True)
645 avail_show = sorted(show, reverse=True)
646
646
647 elif org_scm_instance.alias == 'git':
647 elif org_scm_instance.alias == 'git':
648 c.cs_repo.scm_instance.get_changeset(c.cs_rev) # check it exists - raise ChangesetDoesNotExistError if not
648 c.cs_repo.scm_instance.get_changeset(c.cs_rev) # check it exists - raise ChangesetDoesNotExistError if not
649 c.update_msg = _("Git pull requests don't support iterating yet.")
649 c.update_msg = _("Git pull requests don't support iterating yet.")
650 except ChangesetDoesNotExistError:
650 except ChangesetDoesNotExistError:
651 c.update_msg = _('Error: revision %s was not found. Please create a new pull request!') % c.cs_rev
651 c.update_msg = _('Error: revision %s was not found. Please create a new pull request!') % c.cs_rev
652
652
653 c.avail_revs = avail_revs
653 c.avail_revs = avail_revs
654 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
654 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
655 c.avail_jsdata = json.dumps(graph_data(org_scm_instance, avail_show))
655 c.avail_jsdata = json.dumps(graph_data(org_scm_instance, avail_show))
656
656
657 raw_ids = [x.raw_id for x in c.cs_ranges]
657 raw_ids = [x.raw_id for x in c.cs_ranges]
658 c.cs_comments = c.cs_repo.get_comments(raw_ids)
658 c.cs_comments = c.cs_repo.get_comments(raw_ids)
659 c.statuses = c.cs_repo.statuses(raw_ids)
659 c.statuses = c.cs_repo.statuses(raw_ids)
660
660
661 ignore_whitespace = request.GET.get('ignorews') == '1'
661 ignore_whitespace = request.GET.get('ignorews') == '1'
662 line_context = safe_int(request.GET.get('context'), 3)
662 line_context = safe_int(request.GET.get('context'), 3)
663 c.ignorews_url = _ignorews_url
663 c.ignorews_url = _ignorews_url
664 c.context_url = _context_url
664 c.context_url = _context_url
665 c.fulldiff = request.GET.get('fulldiff')
665 c.fulldiff = request.GET.get('fulldiff')
666 diff_limit = self.cut_off_limit if not c.fulldiff else None
666 diff_limit = self.cut_off_limit if not c.fulldiff else None
667
667
668 # we swap org/other ref since we run a simple diff on one repo
668 # we swap org/other ref since we run a simple diff on one repo
669 log.debug('running diff between %s and %s in %s',
669 log.debug('running diff between %s and %s in %s',
670 c.a_rev, c.cs_rev, org_scm_instance.path)
670 c.a_rev, c.cs_rev, org_scm_instance.path)
671 try:
671 try:
672 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
672 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
673 ignore_whitespace=ignore_whitespace,
673 ignore_whitespace=ignore_whitespace,
674 context=line_context)
674 context=line_context)
675 except ChangesetDoesNotExistError:
675 except ChangesetDoesNotExistError:
676 txtdiff = _("The diff can't be shown - the PR revisions could not be found.")
676 txtdiff = _("The diff can't be shown - the PR revisions could not be found.")
677 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
677 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
678 diff_limit=diff_limit)
678 diff_limit=diff_limit)
679 _parsed = diff_processor.prepare()
679 _parsed = diff_processor.prepare()
680
680
681 c.limited_diff = False
681 c.limited_diff = False
682 if isinstance(_parsed, LimitedDiffContainer):
682 if isinstance(_parsed, LimitedDiffContainer):
683 c.limited_diff = True
683 c.limited_diff = True
684
684
685 c.files = []
685 c.files = []
686 c.changes = {}
686 c.changes = {}
687 c.lines_added = 0
687 c.lines_added = 0
688 c.lines_deleted = 0
688 c.lines_deleted = 0
689
689
690 for f in _parsed:
690 for f in _parsed:
691 st = f['stats']
691 st = f['stats']
692 c.lines_added += st['added']
692 c.lines_added += st['added']
693 c.lines_deleted += st['deleted']
693 c.lines_deleted += st['deleted']
694 fid = h.FID('', f['filename'])
694 fid = h.FID('', f['filename'])
695 c.files.append([fid, f['operation'], f['filename'], f['stats']])
695 c.files.append([fid, f['operation'], f['filename'], f['stats']])
696 htmldiff = diff_processor.as_html(enable_comments=True,
696 htmldiff = diff_processor.as_html(enable_comments=True,
697 parsed_lines=[f])
697 parsed_lines=[f])
698 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
698 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
699
699
700 # inline comments
700 # inline comments
701 c.inline_cnt = 0
701 c.inline_cnt = 0
702 c.inline_comments = cc_model.get_inline_comments(
702 c.inline_comments = cc_model.get_inline_comments(
703 c.db_repo.repo_id,
703 c.db_repo.repo_id,
704 pull_request=pull_request_id)
704 pull_request=pull_request_id)
705 # count inline comments
705 # count inline comments
706 for __, lines in c.inline_comments:
706 for __, lines in c.inline_comments:
707 for comments in lines.values():
707 for comments in lines.values():
708 c.inline_cnt += len(comments)
708 c.inline_cnt += len(comments)
709 # comments
709 # comments
710 c.comments = cc_model.get_comments(c.db_repo.repo_id,
710 c.comments = cc_model.get_comments(c.db_repo.repo_id,
711 pull_request=pull_request_id)
711 pull_request=pull_request_id)
712
712
713 # (badly named) pull-request status calculation based on reviewer votes
713 # (badly named) pull-request status calculation based on reviewer votes
714 (c.pull_request_reviewers,
714 (c.pull_request_reviewers,
715 c.pull_request_pending_reviewers,
715 c.pull_request_pending_reviewers,
716 c.current_voting_result,
716 c.current_voting_result,
717 ) = cs_model.calculate_pull_request_result(c.pull_request)
717 ) = cs_model.calculate_pull_request_result(c.pull_request)
718 c.changeset_statuses = ChangesetStatus.STATUSES
718 c.changeset_statuses = ChangesetStatus.STATUSES
719
719
720 c.as_form = False
720 c.as_form = False
721 c.ancestor = None # there is one - but right here we don't know which
721 c.ancestor = None # there is one - but right here we don't know which
722 return render('/pullrequests/pullrequest_show.html')
722 return render('/pullrequests/pullrequest_show.html')
723
723
724 @LoginRequired()
724 @LoginRequired()
725 @NotAnonymous()
725 @NotAnonymous()
726 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
726 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
727 'repository.admin')
727 'repository.admin')
728 @jsonify
728 @jsonify
729 def comment(self, repo_name, pull_request_id):
729 def comment(self, repo_name, pull_request_id):
730 pull_request = PullRequest.get_or_404(pull_request_id)
730 pull_request = PullRequest.get_or_404(pull_request_id)
731
731
732 status = request.POST.get('changeset_status')
732 status = request.POST.get('changeset_status')
733 close_pr = request.POST.get('save_close')
733 close_pr = request.POST.get('save_close')
734 delete = request.POST.get('save_delete')
734 delete = request.POST.get('save_delete')
735 f_path = request.POST.get('f_path')
735 f_path = request.POST.get('f_path')
736 line_no = request.POST.get('line')
736 line_no = request.POST.get('line')
737
737
738 if (status or close_pr or delete) and (f_path or line_no):
738 if (status or close_pr or delete) and (f_path or line_no):
739 # status votes and closing is only possible in general comments
739 # status votes and closing is only possible in general comments
740 raise HTTPBadRequest()
740 raise HTTPBadRequest()
741
741
742 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
742 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
743 if not allowed_to_change_status:
743 if not allowed_to_change_status:
744 if status or close_pr:
744 if status or close_pr:
745 h.flash(_('No permission to change pull request status'), 'error')
745 h.flash(_('No permission to change pull request status'), 'error')
746 raise HTTPForbidden()
746 raise HTTPForbidden()
747
747
748 if delete == "delete":
748 if delete == "delete":
749 if (pull_request.owner.user_id == c.authuser.user_id or
749 if (pull_request.owner.user_id == c.authuser.user_id or
750 h.HasPermissionAny('hg.admin')() or
750 h.HasPermissionAny('hg.admin')() or
751 h.HasRepoPermissionAny('repository.admin')(pull_request.org_repo.repo_name) or
751 h.HasRepoPermissionAny('repository.admin')(pull_request.org_repo.repo_name) or
752 h.HasRepoPermissionAny('repository.admin')(pull_request.other_repo.repo_name)
752 h.HasRepoPermissionAny('repository.admin')(pull_request.other_repo.repo_name)
753 ) and not pull_request.is_closed():
753 ) and not pull_request.is_closed():
754 PullRequestModel().delete(pull_request)
754 PullRequestModel().delete(pull_request)
755 Session().commit()
755 Session().commit()
756 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
756 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
757 category='success')
757 category='success')
758 return {
758 return {
759 'location': url('my_pullrequests'), # or repo pr list?
759 'location': url('my_pullrequests'), # or repo pr list?
760 }
760 }
761 raise HTTPFound(location=url('my_pullrequests')) # or repo pr list?
761 raise HTTPFound(location=url('my_pullrequests')) # or repo pr list?
762 raise HTTPForbidden()
762 raise HTTPForbidden()
763
763
764 text = request.POST.get('text', '').strip()
764 text = request.POST.get('text', '').strip()
765 if close_pr:
765 if close_pr:
766 text = _('Closing.') + '\n' + text
766 text = _('Closing.') + '\n' + text
767
767
768 comment = create_comment(
768 comment = create_comment(
769 text,
769 text,
770 status,
770 status,
771 pull_request_id=pull_request_id,
771 pull_request_id=pull_request_id,
772 f_path=f_path,
772 f_path=f_path,
773 line_no=line_no,
773 line_no=line_no,
774 closing_pr=close_pr,
774 closing_pr=close_pr,
775 )
775 )
776
776
777 action_logger(self.authuser,
777 action_logger(self.authuser,
778 'user_commented_pull_request:%s' % pull_request_id,
778 'user_commented_pull_request:%s' % pull_request_id,
779 c.db_repo, self.ip_addr, self.sa)
779 c.db_repo, self.ip_addr, self.sa)
780
780
781 if status:
781 if status:
782 ChangesetStatusModel().set_status(
782 ChangesetStatusModel().set_status(
783 c.db_repo.repo_id,
783 c.db_repo.repo_id,
784 status,
784 status,
785 c.authuser.user_id,
785 c.authuser.user_id,
786 comment,
786 comment,
787 pull_request=pull_request_id
787 pull_request=pull_request_id
788 )
788 )
789
789
790 if close_pr:
790 if close_pr:
791 PullRequestModel().close_pull_request(pull_request_id)
791 PullRequestModel().close_pull_request(pull_request_id)
792 action_logger(self.authuser,
792 action_logger(self.authuser,
793 'user_closed_pull_request:%s' % pull_request_id,
793 'user_closed_pull_request:%s' % pull_request_id,
794 c.db_repo, self.ip_addr, self.sa)
794 c.db_repo, self.ip_addr, self.sa)
795
795
796 Session().commit()
796 Session().commit()
797
797
798 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
798 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
799 raise HTTPFound(location=pull_request.url())
799 raise HTTPFound(location=pull_request.url())
800
800
801 data = {
801 data = {
802 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
802 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
803 }
803 }
804 if comment is not None:
804 if comment is not None:
805 c.comment = comment
805 c.comment = comment
806 data.update(comment.get_dict())
806 data.update(comment.get_dict())
807 data.update({'rendered_text':
807 data.update({'rendered_text':
808 render('changeset/changeset_comment_block.html')})
808 render('changeset/changeset_comment_block.html')})
809
809
810 return data
810 return data
811
811
812 @LoginRequired()
812 @LoginRequired()
813 @NotAnonymous()
813 @NotAnonymous()
814 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
814 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
815 'repository.admin')
815 'repository.admin')
816 @jsonify
816 @jsonify
817 def delete_comment(self, repo_name, comment_id):
817 def delete_comment(self, repo_name, comment_id):
818 co = ChangesetComment.get(comment_id)
818 co = ChangesetComment.get(comment_id)
819 if co.pull_request.is_closed():
819 if co.pull_request.is_closed():
820 #don't allow deleting comments on closed pull request
820 #don't allow deleting comments on closed pull request
821 raise HTTPForbidden()
821 raise HTTPForbidden()
822
822
823 owner = co.author.user_id == c.authuser.user_id
823 owner = co.author.user_id == c.authuser.user_id
824 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
824 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
825 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
825 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
826 ChangesetCommentsModel().delete(comment=co)
826 ChangesetCommentsModel().delete(comment=co)
827 Session().commit()
827 Session().commit()
828 return True
828 return True
829 else:
829 else:
830 raise HTTPForbidden()
830 raise HTTPForbidden()
@@ -1,147 +1,147 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.search
15 kallithea.controllers.search
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Search controller for Kallithea
18 Search 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: Aug 7, 2010
22 :created_on: Aug 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 import logging
28 import logging
29 import traceback
29 import traceback
30 import urllib
30 import urllib
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons import request, config, tmpl_context as c
32 from pylons import request, config, tmpl_context as c
33
33
34 from whoosh.index import open_dir, EmptyIndexError
34 from whoosh.index import open_dir, EmptyIndexError
35 from whoosh.qparser import QueryParser, QueryParserError
35 from whoosh.qparser import QueryParser, QueryParserError
36 from whoosh.query import Phrase, Prefix
36 from whoosh.query import Phrase, Prefix
37 from webhelpers.util import update_params
37 from webhelpers.util import update_params
38
38
39 from kallithea.lib.auth import LoginRequired
39 from kallithea.lib.auth import LoginRequired
40 from kallithea.lib.base import BaseRepoController, render
40 from kallithea.lib.base import BaseRepoController, render
41 from kallithea.lib.indexers import CHGSETS_SCHEMA, SCHEMA, CHGSET_IDX_NAME, \
41 from kallithea.lib.indexers import CHGSETS_SCHEMA, SCHEMA, CHGSET_IDX_NAME, \
42 IDX_NAME, WhooshResultWrapper
42 IDX_NAME, WhooshResultWrapper
43 from kallithea.model.repo import RepoModel
43 from kallithea.model.repo import RepoModel
44 from kallithea.lib.utils2 import safe_str, safe_int
44 from kallithea.lib.utils2 import safe_str, safe_int
45 from kallithea.lib.helpers import Page
45 from kallithea.lib.helpers import Page
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 class SearchController(BaseRepoController):
50 class SearchController(BaseRepoController):
51
51
52 def __before__(self):
52 def __before__(self):
53 super(SearchController, self).__before__()
53 super(SearchController, self).__before__()
54
54
55 @LoginRequired()
55 @LoginRequired()
56 def index(self, repo_name=None):
56 def index(self, repo_name=None):
57 c.repo_name = repo_name
57 c.repo_name = repo_name
58 c.formated_results = []
58 c.formated_results = []
59 c.runtime = ''
59 c.runtime = ''
60 c.cur_query = request.GET.get('q', None)
60 c.cur_query = request.GET.get('q', None)
61 c.cur_type = request.GET.get('type', 'content')
61 c.cur_type = request.GET.get('type', 'content')
62 c.cur_search = search_type = {'content': 'content',
62 c.cur_search = search_type = {'content': 'content',
63 'commit': 'message',
63 'commit': 'message',
64 'path': 'path',
64 'path': 'path',
65 'repository': 'repository'
65 'repository': 'repository'
66 }.get(c.cur_type, 'content')
66 }.get(c.cur_type, 'content')
67
67
68 index_name = {
68 index_name = {
69 'content': IDX_NAME,
69 'content': IDX_NAME,
70 'commit': CHGSET_IDX_NAME,
70 'commit': CHGSET_IDX_NAME,
71 'path': IDX_NAME
71 'path': IDX_NAME
72 }.get(c.cur_type, IDX_NAME)
72 }.get(c.cur_type, IDX_NAME)
73
73
74 schema_defn = {
74 schema_defn = {
75 'content': SCHEMA,
75 'content': SCHEMA,
76 'commit': CHGSETS_SCHEMA,
76 'commit': CHGSETS_SCHEMA,
77 'path': SCHEMA
77 'path': SCHEMA
78 }.get(c.cur_type, SCHEMA)
78 }.get(c.cur_type, SCHEMA)
79
79
80 log.debug('IDX: %s', index_name)
80 log.debug('IDX: %s', index_name)
81 log.debug('SCHEMA: %s', schema_defn)
81 log.debug('SCHEMA: %s', schema_defn)
82
82
83 if c.cur_query:
83 if c.cur_query:
84 cur_query = c.cur_query.lower()
84 cur_query = c.cur_query.lower()
85 log.debug(cur_query)
85 log.debug(cur_query)
86
86
87 if c.cur_query:
87 if c.cur_query:
88 p = safe_int(request.GET.get('page', 1), 1)
88 p = safe_int(request.GET.get('page'), 1)
89 highlight_items = set()
89 highlight_items = set()
90 try:
90 try:
91 idx = open_dir(config['app_conf']['index_dir'],
91 idx = open_dir(config['app_conf']['index_dir'],
92 indexname=index_name)
92 indexname=index_name)
93 searcher = idx.searcher()
93 searcher = idx.searcher()
94
94
95 qp = QueryParser(search_type, schema=schema_defn)
95 qp = QueryParser(search_type, schema=schema_defn)
96 if c.repo_name:
96 if c.repo_name:
97 cur_query = u'repository:%s %s' % (c.repo_name, cur_query)
97 cur_query = u'repository:%s %s' % (c.repo_name, cur_query)
98 try:
98 try:
99 query = qp.parse(unicode(cur_query))
99 query = qp.parse(unicode(cur_query))
100 # extract words for highlight
100 # extract words for highlight
101 if isinstance(query, Phrase):
101 if isinstance(query, Phrase):
102 highlight_items.update(query.words)
102 highlight_items.update(query.words)
103 elif isinstance(query, Prefix):
103 elif isinstance(query, Prefix):
104 highlight_items.add(query.text)
104 highlight_items.add(query.text)
105 else:
105 else:
106 for i in query.all_terms():
106 for i in query.all_terms():
107 if i[0] in ['content', 'message']:
107 if i[0] in ['content', 'message']:
108 highlight_items.add(i[1])
108 highlight_items.add(i[1])
109
109
110 matcher = query.matcher(searcher)
110 matcher = query.matcher(searcher)
111
111
112 log.debug('query: %s', query)
112 log.debug('query: %s', query)
113 log.debug('hl terms: %s', highlight_items)
113 log.debug('hl terms: %s', highlight_items)
114 results = searcher.search(query)
114 results = searcher.search(query)
115 res_ln = len(results)
115 res_ln = len(results)
116 c.runtime = '%s results (%.3f seconds)' % (
116 c.runtime = '%s results (%.3f seconds)' % (
117 res_ln, results.runtime
117 res_ln, results.runtime
118 )
118 )
119
119
120 def url_generator(**kw):
120 def url_generator(**kw):
121 q = urllib.quote(safe_str(c.cur_query))
121 q = urllib.quote(safe_str(c.cur_query))
122 return update_params("?q=%s&type=%s" \
122 return update_params("?q=%s&type=%s" \
123 % (q, safe_str(c.cur_type)), **kw)
123 % (q, safe_str(c.cur_type)), **kw)
124 repo_location = RepoModel().repos_path
124 repo_location = RepoModel().repos_path
125 c.formated_results = Page(
125 c.formated_results = Page(
126 WhooshResultWrapper(search_type, searcher, matcher,
126 WhooshResultWrapper(search_type, searcher, matcher,
127 highlight_items, repo_location),
127 highlight_items, repo_location),
128 page=p,
128 page=p,
129 item_count=res_ln,
129 item_count=res_ln,
130 items_per_page=10,
130 items_per_page=10,
131 url=url_generator
131 url=url_generator
132 )
132 )
133
133
134 except QueryParserError:
134 except QueryParserError:
135 c.runtime = _('Invalid search query. Try quoting it.')
135 c.runtime = _('Invalid search query. Try quoting it.')
136 searcher.close()
136 searcher.close()
137 except (EmptyIndexError, IOError):
137 except (EmptyIndexError, IOError):
138 log.error(traceback.format_exc())
138 log.error(traceback.format_exc())
139 log.error('Empty Index data')
139 log.error('Empty Index data')
140 c.runtime = _('There is no index to search in. '
140 c.runtime = _('There is no index to search in. '
141 'Please run whoosh indexer')
141 'Please run whoosh indexer')
142 except Exception:
142 except Exception:
143 log.error(traceback.format_exc())
143 log.error(traceback.format_exc())
144 c.runtime = _('An error occurred during search operation.')
144 c.runtime = _('An error occurred during search operation.')
145
145
146 # Return a rendered template
146 # Return a rendered template
147 return render('/search/search.html')
147 return render('/search/search.html')
General Comments 0
You need to be logged in to leave comments. Login now