release: Merge default into stable for release preparation

This commit is contained in:
Marcin Kuzminski 2017-06-28 21:55:11 +02:00
commit 66eefae578
374 changed files with 13012 additions and 6596 deletions

View file

@ -1,5 +1,5 @@
[bumpversion]
current_version = 4.7.2
current_version = 4.8.0
message = release: Bump version {current_version} to {new_version}
[bumpversion:file:rhodecode/VERSION]

View file

@ -5,26 +5,21 @@ done = false
done = true
[task:rc_tools_pinned]
done = true
[task:fixes_on_stable]
done = true
[task:pip2nix_generated]
done = true
[task:changelog_updated]
done = true
[task:generate_api_docs]
done = true
[release]
state = prepared
version = 4.7.2
[task:updated_translation]
[release]
state = in_progress
version = 4.8.0
[task:generate_js_routes]
[task:updated_trial_license]

View file

@ -33,6 +33,7 @@ include rhodecode/public/502.html
# images, css
include rhodecode/public/css/*.css
include rhodecode/public/images/*.*
include rhodecode/public/images/ee_features/*.*
# sound files
include rhodecode/public/sounds/*.mp3

View file

@ -174,32 +174,33 @@ let
'';
postInstall = ''
echo "Writing meta information for rccontrol to nix-support/rccontrol"
mkdir -p $out/nix-support/rccontrol
cp -v rhodecode/VERSION $out/nix-support/rccontrol/version
echo "DONE: Meta information for rccontrol written"
# python based programs need to be wrapped
ln -s ${self.supervisor}/bin/supervisor* $out/bin/
ln -s ${self.pyramid}/bin/* $out/bin/
ln -s ${self.gunicorn}/bin/gunicorn $out/bin/
ln -s ${self.supervisor}/bin/supervisor* $out/bin/
ln -s ${self.PasteScript}/bin/paster $out/bin/
ln -s ${self.channelstream}/bin/channelstream $out/bin/
ln -s ${self.pyramid}/bin/* $out/bin/ #*/
# rhodecode-tools
# TODO: johbo: re-think this. Do the tools import anything from enterprise?
ln -s ${self.rhodecode-tools}/bin/rhodecode-* $out/bin/
# note that condition should be restricted when adding further tools
for file in $out/bin/*; do #*/
for file in $out/bin/*;
do
wrapProgram $file \
--prefix PYTHONPATH : $PYTHONPATH \
--prefix PATH : $PATH \
--prefix PYTHONPATH : $PYTHONPATH \
--set PYTHONHASHSEED random
done
mkdir $out/etc
cp configs/production.ini $out/etc
echo "Writing meta information for rccontrol to nix-support/rccontrol"
mkdir -p $out/nix-support/rccontrol
cp -v rhodecode/VERSION $out/nix-support/rccontrol/version
echo "DONE: Meta information for rccontrol written"
# TODO: johbo: Make part of ac-tests
if [ ! -f rhodecode/public/js/scripts.js ]; then

View file

@ -62,6 +62,9 @@ Below config if for an Apache Reverse Proxy configuration.
ProxyPass / http://127.0.0.1:10002/ timeout=7200 Keepalive=On
ProxyPassReverse / http://127.0.0.1:10002/
# Increase headers for large Mercurial headers
LimitRequestLine 16380
# strict http prevents from https -> http downgrade
Header always set Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"

View file

@ -5,7 +5,10 @@ Use the following example to configure Nginx as a your web server.
.. code-block:: nginx
## rate limiter for certain pages to prevent brute force attacks
limit_req_zone $binary_remote_addr zone=dl_limit:10m rate=1r/s;
## custom log format
log_format log_custom '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
@ -109,6 +112,12 @@ Use the following example to configure Nginx as a your web server.
proxy_set_header Connection "upgrade";
}
location /_admin/login {
## rate limit this endpoint
limit_req zone=dl_limit burst=10 nodelay;
try_files $uri @rhode;
}
location / {
try_files $uri @rhode;
}

View file

@ -3,8 +3,12 @@
Repository Extra Fields
=======================
Extra fields attached to a |repo| allow you to configure additional actions for
|RCX|. To install and read more about |RCX|, see the :ref:`install-rcx` section.
Extra fields attached to a |repo| allow you to configure additional fields for
each repository. This allows storing custom data per-repository.
It can be used in :ref:`integrations-webhook` or in |RCX|.
To install and read more about |RCX|, see the :ref:`install-rcx` section.
Enabling Extra Fields
---------------------
@ -27,9 +31,14 @@ To configure extra fields per repository, use the following steps:
.. image:: ../images/extra-repo-fields.png
The most important is the `New field key` variable which under the value will
be stored. It needs to be unique for each repository. The label and description
will be generated in repository settings where users can actually save some
values inside generated extra fields.
Example Usage
-------------
Example Usage in extensions
---------------------------
To use the extra fields in an extension, see the example below. For more
information and examples, see the :ref:`extensions-hooks-ref` section.

View file

@ -7,7 +7,7 @@ The VCS Server handles |RCM| backend functionality. You need to configure
a VCS Server to run with a |RCM| instance. If you do not, you will be missing
the connection between |RCM| and its |repos|. This will cause error messages
on the web interface. You can run your setup in the following configurations,
currently the best performance is one VCS Server per |RCM| instance:
currently the best performance is one of following:
* One VCS Server per |RCM| instance.
* One VCS Server handling multiple instances.
@ -59,7 +59,8 @@ instance in the
\vcs.backends <available-vcs-systems>
Set a comma-separated list of the |repo| options available from the
web interface. The default is ``hg, git, svn``,
which is all |repo| types available.
which is all |repo| types available. The order of backends is also the
order backend will try to detect requests type.
\vcs.connection_timeout <seconds>
Set the length of time in seconds that the VCS Server waits for
@ -159,9 +160,10 @@ for full details see the :ref:`RhodeCode Control CLI <control:rcc-cli>`.
- NAME: vcsserver-1
- STATUS: RUNNING
- TYPE: VCSServer
- VERSION: 1.0.0
- URL: http://127.0.0.1:10001
logs:/home/ubuntu/.rccontrol/vcsserver-1/vcsserver.log
- VERSION: 4.7.2 VCSServer
- URL: http://127.0.0.1:10008
- CONFIG: /home/ubuntu/.rccontrol/vcsserver-1/vcsserver.ini
$ rccontrol restart vcsserver-1
Instance "vcsserver-1" successfully stopped.
@ -181,7 +183,9 @@ For a more detailed explanation of the logger levers, see :ref:`debug-mode`.
.. rst-class:: dl-horizontal
\host <ip-address>
Set the host on which the VCS Server will run.
Set the host on which the VCS Server will run. VCSServer is not
protected by any authentication, so we *highly* recommend running it
under localhost ip that is `127.0.0.1`
\port <int>
Set the port number on which the VCS Server will be available.
@ -189,13 +193,22 @@ For a more detailed explanation of the logger levers, see :ref:`debug-mode`.
\locale <locale_utf>
Set the locale the VCS Server expects.
\threadpool_size <int>
Set the size of the threadpool used to communicate
with the WSGI workers. This should be at least 6 times the number of
WSGI worker processes.
\workers <int>
Set the number of process workers.Recommended
value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers
\max_requests <int>
The maximum number of requests a worker will process before restarting.
Any value greater than zero will limit the number of requests a work
will process before automatically restarting. This is a simple method
to help limit the damage of memory leaks.
\max_requests_jitter <int>
The maximum jitter to add to the max_requests setting.
The jitter causes the restart per worker to be randomized by
randint(0, max_requests_jitter). This is intended to stagger worker
restarts to avoid all workers restarting at the same time.
\timeout <seconds>
Set the timeout for RPC communication in seconds.
.. note::
@ -204,27 +217,54 @@ For a more detailed explanation of the logger levers, see :ref:`debug-mode`.
.. code-block:: ini
################################################################################
# RhodeCode VCSServer - configuration #
# RhodeCode VCSServer with HTTP Backend - configuration #
# #
################################################################################
[DEFAULT]
[server:main]
## COMMON ##
host = 127.0.0.1
port = 9900
port = 10002
##########################
## GUNICORN WSGI SERVER ##
##########################
## run with gunicorn --log-config vcsserver.ini --paste vcsserver.ini
use = egg:gunicorn#main
## Sets the number of process workers. Recommended
## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers
workers = 3
## process name
proc_name = rhodecode_vcsserver
## type of worker class, one of sync, gevent
## recommended for bigger setup is using of of other than sync one
worker_class = sync
## The maximum number of simultaneous clients. Valid only for Gevent
#worker_connections = 10
## max number of requests that worker will handle before being gracefully
## restarted, could prevent memory leaks
max_requests = 1000
max_requests_jitter = 30
## amount of time a worker can spend with handling a request before it
## gets killed and restarted. Set to 6hrs
timeout = 21600
[app:main]
use = egg:rhodecode-vcsserver
pyramid.default_locale_name = en
pyramid.includes =
## default locale used by VCS systems
locale = en_US.UTF-8
# number of worker threads, this should be set based on a formula threadpool=N*6
# where N is number of RhodeCode Enterprise workers, eg. running 2 instances
# 8 gunicorn workers each would be 2 * 8 * 6 = 96, threadpool_size = 96
threadpool_size = 16
timeout = 0
# cache regions, please don't change
beaker.cache.regions = repo_object
beaker.cache.repo_object.type = memorylru
beaker.cache.repo_object.max_items = 1000
beaker.cache.repo_object.max_items = 100
# cache auto-expires after N seconds
beaker.cache.repo_object.expire = 10
beaker.cache.repo_object.expire = 300
beaker.cache.repo_object.enabled = true
@ -270,20 +310,6 @@ For a more detailed explanation of the logger levers, see :ref:`debug-mode`.
level = DEBUG
formatter = generic
[handler_file]
class = FileHandler
args = ('vcsserver.log', 'a',)
level = DEBUG
formatter = generic
[handler_file_rotating]
class = logging.handlers.TimedRotatingFileHandler
# 'D', 5 - rotate every 5days
# you can set 'h', 'midnight'
args = ('vcsserver.log', 'D', 5, 10,)
level = DEBUG
formatter = generic
################
## FORMATTERS ##
################

View file

@ -6,7 +6,7 @@ pull_request methods
close_pull_request
------------------
.. py:function:: close_pull_request(apiuser, repoid, pullrequestid, userid=<Optional:<OptionalAttr:apiuser>>)
.. py:function:: close_pull_request(apiuser, repoid, pullrequestid, userid=<Optional:<OptionalAttr:apiuser>>, message=<Optional:''>)
Close the pull request specified by `pullrequestid`.
@ -19,6 +19,9 @@ close_pull_request
:type pullrequestid: int
:param userid: Close the pull request as this user.
:type userid: Optional(str or int)
:param message: Optional message to close the Pull Request with. If not
specified it will be generated automatically.
:type message: Optional(str)
Example output:
@ -27,6 +30,7 @@ close_pull_request
"id": <id_given_in_input>,
"result": {
"pull_request_id": "<int>",
"close_status": "<str:status_lbl>,
"closed": "<bool>"
},
"error": null
@ -105,10 +109,12 @@ create_pull_request
:param description: Set the pull request description.
:type description: Optional(str)
:param reviewers: Set the new pull request reviewers list.
Reviewer defined by review rules will be added automatically to the
defined list.
:type reviewers: Optional(list)
Accepts username strings or objects of the format:
{'username': 'nick', 'reasons': ['original author']}
[{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
get_pull_request
@ -320,7 +326,7 @@ merge_pull_request
update_pull_request
-------------------
.. py:function:: update_pull_request(apiuser, repoid, pullrequestid, title=<Optional:''>, description=<Optional:''>, reviewers=<Optional:None>, update_commits=<Optional:None>, close_pull_request=<Optional:None>)
.. py:function:: update_pull_request(apiuser, repoid, pullrequestid, title=<Optional:''>, description=<Optional:''>, reviewers=<Optional:None>, update_commits=<Optional:None>)
Updates a pull request.
@ -336,10 +342,12 @@ update_pull_request
:type description: Optional(str)
:param reviewers: Update pull request reviewers list with new value.
:type reviewers: Optional(list)
Accepts username strings or objects of the format:
[{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
:param update_commits: Trigger update of commits for this pull request
:type: update_commits: Optional(bool)
:param close_pull_request: Close this pull request with rejected state
:type: close_pull_request: Optional(bool)
Example output:

View file

@ -527,6 +527,7 @@ get_repo_settings
"id": 237,
"result": {
"extensions_largefiles": true,
"extensions_evolve": true,
"hooks_changegroup_push_logger": true,
"hooks_changegroup_repo_size": false,
"hooks_outgoing_pull_logger": true,
@ -762,6 +763,49 @@ lock
}
maintenance
-----------
.. py:function:: maintenance(apiuser, repoid)
Triggers a maintenance on the given repository.
This command can only be run using an |authtoken| with admin
rights to the specified repository. For more information,
see :ref:`config-token-ref`.
This command takes the following options:
:param apiuser: This is filled automatically from the |authtoken|.
:type apiuser: AuthUser
:param repoid: The repository name or repository ID.
:type repoid: str or int
Example output:
.. code-block:: bash
id : <id_given_in_input>
result : {
"msg": "executed maintenance command",
"executed_actions": [
<action_message>, <action_message2>...
],
"repository": "<repository name>"
}
error : null
Example error output:
.. code-block:: bash
id : <id_given_in_input>
result : null
error : {
"Unable to execute maintenance on `<reponame>`"
}
pull
----

View file

@ -66,7 +66,7 @@ RhodeCode VCSServer repositories into the same directory.
RhodeCode currently is using Mercurial Version Control System, please make sure
you have it installed before continuing.
To obtain the required sources, use the following commands:
To obtain the required sources, use the following commands::
mkdir rhodecode-develop && cd rhodecode-develop
hg clone https://code.rhodecode.com/rhodecode-enterprise-ce
@ -80,9 +80,9 @@ To obtain the required sources, use the following commands:
Install some required libraries
-------------------------------
There are some required drivers that we need to install to test RhodeCode
under different types of databases. For example in Ubuntu we need to install
the following.
There are some required drivers and dev libraries that we need to install to
test RhodeCode under different types of databases. For example in Ubuntu we
need to install the following.
required libraries::

View file

@ -20,7 +20,7 @@ and commit files and |repos| while managing their security permissions.
* Migration from existing databases.
* |RCM| SDK.
* Built-in analytics
* Built in integrations including: Slack, Jenkins, Webhooks, Jira, Redmine, Hipchat
* Built in integrations including: Slack, Webhooks (used for Jenkins/TeamCity and other CIs), Jira, Redmine, Hipchat
* Pluggable authentication system.
* Support for AD, |LDAP|, Crowd, CAS, PAM.
* Support for external authentication via Oauth Google, Github, Bitbucket, Twitter.

97
docs/integrations/ci.rst Normal file
View file

@ -0,0 +1,97 @@
.. _integrations-ci:
CI Server integration
=====================
RhodeCode :ref:`integrations-webhook` integration is a powerfull tool to allow
interaction with systems like Jenkin, Bamboo, TeamCity, CircleCi or any other
CI server that allows triggering a build using HTTP call.
Below are few examples on how to use :ref:`integrations-webhook` to trigger
a CI build.
General Webhook
+++++++++++++++
:ref:`integrations-webhook` allows sending a JSON payload information to specified
url with GET or POST methods. There are several variables that could be used
in the URL greatly extending the flexibility of this type of integration.
Most of the modern CI systems such as Jenkins, TeamCity, Bamboo or CircleCi
allows triggering builds via GET or POST calls.
:ref:`integrations-webhook` can be either specified per each repository or
globally, if your CI maps directly to all your projects a global
:ref:`integrations-webhook` integration can be created and will trigger builds
for each change in projects. If only some projects allow triggering builds a
global integration will also work because mostly a CI system will ignore a
call for unspecified builds.
.. note::
A quick note on security. It's recommended to allow IP restrictions
to only allow RhodeCode server to trigger builds. If you need to
specify username and password this could be done by embedding it into a
trigger URL, e.g. `http://user:password@server.com/job/${project_id}
If users require to provide any custom parameters, they can be stored for each
project inside the :ref:`repo-xtra`. For example to migrate a current job that
has a numeric build id, storing this as `jenkins_build_id` key extra field
the url would look like that::
http://server/job/${extra:jenkins_build_id}/
.. note::
Please note that some variables will result in multiple calls.
e.g. for |HG| specifying `${branch}` will trigger as many builds as how
many branches the suer actually pushed. Same applies to `${commit_id}`
This will trigger many builds if many commits are pushed. This allows
triggering individual builds for each pushed commit.
Jenkins
+++++++
To use Jenkins CI with RhodeCode, a Jenkins Build with Parameters should be used.
Plugin details are available here: https://wiki.jenkins.io/display/JENKINS/Build+With+Parameters+Plugin
If the plugin is configured, RhodeCode can trigger builds automatically by
calling such example url provided in :ref:`integrations-webhook` integration::
http://server/job/${project_id}/build-branch-${branch}/buildWithParameters?token=TOKEN&PARAMETER=value&PARAMETER2=value2
Team City
+++++++++
To use TeamCity CI it's enough to call the API and provide a buildId.
Example url after configuring :ref:`repo-xtra` would look like that::
http://teacmtiyserver/viewType.html?buildTypeId=${extra:tc_build_id}
Each project can have many build configurations.
buildTypeId which is a unique ID for each build configuration (job).
CircleCi
++++++++
To use CircleCi, a POST call needs to be triggered. Example build url would
look like this::
http://cicleCiServer/project/${repo_type}/${username}/${repo_id}/tree/${branch}
Circle Ci expects format of::
POST: /project/:vcs-type/:username/:project/tree/:branch
CircleCi API documentation can be found here: https://circleci.com/docs/api/v1-reference/

View file

@ -17,7 +17,8 @@ Type/Name |RC| Edition Description
:ref:`integrations-slack` |RCCEshort| https://slack.com/
:ref:`integrations-hipchat` |RCCEshort| https://www.hipchat.com/
:ref:`integrations-webhook` |RCCEshort| POST events as `json` to a custom url
:ref:`integrations-email` |RCEEshort| Send repo push commits by email
:ref:`integrations-ci` |RCCEshort| Trigger Builds for Common CI Systems
:ref:`integrations-email` |RCCEshort| Send repo push commits by email
:ref:`integrations-redmine` |RCEEshort| Close/Resolve/Reference redmine issues
:ref:`integrations-jira` |RCEEshort| Close/Resolve/Reference JIRA issues
============================ ============ =====================================

View file

@ -3,9 +3,9 @@
Webhook integration
===================
The Webhook integration allows you to POST events such as repository pushes
or pull requests to a custom http endpoint as a json dict with details of the
event.
The :ref:`creating-integrations` integration allows you to POST events such as
repository pushes or pull requests to a custom http endpoint as a JSON dict
with details of the event.
Starting from 4.5.0 release, webhook integration allows to use variables
inside the URL. For example in URL `https://server-example.com/${repo_name}`
@ -14,8 +14,10 @@ triggered from. Some of the variables like
`${branch}` will result in webhook be called multiple times when multiple
branches are pushed.
Some of the variables like `${pull_request_id}` will be replaced only in
the pull request related events.
Starting from 4.8.0 also repository extra fields can be used. A format to use
them is `${extra:field_key}`. It's usefull to use them to specify custom
repo only parameters. Some of the variables like `${pull_request_id}`
will be replaced only in the pull request related events.
To create a webhook integration, select "webhook" in the integration settings
and use the URL and key from your any previous custom webhook created. See

View file

@ -0,0 +1,103 @@
|RCE| 4.8.0 |RNS|
-----------------
Release Date
^^^^^^^^^^^^
- 2017-06-30
New Features
^^^^^^^^^^^^
- Code Review: added new reviewers logic. This features now is Common Criteria
compatible and allows to define Mandatory (non-removable) reviewers.
In addition new options were added to forbid adding new reviewers or forbid
author of commits or the pull request itself to be a reviewer of the code.
- Audit logs: introducing new audit logs tracking most important actions in
the system. Admins can track important events such as deletion of resources,
permissions changes, user groups changes. Each event tracks users with his
IP and user agent.
- Mercurial: enabled evolve extensions. Each repository can be now configured
to support evolve, commit phases, and evolve state are also shown in
commit and changelog views.
- VCS: expose newly pushed bookmarks or branches as quick links to open a
pull request on client output. Allows easier pull request creation via CLI.
General
^^^^^^^
- Core: ported many views into pure pyramid code with python3.6 compatibility.
Now almost 80% of the code is ported, and future ready. It's our ongoing
effort to allow support for modern python version.
- Comments: show author tag in pull request comments to easily
discover the author of changes in discussions.
- Files: allow specifying custom filename for uploaded files via web interface.
- Pull requests: changed who is allowed to close a pull request. Now it's only
super-admin, owner or person who can merge.
Before it was every reviewer can close. Which really doesn't make sense.
- Users: show that user is disabled when editing his properties.
- Integrations: expose user_id, and username in Webhook integration
templates arguments.
- Integrations: exposed extra repo variables in template arguments of
Webhook integration.
- Login: add link when using external auth to make it easier to login
using oauth providers, such as Google or Github.
- Maintenance: added svn verify command to tasks to be able to verify the
filesystem and repo formats from web interface. Allows much easier tracking
of incompatible filesystem storage of subversion repositories.
- Events: expose permalink urls for pull requests, and repositories.
Permalink url should provide a non-changeable url that can be used in
external system.
- Svn: increase possibility to specify compatibility to pre 1.9 version.
Security
^^^^^^^^
- security(high): fixed possibility to delete other users inline comments
for users who were repository admins.
- security(med): fixed XSS inside the tooltip for author string.
- security(med): fixed stored XSS in notifications inbox.
- security(med): use custom writer for RST rendering to prevent injection of javascript: tags.
- security(med): escape flash messaged VCS errors to prevent reflected XSS attacks.
- security(low): use 404 instead of 403 code on permission decorator to
prevent brute force resource discovery attacks.
- security(low): fixed self XSS inside autocomplete files view.
- security(low): fixed self Xss inside repo strip view.
- security(low): fixed self Xss inside the email add functionality.
- security(none): use new safe escaped user attributes across the application.
Will prevent all possible XSS attack vectors from user stored attributes.
This specially can come from external authentication systems which doesn't
validate the data.
Performance
^^^^^^^^^^^
Fixes
^^^^^
- Pull requests: make sure we process comments in the order of IDS when
linking them. In some edge cases it could lead to comments not displaying
correctly.
- Emails: fixed newlines in email templates that can break email sending code.
- Markdown: fixed hr and strong tags styling.
- Notifications: fixed problem with 500 errors on non-numeric entries in url.
- API: use simple schema validator to be consistent how we validate between
API and web views for create user and create user_group calls.
- Users: fixed problem with personal repo group wasn't shown for disabled users.
- Oauth: improve Google extraction of first/last name from returned data.
Upgrade notes
^^^^^^^^^^^^^
- API: the `update_pull_request` method will no longer support a close action.
Users should use the existing `close_pull_request` method which allows
specifying a message and status while closing a pull request.

View file

@ -9,6 +9,7 @@ Release Notes
.. toctree::
:maxdepth: 1
release-notes-4.8.0.rst
release-notes-4.7.2.rst
release-notes-4.7.1.rst
release-notes-4.7.0.rst

View file

@ -3,8 +3,13 @@
Getting Started with VCS
------------------------
When using |RCM|, you will be working with |git| or |hg| |repos| from the
command line.
When using |RCM|, you will be working with |git|, |svn| or |hg| |repos| from the
command line or using a GUI client such as Tortoise, Tower or SourceTree.
|RCM| uses a standard |git|, |svn| and |hg| protocols. So all tools that
can interact with there protocols are supported, including Eclipse or PyCharm
plugins.
If you have never used either before, the following information should
help you set up your local machine so that you can sync changes with the

View file

@ -7,7 +7,7 @@ buildEnv { name = "bower-env"; ignoreCollisions = true; paths = [
(fetchbower "paper-tooltip" "PolymerElements/paper-tooltip#1.1.3" "PolymerElements/paper-tooltip#^1.1.2" "0vmrm1n8k9sk9nvqy03q177axy22pia6i3j1gxbk72j3pqiqvg6k")
(fetchbower "paper-toast" "PolymerElements/paper-toast#1.3.0" "PolymerElements/paper-toast#^1.3.0" "0x9rqxsks5455s8pk4aikpp99ijdn6kxr9gvhwh99nbcqdzcxq1m")
(fetchbower "paper-toggle-button" "PolymerElements/paper-toggle-button#1.2.0" "PolymerElements/paper-toggle-button#^1.2.0" "0mphcng3ngspbpg4jjn0mb91nvr4xc1phq3qswib15h6sfww1b2w")
(fetchbower "iron-ajax" "PolymerElements/iron-ajax#1.4.3" "PolymerElements/iron-ajax#^1.4.3" "0m3dx27arwmlcp00b7n516sc5a51f40p9vapr1nvd57l3i3z0pzm")
(fetchbower "iron-ajax" "PolymerElements/iron-ajax#1.4.3" "PolymerElements/iron-ajax#^1.4.3" "1b1z3112ggjdflgrwbpmnbsh3kgcm4hn255wshvrlzds4w069gja")
(fetchbower "iron-autogrow-textarea" "PolymerElements/iron-autogrow-textarea#1.0.13" "PolymerElements/iron-autogrow-textarea#^1.0.13" "0zwhpl97vii1s8k0lgain8i9dnw29b0mxc5ixdscx9las13n2lqq")
(fetchbower "iron-a11y-keys" "PolymerElements/iron-a11y-keys#1.0.6" "PolymerElements/iron-a11y-keys#^1.0.6" "1xz3mgghfcxixq28sdb654iaxj4nyi1bzcwf77ydkms6fviqs9mv")
(fetchbower "iron-flex-layout" "PolymerElements/iron-flex-layout#1.3.1" "PolymerElements/iron-flex-layout#^1.0.0" "0nswv3ih3bhflgcd2wjfmddqswzgqxb2xbq65jk9w3rkj26hplbl")

View file

@ -601,13 +601,13 @@
};
};
deform = super.buildPythonPackage {
name = "deform-2.0a2";
name = "deform-2.0.4";
buildInputs = with self; [];
doCheck = false;
propagatedBuildInputs = with self; [Chameleon colander peppercorn translationstring zope.deprecation];
propagatedBuildInputs = with self; [Chameleon colander iso8601 peppercorn translationstring zope.deprecation];
src = fetchurl {
url = "https://pypi.python.org/packages/8d/b3/aab57e81da974a806dc9c5fa024a6404720f890a6dcf2e80885e3cb4609a/deform-2.0a2.tar.gz";
md5 = "7a90d41f7fbc18002ce74f39bd90a5e4";
url = "https://pypi.python.org/packages/66/3b/eefcb07abcab7a97f6665aa2d0cf1af741d9d6e78a2e4657fd2b89f89880/deform-2.0.4.tar.gz";
md5 = "34756e42cf50dd4b4430809116c4ec0a";
};
meta = {
license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
@ -887,13 +887,13 @@
};
};
ipython-genutils = super.buildPythonPackage {
name = "ipython-genutils-0.1.0";
name = "ipython-genutils-0.2.0";
buildInputs = with self; [];
doCheck = false;
propagatedBuildInputs = with self; [];
src = fetchurl {
url = "https://pypi.python.org/packages/71/b7/a64c71578521606edbbce15151358598f3dfb72a3431763edc2baf19e71f/ipython_genutils-0.1.0.tar.gz";
md5 = "9a8afbe0978adbcbfcb3b35b2d015a56";
url = "https://pypi.python.org/packages/e8/69/fbeffffc05236398ebfcfb512b6d2511c622871dca1746361006da310399/ipython_genutils-0.2.0.tar.gz";
md5 = "5a4f9781f78466da0ea1a648f3e1f79f";
};
meta = {
license = [ pkgs.lib.licenses.bsdOriginal ];
@ -1004,13 +1004,13 @@
};
};
mistune = super.buildPythonPackage {
name = "mistune-0.7.3";
name = "mistune-0.7.4";
buildInputs = with self; [];
doCheck = false;
propagatedBuildInputs = with self; [];
src = fetchurl {
url = "https://pypi.python.org/packages/88/1e/be99791262b3a794332fda598a07c2749a433b9378586361ba9d8e824607/mistune-0.7.3.tar.gz";
md5 = "4eba50bd121b83716fa4be6a4049004b";
url = "https://pypi.python.org/packages/25/a4/12a584c0c59c9fed529f8b3c47ca8217c0cf8bcc5e1089d3256410cfbdbc/mistune-0.7.4.tar.gz";
md5 = "92d01cb717e9e74429e9bde9d29ac43b";
};
meta = {
license = [ pkgs.lib.licenses.bsdOriginal ];
@ -1082,13 +1082,13 @@
};
};
objgraph = super.buildPythonPackage {
name = "objgraph-2.0.0";
name = "objgraph-3.1.0";
buildInputs = with self; [];
doCheck = false;
propagatedBuildInputs = with self; [];
src = fetchurl {
url = "https://pypi.python.org/packages/d7/33/ace750b59247496ed769b170586c5def7202683f3d98e737b75b767ff29e/objgraph-2.0.0.tar.gz";
md5 = "25b0d5e5adc74aa63ead15699614159c";
url = "https://pypi.python.org/packages/f4/b3/082e54e62094cb2ec84f8d5a49e0142cef99016491cecba83309cff920ae/objgraph-3.1.0.tar.gz";
md5 = "eddbd96039796bfbd13eee403701e64a";
};
meta = {
license = [ pkgs.lib.licenses.mit ];
@ -1186,13 +1186,13 @@
};
};
prompt-toolkit = super.buildPythonPackage {
name = "prompt-toolkit-1.0.13";
name = "prompt-toolkit-1.0.14";
buildInputs = with self; [];
doCheck = false;
propagatedBuildInputs = with self; [six wcwidth];
src = fetchurl {
url = "https://pypi.python.org/packages/23/be/4876b52d5cc159cbd4b0ff6e7aa419a26470849a43a8f647857a4a24467b/prompt_toolkit-1.0.13.tar.gz";
md5 = "427b496d2c147bd3819bc3a7f6e0d493";
url = "https://pypi.python.org/packages/55/56/8c39509b614bda53e638b7500f12577d663ac1b868aef53426fc6a26c3f5/prompt_toolkit-1.0.14.tar.gz";
md5 = "f24061ae133ed32c6b764e92bd48c496";
};
meta = {
license = [ pkgs.lib.licenses.bsdOriginal ];
@ -1641,7 +1641,7 @@
};
};
rhodecode-enterprise-ce = super.buildPythonPackage {
name = "rhodecode-enterprise-ce-4.7.2";
name = "rhodecode-enterprise-ce-4.8.0";
buildInputs = with self; [pytest py pytest-cov pytest-sugar pytest-runner pytest-catchlog pytest-profiling gprof2dot pytest-timeout mock WebTest cov-core coverage configobj];
doCheck = true;
propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments pygments-markdown-lexer Pylons Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress cssselect celery channelstream colander decorator deform docutils gevent gunicorn infrae.cache ipython iso8601 kombu lxml msgpack-python nbconvert packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson subprocess32 waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt];

View file

@ -16,7 +16,7 @@ colander==1.2
configobj==5.0.6
cssselect==1.0.1
decorator==4.0.11
deform==2.0a2
deform==2.0.4
docutils==0.12
dogpile.cache==0.6.1
dogpile.core==0.4.1
@ -38,7 +38,7 @@ meld3==1.0.2
msgpack-python==0.4.8
MySQL-python==1.2.5
nose==1.3.6
objgraph==2.0.0
objgraph==3.1.0
packaging==15.2
paramiko==1.15.1
Paste==2.0.3

View file

@ -1 +1 @@
4.7.2
4.8.0

View file

@ -51,7 +51,7 @@ PYRAMID_SETTINGS = {}
EXTENSIONS = {}
__version__ = ('.'.join((str(each) for each in VERSION[:3])))
__dbversion__ = 71 # defines current db version for migrations
__dbversion__ = 78 # defines current db version for migrations
__platform__ = platform.system()
__license__ = 'AGPLv3, and Commercial License'
__author__ = 'RhodeCode GmbH'

View file

@ -35,8 +35,9 @@ from pyramid.httpexceptions import HTTPNotFound
from rhodecode.api.exc import (
JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
from rhodecode.apps._base import TemplateArgs
from rhodecode.lib.auth import AuthUser
from rhodecode.lib.base import get_ip_addr
from rhodecode.lib.base import get_ip_addr, attach_context_attributes
from rhodecode.lib.ext_json import json
from rhodecode.lib.utils2 import safe_str
from rhodecode.lib.plugins.utils import get_plugin_settings
@ -278,6 +279,11 @@ def request_view(request):
'request': request,
'apiuser': auth_u
})
# register some common functions for usage
attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id,
attach_to_request=True)
try:
ret_value = func(**call_params)
return jsonrpc_response(request, ret_value)

View file

@ -43,16 +43,16 @@ class TestClosePullRequest(object):
response = api_call(self.app, params)
expected = {
'pull_request_id': pull_request_id,
'close_status': 'Rejected',
'closed': True,
}
assert_ok(id_, expected, response.body)
action = 'user_closed_pull_request:%d' % pull_request_id
journal = UserLog.query()\
.filter(UserLog.user_id == author)\
.filter(UserLog.user_id == author) \
.order_by('user_log_id') \
.filter(UserLog.repository_id == repo)\
.filter(UserLog.action == action)\
.all()
assert len(journal) == 1
assert journal[-1].action == 'repo.pull_request.close'
@pytest.mark.backends("git", "hg")
def test_api_close_pull_request_already_closed_error(self, pr_util):

View file

@ -62,13 +62,12 @@ class TestCommentPullRequest(object):
}
assert_ok(id_, expected, response.body)
action = 'user_commented_pull_request:%d' % pull_request_id
journal = UserLog.query()\
.filter(UserLog.user_id == author)\
.filter(UserLog.repository_id == repo)\
.filter(UserLog.action == action)\
.filter(UserLog.repository_id == repo) \
.order_by('user_log_id') \
.all()
assert len(journal) == 2
assert journal[-1].action == 'repo.pull_request.comment.create'
@pytest.mark.backends("git", "hg")
def test_api_comment_pull_request_change_status(

View file

@ -77,7 +77,7 @@ class TestCreatePullRequestApi(object):
assert pull_request.source_repo.repo_name == data['source_repo']
assert pull_request.target_repo.repo_name == data['target_repo']
assert pull_request.revisions == [self.commit_ids['change']]
assert pull_request.reviewers == []
assert len(pull_request.reviewers) == 1
@pytest.mark.backends("git", "hg")
def test_create_with_empty_description(self, backend):
@ -98,7 +98,12 @@ class TestCreatePullRequestApi(object):
def test_create_with_reviewers_specified_by_names(
self, backend, no_notifications):
data = self._prepare_data(backend)
reviewers = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN]
reviewers = [
{'username': TEST_USER_REGULAR_LOGIN,
'reasons': ['added manually']},
{'username': TEST_USER_ADMIN_LOGIN,
'reasons': ['added manually']},
]
data['reviewers'] = reviewers
id_, params = build_data(
self.apikey_regular, 'create_pull_request', **data)
@ -110,16 +115,26 @@ class TestCreatePullRequestApi(object):
assert result['result']['msg'] == expected_message
pull_request_id = result['result']['pull_request_id']
pull_request = PullRequestModel().get(pull_request_id)
actual_reviewers = [r.user.username for r in pull_request.reviewers]
actual_reviewers = [
{'username': r.user.username,
'reasons': ['added manually'],
} for r in pull_request.reviewers
]
assert sorted(actual_reviewers) == sorted(reviewers)
@pytest.mark.backends("git", "hg")
def test_create_with_reviewers_specified_by_ids(
self, backend, no_notifications):
data = self._prepare_data(backend)
reviewer_names = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN]
reviewers = [
UserModel().get_by_username(n).user_id for n in reviewer_names]
{'username': UserModel().get_by_username(
TEST_USER_REGULAR_LOGIN).user_id,
'reasons': ['added manually']},
{'username': UserModel().get_by_username(
TEST_USER_ADMIN_LOGIN).user_id,
'reasons': ['added manually']},
]
data['reviewers'] = reviewers
id_, params = build_data(
self.apikey_regular, 'create_pull_request', **data)
@ -131,14 +146,17 @@ class TestCreatePullRequestApi(object):
assert result['result']['msg'] == expected_message
pull_request_id = result['result']['pull_request_id']
pull_request = PullRequestModel().get(pull_request_id)
actual_reviewers = [r.user.username for r in pull_request.reviewers]
assert sorted(actual_reviewers) == sorted(reviewer_names)
actual_reviewers = [
{'username': r.user.user_id,
'reasons': ['added manually'],
} for r in pull_request.reviewers
]
assert sorted(actual_reviewers) == sorted(reviewers)
@pytest.mark.backends("git", "hg")
def test_create_fails_when_the_reviewer_is_not_found(self, backend):
data = self._prepare_data(backend)
reviewers = ['somebody']
data['reviewers'] = reviewers
data['reviewers'] = [{'username': 'somebody'}]
id_, params = build_data(
self.apikey_regular, 'create_pull_request', **data)
response = api_call(self.app, params)
@ -153,7 +171,7 @@ class TestCreatePullRequestApi(object):
id_, params = build_data(
self.apikey_regular, 'create_pull_request', **data)
response = api_call(self.app, params)
expected_message = 'reviewers should be specified as a list'
expected_message = {u'': '"test_regular,test_admin" is not iterable'}
assert_error(id_, expected_message, given=response.body)
@pytest.mark.backends("git", "hg")

View file

@ -59,6 +59,21 @@ class TestCreateUser(object):
expected = "email `%s` already exist" % (TEST_USER_REGULAR_EMAIL,)
assert_error(id_, expected, given=response.body)
def test_api_create_user_with_wrong_username(self):
bad_username = '<> HELLO WORLD <>'
id_, params = build_data(
self.apikey, 'create_user',
username=bad_username,
email='new@email.com',
password='trololo')
response = api_call(self.app, params)
expected = {'username':
"Username may only contain alphanumeric characters "
"underscores, periods or dashes and must begin with "
"alphanumeric character or underscore"}
assert_error(id_, expected, given=response.body)
def test_api_create_user(self):
username = 'test_new_api_user'
email = username + "@foo.com"
@ -175,7 +190,6 @@ class TestCreateUser(object):
fixture.destroy_repo_group(username)
fixture.destroy_user(usr.user_id)
@mock.patch.object(UserModel, 'create_or_update', crash)
def test_api_create_user_when_exception_happened(self):

View file

@ -112,3 +112,16 @@ class TestCreateUserGroup(object):
expected = 'failed to create group `%s`' % (group_name,)
assert_error(id_, expected, given=response.body)
def test_api_create_user_group_with_wrong_name(self, user_util):
group_name = 'wrong NAME <>'
id_, params = build_data(
self.apikey, 'create_user_group', group_name=group_name)
response = api_call(self.app, params)
expected = {"user_group_name":
"Allowed in name are letters, numbers, and `-`, `_`, "
"`.` Name must start with a letter or number. "
"Got `{}`".format(group_name)}
assert_error(id_, expected, given=response.body)

View file

@ -30,43 +30,45 @@ from rhodecode.api.tests.utils import (
class TestApiDeleteRepo(object):
def test_api_delete_repo(self, backend):
repo = backend.create_repo()
repo_name = repo.repo_name
id_, params = build_data(
self.apikey, 'delete_repo', repoid=repo.repo_name, )
response = api_call(self.app, params)
expected = {
'msg': 'Deleted repository `%s`' % (repo.repo_name,),
'msg': 'Deleted repository `%s`' % (repo_name,),
'success': True
}
assert_ok(id_, expected, given=response.body)
def test_api_delete_repo_by_non_admin(self, backend, user_regular):
repo = backend.create_repo(cur_user=user_regular.username)
repo_name = repo.repo_name
id_, params = build_data(
user_regular.api_key, 'delete_repo', repoid=repo.repo_name, )
response = api_call(self.app, params)
expected = {
'msg': 'Deleted repository `%s`' % (repo.repo_name,),
'msg': 'Deleted repository `%s`' % (repo_name,),
'success': True
}
assert_ok(id_, expected, given=response.body)
def test_api_delete_repo_by_non_admin_no_permission(self, backend):
repo = backend.create_repo()
repo_name = repo.repo_name
id_, params = build_data(
self.apikey_regular, 'delete_repo', repoid=repo.repo_name, )
response = api_call(self.app, params)
expected = 'repository `%s` does not exist' % (repo.repo_name)
expected = 'repository `%s` does not exist' % (repo_name)
assert_error(id_, expected, given=response.body)
def test_api_delete_repo_exception_occurred(self, backend):
repo = backend.create_repo()
repo_name = repo.repo_name
id_, params = build_data(
self.apikey, 'delete_repo', repoid=repo.repo_name, )
with mock.patch.object(RepoModel, 'delete', crash):
response = api_call(self.app, params)
expected = 'failed to delete repository `%s`' % (
repo.repo_name,)
expected = 'failed to delete repository `%s`' % (repo_name,)
assert_error(id_, expected, given=response.body)

View file

@ -28,7 +28,7 @@ from rhodecode.api.tests.utils import (
@pytest.mark.usefixtures("testuser_api", "app")
class TestApiGetGist(object):
def test_api_get_gist(self, gist_util):
def test_api_get_gist(self, gist_util, http_host_stub):
gist = gist_util.create_gist()
gist_id = gist.gist_access_id
gist_created_on = gist.created_on
@ -45,14 +45,14 @@ class TestApiGetGist(object):
'expires': -1.0,
'gist_id': int(gist_id),
'type': 'public',
'url': 'http://test.example.com:80/_admin/gists/%s' % (gist_id,),
'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,),
'acl_level': Gist.ACL_LEVEL_PUBLIC,
'content': None,
}
assert_ok(id_, expected, given=response.body)
def test_api_get_gist_with_content(self, gist_util):
def test_api_get_gist_with_content(self, gist_util, http_host_stub):
mapping = {
u'filename1.txt': {'content': u'hello world'},
u'filename1ą.txt': {'content': u'hello worldę'}
@ -73,7 +73,7 @@ class TestApiGetGist(object):
'expires': -1.0,
'gist_id': int(gist_id),
'type': 'public',
'url': 'http://test.example.com:80/_admin/gists/%s' % (gist_id,),
'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,),
'acl_level': Gist.ACL_LEVEL_PUBLIC,
'content': {
u'filename1.txt': u'hello world',

View file

@ -19,13 +19,13 @@
# and proprietary license terms, please see https://rhodecode.com/licenses/
import mock
import pytest
import urlobject
from pylons import url
from rhodecode.api.tests.utils import (
build_data, api_call, assert_error, assert_ok)
from rhodecode.lib.utils2 import safe_unicode
pytestmark = pytest.mark.backends("git", "hg")
@ -33,7 +33,7 @@ pytestmark = pytest.mark.backends("git", "hg")
@pytest.mark.usefixtures("testuser_api", "app")
class TestGetPullRequest(object):
def test_api_get_pull_request(self, pr_util):
def test_api_get_pull_request(self, pr_util, http_host_only_stub):
from rhodecode.model.pull_request import PullRequestModel
pull_request = pr_util.create_pull_request(mergeable=True)
id_, params = build_data(
@ -50,16 +50,16 @@ class TestGetPullRequest(object):
'pullrequest_show',
repo_name=pull_request.target_repo.repo_name,
pull_request_id=pull_request.pull_request_id, qualified=True))
pr_url = unicode(
url_obj.with_netloc('test.example.com:80'))
source_url = unicode(
pull_request.source_repo.clone_url()
.with_netloc('test.example.com:80'))
target_url = unicode(
pull_request.target_repo.clone_url()
.with_netloc('test.example.com:80'))
shadow_url = unicode(
pr_url = safe_unicode(
url_obj.with_netloc(http_host_only_stub))
source_url = safe_unicode(
pull_request.source_repo.clone_url().with_netloc(http_host_only_stub))
target_url = safe_unicode(
pull_request.target_repo.clone_url().with_netloc(http_host_only_stub))
shadow_url = safe_unicode(
PullRequestModel().get_shadow_clone_url(pull_request))
expected = {
'pull_request_id': pull_request.pull_request_id,
'url': pr_url,
@ -109,7 +109,8 @@ class TestGetPullRequest(object):
'reasons': reasons,
'review_status': st[0][1].status if st else 'not_reviewed',
}
for reviewer, reasons, st in pull_request.reviewers_statuses()
for reviewer, reasons, mandatory, st in
pull_request.reviewers_statuses()
]
}
assert_ok(id_, expected, response.body)

View file

@ -95,13 +95,13 @@ class TestMergePullRequest(object):
assert_ok(id_, expected, response.body)
action = 'user_merged_pull_request:%d' % (pull_request_id, )
journal = UserLog.query()\
.filter(UserLog.user_id == author)\
.filter(UserLog.repository_id == repo)\
.filter(UserLog.action == action)\
.filter(UserLog.repository_id == repo) \
.order_by('user_log_id') \
.all()
assert len(journal) == 1
assert journal[-2].action == 'repo.pull_request.merge'
assert journal[-1].action == 'repo.pull_request.close'
id_, params = build_data(
self.apikey, 'merge_pull_request',

View file

@ -33,7 +33,7 @@ class TestUpdatePullRequest(object):
@pytest.mark.backends("git", "hg")
def test_api_update_pull_request_title_or_description(
self, pr_util, silence_action_logger, no_notifications):
self, pr_util, no_notifications):
pull_request = pr_util.create_pull_request()
id_, params = build_data(
@ -61,7 +61,7 @@ class TestUpdatePullRequest(object):
@pytest.mark.backends("git", "hg")
def test_api_try_update_closed_pull_request(
self, pr_util, silence_action_logger, no_notifications):
self, pr_util, no_notifications):
pull_request = pr_util.create_pull_request()
PullRequestModel().close_pull_request(
pull_request, TEST_USER_ADMIN_LOGIN)
@ -78,8 +78,7 @@ class TestUpdatePullRequest(object):
assert_error(id_, expected, response.body)
@pytest.mark.backends("git", "hg")
def test_api_update_update_commits(
self, pr_util, silence_action_logger, no_notifications):
def test_api_update_update_commits(self, pr_util, no_notifications):
commits = [
{'message': 'a'},
{'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
@ -119,20 +118,28 @@ class TestUpdatePullRequest(object):
@pytest.mark.backends("git", "hg")
def test_api_update_change_reviewers(
self, pr_util, silence_action_logger, no_notifications):
self, user_util, pr_util, no_notifications):
a = user_util.create_user()
b = user_util.create_user()
c = user_util.create_user()
new_reviewers = [
{'username': b.username,'reasons': ['updated via API'],
'mandatory':False},
{'username': c.username, 'reasons': ['updated via API'],
'mandatory':False},
]
users = [x.username for x in User.get_all()]
new = [users.pop(0)]
removed = sorted(new)
added = sorted(users)
added = [b.username, c.username]
removed = [a.username]
pull_request = pr_util.create_pull_request(reviewers=new)
pull_request = pr_util.create_pull_request(
reviewers=[(a.username, ['added via API'], False)])
id_, params = build_data(
self.apikey, 'update_pull_request',
repoid=pull_request.target_repo.repo_name,
pullrequestid=pull_request.pull_request_id,
reviewers=added)
reviewers=new_reviewers)
response = api_call(self.app, params)
expected = {
"msg": "Updated pull request `{}`".format(
@ -152,7 +159,7 @@ class TestUpdatePullRequest(object):
self.apikey, 'update_pull_request',
repoid=pull_request.target_repo.repo_name,
pullrequestid=pull_request.pull_request_id,
reviewers=['bad_name'])
reviewers=[{'username': 'bad_name'}])
response = api_call(self.app, params)
expected = 'user `bad_name` does not exist'
@ -165,7 +172,7 @@ class TestUpdatePullRequest(object):
self.apikey, 'update_pull_request',
repoid='fake',
pullrequestid='fake',
reviewers=['bad_name'])
reviewers=[{'username': 'bad_name'}])
response = api_call(self.app, params)
expected = 'repository `fake` does not exist'
@ -181,7 +188,7 @@ class TestUpdatePullRequest(object):
self.apikey, 'update_pull_request',
repoid=pull_request.target_repo.repo_name,
pullrequestid=999999,
reviewers=['bad_name'])
reviewers=[{'username': 'bad_name'}])
response = api_call(self.app, params)
expected = 'pull request `999999` does not exist'

View file

@ -26,7 +26,7 @@ from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
from rhodecode.api.tests.utils import (
build_data, api_call, assert_error, assert_ok, crash, jsonify)
from rhodecode.tests.fixture import Fixture
from rhodecode.tests.plugin import http_host_stub, http_host_only_stub
fixture = Fixture()
@ -71,14 +71,15 @@ class TestApiUpdateRepo(object):
({'repo_name': 'new_repo_name'},
{
'repo_name': 'new_repo_name',
'url': 'http://test.example.com:80/new_repo_name'
'url': 'http://{}/new_repo_name'.format(http_host_only_stub())
}),
({'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME),
'_group': 'test_group_for_update'},
{
'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME),
'url': 'http://test.example.com:80/test_group_for_update/{}'.format(UPDATE_REPO_NAME)
'url': 'http://{}/test_group_for_update/{}'.format(
http_host_only_stub(), UPDATE_REPO_NAME)
}),
])
def test_api_update_repo(self, updates, expected, backend):
@ -115,7 +116,8 @@ class TestApiUpdateRepo(object):
master_repo = backend.create_repo()
repo = backend.create_repo()
updates = {
'fork_of': master_repo.repo_name
'fork_of': master_repo.repo_name,
'fork_of_id': master_repo.repo_id
}
expected_api_data = repo.get_api_data(include_secrets=True)
expected_api_data.update(updates)
@ -130,6 +132,7 @@ class TestApiUpdateRepo(object):
assert_ok(id_, expected, given=response.body)
result = response.json['result']['repository']
assert result['fork_of'] == master_repo.repo_name
assert result['fork_of_id'] == master_repo.repo_id
def test_api_update_repo_fork_of_not_found(self, backend):
master_repo_name = 'fake-parent-repo'

View file

@ -21,7 +21,8 @@
import logging
from rhodecode.api import jsonrpc_method, JSONRPCError
from rhodecode import events
from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
from rhodecode.api.utils import (
has_superadmin_permission, Optional, OAttr, get_repo_or_error,
get_pull_request_or_error, get_commit_or_error, get_user_or_error,
@ -34,6 +35,9 @@ from rhodecode.model.comment import CommentsModel
from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
from rhodecode.model.pull_request import PullRequestModel, MergeCheck
from rhodecode.model.settings import SettingsModel
from rhodecode.model.validation_schema import Invalid
from rhodecode.model.validation_schema.schemas.reviewer_schema import(
ReviewerListSchema)
log = logging.getLogger(__name__)
@ -224,8 +228,9 @@ def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
@jsonrpc_method()
def merge_pull_request(request, apiuser, repoid, pullrequestid,
userid=Optional(OAttr('apiuser'))):
def merge_pull_request(
request, apiuser, repoid, pullrequestid,
userid=Optional(OAttr('apiuser'))):
"""
Merge the pull request specified by `pullrequestid` into its target
repository.
@ -273,7 +278,12 @@ def merge_pull_request(request, apiuser, repoid, pullrequestid,
merge_possible = not check.failed
if not merge_possible:
reasons = ','.join([msg for _e, msg in check.errors])
error_messages = []
for err_type, error_msg in check.errors:
error_msg = request.translate(error_msg)
error_messages.append(error_msg)
reasons = ','.join(error_messages)
raise JSONRPCError(
'merge not possible for following reasons: {}'.format(reasons))
@ -299,63 +309,6 @@ def merge_pull_request(request, apiuser, repoid, pullrequestid,
return merge_response
@jsonrpc_method()
def close_pull_request(request, apiuser, repoid, pullrequestid,
userid=Optional(OAttr('apiuser'))):
"""
Close the pull request specified by `pullrequestid`.
:param apiuser: This is filled automatically from the |authtoken|.
:type apiuser: AuthUser
:param repoid: Repository name or repository ID to which the pull
request belongs.
:type repoid: str or int
:param pullrequestid: ID of the pull request to be closed.
:type pullrequestid: int
:param userid: Close the pull request as this user.
:type userid: Optional(str or int)
Example output:
.. code-block:: bash
"id": <id_given_in_input>,
"result": {
"pull_request_id": "<int>",
"closed": "<bool>"
},
"error": null
"""
repo = get_repo_or_error(repoid)
if not isinstance(userid, Optional):
if (has_superadmin_permission(apiuser) or
HasRepoPermissionAnyApi('repository.admin')(
user=apiuser, repo_name=repo.repo_name)):
apiuser = get_user_or_error(userid)
else:
raise JSONRPCError('userid is not the same as your user')
pull_request = get_pull_request_or_error(pullrequestid)
if not PullRequestModel().check_user_update(
pull_request, apiuser, api=True):
raise JSONRPCError(
'pull request `%s` close failed, no permission to close.' % (
pullrequestid,))
if pull_request.is_closed():
raise JSONRPCError(
'pull request `%s` is already closed' % (pullrequestid,))
PullRequestModel().close_pull_request(
pull_request.pull_request_id, apiuser)
Session().commit()
data = {
'pull_request_id': pull_request.pull_request_id,
'closed': True,
}
return data
@jsonrpc_method()
def comment_pull_request(
request, apiuser, repoid, pullrequestid, message=Optional(None),
@ -529,24 +482,26 @@ def create_pull_request(
:param description: Set the pull request description.
:type description: Optional(str)
:param reviewers: Set the new pull request reviewers list.
Reviewer defined by review rules will be added automatically to the
defined list.
:type reviewers: Optional(list)
Accepts username strings or objects of the format:
{'username': 'nick', 'reasons': ['original author']}
[{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
"""
source = get_repo_or_error(source_repo)
target = get_repo_or_error(target_repo)
source_db_repo = get_repo_or_error(source_repo)
target_db_repo = get_repo_or_error(target_repo)
if not has_superadmin_permission(apiuser):
_perms = ('repository.admin', 'repository.write', 'repository.read',)
validate_repo_permissions(apiuser, source_repo, source, _perms)
validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
full_source_ref = resolve_ref_or_error(source_ref, source)
full_target_ref = resolve_ref_or_error(target_ref, target)
source_commit = get_commit_or_error(full_source_ref, source)
target_commit = get_commit_or_error(full_target_ref, target)
source_scm = source.scm_instance()
target_scm = target.scm_instance()
full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
source_commit = get_commit_or_error(full_source_ref, source_db_repo)
target_commit = get_commit_or_error(full_target_ref, target_db_repo)
source_scm = source_db_repo.scm_instance()
target_scm = target_db_repo.scm_instance()
commit_ranges = target_scm.compare(
target_commit.raw_id, source_commit.raw_id, source_scm,
@ -562,20 +517,36 @@ def create_pull_request(
raise JSONRPCError('no common ancestor found')
reviewer_objects = Optional.extract(reviewers) or []
if not isinstance(reviewer_objects, list):
raise JSONRPCError('reviewers should be specified as a list')
reviewers_reasons = []
for reviewer_object in reviewer_objects:
reviewer_reasons = []
if isinstance(reviewer_object, (basestring, int)):
reviewer_username = reviewer_object
else:
reviewer_username = reviewer_object['username']
reviewer_reasons = reviewer_object.get('reasons', [])
if reviewer_objects:
schema = ReviewerListSchema()
try:
reviewer_objects = schema.deserialize(reviewer_objects)
except Invalid as err:
raise JSONRPCValidationError(colander_exc=err)
user = get_user_or_error(reviewer_username)
reviewers_reasons.append((user.user_id, reviewer_reasons))
# validate users
for reviewer_object in reviewer_objects:
user = get_user_or_error(reviewer_object['username'])
reviewer_object['user_id'] = user.user_id
get_default_reviewers_data, get_validated_reviewers = \
PullRequestModel().get_reviewer_functions()
reviewer_rules = get_default_reviewers_data(
apiuser.get_instance(), source_db_repo,
source_commit, target_db_repo, target_commit)
# specified rules are later re-validated, thus we can assume users will
# eventually provide those that meet the reviewer criteria.
if not reviewer_objects:
reviewer_objects = reviewer_rules['reviewers']
try:
reviewers = get_validated_reviewers(
reviewer_objects, reviewer_rules)
except ValueError as e:
raise JSONRPCError('Reviewers Validation: {}'.format(e))
pull_request_model = PullRequestModel()
pull_request = pull_request_model.create(
@ -586,7 +557,7 @@ def create_pull_request(
target_ref=full_target_ref,
revisions=reversed(
[commit.raw_id for commit in reversed(commit_ranges)]),
reviewers=reviewers_reasons,
reviewers=reviewers,
title=title,
description=Optional.extract(description)
)
@ -603,7 +574,7 @@ def create_pull_request(
def update_pull_request(
request, apiuser, repoid, pullrequestid, title=Optional(''),
description=Optional(''), reviewers=Optional(None),
update_commits=Optional(None), close_pull_request=Optional(None)):
update_commits=Optional(None)):
"""
Updates a pull request.
@ -619,10 +590,12 @@ def update_pull_request(
:type description: Optional(str)
:param reviewers: Update pull request reviewers list with new value.
:type reviewers: Optional(list)
Accepts username strings or objects of the format:
[{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
:param update_commits: Trigger update of commits for this pull request
:type: update_commits: Optional(bool)
:param close_pull_request: Close this pull request with rejected state
:type: close_pull_request: Optional(bool)
Example output:
@ -665,29 +638,38 @@ def update_pull_request(
pullrequestid,))
reviewer_objects = Optional.extract(reviewers) or []
if not isinstance(reviewer_objects, list):
raise JSONRPCError('reviewers should be specified as a list')
reviewers_reasons = []
reviewer_ids = set()
for reviewer_object in reviewer_objects:
reviewer_reasons = []
if isinstance(reviewer_object, (int, basestring)):
reviewer_username = reviewer_object
else:
reviewer_username = reviewer_object['username']
reviewer_reasons = reviewer_object.get('reasons', [])
if reviewer_objects:
schema = ReviewerListSchema()
try:
reviewer_objects = schema.deserialize(reviewer_objects)
except Invalid as err:
raise JSONRPCValidationError(colander_exc=err)
user = get_user_or_error(reviewer_username)
reviewer_ids.add(user.user_id)
reviewers_reasons.append((user.user_id, reviewer_reasons))
# validate users
for reviewer_object in reviewer_objects:
user = get_user_or_error(reviewer_object['username'])
reviewer_object['user_id'] = user.user_id
get_default_reviewers_data, get_validated_reviewers = \
PullRequestModel().get_reviewer_functions()
# re-use stored rules
reviewer_rules = pull_request.reviewer_data
try:
reviewers = get_validated_reviewers(
reviewer_objects, reviewer_rules)
except ValueError as e:
raise JSONRPCError('Reviewers Validation: {}'.format(e))
else:
reviewers = []
title = Optional.extract(title)
description = Optional.extract(description)
if title or description:
PullRequestModel().edit(
pull_request, title or pull_request.title,
description or pull_request.description)
description or pull_request.description, apiuser)
Session().commit()
commit_changes = {"added": [], "common": [], "removed": []}
@ -699,9 +681,9 @@ def update_pull_request(
Session().commit()
reviewers_changes = {"added": [], "removed": []}
if reviewer_ids:
if reviewers:
added_reviewers, removed_reviewers = \
PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
reviewers_changes['added'] = sorted(
[get_user_or_error(n).username for n in added_reviewers])
@ -709,11 +691,6 @@ def update_pull_request(
[get_user_or_error(n).username for n in removed_reviewers])
Session().commit()
if str2bool(Optional.extract(close_pull_request)):
PullRequestModel().close_pull_request_with_comment(
pull_request, apiuser, repo)
Session().commit()
data = {
'msg': 'Updated pull request `{}`'.format(
pull_request.pull_request_id),
@ -723,3 +700,80 @@ def update_pull_request(
}
return data
@jsonrpc_method()
def close_pull_request(
request, apiuser, repoid, pullrequestid,
userid=Optional(OAttr('apiuser')), message=Optional('')):
"""
Close the pull request specified by `pullrequestid`.
:param apiuser: This is filled automatically from the |authtoken|.
:type apiuser: AuthUser
:param repoid: Repository name or repository ID to which the pull
request belongs.
:type repoid: str or int
:param pullrequestid: ID of the pull request to be closed.
:type pullrequestid: int
:param userid: Close the pull request as this user.
:type userid: Optional(str or int)
:param message: Optional message to close the Pull Request with. If not
specified it will be generated automatically.
:type message: Optional(str)
Example output:
.. code-block:: bash
"id": <id_given_in_input>,
"result": {
"pull_request_id": "<int>",
"close_status": "<str:status_lbl>,
"closed": "<bool>"
},
"error": null
"""
_ = request.translate
repo = get_repo_or_error(repoid)
if not isinstance(userid, Optional):
if (has_superadmin_permission(apiuser) or
HasRepoPermissionAnyApi('repository.admin')(
user=apiuser, repo_name=repo.repo_name)):
apiuser = get_user_or_error(userid)
else:
raise JSONRPCError('userid is not the same as your user')
pull_request = get_pull_request_or_error(pullrequestid)
if pull_request.is_closed():
raise JSONRPCError(
'pull request `%s` is already closed' % (pullrequestid,))
# only owner or admin or person with write permissions
allowed_to_close = PullRequestModel().check_user_update(
pull_request, apiuser, api=True)
if not allowed_to_close:
raise JSONRPCError(
'pull request `%s` close failed, no permission to close.' % (
pullrequestid,))
# message we're using to close the PR, else it's automatically generated
message = Optional.extract(message)
# finally close the PR, with proper message comment
comment, status = PullRequestModel().close_pull_request_with_comment(
pull_request, apiuser, repo, message=message)
status_lbl = ChangesetStatus.get_status_lbl(status)
Session().commit()
data = {
'pull_request_id': pull_request.pull_request_id,
'close_status': status_lbl,
'closed': True,
}
return data

View file

@ -29,6 +29,8 @@ from rhodecode.api.utils import (
get_user_group_or_error, get_user_or_error, validate_repo_permissions,
get_perm_or_error, parse_args, get_origin, build_commit_data,
validate_set_owner_permissions)
from rhodecode.lib import audit_logger
from rhodecode.lib import repo_maintenance
from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
from rhodecode.lib.utils2 import str2bool, time_to_datetime
from rhodecode.lib.ext_json import json
@ -915,12 +917,13 @@ def update_repo(
ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo)
old_values = repo.get_api_data()
schema = repo_schema.RepoSchema().bind(
repo_type_options=rhodecode.BACKENDS.keys(),
repo_ref_options=ref_choices,
# user caller
user=apiuser,
old_values=repo.get_api_data())
old_values=old_values)
try:
schema_data = schema.deserialize(dict(
# we save old value, users cannot change type
@ -965,6 +968,9 @@ def update_repo(
try:
RepoModel().update(repo, **validated_updates)
audit_logger.store_api(
'repo.edit', action_data={'old_data': old_values},
user=apiuser, repo=repo)
Session().commit()
return {
'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
@ -1153,6 +1159,7 @@ def delete_repo(request, apiuser, repoid, forks=Optional('')):
"""
repo = get_repo_or_error(repoid)
repo_name = repo.repo_name
if not has_superadmin_permission(apiuser):
_perms = ('repository.admin',)
validate_repo_permissions(apiuser, repoid, repo, _perms)
@ -1170,18 +1177,26 @@ def delete_repo(request, apiuser, repoid, forks=Optional('')):
'Cannot delete `%s` it still contains attached forks' %
(repo.repo_name,)
)
old_data = repo.get_api_data()
RepoModel().delete(repo, forks=forks)
repo = audit_logger.RepoWrap(repo_id=None,
repo_name=repo.repo_name)
audit_logger.store_api(
'repo.delete', action_data={'old_data': old_data},
user=apiuser, repo=repo)
ScmModel().mark_for_invalidation(repo_name, delete=True)
Session().commit()
return {
'msg': 'Deleted repository `%s`%s' % (
repo.repo_name, _forks_msg),
'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
'success': True
}
except Exception:
log.exception("Exception occurred while trying to delete repo")
raise JSONRPCError(
'failed to delete repository `%s`' % (repo.repo_name,)
'failed to delete repository `%s`' % (repo_name,)
)
@ -1460,7 +1475,7 @@ def comment_commit(
rc_config = SettingsModel().get_all_settings()
renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
status_change_label = ChangesetStatus.get_status_lbl(status)
comm = CommentsModel().create(
comment = CommentsModel().create(
message, repo, user, commit_id=commit_id,
status_change=status_change_label,
status_change_type=status,
@ -1472,7 +1487,7 @@ def comment_commit(
# also do a status change
try:
ChangesetStatusModel().set_status(
repo, status, user, comm, revision=commit_id,
repo, status, user, comment, revision=commit_id,
dont_allow_on_closed_pull_request=True
)
except StatusChangeOnClosedPullRequestError:
@ -1486,7 +1501,7 @@ def comment_commit(
return {
'msg': (
'Commented on commit `%s` for repository `%s`' % (
comm.revision, repo.repo_name)),
comment.revision, repo.repo_name)),
'status_change': status,
'success': True,
}
@ -1867,6 +1882,11 @@ def strip(request, apiuser, repoid, revision, branch):
try:
ScmModel().strip(repo, revision, branch)
audit_logger.store_api(
'repo.commit.strip', action_data={'commit_id': revision},
repo=repo,
user=apiuser, commit=True)
return {
'msg': 'Stripped commit %s from repo `%s`' % (
revision, repo.repo_name),
@ -1902,6 +1922,7 @@ def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
"id": 237,
"result": {
"extensions_largefiles": true,
"extensions_evolve": true,
"hooks_changegroup_push_logger": true,
"hooks_changegroup_repo_size": false,
"hooks_outgoing_pull_logger": true,
@ -1985,3 +2006,65 @@ def set_repo_settings(request, apiuser, repoid, settings):
# Indicate success.
return True
@jsonrpc_method()
def maintenance(request, apiuser, repoid):
"""
Triggers a maintenance on the given repository.
This command can only be run using an |authtoken| with admin
rights to the specified repository. For more information,
see :ref:`config-token-ref`.
This command takes the following options:
:param apiuser: This is filled automatically from the |authtoken|.
:type apiuser: AuthUser
:param repoid: The repository name or repository ID.
:type repoid: str or int
Example output:
.. code-block:: bash
id : <id_given_in_input>
result : {
"msg": "executed maintenance command",
"executed_actions": [
<action_message>, <action_message2>...
],
"repository": "<repository name>"
}
error : null
Example error output:
.. code-block:: bash
id : <id_given_in_input>
result : null
error : {
"Unable to execute maintenance on `<reponame>`"
}
"""
repo = get_repo_or_error(repoid)
if not has_superadmin_permission(apiuser):
_perms = ('repository.admin',)
validate_repo_permissions(apiuser, repoid, repo, _perms)
try:
maintenance = repo_maintenance.RepoMaintenance()
executed_actions = maintenance.execute(repo)
return {
'msg': 'executed maintenance command',
'executed_actions': executed_actions,
'repository': repo.repo_name
}
except Exception:
log.exception("Exception occurred while trying to run maintenance")
raise JSONRPCError(
'Unable to execute maintenance on `%s`' % repo.repo_name)

View file

@ -27,6 +27,7 @@ from rhodecode.api.utils import (
has_superadmin_permission, Optional, OAttr, get_user_or_error,
get_repo_group_or_error, get_perm_or_error, get_user_group_or_error,
get_origin, validate_repo_group_permissions, validate_set_owner_permissions)
from rhodecode.lib import audit_logger
from rhodecode.lib.auth import (
HasRepoGroupPermissionAnyApi, HasUserGroupPermissionAnyApi)
from rhodecode.model.db import Session
@ -222,6 +223,13 @@ def create_repo_group(
group_name=validated_group_name,
group_description=schema_data['repo_group_name'],
copy_permissions=schema_data['repo_group_copy_permissions'])
Session().flush()
repo_group_data = repo_group.get_api_data()
audit_logger.store_api(
'repo_group.create', action_data={'data': repo_group_data},
user=apiuser)
Session().commit()
return {
'msg': 'Created new repo group `%s`' % validated_group_name,
@ -310,8 +318,13 @@ def update_repo_group(
enable_locking=schema_data['repo_group_enable_locking'],
)
old_data = repo_group.get_api_data()
try:
RepoGroupModel().update(repo_group, validated_updates)
audit_logger.store_api(
'repo_group.edit', action_data={'old_data': old_data},
user=apiuser)
Session().commit()
return {
'msg': 'updated repository group ID:%s %s' % (
@ -365,8 +378,12 @@ def delete_repo_group(request, apiuser, repogroupid):
validate_repo_group_permissions(
apiuser, repogroupid, repo_group, ('group.admin',))
old_data = repo_group.get_api_data()
try:
RepoGroupModel().delete(repo_group)
audit_logger.store_api(
'repo_group.delete', action_data={'old_data': old_data},
user=apiuser)
Session().commit()
return {
'msg': 'deleted repo group ID:%s %s' %

View file

@ -20,14 +20,18 @@
import logging
from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden
from rhodecode.api import (
jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
from rhodecode.api.utils import (
Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update)
from rhodecode.lib import audit_logger
from rhodecode.lib.auth import AuthUser, PasswordGenerator
from rhodecode.lib.exceptions import DefaultUserException
from rhodecode.lib.utils2 import safe_int, str2bool
from rhodecode.model.db import Session, User, Repository
from rhodecode.model.user import UserModel
from rhodecode.model import validation_schema
from rhodecode.model.validation_schema.schemas import user_schema
log = logging.getLogger(__name__)
@ -237,20 +241,54 @@ def create_user(request, apiuser, username, email, password=Optional(''),
if isinstance(create_repo_group, basestring):
create_repo_group = str2bool(create_repo_group)
username = Optional.extract(username)
password = Optional.extract(password)
email = Optional.extract(email)
first_name = Optional.extract(firstname)
last_name = Optional.extract(lastname)
active = Optional.extract(active)
admin = Optional.extract(admin)
extern_type = Optional.extract(extern_type)
extern_name = Optional.extract(extern_name)
schema = user_schema.UserSchema().bind(
# user caller
user=apiuser)
try:
schema_data = schema.deserialize(dict(
username=username,
email=email,
password=password,
first_name=first_name,
last_name=last_name,
active=active,
admin=admin,
extern_type=extern_type,
extern_name=extern_name,
))
except validation_schema.Invalid as err:
raise JSONRPCValidationError(colander_exc=err)
try:
user = UserModel().create_or_update(
username=Optional.extract(username),
password=Optional.extract(password),
email=Optional.extract(email),
firstname=Optional.extract(firstname),
lastname=Optional.extract(lastname),
active=Optional.extract(active),
admin=Optional.extract(admin),
extern_type=Optional.extract(extern_type),
extern_name=Optional.extract(extern_name),
username=schema_data['username'],
password=schema_data['password'],
email=schema_data['email'],
firstname=schema_data['first_name'],
lastname=schema_data['last_name'],
active=schema_data['active'],
admin=schema_data['admin'],
extern_type=schema_data['extern_type'],
extern_name=schema_data['extern_name'],
force_password_change=Optional.extract(force_password_change),
create_repo_group=create_repo_group
)
Session().flush()
creation_data = user.get_api_data()
audit_logger.store_api(
'user.create', action_data={'data': creation_data},
user=apiuser)
Session().commit()
return {
'msg': 'created new user `%s`' % username,
@ -326,7 +364,7 @@ def update_user(request, apiuser, userid, username=Optional(None),
raise JSONRPCForbidden()
user = get_user_or_error(userid)
old_data = user.get_api_data()
# only non optional arguments will be stored in updates
updates = {}
@ -343,6 +381,9 @@ def update_user(request, apiuser, userid, username=Optional(None),
store_update(updates, extern_type, 'extern_type')
user = UserModel().update_user(user, **updates)
audit_logger.store_api(
'user.edit', action_data={'old_data': old_data},
user=apiuser)
Session().commit()
return {
'msg': 'updated user ID:%s %s' % (user.user_id, user.username),
@ -405,9 +446,13 @@ def delete_user(request, apiuser, userid):
raise JSONRPCForbidden()
user = get_user_or_error(userid)
old_data = user.get_api_data()
try:
UserModel().delete(userid)
audit_logger.store_api(
'user.delete', action_data={'old_data': old_data},
user=apiuser)
Session().commit()
return {
'msg': 'deleted user ID:%s %s' % (user.user_id, user.username),

View file

@ -20,15 +20,19 @@
import logging
from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden
from rhodecode.api import (
jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
from rhodecode.api.utils import (
Optional, OAttr, store_update, has_superadmin_permission, get_origin,
get_user_or_error, get_user_group_or_error, get_perm_or_error)
from rhodecode.lib import audit_logger
from rhodecode.lib.auth import HasUserGroupPermissionAnyApi, HasPermissionAnyApi
from rhodecode.lib.exceptions import UserGroupAssignedException
from rhodecode.model.db import Session
from rhodecode.model.scm import UserGroupList
from rhodecode.model.user_group import UserGroupModel
from rhodecode.model import validation_schema
from rhodecode.model.validation_schema.schemas import user_group_schema
log = logging.getLogger(__name__)
@ -210,20 +214,41 @@ def create_user_group(
if UserGroupModel().get_by_name(group_name):
raise JSONRPCError("user group `%s` already exist" % (group_name,))
try:
if isinstance(owner, Optional):
owner = apiuser.user_id
if isinstance(owner, Optional):
owner = apiuser.user_id
owner = get_user_or_error(owner)
active = Optional.extract(active)
description = Optional.extract(description)
ug = UserGroupModel().create(
name=group_name, description=description, owner=owner,
active=active)
owner = get_user_or_error(owner)
active = Optional.extract(active)
description = Optional.extract(description)
schema = user_group_schema.UserGroupSchema().bind(
# user caller
user=apiuser)
try:
schema_data = schema.deserialize(dict(
user_group_name=group_name,
user_group_description=description,
user_group_owner=owner.username,
user_group_active=active,
))
except validation_schema.Invalid as err:
raise JSONRPCValidationError(colander_exc=err)
try:
user_group = UserGroupModel().create(
name=schema_data['user_group_name'],
description=schema_data['user_group_description'],
owner=owner,
active=schema_data['user_group_active'])
Session().flush()
creation_data = user_group.get_api_data()
audit_logger.store_api(
'user_group.create', action_data={'data': creation_data},
user=apiuser)
Session().commit()
return {
'msg': 'created new user group `%s`' % group_name,
'user_group': ug.get_api_data()
'user_group': creation_data
}
except Exception:
log.exception("Error occurred during creation of user group")
@ -291,6 +316,7 @@ def update_user_group(request, apiuser, usergroupid, group_name=Optional(''),
if not isinstance(owner, Optional):
owner = get_user_or_error(owner)
old_data = user_group.get_api_data()
updates = {}
store_update(updates, group_name, 'users_group_name')
store_update(updates, description, 'user_group_description')
@ -298,6 +324,9 @@ def update_user_group(request, apiuser, usergroupid, group_name=Optional(''),
store_update(updates, active, 'users_group_active')
try:
UserGroupModel().update(user_group, updates)
audit_logger.store_api(
'user_group.edit', action_data={'old_data': old_data},
user=apiuser)
Session().commit()
return {
'msg': 'updated user group ID:%s %s' % (
@ -359,8 +388,12 @@ def delete_user_group(request, apiuser, usergroupid):
raise JSONRPCError(
'user group `%s` does not exist' % (usergroupid,))
old_data = user_group.get_api_data()
try:
UserGroupModel().delete(user_group)
audit_logger.store_api(
'user_group.delete', action_data={'old_data': old_data},
user=apiuser)
Session().commit()
return {
'msg': 'deleted user group ID:%s %s' % (
@ -438,6 +471,12 @@ def add_user_to_user_group(request, apiuser, usergroupid, userid):
user.username, user_group.users_group_name
)
msg = msg if success else 'User is already in that group'
if success:
user_data = user.get_api_data()
audit_logger.store_api(
'user_group.edit.member.add', action_data={'user': user_data},
user=apiuser)
Session().commit()
return {
@ -501,6 +540,12 @@ def remove_user_from_user_group(request, apiuser, usergroupid, userid):
user.username, user_group.users_group_name
)
msg = msg if success else "User wasn't in group"
if success:
user_data = user.get_api_data()
audit_logger.store_api(
'user_group.edit.member.delete', action_data={'user': user_data},
user=apiuser)
Session().commit()
return {'success': success, 'msg': msg}
except Exception:

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/

View file

@ -24,8 +24,12 @@ from pylons import tmpl_context as c
from pyramid.httpexceptions import HTTPFound
from rhodecode.lib import helpers as h
from rhodecode.lib.utils2 import StrictAttributeDict, safe_int
from rhodecode.lib.utils import PartialRenderer
from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
from rhodecode.lib.ext_json import json
from rhodecode.model import repo
from rhodecode.model import repo_group
from rhodecode.model.db import User
from rhodecode.model.scm import ScmModel
@ -36,6 +40,30 @@ ADMIN_PREFIX = '/_admin'
STATIC_FILE_PREFIX = '/_static'
def add_route_with_slash(config,name, pattern, **kw):
config.add_route(name, pattern, **kw)
if not pattern.endswith('/'):
config.add_route(name + '_slash', pattern + '/', **kw)
def get_format_ref_id(repo):
"""Returns a `repo` specific reference formatter function"""
if h.is_svn(repo):
return _format_ref_id_svn
else:
return _format_ref_id
def _format_ref_id(name, raw_id):
"""Default formatting of a given reference `name`"""
return name
def _format_ref_id_svn(name, raw_id):
"""Special way of formatting a reference for Subversion including path"""
return '%s@%s' % (name, raw_id)
class TemplateArgs(StrictAttributeDict):
pass
@ -77,9 +105,14 @@ class BaseAppView(object):
raise HTTPFound(
self.request.route_path('my_account_password'))
def _get_local_tmpl_context(self):
def _get_local_tmpl_context(self, include_app_defaults=False):
c = TemplateArgs()
c.auth_user = self.request.user
if include_app_defaults:
# NOTE(marcink): after full pyramid migration include_app_defaults
# should be turned on by default
from rhodecode.lib.base import attach_context_attributes
attach_context_attributes(c, self.request, self.request.user.user_id)
return c
def _register_global_c(self, tmpl_args):
@ -121,15 +154,106 @@ class RepoAppView(BaseAppView):
self.db_repo_name = self.db_repo.repo_name
self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
def _get_local_tmpl_context(self):
c = super(RepoAppView, self)._get_local_tmpl_context()
def _handle_missing_requirements(self, error):
log.error(
'Requirements are missing for repository %s: %s',
self.db_repo_name, error.message)
def _get_local_tmpl_context(self, include_app_defaults=False):
c = super(RepoAppView, self)._get_local_tmpl_context(
include_app_defaults=include_app_defaults)
# register common vars for this type of view
c.rhodecode_db_repo = self.db_repo
c.repo_name = self.db_repo_name
c.repository_pull_requests = self.db_repo_pull_requests
c.repository_requirements_missing = False
try:
self.rhodecode_vcs_repo = self.db_repo.scm_instance()
except RepositoryRequirementError as e:
c.repository_requirements_missing = True
self._handle_missing_requirements(e)
return c
class DataGridAppView(object):
"""
Common class to have re-usable grid rendering components
"""
def _extract_ordering(self, request, column_map=None):
column_map = column_map or {}
column_index = safe_int(request.GET.get('order[0][column]'))
order_dir = request.GET.get(
'order[0][dir]', 'desc')
order_by = request.GET.get(
'columns[%s][data][sort]' % column_index, 'name_raw')
# translate datatable to DB columns
order_by = column_map.get(order_by) or order_by
search_q = request.GET.get('search[value]')
return search_q, order_by, order_dir
def _extract_chunk(self, request):
start = safe_int(request.GET.get('start'), 0)
length = safe_int(request.GET.get('length'), 25)
draw = safe_int(request.GET.get('draw'))
return draw, start, length
class BaseReferencesView(RepoAppView):
"""
Base for reference view for branches, tags and bookmarks.
"""
def load_default_context(self):
c = self._get_local_tmpl_context()
# TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
c.repo_info = self.db_repo
self._register_global_c(c)
return c
def load_refs_context(self, ref_items, partials_template):
_render = PartialRenderer(partials_template)
_data = []
pre_load = ["author", "date", "message"]
is_svn = h.is_svn(self.rhodecode_vcs_repo)
format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
for ref_name, commit_id in ref_items:
commit = self.rhodecode_vcs_repo.get_commit(
commit_id=commit_id, pre_load=pre_load)
# TODO: johbo: Unify generation of reference links
use_commit_id = '/' in ref_name or is_svn
files_url = h.url(
'files_home',
repo_name=c.repo_name,
f_path=ref_name if is_svn else '',
revision=commit_id if use_commit_id else ref_name,
at=ref_name)
_data.append({
"name": _render('name', ref_name, files_url),
"name_raw": ref_name,
"date": _render('date', commit.date),
"date_raw": datetime_to_time(commit.date),
"author": _render('author', commit.author),
"commit": _render(
'commit', commit.message, commit.raw_id, commit.idx),
"commit_raw": commit.idx,
"compare": _render(
'compare', format_ref_id(ref_name, commit.raw_id)),
})
c.has_references = bool(_data)
c.data = json.dumps(_data)
class RepoRoutePredicate(object):
def __init__(self, val, config):
self.val = val
@ -140,11 +264,15 @@ class RepoRoutePredicate(object):
phash = text
def __call__(self, info, request):
if hasattr(request, 'vcs_call'):
# skip vcs calls
return
repo_name = info['match']['repo_name']
repo_model = repo.RepoModel()
by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
# if we match quickly from database, short circuit the operation,
# and validate repo based on the type.
if by_name_match:
# register this as request object we can re-use later
request.db_repo = by_name_match
@ -158,6 +286,72 @@ class RepoRoutePredicate(object):
return False
class RepoTypeRoutePredicate(object):
def __init__(self, val, config):
self.val = val or ['hg', 'git', 'svn']
def text(self):
return 'repo_accepted_type = %s' % self.val
phash = text
def __call__(self, info, request):
if hasattr(request, 'vcs_call'):
# skip vcs calls
return
rhodecode_db_repo = request.db_repo
log.debug(
'%s checking repo type for %s in %s',
self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
if rhodecode_db_repo.repo_type in self.val:
return True
else:
log.warning('Current view is not supported for repo type:%s',
rhodecode_db_repo.repo_type)
#
# h.flash(h.literal(
# _('Action not supported for %s.' % rhodecode_repo.alias)),
# category='warning')
# return redirect(
# route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
return False
class RepoGroupRoutePredicate(object):
def __init__(self, val, config):
self.val = val
def text(self):
return 'repo_group_route = %s' % self.val
phash = text
def __call__(self, info, request):
if hasattr(request, 'vcs_call'):
# skip vcs calls
return
repo_group_name = info['match']['repo_group_name']
repo_group_model = repo_group.RepoGroupModel()
by_name_match = repo_group_model.get_by_group_name(
repo_group_name, cache=True)
if by_name_match:
# register this as request object we can re-use later
request.db_repo_group = by_name_match
return True
return False
def includeme(config):
config.add_route_predicate(
'repo_route', RepoRoutePredicate)
config.add_route_predicate(
'repo_accepted_types', RepoTypeRoutePredicate)
config.add_route_predicate(
'repo_group_route', RepoGroupRoutePredicate)

View file

@ -29,6 +29,20 @@ def admin_routes(config):
Admin prefixed routes
"""
config.add_route(
name='admin_audit_logs',
pattern='/audit_logs')
config.add_route(
name='pull_requests_global_0', # backward compat
pattern='/pull_requests/{pull_request_id:[0-9]+}')
config.add_route(
name='pull_requests_global_1', # backward compat
pattern='/pull-requests/{pull_request_id:[0-9]+}')
config.add_route(
name='pull_requests_global',
pattern='/pull-request/{pull_request_id:[0-9]+}')
config.add_route(
name='admin_settings_open_source',
pattern='/settings/open_source')
@ -50,6 +64,11 @@ def admin_routes(config):
name='admin_settings_sessions_cleanup',
pattern='/settings/sessions/cleanup')
# global permissions
config.add_route(
name='admin_permissions_ips',
pattern='/permissions/ips')
# users admin
config.add_route(
name='users',
@ -70,6 +89,28 @@ def admin_routes(config):
name='edit_user_auth_tokens_delete',
pattern='/users/{user_id:\d+}/edit/auth_tokens/delete')
# user emails
config.add_route(
name='edit_user_emails',
pattern='/users/{user_id:\d+}/edit/emails')
config.add_route(
name='edit_user_emails_add',
pattern='/users/{user_id:\d+}/edit/emails/new')
config.add_route(
name='edit_user_emails_delete',
pattern='/users/{user_id:\d+}/edit/emails/delete')
# user IPs
config.add_route(
name='edit_user_ips',
pattern='/users/{user_id:\d+}/edit/ips')
config.add_route(
name='edit_user_ips_add',
pattern='/users/{user_id:\d+}/edit/ips/new')
config.add_route(
name='edit_user_ips_delete',
pattern='/users/{user_id:\d+}/edit/ips/delete')
# user groups management
config.add_route(
name='edit_user_groups_management',
@ -93,6 +134,8 @@ def includeme(config):
navigation_registry = NavigationRegistry(labs_active=labs_active)
config.registry.registerUtility(navigation_registry)
# main admin routes
config.add_route(name='admin_home', pattern=ADMIN_PREFIX)
config.include(admin_routes, route_prefix=ADMIN_PREFIX)
# Scan module for configuration decorators.

View file

@ -25,12 +25,25 @@ import datetime
import pytest
from rhodecode.tests import *
from rhodecode.tests.fixture import FIXTURES
from rhodecode.model.db import UserLog
from rhodecode.model.meta import Session
from rhodecode.lib.utils2 import safe_unicode
dn = os.path.dirname
FIXTURES = os.path.join(dn(dn(os.path.abspath(__file__))), 'fixtures')
def route_path(name, params=None, **kwargs):
import urllib
from rhodecode.apps._base import ADMIN_PREFIX
base_url = {
'admin_home': ADMIN_PREFIX,
'admin_audit_logs': ADMIN_PREFIX + '/audit_logs',
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
class TestAdminController(TestController):
@ -73,100 +86,102 @@ class TestAdminController(TestController):
def test_index(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index'))
response.mustcontain('Admin journal')
response = self.app.get(route_path('admin_audit_logs'))
response.mustcontain('Admin audit logs')
def test_filter_all_entries(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',))
response.mustcontain('2034 entries')
response = self.app.get(route_path('admin_audit_logs'))
all_count = UserLog.query().count()
response.mustcontain('%s entries' % all_count)
def test_filter_journal_filter_exact_match_on_repository(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',
filter='repository:rhodecode'))
response = self.app.get(route_path('admin_audit_logs',
params=dict(filter='repository:rhodecode')))
response.mustcontain('3 entries')
def test_filter_journal_filter_exact_match_on_repository_CamelCase(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',
filter='repository:RhodeCode'))
response = self.app.get(route_path('admin_audit_logs',
params=dict(filter='repository:RhodeCode')))
response.mustcontain('3 entries')
def test_filter_journal_filter_wildcard_on_repository(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',
filter='repository:*test*'))
response = self.app.get(route_path('admin_audit_logs',
params=dict(filter='repository:*test*')))
response.mustcontain('862 entries')
def test_filter_journal_filter_prefix_on_repository(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',
filter='repository:test*'))
response = self.app.get(route_path('admin_audit_logs',
params=dict(filter='repository:test*')))
response.mustcontain('257 entries')
def test_filter_journal_filter_prefix_on_repository_CamelCase(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',
filter='repository:Test*'))
response = self.app.get(route_path('admin_audit_logs',
params=dict(filter='repository:Test*')))
response.mustcontain('257 entries')
def test_filter_journal_filter_prefix_on_repository_and_user(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',
filter='repository:test* AND username:demo'))
response = self.app.get(route_path('admin_audit_logs',
params=dict(filter='repository:test* AND username:demo')))
response.mustcontain('130 entries')
def test_filter_journal_filter_prefix_on_repository_or_target_repo(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',
filter='repository:test* OR repository:rhodecode'))
response = self.app.get(route_path('admin_audit_logs',
params=dict(filter='repository:test* OR repository:rhodecode')))
response.mustcontain('260 entries') # 257 + 3
def test_filter_journal_filter_exact_match_on_username(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',
filter='username:demo'))
response = self.app.get(route_path('admin_audit_logs',
params=dict(filter='username:demo')))
response.mustcontain('1087 entries')
def test_filter_journal_filter_exact_match_on_username_camelCase(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',
filter='username:DemO'))
response = self.app.get(route_path('admin_audit_logs',
params=dict(filter='username:DemO')))
response.mustcontain('1087 entries')
def test_filter_journal_filter_wildcard_on_username(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',
filter='username:*test*'))
response.mustcontain('100 entries')
response = self.app.get(route_path('admin_audit_logs',
params=dict(filter='username:*test*')))
entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count()
response.mustcontain('{} entries'.format(entries_count))
def test_filter_journal_filter_prefix_on_username(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',
filter='username:demo*'))
response = self.app.get(route_path('admin_audit_logs',
params=dict(filter='username:demo*')))
response.mustcontain('1101 entries')
def test_filter_journal_filter_prefix_on_user_or_other_user(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',
filter='username:demo OR username:volcan'))
response = self.app.get(route_path('admin_audit_logs',
params=dict(filter='username:demo OR username:volcan')))
response.mustcontain('1095 entries') # 1087 + 8
def test_filter_journal_filter_wildcard_on_action(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',
filter='action:*pull_request*'))
response = self.app.get(route_path('admin_audit_logs',
params=dict(filter='action:*pull_request*')))
response.mustcontain('187 entries')
def test_filter_journal_filter_on_date(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',
filter='date:20121010'))
response = self.app.get(route_path('admin_audit_logs',
params=dict(filter='date:20121010')))
response.mustcontain('47 entries')
def test_filter_journal_filter_on_date_2(self):
self.log_user()
response = self.app.get(url(controller='admin/admin', action='index',
filter='date:20121020'))
response = self.app.get(route_path('admin_audit_logs',
params=dict(filter='date:20121020')))
response.mustcontain('17 entries')

View file

@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import pytest
from rhodecode.tests import TestController
from rhodecode.tests.fixture import Fixture
fixture = Fixture()
def route_path(name, params=None, **kwargs):
import urllib
from rhodecode.apps._base import ADMIN_PREFIX
base_url = {
'admin_home': ADMIN_PREFIX,
'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}',
'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}',
'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}',
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
class TestAdminMainView(TestController):
def test_redirect_admin_home(self):
self.log_user()
response = self.app.get(route_path('admin_home'), status=302)
assert response.location.endswith('/audit_logs')
def test_redirect_pull_request_view(self, view):
self.log_user()
self.app.get(
route_path(view, pull_request_id='xxxx'),
status=404)
@pytest.mark.backends("git", "hg")
@pytest.mark.parametrize('view', [
'pull_requests_global',
'pull_requests_global_0',
'pull_requests_global_1',
])
def test_redirect_pull_request_view(self, view, pr_util):
self.log_user()
pull_request = pr_util.create_pull_request()
pull_request_id = pull_request.pull_request_id
response = self.app.get(
route_path(view, pull_request_id=pull_request_id),
status=302)
assert response.location.endswith(
'pull-request/{}'.format(pull_request_id))
repo_name = pull_request.target_repo.repo_name
redirect_url = route_path(
'pullrequest_show', repo_name=repo_name,
pull_request_id=pull_request.pull_request_id)
assert redirect_url in response.location

View file

@ -20,7 +20,9 @@
import pytest
from rhodecode.model.db import User, UserApiKeys
from rhodecode.model.db import User, UserApiKeys, UserEmailMap
from rhodecode.model.meta import Session
from rhodecode.model.user import UserModel
from rhodecode.tests import (
TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
@ -44,6 +46,20 @@ def route_path(name, params=None, **kwargs):
ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
'edit_user_auth_tokens_delete':
ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
'edit_user_emails':
ADMIN_PREFIX + '/users/{user_id}/edit/emails',
'edit_user_emails_add':
ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
'edit_user_emails_delete':
ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
'edit_user_ips':
ADMIN_PREFIX + '/users/{user_id}/edit/ips',
'edit_user_ips_add':
ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
'edit_user_ips_delete':
ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
}[name].format(**kwargs)
if params:
@ -135,8 +151,131 @@ class TestAdminUsersView(TestController):
response = self.app.post(
route_path('edit_user_auth_tokens_delete', user_id=user_id),
{'del_auth_token': keys[0].api_key, 'csrf_token': self.csrf_token})
{'del_auth_token': keys[0].user_api_key_id,
'csrf_token': self.csrf_token})
assert_session_flash(response, 'Auth token successfully deleted')
keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
assert 2 == len(keys)
def test_ips(self):
self.log_user()
user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
response.mustcontain('All IP addresses are allowed')
@pytest.mark.parametrize("test_name, ip, ip_range, failure", [
('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
('127_bad_ip', 'foobar', 'foobar', True),
])
def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
self.log_user()
user = user_util.create_user(username=test_name)
user_id = user.user_id
response = self.app.post(
route_path('edit_user_ips_add', user_id=user_id),
params={'new_ip': ip, 'csrf_token': self.csrf_token})
if failure:
assert_session_flash(
response, 'Please enter a valid IPv4 or IpV6 address')
response = self.app.get(route_path('edit_user_ips', user_id=user_id))
response.mustcontain(no=[ip])
response.mustcontain(no=[ip_range])
else:
response = self.app.get(route_path('edit_user_ips', user_id=user_id))
response.mustcontain(ip)
response.mustcontain(ip_range)
def test_ips_delete(self, user_util):
self.log_user()
user = user_util.create_user()
user_id = user.user_id
ip = '127.0.0.1/32'
ip_range = '127.0.0.1 - 127.0.0.1'
new_ip = UserModel().add_extra_ip(user_id, ip)
Session().commit()
new_ip_id = new_ip.ip_id
response = self.app.get(route_path('edit_user_ips', user_id=user_id))
response.mustcontain(ip)
response.mustcontain(ip_range)
self.app.post(
route_path('edit_user_ips_delete', user_id=user_id),
params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
response = self.app.get(route_path('edit_user_ips', user_id=user_id))
response.mustcontain('All IP addresses are allowed')
response.mustcontain(no=[ip])
response.mustcontain(no=[ip_range])
def test_emails(self):
self.log_user()
user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
response = self.app.get(route_path('edit_user_emails', user_id=user.user_id))
response.mustcontain('No additional emails specified')
def test_emails_add(self, user_util):
self.log_user()
user = user_util.create_user()
user_id = user.user_id
self.app.post(
route_path('edit_user_emails_add', user_id=user_id),
params={'new_email': 'example@rhodecode.com',
'csrf_token': self.csrf_token})
response = self.app.get(route_path('edit_user_emails', user_id=user_id))
response.mustcontain('example@rhodecode.com')
def test_emails_add_existing_email(self, user_util, user_regular):
existing_email = user_regular.email
self.log_user()
user = user_util.create_user()
user_id = user.user_id
response = self.app.post(
route_path('edit_user_emails_add', user_id=user_id),
params={'new_email': existing_email,
'csrf_token': self.csrf_token})
assert_session_flash(
response, 'This e-mail address is already taken')
response = self.app.get(route_path('edit_user_emails', user_id=user_id))
response.mustcontain(no=[existing_email])
def test_emails_delete(self, user_util):
self.log_user()
user = user_util.create_user()
user_id = user.user_id
self.app.post(
route_path('edit_user_emails_add', user_id=user_id),
params={'new_email': 'example@rhodecode.com',
'csrf_token': self.csrf_token})
response = self.app.get(route_path('edit_user_emails', user_id=user_id))
response.mustcontain('example@rhodecode.com')
user_email = UserEmailMap.query()\
.filter(UserEmailMap.email == 'example@rhodecode.com') \
.filter(UserEmailMap.user_id == user_id)\
.one()
del_email_id = user_email.email_id
self.app.post(
route_path('edit_user_emails_delete', user_id=user_id),
params={'del_email_id': del_email_id,
'csrf_token': self.csrf_token})
response = self.app.get(route_path('edit_user_emails', user_id=user_id))
response.mustcontain(no=['example@rhodecode.com'])

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2017 RhodeCode GmbH
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
@ -18,42 +18,41 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
"""
Controller for Admin panel of RhodeCode Enterprise
"""
import logging
from pylons import request, tmpl_context as c, url
from pylons.controllers.util import redirect
from pyramid.view import view_config
from sqlalchemy.orm import joinedload
from rhodecode.model.db import UserLog, PullRequest
from rhodecode.apps._base import BaseAppView
from rhodecode.model.db import UserLog
from rhodecode.lib.user_log_filter import user_log_filter
from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
from rhodecode.lib.base import BaseController, render
from rhodecode.lib.utils2 import safe_int
from rhodecode.lib.helpers import Page
log = logging.getLogger(__name__)
class AdminController(BaseController):
class AdminAuditLogsView(BaseAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
self._register_global_c(c)
return c
@LoginRequired()
def __before__(self):
super(AdminController, self).__before__()
@HasPermissionAllDecorator('hg.admin')
def index(self):
@view_config(
route_name='admin_audit_logs', request_method='GET',
renderer='rhodecode:templates/admin/admin_audit_logs.mako')
def admin_audit_logs(self):
c = self.load_default_context()
users_log = UserLog.query()\
.options(joinedload(UserLog.user))\
.options(joinedload(UserLog.repository))
# FILTERING
c.search_term = request.GET.get('filter')
c.search_term = self.request.GET.get('filter')
try:
users_log = user_log_filter(users_log, c.search_term)
except Exception:
@ -62,28 +61,13 @@ class AdminController(BaseController):
users_log = users_log.order_by(UserLog.action_date.desc())
p = safe_int(request.GET.get('page', 1), 1)
p = safe_int(self.request.GET.get('page', 1), 1)
def url_generator(**kw):
return url.current(filter=c.search_term, **kw)
if c.search_term:
kw['filter'] = c.search_term
return self.request.current_route_path(_query=kw)
c.users_log = Page(users_log, page=p, items_per_page=10,
url=url_generator)
c.log_data = render('admin/admin_log.mako')
if request.is_xhr:
return c.log_data
return render('admin/admin.mako')
# global redirect doesn't need permissions
def pull_requests(self, pull_request_id):
"""
Global redirect for Pull Requests
:param pull_request_id: id of pull requests in the system
"""
pull_request = PullRequest.get_or_404(pull_request_id)
repo_name = pull_request.target_repo.repo_name
return redirect(url(
'pullrequest_show', repo_name=repo_name,
pull_request_id=pull_request_id))
c.audit_logs = Page(users_log, page=p, items_per_page=10,
url=url_generator)
return self._get_template_context(c)

View file

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
from rhodecode.apps._base import BaseAppView
from rhodecode.lib import helpers as h
from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
from rhodecode.model.db import PullRequest
log = logging.getLogger(__name__)
class AdminMainView(BaseAppView):
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@view_config(
route_name='admin_home', request_method='GET')
def admin_main(self):
# redirect _admin to audit logs...
raise HTTPFound(h.route_path('admin_audit_logs'))
@LoginRequired()
@view_config(route_name='pull_requests_global_0', request_method='GET')
@view_config(route_name='pull_requests_global_1', request_method='GET')
@view_config(route_name='pull_requests_global', request_method='GET')
def pull_requests(self):
"""
Global redirect for Pull Requests
:param pull_request_id: id of pull requests in the system
"""
pull_request_id = self.request.matchdict.get('pull_request_id')
pull_request = PullRequest.get_or_404(pull_request_id, pyramid_exc=True)
repo_name = pull_request.target_repo.repo_name
raise HTTPFound(
h.route_path('pullrequest_show', repo_name=repo_name,
pull_request_id=pull_request_id))

View file

@ -21,7 +21,7 @@
import collections
import logging
from pylons import tmpl_context as c
from pyramid.view import view_config
from rhodecode.apps._base import BaseAppView
@ -34,15 +34,21 @@ log = logging.getLogger(__name__)
class OpenSourceLicensesAdminSettingsView(BaseAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
self._register_global_c(c)
return c
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@view_config(
route_name='admin_settings_open_source', request_method='GET',
renderer='rhodecode:templates/admin/settings/settings.mako')
def open_source_licenses(self):
c = self.load_default_context()
c.active = 'open_source'
c.navlist = navigation_list(self.request)
c.opensource_licenses = collections.OrderedDict(
sorted(read_opensource_licenses().items(), key=lambda t: t[0]))
return {}
return self._get_template_context(c)

View file

@ -20,7 +20,6 @@
import logging
from pylons import tmpl_context as c
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPFound
@ -37,6 +36,11 @@ log = logging.getLogger(__name__)
class AdminSessionSettingsView(BaseAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
self._register_global_c(c)
return c
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@ -44,6 +48,8 @@ class AdminSessionSettingsView(BaseAppView):
route_name='admin_settings_sessions', request_method='GET',
renderer='rhodecode:templates/admin/settings/settings.mako')
def settings_sessions(self):
c = self.load_default_context()
c.active = 'sessions'
c.navlist = navigation_list(self.request)
@ -59,11 +65,11 @@ class AdminSessionSettingsView(BaseAppView):
c.session_expired_count = c.session_model.get_expired_count(
older_than_seconds)
return {}
return self._get_template_context(c)
@LoginRequired()
@CSRFRequired()
@HasPermissionAllDecorator('hg.admin')
@CSRFRequired()
@view_config(
route_name='admin_settings_sessions_cleanup', request_method='POST')
def settings_sessions_cleanup(self):

View file

@ -33,8 +33,8 @@ log = logging.getLogger(__name__)
class SvnConfigAdminSettingsView(BaseAppView):
@LoginRequired()
@CSRFRequired()
@HasPermissionAllDecorator('hg.admin')
@CSRFRequired()
@view_config(
route_name='admin_settings_vcs_svn_generate_cfg',
request_method='POST', renderer='json')

View file

@ -22,7 +22,6 @@ import logging
import urllib2
import packaging.version
from pylons import tmpl_context as c
from pyramid.view import view_config
import rhodecode
@ -39,6 +38,10 @@ log = logging.getLogger(__name__)
class AdminSystemInfoSettingsView(BaseAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
self._register_global_c(c)
return c
@staticmethod
def get_update_data(update_url):
@ -64,6 +67,7 @@ class AdminSystemInfoSettingsView(BaseAppView):
renderer='rhodecode:templates/admin/settings/settings.mako')
def settings_system_info(self):
_ = self.request.translate
c = self.load_default_context()
c.active = 'system'
c.navlist = navigation_list(self.request)
@ -106,6 +110,7 @@ class AdminSystemInfoSettingsView(BaseAppView):
(_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
(_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
(_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
(_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')),
(_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
(_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
('', '', ''), # spacer
@ -163,7 +168,7 @@ class AdminSystemInfoSettingsView(BaseAppView):
else:
self.request.session.flash(
'You are not allowed to do this', queue='warning')
return {}
return self._get_template_context(c)
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@ -172,6 +177,7 @@ class AdminSystemInfoSettingsView(BaseAppView):
renderer='rhodecode:templates/admin/settings/settings_system_update.mako')
def settings_system_info_check_update(self):
_ = self.request.translate
c = self.load_default_context()
update_url = self.get_update_url()
@ -200,4 +206,4 @@ class AdminSystemInfoSettingsView(BaseAppView):
c.should_upgrade = True
c.important_notices = latest['general']
return {}
return self._get_template_context(c)

View file

@ -20,15 +20,16 @@
import logging
import datetime
import formencode
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
from sqlalchemy.sql.functions import coalesce
from rhodecode.lib.helpers import Page
from rhodecode_tools.lib.ext_json import json
from rhodecode.apps._base import BaseAppView, DataGridAppView
from rhodecode.apps._base import BaseAppView
from rhodecode.lib import audit_logger
from rhodecode.lib.ext_json import json
from rhodecode.lib.auth import (
LoginRequired, HasPermissionAllDecorator, CSRFRequired)
from rhodecode.lib import helpers as h
@ -37,13 +38,13 @@ from rhodecode.lib.utils2 import safe_int, safe_unicode
from rhodecode.model.auth_token import AuthTokenModel
from rhodecode.model.user import UserModel
from rhodecode.model.user_group import UserGroupModel
from rhodecode.model.db import User, or_
from rhodecode.model.db import User, or_, UserIpMap, UserEmailMap, UserApiKeys
from rhodecode.model.meta import Session
log = logging.getLogger(__name__)
class AdminUsersView(BaseAppView):
class AdminUsersView(BaseAppView, DataGridAppView):
ALLOW_SCOPED_TOKENS = False
"""
This view has alternative version inside EE, if modified please take a look
@ -64,28 +65,6 @@ class AdminUsersView(BaseAppView):
# is a pyramid view
raise HTTPFound('/')
def _extract_ordering(self, request):
column_index = safe_int(request.GET.get('order[0][column]'))
order_dir = request.GET.get(
'order[0][dir]', 'desc')
order_by = request.GET.get(
'columns[%s][data][sort]' % column_index, 'name_raw')
# translate datatable to DB columns
order_by = {
'first_name': 'name',
'last_name': 'lastname',
}.get(order_by) or order_by
search_q = request.GET.get('search[value]')
return search_q, order_by, order_dir
def _extract_chunk(self, request):
start = safe_int(request.GET.get('start'), 0)
length = safe_int(request.GET.get('length'), 25)
draw = safe_int(request.GET.get('draw'))
return draw, start, length
@HasPermissionAllDecorator('hg.admin')
@view_config(
route_name='users', request_method='GET',
@ -97,8 +76,8 @@ class AdminUsersView(BaseAppView):
@HasPermissionAllDecorator('hg.admin')
@view_config(
# renderer defined below
route_name='users_data', request_method='GET', renderer='json',
xhr=True)
route_name='users_data', request_method='GET',
renderer='json_ext', xhr=True)
def users_list_data(self):
draw, start, limit = self._extract_chunk(self.request)
search_q, order_by, order_dir = self._extract_ordering(self.request)
@ -149,8 +128,8 @@ class AdminUsersView(BaseAppView):
users_data.append({
"username": h.gravatar_with_user(user.username),
"email": user.email,
"first_name": h.escape(user.name),
"last_name": h.escape(user.lastname),
"first_name": user.first_name,
"last_name": user.last_name,
"last_login": h.format_date(user.last_login),
"last_activity": h.format_date(user.last_activity),
"active": h.bool2icon(user.active),
@ -216,15 +195,23 @@ class AdminUsersView(BaseAppView):
user_id = self.request.matchdict.get('user_id')
c.user = User.get_or_404(user_id, pyramid_exc=True)
self._redirect_for_default_user(c.user.username)
user_data = c.user.get_api_data()
lifetime = safe_int(self.request.POST.get('lifetime'), -1)
description = self.request.POST.get('description')
role = self.request.POST.get('role')
token = AuthTokenModel().create(
c.user.user_id, description, lifetime, role)
token_data = token.get_api_data()
self.maybe_attach_token_scope(token)
audit_logger.store_web(
'user.edit.token.add', action_data={
'data': {'token': token_data, 'user': user_data}},
user=self._rhodecode_user, )
Session().commit()
h.flash(_("Auth token successfully created"), category='success')
@ -242,16 +229,204 @@ class AdminUsersView(BaseAppView):
user_id = self.request.matchdict.get('user_id')
c.user = User.get_or_404(user_id, pyramid_exc=True)
self._redirect_for_default_user(c.user.username)
user_data = c.user.get_api_data()
del_auth_token = self.request.POST.get('del_auth_token')
if del_auth_token:
token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True)
token_data = token.get_api_data()
AuthTokenModel().delete(del_auth_token, c.user.user_id)
audit_logger.store_web(
'user.edit.token.delete', action_data={
'data': {'token': token_data, 'user': user_data}},
user=self._rhodecode_user,)
Session().commit()
h.flash(_("Auth token successfully deleted"), category='success')
return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@view_config(
route_name='edit_user_emails', request_method='GET',
renderer='rhodecode:templates/admin/users/user_edit.mako')
def emails(self):
_ = self.request.translate
c = self.load_default_context()
user_id = self.request.matchdict.get('user_id')
c.user = User.get_or_404(user_id, pyramid_exc=True)
self._redirect_for_default_user(c.user.username)
c.active = 'emails'
c.user_email_map = UserEmailMap.query() \
.filter(UserEmailMap.user == c.user).all()
return self._get_template_context(c)
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@CSRFRequired()
@view_config(
route_name='edit_user_emails_add', request_method='POST')
def emails_add(self):
_ = self.request.translate
c = self.load_default_context()
user_id = self.request.matchdict.get('user_id')
c.user = User.get_or_404(user_id, pyramid_exc=True)
self._redirect_for_default_user(c.user.username)
email = self.request.POST.get('new_email')
user_data = c.user.get_api_data()
try:
UserModel().add_extra_email(c.user.user_id, email)
audit_logger.store_web(
'user.edit.email.add', action_data={'email': email, 'user': user_data},
user=self._rhodecode_user)
Session().commit()
h.flash(_("Added new email address `%s` for user account") % email,
category='success')
except formencode.Invalid as error:
h.flash(h.escape(error.error_dict['email']), category='error')
except Exception:
log.exception("Exception during email saving")
h.flash(_('An error occurred during email saving'),
category='error')
raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@CSRFRequired()
@view_config(
route_name='edit_user_emails_delete', request_method='POST')
def emails_delete(self):
_ = self.request.translate
c = self.load_default_context()
user_id = self.request.matchdict.get('user_id')
c.user = User.get_or_404(user_id, pyramid_exc=True)
self._redirect_for_default_user(c.user.username)
email_id = self.request.POST.get('del_email_id')
user_model = UserModel()
email = UserEmailMap.query().get(email_id).email
user_data = c.user.get_api_data()
user_model.delete_extra_email(c.user.user_id, email_id)
audit_logger.store_web(
'user.edit.email.delete', action_data={'email': email, 'user': user_data},
user=self._rhodecode_user)
Session().commit()
h.flash(_("Removed email address from user account"),
category='success')
raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@view_config(
route_name='edit_user_ips', request_method='GET',
renderer='rhodecode:templates/admin/users/user_edit.mako')
def ips(self):
_ = self.request.translate
c = self.load_default_context()
user_id = self.request.matchdict.get('user_id')
c.user = User.get_or_404(user_id, pyramid_exc=True)
self._redirect_for_default_user(c.user.username)
c.active = 'ips'
c.user_ip_map = UserIpMap.query() \
.filter(UserIpMap.user == c.user).all()
c.inherit_default_ips = c.user.inherit_default_permissions
c.default_user_ip_map = UserIpMap.query() \
.filter(UserIpMap.user == User.get_default_user()).all()
return self._get_template_context(c)
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@CSRFRequired()
@view_config(
route_name='edit_user_ips_add', request_method='POST')
def ips_add(self):
_ = self.request.translate
c = self.load_default_context()
user_id = self.request.matchdict.get('user_id')
c.user = User.get_or_404(user_id, pyramid_exc=True)
# NOTE(marcink): this view is allowed for default users, as we can
# edit their IP white list
user_model = UserModel()
desc = self.request.POST.get('description')
try:
ip_list = user_model.parse_ip_range(
self.request.POST.get('new_ip'))
except Exception as e:
ip_list = []
log.exception("Exception during ip saving")
h.flash(_('An error occurred during ip saving:%s' % (e,)),
category='error')
added = []
user_data = c.user.get_api_data()
for ip in ip_list:
try:
user_model.add_extra_ip(c.user.user_id, ip, desc)
audit_logger.store_web(
'user.edit.ip.add', action_data={'ip': ip, 'user': user_data},
user=self._rhodecode_user)
Session().commit()
added.append(ip)
except formencode.Invalid as error:
msg = error.error_dict['ip']
h.flash(msg, category='error')
except Exception:
log.exception("Exception during ip saving")
h.flash(_('An error occurred during ip saving'),
category='error')
if added:
h.flash(
_("Added ips %s to user whitelist") % (', '.join(ip_list), ),
category='success')
if 'default_user' in self.request.POST:
# case for editing global IP list we do it for 'DEFAULT' user
raise HTTPFound(h.route_path('admin_permissions_ips'))
raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@CSRFRequired()
@view_config(
route_name='edit_user_ips_delete', request_method='POST')
def ips_delete(self):
_ = self.request.translate
c = self.load_default_context()
user_id = self.request.matchdict.get('user_id')
c.user = User.get_or_404(user_id, pyramid_exc=True)
# NOTE(marcink): this view is allowed for default users, as we can
# edit their IP white list
ip_id = self.request.POST.get('del_ip_id')
user_model = UserModel()
user_data = c.user.get_api_data()
ip = UserIpMap.query().get(ip_id).ip_addr
user_model.delete_extra_ip(c.user.user_id, ip_id)
audit_logger.store_web(
'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
user=self._rhodecode_user)
Session().commit()
h.flash(_("Removed ip address from user whitelist"), category='success')
if 'default_user' in self.request.POST:
# case for editing global IP list we do it for 'DEFAULT' user
raise HTTPFound(h.route_path('admin_permissions_ips'))
raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@view_config(
@ -264,7 +439,8 @@ class AdminUsersView(BaseAppView):
c.user = User.get_or_404(user_id, pyramid_exc=True)
c.data = c.user.group_member
self._redirect_for_default_user(c.user.username)
groups = [UserGroupModel.get_user_groups_as_dict(group.users_group) for group in c.user.group_member]
groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
for group in c.user.group_member]
c.groups = json.dumps(groups)
c.active = 'groups'
@ -272,6 +448,7 @@ class AdminUsersView(BaseAppView):
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@CSRFRequired()
@view_config(
route_name='edit_user_groups_management_updates', request_method='POST')
def groups_management_updates(self):
@ -314,15 +491,15 @@ class AdminUsersView(BaseAppView):
p = safe_int(self.request.GET.get('page', 1), 1)
filter_term = self.request.GET.get('filter')
c.user_log = UserModel().get_user_log(c.user, filter_term)
user_log = UserModel().get_user_log(c.user, filter_term)
def url_generator(**kw):
if filter_term:
kw['filter'] = filter_term
return self.request.current_route_path(_query=kw)
c.user_log = Page(c.user_log, page=p, items_per_page=10,
url=url_generator)
c.audit_logs = h.Page(
user_log, page=p, items_per_page=10, url=url_generator)
c.filter_term = filter_term
return self._get_template_context(c)

View file

@ -30,8 +30,6 @@ Channel Stream controller for rhodecode
import logging
import uuid
from pylons import tmpl_context as c
from pyramid.settings import asbool
from pyramid.view import view_config
from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPBadGateway
@ -46,7 +44,6 @@ from rhodecode.lib.channelstream import (
update_history_from_logs,
STATE_PUBLIC_KEYS)
from rhodecode.lib.auth import NotAnonymous
from rhodecode.lib.utils2 import str2bool
log = logging.getLogger(__name__)
@ -82,7 +79,7 @@ class ChannelstreamView(object):
log.error('Incorrect permissions for requested channels')
raise HTTPForbidden()
user = c.rhodecode_user
user = self._rhodecode_user
if user.user_id:
user_data = get_user_data(user.user_id)
else:
@ -95,7 +92,7 @@ class ChannelstreamView(object):
'display_name': None,
'display_link': None,
}
user_data['permissions'] = c.rhodecode_user.permissions
user_data['permissions'] = self._rhodecode_user.permissions
payload = {
'username': user.username,
'user_state': user_data,

View file

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
from rhodecode.config import routing_links
def includeme(config):
config.add_route(
name='home',
pattern='/')
config.add_route(
name='user_autocomplete_data',
pattern='/_users')
config.add_route(
name='user_group_autocomplete_data',
pattern='/_user_groups')
config.add_route(
name='repo_list_data',
pattern='/_repos')
config.add_route(
name='goto_switcher_data',
pattern='/_goto_data')
# register our static links via redirection mechanism
routing_links.connect_redirection_links(config)
# Scan module for configuration decorators.
config.scan()

View file

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
def assert_and_get_content(result):
repos = []
groups = []
commits = []
for data in result:
for data_item in data['children']:
assert data_item['id']
assert data_item['text']
assert data_item['url']
if data_item['type'] == 'repo':
repos.append(data_item)
elif data_item['type'] == 'group':
groups.append(data_item)
elif data_item['type'] == 'commit':
commits.append(data_item)
else:
raise Exception('invalid type %s' % data_item['type'])
return repos, groups, commits

View file

@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import json
import pytest
from . import assert_and_get_content
from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
from rhodecode.tests.fixture import Fixture
from rhodecode.lib.utils import map_groups
from rhodecode.model.repo import RepoModel
from rhodecode.model.repo_group import RepoGroupModel
from rhodecode.model.db import Session, Repository, RepoGroup
fixture = Fixture()
def route_path(name, params=None, **kwargs):
import urllib
base_url = {
'goto_switcher_data': '/_goto_data',
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
class TestGotoSwitcherData(TestController):
required_repos_with_groups = [
'abc',
'abc-fork',
'forks/abcd',
'abcd',
'abcde',
'a/abc',
'aa/abc',
'aaa/abc',
'aaaa/abc',
'repos_abc/aaa/abc',
'abc_repos/abc',
'abc_repos/abcd',
'xxx/xyz',
'forked-abc/a/abc'
]
@pytest.fixture(autouse=True, scope='class')
def prepare(self, request, pylonsapp):
for repo_and_group in self.required_repos_with_groups:
# create structure of groups and return the last group
repo_group = map_groups(repo_and_group)
RepoModel()._create_repo(
repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN,
repo_group=getattr(repo_group, 'group_id', None))
Session().commit()
request.addfinalizer(self.cleanup)
def cleanup(self):
# first delete all repos
for repo_and_groups in self.required_repos_with_groups:
repo = Repository.get_by_repo_name(repo_and_groups)
if repo:
RepoModel().delete(repo)
Session().commit()
# then delete all empty groups
for repo_and_groups in self.required_repos_with_groups:
if '/' in repo_and_groups:
r_group = repo_and_groups.rsplit('/', 1)[0]
repo_group = RepoGroup.get_by_group_name(r_group)
if not repo_group:
continue
parents = repo_group.parents
RepoGroupModel().delete(repo_group, force_delete=True)
Session().commit()
for el in reversed(parents):
RepoGroupModel().delete(el, force_delete=True)
Session().commit()
def test_returns_list_of_repos_and_groups(self, xhr_header):
self.log_user()
response = self.app.get(
route_path('goto_switcher_data'),
extra_environ=xhr_header, status=200)
result = json.loads(response.body)['results']
repos, groups, commits = assert_and_get_content(result)
assert len(repos) == len(Repository.get_all())
assert len(groups) == len(RepoGroup.get_all())
assert len(commits) == 0
def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
self.log_user()
response = self.app.get(
route_path('goto_switcher_data'),
params={'query': 'abc'},
extra_environ=xhr_header, status=200)
result = json.loads(response.body)['results']
repos, groups, commits = assert_and_get_content(result)
assert len(repos) == 13
assert len(groups) == 5
assert len(commits) == 0
def test_returns_list_of_properly_sorted_and_filtered(self, xhr_header):
self.log_user()
response = self.app.get(
route_path('goto_switcher_data'),
params={'query': 'abc'},
extra_environ=xhr_header, status=200)
result = json.loads(response.body)['results']
repos, groups, commits = assert_and_get_content(result)
test_repos = [x['text'] for x in repos[:4]]
assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos
test_groups = [x['text'] for x in groups[:4]]
assert ['abc_repos', 'repos_abc',
'forked-abc', 'forked-abc/a'] == test_groups

View file

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import json
from . import assert_and_get_content
from rhodecode.tests import TestController
from rhodecode.tests.fixture import Fixture
from rhodecode.model.db import Repository
fixture = Fixture()
def route_path(name, params=None, **kwargs):
import urllib
base_url = {
'repo_list_data': '/_repos',
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
class TestRepoListData(TestController):
def test_returns_list_of_repos_and_groups(self, xhr_header):
self.log_user()
response = self.app.get(
route_path('repo_list_data'),
extra_environ=xhr_header, status=200)
result = json.loads(response.body)['results']
repos, groups, commits = assert_and_get_content(result)
assert len(repos) == len(Repository.get_all())
assert len(groups) == 0
assert len(commits) == 0
def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
self.log_user()
response = self.app.get(
route_path('repo_list_data'),
params={'query': 'vcs_test_git'},
extra_environ=xhr_header, status=200)
result = json.loads(response.body)['results']
repos, groups, commits = assert_and_get_content(result)
assert len(repos) == len(Repository.query().filter(
Repository.repo_name.ilike('%vcs_test_git%')).all())
assert len(groups) == 0
assert len(commits) == 0
def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header):
self.log_user()
response = self.app.get(
route_path('repo_list_data'),
params={'query': 'vcs_test_git', 'repo_type': 'git'},
extra_environ=xhr_header, status=200)
result = json.loads(response.body)['results']
repos, groups, commits = assert_and_get_content(result)
assert len(repos) == len(Repository.query().filter(
Repository.repo_name.ilike('%vcs_test_git%')).all())
assert len(groups) == 0
assert len(commits) == 0
def test_returns_list_of_repos_non_ascii_query(self, xhr_header):
self.log_user()
response = self.app.get(
route_path('repo_list_data'),
params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'},
extra_environ=xhr_header, status=200)
result = json.loads(response.body)['results']
repos, groups, commits = assert_and_get_content(result)
assert len(repos) == 0
assert len(groups) == 0
assert len(commits) == 0

View file

@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import json
import pytest
from rhodecode.tests import TestController
from rhodecode.tests.fixture import Fixture
fixture = Fixture()
def route_path(name, params=None, **kwargs):
import urllib
base_url = {
'user_autocomplete_data': '/_users',
'user_group_autocomplete_data': '/_user_groups'
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
class TestUserAutocompleteData(TestController):
def test_returns_list_of_users(self, user_util, xhr_header):
self.log_user()
user = user_util.create_user(active=True)
user_name = user.username
response = self.app.get(
route_path('user_autocomplete_data'),
extra_environ=xhr_header, status=200)
result = json.loads(response.body)
values = [suggestion['value'] for suggestion in result['suggestions']]
assert user_name in values
def test_returns_inactive_users_when_active_flag_sent(
self, user_util, xhr_header):
self.log_user()
user = user_util.create_user(active=False)
user_name = user.username
response = self.app.get(
route_path('user_autocomplete_data',
params=dict(user_groups='true', active='0')),
extra_environ=xhr_header, status=200)
result = json.loads(response.body)
values = [suggestion['value'] for suggestion in result['suggestions']]
assert user_name in values
response = self.app.get(
route_path('user_autocomplete_data',
params=dict(user_groups='true', active='1')),
extra_environ=xhr_header, status=200)
result = json.loads(response.body)
values = [suggestion['value'] for suggestion in result['suggestions']]
assert user_name not in values
def test_returns_groups_when_user_groups_flag_sent(
self, user_util, xhr_header):
self.log_user()
group = user_util.create_user_group(user_groups_active=True)
group_name = group.users_group_name
response = self.app.get(
route_path('user_autocomplete_data',
params=dict(user_groups='true')),
extra_environ=xhr_header, status=200)
result = json.loads(response.body)
values = [suggestion['value'] for suggestion in result['suggestions']]
assert group_name in values
@pytest.mark.parametrize('query, count', [
('hello1', 0),
('dev', 2),
])
def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header,
query, count):
self.log_user()
user_util._test_name = 'dev-test'
user_util.create_user()
user_util._test_name = 'dev-group-test'
user_util.create_user_group()
response = self.app.get(
route_path('user_autocomplete_data',
params=dict(user_groups='true', query=query)),
extra_environ=xhr_header, status=200)
result = json.loads(response.body)
assert len(result['suggestions']) == count

View file

@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import json
import pytest
from rhodecode.tests import TestController
from rhodecode.tests.fixture import Fixture
fixture = Fixture()
def route_path(name, params=None, **kwargs):
import urllib
base_url = {
'user_autocomplete_data': '/_users',
'user_group_autocomplete_data': '/_user_groups'
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
class TestUserGroupAutocompleteData(TestController):
def test_returns_list_of_user_groups(self, user_util, xhr_header):
self.log_user()
user_group = user_util.create_user_group(active=True)
user_group_name = user_group.users_group_name
response = self.app.get(
route_path('user_group_autocomplete_data'),
extra_environ=xhr_header, status=200)
result = json.loads(response.body)
values = [suggestion['value'] for suggestion in result['suggestions']]
assert user_group_name in values
def test_returns_inactive_user_groups_when_active_flag_sent(
self, user_util, xhr_header):
self.log_user()
user_group = user_util.create_user_group(active=False)
user_group_name = user_group.users_group_name
response = self.app.get(
route_path('user_group_autocomplete_data',
params=dict(active='0')),
extra_environ=xhr_header, status=200)
result = json.loads(response.body)
values = [suggestion['value'] for suggestion in result['suggestions']]
assert user_group_name in values
response = self.app.get(
route_path('user_group_autocomplete_data',
params=dict(active='1')),
extra_environ=xhr_header, status=200)
result = json.loads(response.body)
values = [suggestion['value'] for suggestion in result['suggestions']]
assert user_group_name not in values
@pytest.mark.parametrize('query, count', [
('hello1', 0),
('dev', 1),
])
def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, query, count):
self.log_user()
user_util._test_name = 'dev-test'
user_util.create_user_group()
response = self.app.get(
route_path('user_group_autocomplete_data',
params=dict(user_groups='true',
query=query)),
extra_environ=xhr_header, status=200)
result = json.loads(response.body)
assert len(result['suggestions']) == count

View file

@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import pytest
import rhodecode
from rhodecode.model.db import Repository
from rhodecode.model.meta import Session
from rhodecode.model.repo import RepoModel
from rhodecode.model.repo_group import RepoGroupModel
from rhodecode.model.settings import SettingsModel
from rhodecode.tests import TestController
from rhodecode.tests.fixture import Fixture
from rhodecode.lib import helpers as h
fixture = Fixture()
def route_path(name, **kwargs):
return {
'home': '/',
'repo_group_home': '/{repo_group_name}'
}[name].format(**kwargs)
class TestHomeController(TestController):
def test_index(self):
self.log_user()
response = self.app.get(route_path('home'))
# if global permission is set
response.mustcontain('Add Repository')
# search for objects inside the JavaScript JSON
for repo in Repository.getAll():
response.mustcontain('"name_raw": "%s"' % repo.repo_name)
def test_index_contains_statics_with_ver(self):
from pylons import tmpl_context as c
self.log_user()
response = self.app.get(route_path('home'))
rhodecode_version_hash = c.rhodecode_version_hash
response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
response.mustcontain('rhodecode-components.js?ver={0}'.format(rhodecode_version_hash))
def test_index_contains_backend_specific_details(self, backend):
self.log_user()
response = self.app.get(route_path('home'))
tip = backend.repo.get_commit().raw_id
# html in javascript variable:
response.mustcontain(r'<i class=\"icon-%s\"' % (backend.alias, ))
response.mustcontain(r'href=\"/%s\"' % (backend.repo_name, ))
response.mustcontain("""/%s/changeset/%s""" % (backend.repo_name, tip))
response.mustcontain("""Added a symlink""")
def test_index_with_anonymous_access_disabled(self):
with fixture.anon_access(False):
response = self.app.get(route_path('home'), status=302)
assert 'login' in response.location
def test_index_page_on_groups(self, autologin_user, repo_group):
response = self.app.get(route_path('repo_group_home', repo_group_name='gr1'))
response.mustcontain("gr1/repo_in_group")
def test_index_page_on_group_with_trailing_slash(
self, autologin_user, repo_group):
response = self.app.get(route_path('repo_group_home', repo_group_name='gr1') + '/')
response.mustcontain("gr1/repo_in_group")
@pytest.fixture(scope='class')
def repo_group(self, request):
gr = fixture.create_repo_group('gr1')
fixture.create_repo(name='gr1/repo_in_group', repo_group=gr)
@request.addfinalizer
def cleanup():
RepoModel().delete('gr1/repo_in_group')
RepoGroupModel().delete(repo_group='gr1', force_delete=True)
Session().commit()
def test_index_with_name_with_tags(self, user_util, autologin_user):
user = user_util.create_user()
username = user.username
user.name = '<img src="/image1" onload="alert(\'Hello, World!\');">'
user.lastname = '#"><img src=x onerror=prompt(document.cookie);>'
Session().add(user)
Session().commit()
user_util.create_repo(owner=username)
response = self.app.get(route_path('home'))
response.mustcontain(h.html_escape(user.first_name))
response.mustcontain(h.html_escape(user.last_name))
@pytest.mark.parametrize("name, state", [
('Disabled', False),
('Enabled', True),
])
def test_index_show_version(self, autologin_user, name, state):
version_string = 'RhodeCode Enterprise %s' % rhodecode.__version__
sett = SettingsModel().create_or_update_setting(
'show_version', state, 'bool')
Session().add(sett)
Session().commit()
SettingsModel().invalidate_settings_cache()
response = self.app.get(route_path('home'))
if state is True:
response.mustcontain(version_string)
if state is False:
response.mustcontain(no=[version_string])

View file

@ -0,0 +1,304 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import re
import logging
from pyramid.view import view_config
from rhodecode.apps._base import BaseAppView
from rhodecode.lib import helpers as h
from rhodecode.lib.auth import LoginRequired, NotAnonymous, \
HasRepoGroupPermissionAnyDecorator
from rhodecode.lib.index import searcher_from_config
from rhodecode.lib.utils2 import safe_unicode, str2bool
from rhodecode.lib.ext_json import json
from rhodecode.model.db import func, Repository, RepoGroup
from rhodecode.model.repo import RepoModel
from rhodecode.model.repo_group import RepoGroupModel
from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList
from rhodecode.model.user import UserModel
from rhodecode.model.user_group import UserGroupModel
log = logging.getLogger(__name__)
class HomeView(BaseAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
c.user = c.auth_user.get_instance()
self._register_global_c(c)
return c
@LoginRequired()
@view_config(
route_name='user_autocomplete_data', request_method='GET',
renderer='json_ext', xhr=True)
def user_autocomplete_data(self):
query = self.request.GET.get('query')
active = str2bool(self.request.GET.get('active') or True)
include_groups = str2bool(self.request.GET.get('user_groups'))
expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
log.debug('generating user list, query:%s, active:%s, with_groups:%s',
query, active, include_groups)
_users = UserModel().get_users(
name_contains=query, only_active=active)
def maybe_skip_default_user(usr):
if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
return False
return True
_users = filter(maybe_skip_default_user, _users)
if include_groups:
# extend with user groups
_user_groups = UserGroupModel().get_user_groups(
name_contains=query, only_active=active,
expand_groups=expand_groups)
_users = _users + _user_groups
return {'suggestions': _users}
@LoginRequired()
@NotAnonymous()
@view_config(
route_name='user_group_autocomplete_data', request_method='GET',
renderer='json_ext', xhr=True)
def user_group_autocomplete_data(self):
query = self.request.GET.get('query')
active = str2bool(self.request.GET.get('active') or True)
expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
log.debug('generating user group list, query:%s, active:%s',
query, active)
_user_groups = UserGroupModel().get_user_groups(
name_contains=query, only_active=active,
expand_groups=expand_groups)
_user_groups = _user_groups
return {'suggestions': _user_groups}
def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
query = Repository.query()\
.order_by(func.length(Repository.repo_name))\
.order_by(Repository.repo_name)
if repo_type:
query = query.filter(Repository.repo_type == repo_type)
if name_contains:
ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
query = query.filter(
Repository.repo_name.ilike(ilike_expression))
query = query.limit(limit)
all_repos = query.all()
# permission checks are inside this function
repo_iter = ScmModel().get_repos(all_repos)
return [
{
'id': obj['name'],
'text': obj['name'],
'type': 'repo',
'obj': obj['dbrepo'],
'url': h.route_path('repo_summary', repo_name=obj['name'])
}
for obj in repo_iter]
def _get_repo_group_list(self, name_contains=None, limit=20):
query = RepoGroup.query()\
.order_by(func.length(RepoGroup.group_name))\
.order_by(RepoGroup.group_name)
if name_contains:
ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
query = query.filter(
RepoGroup.group_name.ilike(ilike_expression))
query = query.limit(limit)
all_groups = query.all()
repo_groups_iter = ScmModel().get_repo_groups(all_groups)
return [
{
'id': obj.group_name,
'text': obj.group_name,
'type': 'group',
'obj': {},
'url': h.route_path('repo_group_home', repo_group_name=obj.group_name)
}
for obj in repo_groups_iter]
def _get_hash_commit_list(self, auth_user, hash_starts_with=None):
if not hash_starts_with or len(hash_starts_with) < 3:
return []
commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with)
if len(commit_hashes) != 1:
return []
commit_hash_prefix = commit_hashes[0]
searcher = searcher_from_config(self.request.registry.settings)
result = searcher.search(
'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user,
raise_on_exc=False)
return [
{
'id': entry['commit_id'],
'text': entry['commit_id'],
'type': 'commit',
'obj': {'repo': entry['repository']},
'url': h.url('changeset_home',
repo_name=entry['repository'],
revision=entry['commit_id'])
}
for entry in result['results']]
@LoginRequired()
@view_config(
route_name='repo_list_data', request_method='GET',
renderer='json_ext', xhr=True)
def repo_list_data(self):
_ = self.request.translate
query = self.request.GET.get('query')
repo_type = self.request.GET.get('repo_type')
log.debug('generating repo list, query:%s, repo_type:%s',
query, repo_type)
res = []
repos = self._get_repo_list(query, repo_type=repo_type)
if repos:
res.append({
'text': _('Repositories'),
'children': repos
})
data = {
'more': False,
'results': res
}
return data
@LoginRequired()
@view_config(
route_name='goto_switcher_data', request_method='GET',
renderer='json_ext', xhr=True)
def goto_switcher_data(self):
c = self.load_default_context()
_ = self.request.translate
query = self.request.GET.get('query')
log.debug('generating goto switcher list, query %s', query)
res = []
repo_groups = self._get_repo_group_list(query)
if repo_groups:
res.append({
'text': _('Groups'),
'children': repo_groups
})
repos = self._get_repo_list(query)
if repos:
res.append({
'text': _('Repositories'),
'children': repos
})
commits = self._get_hash_commit_list(c.auth_user, query)
if commits:
unique_repos = {}
for commit in commits:
unique_repos.setdefault(commit['obj']['repo'], []
).append(commit)
for repo in unique_repos:
res.append({
'text': _('Commits in %(repo)s') % {'repo': repo},
'children': unique_repos[repo]
})
data = {
'more': False,
'results': res
}
return data
def _get_groups_and_repos(self, repo_group_id=None):
# repo groups groups
repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
_perms = ['group.read', 'group.write', 'group.admin']
repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
repo_group_list=repo_group_list_acl, admin=False)
# repositories
repo_list = Repository.get_all_repos(group_id=repo_group_id)
_perms = ['repository.read', 'repository.write', 'repository.admin']
repo_list_acl = RepoList(repo_list, perm_set=_perms)
repo_data = RepoModel().get_repos_as_dict(
repo_list=repo_list_acl, admin=False)
return repo_data, repo_group_data
@LoginRequired()
@view_config(
route_name='home', request_method='GET',
renderer='rhodecode:templates/index.mako')
def main_page(self):
c = self.load_default_context()
c.repo_group = None
repo_data, repo_group_data = self._get_groups_and_repos()
# json used to render the grids
c.repos_data = json.dumps(repo_data)
c.repo_groups_data = json.dumps(repo_group_data)
return self._get_template_context(c)
@LoginRequired()
@HasRepoGroupPermissionAnyDecorator(
'group.read', 'group.write', 'group.admin')
@view_config(
route_name='repo_group_home', request_method='GET',
renderer='rhodecode:templates/index_repo_group.mako')
@view_config(
route_name='repo_group_home_slash', request_method='GET',
renderer='rhodecode:templates/index_repo_group.mako')
def repo_group_main_page(self):
c = self.load_default_context()
c.repo_group = self.request.db_repo_group
repo_data, repo_group_data = self._get_groups_and_repos(
c.repo_group.group_id)
# json used to render the grids
c.repos_data = json.dumps(repo_data)
c.repo_groups_data = json.dumps(repo_group_data)
return self._get_template_context(c)

View file

@ -25,7 +25,6 @@ import formencode
import logging
import urlparse
from pylons import url
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
from recaptcha.client.captcha import submit
@ -34,6 +33,7 @@ from rhodecode.apps._base import BaseAppView
from rhodecode.authentication.base import authenticate, HTTP_TYPE
from rhodecode.events import UserRegistered
from rhodecode.lib import helpers as h
from rhodecode.lib import audit_logger
from rhodecode.lib.auth import (
AuthUser, HasPermissionAnyDecorator, CSRFRequired)
from rhodecode.lib.base import get_ip_addr
@ -90,20 +90,21 @@ def get_came_from(request):
came_from = safe_str(request.GET.get('came_from', ''))
parsed = urlparse.urlparse(came_from)
allowed_schemes = ['http', 'https']
default_came_from = h.route_path('home')
if parsed.scheme and parsed.scheme not in allowed_schemes:
log.error('Suspicious URL scheme detected %s for url %s' %
(parsed.scheme, parsed))
came_from = url('home')
came_from = default_came_from
elif parsed.netloc and request.host != parsed.netloc:
log.error('Suspicious NETLOC detected %s for url %s server url '
'is: %s' % (parsed.netloc, parsed, request.host))
came_from = url('home')
came_from = default_came_from
elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
log.error('Header injection detected `%s` for url %s server url ' %
(parsed.path, parsed))
came_from = url('home')
came_from = default_came_from
return came_from or url('home')
return came_from or default_came_from
class LoginView(BaseAppView):
@ -166,6 +167,15 @@ class LoginView(BaseAppView):
username=form_result['username'],
remember=form_result['remember'])
log.debug('Redirecting to "%s" after login.', c.came_from)
audit_user = audit_logger.UserWrap(
username=self.request.params.get('username'),
ip_addr=self.request.remote_addr)
action_data = {'user_agent': self.request.user_agent}
audit_logger.store_web(
'user.login.success', action_data=action_data,
user=audit_user, commit=True)
raise HTTPFound(c.came_from, headers=headers)
except formencode.Invalid as errors:
defaults = errors.value
@ -176,6 +186,14 @@ class LoginView(BaseAppView):
'errors': errors.error_dict,
'defaults': defaults,
})
audit_user = audit_logger.UserWrap(
username=self.request.params.get('username'),
ip_addr=self.request.remote_addr)
action_data = {'user_agent': self.request.user_agent}
audit_logger.store_web(
'user.login.failure', action_data=action_data,
user=audit_user, commit=True)
return render_ctx
except UserCreationError as e:
@ -191,8 +209,13 @@ class LoginView(BaseAppView):
def logout(self):
auth_user = self._rhodecode_user
log.info('Deleting session for user: `%s`', auth_user)
action_data = {'user_agent': self.request.user_agent}
audit_logger.store_web(
'user.logout', action_data=action_data,
user=auth_user, commit=True)
self.session.delete()
return HTTPFound(url('home'))
return HTTPFound(h.route_path('home'))
@HasPermissionAnyDecorator(
'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
@ -338,6 +361,12 @@ class LoginView(BaseAppView):
form_result, password_reset_url)
# Display success message and redirect.
self.session.flash(msg, queue='success')
action_data = {'email': user_email,
'user_agent': self.request.user_agent}
audit_logger.store_web(
'user.password.reset_request', action_data=action_data,
user=self._rhodecode_user, commit=True)
return HTTPFound(self.request.route_path('reset_password'))
except formencode.Invalid as errors:

View file

@ -41,12 +41,45 @@ def includeme(config):
pattern=ADMIN_PREFIX + '/my_account/auth_tokens')
config.add_route(
name='my_account_auth_tokens_add',
pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new',
)
pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new')
config.add_route(
name='my_account_auth_tokens_delete',
pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete',
)
pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete')
config.add_route(
name='my_account_emails',
pattern=ADMIN_PREFIX + '/my_account/emails')
config.add_route(
name='my_account_emails_add',
pattern=ADMIN_PREFIX + '/my_account/emails/new')
config.add_route(
name='my_account_emails_delete',
pattern=ADMIN_PREFIX + '/my_account/emails/delete')
config.add_route(
name='my_account_repos',
pattern=ADMIN_PREFIX + '/my_account/repos')
config.add_route(
name='my_account_watched',
pattern=ADMIN_PREFIX + '/my_account/watched')
config.add_route(
name='my_account_perms',
pattern=ADMIN_PREFIX + '/my_account/perms')
config.add_route(
name='my_account_notifications',
pattern=ADMIN_PREFIX + '/my_account/notifications')
config.add_route(
name='my_account_notifications_toggle_visibility',
pattern=ADMIN_PREFIX + '/my_account/toggle_visibility')
# channelstream test
config.add_route(
name='my_account_notifications_test_channelstream',
pattern=ADMIN_PREFIX + '/my_account/test_channelstream')
# Scan module for configuration decorators.
config.scan()

View file

@ -103,7 +103,7 @@ class TestMyAccountAuthTokens(TestController):
response = self.app.post(
route_path('my_account_auth_tokens_delete'),
{'del_auth_token': keys[0].api_key, 'csrf_token': self.csrf_token})
{'del_auth_token': keys[0].user_api_key_id, 'csrf_token': self.csrf_token})
assert_session_flash(response, 'Auth token successfully deleted')
user = User.get(user_id)

View file

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import pytest
from rhodecode.apps._base import ADMIN_PREFIX
from rhodecode.model.db import User, UserEmailMap
from rhodecode.tests import (
TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
assert_session_flash)
from rhodecode.tests.fixture import Fixture
fixture = Fixture()
def route_path(name, **kwargs):
return {
'my_account_emails':
ADMIN_PREFIX + '/my_account/emails',
'my_account_emails_add':
ADMIN_PREFIX + '/my_account/emails/new',
'my_account_emails_delete':
ADMIN_PREFIX + '/my_account/emails/delete',
}[name].format(**kwargs)
class TestMyAccountEmails(TestController):
def test_my_account_my_emails(self):
self.log_user()
response = self.app.get(route_path('my_account_emails'))
response.mustcontain('No additional emails specified')
def test_my_account_my_emails_add_existing_email(self):
self.log_user()
response = self.app.get(route_path('my_account_emails'))
response.mustcontain('No additional emails specified')
response = self.app.post(route_path('my_account_emails_add'),
{'new_email': TEST_USER_REGULAR_EMAIL,
'csrf_token': self.csrf_token})
assert_session_flash(response, 'This e-mail address is already taken')
def test_my_account_my_emails_add_mising_email_in_form(self):
self.log_user()
response = self.app.get(route_path('my_account_emails'))
response.mustcontain('No additional emails specified')
response = self.app.post(route_path('my_account_emails_add'),
{'csrf_token': self.csrf_token})
assert_session_flash(response, 'Please enter an email address')
def test_my_account_my_emails_add_remove(self):
self.log_user()
response = self.app.get(route_path('my_account_emails'))
response.mustcontain('No additional emails specified')
response = self.app.post(route_path('my_account_emails_add'),
{'new_email': 'foo@barz.com',
'csrf_token': self.csrf_token})
response = self.app.get(route_path('my_account_emails'))
email_id = UserEmailMap.query().filter(
UserEmailMap.user == User.get_by_username(
TEST_USER_ADMIN_LOGIN)).filter(
UserEmailMap.email == 'foo@barz.com').one().email_id
response.mustcontain('foo@barz.com')
response.mustcontain('<input id="del_email_id" name="del_email_id" '
'type="hidden" value="%s" />' % email_id)
response = self.app.post(
route_path('my_account_emails_delete'), {
'del_email_id': email_id,
'csrf_token': self.csrf_token})
assert_session_flash(response, 'Email successfully deleted')
response = self.app.get(route_path('my_account_emails'))
response.mustcontain('No additional emails specified')

View file

@ -132,6 +132,7 @@ class TestMyAccountPassword(TestController):
self.app.post(route_path('my_account_password'), form_data)
response = self.app.get(route_path('home'))
new_password_hash = response.session['rhodecode_user']['password']
session = response.get_session_from_response()
new_password_hash = session['rhodecode_user']['password']
assert old_password_hash != new_password_hash

View file

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import pytest
from rhodecode.apps._base import ADMIN_PREFIX
from rhodecode.model.db import User, UserEmailMap, Repository, UserFollowing
from rhodecode.tests import (
TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
assert_session_flash)
from rhodecode.tests.fixture import Fixture
fixture = Fixture()
def route_path(name, **kwargs):
return {
'my_account_repos':
ADMIN_PREFIX + '/my_account/repos',
'my_account_watched':
ADMIN_PREFIX + '/my_account/watched',
'my_account_perms':
ADMIN_PREFIX + '/my_account/perms',
'my_account_notifications':
ADMIN_PREFIX + '/my_account/notifications',
}[name].format(**kwargs)
class TestMyAccountSimpleViews(TestController):
def test_my_account_my_repos(self, autologin_user):
response = self.app.get(route_path('my_account_repos'))
repos = Repository.query().filter(
Repository.user == User.get_by_username(
TEST_USER_ADMIN_LOGIN)).all()
for repo in repos:
response.mustcontain('"name_raw": "%s"' % repo.repo_name)
def test_my_account_my_watched(self, autologin_user):
response = self.app.get(route_path('my_account_watched'))
repos = UserFollowing.query().filter(
UserFollowing.user == User.get_by_username(
TEST_USER_ADMIN_LOGIN)).all()
for repo in repos:
response.mustcontain(
'"name_raw": "%s"' % repo.follows_repository.repo_name)
def test_my_account_perms(self, autologin_user):
response = self.app.get(route_path('my_account_perms'))
assert_response = response.assert_response()
assert assert_response.get_elements('.perm_tag.none')
assert assert_response.get_elements('.perm_tag.read')
assert assert_response.get_elements('.perm_tag.write')
assert assert_response.get_elements('.perm_tag.admin')
def test_my_account_notifications(self, autologin_user):
response = self.app.get(route_path('my_account_notifications'))
response.mustcontain('Test flash message')

View file

@ -19,18 +19,28 @@
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
import datetime
import formencode
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
from rhodecode.apps._base import BaseAppView
from rhodecode import forms
from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
from rhodecode.lib import helpers as h
from rhodecode.lib import audit_logger
from rhodecode.lib.ext_json import json
from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
from rhodecode.lib.channelstream import channelstream_request, \
ChannelstreamException
from rhodecode.lib.utils2 import safe_int, md5
from rhodecode.model.auth_token import AuthTokenModel
from rhodecode.model.db import (
Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload)
from rhodecode.model.meta import Session
from rhodecode.model.scm import RepoList
from rhodecode.model.user import UserModel
from rhodecode.model.repo import RepoModel
from rhodecode.model.validation_schema.schemas import user_schema
log = logging.getLogger(__name__)
@ -158,7 +168,7 @@ class MyAccountView(BaseAppView):
@NotAnonymous()
@CSRFRequired()
@view_config(
route_name='my_account_auth_tokens_add', request_method='POST')
route_name='my_account_auth_tokens_add', request_method='POST',)
def my_account_auth_tokens_add(self):
_ = self.request.translate
c = self.load_default_context()
@ -169,7 +179,13 @@ class MyAccountView(BaseAppView):
token = AuthTokenModel().create(
c.user.user_id, description, lifetime, role)
token_data = token.get_api_data()
self.maybe_attach_token_scope(token)
audit_logger.store_web(
'user.edit.token.add', action_data={
'data': {'token': token_data, 'user': 'self'}},
user=self._rhodecode_user, )
Session().commit()
h.flash(_("Auth token successfully created"), category='success')
@ -187,8 +203,197 @@ class MyAccountView(BaseAppView):
del_auth_token = self.request.POST.get('del_auth_token')
if del_auth_token:
token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True)
token_data = token.get_api_data()
AuthTokenModel().delete(del_auth_token, c.user.user_id)
audit_logger.store_web(
'user.edit.token.delete', action_data={
'data': {'token': token_data, 'user': 'self'}},
user=self._rhodecode_user,)
Session().commit()
h.flash(_("Auth token successfully deleted"), category='success')
return HTTPFound(h.route_path('my_account_auth_tokens'))
@LoginRequired()
@NotAnonymous()
@view_config(
route_name='my_account_emails', request_method='GET',
renderer='rhodecode:templates/admin/my_account/my_account.mako')
def my_account_emails(self):
_ = self.request.translate
c = self.load_default_context()
c.active = 'emails'
c.user_email_map = UserEmailMap.query()\
.filter(UserEmailMap.user == c.user).all()
return self._get_template_context(c)
@LoginRequired()
@NotAnonymous()
@CSRFRequired()
@view_config(
route_name='my_account_emails_add', request_method='POST')
def my_account_emails_add(self):
_ = self.request.translate
c = self.load_default_context()
email = self.request.POST.get('new_email')
try:
UserModel().add_extra_email(c.user.user_id, email)
audit_logger.store_web(
'user.edit.email.add', action_data={
'data': {'email': email, 'user': 'self'}},
user=self._rhodecode_user,)
Session().commit()
h.flash(_("Added new email address `%s` for user account") % email,
category='success')
except formencode.Invalid as error:
h.flash(h.escape(error.error_dict['email']), category='error')
except Exception:
log.exception("Exception in my_account_emails")
h.flash(_('An error occurred during email saving'),
category='error')
return HTTPFound(h.route_path('my_account_emails'))
@LoginRequired()
@NotAnonymous()
@CSRFRequired()
@view_config(
route_name='my_account_emails_delete', request_method='POST')
def my_account_emails_delete(self):
_ = self.request.translate
c = self.load_default_context()
del_email_id = self.request.POST.get('del_email_id')
if del_email_id:
email = UserEmailMap.get_or_404(del_email_id, pyramid_exc=True).email
UserModel().delete_extra_email(c.user.user_id, del_email_id)
audit_logger.store_web(
'user.edit.email.delete', action_data={
'data': {'email': email, 'user': 'self'}},
user=self._rhodecode_user,)
Session().commit()
h.flash(_("Email successfully deleted"),
category='success')
return HTTPFound(h.route_path('my_account_emails'))
@LoginRequired()
@NotAnonymous()
@CSRFRequired()
@view_config(
route_name='my_account_notifications_test_channelstream',
request_method='POST', renderer='json_ext')
def my_account_notifications_test_channelstream(self):
message = 'Test message sent via Channelstream by user: {}, on {}'.format(
self._rhodecode_user.username, datetime.datetime.now())
payload = {
# 'channel': 'broadcast',
'type': 'message',
'timestamp': datetime.datetime.utcnow(),
'user': 'system',
'pm_users': [self._rhodecode_user.username],
'message': {
'message': message,
'level': 'info',
'topic': '/notifications'
}
}
registry = self.request.registry
rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
channelstream_config = rhodecode_plugins.get('channelstream', {})
try:
channelstream_request(channelstream_config, [payload], '/message')
except ChannelstreamException as e:
log.exception('Failed to send channelstream data')
return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
return {"response": 'Channelstream data sent. '
'You should see a new live message now.'}
def _load_my_repos_data(self, watched=False):
if watched:
admin = False
follows_repos = Session().query(UserFollowing)\
.filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
.options(joinedload(UserFollowing.follows_repository))\
.all()
repo_list = [x.follows_repository for x in follows_repos]
else:
admin = True
repo_list = Repository.get_all_repos(
user_id=self._rhodecode_user.user_id)
repo_list = RepoList(repo_list, perm_set=[
'repository.read', 'repository.write', 'repository.admin'])
repos_data = RepoModel().get_repos_as_dict(
repo_list=repo_list, admin=admin)
# json used to render the grid
return json.dumps(repos_data)
@LoginRequired()
@NotAnonymous()
@view_config(
route_name='my_account_repos', request_method='GET',
renderer='rhodecode:templates/admin/my_account/my_account.mako')
def my_account_repos(self):
c = self.load_default_context()
c.active = 'repos'
# json used to render the grid
c.data = self._load_my_repos_data()
return self._get_template_context(c)
@LoginRequired()
@NotAnonymous()
@view_config(
route_name='my_account_watched', request_method='GET',
renderer='rhodecode:templates/admin/my_account/my_account.mako')
def my_account_watched(self):
c = self.load_default_context()
c.active = 'watched'
# json used to render the grid
c.data = self._load_my_repos_data(watched=True)
return self._get_template_context(c)
@LoginRequired()
@NotAnonymous()
@view_config(
route_name='my_account_perms', request_method='GET',
renderer='rhodecode:templates/admin/my_account/my_account.mako')
def my_account_perms(self):
c = self.load_default_context()
c.active = 'perms'
c.perm_user = c.auth_user
return self._get_template_context(c)
@LoginRequired()
@NotAnonymous()
@view_config(
route_name='my_account_notifications', request_method='GET',
renderer='rhodecode:templates/admin/my_account/my_account.mako')
def my_notifications(self):
c = self.load_default_context()
c.active = 'notifications'
return self._get_template_context(c)
@LoginRequired()
@NotAnonymous()
@CSRFRequired()
@view_config(
route_name='my_account_notifications_toggle_visibility',
request_method='POST', renderer='json_ext')
def my_notifications_toggle_visibility(self):
user = self._rhodecode_db_user
new_status = not user.user_data.get('notification_status', True)
user.update_userdata(notification_status=new_status)
Session().commit()
return user.user_data['notification_status']

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2017 RhodeCode GmbH
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
@ -18,21 +18,18 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
"""
Tags controller for rhodecode
"""
import logging
from rhodecode.controllers.base_references import BaseReferencesController
log = logging.getLogger(__name__)
from rhodecode.config.routing import ADMIN_PREFIX
class TagsController(BaseReferencesController):
def admin_routes(config):
config.add_route(
name='ops_ping',
pattern='/ping')
partials_template = 'tags/tags_data.mako'
template = 'tags/tags.mako'
def _get_reference_items(self, repo):
return repo.tags.items()
def includeme(config):
config.include(admin_routes, route_prefix=ADMIN_PREFIX + '/ops')
# Scan module for configuration decorators.
config.scan()

View file

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
from pyramid.view import view_config
from rhodecode.apps._base import BaseAppView
log = logging.getLogger(__name__)
class OpsView(BaseAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
c.user = c.auth_user.get_instance()
self._register_global_c(c)
return c
@view_config(
route_name='ops_ping', request_method='GET',
renderer='json_ext')
def ops_ping(self):
data = {
'instance': self.request.registry.settings.get('instance_id'),
}
if getattr(self.request, 'user'):
data.update({
'caller_ip': self.request.user.ip_addr,
'caller_name': self.request.user.username,
})
return {'ok': data}

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
from rhodecode.apps._base import add_route_with_slash
def includeme(config):
# Summary
add_route_with_slash(
config,
name='repo_group_home',
pattern='/{repo_group_name:.*?[^/]}', repo_group_route=True)
# Scan module for configuration decorators.
config.scan()

View file

@ -17,30 +17,137 @@
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
from rhodecode.apps._base import add_route_with_slash
def includeme(config):
# Summary
# NOTE(marcink): one additional route is defined in very bottom, catch
# all pattern
config.add_route(
name='repo_summary_explicit',
pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
config.add_route(
name='repo_summary_commits',
pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
# repo commits
config.add_route(
name='repo_commit',
pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
# refs data
config.add_route(
name='repo_refs_data',
pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
config.add_route(
name='repo_refs_changelog_data',
pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
config.add_route(
name='repo_stats',
pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
# Tags
config.add_route(
name='tags_home',
pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
# Branches
config.add_route(
name='branches_home',
pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
config.add_route(
name='bookmarks_home',
pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
# Pull Requests
config.add_route(
name='pullrequest_show',
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}',
repo_route=True)
config.add_route(
name='pullrequest_show_all',
pattern='/{repo_name:.*?[^/]}/pull-request',
repo_route=True, repo_accepted_types=['hg', 'git'])
config.add_route(
name='pullrequest_show_all_data',
pattern='/{repo_name:.*?[^/]}/pull-request-data',
repo_route=True, repo_accepted_types=['hg', 'git'])
# Settings
config.add_route(
name='edit_repo',
pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
# Settings advanced
config.add_route(
name='edit_repo_advanced',
pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
config.add_route(
name='edit_repo_advanced_delete',
pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
config.add_route(
name='edit_repo_advanced_locking',
pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
config.add_route(
name='edit_repo_advanced_journal',
pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
config.add_route(
name='edit_repo_advanced_fork',
pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
# Caches
config.add_route(
name='edit_repo_caches',
pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
# Permissions
config.add_route(
name='edit_repo_perms',
pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
# Repo Review Rules
config.add_route(
name='repo_reviewers',
pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
config.add_route(
name='repo_default_reviewers_data',
pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
# Maintenance
config.add_route(
name='repo_maintenance',
pattern='/{repo_name:.*?[^/]}/maintenance', repo_route=True)
pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
config.add_route(
name='repo_maintenance_execute',
pattern='/{repo_name:.*?[^/]}/maintenance/execute', repo_route=True)
pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
# Strip
config.add_route(
name='strip',
pattern='/{repo_name:.*?[^/]}/strip', repo_route=True)
pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
config.add_route(
name='strip_check',
pattern='/{repo_name:.*?[^/]}/strip_check', repo_route=True)
pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
config.add_route(
name='strip_execute',
pattern='/{repo_name:.*?[^/]}/strip_execute', repo_route=True)
pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
# NOTE(marcink): needs to be at the end for catch-all
add_route_with_slash(
config,
name='repo_summary',
pattern='/{repo_name:.*?[^/]}', repo_route=True)
# Scan module for configuration decorators.
config.scan()

View file

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import pytest
from rhodecode.model.db import Repository
def route_path(name, params=None, **kwargs):
import urllib
base_url = {
'pullrequest_show_all': '/{repo_name}/pull-request',
'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
@pytest.mark.backends("git", "hg")
@pytest.mark.usefixtures('autologin_user', 'app')
class TestPullRequestList(object):
@pytest.mark.parametrize('params, expected_title', [
({'source': 0, 'closed': 1}, 'Closed Pull Requests'),
({'source': 0, 'my': 1}, 'opened by me'),
({'source': 0, 'awaiting_review': 1}, 'awaiting review'),
({'source': 0, 'awaiting_my_review': 1}, 'awaiting my review'),
({'source': 1}, 'Pull Requests from'),
])
def test_showing_list_page(self, backend, pr_util, params, expected_title):
pull_request = pr_util.create_pull_request()
response = self.app.get(
route_path('pullrequest_show_all',
repo_name=pull_request.target_repo.repo_name,
params=params))
assert_response = response.assert_response()
assert_response.element_equals_to('.panel-title', expected_title)
element = assert_response.get_element('.panel-title')
element_text = assert_response._element_to_string(element)
def test_showing_list_page_data(self, backend, pr_util, xhr_header):
pull_request = pr_util.create_pull_request()
response = self.app.get(
route_path('pullrequest_show_all_data',
repo_name=pull_request.target_repo.repo_name),
extra_environ=xhr_header)
assert response.json['recordsTotal'] == 1
assert response.json['data'][0]['description'] == 'Description'
def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
xss_description = "<script>alert('Hi!')</script>"
pull_request = pr_util.create_pull_request(description=xss_description)
response = self.app.get(
route_path('pullrequest_show_all_data',
repo_name=pull_request.target_repo.repo_name),
extra_environ=xhr_header)
assert response.json['recordsTotal'] == 1
assert response.json['data'][0]['description'] == \
"&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"

View file

@ -18,24 +18,35 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import pytest
from rhodecode.model.db import Repository
from rhodecode.tests import *
class TestBookmarksController(TestController):
def route_path(name, params=None, **kwargs):
import urllib
base_url = {
'bookmarks_home': '/{repo_name}/bookmarks',
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
@pytest.mark.usefixtures('autologin_user', 'app')
class TestBookmarks(object):
def test_index(self, backend):
self.log_user()
if backend.alias == 'hg':
response = self.app.get(url(controller='bookmarks',
action='index',
repo_name=backend.repo_name))
response = self.app.get(
route_path('bookmarks_home', repo_name=backend.repo_name))
repo = Repository.get_by_repo_name(backend.repo_name)
for commit_id, obj_name in repo.scm_instance().bookmarks.items():
assert commit_id in response
assert obj_name in response
else:
self.app.get(url(controller='bookmarks',
action='index',
repo_name=backend.repo_name), status=404)
self.app.get(
route_path('bookmarks_home', repo_name=backend.repo_name),
status=404)

View file

@ -18,17 +18,28 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import pytest
from rhodecode.model.db import Repository
from rhodecode.tests import *
class TestBranchesController(TestController):
def route_path(name, params=None, **kwargs):
import urllib
base_url = {
'branches_home': '/{repo_name}/branches',
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
@pytest.mark.usefixtures('autologin_user', 'app')
class TestBranchesController(object):
def test_index(self, backend):
self.log_user()
response = self.app.get(url(controller='branches',
action='index',
repo_name=backend.repo_name))
response = self.app.get(
route_path('branches_home', repo_name=backend.repo_name))
repo = Repository.get_by_repo_name(backend.repo_name)

View file

@ -0,0 +1,233 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import mock
import pytest
from rhodecode.lib.utils2 import str2bool
from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
from rhodecode.model.meta import Session
from rhodecode.tests import (
url, HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN,
assert_session_flash)
from rhodecode.tests.fixture import Fixture
fixture = Fixture()
def route_path(name, params=None, **kwargs):
import urllib
base_url = {
'edit_repo': '/{repo_name}/settings',
'edit_repo_advanced': '/{repo_name}/settings/advanced',
'edit_repo_caches': '/{repo_name}/settings/caches',
'edit_repo_perms': '/{repo_name}/settings/permissions',
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
def _get_permission_for_user(user, repo):
perm = UserRepoToPerm.query()\
.filter(UserRepoToPerm.repository ==
Repository.get_by_repo_name(repo))\
.filter(UserRepoToPerm.user == User.get_by_username(user))\
.all()
return perm
@pytest.mark.usefixtures('autologin_user', 'app')
class TestAdminRepoSettings(object):
@pytest.mark.parametrize('urlname', [
'edit_repo',
'edit_repo_caches',
'edit_repo_perms',
'edit_repo_advanced',
])
def test_show_page(self, urlname, app, backend):
app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
def test_edit_accessible_when_missing_requirements(
self, backend_hg, autologin_user):
scm_patcher = mock.patch.object(
Repository, 'scm_instance', side_effect=RepositoryRequirementError)
with scm_patcher:
self.app.get(route_path('edit_repo', repo_name=backend_hg.repo_name))
@pytest.mark.parametrize('urlname', [
'repo_vcs_settings',
'repo_settings_issuetracker',
'edit_repo_fields',
'edit_repo_remote',
'edit_repo_statistics',
])
def test_show_page_pylons(self, urlname, app):
app.get(url(urlname, repo_name=HG_REPO))
@pytest.mark.parametrize('update_settings', [
{'repo_description': 'alter-desc'},
{'repo_owner': TEST_USER_REGULAR_LOGIN},
{'repo_private': 'true'},
{'repo_enable_locking': 'true'},
{'repo_enable_downloads': 'true'},
])
def test_update_repo_settings(self, update_settings, csrf_token, backend, user_util):
repo = user_util.create_repo(repo_type=backend.alias)
repo_name = repo.repo_name
params = fixture._get_repo_create_params(
csrf_token=csrf_token,
repo_name=repo_name,
repo_type=backend.alias,
repo_owner=TEST_USER_ADMIN_LOGIN,
repo_description='DESC',
repo_private='false',
repo_enable_locking='false',
repo_enable_downloads='false')
params.update(update_settings)
self.app.post(
route_path('edit_repo', repo_name=repo_name),
params=params, status=302)
repo = Repository.get_by_repo_name(repo_name)
assert repo.user.username == \
update_settings.get('repo_owner', repo.user.username)
assert repo.description == \
update_settings.get('repo_description', repo.description)
assert repo.private == \
str2bool(update_settings.get(
'repo_private', repo.private))
assert repo.enable_locking == \
str2bool(update_settings.get(
'repo_enable_locking', repo.enable_locking))
assert repo.enable_downloads == \
str2bool(update_settings.get(
'repo_enable_downloads', repo.enable_downloads))
def test_update_repo_name_via_settings(self, csrf_token, user_util, backend):
repo = user_util.create_repo(repo_type=backend.alias)
repo_name = repo.repo_name
repo_group = user_util.create_repo_group()
repo_group_name = repo_group.group_name
new_name = repo_group_name + '_' + repo_name
params = fixture._get_repo_create_params(
csrf_token=csrf_token,
repo_name=new_name,
repo_type=backend.alias,
repo_owner=TEST_USER_ADMIN_LOGIN,
repo_description='DESC',
repo_private='false',
repo_enable_locking='false',
repo_enable_downloads='false')
self.app.post(
route_path('edit_repo', repo_name=repo_name),
params=params, status=302)
repo = Repository.get_by_repo_name(new_name)
assert repo.repo_name == new_name
def test_update_repo_group_via_settings(self, csrf_token, user_util, backend):
repo = user_util.create_repo(repo_type=backend.alias)
repo_name = repo.repo_name
repo_group = user_util.create_repo_group()
repo_group_name = repo_group.group_name
repo_group_id = repo_group.group_id
new_name = repo_group_name + '/' + repo_name
params = fixture._get_repo_create_params(
csrf_token=csrf_token,
repo_name=repo_name,
repo_type=backend.alias,
repo_owner=TEST_USER_ADMIN_LOGIN,
repo_description='DESC',
repo_group=repo_group_id,
repo_private='false',
repo_enable_locking='false',
repo_enable_downloads='false')
self.app.post(
route_path('edit_repo', repo_name=repo_name),
params=params, status=302)
repo = Repository.get_by_repo_name(new_name)
assert repo.repo_name == new_name
def test_set_private_flag_sets_default_user_permissions_to_none(
self, autologin_user, backend, csrf_token):
# initially repository perm should be read
perm = _get_permission_for_user(user='default', repo=backend.repo_name)
assert len(perm) == 1
assert perm[0].permission.permission_name == 'repository.read'
assert not backend.repo.private
response = self.app.post(
route_path('edit_repo', repo_name=backend.repo_name),
params=fixture._get_repo_create_params(
repo_private='true',
repo_name=backend.repo_name,
repo_type=backend.alias,
repo_owner=TEST_USER_ADMIN_LOGIN,
csrf_token=csrf_token), status=302)
assert_session_flash(
response,
msg='Repository %s updated successfully' % (backend.repo_name))
repo = Repository.get_by_repo_name(backend.repo_name)
assert repo.private is True
# now the repo default permission should be None
perm = _get_permission_for_user(user='default', repo=backend.repo_name)
assert len(perm) == 1
assert perm[0].permission.permission_name == 'repository.none'
response = self.app.post(
route_path('edit_repo', repo_name=backend.repo_name),
params=fixture._get_repo_create_params(
repo_private='false',
repo_name=backend.repo_name,
repo_type=backend.alias,
repo_owner=TEST_USER_ADMIN_LOGIN,
csrf_token=csrf_token), status=302)
assert_session_flash(
response,
msg='Repository %s updated successfully' % (backend.repo_name))
assert backend.repo.private is False
# we turn off private now the repo default permission should stay None
perm = _get_permission_for_user(user='default', repo=backend.repo_name)
assert len(perm) == 1
assert perm[0].permission.permission_name == 'repository.none'
# update this permission back
perm[0].permission = Permission.get_by_key('repository.read')
Session().add(perm[0])
Session().commit()

View file

@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import pytest
from rhodecode.lib.utils2 import safe_unicode, safe_str
from rhodecode.model.db import Repository
from rhodecode.model.repo import RepoModel
from rhodecode.tests import (
HG_REPO, GIT_REPO, assert_session_flash, no_newline_id_generator)
from rhodecode.tests.fixture import Fixture
from rhodecode.tests.utils import repo_on_filesystem
fixture = Fixture()
def route_path(name, params=None, **kwargs):
import urllib
base_url = {
'repo_summary_explicit': '/{repo_name}/summary',
'repo_summary': '/{repo_name}',
'edit_repo_advanced': '/{repo_name}/settings/advanced',
'edit_repo_advanced_delete': '/{repo_name}/settings/advanced/delete',
'edit_repo_advanced_fork': '/{repo_name}/settings/advanced/fork',
'edit_repo_advanced_locking': '/{repo_name}/settings/advanced/locking',
'edit_repo_advanced_journal': '/{repo_name}/settings/advanced/journal',
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
@pytest.mark.usefixtures('autologin_user', 'app')
class TestAdminRepoSettingsAdvanced(object):
def test_set_repo_fork_has_no_self_id(self, autologin_user, backend):
repo = backend.repo
response = self.app.get(
route_path('edit_repo_advanced', repo_name=backend.repo_name))
opt = """<option value="%s">vcs_test_git</option>""" % repo.repo_id
response.mustcontain(no=[opt])
def test_set_fork_of_target_repo(
self, autologin_user, backend, csrf_token):
target_repo = 'target_%s' % backend.alias
fixture.create_repo(target_repo, repo_type=backend.alias)
repo2 = Repository.get_by_repo_name(target_repo)
response = self.app.post(
route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
params={'id_fork_of': repo2.repo_id,
'csrf_token': csrf_token})
repo = Repository.get_by_repo_name(backend.repo_name)
repo2 = Repository.get_by_repo_name(target_repo)
assert_session_flash(
response,
'Marked repo %s as fork of %s' % (repo.repo_name, repo2.repo_name))
assert repo.fork == repo2
response = response.follow()
# check if given repo is selected
opt = 'This repository is a fork of <a href="%s">%s</a>' % (
route_path('repo_summary', repo_name=repo2.repo_name),
repo2.repo_name)
response.mustcontain(opt)
fixture.destroy_repo(target_repo, forks='detach')
@pytest.mark.backends("hg", "git")
def test_set_fork_of_other_type_repo(
self, autologin_user, backend, csrf_token):
TARGET_REPO_MAP = {
'git': {
'type': 'hg',
'repo_name': HG_REPO},
'hg': {
'type': 'git',
'repo_name': GIT_REPO},
}
target_repo = TARGET_REPO_MAP[backend.alias]
repo2 = Repository.get_by_repo_name(target_repo['repo_name'])
response = self.app.post(
route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
params={'id_fork_of': repo2.repo_id,
'csrf_token': csrf_token})
assert_session_flash(
response,
'Cannot set repository as fork of repository with other type')
def test_set_fork_of_none(self, autologin_user, backend, csrf_token):
# mark it as None
response = self.app.post(
route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
params={'id_fork_of': None, '_method': 'put',
'csrf_token': csrf_token})
assert_session_flash(
response,
'Marked repo %s as fork of %s'
% (backend.repo_name, "Nothing"))
assert backend.repo.fork is None
def test_set_fork_of_same_repo(self, autologin_user, backend, csrf_token):
repo = Repository.get_by_repo_name(backend.repo_name)
response = self.app.post(
route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
params={'id_fork_of': repo.repo_id, 'csrf_token': csrf_token})
assert_session_flash(
response, 'An error occurred during this operation')
@pytest.mark.parametrize(
"suffix",
['', u'ąęł' , '123'],
ids=no_newline_id_generator)
def test_advanced_delete(self, autologin_user, backend, suffix, csrf_token):
repo = backend.create_repo(name_suffix=suffix)
repo_name = repo.repo_name
repo_name_str = safe_str(repo.repo_name)
response = self.app.post(
route_path('edit_repo_advanced_delete', repo_name=repo_name_str),
params={'csrf_token': csrf_token})
assert_session_flash(response,
u'Deleted repository `{}`'.format(repo_name))
response.follow()
# check if repo was deleted from db
assert RepoModel().get_by_repo_name(repo_name) is None
assert not repo_on_filesystem(repo_name_str)

View file

@ -23,16 +23,16 @@ import re
import mock
import pytest
from rhodecode.controllers import summary
from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
from rhodecode.lib import helpers as h
from rhodecode.lib.compat import OrderedDict
from rhodecode.lib.utils2 import AttributeDict
from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
from rhodecode.model.db import Repository
from rhodecode.model.meta import Session
from rhodecode.model.repo import RepoModel
from rhodecode.model.scm import ScmModel
from rhodecode.tests import (
TestController, url, HG_REPO, assert_session_flash)
from rhodecode.tests import assert_session_flash
from rhodecode.tests.fixture import Fixture
from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
@ -40,67 +40,31 @@ from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
fixture = Fixture()
class TestSummaryController(TestController):
def test_index(self, backend):
self.log_user()
repo_id = backend.repo.repo_id
repo_name = backend.repo_name
with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
return_value=False):
response = self.app.get(url('summary_home', repo_name=repo_name))
def route_path(name, params=None, **kwargs):
import urllib
# repo type
response.mustcontain(
'<i class="icon-%s">' % (backend.alias, )
)
# public/private
response.mustcontain(
"""<i class="icon-unlock-alt">"""
)
base_url = {
'repo_summary': '/{repo_name}',
'repo_stats': '/{repo_name}/repo_stats/{commit_id}',
'repo_refs_data': '/{repo_name}/refs-data',
'repo_refs_changelog_data': '/{repo_name}/refs-data-changelog'
# clone url...
response.mustcontain(
'id="clone_url" readonly="readonly"'
' value="http://test_admin@test.example.com:80/%s"' % (repo_name, ))
response.mustcontain(
'id="clone_url_id" readonly="readonly"'
' value="http://test_admin@test.example.com:80/_%s"' % (repo_id, ))
}[name].format(**kwargs)
def test_index_svn_without_proxy(self, backend_svn):
self.log_user()
repo_id = backend_svn.repo.repo_id
repo_name = backend_svn.repo_name
response = self.app.get(url('summary_home', repo_name=repo_name))
# clone url...
response.mustcontain(
'id="clone_url" disabled'
' value="http://test_admin@test.example.com:80/%s"' % (repo_name, ))
response.mustcontain(
'id="clone_url_id" disabled'
' value="http://test_admin@test.example.com:80/_%s"' % (repo_id, ))
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
def test_index_with_trailing_slash(self, autologin_user, backend):
@pytest.mark.usefixtures('app')
class TestSummaryView(object):
def test_index(self, autologin_user, backend, http_host_only_stub):
repo_id = backend.repo.repo_id
repo_name = backend.repo_name
with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
return_value=False):
response = self.app.get(
url('summary_home', repo_name=repo_name) + '/',
status=200)
# clone url...
response.mustcontain(
'id="clone_url" readonly="readonly"'
' value="http://test_admin@test.example.com:80/%s"' % (repo_name, ))
response.mustcontain(
'id="clone_url_id" readonly="readonly"'
' value="http://test_admin@test.example.com:80/_%s"' % (repo_id, ))
def test_index_by_id(self, backend):
self.log_user()
repo_id = backend.repo.repo_id
response = self.app.get(url(
'summary_home', repo_name='_%s' % (repo_id,)))
route_path('repo_summary', repo_name=repo_name))
# repo type
response.mustcontain(
@ -111,10 +75,63 @@ class TestSummaryController(TestController):
"""<i class="icon-unlock-alt">"""
)
def test_index_by_repo_having_id_path_in_name_hg(self):
self.log_user()
# clone url...
response.mustcontain(
'id="clone_url" readonly="readonly"'
' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
response.mustcontain(
'id="clone_url_id" readonly="readonly"'
' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
def test_index_svn_without_proxy(
self, autologin_user, backend_svn, http_host_only_stub):
repo_id = backend_svn.repo.repo_id
repo_name = backend_svn.repo_name
response = self.app.get(route_path('repo_summary', repo_name=repo_name))
# clone url...
response.mustcontain(
'id="clone_url" disabled'
' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
response.mustcontain(
'id="clone_url_id" disabled'
' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
def test_index_with_trailing_slash(
self, autologin_user, backend, http_host_only_stub):
repo_id = backend.repo.repo_id
repo_name = backend.repo_name
with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
return_value=False):
response = self.app.get(
route_path('repo_summary', repo_name=repo_name) + '/',
status=200)
# clone url...
response.mustcontain(
'id="clone_url" readonly="readonly"'
' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
response.mustcontain(
'id="clone_url_id" readonly="readonly"'
' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
def test_index_by_id(self, autologin_user, backend):
repo_id = backend.repo.repo_id
response = self.app.get(
route_path('repo_summary', repo_name='_%s' % (repo_id,)))
# repo type
response.mustcontain(
'<i class="icon-%s">' % (backend.alias, )
)
# public/private
response.mustcontain(
"""<i class="icon-unlock-alt">"""
)
def test_index_by_repo_having_id_path_in_name_hg(self, autologin_user):
fixture.create_repo(name='repo_1')
response = self.app.get(url('summary_home', repo_name='repo_1'))
response = self.app.get(route_path('repo_summary', repo_name='repo_1'))
try:
response.mustcontain("repo_1")
@ -122,11 +139,11 @@ class TestSummaryController(TestController):
RepoModel().delete(Repository.get_by_repo_name('repo_1'))
Session().commit()
def test_index_with_anonymous_access_disabled(self):
with fixture.anon_access(False):
response = self.app.get(url('summary_home', repo_name=HG_REPO),
status=302)
assert 'login' in response.location
def test_index_with_anonymous_access_disabled(
self, backend, disable_anonymous_user):
response = self.app.get(
route_path('repo_summary', repo_name=backend.repo_name), status=302)
assert 'login' in response.location
def _enable_stats(self, repo):
r = Repository.get_by_repo_name(repo)
@ -173,17 +190,15 @@ class TestSummaryController(TestController):
},
}
def test_repo_stats(self, backend, xhr_header):
self.log_user()
def test_repo_stats(self, autologin_user, backend, xhr_header):
response = self.app.get(
url('repo_stats',
repo_name=backend.repo_name, commit_id='tip'),
route_path(
'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
extra_environ=xhr_header,
status=200)
assert re.match(r'6[\d\.]+ KiB', response.json['size'])
def test_repo_stats_code_stats_enabled(self, backend, xhr_header):
self.log_user()
def test_repo_stats_code_stats_enabled(self, autologin_user, backend, xhr_header):
repo_name = backend.repo_name
# codes stats
@ -191,8 +206,8 @@ class TestSummaryController(TestController):
ScmModel().mark_for_invalidation(repo_name)
response = self.app.get(
url('repo_stats',
repo_name=backend.repo_name, commit_id='tip'),
route_path(
'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
extra_environ=xhr_header,
status=200)
@ -203,7 +218,7 @@ class TestSummaryController(TestController):
def test_repo_refs_data(self, backend):
response = self.app.get(
url('repo_refs_data', repo_name=backend.repo_name),
route_path('repo_refs_data', repo_name=backend.repo_name),
status=200)
# Ensure that there is the correct amount of items in the result
@ -220,72 +235,68 @@ class TestSummaryController(TestController):
Repository, 'scm_instance', side_effect=RepositoryRequirementError)
with scm_patcher:
response = self.app.get(url('summary_home', repo_name=repo_name))
response = self.app.get(route_path('repo_summary', repo_name=repo_name))
assert_response = AssertResponse(response)
assert_response.element_contains(
'.main .alert-warning strong', 'Missing requirements')
assert_response.element_contains(
'.main .alert-warning',
'These commits cannot be displayed, because this repository'
' uses the Mercurial largefiles extension, which was not enabled.')
'Commits cannot be displayed, because this repository '
'uses one or more extensions, which was not enabled.')
def test_missing_requirements_page_does_not_contains_switch_to(
self, backend):
self.log_user()
self, autologin_user, backend):
repo_name = backend.repo_name
scm_patcher = mock.patch.object(
Repository, 'scm_instance', side_effect=RepositoryRequirementError)
with scm_patcher:
response = self.app.get(url('summary_home', repo_name=repo_name))
response = self.app.get(route_path('repo_summary', repo_name=repo_name))
response.mustcontain(no='Switch To')
@pytest.mark.usefixtures('pylonsapp')
class TestSwitcherReferenceData:
@pytest.mark.usefixtures('app')
class TestRepoLocation(object):
def test_creates_reference_urls_based_on_name(self):
references = {
'name': 'commit_id',
}
controller = summary.SummaryController()
is_svn = False
result = controller._switcher_reference_data(
'repo_name', references, is_svn)
expected_url = h.url(
'files_home', repo_name='repo_name', revision='name',
at='name')
assert result[0]['files_url'] == expected_url
@pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii'])
def test_manual_delete(self, autologin_user, backend, suffix, csrf_token):
repo = backend.create_repo(name_suffix=suffix)
repo_name = repo.repo_name
def test_urls_contain_commit_id_if_slash_in_name(self):
references = {
'name/with/slash': 'commit_id',
}
controller = summary.SummaryController()
is_svn = False
result = controller._switcher_reference_data(
'repo_name', references, is_svn)
expected_url = h.url(
'files_home', repo_name='repo_name', revision='commit_id',
at='name/with/slash')
assert result[0]['files_url'] == expected_url
# delete from file system
RepoModel()._delete_filesystem_repo(repo)
def test_adds_reference_to_path_for_svn(self):
references = {
'name/with/slash': 'commit_id',
}
controller = summary.SummaryController()
is_svn = True
result = controller._switcher_reference_data(
'repo_name', references, is_svn)
expected_url = h.url(
'files_home', repo_name='repo_name', f_path='name/with/slash',
revision='commit_id', at='name/with/slash')
assert result[0]['files_url'] == expected_url
# test if the repo is still in the database
new_repo = RepoModel().get_by_repo_name(repo_name)
assert new_repo.repo_name == repo_name
# check if repo is not in the filesystem
assert not repo_on_filesystem(repo_name)
self.assert_repo_not_found_redirect(repo_name)
def assert_repo_not_found_redirect(self, repo_name):
# run the check page that triggers the other flash message
response = self.app.get(h.url('repo_check_home', repo_name=repo_name))
assert_session_flash(
response, 'The repository at %s cannot be located.' % repo_name)
@pytest.mark.usefixtures('pylonsapp')
class TestCreateReferenceData:
@pytest.fixture()
def summary_view(context_stub, request_stub, user_util):
"""
Bootstrap view to test the view functions
"""
request_stub.matched_route = AttributeDict(name='test_view')
request_stub.user = user_util.create_user().AuthUser
request_stub.db_repo = user_util.create_repo()
view = RepoSummaryView(context=context_stub, request=request_stub)
return view
@pytest.mark.usefixtures('app')
class TestCreateReferenceData(object):
@pytest.fixture
def example_refs(self):
@ -296,14 +307,13 @@ class TestCreateReferenceData:
]
return example_refs
def test_generates_refs_based_on_commit_ids(self, example_refs):
def test_generates_refs_based_on_commit_ids(self, example_refs, summary_view):
repo = mock.Mock()
repo.name = 'test-repo'
repo.alias = 'git'
full_repo_name = 'pytest-repo-group/' + repo.name
controller = summary.SummaryController()
result = controller._create_reference_data(
result = summary_view._create_reference_data(
repo, full_repo_name, example_refs)
expected_files_url = '/{}/files/'.format(full_repo_name)
@ -332,13 +342,13 @@ class TestCreateReferenceData:
}]
assert result == expected_result
def test_generates_refs_with_path_for_svn(self, example_refs):
def test_generates_refs_with_path_for_svn(self, example_refs, summary_view):
repo = mock.Mock()
repo.name = 'test-repo'
repo.alias = 'svn'
full_repo_name = 'pytest-repo-group/' + repo.name
controller = summary.SummaryController()
result = controller._create_reference_data(
result = summary_view._create_reference_data(
repo, full_repo_name, example_refs)
expected_files_url = '/{}/files/'.format(full_repo_name)
@ -372,35 +382,9 @@ class TestCreateReferenceData:
assert result == expected_result
@pytest.mark.usefixtures("app")
class TestRepoLocation:
@pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii'])
def test_manual_delete(self, autologin_user, backend, suffix, csrf_token):
repo = backend.create_repo(name_suffix=suffix)
repo_name = repo.repo_name
# delete from file system
RepoModel()._delete_filesystem_repo(repo)
# test if the repo is still in the database
new_repo = RepoModel().get_by_repo_name(repo_name)
assert new_repo.repo_name == repo_name
# check if repo is not in the filesystem
assert not repo_on_filesystem(repo_name)
self.assert_repo_not_found_redirect(repo_name)
def assert_repo_not_found_redirect(self, repo_name):
# run the check page that triggers the other flash message
response = self.app.get(url('repo_check_home', repo_name=repo_name))
assert_session_flash(
response, 'The repository at %s cannot be located.' % repo_name)
class TestCreateFilesUrl(object):
def test_creates_non_svn_url(self):
controller = summary.SummaryController()
def test_creates_non_svn_url(self, summary_view):
repo = mock.Mock()
repo.name = 'abcde'
full_repo_name = 'test-repo-group/' + repo.name
@ -408,16 +392,15 @@ class TestCreateFilesUrl(object):
raw_id = 'deadbeef0123456789'
is_svn = False
with mock.patch.object(summary.h, 'url') as url_mock:
result = controller._create_files_url(
with mock.patch('rhodecode.lib.helpers.url') as url_mock:
result = summary_view._create_files_url(
repo, full_repo_name, ref_name, raw_id, is_svn)
url_mock.assert_called_once_with(
'files_home', repo_name=full_repo_name, f_path='',
revision=ref_name, at=ref_name)
assert result == url_mock.return_value
def test_creates_svn_url(self):
controller = summary.SummaryController()
def test_creates_svn_url(self, summary_view):
repo = mock.Mock()
repo.name = 'abcde'
full_repo_name = 'test-repo-group/' + repo.name
@ -425,16 +408,15 @@ class TestCreateFilesUrl(object):
raw_id = 'deadbeef0123456789'
is_svn = True
with mock.patch.object(summary.h, 'url') as url_mock:
result = controller._create_files_url(
with mock.patch('rhodecode.lib.helpers.url') as url_mock:
result = summary_view._create_files_url(
repo, full_repo_name, ref_name, raw_id, is_svn)
url_mock.assert_called_once_with(
'files_home', repo_name=full_repo_name, f_path=ref_name,
revision=raw_id, at=ref_name)
assert result == url_mock.return_value
def test_name_has_slashes(self):
controller = summary.SummaryController()
def test_name_has_slashes(self, summary_view):
repo = mock.Mock()
repo.name = 'abcde'
full_repo_name = 'test-repo-group/' + repo.name
@ -442,8 +424,8 @@ class TestCreateFilesUrl(object):
raw_id = 'deadbeef0123456789'
is_svn = False
with mock.patch.object(summary.h, 'url') as url_mock:
result = controller._create_files_url(
with mock.patch('rhodecode.lib.helpers.url') as url_mock:
result = summary_view._create_files_url(
repo, full_repo_name, ref_name, raw_id, is_svn)
url_mock.assert_called_once_with(
'files_home', repo_name=full_repo_name, f_path='', revision=raw_id,
@ -462,42 +444,39 @@ class TestReferenceItems(object):
def _format_function(name, id_):
return 'format_function_{}_{}'.format(name, id_)
def test_creates_required_amount_of_items(self):
def test_creates_required_amount_of_items(self, summary_view):
amount = 100
refs = {
'ref{}'.format(i): '{0:040d}'.format(i)
for i in range(amount)
}
controller = summary.SummaryController()
url_patcher = mock.patch.object(
controller, '_create_files_url')
svn_patcher = mock.patch.object(
summary.h, 'is_svn', return_value=False)
url_patcher = mock.patch.object(summary_view, '_create_files_url')
svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
return_value=False)
with url_patcher as url_mock, svn_patcher:
result = controller._create_reference_items(
result = summary_view._create_reference_items(
self.repo, self.repo_full_name, refs, self.ref_type,
self._format_function)
assert len(result) == amount
assert url_mock.call_count == amount
def test_single_item_details(self):
def test_single_item_details(self, summary_view):
ref_name = 'ref1'
ref_id = 'deadbeef'
refs = {
ref_name: ref_id
}
controller = summary.SummaryController()
svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
return_value=False)
url_patcher = mock.patch.object(
controller, '_create_files_url', return_value=self.fake_url)
svn_patcher = mock.patch.object(
summary.h, 'is_svn', return_value=False)
summary_view, '_create_files_url', return_value=self.fake_url)
with url_patcher as url_mock, svn_patcher:
result = controller._create_reference_items(
result = summary_view._create_reference_items(
self.repo, self.repo_full_name, refs, self.ref_type,
self._format_function)

View file

@ -18,16 +18,27 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import pytest
from rhodecode.model.db import Repository
from rhodecode.tests import *
class TestTagsController(TestController):
def route_path(name, params=None, **kwargs):
import urllib
base_url = {
'tags_home': '/{repo_name}/tags',
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
@pytest.mark.usefixtures('autologin_user', 'app')
class TestTagsController(object):
def test_index(self, backend):
self.log_user()
response = self.app.get(url(controller='tags',
action='index',
repo_name=backend.repo_name))
response = self.app.get(
route_path('tags_home', repo_name=backend.repo_name))
repo = Repository.get_by_repo_name(backend.repo_name)

View file

@ -23,25 +23,24 @@ import pytest
import rhodecode
from rhodecode.model.settings import SettingsModel
from rhodecode.tests import url, HG_REPO
from rhodecode.tests import url
from rhodecode.tests.utils import AssertResponse
def route_path(name, params=None, **kwargs):
import urllib
base_url = {
'edit_repo': '/{repo_name}/settings',
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
@pytest.mark.usefixtures('autologin_user', 'app')
class TestAdminRepoSettingsController:
@pytest.mark.parametrize('urlname', [
'edit_repo',
'edit_repo_perms',
'edit_repo_advanced',
'repo_vcs_settings',
'edit_repo_fields',
'repo_settings_issuetracker',
'edit_repo_caches',
'edit_repo_remote',
'edit_repo_statistics',
])
def test_simple_get(self, urlname, app):
app.get(url(urlname, repo_name=HG_REPO))
class TestAdminRepoVcsSettings(object):
@pytest.mark.parametrize('setting_name, setting_backends', [
('hg_use_rebase_for_merging', ['hg']),

View file

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
from rhodecode.lib import helpers as h
from rhodecode.lib.utils2 import safe_int
def reviewer_as_json(user, reasons=None, mandatory=False):
"""
Returns json struct of a reviewer for frontend
:param user: the reviewer
:param reasons: list of strings of why they are reviewers
:param mandatory: bool, to set user as mandatory
"""
return {
'user_id': user.user_id,
'reasons': reasons or [],
'mandatory': mandatory,
'username': user.username,
'first_name': user.first_name,
'last_name': user.last_name,
'gravatar_link': h.gravatar_url(user.email, 14),
}
def get_default_reviewers_data(
current_user, source_repo, source_commit, target_repo, target_commit):
""" Return json for default reviewers of a repository """
reasons = ['Default reviewer', 'Repository owner']
default = reviewer_as_json(
user=current_user, reasons=reasons, mandatory=False)
return {
'api_ver': 'v1', # define version for later possible schema upgrade
'reviewers': [default],
'rules': {},
'rules_data': {},
}
def validate_default_reviewers(review_members, reviewer_rules):
"""
Function to validate submitted reviewers against the saved rules
"""
reviewers = []
reviewer_by_id = {}
for r in review_members:
reviewer_user_id = safe_int(r['user_id'])
entry = (reviewer_user_id, r['reasons'], r['mandatory'])
reviewer_by_id[reviewer_user_id] = entry
reviewers.append(entry)
return reviewers

View file

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2011-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
from pyramid.httpexceptions import HTTPNotFound
from pyramid.view import view_config
from rhodecode.apps._base import BaseReferencesView
from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
from rhodecode.lib import helpers as h
log = logging.getLogger(__name__)
class RepoBookmarksView(BaseReferencesView):
@LoginRequired()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@view_config(
route_name='bookmarks_home', request_method='GET',
renderer='rhodecode:templates/bookmarks/bookmarks.mako')
def bookmarks(self):
c = self.load_default_context()
if not h.is_hg(self.db_repo):
raise HTTPNotFound()
ref_items = self.rhodecode_vcs_repo.bookmarks.items()
self.load_refs_context(
ref_items=ref_items, partials_template='bookmarks/bookmarks_data.mako')
return self._get_template_context(c)

View file

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2011-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
from pyramid.view import view_config
from rhodecode.apps._base import BaseReferencesView
from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
log = logging.getLogger(__name__)
class RepoBranchesView(BaseReferencesView):
@LoginRequired()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@view_config(
route_name='branches_home', request_method='GET',
renderer='rhodecode:templates/branches/branches.mako')
def branches(self):
c = self.load_default_context()
c.closed_branches = self.rhodecode_vcs_repo.branches_closed
# NOTE(marcink):
# we need this trick because of PartialRenderer still uses the
# global 'c', we might not need this after full pylons migration
self._register_global_c(c)
ref_items = self.rhodecode_vcs_repo.branches_all.items()
self.load_refs_context(
ref_items=ref_items, partials_template='branches/branches_data.mako')
return self._get_template_context(c)

View file

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2011-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
from rhodecode.apps._base import RepoAppView
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
CSRFRequired
from rhodecode.lib import helpers as h
from rhodecode.model.meta import Session
from rhodecode.model.scm import ScmModel
log = logging.getLogger(__name__)
class RepoCachesView(RepoAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
# TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
c.repo_info = self.db_repo
self._register_global_c(c)
return c
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.admin')
@view_config(
route_name='edit_repo_caches', request_method='GET',
renderer='rhodecode:templates/admin/repos/repo_edit.mako')
def repo_caches(self):
c = self.load_default_context()
c.active = 'caches'
return self._get_template_context(c)
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.admin')
@CSRFRequired()
@view_config(
route_name='edit_repo_caches', request_method='POST')
def repo_caches_purge(self):
_ = self.request.translate
c = self.load_default_context()
c.active = 'caches'
try:
ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
Session().commit()
h.flash(_('Cache invalidation successful'),
category='success')
except Exception:
log.exception("Exception during cache invalidation")
h.flash(_('An error occurred during cache invalidation'),
category='error')
raise HTTPFound(h.route_path(
'edit_repo_caches', repo_name=self.db_repo_name))

View file

@ -41,7 +41,6 @@ class RepoMaintenanceView(RepoAppView):
return c
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator('repository.admin')
@view_config(
route_name='repo_maintenance', request_method='GET',
@ -54,7 +53,6 @@ class RepoMaintenanceView(RepoAppView):
return self._get_template_context(c)
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator('repository.admin')
@view_config(
route_name='repo_maintenance_execute', request_method='GET',

View file

@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2011-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
import deform
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
from rhodecode.apps._base import RepoAppView
from rhodecode.forms import RcForm
from rhodecode.lib import helpers as h
from rhodecode.lib import audit_logger
from rhodecode.lib.auth import (
LoginRequired, HasRepoPermissionAnyDecorator,
HasRepoPermissionAllDecorator, CSRFRequired)
from rhodecode.model.db import RepositoryField, RepoGroup
from rhodecode.model.forms import RepoPermsForm
from rhodecode.model.meta import Session
from rhodecode.model.repo import RepoModel
from rhodecode.model.scm import RepoGroupList, ScmModel
from rhodecode.model.validation_schema.schemas import repo_schema
log = logging.getLogger(__name__)
class RepoSettingsPermissionsView(RepoAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
# TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
c.repo_info = self.db_repo
self._register_global_c(c)
return c
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.admin')
@view_config(
route_name='edit_repo_perms', request_method='GET',
renderer='rhodecode:templates/admin/repos/repo_edit.mako')
def edit_permissions(self):
c = self.load_default_context()
c.active = 'permissions'
return self._get_template_context(c)
@LoginRequired()
@HasRepoPermissionAllDecorator('repository.admin')
@CSRFRequired()
@view_config(
route_name='edit_repo_perms', request_method='POST',
renderer='rhodecode:templates/admin/repos/repo_edit.mako')
def edit_permissions_update(self):
_ = self.request.translate
c = self.load_default_context()
c.active = 'permissions'
data = self.request.POST
# store private flag outside of HTML to verify if we can modify
# default user permissions, prevents submition of FAKE post data
# into the form for private repos
data['repo_private'] = self.db_repo.private
form = RepoPermsForm()().to_python(data)
changes = RepoModel().update_permissions(
self.db_repo_name, form['perm_additions'], form['perm_updates'],
form['perm_deletions'])
action_data = {
'added': changes['added'],
'updated': changes['updated'],
'deleted': changes['deleted'],
}
audit_logger.store_web(
'repo.edit.permissions', action_data=action_data,
user=self._rhodecode_user, repo=self.db_repo)
Session().commit()
h.flash(_('Repository permissions updated'), category='success')
raise HTTPFound(
self.request.route_path('edit_repo_perms', repo_name=self.db_repo_name))

View file

@ -0,0 +1,584 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2011-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
import collections
from pyramid.httpexceptions import HTTPFound, HTTPNotFound
from pyramid.view import view_config
from rhodecode.apps._base import RepoAppView, DataGridAppView
from rhodecode.lib import helpers as h, diffs, codeblocks
from rhodecode.lib.auth import (
LoginRequired, HasRepoPermissionAnyDecorator)
from rhodecode.lib.utils import PartialRenderer
from rhodecode.lib.utils2 import str2bool, safe_int, safe_str
from rhodecode.lib.vcs.backends.base import EmptyCommit
from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, \
RepositoryRequirementError, NodeDoesNotExistError
from rhodecode.model.comment import CommentsModel
from rhodecode.model.db import PullRequest, PullRequestVersion, \
ChangesetComment, ChangesetStatus
from rhodecode.model.pull_request import PullRequestModel, MergeCheck
log = logging.getLogger(__name__)
class RepoPullRequestsView(RepoAppView, DataGridAppView):
def load_default_context(self):
c = self._get_local_tmpl_context(include_app_defaults=True)
# TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
c.repo_info = self.db_repo
c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
self._register_global_c(c)
return c
def _get_pull_requests_list(
self, repo_name, source, filter_type, opened_by, statuses):
draw, start, limit = self._extract_chunk(self.request)
search_q, order_by, order_dir = self._extract_ordering(self.request)
_render = PartialRenderer('data_table/_dt_elements.mako')
# pagination
if filter_type == 'awaiting_review':
pull_requests = PullRequestModel().get_awaiting_review(
repo_name, source=source, opened_by=opened_by,
statuses=statuses, offset=start, length=limit,
order_by=order_by, order_dir=order_dir)
pull_requests_total_count = PullRequestModel().count_awaiting_review(
repo_name, source=source, statuses=statuses,
opened_by=opened_by)
elif filter_type == 'awaiting_my_review':
pull_requests = PullRequestModel().get_awaiting_my_review(
repo_name, source=source, opened_by=opened_by,
user_id=self._rhodecode_user.user_id, statuses=statuses,
offset=start, length=limit, order_by=order_by,
order_dir=order_dir)
pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
repo_name, source=source, user_id=self._rhodecode_user.user_id,
statuses=statuses, opened_by=opened_by)
else:
pull_requests = PullRequestModel().get_all(
repo_name, source=source, opened_by=opened_by,
statuses=statuses, offset=start, length=limit,
order_by=order_by, order_dir=order_dir)
pull_requests_total_count = PullRequestModel().count_all(
repo_name, source=source, statuses=statuses,
opened_by=opened_by)
data = []
comments_model = CommentsModel()
for pr in pull_requests:
comments = comments_model.get_all_comments(
self.db_repo.repo_id, pull_request=pr)
data.append({
'name': _render('pullrequest_name',
pr.pull_request_id, pr.target_repo.repo_name),
'name_raw': pr.pull_request_id,
'status': _render('pullrequest_status',
pr.calculated_review_status()),
'title': _render(
'pullrequest_title', pr.title, pr.description),
'description': h.escape(pr.description),
'updated_on': _render('pullrequest_updated_on',
h.datetime_to_time(pr.updated_on)),
'updated_on_raw': h.datetime_to_time(pr.updated_on),
'created_on': _render('pullrequest_updated_on',
h.datetime_to_time(pr.created_on)),
'created_on_raw': h.datetime_to_time(pr.created_on),
'author': _render('pullrequest_author',
pr.author.full_contact, ),
'author_raw': pr.author.full_name,
'comments': _render('pullrequest_comments', len(comments)),
'comments_raw': len(comments),
'closed': pr.is_closed(),
})
data = ({
'draw': draw,
'data': data,
'recordsTotal': pull_requests_total_count,
'recordsFiltered': pull_requests_total_count,
})
return data
@LoginRequired()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@view_config(
route_name='pullrequest_show_all', request_method='GET',
renderer='rhodecode:templates/pullrequests/pullrequests.mako')
def pull_request_list(self):
c = self.load_default_context()
req_get = self.request.GET
c.source = str2bool(req_get.get('source'))
c.closed = str2bool(req_get.get('closed'))
c.my = str2bool(req_get.get('my'))
c.awaiting_review = str2bool(req_get.get('awaiting_review'))
c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
c.active = 'open'
if c.my:
c.active = 'my'
if c.closed:
c.active = 'closed'
if c.awaiting_review and not c.source:
c.active = 'awaiting'
if c.source and not c.awaiting_review:
c.active = 'source'
if c.awaiting_my_review:
c.active = 'awaiting_my'
return self._get_template_context(c)
@LoginRequired()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@view_config(
route_name='pullrequest_show_all_data', request_method='GET',
renderer='json_ext', xhr=True)
def pull_request_list_data(self):
# additional filters
req_get = self.request.GET
source = str2bool(req_get.get('source'))
closed = str2bool(req_get.get('closed'))
my = str2bool(req_get.get('my'))
awaiting_review = str2bool(req_get.get('awaiting_review'))
awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
filter_type = 'awaiting_review' if awaiting_review \
else 'awaiting_my_review' if awaiting_my_review \
else None
opened_by = None
if my:
opened_by = [self._rhodecode_user.user_id]
statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
if closed:
statuses = [PullRequest.STATUS_CLOSED]
data = self._get_pull_requests_list(
repo_name=self.db_repo_name, source=source,
filter_type=filter_type, opened_by=opened_by, statuses=statuses)
return data
def _get_pr_version(self, pull_request_id, version=None):
pull_request_id = safe_int(pull_request_id)
at_version = None
if version and version == 'latest':
pull_request_ver = PullRequest.get(pull_request_id)
pull_request_obj = pull_request_ver
_org_pull_request_obj = pull_request_obj
at_version = 'latest'
elif version:
pull_request_ver = PullRequestVersion.get_or_404(version)
pull_request_obj = pull_request_ver
_org_pull_request_obj = pull_request_ver.pull_request
at_version = pull_request_ver.pull_request_version_id
else:
_org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
pull_request_id)
pull_request_display_obj = PullRequest.get_pr_display_object(
pull_request_obj, _org_pull_request_obj)
return _org_pull_request_obj, pull_request_obj, \
pull_request_display_obj, at_version
def _get_diffset(self, source_repo_name, source_repo,
source_ref_id, target_ref_id,
target_commit, source_commit, diff_limit, fulldiff,
file_limit, display_inline_comments):
vcs_diff = PullRequestModel().get_diff(
source_repo, source_ref_id, target_ref_id)
diff_processor = diffs.DiffProcessor(
vcs_diff, format='newdiff', diff_limit=diff_limit,
file_limit=file_limit, show_full_diff=fulldiff)
_parsed = diff_processor.prepare()
def _node_getter(commit):
def get_node(fname):
try:
return commit.get_node(fname)
except NodeDoesNotExistError:
return None
return get_node
diffset = codeblocks.DiffSet(
repo_name=self.db_repo_name,
source_repo_name=source_repo_name,
source_node_getter=_node_getter(target_commit),
target_node_getter=_node_getter(source_commit),
comments=display_inline_comments
)
diffset = diffset.render_patchset(
_parsed, target_commit.raw_id, source_commit.raw_id)
return diffset
@LoginRequired()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
# @view_config(
# route_name='pullrequest_show', request_method='GET',
# renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
def pull_request_show(self):
pull_request_id = safe_int(
self.request.matchdict.get('pull_request_id'))
c = self.load_default_context()
version = self.request.GET.get('version')
from_version = self.request.GET.get('from_version') or version
merge_checks = self.request.GET.get('merge_checks')
c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
(pull_request_latest,
pull_request_at_ver,
pull_request_display_obj,
at_version) = self._get_pr_version(
pull_request_id, version=version)
pr_closed = pull_request_latest.is_closed()
if pr_closed and (version or from_version):
# not allow to browse versions
raise HTTPFound(h.route_path(
'pullrequest_show', repo_name=self.db_repo_name,
pull_request_id=pull_request_id))
versions = pull_request_display_obj.versions()
c.at_version = at_version
c.at_version_num = (at_version
if at_version and at_version != 'latest'
else None)
c.at_version_pos = ChangesetComment.get_index_from_version(
c.at_version_num, versions)
(prev_pull_request_latest,
prev_pull_request_at_ver,
prev_pull_request_display_obj,
prev_at_version) = self._get_pr_version(
pull_request_id, version=from_version)
c.from_version = prev_at_version
c.from_version_num = (prev_at_version
if prev_at_version and prev_at_version != 'latest'
else None)
c.from_version_pos = ChangesetComment.get_index_from_version(
c.from_version_num, versions)
# define if we're in COMPARE mode or VIEW at version mode
compare = at_version != prev_at_version
# pull_requests repo_name we opened it against
# ie. target_repo must match
if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
raise HTTPNotFound()
c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
pull_request_at_ver)
c.pull_request = pull_request_display_obj
c.pull_request_latest = pull_request_latest
if compare or (at_version and not at_version == 'latest'):
c.allowed_to_change_status = False
c.allowed_to_update = False
c.allowed_to_merge = False
c.allowed_to_delete = False
c.allowed_to_comment = False
c.allowed_to_close = False
else:
can_change_status = PullRequestModel().check_user_change_status(
pull_request_at_ver, self._rhodecode_user)
c.allowed_to_change_status = can_change_status and not pr_closed
c.allowed_to_update = PullRequestModel().check_user_update(
pull_request_latest, self._rhodecode_user) and not pr_closed
c.allowed_to_merge = PullRequestModel().check_user_merge(
pull_request_latest, self._rhodecode_user) and not pr_closed
c.allowed_to_delete = PullRequestModel().check_user_delete(
pull_request_latest, self._rhodecode_user) and not pr_closed
c.allowed_to_comment = not pr_closed
c.allowed_to_close = c.allowed_to_merge and not pr_closed
c.forbid_adding_reviewers = False
c.forbid_author_to_review = False
c.forbid_commit_author_to_review = False
if pull_request_latest.reviewer_data and \
'rules' in pull_request_latest.reviewer_data:
rules = pull_request_latest.reviewer_data['rules'] or {}
try:
c.forbid_adding_reviewers = rules.get(
'forbid_adding_reviewers')
c.forbid_author_to_review = rules.get(
'forbid_author_to_review')
c.forbid_commit_author_to_review = rules.get(
'forbid_commit_author_to_review')
except Exception:
pass
# check merge capabilities
_merge_check = MergeCheck.validate(
pull_request_latest, user=self._rhodecode_user)
c.pr_merge_errors = _merge_check.error_details
c.pr_merge_possible = not _merge_check.failed
c.pr_merge_message = _merge_check.merge_msg
c.pull_request_review_status = _merge_check.review_status
if merge_checks:
self.request.override_renderer = \
'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
return self._get_template_context(c)
comments_model = CommentsModel()
# reviewers and statuses
c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
# GENERAL COMMENTS with versions #
q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
q = q.order_by(ChangesetComment.comment_id.asc())
general_comments = q
# pick comments we want to render at current version
c.comment_versions = comments_model.aggregate_comments(
general_comments, versions, c.at_version_num)
c.comments = c.comment_versions[c.at_version_num]['until']
# INLINE COMMENTS with versions #
q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
q = q.order_by(ChangesetComment.comment_id.asc())
inline_comments = q
c.inline_versions = comments_model.aggregate_comments(
inline_comments, versions, c.at_version_num, inline=True)
# inject latest version
latest_ver = PullRequest.get_pr_display_object(
pull_request_latest, pull_request_latest)
c.versions = versions + [latest_ver]
# if we use version, then do not show later comments
# than current version
display_inline_comments = collections.defaultdict(
lambda: collections.defaultdict(list))
for co in inline_comments:
if c.at_version_num:
# pick comments that are at least UPTO given version, so we
# don't render comments for higher version
should_render = co.pull_request_version_id and \
co.pull_request_version_id <= c.at_version_num
else:
# showing all, for 'latest'
should_render = True
if should_render:
display_inline_comments[co.f_path][co.line_no].append(co)
# load diff data into template context, if we use compare mode then
# diff is calculated based on changes between versions of PR
source_repo = pull_request_at_ver.source_repo
source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
target_repo = pull_request_at_ver.target_repo
target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
if compare:
# in compare switch the diff base to latest commit from prev version
target_ref_id = prev_pull_request_display_obj.revisions[0]
# despite opening commits for bookmarks/branches/tags, we always
# convert this to rev to prevent changes after bookmark or branch change
c.source_ref_type = 'rev'
c.source_ref = source_ref_id
c.target_ref_type = 'rev'
c.target_ref = target_ref_id
c.source_repo = source_repo
c.target_repo = target_repo
c.commit_ranges = []
source_commit = EmptyCommit()
target_commit = EmptyCommit()
c.missing_requirements = False
source_scm = source_repo.scm_instance()
target_scm = target_repo.scm_instance()
# try first shadow repo, fallback to regular repo
try:
commits_source_repo = pull_request_latest.get_shadow_repo()
except Exception:
log.debug('Failed to get shadow repo', exc_info=True)
commits_source_repo = source_scm
c.commits_source_repo = commits_source_repo
commit_cache = {}
try:
pre_load = ["author", "branch", "date", "message"]
show_revs = pull_request_at_ver.revisions
for rev in show_revs:
comm = commits_source_repo.get_commit(
commit_id=rev, pre_load=pre_load)
c.commit_ranges.append(comm)
commit_cache[comm.raw_id] = comm
# Order here matters, we first need to get target, and then
# the source
target_commit = commits_source_repo.get_commit(
commit_id=safe_str(target_ref_id))
source_commit = commits_source_repo.get_commit(
commit_id=safe_str(source_ref_id))
except CommitDoesNotExistError:
log.warning(
'Failed to get commit from `{}` repo'.format(
commits_source_repo), exc_info=True)
except RepositoryRequirementError:
log.warning(
'Failed to get all required data from repo', exc_info=True)
c.missing_requirements = True
c.ancestor = None # set it to None, to hide it from PR view
try:
ancestor_id = source_scm.get_common_ancestor(
source_commit.raw_id, target_commit.raw_id, target_scm)
c.ancestor_commit = source_scm.get_commit(ancestor_id)
except Exception:
c.ancestor_commit = None
c.statuses = source_repo.statuses(
[x.raw_id for x in c.commit_ranges])
# auto collapse if we have more than limit
collapse_limit = diffs.DiffProcessor._collapse_commits_over
c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
c.compare_mode = compare
# diff_limit is the old behavior, will cut off the whole diff
# if the limit is applied otherwise will just hide the
# big files from the front-end
diff_limit = c.visual.cut_off_limit_diff
file_limit = c.visual.cut_off_limit_file
c.missing_commits = False
if (c.missing_requirements
or isinstance(source_commit, EmptyCommit)
or source_commit == target_commit):
c.missing_commits = True
else:
c.diffset = self._get_diffset(
c.source_repo.repo_name, commits_source_repo,
source_ref_id, target_ref_id,
target_commit, source_commit,
diff_limit, c.fulldiff, file_limit, display_inline_comments)
c.limited_diff = c.diffset.limited_diff
# calculate removed files that are bound to comments
comment_deleted_files = [
fname for fname in display_inline_comments
if fname not in c.diffset.file_stats]
c.deleted_files_comments = collections.defaultdict(dict)
for fname, per_line_comments in display_inline_comments.items():
if fname in comment_deleted_files:
c.deleted_files_comments[fname]['stats'] = 0
c.deleted_files_comments[fname]['comments'] = list()
for lno, comments in per_line_comments.items():
c.deleted_files_comments[fname]['comments'].extend(
comments)
# this is a hack to properly display links, when creating PR, the
# compare view and others uses different notation, and
# compare_commits.mako renders links based on the target_repo.
# We need to swap that here to generate it properly on the html side
c.target_repo = c.source_repo
c.commit_statuses = ChangesetStatus.STATUSES
c.show_version_changes = not pr_closed
if c.show_version_changes:
cur_obj = pull_request_at_ver
prev_obj = prev_pull_request_at_ver
old_commit_ids = prev_obj.revisions
new_commit_ids = cur_obj.revisions
commit_changes = PullRequestModel()._calculate_commit_id_changes(
old_commit_ids, new_commit_ids)
c.commit_changes_summary = commit_changes
# calculate the diff for commits between versions
c.commit_changes = []
mark = lambda cs, fw: list(
h.itertools.izip_longest([], cs, fillvalue=fw))
for c_type, raw_id in mark(commit_changes.added, 'a') \
+ mark(commit_changes.removed, 'r') \
+ mark(commit_changes.common, 'c'):
if raw_id in commit_cache:
commit = commit_cache[raw_id]
else:
try:
commit = commits_source_repo.get_commit(raw_id)
except CommitDoesNotExistError:
# in case we fail extracting still use "dummy" commit
# for display in commit diff
commit = h.AttributeDict(
{'raw_id': raw_id,
'message': 'EMPTY or MISSING COMMIT'})
c.commit_changes.append([c_type, commit])
# current user review statuses for each version
c.review_versions = {}
if self._rhodecode_user.user_id in allowed_reviewers:
for co in general_comments:
if co.author.user_id == self._rhodecode_user.user_id:
# each comment has a status change
status = co.status_change
if status:
_ver_pr = status[0].comment.pull_request_version_id
c.review_versions[_ver_pr] = status[0]
return self._get_template_context(c)

View file

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
from pyramid.view import view_config
from rhodecode.apps._base import RepoAppView
from rhodecode.apps.repository.utils import get_default_reviewers_data
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
log = logging.getLogger(__name__)
class RepoReviewRulesView(RepoAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
# TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
c.repo_info = self.db_repo
self._register_global_c(c)
return c
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.admin')
@view_config(
route_name='repo_reviewers', request_method='GET',
renderer='rhodecode:templates/admin/repos/repo_edit.mako')
def repo_review_rules(self):
c = self.load_default_context()
c.active = 'reviewers'
return self._get_template_context(c)
@LoginRequired()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@view_config(
route_name='repo_default_reviewers_data', request_method='GET',
renderer='json_ext')
def repo_default_reviewers_data(self):
review_data = get_default_reviewers_data(
self.db_repo.user, None, None, None, None)
return review_data

View file

@ -0,0 +1,179 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2011-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
import deform
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
from rhodecode.apps._base import RepoAppView
from rhodecode.forms import RcForm
from rhodecode.lib import helpers as h
from rhodecode.lib import audit_logger
from rhodecode.lib.auth import (
LoginRequired, HasRepoPermissionAnyDecorator,
HasRepoPermissionAllDecorator, CSRFRequired)
from rhodecode.model.db import RepositoryField, RepoGroup
from rhodecode.model.meta import Session
from rhodecode.model.repo import RepoModel
from rhodecode.model.scm import RepoGroupList, ScmModel
from rhodecode.model.validation_schema.schemas import repo_schema
log = logging.getLogger(__name__)
class RepoSettingsView(RepoAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
# TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
c.repo_info = self.db_repo
acl_groups = RepoGroupList(
RepoGroup.query().all(),
perm_set=['group.write', 'group.admin'])
c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
c.repo_groups_choices = map(lambda k: k[0], c.repo_groups)
# in case someone no longer have a group.write access to a repository
# pre fill the list with this entry, we don't care if this is the same
# but it will allow saving repo data properly.
repo_group = self.db_repo.group
if repo_group and repo_group.group_id not in c.repo_groups_choices:
c.repo_groups_choices.append(repo_group.group_id)
c.repo_groups.append(RepoGroup._generate_choice(repo_group))
if c.repository_requirements_missing or self.rhodecode_vcs_repo is None:
# we might be in missing requirement state, so we load things
# without touching scm_instance()
c.landing_revs_choices, c.landing_revs = \
ScmModel().get_repo_landing_revs()
else:
c.landing_revs_choices, c.landing_revs = \
ScmModel().get_repo_landing_revs(self.db_repo)
c.personal_repo_group = c.auth_user.personal_repo_group
c.repo_fields = RepositoryField.query()\
.filter(RepositoryField.repository == self.db_repo).all()
self._register_global_c(c)
return c
def _get_schema(self, c, old_values=None):
return repo_schema.RepoSettingsSchema().bind(
repo_type=self.db_repo.repo_type,
repo_type_options=[self.db_repo.repo_type],
repo_ref_options=c.landing_revs_choices,
repo_ref_items=c.landing_revs,
repo_repo_group_options=c.repo_groups_choices,
repo_repo_group_items=c.repo_groups,
# user caller
user=self._rhodecode_user,
old_values=old_values
)
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.admin')
@view_config(
route_name='edit_repo', request_method='GET',
renderer='rhodecode:templates/admin/repos/repo_edit.mako')
def edit_settings(self):
c = self.load_default_context()
c.active = 'settings'
defaults = RepoModel()._get_defaults(self.db_repo_name)
defaults['repo_owner'] = defaults['user']
defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev']
schema = self._get_schema(c)
c.form = RcForm(schema, appstruct=defaults)
return self._get_template_context(c)
@LoginRequired()
@HasRepoPermissionAllDecorator('repository.admin')
@CSRFRequired()
@view_config(
route_name='edit_repo', request_method='POST',
renderer='rhodecode:templates/admin/repos/repo_edit.mako')
def edit_settings_update(self):
_ = self.request.translate
c = self.load_default_context()
c.active = 'settings'
old_repo_name = self.db_repo_name
old_values = self.db_repo.get_api_data()
schema = self._get_schema(c, old_values=old_values)
c.form = RcForm(schema)
pstruct = self.request.POST.items()
pstruct.append(('repo_type', self.db_repo.repo_type))
try:
schema_data = c.form.validate(pstruct)
except deform.ValidationFailure as err_form:
return self._get_template_context(c)
# data is now VALID, proceed with updates
# save validated data back into the updates dict
validated_updates = dict(
repo_name=schema_data['repo_group']['repo_name_without_group'],
repo_group=schema_data['repo_group']['repo_group_id'],
user=schema_data['repo_owner'],
repo_description=schema_data['repo_description'],
repo_private=schema_data['repo_private'],
clone_uri=schema_data['repo_clone_uri'],
repo_landing_rev=schema_data['repo_landing_commit_ref'],
repo_enable_statistics=schema_data['repo_enable_statistics'],
repo_enable_locking=schema_data['repo_enable_locking'],
repo_enable_downloads=schema_data['repo_enable_downloads'],
)
# detect if CLONE URI changed, if we get OLD means we keep old values
if schema_data['repo_clone_uri_change'] == 'OLD':
validated_updates['clone_uri'] = self.db_repo.clone_uri
# use the new full name for redirect
new_repo_name = schema_data['repo_group']['repo_name_with_group']
# save extra fields into our validated data
for key, value in pstruct:
if key.startswith(RepositoryField.PREFIX):
validated_updates[key] = value
try:
RepoModel().update(self.db_repo, **validated_updates)
ScmModel().mark_for_invalidation(new_repo_name)
audit_logger.store_web(
'repo.edit', action_data={'old_data': old_values},
user=self._rhodecode_user, repo=self.db_repo)
Session().commit()
h.flash(_('Repository {} updated successfully').format(
old_repo_name), category='success')
except Exception:
log.exception("Exception during update of repository")
h.flash(_('Error occurred during update of repository {}').format(
old_repo_name), category='error')
raise HTTPFound(
self.request.route_path('edit_repo', repo_name=new_repo_name))

View file

@ -0,0 +1,226 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2011-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPFound
from rhodecode.apps._base import RepoAppView
from rhodecode.lib import helpers as h
from rhodecode.lib import audit_logger
from rhodecode.lib.auth import (
LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
from rhodecode.lib.exceptions import AttachedForksError
from rhodecode.lib.utils2 import safe_int
from rhodecode.lib.vcs import RepositoryError
from rhodecode.model.db import Session, UserFollowing, User, Repository
from rhodecode.model.repo import RepoModel
from rhodecode.model.scm import ScmModel
log = logging.getLogger(__name__)
class RepoSettingsView(RepoAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
# TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
c.repo_info = self.db_repo
self._register_global_c(c)
return c
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.admin')
@view_config(
route_name='edit_repo_advanced', request_method='GET',
renderer='rhodecode:templates/admin/repos/repo_edit.mako')
def edit_advanced(self):
c = self.load_default_context()
c.active = 'advanced'
c.default_user_id = User.get_default_user().user_id
c.in_public_journal = UserFollowing.query() \
.filter(UserFollowing.user_id == c.default_user_id) \
.filter(UserFollowing.follows_repository == c.repo_info).scalar()
c.has_origin_repo_read_perm = False
if self.db_repo.fork:
c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
'repository.write', 'repository.read', 'repository.admin')(
self.db_repo.fork.repo_name, 'repo set as fork page')
return self._get_template_context(c)
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.admin')
@CSRFRequired()
@view_config(
route_name='edit_repo_advanced_delete', request_method='POST',
renderer='rhodecode:templates/admin/repos/repo_edit.mako')
def edit_advanced_delete(self):
"""
Deletes the repository, or shows warnings if deletion is not possible
because of attached forks or other errors.
"""
_ = self.request.translate
handle_forks = self.request.POST.get('forks', None)
try:
_forks = self.db_repo.forks.count()
if _forks and handle_forks:
if handle_forks == 'detach_forks':
handle_forks = 'detach'
h.flash(_('Detached %s forks') % _forks, category='success')
elif handle_forks == 'delete_forks':
handle_forks = 'delete'
h.flash(_('Deleted %s forks') % _forks, category='success')
old_data = self.db_repo.get_api_data()
RepoModel().delete(self.db_repo, forks=handle_forks)
repo = audit_logger.RepoWrap(repo_id=None,
repo_name=self.db_repo.repo_name)
audit_logger.store_web(
'repo.delete', action_data={'old_data': old_data},
user=self._rhodecode_user, repo=repo)
ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
h.flash(
_('Deleted repository `%s`') % self.db_repo_name,
category='success')
Session().commit()
except AttachedForksError:
repo_advanced_url = h.route_path(
'edit_repo_advanced', repo_name=self.db_repo_name,
_anchor='advanced-delete')
delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url)
h.flash(_('Cannot delete `{repo}` it still contains attached forks. '
'Try using {delete_or_detach} option.')
.format(repo=self.db_repo_name, delete_or_detach=delete_anchor),
category='warning')
# redirect to advanced for forks handle action ?
raise HTTPFound(repo_advanced_url)
except Exception:
log.exception("Exception during deletion of repository")
h.flash(_('An error occurred during deletion of `%s`')
% self.db_repo_name, category='error')
# redirect to advanced for more deletion options
raise HTTPFound(
h.route_path('edit_repo_advanced', repo_name=self.db_repo_name),
_anchor='advanced-delete')
raise HTTPFound(h.route_path('home'))
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.admin')
@CSRFRequired()
@view_config(
route_name='edit_repo_advanced_journal', request_method='POST',
renderer='rhodecode:templates/admin/repos/repo_edit.mako')
def edit_advanced_journal(self):
"""
Set's this repository to be visible in public journal,
in other words making default user to follow this repo
"""
_ = self.request.translate
try:
user_id = User.get_default_user().user_id
ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id)
h.flash(_('Updated repository visibility in public journal'),
category='success')
Session().commit()
except Exception:
h.flash(_('An error occurred during setting this '
'repository in public journal'),
category='error')
raise HTTPFound(
h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.admin')
@CSRFRequired()
@view_config(
route_name='edit_repo_advanced_fork', request_method='POST',
renderer='rhodecode:templates/admin/repos/repo_edit.mako')
def edit_advanced_fork(self):
"""
Mark given repository as a fork of another
"""
_ = self.request.translate
new_fork_id = self.request.POST.get('id_fork_of')
try:
if new_fork_id and not new_fork_id.isdigit():
log.error('Given fork id %s is not an INT', new_fork_id)
fork_id = safe_int(new_fork_id)
repo = ScmModel().mark_as_fork(
self.db_repo_name, fork_id, self._rhodecode_user.user_id)
fork = repo.fork.repo_name if repo.fork else _('Nothing')
Session().commit()
h.flash(_('Marked repo %s as fork of %s') % (self.db_repo_name, fork),
category='success')
except RepositoryError as e:
log.exception("Repository Error occurred")
h.flash(str(e), category='error')
except Exception as e:
log.exception("Exception while editing fork")
h.flash(_('An error occurred during this operation'),
category='error')
raise HTTPFound(
h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.admin')
@CSRFRequired()
@view_config(
route_name='edit_repo_advanced_locking', request_method='POST',
renderer='rhodecode:templates/admin/repos/repo_edit.mako')
def edit_advanced_locking(self):
"""
Toggle locking of repository
"""
_ = self.request.translate
set_lock = self.request.POST.get('set_lock')
set_unlock = self.request.POST.get('set_unlock')
try:
if set_lock:
Repository.lock(self.db_repo, self._rhodecode_user.user_id,
lock_reason=Repository.LOCK_WEB)
h.flash(_('Locked repository'), category='success')
elif set_unlock:
Repository.unlock(self.db_repo)
h.flash(_('Unlocked repository'), category='success')
except Exception as e:
log.exception("Exception during unlocking")
h.flash(_('An error occurred during unlocking'), category='error')
raise HTTPFound(
h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2011-2017 RhodeCode GmbH
# Copyright (C) 2017-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
@ -22,9 +22,11 @@ import logging
from pyramid.view import view_config
from rhodecode.apps._base import RepoAppView
from rhodecode.lib import audit_logger
from rhodecode.lib import helpers as h
from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator,
NotAnonymous)
NotAnonymous, CSRFRequired)
from rhodecode.lib.ext_json import json
log = logging.getLogger(__name__)
@ -40,7 +42,6 @@ class StripView(RepoAppView):
return c
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator('repository.admin')
@view_config(
route_name='strip', request_method='GET',
@ -53,41 +54,39 @@ class StripView(RepoAppView):
return self._get_template_context(c)
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator('repository.admin')
@CSRFRequired()
@view_config(
route_name='strip_check', request_method='POST',
renderer='json', xhr=True
)
renderer='json', xhr=True)
def strip_check(self):
from rhodecode.lib.vcs.backends.base import EmptyCommit
data = {}
rp = self.request.POST
for i in range(1, 11):
chset = 'changeset_id-%d'%(i,)
chset = 'changeset_id-%d' % (i,)
check = rp.get(chset)
if check:
data[i] = self.db_repo.get_changeset(rp[chset])
if isinstance(data[i], EmptyCommit):
data[i] = {'rev': None, 'commit': rp[chset]}
data[i] = {'rev': None, 'commit': h.escape(rp[chset])}
else:
data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch, 'author': data[i].author,
data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch,
'author': data[i].author,
'comment': data[i].message}
else:
break
return data
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator('repository.admin')
@CSRFRequired()
@view_config(
route_name='strip_execute', request_method='POST',
renderer='json', xhr=True
)
renderer='json', xhr=True)
def strip_execute(self):
from rhodecode.model.scm import ScmModel
from rhodecode.lib.ext_json import json
c = self.load_default_context()
user = self._rhodecode_user
@ -95,16 +94,23 @@ class StripView(RepoAppView):
data = {}
for idx in rp:
commit = json.loads(rp[idx])
#If someone put two times the same branch
# If someone put two times the same branch
if commit['branch'] in data.keys():
continue
try:
ScmModel().strip(repo=c.repo_info,
commit_id=commit['rev'], branch=commit['branch'])
log.info('Stripped commit %s from repo `%s` by %s' % (commit['rev'], c.repo_info.repo_name, user))
ScmModel().strip(
repo=c.repo_info,
commit_id=commit['rev'], branch=commit['branch'])
log.info('Stripped commit %s from repo `%s` by %s' % (
commit['rev'], c.repo_info.repo_name, user))
data[commit['rev']] = True
except Exception, e:
audit_logger.store_web(
'repo.commit.strip', action_data={'commit_id': commit['rev']},
repo=self.db_repo, user=self._rhodecode_user, commit=True)
except Exception as e:
data[commit['rev']] = False
log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s' % (commit['rev'],
c.repo_info.repo_name, user, e.message))
log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s' % (
commit['rev'], self.db_repo_name, user, e.message))
return data

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2017 RhodeCode GmbH
# Copyright (C) 2011-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
@ -18,47 +18,51 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
"""
Summary controller for RhodeCode Enterprise
"""
import logging
from string import lower
import string
from pyramid.view import view_config
from beaker.cache import cache_region
from pylons import tmpl_context as c, request
from pylons.i18n.translation import _
from beaker.cache import cache_region, region_invalidate
from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
from rhodecode.controllers import utils
from rhodecode.controllers.changelog import _load_changelog_summary
from rhodecode.apps._base import RepoAppView
from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
from rhodecode.lib import caches, helpers as h
from rhodecode.lib.utils import jsonify
from rhodecode.lib.utils2 import safe_str
from rhodecode.lib.auth import (
LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, XHRRequired)
from rhodecode.lib.base import BaseRepoController, render
from rhodecode.lib.helpers import RepoPage
from rhodecode.lib.utils2 import safe_str, safe_int
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
from rhodecode.lib.ext_json import json
from rhodecode.lib.vcs.backends.base import EmptyCommit
from rhodecode.lib.vcs.exceptions import (
CommitError, EmptyRepositoryError, NodeDoesNotExistError)
from rhodecode.lib.vcs.exceptions import CommitError, EmptyRepositoryError
from rhodecode.model.db import Statistics, CacheKey, User
from rhodecode.model.meta import Session
from rhodecode.model.repo import ReadmeFinder
from rhodecode.model.scm import ScmModel
log = logging.getLogger(__name__)
class SummaryController(BaseRepoController):
class RepoSummaryView(RepoAppView):
def __before__(self):
super(SummaryController, self).__before__()
def load_default_context(self):
c = self._get_local_tmpl_context(include_app_defaults=True)
def __get_readme_data(self, db_repo):
# TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
c.repo_info = self.db_repo
c.rhodecode_repo = None
if not c.repository_requirements_missing:
c.rhodecode_repo = self.rhodecode_vcs_repo
self._register_global_c(c)
return c
def _get_readme_data(self, db_repo, default_renderer):
repo_name = db_repo.repo_name
log.debug('Looking for README file')
default_renderer = c.visual.default_renderer
@cache_region('long_term')
def _generate_readme(cache_key):
@ -114,16 +118,58 @@ class SummaryController(BaseRepoController):
log.exception(
"Exception while trying to render the README")
def _load_commits_context(self, c):
p = safe_int(self.request.GET.get('page'), 1)
size = safe_int(self.request.GET.get('size'), 10)
def url_generator(**kw):
query_params = {
'size': size
}
query_params.update(kw)
return h.route_path(
'repo_summary_commits',
repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
pre_load = ['author', 'branch', 'date', 'message']
try:
collection = self.rhodecode_vcs_repo.get_commits(pre_load=pre_load)
except EmptyRepositoryError:
collection = self.rhodecode_vcs_repo
c.repo_commits = RepoPage(
collection, page=p, items_per_page=size, url=url_generator)
page_ids = [x.raw_id for x in c.repo_commits]
c.comments = self.db_repo.get_comments(page_ids)
c.statuses = self.db_repo.statuses(page_ids)
@LoginRequired()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
def index(self, repo_name):
@view_config(
route_name='repo_summary_commits', request_method='GET',
renderer='rhodecode:templates/summary/summary_commits.mako')
def summary_commits(self):
c = self.load_default_context()
self._load_commits_context(c)
return self._get_template_context(c)
@LoginRequired()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@view_config(
route_name='repo_summary', request_method='GET',
renderer='rhodecode:templates/summary/summary.mako')
@view_config(
route_name='repo_summary_slash', request_method='GET',
renderer='rhodecode:templates/summary/summary.mako')
def summary(self):
c = self.load_default_context()
# Prepare the clone URL
username = ''
if c.rhodecode_user.username != User.DEFAULT_USER:
username = safe_str(c.rhodecode_user.username)
if self._rhodecode_user.username != User.DEFAULT_USER:
username = safe_str(self._rhodecode_user.username)
_def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl
if '{repo}' in _def_clone_uri:
@ -133,23 +179,23 @@ class SummaryController(BaseRepoController):
_def_clone_uri_by_id = _def_clone_uri.replace(
'_{repoid}', '{repo}')
c.clone_repo_url = c.rhodecode_db_repo.clone_url(
c.clone_repo_url = self.db_repo.clone_url(
user=username, uri_tmpl=_def_clone_uri)
c.clone_repo_url_id = c.rhodecode_db_repo.clone_url(
c.clone_repo_url_id = self.db_repo.clone_url(
user=username, uri_tmpl=_def_clone_uri_by_id)
# If enabled, get statistics data
c.show_stats = bool(c.rhodecode_db_repo.enable_statistics)
c.show_stats = bool(self.db_repo.enable_statistics)
stats = self.sa.query(Statistics)\
.filter(Statistics.repository == c.rhodecode_db_repo)\
stats = Session().query(Statistics) \
.filter(Statistics.repository == self.db_repo) \
.scalar()
c.stats_percentage = 0
if stats and stats.languages:
c.no_data = False is c.rhodecode_db_repo.enable_statistics
c.no_data = False is self.db_repo.enable_statistics
lang_stats_d = json.loads(stats.languages)
# Sort first by decreasing count and second by the file extension,
@ -165,51 +211,59 @@ class SummaryController(BaseRepoController):
c.no_data = True
c.trending_languages = json.dumps({})
c.enable_downloads = c.rhodecode_db_repo.enable_downloads
c.repository_followers = self.scm_model.get_followers(
c.rhodecode_db_repo)
c.repository_forks = self.scm_model.get_forks(c.rhodecode_db_repo)
c.repository_is_user_following = self.scm_model.is_following_repo(
c.repo_name, c.rhodecode_user.user_id)
scm_model = ScmModel()
c.enable_downloads = self.db_repo.enable_downloads
c.repository_followers = scm_model.get_followers(self.db_repo)
c.repository_forks = scm_model.get_forks(self.db_repo)
c.repository_is_user_following = scm_model.is_following_repo(
self.db_repo_name, self._rhodecode_user.user_id)
# first interaction with the VCS instance after here...
if c.repository_requirements_missing:
return render('summary/missing_requirements.mako')
self.request.override_renderer = \
'rhodecode:templates/summary/missing_requirements.mako'
return self._get_template_context(c)
c.readme_data, c.readme_file = \
self.__get_readme_data(c.rhodecode_db_repo)
self._get_readme_data(self.db_repo, c.visual.default_renderer)
_load_changelog_summary()
# loads the summary commits template context
self._load_commits_context(c)
if request.is_xhr:
return render('changelog/changelog_summary_data.mako')
return self._get_template_context(c)
return render('summary/summary.mako')
def get_request_commit_id(self):
return self.request.matchdict['commit_id']
@LoginRequired()
@XHRRequired()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@jsonify
def repo_stats(self, repo_name, commit_id):
@view_config(
route_name='repo_stats', request_method='GET',
renderer='json_ext')
def repo_stats(self):
commit_id = self.get_request_commit_id()
_namespace = caches.get_repo_namespace_key(
caches.SUMMARY_STATS, repo_name)
show_stats = bool(c.rhodecode_db_repo.enable_statistics)
cache_manager = caches.get_cache_manager('repo_cache_long', _namespace)
caches.SUMMARY_STATS, self.db_repo_name)
show_stats = bool(self.db_repo.enable_statistics)
cache_manager = caches.get_cache_manager(
'repo_cache_long', _namespace)
_cache_key = caches.compute_key_from_params(
repo_name, commit_id, show_stats)
self.db_repo_name, commit_id, show_stats)
def compute_stats():
code_stats = {}
size = 0
try:
scm_instance = c.rhodecode_db_repo.scm_instance()
scm_instance = self.db_repo.scm_instance()
commit = scm_instance.get_commit(commit_id)
for node in commit.get_filenodes_generator():
size += node.size
if not show_stats:
continue
ext = lower(node.extension)
ext = string.lower(node.extension)
ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
if ext_info:
if ext in code_stats:
@ -224,35 +278,24 @@ class SummaryController(BaseRepoController):
stats = cache_manager.get(_cache_key, createfunc=compute_stats)
return stats
def _switcher_reference_data(self, repo_name, references, is_svn):
"""Prepare reference data for given `references`"""
items = []
for name, commit_id in references.items():
use_commit_id = '/' in name or is_svn
items.append({
'name': name,
'commit_id': commit_id,
'files_url': h.url(
'files_home',
repo_name=repo_name,
f_path=name if is_svn else '',
revision=commit_id if use_commit_id else name,
at=name)
})
return items
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
@jsonify
def repo_refs_data(self, repo_name):
repo = c.rhodecode_repo
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@view_config(
route_name='repo_refs_data', request_method='GET',
renderer='json_ext')
def repo_refs_data(self):
_ = self.request.translate
self.load_default_context()
repo = self.rhodecode_vcs_repo
refs_to_create = [
(_("Branch"), repo.branches, 'branch'),
(_("Tag"), repo.tags, 'tag'),
(_("Bookmark"), repo.bookmarks, 'book'),
]
res = self._create_reference_data(repo, repo_name, refs_to_create)
res = self._create_reference_data(
repo, self.db_repo_name, refs_to_create)
data = {
'more': False,
'results': res
@ -260,18 +303,16 @@ class SummaryController(BaseRepoController):
return data
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
@jsonify
def repo_default_reviewers_data(self, repo_name):
return {
'reviewers': [utils.reviewer_as_json(
user=c.rhodecode_db_repo.user, reasons=None)]
}
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@view_config(
route_name='repo_refs_changelog_data', request_method='GET',
renderer='json_ext')
def repo_refs_changelog_data(self):
_ = self.request.translate
self.load_default_context()
@jsonify
def repo_refs_changelog_data(self, repo_name):
repo = c.rhodecode_repo
repo = self.rhodecode_vcs_repo
refs_to_create = [
(_("Branches"), repo.branches, 'branch'),
@ -279,7 +320,8 @@ class SummaryController(BaseRepoController):
# TODO: enable when vcs can handle bookmarks filters
# (_("Bookmarks"), repo.bookmarks, "book"),
]
res = self._create_reference_data(repo, repo_name, refs_to_create)
res = self._create_reference_data(
repo, self.db_repo_name, refs_to_create)
data = {
'more': False,
'results': res
@ -295,7 +337,8 @@ class SummaryController(BaseRepoController):
result.append({
'text': title,
'children': self._create_reference_items(
repo, full_repo_name, refs, ref_type, format_ref_id),
repo, full_repo_name, refs, ref_type,
format_ref_id),
})
return result
@ -315,8 +358,7 @@ class SummaryController(BaseRepoController):
})
return result
def _create_files_url(self, repo, full_repo_name, ref_name, raw_id,
is_svn):
def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
use_commit_id = '/' in ref_name or is_svn
return h.url(
'files_home',

View file

@ -17,30 +17,29 @@
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
"""
Bookmarks controller for rhodecode
"""
import logging
from pyramid.view import view_config
from pylons import tmpl_context as c
from webob.exc import HTTPNotFound
from rhodecode.controllers.base_references import BaseReferencesController
from rhodecode.lib import helpers as h
from rhodecode.apps._base import BaseReferencesView
from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
log = logging.getLogger(__name__)
class BookmarksController(BaseReferencesController):
class RepoTagsView(BaseReferencesView):
partials_template = 'bookmarks/bookmarks_data.mako'
template = 'bookmarks/bookmarks.mako'
@LoginRequired()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@view_config(
route_name='tags_home', request_method='GET',
renderer='rhodecode:templates/tags/tags.mako')
def tags(self):
c = self.load_default_context()
def __before__(self):
super(BookmarksController, self).__before__()
if not h.is_hg(c.rhodecode_repo):
raise HTTPNotFound()
ref_items = self.rhodecode_vcs_repo.tags.items()
self.load_refs_context(
ref_items=ref_items, partials_template='tags/tags_data.mako')
def _get_reference_items(self, repo):
return repo.bookmarks.items()
return self._get_template_context(c)

View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
from rhodecode.apps._base import ADMIN_PREFIX
def includeme(config):
config.add_route(
name='search',
pattern=ADMIN_PREFIX + '/search')
config.add_route(
name='search_repo',
pattern='/{repo_name:.*?[^/]}/search', repo_route=True)
# Scan module for configuration decorators.
config.scan()
# # FULL TEXT SEARCH
# rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
# controller='search')
# rmap.connect('search_repo_home', '/{repo_name}/search',
# controller='search',
# action='index',
# conditions={'function': check_repo},
# requirements=URL_NAME_REQUIREMENTS)

View file

View file

@ -25,16 +25,27 @@ import pytest
from whoosh import query
from rhodecode.tests import (
TestController, url, SkipTest, HG_REPO,
TestController, SkipTest, HG_REPO,
TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
from rhodecode.tests.utils import AssertResponse
def route_path(name, **kwargs):
from rhodecode.apps._base import ADMIN_PREFIX
return {
'search':
ADMIN_PREFIX + '/search',
'search_repo':
'/{repo_name}/search',
}[name].format(**kwargs)
class TestSearchController(TestController):
def test_index(self):
self.log_user()
response = self.app.get(url(controller='search', action='index'))
response = self.app.get(route_path('search'))
assert_response = AssertResponse(response)
assert_response.one_element_exists('input#q')
@ -43,17 +54,15 @@ class TestSearchController(TestController):
raise SkipTest('skipped due to existing index')
else:
self.log_user()
response = self.app.get(url(controller='search', action='index'),
response = self.app.get(route_path('search'),
{'q': HG_REPO})
response.mustcontain('There is no index to search in. '
'Please run whoosh indexer')
def test_search_validation(self):
self.log_user()
response = self.app.get(
url(controller='search', action='index'), {'q': query,
'type': 'content',
'page_limit': 1000})
response = self.app.get(route_path('search'),
{'q': query, 'type': 'content', 'page_limit': 1000})
response.mustcontain(
'page_limit - 1000 is greater than maximum value 500')
@ -88,10 +97,8 @@ class TestSearchController(TestController):
])
def test_search_files(self, query, expected_hits, expected_paths):
self.log_user()
response = self.app.get(
url(controller='search', action='index'), {'q': query,
'type': 'content',
'page_limit': 500})
response = self.app.get(route_path('search'),
{'q': query, 'type': 'content', 'page_limit': 500})
response.mustcontain('%s results' % expected_hits)
for path in expected_paths:
@ -137,10 +144,8 @@ class TestSearchController(TestController):
def test_search_commit_messages(
self, query, expected_hits, expected_commits, enabled_backends):
self.log_user()
response = self.app.get(
url(controller='search', action='index'), {'q': query,
'type': 'commit',
'page_limit': 500})
response = self.app.get(route_path('search'),
{'q': query, 'type': 'commit', 'page_limit': 500})
response.mustcontain('%s results' % expected_hits)
for backend, commit_id in expected_commits:
@ -156,10 +161,8 @@ class TestSearchController(TestController):
])
def test_search_file_paths(self, query, expected_hits, expected_paths):
self.log_user()
response = self.app.get(
url(controller='search', action='index'), {'q': query,
'type': 'path',
'page_limit': 500})
response = self.app.get(route_path('search'),
{'q': query, 'type': 'path', 'page_limit': 500})
response.mustcontain('%s results' % expected_hits)
for path in expected_paths:
@ -168,8 +171,7 @@ class TestSearchController(TestController):
def test_search_commit_message_specific_repo(self, backend):
self.log_user()
response = self.app.get(
url(controller='search', action='index',
repo_name=backend.repo_name),
route_path('search_repo',repo_name=backend.repo_name),
{'q': 'bother to ask where to fetch repo during tests',
'type': 'commit'})
@ -178,8 +180,7 @@ class TestSearchController(TestController):
def test_filters_are_not_applied_for_admin_user(self):
self.log_user()
with mock.patch('whoosh.searching.Searcher.search') as search_mock:
self.app.get(
url(controller='search', action='index'),
self.app.get(route_path('search'),
{'q': 'test query', 'type': 'commit'})
assert search_mock.call_count == 1
_, kwargs = search_mock.call_args
@ -188,8 +189,7 @@ class TestSearchController(TestController):
def test_filters_are_applied_for_normal_user(self, enabled_backends):
self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
with mock.patch('whoosh.searching.Searcher.search') as search_mock:
self.app.get(
url(controller='search', action='index'),
self.app.get(route_path('search'),
{'q': 'test query', 'type': 'commit'})
assert search_mock.call_count == 1
_, kwargs = search_mock.call_args

View file

@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2011-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
import urllib
from pyramid.view import view_config
from webhelpers.util import update_params
from rhodecode.apps._base import BaseAppView, RepoAppView
from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
from rhodecode.lib.helpers import Page
from rhodecode.lib.utils2 import safe_str, safe_int
from rhodecode.lib.index import searcher_from_config
from rhodecode.model import validation_schema
from rhodecode.model.validation_schema.schemas import search_schema
log = logging.getLogger(__name__)
def search(request, tmpl_context, repo_name):
searcher = searcher_from_config(request.registry.settings)
formatted_results = []
execution_time = ''
schema = search_schema.SearchParamsSchema()
search_params = {}
errors = []
try:
search_params = schema.deserialize(
dict(search_query=request.GET.get('q'),
search_type=request.GET.get('type'),
search_sort=request.GET.get('sort'),
page_limit=request.GET.get('page_limit'),
requested_page=request.GET.get('page'))
)
except validation_schema.Invalid as e:
errors = e.children
def url_generator(**kw):
q = urllib.quote(safe_str(search_query))
return update_params(
"?q=%s&type=%s" % (q, safe_str(search_type)), **kw)
c = tmpl_context
search_query = search_params.get('search_query')
search_type = search_params.get('search_type')
search_sort = search_params.get('search_sort')
if search_params.get('search_query'):
page_limit = search_params['page_limit']
requested_page = search_params['requested_page']
try:
search_result = searcher.search(
search_query, search_type, c.auth_user, repo_name,
requested_page, page_limit, search_sort)
formatted_results = Page(
search_result['results'], page=requested_page,
item_count=search_result['count'],
items_per_page=page_limit, url=url_generator)
finally:
searcher.cleanup()
if not search_result['error']:
execution_time = '%s results (%.3f seconds)' % (
search_result['count'],
search_result['runtime'])
elif not errors:
node = schema['search_query']
errors = [
validation_schema.Invalid(node, search_result['error'])]
c.perm_user = c.auth_user
c.repo_name = repo_name
c.sort = search_sort
c.url_generator = url_generator
c.errors = errors
c.formatted_results = formatted_results
c.runtime = execution_time
c.cur_query = search_query
c.search_type = search_type
c.searcher = searcher
class SearchView(BaseAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
self._register_global_c(c)
return c
@LoginRequired()
@view_config(
route_name='search', request_method='GET',
renderer='rhodecode:templates/search/search.mako')
def search(self):
c = self.load_default_context()
search(self.request, c, repo_name=None)
return self._get_template_context(c)
class SearchRepoView(RepoAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
self._register_global_c(c)
return c
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.admin')
@view_config(
route_name='search_repo', request_method='GET',
renderer='rhodecode:templates/search/search.mako')
def search_repo(self):
c = self.load_default_context()
search(self.request, c, repo_name=self.db_repo_name)
return self._get_template_context(c)

View file

@ -564,8 +564,6 @@ def authenticate(username, password, environ=None, auth_type=None,
for plugin in authn_registry.get_plugins_for_authentication():
plugin.set_auth_type(auth_type)
plugin.set_calling_scope_repo(acl_repo_name)
user = plugin.get_user(username)
display_user = user.username if user else username
if headers_only and not plugin.is_headers_auth:
log.debug('Auth type is for headers only and plugin `%s` is not '

Some files were not shown because too many files have changed in this diff Show more