release: Merge default into stable for release preparation
This commit is contained in:
commit
66eefae578
374 changed files with 13012 additions and 6596 deletions
|
|
@ -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]
|
||||
|
|
|
|||
13
.release.cfg
13
.release.cfg
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
19
default.nix
19
default.nix
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 ##
|
||||
################
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
----
|
||||
|
||||
|
|
|
|||
|
|
@ -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::
|
||||
|
||||
|
|
|
|||
|
|
@ -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
97
docs/integrations/ci.rst
Normal 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/
|
||||
|
|
@ -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
|
||||
============================ ============ =====================================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
103
docs/release-notes/release-notes-4.8.0.rst
Normal file
103
docs/release-notes/release-notes-4.8.0.rst
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
4.7.2
|
||||
4.8.0
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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' %
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
82
rhodecode/apps/admin/tests/test_admin_main_views.py
Normal file
82
rhodecode/apps/admin/tests/test_admin_main_views.py
Normal 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
|
||||
|
|
@ -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'])
|
||||
|
|
@ -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)
|
||||
63
rhodecode/apps/admin/views/main_views.py
Normal file
63
rhodecode/apps/admin/views/main_views.py
Normal 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))
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
49
rhodecode/apps/home/__init__.py
Normal file
49
rhodecode/apps/home/__init__.py
Normal 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()
|
||||
40
rhodecode/apps/home/tests/__init__.py
Normal file
40
rhodecode/apps/home/tests/__init__.py
Normal 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
|
||||
151
rhodecode/apps/home/tests/test_get_goto_switched_data.py
Normal file
151
rhodecode/apps/home/tests/test_get_goto_switched_data.py
Normal 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
|
||||
103
rhodecode/apps/home/tests/test_get_repo_list_data.py
Normal file
103
rhodecode/apps/home/tests/test_get_repo_list_data.py
Normal 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
|
||||
112
rhodecode/apps/home/tests/test_get_user_data.py
Normal file
112
rhodecode/apps/home/tests/test_get_user_data.py
Normal 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
|
||||
117
rhodecode/apps/home/tests/test_get_user_group_data.py
Normal file
117
rhodecode/apps/home/tests/test_get_user_group_data.py
Normal 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
|
||||
134
rhodecode/apps/home/tests/test_home.py
Normal file
134
rhodecode/apps/home/tests/test_home.py
Normal 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])
|
||||
304
rhodecode/apps/home/views.py
Normal file
304
rhodecode/apps/home/views.py
Normal 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)
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
93
rhodecode/apps/my_account/tests/test_my_account_emails.py
Normal file
93
rhodecode/apps/my_account/tests/test_my_account_emails.py
Normal 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')
|
||||
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
|
|
@ -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']
|
||||
|
|
@ -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()
|
||||
54
rhodecode/apps/ops/views.py
Normal file
54
rhodecode/apps/ops/views.py
Normal 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}
|
||||
|
||||
|
||||
|
||||
33
rhodecode/apps/repo_group/__init__.py
Normal file
33
rhodecode/apps/repo_group/__init__.py
Normal 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()
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
0
rhodecode/apps/repository/tests/__init__.py
Normal file
0
rhodecode/apps/repository/tests/__init__.py
Normal file
83
rhodecode/apps/repository/tests/test_pull_requests_list.py
Normal file
83
rhodecode/apps/repository/tests/test_pull_requests_list.py
Normal 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'] == \
|
||||
"<script>alert('Hi!')</script>"
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
||||
233
rhodecode/apps/repository/tests/test_repo_settings.py
Normal file
233
rhodecode/apps/repository/tests/test_repo_settings.py
Normal 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()
|
||||
150
rhodecode/apps/repository/tests/test_repo_settings_advanced.py
Normal file
150
rhodecode/apps/repository/tests/test_repo_settings_advanced.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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']),
|
||||
76
rhodecode/apps/repository/utils.py
Normal file
76
rhodecode/apps/repository/utils.py
Normal 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
|
||||
50
rhodecode/apps/repository/views/repo_bookmarks.py
Normal file
50
rhodecode/apps/repository/views/repo_bookmarks.py
Normal 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)
|
||||
51
rhodecode/apps/repository/views/repo_branches.py
Normal file
51
rhodecode/apps/repository/views/repo_branches.py
Normal 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)
|
||||
78
rhodecode/apps/repository/views/repo_caches.py
Normal file
78
rhodecode/apps/repository/views/repo_caches.py
Normal 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))
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
98
rhodecode/apps/repository/views/repo_permissions.py
Normal file
98
rhodecode/apps/repository/views/repo_permissions.py
Normal 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))
|
||||
584
rhodecode/apps/repository/views/repo_pull_requests.py
Normal file
584
rhodecode/apps/repository/views/repo_pull_requests.py
Normal 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)
|
||||
64
rhodecode/apps/repository/views/repo_review_rules.py
Normal file
64
rhodecode/apps/repository/views/repo_review_rules.py
Normal 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
|
||||
|
||||
|
||||
179
rhodecode/apps/repository/views/repo_settings.py
Normal file
179
rhodecode/apps/repository/views/repo_settings.py
Normal 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))
|
||||
226
rhodecode/apps/repository/views/repo_settings_advanced.py
Normal file
226
rhodecode/apps/repository/views/repo_settings_advanced.py
Normal 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))
|
||||
|
|
@ -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
|
||||
|
|
@ -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',
|
||||
|
|
@ -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)
|
||||
44
rhodecode/apps/search/__init__.py
Normal file
44
rhodecode/apps/search/__init__.py
Normal 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)
|
||||
0
rhodecode/apps/search/tests/__init__.py
Normal file
0
rhodecode/apps/search/tests/__init__.py
Normal 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
|
||||
133
rhodecode/apps/search/views.py
Normal file
133
rhodecode/apps/search/views.py
Normal 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)
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue