##// END OF EJS Templates
docs update
marcink -
r1839:9da24750 beta
parent child Browse files
Show More
@@ -1,150 +1,149 b''
1 =================================================
1 ========================
2 Welcome to RhodeCode (RhodiumCode) documentation!
2 RhodeCode documentation!
3 =================================================
3 ========================
4
4
5 ``RhodeCode`` is a fast and powerful management tool for Mercurial_ and GIT_
5 ``RhodeCode`` is a fast and powerful management tool for Mercurial_ and GIT_
6 with a built in push/pull server and full text search.
6 with a built in push/pull server and full text search.
7 It works on http/https and has a built in permission/authentication system with
7 It works on http/https and has a built in permission/authentication system with
8 the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also supports
8 the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also supports
9 simple API so it's easy integrable with existing external systems.
9 simple API so it's easy integrable with existing external systems.
10
10
11 RhodeCode is similar in some respects to github or bitbucket_,
11 RhodeCode is similar in some respects to github or bitbucket_,
12 however RhodeCode can be run as standalone hosted application on your own server.
12 however RhodeCode can be run as standalone hosted application on your own server.
13 It is open source and donation ware and focuses more on providing a customized,
13 It is open source and donation ware and focuses more on providing a customized,
14 self administered interface for Mercurial and GIT repositories.
14 self administered interface for Mercurial and GIT repositories.
15 RhodeCode is powered by a vcs_ library that Lukasz Balcerzak and I created to
15 RhodeCode is powered by a vcs_ library that Lukasz Balcerzak and I created to
16 handle multiple different version control systems.
16 handle multiple different version control systems.
17
17
18 RhodeCode uses `Semantic Versioning <http://semver.org/>`_
18 RhodeCode uses `Semantic Versioning <http://semver.org/>`_
19
19
20 RhodeCode demo
20 RhodeCode demo
21 --------------
21 --------------
22
22
23 http://demo.rhodecode.org
23 http://demo.rhodecode.org
24
24
25 The default access is anonymous but you can login to an administrative account
25 The default access is anonymous but you can login to an administrative account
26 using the following credentials:
26 using the following credentials:
27
27
28 - username: demo
28 - username: demo
29 - password: demo12
29 - password: demo12
30
30
31 Source code
31 Source code
32 -----------
32 -----------
33
33
34 The latest sources can be obtained from official RhodeCode instance
34 The latest sources can be obtained from official RhodeCode instance
35 https://secure.rhodecode.org
35 https://secure.rhodecode.org
36
36
37
37
38 MIRRORS:
38 MIRRORS:
39
39
40 Issue tracker and sources at bitbucket_
40 Issue tracker and sources at bitbucket_
41
41
42 http://bitbucket.org/marcinkuzminski/rhodecode
42 http://bitbucket.org/marcinkuzminski/rhodecode
43
43
44 Sources at github_
44 Sources at github_
45
45
46 https://github.com/marcinkuzminski/rhodecode
46 https://github.com/marcinkuzminski/rhodecode
47
47
48 Installation
48 Installation
49 ------------
49 ------------
50
50
51 Please visit http://packages.python.org/RhodeCode/installation.html
51 Please visit http://packages.python.org/RhodeCode/installation.html
52
52
53
53
54 RhodeCode Features
54 RhodeCode Features
55 ------------------
55 ------------------
56
56
57 - Has its own middleware to handle mercurial_ protocol requests.
57 - Has its own middleware to handle mercurial_ protocol requests.
58 Each request can be logged and authenticated.
58 Each request can be logged and authenticated.
59 - Runs on threads unlike hgweb. You can make multiple pulls/pushes simultaneous.
59 - Runs on threads unlike hgweb. You can make multiple pulls/pushes simultaneous.
60 Supports http/https and LDAP
60 Supports http/https and LDAP
61 - Full permissions (private/read/write/admin) and authentication per project.
61 - Full permissions (private/read/write/admin) and authentication per project.
62 One account for web interface and mercurial_ push/pull/clone operations.
62 One account for web interface and mercurial_ push/pull/clone operations.
63 - Have built in users groups for easier permission management
63 - Have built in users groups for easier permission management
64 - Repository groups let you group repos and manage them easier.
64 - Repository groups let you group repos and manage them easier.
65 - Users can fork other users repo. RhodeCode have also compare view to see
65 - Users can fork other users repo. RhodeCode have also compare view to see
66 combined changeset for all changeset made within single push.
66 combined changeset for all changeset made within single push.
67 - Build in commit-api let's you add, edit and commit files right from RhodeCode
67 - Build in commit-api let's you add, edit and commit files right from RhodeCode
68 interface using simple editor or upload form for binaries.
68 interface using simple editor or upload form for binaries.
69 - Mako templates let's you customize the look and feel of the application.
69 - Mako templates let's you customize the look and feel of the application.
70 - Beautiful diffs, annotations and source code browsing all colored by pygments.
70 - Beautiful diffs, annotations and source code browsing all colored by pygments.
71 Raw diffs are made in git-diff format, including git_ binary-patches
71 Raw diffs are made in git-diff format, including git_ binary-patches
72 - Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics
72 - Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics
73 - Admin interface with user/permission management. Admin activity journal, logs
73 - Admin interface with user/permission management. Admin activity journal, logs
74 pulls, pushes, forks, registrations and other actions made by all users.
74 pulls, pushes, forks, registrations and other actions made by all users.
75 - Server side forks. It is possible to fork a project and modify it freely
75 - Server side forks. It is possible to fork a project and modify it freely
76 without breaking the main repository. You can even write Your own hooks
76 without breaking the main repository. You can even write Your own hooks
77 and install them
77 and install them
78 - code review with notification system, inline commenting, all parsed using
78 - code review with notification system, inline commenting, all parsed using
79 rst syntax
79 rst syntax
80 - rst and markdown README support for repositories
80 - rst and markdown README support for repositories
81 - Full text search powered by Whoosh on the source files, and file names.
81 - Full text search powered by Whoosh on the source files, and file names.
82 Build in indexing daemons, with optional incremental index build
82 Build in indexing daemons, with optional incremental index build
83 (no external search servers required all in one application)
83 (no external search servers required all in one application)
84 - Setup project descriptions and info inside built in db for easy, non
84 - Setup project descriptions and info inside built in db for easy, non
85 file-system operations
85 file-system operations
86 - Intelligent cache with invalidation after push or project change, provides
86 - Intelligent cache with invalidation after push or project change, provides
87 high performance and always up to date data.
87 high performance and always up to date data.
88 - Rss / atom feeds, gravatar support, download sources as zip/tar/gz
88 - Rss / atom feeds, gravatar support, download sources as zip/tar/gz
89 - Async tasks for speed and performance using celery_ (works without them too)
89 - Async tasks for speed and performance using celery_ (works without them too)
90 - Backup scripts can do backup of whole app and send it over scp to desired
90 - Backup scripts can do backup of whole app and send it over scp to desired
91 location
91 location
92 - Based on pylons / sqlalchemy / sqlite / whoosh / vcs
92 - Based on pylons / sqlalchemy / sqlite / whoosh / vcs
93
93
94
94
95 .. include:: ./docs/screenshots.rst
95 .. include:: ./docs/screenshots.rst
96
96
97
97
98 Incoming / Plans
98 Incoming / Plans
99 ----------------
99 ----------------
100
100
101 - Finer granular permissions per branch, repo group or subrepo
101 - Finer granular permissions per branch, repo group or subrepo
102 - pull requests and web based merges
102 - pull requests and web based merges
103 - per line file history
103 - per line file history
104 - SSH based authentication with server side key management
104 - SSH based authentication with server side key management
105 - Redmine and other bugtrackers integration
106 - Commit based built in wiki system
105 - Commit based built in wiki system
107 - More statistics and graph (global annotation + some more statistics)
106 - More statistics and graph (global annotation + some more statistics)
108 - Other advancements as development continues (or you can of course make
107 - Other advancements as development continues (or you can of course make
109 additions and or requests)
108 additions and or requests)
110
109
111 License
110 License
112 -------
111 -------
113
112
114 ``RhodeCode`` is released under the GPLv3 license.
113 ``RhodeCode`` is released under the GPLv3 license.
115
114
116
115
117 Mailing group Q&A
116 Mailing group Q&A
118 -----------------
117 -----------------
119
118
120 Join the `Google group <http://groups.google.com/group/rhodecode>`_
119 Join the `Google group <http://groups.google.com/group/rhodecode>`_
121
120
122 Open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
121 Open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
123
122
124 Join #rhodecode on FreeNode (irc.freenode.net)
123 Join #rhodecode on FreeNode (irc.freenode.net)
125 or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
124 or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
126
125
127 Online documentation
126 Online documentation
128 --------------------
127 --------------------
129
128
130 Online documentation for the current version of RhodeCode is available at
129 Online documentation for the current version of RhodeCode is available at
131 http://packages.python.org/RhodeCode/.
130 http://packages.python.org/RhodeCode/.
132 You may also build the documentation for yourself - go into ``docs/`` and run::
131 You may also build the documentation for yourself - go into ``docs/`` and run::
133
132
134 make html
133 make html
135
134
136 (You need to have sphinx_ installed to build the documentation. If you don't
135 (You need to have sphinx_ installed to build the documentation. If you don't
137 have sphinx_ installed you can install it via the command:
136 have sphinx_ installed you can install it via the command:
138 ``easy_install sphinx``)
137 ``easy_install sphinx``)
139
138
140 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
139 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
141 .. _python: http://www.python.org/
140 .. _python: http://www.python.org/
142 .. _sphinx: http://sphinx.pocoo.org/
141 .. _sphinx: http://sphinx.pocoo.org/
143 .. _mercurial: http://mercurial.selenic.com/
142 .. _mercurial: http://mercurial.selenic.com/
144 .. _bitbucket: http://bitbucket.org/
143 .. _bitbucket: http://bitbucket.org/
145 .. _github: http://github.com/
144 .. _github: http://github.com/
146 .. _subversion: http://subversion.tigris.org/
145 .. _subversion: http://subversion.tigris.org/
147 .. _git: http://git-scm.com/
146 .. _git: http://git-scm.com/
148 .. _celery: http://celeryproject.org/
147 .. _celery: http://celeryproject.org/
149 .. _Sphinx: http://sphinx.pocoo.org/
148 .. _Sphinx: http://sphinx.pocoo.org/
150 .. _vcs: http://pypi.python.org/pypi/vcs No newline at end of file
149 .. _vcs: http://pypi.python.org/pypi/vcs
@@ -1,421 +1,426 b''
1 .. _api:
1 .. _api:
2
2
3
3
4 API
4 API
5 ===
5 ===
6
6
7
7
8 Starting from RhodeCode version 1.2 a simple API was implemented.
8 Starting from RhodeCode version 1.2 a simple API was implemented.
9 There's a single schema for calling all api methods. API is implemented
9 There's a single schema for calling all api methods. API is implemented
10 with JSON protocol both ways. An url to send API request in RhodeCode is
10 with JSON protocol both ways. An url to send API request in RhodeCode is
11 <your_server>/_admin/api
11 <your_server>/_admin/api
12
12
13 API ACCESS FOR WEB VIEWS
14 ++++++++++++++++++++++++
13
15
14 API access can also be turned on for each view decorated with `@LoginRequired`
16 API access can also be turned on for each view decorated with `@LoginRequired`
15 decorator. To enable API access simple change standard login decorator into
17 decorator. To enable API access simple change standard login decorator into
16 `@LoginRequired(api_access=True)`. After such a change view can be accessed
18 `@LoginRequired(api_access=True)`. After such a change view can be accessed
17 by adding a GET parameter to url `?api_key=<api_key>`. By default it's only
19 by adding a GET parameter to url `?api_key=<api_key>`. By default it's only
18 enabled on RSS/ATOM feed views.
20 enabled on RSS/ATOM feed views.
19
21
20
22
23 API ACCESS
24 ++++++++++
25
21 All clients are required to send JSON-RPC spec JSON data::
26 All clients are required to send JSON-RPC spec JSON data::
22
27
23 {
28 {
24 "id:<id>,
29 "id:<id>,
25 "api_key":"<api_key>",
30 "api_key":"<api_key>",
26 "method":"<method_name>",
31 "method":"<method_name>",
27 "args":{"<arg_key>":"<arg_val>"}
32 "args":{"<arg_key>":"<arg_val>"}
28 }
33 }
29
34
30 Example call for autopulling remotes repos using curl::
35 Example call for autopulling remotes repos using curl::
31 curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"id":1,"api_key":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull","args":{"repo":"CPython"}}'
36 curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"id":1,"api_key":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull","args":{"repo":"CPython"}}'
32
37
33 Simply provide
38 Simply provide
34 - *id* A value of any type, which is used to match the response with the request that it is replying to.
39 - *id* A value of any type, which is used to match the response with the request that it is replying to.
35 - *api_key* for access and permission validation.
40 - *api_key* for access and permission validation.
36 - *method* is name of method to call
41 - *method* is name of method to call
37 - *args* is an key:value list of arguments to pass to method
42 - *args* is an key:value list of arguments to pass to method
38
43
39 .. note::
44 .. note::
40
45
41 api_key can be found in your user account page
46 api_key can be found in your user account page
42
47
43
48
44 RhodeCode API will return always a JSON-RPC response::
49 RhodeCode API will return always a JSON-RPC response::
45
50
46 {
51 {
47 "id":<id>,
52 "id":<id>,
48 "result": "<result>",
53 "result": "<result>",
49 "error": null
54 "error": null
50 }
55 }
51
56
52 All responses from API will be `HTTP/1.0 200 OK`, if there's an error while
57 All responses from API will be `HTTP/1.0 200 OK`, if there's an error while
53 calling api *error* key from response will contain failure description
58 calling api *error* key from response will contain failure description
54 and result will be null.
59 and result will be null.
55
60
56 API METHODS
61 API METHODS
57 +++++++++++
62 +++++++++++
58
63
59
64
60 pull
65 pull
61 ----
66 ----
62
67
63 Pulls given repo from remote location. Can be used to automatically keep
68 Pulls given repo from remote location. Can be used to automatically keep
64 remote repos up to date. This command can be executed only using api_key
69 remote repos up to date. This command can be executed only using api_key
65 belonging to user with admin rights
70 belonging to user with admin rights
66
71
67 INPUT::
72 INPUT::
68
73
69 api_key : "<api_key>"
74 api_key : "<api_key>"
70 method : "pull"
75 method : "pull"
71 args : {
76 args : {
72 "repo" : "<repo_name>"
77 "repo" : "<repo_name>"
73 }
78 }
74
79
75 OUTPUT::
80 OUTPUT::
76
81
77 result : "Pulled from <repo_name>"
82 result : "Pulled from <repo_name>"
78 error : null
83 error : null
79
84
80
85
81 get_users
86 get_users
82 ---------
87 ---------
83
88
84 Lists all existing users. This command can be executed only using api_key
89 Lists all existing users. This command can be executed only using api_key
85 belonging to user with admin rights.
90 belonging to user with admin rights.
86
91
87 INPUT::
92 INPUT::
88
93
89 api_key : "<api_key>"
94 api_key : "<api_key>"
90 method : "get_users"
95 method : "get_users"
91 args : { }
96 args : { }
92
97
93 OUTPUT::
98 OUTPUT::
94
99
95 result: [
100 result: [
96 {
101 {
97 "id" : "<id>",
102 "id" : "<id>",
98 "username" : "<username>",
103 "username" : "<username>",
99 "firstname": "<firstname>",
104 "firstname": "<firstname>",
100 "lastname" : "<lastname>",
105 "lastname" : "<lastname>",
101 "email" : "<email>",
106 "email" : "<email>",
102 "active" : "<bool>",
107 "active" : "<bool>",
103 "admin" :  "<bool>",
108 "admin" :  "<bool>",
104 "ldap" : "<ldap_dn>"
109 "ldap" : "<ldap_dn>"
105 },
110 },
106
111
107 ]
112 ]
108 error: null
113 error: null
109
114
110 create_user
115 create_user
111 -----------
116 -----------
112
117
113 Creates new user in RhodeCode. This command can be executed only using api_key
118 Creates new user in RhodeCode. This command can be executed only using api_key
114 belonging to user with admin rights.
119 belonging to user with admin rights.
115
120
116 INPUT::
121 INPUT::
117
122
118 api_key : "<api_key>"
123 api_key : "<api_key>"
119 method : "create_user"
124 method : "create_user"
120 args : {
125 args : {
121 "username" : "<username>",
126 "username" : "<username>",
122 "password" : "<password>",
127 "password" : "<password>",
123 "firstname" : "<firstname>",
128 "firstname" : "<firstname>",
124 "lastname" : "<lastname>",
129 "lastname" : "<lastname>",
125 "email" : "<useremail>"
130 "email" : "<useremail>"
126 "active" : "<bool> = True",
131 "active" : "<bool> = True",
127 "admin" : "<bool> = False",
132 "admin" : "<bool> = False",
128 "ldap_dn" : "<ldap_dn> = None"
133 "ldap_dn" : "<ldap_dn> = None"
129 }
134 }
130
135
131 OUTPUT::
136 OUTPUT::
132
137
133 result: {
138 result: {
134 "msg" : "created new user <username>"
139 "msg" : "created new user <username>"
135 }
140 }
136 error: null
141 error: null
137
142
138 get_users_groups
143 get_users_groups
139 ----------------
144 ----------------
140
145
141 Lists all existing users groups. This command can be executed only using api_key
146 Lists all existing users groups. This command can be executed only using api_key
142 belonging to user with admin rights.
147 belonging to user with admin rights.
143
148
144 INPUT::
149 INPUT::
145
150
146 api_key : "<api_key>"
151 api_key : "<api_key>"
147 method : "get_users_groups"
152 method : "get_users_groups"
148 args : { }
153 args : { }
149
154
150 OUTPUT::
155 OUTPUT::
151
156
152 result : [
157 result : [
153 {
158 {
154 "id" : "<id>",
159 "id" : "<id>",
155 "name" : "<name>",
160 "name" : "<name>",
156 "active": "<bool>",
161 "active": "<bool>",
157 "members" : [
162 "members" : [
158 {
163 {
159 "id" : "<userid>",
164 "id" : "<userid>",
160 "username" : "<username>",
165 "username" : "<username>",
161 "firstname": "<firstname>",
166 "firstname": "<firstname>",
162 "lastname" : "<lastname>",
167 "lastname" : "<lastname>",
163 "email" : "<email>",
168 "email" : "<email>",
164 "active" : "<bool>",
169 "active" : "<bool>",
165 "admin" :  "<bool>",
170 "admin" :  "<bool>",
166 "ldap" : "<ldap_dn>"
171 "ldap" : "<ldap_dn>"
167 },
172 },
168
173
169 ]
174 ]
170 }
175 }
171 ]
176 ]
172 error : null
177 error : null
173
178
174 get_users_group
179 get_users_group
175 ---------------
180 ---------------
176
181
177 Gets an existing users group. This command can be executed only using api_key
182 Gets an existing users group. This command can be executed only using api_key
178 belonging to user with admin rights.
183 belonging to user with admin rights.
179
184
180 INPUT::
185 INPUT::
181
186
182 api_key : "<api_key>"
187 api_key : "<api_key>"
183 method : "get_users_group"
188 method : "get_users_group"
184 args : {
189 args : {
185 "group_name" : "<name>"
190 "group_name" : "<name>"
186 }
191 }
187
192
188 OUTPUT::
193 OUTPUT::
189
194
190 result : None if group not exist
195 result : None if group not exist
191 {
196 {
192 "id" : "<id>",
197 "id" : "<id>",
193 "name" : "<name>",
198 "name" : "<name>",
194 "active": "<bool>",
199 "active": "<bool>",
195 "members" : [
200 "members" : [
196 { "id" : "<userid>",
201 { "id" : "<userid>",
197 "username" : "<username>",
202 "username" : "<username>",
198 "firstname": "<firstname>",
203 "firstname": "<firstname>",
199 "lastname" : "<lastname>",
204 "lastname" : "<lastname>",
200 "email" : "<email>",
205 "email" : "<email>",
201 "active" : "<bool>",
206 "active" : "<bool>",
202 "admin" :  "<bool>",
207 "admin" :  "<bool>",
203 "ldap" : "<ldap_dn>"
208 "ldap" : "<ldap_dn>"
204 },
209 },
205
210
206 ]
211 ]
207 }
212 }
208 error : null
213 error : null
209
214
210 create_users_group
215 create_users_group
211 ------------------
216 ------------------
212
217
213 Creates new users group. This command can be executed only using api_key
218 Creates new users group. This command can be executed only using api_key
214 belonging to user with admin rights
219 belonging to user with admin rights
215
220
216 INPUT::
221 INPUT::
217
222
218 api_key : "<api_key>"
223 api_key : "<api_key>"
219 method : "create_users_group"
224 method : "create_users_group"
220 args: {
225 args: {
221 "name": "<name>",
226 "name": "<name>",
222 "active":"<bool> = True"
227 "active":"<bool> = True"
223 }
228 }
224
229
225 OUTPUT::
230 OUTPUT::
226
231
227 result: {
232 result: {
228 "id": "<newusersgroupid>",
233 "id": "<newusersgroupid>",
229 "msg": "created new users group <name>"
234 "msg": "created new users group <name>"
230 }
235 }
231 error: null
236 error: null
232
237
233 add_user_to_users_group
238 add_user_to_users_group
234 -----------------------
239 -----------------------
235
240
236 Adds a user to a users group. This command can be executed only using api_key
241 Adds a user to a users group. This command can be executed only using api_key
237 belonging to user with admin rights
242 belonging to user with admin rights
238
243
239 INPUT::
244 INPUT::
240
245
241 api_key : "<api_key>"
246 api_key : "<api_key>"
242 method : "add_user_users_group"
247 method : "add_user_users_group"
243 args: {
248 args: {
244 "group_name" : "<groupname>",
249 "group_name" : "<groupname>",
245 "username" : "<username>"
250 "username" : "<username>"
246 }
251 }
247
252
248 OUTPUT::
253 OUTPUT::
249
254
250 result: {
255 result: {
251 "id": "<newusersgroupmemberid>",
256 "id": "<newusersgroupmemberid>",
252 "msg": "created new users group member"
257 "msg": "created new users group member"
253 }
258 }
254 error: null
259 error: null
255
260
256 get_repos
261 get_repos
257 ---------
262 ---------
258
263
259 Lists all existing repositories. This command can be executed only using api_key
264 Lists all existing repositories. This command can be executed only using api_key
260 belonging to user with admin rights
265 belonging to user with admin rights
261
266
262 INPUT::
267 INPUT::
263
268
264 api_key : "<api_key>"
269 api_key : "<api_key>"
265 method : "get_repos"
270 method : "get_repos"
266 args: { }
271 args: { }
267
272
268 OUTPUT::
273 OUTPUT::
269
274
270 result: [
275 result: [
271 {
276 {
272 "id" : "<id>",
277 "id" : "<id>",
273 "name" : "<name>"
278 "name" : "<name>"
274 "type" : "<type>",
279 "type" : "<type>",
275 "description" : "<description>"
280 "description" : "<description>"
276 },
281 },
277
282
278 ]
283 ]
279 error: null
284 error: null
280
285
281 get_repo
286 get_repo
282 --------
287 --------
283
288
284 Gets an existing repository. This command can be executed only using api_key
289 Gets an existing repository. This command can be executed only using api_key
285 belonging to user with admin rights
290 belonging to user with admin rights
286
291
287 INPUT::
292 INPUT::
288
293
289 api_key : "<api_key>"
294 api_key : "<api_key>"
290 method : "get_repo"
295 method : "get_repo"
291 args: {
296 args: {
292 "name" : "<name>"
297 "name" : "<name>"
293 }
298 }
294
299
295 OUTPUT::
300 OUTPUT::
296
301
297 result: None if repository not exist
302 result: None if repository not exist
298 {
303 {
299 "id" : "<id>",
304 "id" : "<id>",
300 "name" : "<name>"
305 "name" : "<name>"
301 "type" : "<type>",
306 "type" : "<type>",
302 "description" : "<description>",
307 "description" : "<description>",
303 "members" : [
308 "members" : [
304 { "id" : "<userid>",
309 { "id" : "<userid>",
305 "username" : "<username>",
310 "username" : "<username>",
306 "firstname": "<firstname>",
311 "firstname": "<firstname>",
307 "lastname" : "<lastname>",
312 "lastname" : "<lastname>",
308 "email" : "<email>",
313 "email" : "<email>",
309 "active" : "<bool>",
314 "active" : "<bool>",
310 "admin" :  "<bool>",
315 "admin" :  "<bool>",
311 "ldap" : "<ldap_dn>",
316 "ldap" : "<ldap_dn>",
312 "permission" : "repository.(read|write|admin)"
317 "permission" : "repository.(read|write|admin)"
313 },
318 },
314
319
315 {
320 {
316 "id" : "<usersgroupid>",
321 "id" : "<usersgroupid>",
317 "name" : "<usersgroupname>",
322 "name" : "<usersgroupname>",
318 "active": "<bool>",
323 "active": "<bool>",
319 "permission" : "repository.(read|write|admin)"
324 "permission" : "repository.(read|write|admin)"
320 },
325 },
321
326
322 ]
327 ]
323 }
328 }
324 error: null
329 error: null
325
330
326 get_repo_nodes
331 get_repo_nodes
327 --------------
332 --------------
328
333
329 returns a list of nodes and it's children in a flat list for a given path
334 returns a list of nodes and it's children in a flat list for a given path
330 at given revision. It's possible to specify ret_type to show only files or
335 at given revision. It's possible to specify ret_type to show only files or
331 dirs. This command can be executed only using api_key belonging to user
336 dirs. This command can be executed only using api_key belonging to user
332 with admin rights
337 with admin rights
333
338
334 INPUT::
339 INPUT::
335
340
336 api_key : "<api_key>"
341 api_key : "<api_key>"
337 method : "get_repo_nodes"
342 method : "get_repo_nodes"
338 args: {
343 args: {
339 "repo_name" : "<name>",
344 "repo_name" : "<name>",
340 "revision" : "<revision>",
345 "revision" : "<revision>",
341 "root_path" : "<root_path>",
346 "root_path" : "<root_path>",
342 "ret_type" : "<ret_type>" = 'all'
347 "ret_type" : "<ret_type>" = 'all'
343 }
348 }
344
349
345 OUTPUT::
350 OUTPUT::
346
351
347 result: [
352 result: [
348 {
353 {
349 "name" : "<name>"
354 "name" : "<name>"
350 "type" : "<type>",
355 "type" : "<type>",
351 },
356 },
352
357
353 ]
358 ]
354 error: null
359 error: null
355
360
356
361
357
362
358 create_repo
363 create_repo
359 -----------
364 -----------
360
365
361 Creates a repository. This command can be executed only using api_key
366 Creates a repository. This command can be executed only using api_key
362 belonging to user with admin rights.
367 belonging to user with admin rights.
363 If repository name contains "/", all needed repository groups will be created.
368 If repository name contains "/", all needed repository groups will be created.
364 For example "foo/bar/baz" will create groups "foo", "bar" (with "foo" as parent),
369 For example "foo/bar/baz" will create groups "foo", "bar" (with "foo" as parent),
365 and create "baz" repository with "bar" as group.
370 and create "baz" repository with "bar" as group.
366
371
367 INPUT::
372 INPUT::
368
373
369 api_key : "<api_key>"
374 api_key : "<api_key>"
370 method : "create_repo"
375 method : "create_repo"
371 args: {
376 args: {
372 "name" : "<name>",
377 "name" : "<name>",
373 "owner_name" : "<ownername>",
378 "owner_name" : "<ownername>",
374 "description" : "<description> = ''",
379 "description" : "<description> = ''",
375 "repo_type" : "<type> = 'hg'",
380 "repo_type" : "<type> = 'hg'",
376 "private" : "<bool> = False"
381 "private" : "<bool> = False"
377 }
382 }
378
383
379 OUTPUT::
384 OUTPUT::
380
385
381 result: None
386 result: None
382 error: null
387 error: null
383
388
384 add_user_to_repo
389 add_user_to_repo
385 ----------------
390 ----------------
386
391
387 Add a user to a repository. This command can be executed only using api_key
392 Add a user to a repository. This command can be executed only using api_key
388 belonging to user with admin rights.
393 belonging to user with admin rights.
389 If "perm" is None, user will be removed from the repository.
394 If "perm" is None, user will be removed from the repository.
390
395
391 INPUT::
396 INPUT::
392
397
393 api_key : "<api_key>"
398 api_key : "<api_key>"
394 method : "add_user_to_repo"
399 method : "add_user_to_repo"
395 args: {
400 args: {
396 "repo_name" : "<reponame>",
401 "repo_name" : "<reponame>",
397 "username" : "<username>",
402 "username" : "<username>",
398 "perm" : "(None|repository.(read|write|admin))",
403 "perm" : "(None|repository.(read|write|admin))",
399 }
404 }
400
405
401 OUTPUT::
406 OUTPUT::
402
407
403 result: None
408 result: None
404 error: null
409 error: null
405
410
406 add_users_group_to_repo
411 add_users_group_to_repo
407 -----------------------
412 -----------------------
408
413
409 Add a users group to a repository. This command can be executed only using
414 Add a users group to a repository. This command can be executed only using
410 api_key belonging to user with admin rights. If "perm" is None, group will
415 api_key belonging to user with admin rights. If "perm" is None, group will
411 be removed from the repository.
416 be removed from the repository.
412
417
413 INPUT::
418 INPUT::
414
419
415 api_key : "<api_key>"
420 api_key : "<api_key>"
416 method : "add_users_group_to_repo"
421 method : "add_users_group_to_repo"
417 args: {
422 args: {
418 "repo_name" : "<reponame>",
423 "repo_name" : "<reponame>",
419 "group_name" : "<groupname>",
424 "group_name" : "<groupname>",
420 "perm" : "(None|repository.(read|write|admin))",
425 "perm" : "(None|repository.(read|write|admin))",
421 } No newline at end of file
426 }
@@ -1,10 +1,10 b''
1 .. _api:
1 .. _indexapi:
2
2
3 API Reference
3 API Reference
4 =============
4 =============
5
5
6 .. toctree::
6 .. toctree::
7 :maxdepth: 3
7 :maxdepth: 3
8
8
9 models
9 models
10 api No newline at end of file
10 api
@@ -1,19 +1,34 b''
1 .. _models:
1 .. _models:
2
2
3 The :mod:`models` Module
3 The :mod:`models` Module
4 ========================
4 ========================
5
5
6 .. automodule:: rhodecode.model
6 .. automodule:: rhodecode.model
7 :members:
7 :members:
8
8
9 .. automodule:: rhodecode.model.comment
10 :members:
11
12 .. automodule:: rhodecode.model.notification
13 :members:
14
9 .. automodule:: rhodecode.model.permission
15 .. automodule:: rhodecode.model.permission
10 :members:
16 :members:
11
17
18 .. automodule:: rhodecode.model.repo_permission
19 :members:
20
12 .. automodule:: rhodecode.model.repo
21 .. automodule:: rhodecode.model.repo
13 :members:
22 :members:
14
23
24 .. automodule:: rhodecode.model.repos_group
25 :members:
26
15 .. automodule:: rhodecode.model.scm
27 .. automodule:: rhodecode.model.scm
16 :members:
28 :members:
17
29
18 .. automodule:: rhodecode.model.user
30 .. automodule:: rhodecode.model.user
19 :members:
31 :members:
32
33 .. automodule:: rhodecode.model.users_group
34 :members: No newline at end of file
@@ -1,59 +1,58 b''
1 .. _index:
1 .. _index:
2
2
3 .. include:: ./../README.rst
3 .. include:: ./../README.rst
4
4
5 Documentation
5 Users Guide
6 -------------
6 -----------
7
7
8 **Installation:**
8 **Installation:**
9
9
10 .. toctree::
10 .. toctree::
11 :maxdepth: 1
11 :maxdepth: 1
12
12
13 installation
13 installation
14 setup
14 setup
15 upgrade
15 upgrade
16
16
17 **Usage**
17 **Usage**
18
18
19 .. toctree::
19 .. toctree::
20 :maxdepth: 1
20 :maxdepth: 1
21
21
22 usage/general
22 usage/general
23 usage/enable_git
23 usage/enable_git
24 usage/statistics
24 usage/statistics
25 usage/backup
25 usage/backup
26 usage/api_key_access
27
26
28 **Develop**
27 **Develop**
29
28
30 .. toctree::
29 .. toctree::
31 :maxdepth: 1
30 :maxdepth: 1
32
31
33 contributing
32 contributing
34 changelog
33 changelog
35
34
36 **API**
35 **API**
37
36
38 .. toctree::
37 .. toctree::
39 :maxdepth: 2
38 :maxdepth: 1
40
39
41 api/index
40 api/index
42
41
43
42
44 Other topics
43 Other topics
45 ------------
44 ------------
46
45
47 * :ref:`genindex`
46 * :ref:`genindex`
48 * :ref:`search`
47 * :ref:`search`
49
48
50 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
49 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
51 .. _python: http://www.python.org/
50 .. _python: http://www.python.org/
52 .. _django: http://www.djangoproject.com/
51 .. _django: http://www.djangoproject.com/
53 .. _mercurial: http://mercurial.selenic.com/
52 .. _mercurial: http://mercurial.selenic.com/
54 .. _bitbucket: http://bitbucket.org/
53 .. _bitbucket: http://bitbucket.org/
55 .. _subversion: http://subversion.tigris.org/
54 .. _subversion: http://subversion.tigris.org/
56 .. _git: http://git-scm.com/
55 .. _git: http://git-scm.com/
57 .. _celery: http://celeryproject.org/
56 .. _celery: http://celeryproject.org/
58 .. _Sphinx: http://sphinx.pocoo.org/
57 .. _Sphinx: http://sphinx.pocoo.org/
59 .. _vcs: http://pypi.python.org/pypi/vcs No newline at end of file
58 .. _vcs: http://pypi.python.org/pypi/vcs
@@ -1,219 +1,219 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.notification
3 rhodecode.model.notification
4 ~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Model for notifications
6 Model for notifications
7
7
8
8
9 :created_on: Nov 20, 2011
9 :created_on: Nov 20, 2011
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
26
27 import os
27 import os
28 import logging
28 import logging
29 import traceback
29 import traceback
30 import datetime
30 import datetime
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33
33
34 import rhodecode
34 import rhodecode
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
36 from rhodecode.model import BaseModel
36 from rhodecode.model import BaseModel
37 from rhodecode.model.db import Notification, User, UserNotification
37 from rhodecode.model.db import Notification, User, UserNotification
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 class NotificationModel(BaseModel):
42 class NotificationModel(BaseModel):
43
43
44 def __get_user(self, user):
44 def __get_user(self, user):
45 if isinstance(user, basestring):
45 if isinstance(user, basestring):
46 return User.get_by_username(username=user)
46 return User.get_by_username(username=user)
47 else:
47 else:
48 return self._get_instance(User, user)
48 return self._get_instance(User, user)
49
49
50 def __get_notification(self, notification):
50 def __get_notification(self, notification):
51 if isinstance(notification, Notification):
51 if isinstance(notification, Notification):
52 return notification
52 return notification
53 elif isinstance(notification, int):
53 elif isinstance(notification, int):
54 return Notification.get(notification)
54 return Notification.get(notification)
55 else:
55 else:
56 if notification:
56 if notification:
57 raise Exception('notification must be int or Instance'
57 raise Exception('notification must be int or Instance'
58 ' of Notification got %s' % type(notification))
58 ' of Notification got %s' % type(notification))
59
59
60 def create(self, created_by, subject, body, recipients=None,
60 def create(self, created_by, subject, body, recipients=None,
61 type_=Notification.TYPE_MESSAGE, with_email=True,
61 type_=Notification.TYPE_MESSAGE, with_email=True,
62 email_kwargs={}):
62 email_kwargs={}):
63 """
63 """
64
64
65 Creates notification of given type
65 Creates notification of given type
66
66
67 :param created_by: int, str or User instance. User who created this
67 :param created_by: int, str or User instance. User who created this
68 notification
68 notification
69 :param subject:
69 :param subject:
70 :param body:
70 :param body:
71 :param recipients: list of int, str or User objects, when None
71 :param recipients: list of int, str or User objects, when None
72 is given send to all admins
72 is given send to all admins
73 :param type_: type of notification
73 :param type_: type of notification
74 :param with_email: send email with this notification
74 :param with_email: send email with this notification
75 :param email_kwargs: additional dict to pass as args to email template
75 :param email_kwargs: additional dict to pass as args to email template
76 """
76 """
77 from rhodecode.lib.celerylib import tasks, run_task
77 from rhodecode.lib.celerylib import tasks, run_task
78
78
79 if recipients and not getattr(recipients, '__iter__', False):
79 if recipients and not getattr(recipients, '__iter__', False):
80 raise Exception('recipients must be a list of iterable')
80 raise Exception('recipients must be a list of iterable')
81
81
82 created_by_obj = self.__get_user(created_by)
82 created_by_obj = self.__get_user(created_by)
83
83
84 if recipients:
84 if recipients:
85 recipients_objs = []
85 recipients_objs = []
86 for u in recipients:
86 for u in recipients:
87 obj = self.__get_user(u)
87 obj = self.__get_user(u)
88 if obj:
88 if obj:
89 recipients_objs.append(obj)
89 recipients_objs.append(obj)
90 recipients_objs = set(recipients_objs)
90 recipients_objs = set(recipients_objs)
91 else:
91 else:
92 # empty recipients means to all admins
92 # empty recipients means to all admins
93 recipients_objs = User.query().filter(User.admin == True).all()
93 recipients_objs = User.query().filter(User.admin == True).all()
94
94
95 notif = Notification.create(created_by=created_by_obj, subject=subject,
95 notif = Notification.create(created_by=created_by_obj, subject=subject,
96 body=body, recipients=recipients_objs,
96 body=body, recipients=recipients_objs,
97 type_=type_)
97 type_=type_)
98
98
99 if with_email is False:
99 if with_email is False:
100 return notif
100 return notif
101
101
102 # send email with notification
102 # send email with notification
103 for rec in recipients_objs:
103 for rec in recipients_objs:
104 email_subject = NotificationModel().make_description(notif, False)
104 email_subject = NotificationModel().make_description(notif, False)
105 type_ = type_
105 type_ = type_
106 email_body = body
106 email_body = body
107 kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)}
107 kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)}
108 kwargs.update(email_kwargs)
108 kwargs.update(email_kwargs)
109 email_body_html = EmailNotificationModel()\
109 email_body_html = EmailNotificationModel()\
110 .get_email_tmpl(type_, **kwargs)
110 .get_email_tmpl(type_, **kwargs)
111 run_task(tasks.send_email, rec.email, email_subject, email_body,
111 run_task(tasks.send_email, rec.email, email_subject, email_body,
112 email_body_html)
112 email_body_html)
113
113
114 return notif
114 return notif
115
115
116 def delete(self, user, notification):
116 def delete(self, user, notification):
117 # we don't want to remove actual notification just the assignment
117 # we don't want to remove actual notification just the assignment
118 try:
118 try:
119 notification = self.__get_notification(notification)
119 notification = self.__get_notification(notification)
120 user = self.__get_user(user)
120 user = self.__get_user(user)
121 if notification and user:
121 if notification and user:
122 obj = UserNotification.query()\
122 obj = UserNotification.query()\
123 .filter(UserNotification.user == user)\
123 .filter(UserNotification.user == user)\
124 .filter(UserNotification.notification
124 .filter(UserNotification.notification
125 == notification)\
125 == notification)\
126 .one()
126 .one()
127 self.sa.delete(obj)
127 self.sa.delete(obj)
128 return True
128 return True
129 except Exception:
129 except Exception:
130 log.error(traceback.format_exc())
130 log.error(traceback.format_exc())
131 raise
131 raise
132
132
133 def get_for_user(self, user):
133 def get_for_user(self, user):
134 user = self.__get_user(user)
134 user = self.__get_user(user)
135 return user.notifications
135 return user.notifications
136
136
137 def mark_all_read_for_user(self, user):
137 def mark_all_read_for_user(self, user):
138 user = self.__get_user(user)
138 user = self.__get_user(user)
139 UserNotification.query()\
139 UserNotification.query()\
140 .filter(UserNotification.read==False)\
140 .filter(UserNotification.read==False)\
141 .update({'read': True})
141 .update({'read': True})
142
142
143 def get_unread_cnt_for_user(self, user):
143 def get_unread_cnt_for_user(self, user):
144 user = self.__get_user(user)
144 user = self.__get_user(user)
145 return UserNotification.query()\
145 return UserNotification.query()\
146 .filter(UserNotification.read == False)\
146 .filter(UserNotification.read == False)\
147 .filter(UserNotification.user == user).count()
147 .filter(UserNotification.user == user).count()
148
148
149 def get_unread_for_user(self, user):
149 def get_unread_for_user(self, user):
150 user = self.__get_user(user)
150 user = self.__get_user(user)
151 return [x.notification for x in UserNotification.query()\
151 return [x.notification for x in UserNotification.query()\
152 .filter(UserNotification.read == False)\
152 .filter(UserNotification.read == False)\
153 .filter(UserNotification.user == user).all()]
153 .filter(UserNotification.user == user).all()]
154
154
155 def get_user_notification(self, user, notification):
155 def get_user_notification(self, user, notification):
156 user = self.__get_user(user)
156 user = self.__get_user(user)
157 notification = self.__get_notification(notification)
157 notification = self.__get_notification(notification)
158
158
159 return UserNotification.query()\
159 return UserNotification.query()\
160 .filter(UserNotification.notification == notification)\
160 .filter(UserNotification.notification == notification)\
161 .filter(UserNotification.user == user).scalar()
161 .filter(UserNotification.user == user).scalar()
162
162
163 def make_description(self, notification, show_age=True):
163 def make_description(self, notification, show_age=True):
164 """
164 """
165 Creates a human readable description based on properties
165 Creates a human readable description based on properties
166 of notification object
166 of notification object
167 """
167 """
168
168
169 _map = {notification.TYPE_CHANGESET_COMMENT:_('commented on commit'),
169 _map = {notification.TYPE_CHANGESET_COMMENT:_('commented on commit'),
170 notification.TYPE_MESSAGE:_('sent message'),
170 notification.TYPE_MESSAGE:_('sent message'),
171 notification.TYPE_MENTION:_('mentioned you'),
171 notification.TYPE_MENTION:_('mentioned you'),
172 notification.TYPE_REGISTRATION:_('registered in RhodeCode')}
172 notification.TYPE_REGISTRATION:_('registered in RhodeCode')}
173
173
174 DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
174 DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
175
175
176 tmpl = "%(user)s %(action)s %(when)s"
176 tmpl = "%(user)s %(action)s %(when)s"
177 if show_age:
177 if show_age:
178 when = h.age(notification.created_on)
178 when = h.age(notification.created_on)
179 else:
179 else:
180 DTF = lambda d: datetime.datetime.strftime(d, DATETIME_FORMAT)
180 DTF = lambda d: datetime.datetime.strftime(d, DATETIME_FORMAT)
181 when = DTF(notification.created_on)
181 when = DTF(notification.created_on)
182 data = dict(user=notification.created_by_user.username,
182 data = dict(user=notification.created_by_user.username,
183 action=_map[notification.type_],
183 action=_map[notification.type_],
184 when=when)
184 when=when)
185 return tmpl % data
185 return tmpl % data
186
186
187
187
188 class EmailNotificationModel(BaseModel):
188 class EmailNotificationModel(BaseModel):
189
189
190 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
190 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
191 TYPE_PASSWORD_RESET = 'passoword_link'
191 TYPE_PASSWORD_RESET = 'passoword_link'
192 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
192 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
193 TYPE_DEFAULT = 'default'
193 TYPE_DEFAULT = 'default'
194
194
195 def __init__(self):
195 def __init__(self):
196 self._template_root = rhodecode.CONFIG['pylons.paths']['templates'][0]
196 self._template_root = rhodecode.CONFIG['pylons.paths']['templates'][0]
197 self._tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
197 self._tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
198
198
199 self.email_types = {
199 self.email_types = {
200 self.TYPE_CHANGESET_COMMENT:'email_templates/changeset_comment.html',
200 self.TYPE_CHANGESET_COMMENT:'email_templates/changeset_comment.html',
201 self.TYPE_PASSWORD_RESET:'email_templates/password_reset.html',
201 self.TYPE_PASSWORD_RESET:'email_templates/password_reset.html',
202 self.TYPE_REGISTRATION:'email_templates/registration.html',
202 self.TYPE_REGISTRATION:'email_templates/registration.html',
203 self.TYPE_DEFAULT:'email_templates/default.html'
203 self.TYPE_DEFAULT:'email_templates/default.html'
204 }
204 }
205
205
206 def get_email_tmpl(self, type_, **kwargs):
206 def get_email_tmpl(self, type_, **kwargs):
207 """
207 """
208 return generated template for email based on given type
208 return generated template for email based on given type
209
209
210 :param type_:
210 :param type_:
211 """
211 """
212
212
213 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
213 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
214 email_template = self._tmpl_lookup.get_template(base)
214 email_template = self._tmpl_lookup.get_template(base)
215 # translator inject
215 # translator inject
216 _kwargs = {'_':_}
216 _kwargs = {'_':_}
217 _kwargs.update(kwargs)
217 _kwargs.update(kwargs)
218 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
218 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
219 return email_template.render(**_kwargs)
219 return email_template.render(**_kwargs)
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now