Compare commits

...

118 Commits

Author SHA1 Message Date
Francesco Mangiacrapa f2c13b162e reverted original commit 2023-09-29 16:21:44 +02:00
Francesco Mangiacrapa 74aee9bd48 removed base.BaseController from
AcquiredDatasetsControllerUI(base.BaseController)
2023-09-27 16:54:08 +02:00
Francesco Mangiacrapa db7e30259e Updated 2023-09-27 16:33:11 +02:00
Francisco de la Vega 9f3929344a
Update software version (#57) 2020-03-16 11:11:32 +01:00
Francisco de la Vega da521ebad8
Fix validation issues (#53)
* Remove username format validator
* Avoid issues with default HTTP ports in resource validation
2020-03-13 12:01:34 +01:00
SSladarov 5341906767 Allow to access datasets description page when private (#51) 2019-05-30 15:38:30 +02:00
Francisco de la Vega bdb1e3ff57
Merge pull request #50 from fdelavega/task/test-env
Test with CKAN 2.8.1 and CKAN 2.8.2
2019-05-28 09:45:53 +02:00
Francisco de la Vega a6c9541bbb Test with CKAN 2.8.1 and CKAN 2.8.2 2019-04-08 17:02:40 +02:00
Álvaro Arranz 520da2652c
Bump plugin version 2018-07-17 17:06:02 +02:00
Francisco de la Vega cc776c68d9
Merge pull request #48 from aarranz/feature/env-vars
Add support for configuring the plugin using environment variables
2018-07-17 16:44:14 +02:00
Álvaro Arranz 2b8ef43dee
Test evironment configuration 2018-07-17 14:01:47 +02:00
Álvaro Arranz d98a5e4831
Add support for configuring the plugin using environment variables 2018-07-16 12:53:46 +02:00
Álvaro Arranz 1ab4484f66
Merge pull request #47 from aarranz/feature/support-ckan-2.8
Add support for CKAN 2.8
2018-07-16 11:39:06 +02:00
Álvaro Arranz 6edcb98629
Improve code coverage 2018-07-16 09:51:33 +02:00
Álvaro Arranz a6eb52bb9d
Unittest for views 2018-07-16 00:40:35 +02:00
Álvaro Arranz 2430c0d46a
Fixes 2018-07-15 22:02:45 +02:00
Álvaro Arranz c8bba3c15f
Upgrade selenium 2018-07-15 18:59:00 +02:00
Álvaro Arranz 56401296d3
CI: Fix some problems selecting tags and users using selenium 2018-07-15 14:59:34 +02:00
Álvaro Arranz 9d86939989
Restore IRoutes for CKAN 2.7 2018-07-15 02:54:37 +02:00
Álvaro Arranz 730f5c7b6e
CKAN 2.8 requires 8 characters passwords 2018-07-14 23:41:49 +02:00
Álvaro Arranz f1341686c0
Migrate from IRoute to IBlueprint 2018-07-14 23:34:04 +02:00
Álvaro Arranz c43c549539
Allow normal users to create datasets and organizations when passing unittests 2018-07-14 12:21:51 +02:00
Álvaro Arranz 5e2920f31c
Merge pull request #45 from aarranz/fix/selenium-race-conditions
Fix/selenium race conditions
2018-07-13 20:50:55 +02:00
Álvaro Arranz fd5c692652
Fix selenium race conditions 2018-07-13 20:21:42 +02:00
Álvaro Arranz bfb9d6d62a
Merge pull request #46 from aarranz/fix/invalid-integer-in-tests
Fix Invalid Integer error messages when passing unit tests
2018-07-13 20:08:34 +02:00
Álvaro Arranz bafdd6a707
Fix Invalid Integer error messages when passing unit tests 2018-07-13 19:42:43 +02:00
Álvaro Arranz 1598723d4c
Migrate from nose_parameterized to parameterized 2018-07-13 10:55:29 +02:00
Álvaro Arranz 04144863fa
Merge pull request #43 from aarranz/fix/basic-pep8-fixes
Fix some basic pep8 problems
2018-07-13 10:44:58 +02:00
Álvaro Arranz ce4ec44f1b
Fix some basic pep8 problems 2018-07-13 00:18:19 +02:00
Álvaro Arranz 1030939052
Use config from ckan.plugins.toolkit on tests 2018-07-12 09:33:17 +02:00
Álvaro Arranz 68dc704d6b
Use config from ckan.plugins.toolkit 2018-07-12 01:34:22 +02:00
Álvaro Arranz 73e4049c15
CI: Disable CKAN 2.6 2018-07-12 01:25:19 +02:00
Álvaro Arranz b0e30ae0f9
Merge pull request #42 from aarranz/feature/improved-ci
CI: General improvements
2018-07-12 01:04:18 +02:00
Álvaro Arranz 8d0a086159
CI: General improvements 2018-07-12 01:01:46 +02:00
Álvaro Arranz d1b8eb2662
Merge branch 'develop' 2018-07-12 00:11:55 +02:00
Álvaro Arranz c5f0efe809
Merge pull request #41 from psonis/patch-1
Update README.md
2018-07-10 11:51:45 +02:00
psonis 2263bdb17f
Update README.md
small typo on line 36.
2018-07-10 10:52:39 +03:00
Álvaro Arranz a24880adc8 Bump plugin version 2018-02-07 00:26:30 +01:00
Francisco de la Vega 1f9d13f3e3
Merge pull request #40 from conwetlab/develop
Merge develop for release 0.3
2018-01-04 13:18:02 +01:00
Francisco de la Vega 342a42b4d9 Update selenium tests, org members can see non searchable datasets 2017-12-28 10:44:12 +01:00
Francisco de la Vega 958d621ff4 Update selenium tests, new version always displays datasets to owners 2017-12-27 19:07:09 +01:00
Francisco de la Vega af8283794c Update organization selenium tests, user are not logged out 2017-12-27 14:23:20 +01:00
Francisco de la Vega acaea8782c Update selenium tests, users are not logged out in new version 2017-12-27 13:42:23 +01:00
Francisco de la Vega f24ec2b428 Update non-authorized dataset access, new versions show a 404 error 2017-12-26 15:23:35 +01:00
Francisco de la Vega 1682108037 Remove replicated settings in test.ini 2017-12-26 13:56:41 +01:00
Francisco de la Vega a7f7069975 Include an explicit wait for log in button in selenium tests 2017-12-26 13:26:16 +01:00
Francisco de la Vega fd7f99a438 Include travis and coveralls badges 2017-12-26 11:22:24 +01:00
Francisco de la Vega 76b5bb82d9 Configure travis to execute selenium tests 2017-12-22 15:06:10 +01:00
Francisco de la Vega 19894497b2 Update testing CKAN versions 2017-12-22 14:52:43 +01:00
Francisco de la Vega 65d208421f Merge branch 'develop' of https://github.com/conwetlab/ckanext-privatedatasets into develop 2017-12-22 14:42:44 +01:00
Francisco de la Vega 2cba3fbe0f Configure travis CI for testing 2017-12-22 14:42:22 +01:00
Álvaro Arranz 62c8fa4f8f Add fontawesome 4 css classes 2017-11-16 15:49:52 +01:00
Francisco de la Vega c7eef4df32 Implement IPermissionLabel interface to suppport searchable datasets in v2.7 2017-11-15 19:09:36 +01:00
Álvaro Arranz efae19f0fb Add fontawesome 4 css classes 2017-11-14 11:06:41 +01:00
Álvaro Arranz 77f4e50f65 Add fontawesome 4 css classes 2017-11-14 11:03:25 +01:00
fdelavega 8d8c28dd85 Update tests to support revoke_access rename 2017-04-26 17:58:26 +02:00
fdelavega 018b84127c Fix problem with renamed revoked_access action 2017-04-26 16:52:53 +02:00
Francisco de la Vega 6fc906a003 Merge pull request #37 from Cazaril/develop
Delete users from private datasets
2017-04-26 16:19:47 +02:00
egonzalo 3673353891 reword of some log messages 2016-11-10 13:33:05 +01:00
egonzalo 6bbd9f5b95 Log format change in actions.py 2016-11-03 10:14:28 +01:00
egonzalo 1bb294832e Replace package_deleted function name for revoke_access 2016-11-02 11:37:37 +01:00
Aitor Magán García a1490bf88f Force html5lib version to be used 2016-07-19 21:30:23 +02:00
Aitor Magán García d1f92cba6e Fix setuptools version 2016-07-19 21:26:04 +02:00
Aitor Magán García 596464e41e Fix deactivate 2016-07-19 21:21:10 +02:00
Aitor Magán García b3aeafda5e Reload virtenv 2016-07-19 21:17:46 +02:00
Aitor Magán García 06e09083b0 Update CKAN version to be used 2016-07-19 21:13:28 +02:00
Aitor Magán García ad26e11963 Upgrade setuptools before installing dependencies 2016-07-19 20:59:15 +02:00
aitor.magan 83f81d56ea Merge branch 'develop' of github.com:conwetlab/ckanext-privatedatasets into develop 2016-07-18 23:59:46 +02:00
Aitor Magán García 00e8a69ec2 update solr repository 2016-07-18 23:51:03 +02:00
Aitor Magán f7ee7e325f Update Solr version 2016-07-18 23:51:03 +02:00
Aitor Magán 7e9305db29 Dockerize tests 2016-07-18 23:51:03 +02:00
Aitor Magán 843ad8a264 Dockerize tests 2016-07-18 23:51:03 +02:00
aitor.magan e77e91045e Modify tests to accept PR #36 2016-07-18 23:50:36 +02:00
Cazaril 014779a096 Refactor and test in order to comply to PR requirements 2016-07-11 14:33:27 +02:00
Eugenio Gonzalo 5b6bd59715 Added tests for the remove users API endpoint. 2016-07-07 12:28:08 +02:00
Cazaril 0eca66d015 Added remove users from private dataset API endpoint 2016-07-07 12:27:57 +02:00
Rômulo Barroso Victor 4cf0e9a70a Fix authz imports
According to ckan/ckan#2818, ckan.new_authz is deprecated. 
This fix is similar to 40bf2d7
2016-07-05 18:36:02 -03:00
Aitor Magán García 5a483b798e update solr repository 2016-05-30 23:13:35 +02:00
Aitor Magán a1d9516faf Update Solr version 2016-04-07 12:37:53 +02:00
Aitor Magán e98c44bdb7 Merge branch 'dockerize_tests' into develop 2016-03-09 11:59:50 +01:00
Aitor Magán c47949155e Dockerize tests 2016-03-09 10:48:10 +01:00
Aitor Magán 273f902d76 Dockerize tests 2016-03-02 15:43:23 +01:00
Aitor Magán García 57e486c5e8 Include instructions to install manually 2015-11-25 19:28:22 +01:00
Aitor Magán 4d920b1abc Add coverage report notice 2015-11-25 17:54:13 +01:00
Aitor Magán 3116417ed1 Merge branch 'develop' 2015-11-25 13:45:21 +01:00
Aitor Magán af7468ee6b Change download URL 2015-11-25 13:45:16 +01:00
Aitor Magán García 07ff283ec6 Merge pull request #34 from conwetlab/develop
Installing via PyPi
2015-11-25 13:34:03 +01:00
Aitor Magán 07d22a8733 Increase Selenium version 2015-11-25 13:14:46 +01:00
Aitor Magán e0975bb820 Modify README to include the PyPi instructions to install the extension 2015-11-25 12:58:20 +01:00
Aitor Magán 66118bb712 Include setup.cfg 2015-11-25 12:49:44 +01:00
Aitor Magán 5454b614b4 Use setup.py to run tests 2015-11-25 12:48:58 +01:00
Aitor Magán García e33a14c1d9 Merge pull request #33 from conwetlab/develop
Ease the installation process
2015-11-24 17:53:46 +01:00
Aitor Magán 4071213a55 Update installation instructions 2015-11-24 17:52:58 +01:00
Aitor Magán 19f929d60b Include MANIFEST.in && update version number 2015-11-24 17:45:52 +01:00
Aitor Magán García 47e65279bd Add FIWARE reference 2015-06-30 16:01:41 +02:00
Aitor Magán García 172bfa221f Merge pull request #32 from conwetlab/develop
Fix Selenium Tests
2015-06-30 15:58:57 +02:00
Aitor Magán García 98923e431b Merge pull request #31 from conwetlab/master
Fix Selenium Tests
2015-06-30 15:57:15 +02:00
Aitor Magán 3362eec822 Add missing file 2015-06-30 14:36:30 +02:00
Aitor Magán 3359252c6a Avoid modified packages to jump to the first position 2015-06-30 13:13:31 +02:00
Aitor Magán 4eb13dcc65 Remove old comments 2015-06-30 13:12:06 +02:00
Aitor Magán 844cad96a8 Fix Selenium Tests. Full compatibility with CKAN 2.3. 2015-06-30 12:10:05 +02:00
Aitor Magán García 10ab4e0f05 Merge pull request #30 from conwetlab/develop
Merge #22 into master
2015-06-18 13:24:31 +02:00
Aitor Magán 961aef16d9 Open acquire URL in a new window/tab 2015-06-18 13:21:10 +02:00
Aitor Magán 8fb0e38961 Add test for acquire_button 2015-06-18 13:20:56 +02:00
Aitor Magán 304ee2a62e Merge branch 'develop' of github.com:conwetlab/ckanext-privatedatasets into develop 2015-06-18 13:05:52 +02:00
Aitor Magán 4b59fc982b Minor refactor 2015-06-18 13:05:37 +02:00
Aitor Magán fc0cf6fef3 Merge branch 'pgalves-upstream' into develop 2015-06-18 12:46:06 +02:00
Aitor Magán 2b860aa8e5 Merge branch 'upstream' of https://github.com/pgalves/ckanext-privatedatasets into pgalves-upstream 2015-06-18 12:45:40 +02:00
Aitor Magán García 18623c7d1e Merge pull request #28 from conwetlab/develop
Compatibility with CKAN 2.3 (develop -> master)
2015-04-09 10:40:36 +02:00
Aitor Magán García eb465af142 Merge pull request #27 from conwetlab/ckan-2.3
Compatibility with CKAN 2.3
2015-04-09 10:39:47 +02:00
Aitor Magán fab7c88bb2 Minor improvement. Add tests 2015-04-09 10:34:44 +02:00
Aitor Magán 3681c587f4 Minor improvement. Ensure backwards compatibility 2015-04-08 14:34:35 +02:00
Aitor Magán García 84000b72ea Remove unnecesary auth function. Problem is fixed in CKAN 2.3 2015-03-11 00:49:55 +01:00
Aitor Magán García 28ef087f47 Remove unnecesary "is" 2015-02-26 17:52:27 +01:00
Pedro Alves a95157d370 Removed PDF documentation to pull only the code. 2015-01-21 18:16:11 +00:00
Pedro Alves 4c60a579df Added Get Access Button: this button shows in private datsets that can be aquired in the Store, links to the corresponding offering in Store. Replaces the flash message with link in the original ckan extension
Cherry-pick to create pull request without the pdf documentation.
2015-01-21 18:09:40 +00:00
Pedro Alves 30d10a07ef Added PDF file with a tutorial on how to publish datasets (as offerings) from FIWARE Data in the FIWARE Store.
Added PDF file with a tutorial on how to get access to a private acquirable data.
2014-12-31 12:05:11 +00:00
Pedro Alves 040d329d46 Added Get Access Button: this button shows in private datsets that can be aquired in the Store, links to the corresponding offering in Store. Replaces the flash message with link in the original ckan extension 2014-12-12 12:00:57 +00:00
42 changed files with 1639 additions and 799 deletions

5
.gitignore vendored
View File

@ -20,7 +20,7 @@ var/
*.egg-info/
.installed.cfg
*.egg
.eggs
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
@ -50,3 +50,6 @@ coverage.xml
# Sphinx documentation
docs/_build/
.idea
venv

30
.travis.yml Normal file
View File

@ -0,0 +1,30 @@
sudo: required
language: python
python:
- "2.7"
env:
- CKANVERSION=2.7.3
- CKANVERSION=2.8.1
- CKANVERSION=2.8.2
services:
- redis-server
- postgresql
- xvfb
addons:
firefox: "60.1.0esr"
before_install:
- wget https://github.com/mozilla/geckodriver/releases/download/v0.21.0/geckodriver-v0.21.0-linux64.tar.gz
- mkdir geckodriver
- tar -xzf geckodriver-v0.21.0-linux64.tar.gz -C geckodriver
- export PATH=$PATH:$PWD/geckodriver
install:
- bash bin/travis-build.bash
before_script:
- "export DISPLAY=:99.0"
- sleep 3 # give xvfb some time to start
script:
- sh bin/travis-run.sh
after_success: coveralls
branches:
only:
- master

3
MANIFEST.in Normal file
View File

@ -0,0 +1,3 @@
recursive-include ckanext/privatedatasets/templates *
recursive-include ckanext/privatedatasets/templates_2.8 *
recursive-include ckanext/privatedatasets/fanstatic *

View File

@ -1,40 +1,60 @@
CKAN Private Datasets [![Build Status](https://build.conwet.fi.upm.es/jenkins/buildStatus/icon?job=ckan_privatedatasets-develop)](https://build.conwet.fi.upm.es/jenkins/job/ckan_privatedatasets-develop/)
D4Science CKAN Private Datasets [![Build Status](https://travis-ci.org/conwetlab/ckanext-privatedatasets.svg?branch=master)](https://travis-ci.org/conwetlab/ckanext-privatedatasets) [![Coverage Status](https://coveralls.io/repos/github/conwetlab/ckanext-privatedatasets/badge.svg?branch=master)](https://coveralls.io/github/conwetlab/ckanext-privatedatasets?branch=develop)
=====================
This CKAN extension allows a user to create private datasets that only certain users will be able to see. When a dataset is being created, it's possible to specify the list of users that can see this dataset. In addition, the extension provides an HTTP API that allows to add users programatically.
This CKAN extension allows a user to create private datasets that only certain users will be able to see. When a dataset is being created, it's possible to specify the list of users that can see this dataset. In addition, the extension provides an HTTP API that allows to add users programmatically.
This project is part of [FIWARE](http://www.fiware.org).
Installation
------------
Install this extension in your CKAN is instance is as easy as intall any other CKAN extension.
Install this extension in your CKAN instance is as easy as install any other CKAN extension.
* Download the source from this GitHub repo.
* Activate your virtual environment (generally by running `. /usr/lib/ckan/default/bin/activate`)
* Install the extension by running `python setup.py develop`
* Modify your configuration file (generally in `/etc/ckan/default/production.ini`) and add `privatedatasets` in the `ckan.plugins` setting.
* Activate your virtual environment
```
. /usr/lib/ckan/default/bin/activate
```
* Install the extension by running
```
pip install ckanext-privatedatasets
```
> **Note**: If you prefer, you can also download the source code and install the extension manually. To do so, execute the following commands:
> ```
> $ git clone https://github.com/conwetlab/ckanext-privatedatasets.git
> $ cd ckanext-privatedatasets
> $ python setup.py install
> ```
* Modify your configuration file (generally in `/etc/ckan/default/production.ini`) and add `privatedatasets` in the `ckan.plugins` property.
```
ckan.plugins = privatedatasets <OTHER_PLUGINS>
```
* In the same config file, specify the location of your parser by adding the `ckan.privatedatasets.parser` setting. For example, to set the [FiWareNotificationParser](https://github.com/conwetlab/ckanext-privatedatasets/blob/master/ckanext/privatedatasets/parsers/fiware.py) as notification parser, add the following line: `ckan.privatedatasets.parser = ckanext.privatedatasets.parsers.fiware:FiWareNotificationParser`.
* If you want you can also add some preferences to set if the Acquire URL should be shown when the user is to create and/or editing a dataset:
* To show the Acquire URL when the user is **creating** a dataset, you should set the following preference: `ckan.privatedatasets.show_acquire_url_on_create = True`. By default, the value of this preference is set to `False`.
* To show the Acquire URL when the user is **editing** a dataset, you should set the following preference: `ckan.privatedatasets.show_acquire_url_on_edit = True`. By default, the value of this preference is set to `False`.
* In some cases you will want to secure the notification callback in order to filter the entities (user, machines...) that can send them. To do so, you can follow the instructions in the section [Securing the Notification Callback](#securing-the-notification-callback).
* Restart your apache2 reserver (`sudo service apache2 restart`)
* In some cases you will want to secure the notification callback in order to filter the entities (user, machines...) that can send them. To do so, you can follow the instructions in the section [Securing the Notification Callback](#securing-the-notification-callback).
* Restart your apache2 server
```
sudo service apache2 restart
```
* That's All!
Creating a notification parser
------------------------------
Since each service can send notifications in a different way, the extension allows developers to create their own notifications parser. As default, we provide you a basic parser based on the notifications sent by the [FiWare Store](https://github.com/conwetlab/wstore/).
Since each service can send notifications in a different way, the extension allows developers to create their own notifications parser. As default, we provide you a basic parser based on the notifications sent by the [FiWare Store](https://github.com/conwetlab/wstore/).
If you want to create your own parser, you have to:
1. Create a class with a method called `parse_notification`. This method will recieve one argument that will include the notification body.
1. Create a class with a method called `parse_notification`. This method will receive one argument that will include the notification body.
2. Parse the notification as you like. You can raise a CKAN's default exception (`ValidationError`, `ObjectNotFound`, `NotAuthorized`, `ValidationError`, `SearchError`, `SearchQueryError` or `SearchIndexError`) if you find an error parsing the notification.
3. Return a dictionary with the structure attached below. The `users_datasets` is the lists of datasets available for each user (each element of this list is a dictionary with two fields: `user` and `datasets`).
3. Return a dictionary with the structure attached below. The `users_datasets` is the lists of datasets available for each user (each element of this list is a dictionary with two fields: `user` and `datasets`).
```
{'users_datasets': [{'user': 'user_name', 'datasets': ['ds1', 'ds2', ...]},
{'user': 'user_name2', 'datasets': ['ds1', 'ds4', ...] }]}
```
Finally, you have to modify your config file and specify in the `ckan.privatedatasets.parser` the location of your own parser.
Finally, you have to modify your config file and specify in the `ckan.privatedatasets.parser` the location of your own parser.
At this point, you will be able to add users via API by accessing the following URL:
@ -89,13 +109,13 @@ crlDistributionPoints = @crl
URI=http://testca.local/ca.crl
```
Then, you should create your CA using the previous config file. To do so, you can execure the following line (replace `<PATH_TO_SSL_CONFIG_FILE>` by the real path of your OpenSSL config file):
Then, you should create your CA using the previous config file. To do so, you can execute the following line (replace `<PATH_TO_SSL_CONFIG_FILE>` by the real path of your OpenSSL config file):
```
$ openssl req -config <PATH_TO_SSL_CONFIG_FILE> -newkey rsa:2048 -nodes -keyform PEM -keyout ca.key -x509 -days 3650 -extensions certauth -outform PEM -out ca.cer
```
Afterwards, you will need to filter the notification callback to be callable only by those entities that use a valid certificate (the one signed by the CA created previously). To achieve this, edit the file `/etc/apache2/sites-available/ckan_default` and add the following lines inmediatly after the SSL configuration (replace `<PATH_TO_SSL_CONFIG_FILE>` by the real path of your OpenSSL config file):
Afterwards, you will need to filter the notification callback to be callable only by those entities that use a valid certificate (the one signed by the CA created previously). To achieve this, edit the file `/etc/apache2/sites-available/ckan_default` and add the following lines immediately after the SSL configuration (replace `<PATH_TO_SSL_CONFIG_FILE>` by the real path of your OpenSSL config file):
```
<Location /api/action/dataset_acquired>
@ -119,17 +139,12 @@ $ openssl x509 -req -in client.req -CA ca.cer -CAkey ca.key -set_serial 101 -ext
$ openssl pkcs12 -export -inkey client.key -in client.cer -out client.p12
```
That's all! You notification callback is completly secure now! Enjoy it :)
That's all! You notification callback is completely secure now! Enjoy it :)
Tests
-----
This sofware contains a set of test to detect errors and failures. You can run this tests by running the following command:
This sofware contains a set of test to detect errors and failures. You can run this tests by running the following command (this command will generate coverage reports):
```
nosetests --ckan --with-pylons=test.ini ckanext/privatedatasets/tests/
```
**Note:** The `test.ini` file contains a link to the CKAN `test-core.ini` file. You will need to change that link to the real path of the file in your system (generally `/usr/lib/ckan/default/src/ckan/test-core.ini`).
You can also generate coverage reports by running:
```
nosetests --ckan --with-xunit --with-pylons=test.ini ckanext/privatedatasets/tests/ --with-coverage --cover-package=ckanext.privatedatasets --cover-inclusive --cover-erase . --cover-xml
python setup.py nosetests
```
**Note:** The `test.ini` file contains a link to the CKAN `test-core.ini` file. You will need to change that link to the real path of the file in your system (generally `/usr/lib/ckan/default/src/ckan/test-core.ini`).

View File

@ -1,107 +0,0 @@
#!/bin/bash
set -xe
trap 'jobs -p | xargs --no-run-if-empty kill' INT TERM EXIT
export PATH=$PATH:/usr/local/bin
export PIP_DOWNLOAD_CACHE=~/.pip_cache
WD=`pwd`
POSTGRES_PORT=${POSTGRES_PORT:=5432}
CACHE_DIR=~/.cache
echo "Downloading CKAN..."
git clone https://github.com/ckan/ckan
cd ckan
git checkout release-v2.2.2
cd $WD
echo "Checking Solr..."
SOLR_ACTIVE=`nc -z localhost 8983; echo $?`
if [ $SOLR_ACTIVE -ne 0 ]
then
echo "Downloading Solr..."
CACHE_DIR=~/.cache
FILE=solr-4.8.1.tgz
SOLAR_UNZIP_FOLDER=solr-4.8.1
# If the solar folder does not exist, we have to build it
if [ ! -d "$CACHE_DIR/$SOLAR_UNZIP_FOLDER" ]
then
# Download the solar installation file if it does not exist
wget --quiet --timestamping --directory-prefix=$CACHE_DIR http://apache.rediris.es/lucene/solr/4.8.1/$FILE
# Unzip the folder
tar -xf "$CACHE_DIR/$FILE" --directory "$CACHE_DIR"
# Delete the downloaded tar.gz
rm "$CACHE_DIR/$FILE"
fi
echo "Configuring and starting Solr..."
ln -s "$CACHE_DIR/$SOLAR_UNZIP_FOLDER" .
mv "$SOLAR_UNZIP_FOLDER/example/solr/collection1/conf/schema.xml" "$SOLAR_UNZIP_FOLDER/example/solr/collection1/conf/schema.xml.bak"
ln -s $WD/ckan/ckan/config/solr/schema.xml "$SOLAR_UNZIP_FOLDER/example/solr/collection1/conf/schema.xml"
cd solr-4.8.1/example
java -jar start.jar 2>&1 > /dev/null &
cd $WD
else
echo "Solar is already installed..."
fi
echo "Setting up virtualenv..."
virtualenv --no-site-packages virtualenv
source virtualenv/bin/activate
pip install --upgrade pip
echo "Installing CKAN dependencies..."
cd ckan
python setup.py develop
pip install -r requirements.txt --allow-all-external
pip install -r dev-requirements.txt --allow-all-external
cd ..
echo "Removing databases from old executions..."
sudo -u postgres psql -c "DROP DATABASE IF EXISTS datastore_test;"
sudo -u postgres psql -c "DROP DATABASE IF EXISTS ckan_test;"
sudo -u postgres psql -c "DROP USER IF EXISTS ckan_default;"
echo "Creating the PostgreSQL user and database..."
sudo -u postgres psql -c "CREATE USER ckan_default WITH PASSWORD 'pass';"
sudo -u postgres psql -c 'CREATE DATABASE ckan_test WITH OWNER ckan_default;'
sudo -u postgres psql -c 'CREATE DATABASE datastore_test WITH OWNER ckan_default;'
echo "Modifying the configuration to setup properly the Postgres port..."
mkdir -p data/storage
echo "
sqlalchemy.url = postgresql://ckan_default:pass@localhost:$POSTGRES_PORT/ckan_test
ckan.datastore.write_url = postgresql://ckan_default:pass@localhost:$POSTGRES_PORT/datastore_test
ckan.datastore.read_url = postgresql://datastore_default:pass@localhost:$POSTGRES_PORT/datastore_test
ckan.storage_path=data/storage" >> test.ini
echo "Initializing the database..."
sed -i "s/\(postgresql:\/\/.\+@localhost\)/\1:$POSTGRES_PORT/g" ckan/test-core.ini
cd ckan
paster db init -c test-core.ini
cd ..
echo "Installing ckanext-privatedatasets and its requirements..."
python setup.py develop
pip install -r dev-requirements.txt
echo "Running tests..."
nosetests --ckan --with-xunit --with-pylons=test.ini ckanext/privatedatasets/tests/ --with-coverage \
--cover-package=ckanext.privatedatasets --cover-inclusive --cover-erase . --cover-xml

50
bin/travis-build.bash Executable file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -e
echo "This is travis-build.bash..."
echo "Installing the packages that CKAN requires..."
sudo apt-get update -qq
sudo apt-get install solr-jetty
echo "Installing CKAN and its Python dependencies..."
git clone https://github.com/ckan/ckan
cd ckan
git checkout ckan-$CKANVERSION
python setup.py develop
sed -i "s|psycopg2==2.4.5|psycopg2==2.7.1|g" requirements.txt
pip install -r requirements.txt
pip install -r dev-requirements.txt
cd -
echo "Checking solr"
ls -la /etc/
echo "Setting up Solr..."
# solr is multicore for tests on ckan master now, but it's easier to run tests
# on Travis single-core still.
# see https://github.com/ckan/ckan/issues/2972
sed -i -e 's/solr_url.*/solr_url = http:\/\/127.0.0.1:8080\/solr/' ckan/test-core.ini
printf "NO_START=0\nJETTY_HOST=127.0.0.1\nJETTY_PORT=8080\nJAVA_HOME=$JAVA_HOME" | sudo tee /etc/default/jetty
sudo cp ckan/ckan/config/solr/schema.xml /etc/solr/conf/schema.xml
sudo service jetty8 restart
echo "Creating the PostgreSQL user and database..."
sudo -u postgres psql -c "CREATE USER ckan_default WITH PASSWORD 'pass';"
sudo -u postgres psql -c "CREATE USER datastore_default WITH PASSWORD 'pass';"
sudo -u postgres psql -c "CREATE DATABASE ckan_test WITH OWNER ckan_default;"
sudo -u postgres psql -c "CREATE DATABASE datastore_test WITH OWNER ckan_default;"
echo "Initialising the database..."
cd ckan
paster db init -c test-core.ini
cd -
echo "Installing ckanext-privatedatasets and its requirements..."
python setup.py develop
echo "travis-build.bash is done."

8
bin/travis-run.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
echo "Starting Jetty"
sudo service jetty8 restart
sudo netstat -ntlp
python setup.py nosetests

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid
# Copyright (c) 2014 - 2017 CoNWeT Lab., Universidad Politécnica de Madrid
# Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L.
# This file is part of CKAN Private Dataset Extension.
@ -17,12 +18,16 @@
# You should have received a copy of the GNU Affero General Public License
# along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>.
import ckan.plugins as plugins
import ckanext.privatedatasets.constants as constants
import db
from __future__ import absolute_import
import importlib
import logging
import pylons.config as config
import os
import ckan.plugins as plugins
from ckanext.privatedatasets import constants, db
log = logging.getLogger(__name__)
@ -52,92 +57,9 @@ def package_acquired(context, request_data):
:rtype: dict
'''
context['method'] = 'grant'
return _process_package(context, request_data)
log.info('Notification received: %s' % request_data)
# Check access
plugins.toolkit.check_access(constants.PACKAGE_ACQUIRED, context, request_data)
# Get the parser from the configuration
class_path = config.get(PARSER_CONFIG_PROP, '')
if class_path != '':
try:
cls = class_path.split(':')
class_package = cls[0]
class_name = cls[1]
parser_cls = getattr(importlib.import_module(class_package), class_name)
parser = parser_cls()
except Exception as e:
raise plugins.toolkit.ValidationError({'message': '%s: %s' % (type(e).__name__, str(e))})
else:
raise plugins.toolkit.ValidationError({'message': '%s not configured' % PARSER_CONFIG_PROP})
# Parse the result using the parser set in the configuration
# Expected result: {'errors': ["...", "...", ...]
# 'users_datasets': [{'user': 'user_name', 'datasets': ['ds1', 'ds2', ...]}, ...]}
result = parser.parse_notification(request_data)
warns = []
# Introduce the users into the datasets
for user_info in result['users_datasets']:
for dataset_id in user_info['datasets']:
try:
context_pkg_show = context.copy()
context_pkg_show['ignore_auth'] = True
context_pkg_show[constants.CONTEXT_CALLBACK] = True
dataset = plugins.toolkit.get_action('package_show')(context_pkg_show, {'id': dataset_id})
# This operation can only be performed with private datasets
# This check is redundant since the package_update function will throw an exception
# if a list of allowed users is included in a public dataset. However, this check
# should be performed in order to avoid strange future exceptions
if dataset.get('private', None) is True:
# Create the array if it does not exist
if constants.ALLOWED_USERS not in dataset or dataset[constants.ALLOWED_USERS] is None:
dataset[constants.ALLOWED_USERS] = []
# Add the user only if it is not in the list
if user_info['user'] not in dataset[constants.ALLOWED_USERS]:
dataset[constants.ALLOWED_USERS].append(user_info['user'])
context_pkg_update = context.copy()
context_pkg_update['ignore_auth'] = True
# Set creator as the user who is performing the changes
user_show = plugins.toolkit.get_action('user_show')
creator_user_id = dataset.get('creator_user_id', '')
user_show_context = {'ignore_auth': True}
user = user_show(user_show_context, {'id': creator_user_id})
context_pkg_update['user'] = user.get('name', '')
plugins.toolkit.get_action('package_update')(context_pkg_update, dataset)
log.info('Allowed Users added correctly')
else:
log.warn('The user %s is already allowed to access the %s dataset' % (user_info['user'], dataset_id))
else:
log.warn('Dataset %s is public. Allowed Users cannot be added')
warns.append('Unable to upload the dataset %s: It\'s a public dataset' % dataset_id)
except plugins.toolkit.ObjectNotFound:
# If a dataset does not exist in the instance, an error message will be returned to the user.
# However the process won't stop and the process will continue with the remaining datasets.
log.warn('Dataset %s was not found in this instance' % dataset_id)
warns.append('Dataset %s was not found in this instance' % dataset_id)
except plugins.toolkit.ValidationError as e:
# Some datasets does not allow to introduce the list of allowed users since this property is
# only valid for private datasets outside an organization. In this case, a wanr will return
# but the process will continue
# WARN: This exception should not be risen anymore since public datasets are not updated.
message = '%s(%s): %s' % (dataset_id, constants.ALLOWED_USERS, e.error_dict[constants.ALLOWED_USERS][0])
log.warn(message)
warns.append(message)
# Return warnings that inform about non-existing datasets
if len(warns) > 0:
return {'warns': warns}
def acquisitions_list(context, data_dict):
'''
@ -178,7 +100,7 @@ def acquisitions_list(context, data_dict):
# Get the datasets
for dataset in query:
try:
dataset_show_func = 'dataset_show'
dataset_show_func = 'package_show'
func_data_dict = {'id': dataset.package_id}
internal_context = context.copy()
@ -194,3 +116,124 @@ def acquisitions_list(context, data_dict):
pass
return result
def revoke_access(context, request_data):
'''
API action to be called in order to revoke access grants of an user.
This API should be called to delete the user from the list of allowed users.
Since each service can provide a different way of pushing the data, the received
data will be forwarded to the parser set in the preferences. This parser should
return a dict similar to the following one:
{'errors': ["...", "...", ...]
'users_datasets': [{'user': 'user_name', 'datasets': ['ds1', 'ds2', ...]}, ...]}
1) 'errors' contains the list of errors. It should be empty if no errors arised
while the notification is parsed
2) 'users_datasets' is the lists of datasets available for each user (each element
of this list is a dictionary with two fields: user and datasets).
:parameter request_data: Depends on the parser
:type request_data: dict
:return: A list of warnings or None if the list of warnings is empty
:rtype: dict
'''
context['method'] = 'revoke'
return _process_package(context, request_data)
def _process_package(context, request_data):
log.info('Notification received: %s' % request_data)
# Check access
method = constants.PACKAGE_ACQUIRED if context.get('method') == 'grant' else constants.PACKAGE_DELETED
plugins.toolkit.check_access(method, context, request_data)
# Get the parser from the configuration
class_path = os.environ.get(PARSER_CONFIG_PROP.upper().replace('.', '_'), plugins.toolkit.config.get(PARSER_CONFIG_PROP, ''))
if class_path != '':
try:
cls = class_path.split(':')
class_package = cls[0]
class_name = cls[1]
parser_cls = getattr(importlib.import_module(class_package), class_name)
parser = parser_cls()
except Exception as e:
raise plugins.toolkit.ValidationError({'message': '%s: %s' % (type(e).__name__, str(e))})
else:
raise plugins.toolkit.ValidationError({'message': '%s not configured' % PARSER_CONFIG_PROP})
# Parse the result using the parser set in the configuration
# Expected result: {'errors': ["...", "...", ...]
# 'users_datasets': [{'user': 'user_name', 'datasets': ['ds1', 'ds2', ...]}, ...]}
result = parser.parse_notification(request_data)
warns = []
for user_info in result['users_datasets']:
for dataset_id in user_info['datasets']:
try:
context_pkg_show = context.copy()
context_pkg_show['ignore_auth'] = True
context_pkg_show[constants.CONTEXT_CALLBACK] = True
dataset = plugins.toolkit.get_action('package_show')(context_pkg_show, {'id': dataset_id})
# This operation can only be performed with private datasets
# This check is redundant since the package_update function will throw an exception
# if a list of allowed users is included in a public dataset. However, this check
# should be performed in order to avoid strange future exceptions
if dataset.get('private', None) is True:
# Create the array if it does not exist
if constants.ALLOWED_USERS not in dataset or dataset[constants.ALLOWED_USERS] is None:
dataset[constants.ALLOWED_USERS] = []
method = context['method'] == 'grant'
present = user_info['user'] in dataset[constants.ALLOWED_USERS]
# Deletes the user only if it is in the list
if (not method and present) or (method and not present):
if method:
dataset[constants.ALLOWED_USERS].append(user_info['user'])
else:
dataset[constants.ALLOWED_USERS].remove(user_info['user'])
context_pkg_update = context.copy()
context_pkg_update['ignore_auth'] = True
# Set creator as the user who is performing the changes
user_show = plugins.toolkit.get_action('user_show')
creator_user_id = dataset.get('creator_user_id', '')
user_show_context = {'ignore_auth': True}
user = user_show(user_show_context, {'id': creator_user_id})
context_pkg_update['user'] = user.get('name', '')
plugins.toolkit.get_action('package_update')(context_pkg_update, dataset)
log.info('Action %s access to dataset ended successfully' % context['method'])
else:
log.warn('Action %s access to dataset not completed. The dataset %s already %s access to the user %s' % (context['method'], dataset_id, context['method'], user_info['user']))
else:
log.warn('Dataset %s is public. Cannot %s access to users' % (dataset_id, context['method']))
warns.append('Unable to upload the dataset %s: It\'s a public dataset' % dataset_id)
except plugins.toolkit.ObjectNotFound:
# If a dataset does not exist in the instance, an error message will be returned to the user.
# However the process won't stop and the process will continue with the remaining datasets.
log.warn('Dataset %s was not found in this instance' % dataset_id)
warns.append('Dataset %s was not found in this instance' % dataset_id)
except plugins.toolkit.ValidationError as e:
# Some datasets does not allow to introduce the list of allowed users since this property is
# only valid for private datasets outside an organization. In this case, a wanr will return
# but the process will continue
# WARN: This exception should not be risen anymore since public datasets are not updated.
message = '%s(%s): %s' % (dataset_id, constants.ALLOWED_USERS, e.error_dict[constants.ALLOWED_USERS][0])
log.warn(message)
warns.append(message)
# Return warnings that inform about non-existing datasets
if len(warns) > 0:
return {'warns': warns}

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid
# Copyright (c) 2014 - 2017 CoNWeT Lab., Universidad Politécnica de Madrid
# This file is part of CKAN Private Dataset Extension.
@ -17,13 +17,15 @@
# You should have received a copy of the GNU Affero General Public License
# along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
import ckan.authz as authz
from ckan.common import _, request
import ckan.lib.helpers as helpers
import ckan.logic.auth as logic_auth
import ckan.plugins.toolkit as tk
import ckan.new_authz as new_authz
import db
from ckan.common import _, request
from ckanext.privatedatasets import db
@tk.auth_allow_anonymous_access
@ -39,41 +41,37 @@ def package_show(context, data_dict):
# Not active packages can only be seen by its owners
if package.state == 'active':
# anyone can see a public package
if not package.private:
return {'success': True}
if package.private:
# if the user has rights to read in the organization or in the group
if package.owner_org:
authorized = new_authz.has_user_permission_for_group_or_org(
package.owner_org, user, 'read')
else:
authorized = False
acquired = False
# if the user is not authorized yet, we should check if the
# user is in the allowed_users object
if not authorized:
# Init the model
db.init_db(context['model'])
if package.owner_org:
acquired = authz.has_user_permission_for_group_or_org(
package.owner_org, user, 'read')
# Branch not executed if the database return an empty list
if db.AllowedUser.get(package_id=package.id, user_name=user):
authorized = True
if not acquired:
# Init the model
db.init_db(context['model'])
if not authorized:
# Show a flash message with the URL to acquire the dataset
# This message only can be shown when the user tries to access the dataset via its URL (/dataset/...)
# The message cannot be displayed in other pages that uses the package_show function such as
# the user profile page
# Branch not executed if the database return an empty list
if db.AllowedUser.get(package_id=package.id, user_name=user):
acquired = True
if hasattr(package, 'extras') and 'acquire_url' in package.extras and request.path.startswith('/dataset/')\
and package.extras['acquire_url'] != '':
helpers.flash_notice(_('This private dataset can be acquired. To do so, please click ' +
'<a target="_blank" href="%s">here</a>') % package.extras['acquire_url'],
allow_html=True)
if not acquired:
return {'success': False, 'msg': _('User %s not authorized to read package %s') % (user, package.id)}
else:
return {'success': True}
# Show a flash message with the URL to acquire the dataset
# This message only can be shown when the user tries to access the dataset via its URL (/dataset/...)
# The message cannot be displayed in other pages that uses the package_show function such as
# the user profile page
if hasattr(package, 'extras') and 'acquire_url' in package.extras and request.path.startswith(
'/dataset/') \
and package.extras['acquire_url'] != '':
helpers.flash_notice(_('This private dataset can be acquired. To do so, please click ' +
'<a target="_blank" href="%s">here</a>') % package.extras['acquire_url'],
allow_html=True)
return {'success': True}
else:
return {'success': False, 'msg': _('User %s not authorized to read package %s') % (user, package.id)}
@ -89,7 +87,7 @@ def package_update(context, data_dict):
# if the user has rights to update a dataset in the organization or in the group
if package and package.owner_org:
authorized = new_authz.has_user_permission_for_group_or_org(
authorized = authz.has_user_permission_for_group_or_org(
package.owner_org, user, 'update_dataset')
else:
authorized = False
@ -102,38 +100,62 @@ def package_update(context, data_dict):
@tk.auth_allow_anonymous_access
def resource_show(context, data_dict):
# This function is needed since CKAN resource_show function uses the default package_show
# function instead of the one defined in the plugin.
# A bug is openend in order to be able to remove this function
# https://github.com/ckan/ckan/issues/1818
# It's fixed now, so this function can be deleted when the new version is released.
_model = context['model']
user = context.get('user')
resource = logic_auth.get_resource_object(context, data_dict)
user = context.get('user')
user_obj = context.get('auth_user_obj')
resource = logic_auth.get_resource_object(context, data_dict)
# check authentication against package
query = _model.Session.query(_model.Package)\
.join(_model.ResourceGroup)\
.join(_model.Resource)\
.filter(_model.ResourceGroup.id == resource.resource_group_id)
pkg = query.first()
if not pkg:
package_dict = {'id': resource.package_id}
package = logic_auth.get_package_object(context, package_dict)
if not package:
raise tk.ObjectNotFound(_('No package found for this resource, cannot check auth.'))
pkg_dict = {'id': pkg.id}
authorized = package_show(context, pkg_dict).get('success')
if not authorized:
return {'success': False, 'msg': _('User %s not authorized to read resource %s') % (user, resource.id)}
else:
if package and user_obj and package.creator_user_id == user_obj.id:
return {'success': True}
# active packages can only be seen by its owners
if package.state == 'active':
# anyone can see a public package
if not package.private:
return {'success': True}
# if the user has rights to read in the organization or in the group
if package.owner_org:
authorized = authz.has_user_permission_for_group_or_org(
package.owner_org, user, 'read')
else:
authorized = False
if not authorized:
# Init the model
db.init_db(context['model'])
# Branch not executed if the database return an empty list
if db.AllowedUser.get(package_id=package.id, user_name=user):
authorized = True
if not authorized:
return {'success': False, 'msg': _('User %s not authorized to read resource %s') % (user, resource.id)}
else:
return {'success': True}
else:
return {'success': False, 'msg': _('User %s not authorized to read resource %s') % (user, resource.id)}
@tk.auth_allow_anonymous_access
def package_acquired(context, data_dict):
# TODO: Improve security
return {'success': True}
def acquisitions_list(context, data_dict):
# Users can get only their acquisitions list
return {'success': context['user'] == data_dict['user']}
@tk.auth_allow_anonymous_access
def revoke_access(context, data_dict):
# TODO: Check functionality and improve security(if needed)
return {'success': True}

View File

@ -24,3 +24,4 @@ SEARCHABLE = 'searchable'
ACQUIRE_URL = 'acquire_url'
CONTEXT_CALLBACK = 'updating_via_cb'
PACKAGE_ACQUIRED = 'package_acquired'
PACKAGE_DELETED = 'revoke_access'

View File

@ -1,51 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid
# This file is part of CKAN Private Dataset Extension.
# CKAN Private Dataset Extension is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# CKAN Private Dataset Extension 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 Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>.
import ckanext.privatedatasets.constants as constants
import ckan.lib.base as base
import ckan.model as model
import ckan.plugins as plugins
import logging
from ckan.common import _
log = logging.getLogger(__name__)
class AcquiredDatasetsControllerUI(base.BaseController):
def user_acquired_datasets(self):
c = plugins.toolkit.c
context = {
'model': model,
'session': model.Session,
'user': plugins.toolkit.c.user,
}
# Get user information
try:
c.user_dict = plugins.toolkit.get_action('user_show')(context.copy(), {'user_obj': c.userobj})
c.user_dict['acquired_datasets'] = plugins.toolkit.get_action(constants.ACQUISITIONS_LIST)(context.copy(), None)
except plugins.toolkit.ObjectNotFound:
plugins.toolkit.abort(404, _('User not found'))
except plugins.toolkit.NotAuthorized:
plugins.toolkit.abort(401, _('Not authorized to see this page'))
return plugins.toolkit.render('user/dashboard_acquired.html')

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid
# Copyright (c) 2019 Future Internet Consulting and Development Solutions S.L.
# This file is part of CKAN Private Dataset Extension.
@ -17,13 +18,16 @@
# You should have received a copy of the GNU Affero General Public License
# along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>.
import constants
import db
from __future__ import absolute_import
from itertools import count
import re
from ckan.plugins import toolkit
from ckan.common import _
from itertools import count
import six
from ckanext.privatedatasets import constants, db
def private_datasets_metadata_checker(key, data, errors, context):
@ -33,7 +37,7 @@ def private_datasets_metadata_checker(key, data, errors, context):
# Avoid missing value
# "if not private_val:" is not valid because private_val can be False
if not isinstance(private_val, basestring) and not isinstance(private_val, bool):
if not isinstance(private_val, six.string_types) and not isinstance(private_val, bool):
private_val = None
# If the private field is not included in the data dict, we must check the current value
@ -57,7 +61,7 @@ def allowed_users_convert(key, data, errors, context):
# Get the allowed user list
if (constants.ALLOWED_USERS,) in data and isinstance(data[(constants.ALLOWED_USERS,)], list):
allowed_users = data[(constants.ALLOWED_USERS,)]
elif (constants.ALLOWED_USERS_STR,) in data and isinstance(data[(constants.ALLOWED_USERS_STR,)], basestring):
elif (constants.ALLOWED_USERS_STR,) in data and isinstance(data[(constants.ALLOWED_USERS_STR,)], six.string_types):
allowed_users_str = data[(constants.ALLOWED_USERS_STR,)].strip()
allowed_users = [allowed_user for allowed_user in allowed_users_str.split(',') if allowed_user.strip() != '']
else:
@ -71,7 +75,6 @@ def allowed_users_convert(key, data, errors, context):
else:
for num, allowed_user in zip(count(current_index + 1), allowed_users):
allowed_user = allowed_user.strip()
toolkit.get_validator('name_validator')(allowed_user, context) # User name should be validated
data[(key[0], num)] = allowed_user

View File

@ -17,6 +17,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
import sqlalchemy as sa
AllowedUser = None
@ -38,7 +40,9 @@ def init_db(model):
AllowedUser = _AllowedUser
# FIXME: Maybe a default value should not be included...
package_allowed_users_table = sa.Table('package_allowed_users', model.meta.metadata,
package_allowed_users_table = sa.Table(
'package_allowed_users',
model.meta.metadata,
sa.Column('package_id', sa.types.UnicodeText, primary_key=True, default=u''),
sa.Column('user_name', sa.types.UnicodeText, primary_key=True, default=u''),
)

6
ckanext/privatedatasets/fanstatic/custom.css Normal file → Executable file
View File

@ -4,4 +4,10 @@
.label-owner {
background-color: #e0051e;
}
.divider {
margin-left:10px;
height:auto;
display:inline-block;
}

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid
# Copyright (c) 2014-2015 CoNWeT Lab., Universidad Politécnica de Madrid
# Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L.
# This file is part of CKAN Private Dataset Extension.
@ -17,11 +18,19 @@
# You should have received a copy of the GNU Affero General Public License
# along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
import logging
import os
from ckan.common import request
import ckan.model as model
import ckan.plugins.toolkit as tk
import db
from pylons import config
from ckanext.privatedatasets import db
log = logging.getLogger(__name__)
def is_dataset_acquired(pkg_dict):
@ -57,9 +66,9 @@ def can_read(pkg_dict):
def get_config_bool_value(config_name, default_value=False):
value = config.get(config_name, default_value)
value = value if type(value) == bool else value != 'False'
return value
env_name = config_name.upper().replace('.', '_')
value = os.environ.get(env_name, tk.config.get(config_name, default_value))
return value if type(value) == bool else value.strip().lower() in ('true', '1', 'on')
def show_acquire_url_on_create():
@ -68,3 +77,26 @@ def show_acquire_url_on_create():
def show_acquire_url_on_edit():
return get_config_bool_value('ckan.privatedatasets.show_acquire_url_on_edit')
def acquire_button(package):
'''
Return a Get Access button for the given package id when the dataset has
an acquisition URL.
:param package: the the package to request access when the get access
button is clicked
:type package: Package
:returns: a get access button as an HTML snippet
:rtype: string
'''
if 'acquire_url' in package and request.path.startswith('/dataset')\
and package['acquire_url'] != '':
url_dest = package['acquire_url']
data = {'url_dest': url_dest}
return tk.render_snippet('snippets/acquire_button.html', data)
else:
return ''

View File

@ -17,23 +17,23 @@
# You should have received a copy of the GNU Affero General Public License
# along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>.
import ckan.plugins.toolkit as tk
import re
from urlparse import urlparse
from ckan.common import request
import ckan.plugins.toolkit as tk
import six
class FiWareNotificationParser(object):
def parse_notification(self, request_data):
my_host = request.host
fields = ['customer_name', 'resources']
for field in fields:
if not field in request_data:
if field not in request_data:
raise tk.ValidationError({'message': '%s not found in the request' % field})
# Parse the body
@ -41,7 +41,7 @@ class FiWareNotificationParser(object):
user_name = request_data['customer_name']
datasets = []
if not isinstance(user_name, basestring):
if not isinstance(user_name, six.string_types):
raise tk.ValidationError({'message': 'Invalid customer_name format'})
if not isinstance(resources, list):
@ -52,12 +52,18 @@ class FiWareNotificationParser(object):
parsed_url = urlparse(resource['url'])
dataset_name = re.findall('^/dataset/([^/]+).*$', parsed_url.path)
resource_url = parsed_url.netloc
if ':' in my_host and ':' not in resource_url:
# Add the default port depending on the protocol
default_port = '80' if parsed_url.protocol == 'http' else '443'
resource_url = resource_url + default_port
if len(dataset_name) == 1:
if parsed_url.netloc == my_host:
if resource_url == my_host:
datasets.append(dataset_name[0])
else:
raise tk.ValidationError({'message': 'Dataset %s is associated with the CKAN instance located at %s'
% (dataset_name[0], parsed_url.netloc)})
raise tk.ValidationError({'message': 'Dataset %s is associated with the CKAN instance located at %s, expected %s'
% (dataset_name[0], resource_url, my_host)})
else:
raise tk.ValidationError({'message': 'Invalid resource format'})

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid
# Copyright (c) 2014 - 2017 CoNWeT Lab., Universidad Politécnica de Madrid
# Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L.
# This file is part of CKAN Private Dataset Extension.
@ -17,30 +18,33 @@
# You should have received a copy of the GNU Affero General Public License
# along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>.
import ckan.lib.search as search
import ckan.model as model
import ckan.plugins as p
import ckan.plugins.toolkit as tk
import auth
import actions
import constants
import converters_validators as conv_val
import db
import helpers as helpers
from __future__ import absolute_import, unicode_literals
from ckan import model, plugins as p
from ckan.lib import search
from ckan.lib.plugins import DefaultPermissionLabels
from ckan.plugins import toolkit as tk
from flask import Blueprint
from ckanext.privatedatasets import auth, actions, constants, converters_validators as conv_val, db, helpers
from ckanext.privatedatasets.views import acquired_datasets
HIDDEN_FIELDS = [constants.ALLOWED_USERS, constants.SEARCHABLE]
class PrivateDatasets(p.SingletonPlugin, tk.DefaultDatasetForm):
class PrivateDatasets(p.SingletonPlugin, tk.DefaultDatasetForm, DefaultPermissionLabels):
p.implements(p.IDatasetForm)
p.implements(p.IAuthFunctions)
p.implements(p.IConfigurer)
p.implements(p.IBlueprint)
p.implements(p.IRoutes, inherit=True)
p.implements(p.IActions)
p.implements(p.IPackageController, inherit=True)
p.implements(p.ITemplateHelpers)
p.implements(p.IPermissionLabels)
p.implements(p.IResourceController)
######################################################################
############################ DATASET FORM ############################
@ -108,11 +112,14 @@ class PrivateDatasets(p.SingletonPlugin, tk.DefaultDatasetForm):
######################################################################
def get_auth_functions(self):
return {'package_show': auth.package_show,
'package_update': auth.package_update,
'resource_show': auth.resource_show,
constants.PACKAGE_ACQUIRED: auth.package_acquired,
constants.ACQUISITIONS_LIST: auth.acquisitions_list}
auth_functions = {'package_show': auth.package_show,
'package_update': auth.package_update,
'resource_show': auth.resource_show,
constants.PACKAGE_ACQUIRED: auth.package_acquired,
constants.ACQUISITIONS_LIST: auth.acquisitions_list,
constants.PACKAGE_DELETED: auth.revoke_access}
return auth_functions
######################################################################
############################ ICONFIGURER #############################
@ -121,32 +128,42 @@ class PrivateDatasets(p.SingletonPlugin, tk.DefaultDatasetForm):
def update_config(self, config):
# Add this plugin's templates dir to CKAN's extra_template_paths, so
# that CKAN will use this plugin's custom templates.
tk.add_template_directory(config, 'templates')
if p.toolkit.check_ckan_version(min_version='2.8'):
tk.add_template_directory(config, 'templates_2.8')
else:
tk.add_template_directory(config, 'templates')
# Register this plugin's fanstatic directory with CKAN.
tk.add_resource('fanstatic', 'privatedatasets')
tk.add_resource(b'fanstatic', b'privatedatasets')
######################################################################
############################## IROUTES ###############################
############################# IBLUEPRINT #############################
######################################################################
# Deprecated but Required for CKAN 2.7
def before_map(self, m):
# DataSet acquired notification
m.connect('user_acquired_datasets', '/dashboard/acquired', ckan_icon='shopping-cart',
controller='ckanext.privatedatasets.controllers.ui_controller:AcquiredDatasetsControllerUI',
action='user_acquired_datasets', conditions=dict(method=['GET']))
if p.toolkit.check_ckan_version(max_version='2.7.99'):
m.connect('user_acquired_datasets', '/dashboard/acquired', ckan_icon='shopping-cart',
controller='ckanext.privatedatasets.views:AcquiredDatasetsControllerUI',
action='acquired_datasets', conditions=dict(method=['GET']))
return m
def get_blueprint(self):
blueprint = Blueprint('privatedatasets', self.__module__)
if p.toolkit.check_ckan_version(min_version='2.8'):
blueprint.add_url_rule('/dashboard/acquired', 'acquired_datasets', acquired_datasets)
return blueprint
######################################################################
############################## IACTIONS ##############################
######################################################################
def get_actions(self):
return {
constants.PACKAGE_ACQUIRED: actions.package_acquired,
constants.ACQUISITIONS_LIST: actions.acquisitions_list
}
action_functions = {constants.PACKAGE_ACQUIRED: actions.package_acquired,
constants.ACQUISITIONS_LIST: actions.acquisitions_list,
constants.PACKAGE_DELETED: actions.revoke_access}
return action_functions
######################################################################
######################### IPACKAGECONTROLLER #########################
@ -211,8 +228,10 @@ class PrivateDatasets(p.SingletonPlugin, tk.DefaultDatasetForm):
'validate': False,
'use_cache': False},
{'id': package_id})
# Prevent acquired datasets jumping to the first position
new_pkg_dict['metadata_modified'] = new_pkg_dict.get('revision_timestamp', '')
revision = tk.get_action('revision_show')({'ignore_auth': True}, {'id': new_pkg_dict['revision_id']})
new_pkg_dict['metadata_modified'] = revision.get('timestamp', '')
self.indexer.update_dict(new_pkg_dict)
return pkg_dict
@ -222,6 +241,16 @@ class PrivateDatasets(p.SingletonPlugin, tk.DefaultDatasetForm):
def after_show(self, context, pkg_dict):
void = False;
for resource in pkg_dict['resources']:
if resource == {}:
void = True
if void:
del pkg_dict['resources']
del pkg_dict['num_resources']
user_obj = context.get('auth_user_obj')
updating_via_api = context.get(constants.CONTEXT_CALLBACK, False)
@ -253,7 +282,7 @@ class PrivateDatasets(p.SingletonPlugin, tk.DefaultDatasetForm):
def after_search(self, search_results, search_params):
for result in search_results['results']:
# Extra fields should not be returned
# Extra fields should not be returned
# The original list cannot be modified
attrs = list(HIDDEN_FIELDS)
@ -272,12 +301,74 @@ class PrivateDatasets(p.SingletonPlugin, tk.DefaultDatasetForm):
# NotAuthorized exception is risen when the user is not allowed
# to read the package.
attrs.append('resources')
# Delete
# Delete
self._delete_pkg_atts(result, attrs)
return search_results
####
def before_view(self, pkg_dict):
for resource in pkg_dict['resources']:
context = {
'model': model,
'session': model.Session,
'user': tk.c.user,
'user_obj': tk.c.userobj
}
try:
tk.check_access('resource_show', context, resource)
except tk.NotAuthorized:
pkg_dict['resources'].remove(resource)
pkg_dict = self.before_view(pkg_dict)
return pkg_dict
def get_dataset_labels(self, dataset_obj):
labels = super(PrivateDatasets, self).get_dataset_labels(
dataset_obj)
if getattr(dataset_obj, 'searchable', False):
labels.append('searchable')
return labels
def get_user_dataset_labels(self, user_obj):
labels = super(PrivateDatasets, self).get_user_dataset_labels(
user_obj)
labels.append('searchable')
return labels
######################################################################
######################### IRESOURCECONTROLLER ########################
######################################################################
def before_create(self, context, resource):
pass
def before_update(self, context, current, resource):
pass
def before_delete(self, context, resource, resources):
pass
def before_show(self, resource_dict):
context = {
'model': model,
'session': model.Session,
'user': tk.c.user,
'user_obj': tk.c.userobj
}
try:
tk.check_access('resource_show', context, resource_dict)
except tk.NotAuthorized:
resource_dict.clear()
return resource_dict
######################################################################
######################### ITEMPLATESHELPER ###########################
######################################################################
@ -288,5 +379,6 @@ class PrivateDatasets(p.SingletonPlugin, tk.DefaultDatasetForm):
'is_owner': helpers.is_owner,
'can_read': helpers.can_read,
'show_acquire_url_on_create': helpers.show_acquire_url_on_create,
'show_acquire_url_on_edit': helpers.show_acquire_url_on_edit
'show_acquire_url_on_edit': helpers.show_acquire_url_on_edit,
'acquire_button': helpers.acquire_button
}

View File

@ -49,7 +49,7 @@
{% endfor %}
</select>
<span class="info-block info-inline">
<i class="icon-info-sign"></i>
<i class="icon-info-sign fa fa-info-circle"></i>
{% trans %}
Private datasets can only be accessed by certain users, while public datasets can be accessed by anyone.
{% endtrans %}
@ -68,7 +68,7 @@
{% endfor %}
</select>
<span class="info-block info-inline">
<i class="icon-info-sign"></i>
<i class="icon-info-sign fa fa-info-circle"></i>
{% trans %}
Searchable datasets can be searched by anyone, while not-searchable datasets can only be accessed by entering directly its URL.
{% endtrans %}

View File

@ -0,0 +1,15 @@
{#
Displays a Get Access button to request access to a private dataset.
ulr_dest - target url
Example:
{% snippet 'snippets/acquire_button.html', url_dest=url %}
#}
<a href={{ url_dest }} class="btn btn-mini" target="_blank">
<i class="icon-shopping-cart fa fa-shopping-cart"></i>
{{ _('Acquire') }}
</a>

View File

@ -26,25 +26,35 @@ Example:
{% block package_item_content %}
<div class="dataset-content">
<h3 class="dataset-heading">
{% if package.private and not h.can_read(package) %}
{% if package.private and not owner and not acquired %}
<span class="dataset-private label label-inverse">
<i class="icon-lock"></i>
<i class="icon-lock fa fa-lock"></i>
{{ _('Private') }}
</span>
{% endif %}
{% if acquired and not owner %}
<span class="dataset-private label label-acquired">
<i class="icon-shopping-cart"></i>
<i class="icon-shopping-cart fa fa-shopping-cart"></i>
{{ _('Acquired') }}
</span>
{% endif %}
{% if owner %}
<span class="dataset-private label label-owner">
<i class="icon-user"></i>
<i class="icon-user fa fa-user"></i>
{{ _('Owner') }}
</span>
{% endif %}
{{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }}
<!-- Customizations Acquire Button -->
{% if package.private and not owner and not acquired %}
{{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }}
<div class="divider"/>
{{ h.acquire_button(package) }}
{% else %}
{{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }}
{% endif %}
<!-- End of customizations Acquire Button -->
{% if package.get('state', '').startswith('draft') %}
<span class="label label-info">{{ _('Draft') }}</span>
{% elif package.get('state', '').startswith('deleted') %}

View File

@ -46,4 +46,4 @@
{% endblock %}
</div>
</article>
{% endblock %}
{% endblock %}

View File

@ -8,12 +8,12 @@
{% block primary_content_inner %}
<h2 class="hide-heading">{{ _('Acquired Datasets') }}</h2>
{% if c.user_dict.acquired_datasets %}
{% snippet 'snippets/package_list.html', packages=c.user_dict.acquired_datasets %}
{% if acquired_datasets %}
{% snippet 'snippets/package_list.html', packages=acquired_datasets %}
{% else %}
<p class="empty">
{{ _('You haven\'t acquired any datasets.') }}
{% link_for _('Acquire one now?'), controller='package', action='search' %}
</p>
{% endif %}
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,107 @@
{% ckan_extends %}
{% block package_basic_fields_org %}
{% resource 'privatedatasets/allowed_users.js' %}
{# if we have a default group then this wants remembering #}
{% if data.group_id %}
<input type="hidden" name="groups__0__id" value="{{ data.group_id }}" />
{% endif %}
{% set dataset_is_draft = data.get('state', 'draft').startswith('draft') or data.get('state', 'none') == 'none' %}
{% set dataset_has_organization = data.owner_org or data.group_id %}
{% set organizations_available = h.organizations_available('create_dataset') %}
{% set user_is_sysadmin = h.check_access('sysadmin') %}
{% set show_organizations_selector = organizations_available and (user_is_sysadmin or dataset_is_draft) %}
{% set editing = 'id' in data %}
{% if show_organizations_selector and show_visibility_selector %}
<div>
{% endif %}
{% if show_organizations_selector %}
{% set existing_org = data.owner_org or data.group_id %}
<div class="control-group">
<label for="field-organizations" class="control-label">{{ _('Organization') }}</label>
<div class="controls">
<select id="field-organizations" name="owner_org" data-module="autocomplete">
{% if h.check_config_permission('create_unowned_dataset') %}
<option value="" {% if not selected_org and data.id %} selected="selected" {% endif %}>{{ _('No organization') }}</option>
{% endif %}
{% for organization in organizations_available %}
{# get out first org from users list only if there is not an existing org #}
{% set selected_org = (existing_org and existing_org == organization.id) or (not existing_org and not data.id and organization.id == organizations_available[0].id) %}
<option value="{{ organization.id }}" {% if selected_org %} selected="selected" {% endif %}>{{ organization.name }}</option>
{% endfor %}
</select>
</div>
</div>
{% endif %}
{% block package_metadata_fields_visibility %}
<div class="control-group">
<label for="field-private" class="control-label">{{ _('Visibility') }}</label>
<div class="controls">
<select id="field-private" name="private" data-module="allowed-users">
{% for option in [('True', _('Private')), ('False', _('Public'))] %}
<option value="{{ option[0] }}" {% if option[0] == data.private|trim %}selected="selected"{% endif %}>{{ option[1] }}</option>
{% endfor %}
</select>
<span class="info-block info-inline">
<i class="icon-info-sign fa fa-info-circle"></i>
{% trans %}
Private datasets can only be accessed by certain users, while public datasets can be accessed by anyone.
{% endtrans %}
</span>
</div>
</div>
{% endblock %}
{% block package_metadata_fields_protected %}
<div class="control-group">
<label for="field-searchable" class="control-label">{{ _('Searchable') }}</label>
<div class="controls">
<select id="field-searchable" name="searchable">
{% for option in [('True', _('True')), ('False', _('False'))] %}
<option value="{{ option[0] }}" {% if option[0] == data.searchable|trim %}selected="selected"{% endif %}>{{ option[1] }}</option>
{% endfor %}
</select>
<span class="info-block info-inline">
<i class="icon-info-sign fa fa-info-circle"></i>
{% trans %}
Searchable datasets can be searched by anyone, while not-searchable datasets can only be accessed by entering directly its URL.
{% endtrans %}
</span>
</div>
</div>
{% endblock %}
{% if show_organizations_selector and show_visibility_selector %}
</div>
{% endif %}
{% set users_attrs = {'data-module': 'autocomplete', 'data-module-tags': '', 'data-module-source': '/api/2/util/user/autocomplete?q=?'} %}
{{ form.input('allowed_users_str', label=_('Allowed Users'), id='field-allowed_users_str', placeholder=_('Allowed Users'), value=h.get_allowed_users_str(data.allowed_users), error=errors.custom_text, classes=['control-full'], attrs=users_attrs) }}
{% if editing and h.show_acquire_url_on_edit() or not editing and h.show_acquire_url_on_create() %}
{{ form.input('acquire_url', label=_('Acquire URL'), id='field-acquire_url', placeholder=_('http://example.com/acquire/'), value=data.acquire_url, error=errors.custom_text, classes=['control-medium']) }}
{% else %}
<input type="hidden" name="acquire_url" id="acquire_url" value="{{ data.acquire_url }}" />
{% endif %}
{% if data.id and h.check_access('package_delete', {'id': data.id}) and data.state != 'active' %}
<div class="control-group">
<label for="field-state" class="control-label">{{ _('State') }}</label>
<div class="controls">
<select id="field-state" name="state">
<option value="active" {% if data.get('state', 'none') == 'active' %} selected="selected" {% endif %}>{{ _('Active') }}</option>
<option value="deleted" {% if data.get('state', 'none') == 'deleted' %} selected="selected" {% endif %}>{{ _('Deleted') }}</option>
</select>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,15 @@
{#
Displays a Get Access button to request access to a private dataset.
ulr_dest - target url
Example:
{% snippet 'snippets/acquire_button.html', url_dest=url %}
#}
<a href={{ url_dest }} class="btn btn-mini" target="_blank">
<i class="icon-shopping-cart fa fa-shopping-cart"></i>
{{ _('Acquire') }}
</a>

View File

@ -0,0 +1,82 @@
{#
Displays a single of dataset.
package - A package to display.
item_class - The class name to use on the list item.
hide_resources - If true hides the resources (default: false).
banner - If true displays a popular banner (default: false).
truncate - The length to trucate the description to (default: 180)
truncate_title - The length to truncate the title to (default: 80).
Example:
{% snippet 'snippets/package_item.html', package=c.datasets[0] %}
#}
{% set truncate = truncate or 180 %}
{% set truncate_title = truncate_title or 80 %}
{% set title = package.title or package.name %}
{% set notes = h.markdown_extract(package.notes, extract_length=truncate) %}
{% set acquired = h.is_dataset_acquired(package) %}
{% set owner = h.is_owner(package) %}
{% resource 'privatedatasets/custom.css' %}
<li class="{{ item_class or "dataset-item" }}">
{% block package_item_content %}
<div class="dataset-content">
<h3 class="dataset-heading">
{% if package.private and not owner and not acquired%}
<span class="dataset-private label label-inverse">
<i class="icon-lock fa fa-lock"></i>
{{ _('Private') }}
</span>
{% endif %}
{% if acquired and not owner %}
<span class="dataset-private label label-acquired">
<i class="icon-shopping-cart fa fa-shopping-cart"></i>
{{ _('Acquired') }}
</span>
{% endif %}
{% if owner %}
<span class="dataset-private label label-owner">
<i class="icon-user fa fa-user"></i>
{{ _('Owner') }}
</span>
{% endif %}
<!-- Customizations Acquire Button -->
{% if package.private and not owner and not acquired %}
{{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }}
<div class="divider"/>
{{ h.acquire_button(package) }}
{% else %}
{{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }}
{% endif %}
<!-- End of customizations Acquire Button -->
{% if package.get('state', '').startswith('draft') %}
<span class="label label-info">{{ _('Draft') }}</span>
{% elif package.get('state', '').startswith('deleted') %}
<span class="label label-important">{{ _('Deleted') }}</span>
{% endif %}
{{ h.popular('recent views', package.tracking_summary.recent, min=10) if package.tracking_summary }}
</h3>
{% if banner %}
<span class="banner">{{ _('Popular') }}</span>
{% endif %}
{% if notes %}
<div>{{ notes|urlize }}</div>
{% endif %}
</div>
{% if package.resources and not hide_resources %}
<ul class="dataset-resources unstyled">
{% for resource in h.dict_list_reduce(package.resources, 'format') %}
<li>
<a href="{{ h.url_for(controller='package', action='read', id=package.name) }}" class="label" data-format="{{ resource.lower() }}">{{ resource }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}
</li>

View File

@ -0,0 +1,49 @@
{% extends "user/edit_base.html" %}
{% set user = g.userobj %}
{% block breadcrumb_content %}
<li class="active"><a href="{{ h.url_for('dashboard.index') }}">{{ _('Dashboard') }}</a></li>
{% endblock %}
{% block secondary %}{% endblock %}
{% block primary %}
<article class="module">
{% block page_header %}
<header class="module-content page-header hug">
<div class="content_action">
{% link_for _('Edit settings'), named_route='user.edit', id=user.name, class_='btn btn-default', icon='cog' %}
</div>
<ul class="nav nav-tabs">
{{ h.build_nav_icon('dashboard.index', _('News feed')) }}
{{ h.build_nav_icon('dashboard.datasets', _('My Datasets')) }}
{{ h.build_nav_icon('privatedatasets.acquired_datasets', _('Acquired Datasets')) }}
{{ h.build_nav_icon('dashboard.organizations', _('My Organizations')) }}
{{ h.build_nav_icon('dashboard.groups', _('My Groups')) }}
</ul>
</header>
{% endblock %}
<div class="module-content">
{% if self.page_primary_action() | trim %}
<div class="page_primary_action">
{% block page_primary_action %}{% endblock %}
</div>
{% endif %}
{% block primary_content_inner %}
<div data-module="dashboard">
{% snippet 'user/snippets/followee_dropdown.html', context=dashboard_activity_stream_context, followees=followee_list %}
<h2 class="page-heading">
{% block page_heading %}
{{ _('News feed') }}
{% endblock %}
<small>{{ _("Activity from items that I'm following") }}</small>
</h2>
{% block activity_stream %}
{{ dashboard_activity_stream|safe }}
{% endblock %}
</div>
{% endblock %}
</div>
</article>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "user/dashboard.html" %}
{% block dashboard_activity_stream_context %}{% endblock %}
{% block page_primary_action %}
{% link_for _('Acquire Dataset'), controller='package', action='search', class_="btn btn-primary", icon="shopping-cart" %}
{% endblock %}
{% block primary_content_inner %}
<h2 class="hide-heading">{{ _('Acquired Datasets') }}</h2>
{% if acquired_datasets %}
{% snippet 'snippets/package_list.html', packages=acquired_datasets %}
{% else %}
<p class="empty">
{{ _('You haven\'t acquired any datasets.') }}
{% link_for _('Acquire one now?'), controller='package', action='search' %}
</p>
{% endif %}
{% endblock %}

View File

@ -21,7 +21,7 @@ import ckanext.privatedatasets.actions as actions
import unittest
from mock import MagicMock
from nose_parameterized import parameterized
from parameterized import parameterized
PARSER_CONFIG_PROP = 'ckan.privatedatasets.parser'
IMPORT_ERROR_MSG = 'Unable to load the module'
@ -34,9 +34,6 @@ class ActionsTest(unittest.TestCase):
def setUp(self):
# Load the mocks
self._config = actions.config
actions.config = MagicMock()
self._importlib = actions.importlib
actions.importlib = MagicMock()
@ -48,7 +45,6 @@ class ActionsTest(unittest.TestCase):
def tearDown(self):
# Unmock
actions.config = self._config
actions.importlib = self._importlib
actions.plugins = self._plugins
actions.db = self._db
@ -64,10 +60,9 @@ class ActionsTest(unittest.TestCase):
])
def test_class_cannot_be_loaded(self, class_path, class_name, path_exist, class_exist, expected_error):
class_package = class_path
class_package += ':' + class_name if class_name else ''
actions.config = {PARSER_CONFIG_PROP: class_package}
actions.plugins.toolkit.config = {PARSER_CONFIG_PROP: class_package}
# Recover exception
actions.plugins.toolkit.ValidationError = self._plugins.toolkit.ValidationError
@ -79,8 +74,7 @@ class ActionsTest(unittest.TestCase):
actions.importlib.import_module = MagicMock(side_effect=ImportError(IMPORT_ERROR_MSG) if not path_exist else None,
return_value=package if path_exist else None)
# Call the function
if expected_error:
with self.assertRaises(actions.plugins.toolkit.ValidationError) as cm:
actions.package_acquired({}, {})
@ -95,7 +89,7 @@ class ActionsTest(unittest.TestCase):
def configure_mocks(self, parse_result, datasets_not_found=[], not_updatable_datasets=[],
allowed_users=None, creator_user={'id': '1234', 'name': 'ckan'}):
actions.config = {PARSER_CONFIG_PROP: 'valid.path:%s' % CLASS_NAME}
actions.plugins.toolkit.config = {PARSER_CONFIG_PROP: 'valid.path:%s' % CLASS_NAME}
# Configure mocks
parser_instance = MagicMock()
@ -163,18 +157,14 @@ class ActionsTest(unittest.TestCase):
])
def test_add_users(self, users_info, datasets_not_found, not_updatable_datasets, allowed_users=[]):
parse_result = {'users_datasets': []}
parse_result = {'users_datasets': [{'user': user, 'datasets': users_info[user]} for user in users_info]}
creator_user = {'name': 'ckan', 'id': '1234'}
# Transform user_info
for user in users_info:
parse_result['users_datasets'].append({'user': user, 'datasets': users_info[user]})
parse_notification, package_show, package_update, user_show = self.configure_mocks(parse_result,
datasets_not_found, not_updatable_datasets, allowed_users, creator_user)
# Call the function
context = {'user': 'user1', 'model': 'model', 'auth_obj': {'id': 1}}
context = {'user': 'user1', 'model': 'model', 'auth_obj': {'id': 1}, 'method': 'grant'}
result = actions.package_acquired(context, users_info)
# Calculate the list of warns
@ -295,3 +285,80 @@ class ActionsTest(unittest.TestCase):
expected_acquired_datasets.append(pkg)
self.assertEquals(expected_acquired_datasets, result)
@parameterized.expand([
# Simple Test: one user and one dataset
({'user1': ['ds1']}, [], [], None), #Test with and non-existing list of allowed users
({'user2': ['ds1']}, [], [], []), #Test remove a non-existing user
({'user3': ['ds1']}, [], [], ['user3']), #Test remove an existing user
({'user4': ['ds1']}, [], [], ['another_user']), #Test remove non-existing from an already populated list
({'user5': ['ds1']}, [], [], ['another_user', 'user5']),
({'user6': ['ds1']}, ['ds1'], [], None), #Test remove from an unknown place
({'user61': ['ds1']}, ['ds1'], [], []),
({'user62': ['ds1']}, ['ds1'], [], ['user6']),
({'user7': ['ds1']}, [], ['ds1'], []), #Tests deleting from a public dataset
({'user8': ['ds1']}, [], ['ds1'], ['another_user']),
({'user9': ['ds1']}, [], ['ds1'], ['another_user', 'another_one']),
({'user91': ['ds1']}, ['ds1'], ['ds1'], ['another_user', 'another_one']), # Checking the behaviour when the unknown dataset is public
({'user92': ['ds1']}, ['ds1'], ['ds1'], ['another_user', 'another_one','user92']),
# Complex test: some users and some datasets
({'user1': ['ds1', 'ds2', 'ds3', 'ds4'], 'user2': ['ds5', 'ds6', 'ds7']}, ['ds3', 'ds6'], ['ds4', 'ds7'], []),
({'user3': ['ds1', 'ds2', 'ds3', 'ds4'], 'user4': ['ds5', 'ds6', 'ds7']}, ['ds3', 'ds6'], ['ds4', 'ds7'], ['another_user']),
({'user5': ['ds1', 'ds2', 'ds3', 'ds4'], 'user6': ['ds5', 'ds6', 'ds7']}, ['ds3', 'ds6'], ['ds4', 'ds7'], ['another_user', 'another_one']),
({'user7': ['ds1', 'ds2', 'ds3', 'ds4'], 'user8': ['ds5', 'ds6', 'ds7']}, ['ds3', 'ds6'], ['ds4', 'ds7'], ['another_user', 'another_one', 'user7'])
])
def test_delete_users(self, users_info, datasets_not_found, not_updatable_datasets, allowed_users=[]):
parse_result = {'users_datasets': []}
creator_user = {'name': 'ckan', 'id': '1234'}
# Transform user_info
for user in users_info:
parse_result['users_datasets'].append({'user': user, 'datasets': users_info[user]})
parse_delete, package_show, package_update, user_show = self.configure_mocks(parse_result,
datasets_not_found, not_updatable_datasets, allowed_users, creator_user)
# Call the function
context = {'user': 'user1', 'model': 'model', 'auth_obj': {'id': 1}, 'method': 'revoke'}
result = actions.revoke_access(context, users_info)
# Calculate the list of warns
warns = []
for user_datasets in parse_result['users_datasets']:
for dataset_id in user_datasets['datasets']:
if dataset_id in datasets_not_found:
warns.append('Dataset %s was not found in this instance' % dataset_id)
elif dataset_id in not_updatable_datasets:
# warns.append('%s(%s): %s' % (dataset_id, 'allowed_users', ADD_USERS_ERROR))
warns.append('Unable to upload the dataset %s: It\'s a public dataset' % dataset_id)
expected_result = {'warns': warns} if len(warns) > 0 else None
# Check that the returned result is as expected
self.assertEquals(expected_result, result)
# Check that the initial functions (check_access and parse_notification) has been called properly
parse_delete.assert_called_once_with(users_info)
actions.plugins.toolkit.check_access.assert_called_once_with('revoke_access', context, users_info)
for user_datasets in parse_result['users_datasets']:
for dataset_id in user_datasets['datasets']:
# The show function is always called
context_show = context.copy()
context_show['ignore_auth'] = True
context_show['updating_via_cb'] = True
package_show.assert_any_call(context_show, {'id': dataset_id})
# The update function is called only when the show function does not throw an exception and
# when the user is not in the list of allowed users.
if dataset_id not in datasets_not_found and allowed_users is not None and user_datasets['user'] in allowed_users and dataset_id not in not_updatable_datasets:
# Calculate the list of allowed_users
expected_allowed_users = list(allowed_users)
expected_allowed_users.remove(user_datasets['user'])
context_update = context.copy()
context_update['ignore_auth'] = True
context_update['user'] = creator_user['name']
package_update.assert_any_call(context_update, {'id': dataset_id, 'allowed_users': expected_allowed_users, 'private': True, 'creator_user_id': creator_user['id']})

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid
# Copyright (c) 2014 - 2017 CoNWeT Lab., Universidad Politécnica de Madrid
# This file is part of CKAN Private Dataset Extension.
@ -21,7 +21,7 @@ import unittest
import ckanext.privatedatasets.auth as auth
from mock import MagicMock
from nose_parameterized import parameterized
from parameterized import parameterized
class AuthTest(unittest.TestCase):
@ -37,8 +37,8 @@ class AuthTest(unittest.TestCase):
self._helpers = auth.helpers
auth.helpers = MagicMock()
self._new_authz = auth.new_authz
auth.new_authz = MagicMock()
self._authz = auth.authz
auth.authz = MagicMock()
self._tk = auth.tk
auth.tk = MagicMock()
@ -50,10 +50,9 @@ class AuthTest(unittest.TestCase):
auth.logic_auth = self._logic_auth
auth.request = self._request
auth.helpers = self._helpers
auth.new_authz = self._new_authz
auth.authz = self._authz
auth.tk = self._tk
auth.db = self._db
if hasattr(self, '_package_show'):
auth.package_show = self._package_show
@ -66,12 +65,12 @@ class AuthTest(unittest.TestCase):
# Anonymous user (public)
(None, None, None, False, 'active', None, None, None, None, None, True),
# Anonymous user (private)
(None, None, None, True, 'active', None, None, None, None, '/', False),
(None, None, '', True, 'active', None, None, '', None, '/', False),
(None, None, None, True, 'active', None, None, None, None, '/', True),
(None, None, '', True, 'active', None, None, '', None, '/', True),
# Anonymous user (private). Buy URL not shown
(None, None, None, True, 'active', None, None, None, 'google.es', '/', False),
(None, None, None, True, 'active', None, None, None, 'google.es', '/', True),
# Anonymous user (private). Buy URL show
(None, None, None, True, 'active', None, None, None, 'google.es', '/dataset/testds', False),
(None, None, None, True, 'active', None, None, None, 'google.es', '/dataset/testds', True),
# The creator can always see the dataset
(1, 1, None, False, 'active', None, None, None, None, None, True),
(1, 1, None, True, 'active', 'conwet', None, None, None, None, True),
@ -79,25 +78,26 @@ class AuthTest(unittest.TestCase):
(1, 1, None, False, 'draft', None, None, None, None, None, True),
# Other user (no organizations)
(1, 2, 'test', False, 'active', None, None, None, None, None, True),
(1, 2, 'test', True, 'active', None, None, None, 'google.es', '/', False), # Buy MSG not shown
(1, 2, 'test', True, 'active', None, None, None, None, '/dataset/testds', False), # Buy MSG not shown
(1, 2, 'test', True, 'active', None, None, None, 'google.es', '/dataset/testds', False), # Buy MSG shown
(1, 2, 'test', False, 'draft', None, None, None, None, None, False),
(1, 2, 'test', True, 'active', None, None, None, 'google.es', '/', True), # Buy MSG not shown
(1, 2, 'test', True, 'active', None, None, None, None, '/dataset/testds', True), # Buy MSG not shown
(1, 2, 'test', True, 'active', None, None, None, 'google.es', '/dataset/testds', True), # Buy MSG shown
(1, 2, 'test', False, 'draft', None, None, None, None, None, False),
# Other user but authorized in the list of authorized users
(1, 2, 'test', True, 'active', None, None, True, None, None, True),
# Other user and not authorized in the list of authorized users
(1, 2, 'test', True, 'active', None, None, False, 'google.es', '/', False),
(1, 2, 'test', True, 'active', None, None, False, 'google.es', '/dataset/testds', False),
(1, 2, 'test', True, 'active', None, None, False, 'google.es', '/', True),
(1, 2, 'test', True, 'active', None, None, False, 'google.es', '/dataset/testds', True),
# Other user with organizations
(1, 2, 'test', False, 'active', 'conwet', False, None, None, None, True),
(1, 2, 'test', True, 'active', 'conwet', False, None, None, None, False),
(1, 2, 'test', True, 'active', 'conwet', False, None, None, None, True),
(1, 2, 'test', True, 'active', 'conwet', True, None, None, None, True),
(1, 2, 'test', True, 'draft', 'conwet', True, None, None, None, False),
# Other user with organizations (user is not in the organization)
(1, 2, 'test', True, 'active', 'conwet', False, True, None, None, True),
(1, 2, 'test', True, 'active', 'conwet', False, False, None, None, False),
(1, 2, 'test', True, 'active', 'conwet', False, False, 'google.es', '/dataset/testds', False),
(1, 2, 'test', True, 'active', 'conwet', False, False, 'google.es', '/', False) ])
(1, 2, 'test', True, 'active', 'conwet', False, False, None, None, True),
(1, 2, 'test', True, 'active', 'conwet', False, False, 'google.es', '/dataset/testds', True),
(1, 2, 'test', True, 'active', 'conwet', False, False, 'google.es', '/', True)
])
def test_auth_package_show(self, creator_user_id, user_obj_id, user, private, state, owner_org,
owner_member, db_auth, acquire_url, request_path, authorized):
@ -123,8 +123,8 @@ class AuthTest(unittest.TestCase):
returned_package.extras['acquire_url'] = acquire_url
auth.logic_auth.get_package_object = MagicMock(return_value=returned_package)
auth.new_authz.has_user_permission_for_group_or_org = MagicMock(return_value=owner_member)
auth.request.path = MagicMock(return_value=request_path)
auth.authz.has_user_permission_for_group_or_org = MagicMock(return_value=owner_member)
auth.request.path = request_path
# Prepare the context
context = {'model': MagicMock()}
@ -140,14 +140,14 @@ class AuthTest(unittest.TestCase):
# Check the result
self.assertEquals(authorized, result['success'])
# Premissions for organization are checked when the dataset is private, it belongs to an organization
# Permissions for organization are checked when the dataset is private, it belongs to an organization
# and when the dataset has not been created by the user who is asking for it
if private and owner_org and state == 'active' and creator_user_id != user_obj_id:
auth.new_authz.has_user_permission_for_group_or_org.assert_called_once_with(owner_org, user, 'read')
auth.authz.has_user_permission_for_group_or_org.assert_called_once_with(owner_org, user, 'read')
else:
self.assertEquals(0, auth.new_authz.has_user_permission_for_group_or_org.call_count)
self.assertEquals(0, auth.authz.has_user_permission_for_group_or_org.call_count)
# The databse is only initialized when:
# The database is only initialized when:
# * the dataset is private AND
# * the dataset is active AND
# * the dataset has no organization OR the user does not belong to that organization AND
@ -159,10 +159,10 @@ class AuthTest(unittest.TestCase):
self.assertEquals(0, auth.db.init_db.call_count)
# Conditions to buy a dataset; It should be private, active and should not belong to any organization
if not authorized and state == 'active' and not owner_org and request_path.startswith('/dataset/'):
auth.helpers.flash_error.assert_called_once()
if authorized and state == 'active' and request_path and request_path.startswith('/dataset/') and acquire_url:
auth.helpers.flash_notice.assert_called_once()
else:
self.assertEquals(0, auth.helpers.flash_error.call_count)
self.assertEquals(0, auth.helpers.flash_notice.call_count)
@parameterized.expand([
(None, None, None, None, None, False), # Anonymous user
@ -179,7 +179,7 @@ class AuthTest(unittest.TestCase):
returned_package.owner_org = owner_org
auth.logic_auth.get_package_object = MagicMock(return_value=returned_package)
auth.new_authz.has_user_permission_for_group_or_org = MagicMock(return_value=owner_member)
auth.authz.has_user_permission_for_group_or_org = MagicMock(return_value=owner_member)
# Prepare the context
context = {}
@ -198,59 +198,107 @@ class AuthTest(unittest.TestCase):
# Permissions for organization are checked when the user asking to update the dataset is not the creator
# and when the dataset has organization
if creator_user_id != user_obj_id and owner_org:
auth.new_authz.has_user_permission_for_group_or_org.assert_called_once_with(owner_org, user, 'update_dataset')
auth.authz.has_user_permission_for_group_or_org.assert_called_once_with(owner_org, user, 'update_dataset')
else:
self.assertEquals(0, auth.new_authz.has_user_permission_for_group_or_org.call_count)
self.assertEquals(0, auth.authz.has_user_permission_for_group_or_org.call_count)
@parameterized.expand([
(True, True),
(True, False),
(False, False),
(False, False)
# if package dont exist
(False, None, None, None, False, 'active', None, None, None, False),
# Anonymous user only can view resources of a public and active package
(True, None, None, None, False, 'active', None, None, None, True),
(True, None, None, None, True, 'active', None, None, None, False),
(True, None, None, '', True, 'active', None, None, None, False),
# The creator can always see the resource
(True, 1, 1, None, False, 'active', None, None, None, True),
(True, 1, 1, None, True, 'active', None, None, None, True),
(True, 1, 1, None, True, 'draft', None, None, None, True),
(True, 1, 1, None, False, 'draft', None, None, None, True),
# Other user (no organizations)
(True, 1, 2, 'test', False, 'active', None, None, None, True),
(True, 1, 2, 'test', True, 'active', None, None, None, False),
(True, 1, 2, 'test', True, 'draft', None, None, None, False),
# Other user but authorized in the list of authorized users
(True, 1, 2, 'test', True, 'active', None, None, True, True),
# Other user and not authorized in the list of authorized users
(True, 1, 2, 'test', True, 'active', None, None, False, False),
# Other user with organizations
(True, 1, 2, 'test', True, 'active', 'conwet', False, None, False),
(True, 1, 2, 'test', True, 'active', 'conwet', True, None, True),
])
def test_auth_resource_show(self, exist_pkg=True, authorized_pkg=True):
def test_auth_resource_show(self, exist_pkg, creator_user_id, user_obj_id, user, private, state, owner_org,
owner_member, db_auth, authorized):
#Recover the exception
auth.tk.ObjectNotFound = self._tk.ObjectNotFound
# Mock the calls
package = MagicMock()
package.id = '1'
# Configure the mocks
if exist_pkg:
returned_package = MagicMock()
returned_package.creator_user_id = creator_user_id
returned_package.private = private
returned_package.state = state
returned_package.owner_org = owner_org
returned_package.extras = {}
else:
returned_package = None
final_query = MagicMock()
final_query.first = MagicMock(return_value=package if exist_pkg else None)
returned_resource = MagicMock()
returned_resource.package_id = 1
second_join = MagicMock()
second_join.filter = MagicMock(return_value=final_query)
# Configure the database
db_response = []
if db_auth is True:
out = auth.db.AllowedUser()
out.package_id = 'package_id'
out.user_name = user
db_response.append(out)
first_join = MagicMock()
first_join.join = MagicMock(return_value=second_join)
# Prepare the context
context = {'model': MagicMock()}
if user is not None:
context['user'] = user
if user_obj_id is not None:
context['auth_user_obj'] = MagicMock()
context['auth_user_obj'].id = user_obj_id
query = MagicMock()
query.join = MagicMock(return_value=first_join)
auth.db.AllowedUser.get = MagicMock(return_value=db_response)
auth.logic_auth.get_resource_object = MagicMock(return_value=returned_resource)
auth.logic_auth.get_package_object = MagicMock(return_value=returned_package)
auth.authz.has_user_permission_for_group_or_org = MagicMock(return_value=owner_member)
model = MagicMock()
session = MagicMock()
session.query = MagicMock(return_value=query)
model.Session = session
# Create the context
context = {}
context['model'] = model
# Mock the package_show function
self._package_show = auth.package_show
success = True if authorized_pkg else False
auth.package_show = MagicMock(return_value={'success': success})
# Prepare the context
context = {'model': MagicMock()}
if user is not None:
context['user'] = user
if user_obj_id is not None:
context['auth_user_obj'] = MagicMock()
context['auth_user_obj'].id = user_obj_id
if not exist_pkg:
self.assertRaises(self._tk.ObjectNotFound, auth.resource_show, context, {})
else:
result = auth.resource_show(context, {})
self.assertEquals(authorized_pkg, result['success'])
self.assertEquals(authorized, result['success'])
if private and owner_org and state == 'active' and creator_user_id != user_obj_id:
auth.authz.has_user_permission_for_group_or_org.assert_called_once_with(owner_org, user, 'read')
else:
self.assertEquals(0, auth.authz.has_user_permission_for_group_or_org.call_count)
if private and state == 'active' and (not owner_org or not owner_member) and (creator_user_id != user_obj_id or user_obj_id is None):
# Check that the database has been initialized properly
auth.db.init_db.assert_called_once_with(context['model'])
else:
self.assertEquals(0, auth.db.init_db.call_count)
def test_package_acquired(self):
self.assertTrue(auth.package_acquired({}, {})['success'])
def test_package_deleted(self):
self.assertTrue(auth.revoke_access({}, {})['success'])
@parameterized.expand([
({'user': 'user_1'}, {'user': 'user_1'}, True),
({'user': 'user_2'}, {'user': 'user_1'}, False),

View File

@ -1,110 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid
# This file is part of CKAN Private Dataset Extension.
# CKAN Private Dataset Extension is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# CKAN Private Dataset Extension 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 Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>.
import ckanext.privatedatasets.controllers.ui_controller as controller
import unittest
from mock import MagicMock, ANY
from nose_parameterized import parameterized
class UIControllerTest(unittest.TestCase):
def setUp(self):
# Get the instance
self.instanceUI = controller.AcquiredDatasetsControllerUI()
# Load the mocks
self._plugins = controller.plugins
controller.plugins = MagicMock()
self._model = controller.model
controller.model = MagicMock()
# Set exceptions
controller.plugins.toolkit.ObjectNotFound = self._plugins.toolkit.ObjectNotFound
controller.plugins.toolkit.NotAuthorized = self._plugins.toolkit.NotAuthorized
def tearDown(self):
# Unmock
controller.plugins = self._plugins
controller.model = self._model
@parameterized.expand([
(controller.plugins.toolkit.ObjectNotFound, 404),
(controller.plugins.toolkit.NotAuthorized, 401)
])
def test_exceptions_loading_users(self, exception, expected_status):
# Configure the mock
user_show = MagicMock(side_effect=exception)
controller.plugins.toolkit.get_action = MagicMock(return_value=user_show)
# Call the function
self.instanceUI.user_acquired_datasets()
# Assertations
expected_context = {
'model': controller.model,
'session': controller.model.Session,
'user': controller.plugins.toolkit.c.user
}
user_show.assert_called_once_with(expected_context, {'user_obj': controller.plugins.toolkit.c.userobj})
controller.plugins.toolkit.abort.assert_called_once_with(expected_status, ANY)
def test_no_error_loading_users(self):
user = 'example_user_test'
controller.plugins.toolkit.c.user = user
# actions
default_user = {'user_name': 'test', 'another_val': 'example value'}
user_show = MagicMock(return_value=default_user)
acquisitions_list = MagicMock()
def _get_action(action):
if action == 'user_show':
return user_show
else:
return acquisitions_list
controller.plugins.toolkit.get_action = MagicMock(side_effect=_get_action)
# Call the function
returned = self.instanceUI.user_acquired_datasets()
# User_show called correctly
expected_context = {
'model': controller.model,
'session': controller.model.Session,
'user': controller.plugins.toolkit.c.user
}
user_show.assert_called_once_with(expected_context, {'user_obj': controller.plugins.toolkit.c.userobj})
# Query called correctry
expected_user = default_user.copy()
expected_user['acquired_datasets'] = acquisitions_list.return_value
acquisitions_list.assert_called_with(expected_context, None)
self.assertEquals(expected_user, controller.plugins.toolkit.c.user_dict)
# Check that the render method has been called and that its result has been returned
self.assertEquals(controller.plugins.toolkit.render.return_value, returned)
controller.plugins.toolkit.render.assert_called_once_with('user/dashboard_acquired.html')

View File

@ -21,7 +21,7 @@ import unittest
import ckanext.privatedatasets.converters_validators as conv_val
from mock import MagicMock
from nose_parameterized import parameterized
from parameterized import parameterized
class ConvertersValidatorsTest(unittest.TestCase):
@ -127,10 +127,6 @@ class ConvertersValidatorsTest(unittest.TestCase):
def test_allowed_user_convert(self, users, previous_users, expected_users):
key_str = 'allowed_users_str'
key = 'allowed_users'
# Configure mock
name_validator = MagicMock()
conv_val.toolkit.get_validator = MagicMock(return_value=name_validator)
# Fullfill the data dictionary
# * list should be included in the allowed_users filed
@ -151,7 +147,6 @@ class ConvertersValidatorsTest(unittest.TestCase):
# Check that the users are set properly
for i in range(previous_users, previous_users + len(expected_users)):
name_validator.assert_any_call(expected_users[i - previous_users], context)
self.assertEquals(expected_users[i - previous_users], data[(key, i)])
@parameterized.expand([

View File

@ -21,7 +21,7 @@ import unittest
import ckanext.privatedatasets.parsers.fiware as fiware
from mock import MagicMock
from nose_parameterized import parameterized
from parameterized import parameterized
TEST_CASES = {
@ -39,26 +39,26 @@ TEST_CASES = {
'error': {
'host': 'localhost',
'json': {"customer_name": "test", "resources": [{"url": "http://localhosta/dataset/ds1"}]},
'error': 'Dataset ds1 is associated with the CKAN instance located at localhosta',
'error': 'Dataset ds1 is associated with the CKAN instance located at localhosta, expected localhost',
},
'error_one_ds': {
'host': 'localhost',
'json': {"customer_name": "test", "resources": [{"url": "http://localhosta/dataset/ds1"},
{"url": "http://localhost/dataset/ds2"}]},
'error': 'Dataset ds1 is associated with the CKAN instance located at localhosta',
'error': 'Dataset ds1 is associated with the CKAN instance located at localhosta, expected localhost',
},
'two_errors': {
'host': 'localhost',
'json': {"customer_name": "test", "resources": [{"url": "http://localhosta/dataset/ds1"},
{"url": "http://localhostb/dataset/ds2"}]},
'error': 'Dataset ds1 is associated with the CKAN instance located at localhosta',
'error': 'Dataset ds1 is associated with the CKAN instance located at localhosta, expected localhost',
},
'two_errors_two_ds': {
'host': 'example.com',
'json': {"customer_name": "test", "resources": [{"url": "http://localhosta/dataset/ds1"},
{"url": "http://example.es/dataset/ds2"}, {"url": "http://example.com/dataset/ds3"},
{"url": "http://example.com/dataset/ds4"}]},
'error': 'Dataset ds1 is associated with the CKAN instance located at localhosta',
'error': 'Dataset ds1 is associated with the CKAN instance located at localhosta, expected example.com',
},
'no_customer_name': {
'host': 'localhost',

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid
# Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L.
# This file is part of CKAN Private Dataset Extension.
@ -20,8 +21,8 @@
import unittest
import ckanext.privatedatasets.helpers as helpers
from mock import MagicMock
from nose_parameterized import parameterized
from mock import patch, MagicMock
from parameterized import parameterized
class HelpersTest(unittest.TestCase):
@ -33,18 +34,19 @@ class HelpersTest(unittest.TestCase):
self._tk = helpers.tk
helpers.tk = MagicMock()
helpers.tk.config = {}
self._db = helpers.db
helpers.db = MagicMock()
self._config = helpers.config
helpers.config = {}
self._request = helpers.request
helpers.request = MagicMock()
def tearDown(self):
helpers.model = self._model
helpers.tk = self._tk
helpers.db = self._db
helpers.config = self._config
helpers.request = self._request
@parameterized.expand([
(False, 'user', False),
@ -125,25 +127,80 @@ class HelpersTest(unittest.TestCase):
helpers.tk.check_access.assert_called_once_with('package_show', context, package)
@parameterized.expand([
(None, False),
('True', True),
('False', False)
(None, False, None),
('True', True, None),
('False', False, None),
('afa ', False, None),
(True, True, None),
(False, False, None),
(False, True , 'true'),
(False, True , 'on'),
(False, True , '1'),
(True, False, '0'),
(True, False, 'off'),
(True, False, 'fAlsE'),
(True, False, 'fAlsE'),
])
def test_show_acquire_url_on_create(self, config_value, expected_value):
@patch("ckanext.privatedatasets.helpers.os.environ", new={})
def test_show_acquire_url_on_create(self, config_value, expected_value, env_val):
# {} is shared between tests, so we have clear it each time
helpers.os.environ.clear()
if config_value is not None:
helpers.config['ckan.privatedatasets.show_acquire_url_on_create'] = config_value
helpers.tk.config['ckan.privatedatasets.show_acquire_url_on_create'] = config_value
if env_val:
helpers.os.environ['CKAN_PRIVATEDATASETS_SHOW_ACQUIRE_URL_ON_CREATE'] = env_val
# Call the function
self.assertEquals(expected_value, helpers.show_acquire_url_on_create())
@parameterized.expand([
(None, False),
('True', True),
('False', False)
(None, False, None),
('True', True, None),
(' tRUe', True, None),
('False', False, None),
(True, True, None),
(False, False, None),
(False, True , 'trUe'),
(False, True , 'on'),
(False, True , '1'),
(True, False, '0'),
(True, False, 'off'),
(True, False, 'fAlsE'),
(True, False, 'potato'),
])
def test_show_acquire_url_on_edit(self, config_value, expected_value):
@patch("ckanext.privatedatasets.helpers.os.environ", new={})
def test_show_acquire_url_on_edit(self, config_value, expected_value, env_val):
# {} is shared between tests, so we have clear it each time
helpers.os.environ.clear()
if config_value is not None:
helpers.config['ckan.privatedatasets.show_acquire_url_on_edit'] = config_value
helpers.tk.config['ckan.privatedatasets.show_acquire_url_on_edit'] = config_value
if env_val:
helpers.os.environ['CKAN_PRIVATEDATASETS_SHOW_ACQUIRE_URL_ON_EDIT'] = env_val
# Call the function
self.assertEquals(expected_value, helpers.show_acquire_url_on_edit())
@parameterized.expand([
({}, '/dataset', False),
({'acquire_url': 'http://fiware.org'}, '/dataset', True),
({'acquire_url': ''}, '/dataset', False),
({'acquire_url': 'http://fiware.org'}, '/user', False),
])
def test_acquire_button(self, package, path, button_expected):
# Mocking
helpers.request.path = path
# Call the function and check response
result = helpers.acquire_button(package)
if button_expected:
helpers.tk.render_snippet.assert_called_once_with('snippets/acquire_button.html',
{'url_dest': package['acquire_url']})
self.assertEquals(result, helpers.tk.render_snippet.return_value)
else:
self.assertEquals(result, '')

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid
# Copyright (c) 2014 - 2017 CoNWeT Lab., Universidad Politécnica de Madrid
# This file is part of CKAN Private Dataset Extension.
@ -19,10 +19,12 @@
import unittest
import copy
import ckanext.privatedatasets.plugin as plugin
from flask import Blueprint
from mock import MagicMock
from nose_parameterized import parameterized
from parameterized import parameterized
import ckanext.privatedatasets.plugin as plugin
class PluginTest(unittest.TestCase):
@ -51,7 +53,7 @@ class PluginTest(unittest.TestCase):
(plugin.p.IDatasetForm,),
(plugin.p.IAuthFunctions,),
(plugin.p.IConfigurer,),
(plugin.p.IRoutes,),
(plugin.p.IBlueprint,),
(plugin.p.IActions,),
(plugin.p.IPackageController,),
(plugin.p.ITemplateHelpers,)
@ -62,9 +64,10 @@ class PluginTest(unittest.TestCase):
@parameterized.expand([
('package_show', plugin.auth.package_show),
('package_update', plugin.auth.package_update),
('package_show', plugin.auth.package_show),
('resource_show', plugin.auth.resource_show),
('package_acquired', plugin.auth.package_acquired),
('acquisitions_list', plugin.auth.acquisitions_list)
('acquisitions_list', plugin.auth.acquisitions_list),
('revoke_access', plugin.auth.revoke_access)
])
def test_auth_function(self, function_name, expected_function):
auth_functions = self.privateDatasets.get_auth_functions()
@ -76,22 +79,20 @@ class PluginTest(unittest.TestCase):
self.privateDatasets.update_config(config)
# Test that functions are called as expected
plugin.tk.add_template_directory.assert_called_once_with(config, 'templates')
if self._tk.check_ckan_version(min_version='2.8'):
plugin.tk.add_template_directory.assert_called_once_with(config, 'templates_2.8')
else:
plugin.tk.add_template_directory.assert_called_once_with(config, 'templates')
plugin.tk.add_resource('fanstatic', 'privatedatasets')
def test_map(self):
def test_get_blueprint(self):
# Call the method
m = MagicMock()
self.privateDatasets.before_map(m)
# Test that the connect method has been called
m.connect.assert_any_call('user_acquired_datasets', '/dashboard/acquired', ckan_icon='shopping-cart',
controller='ckanext.privatedatasets.controllers.ui_controller:AcquiredDatasetsControllerUI',
action='user_acquired_datasets', conditions=dict(method=['GET']))
self.assertIsInstance(self.privateDatasets.get_blueprint(), Blueprint)
@parameterized.expand([
('package_acquired', plugin.actions.package_acquired),
('acquisitions_list', plugin.actions.acquisitions_list)
('acquisitions_list', plugin.actions.acquisitions_list),
('revoke_access', plugin.actions.revoke_access)
])
def test_actions_function(self, function_name, expected_function):
actions = self.privateDatasets.get_actions()
@ -208,41 +209,73 @@ class PluginTest(unittest.TestCase):
self.assertTrue(found)
@parameterized.expand([
(True, 1, 1, False, True, True),
(True, 1, 2, False, True, True),
(True, 1, 1, True, True, True),
(True, 1, 2, True, True, True),
(True, 1, None, None, True, True),
(True, 1, 1, None, True, True),
(True, 1, None, True, True, True),
(True, 1, None, False, True, True),
(False, 1, 1, False, True, True),
(False, 1, 2, False, True, False),
(False, 1, 1, True, True, True),
(False, 1, 2, True, True, True),
(False, 1, None, None, True, False),
(False, 1, 1, None, True, True),
(False, 1, None, True, True, True),
(False, 1, None, False, True, False),
(True, 1, 1, False, False, False),
(True, 1, 2, False, False, False),
(True, 1, 1, True, False, False),
(True, 1, 2, True, False, False),
(True, 1, None, None, False, False),
(True, 1, 1, None, False, False),
(True, 1, None, True, False, False),
(True, 1, None, False, False, False),
(False, 1, 1, False, False, False),
(False, 1, 2, False, False, False),
(False, 1, 1, True, False, False),
(False, 1, 2, True, False, False),
(False, 1, None, None, False, False),
(False, 1, 1, None, False, False),
(False, 1, None, True, False, False),
(False, 1, None, False, False, False),
(True, 1, 1, False, True, True, [{'id': 1}, {'id': 2}], True),
(True, 1, 2, False, True, True, [{'id': 1}, {'id': 2}], True),
(True, 1, 1, True, True, True, [{'id': 1}, {'id': 2}], True),
(True, 1, 2, True, True, True, [{'id': 1}, {'id': 2}], True),
(True, 1, None, None, True, True, [{'id': 1}, {'id': 2}], True),
(True, 1, 1, None, True, True, [{'id': 1}, {'id': 2}], True),
(True, 1, None, True, True, True, [{'id': 1}, {'id': 2}], True),
(True, 1, None, False, True, True, [{'id': 1}, {'id': 2}], True),
(False, 1, 1, False, True, True, [{'id': 1}, {'id': 2}], True),
(False, 1, 2, False, True, False, [{'id': 1}, {'id': 2}], True),
(False, 1, 1, True, True, True, [{'id': 1}, {'id': 2}], True),
(False, 1, 2, True, True, True, [{'id': 1}, {'id': 2}], True),
(False, 1, None, None, True, False, [{'id': 1}, {'id': 2}], True),
(False, 1, 1, None, True, True, [{'id': 1}, {'id': 2}], True),
(False, 1, None, True, True, True, [{'id': 1}, {'id': 2}], True),
(False, 1, None, False, True, False, [{'id': 1}, {'id': 2}], True),
(True, 1, 1, False, False, False, [{'id': 1}, {'id': 2}], True),
(True, 1, 2, False, False, False, [{'id': 1}, {'id': 2}], True),
(True, 1, 1, True, False, False, [{'id': 1}, {'id': 2}], True),
(True, 1, 2, True, False, False, [{'id': 1}, {'id': 2}], True),
(True, 1, None, None, False, False, [{'id': 1}, {'id': 2}], True),
(True, 1, 1, None, False, False, [{'id': 1}, {'id': 2}], True),
(True, 1, None, True, False, False, [{'id': 1}, {'id': 2}], True),
(True, 1, None, False, False, False, [{'id': 1}, {'id': 2}], True),
(False, 1, 1, False, False, False, [{'id': 1}, {'id': 2}], True),
(False, 1, 2, False, False, False, [{'id': 1}, {'id': 2}], True),
(False, 1, 1, True, False, False, [{'id': 1}, {'id': 2}], True),
(False, 1, 2, True, False, False, [{'id': 1}, {'id': 2}], True),
(False, 1, None, None, False, False, [{'id': 1}, {'id': 2}], True),
(False, 1, 1, None, False, False, [{'id': 1}, {'id': 2}], True),
(False, 1, None, True, False, False, [{'id': 1}, {'id': 2}], True),
(False, 1, None, False, False, False, [{'id': 1}, {'id': 2}], True),
(True, 1, 1, False, True, True, [{}, {}], False),
(True, 1, 2, False, True, True, [{}, {}], False),
(True, 1, 1, True, True, True, [{}, {}], False),
(True, 1, 2, True, True, True, [{}, {}], False),
(True, 1, None, None, True, True, [{}, {}], False),
(True, 1, 1, None, True, True, [{}, {}], False),
(True, 1, None, True, True, True, [{}, {}], False),
(True, 1, None, False, True, True, [{}, {}], False),
(False, 1, 1, False, True, True, [{}, {}], False),
(False, 1, 2, False, True, False, [{}, {}], False),
(False, 1, 1, True, True, True, [{}, {}], False),
(False, 1, 2, True, True, True, [{}, {}], False),
(False, 1, None, None, True, False, [{}, {}], False),
(False, 1, 1, None, True, True, [{}, {}], False),
(False, 1, None, True, True, True, [{}, {}], False),
(False, 1, None, False, True, False, [{}, {}], False),
(True, 1, 1, False, False, False, [{}, {}], False),
(True, 1, 2, False, False, False, [{}, {}], False),
(True, 1, 1, True, False, False, [{}, {}], False),
(True, 1, 2, True, False, False, [{}, {}], False),
(True, 1, None, None, False, False, [{}, {}], False),
(True, 1, 1, None, False, False, [{}, {}], False),
(True, 1, None, True, False, False, [{}, {}], False),
(True, 1, None, False, False, False, [{}, {}], False),
(False, 1, 1, False, False, False, [{}, {}], False),
(False, 1, 2, False, False, False, [{}, {}], False),
(False, 1, 1, True, False, False, [{}, {}], False),
(False, 1, 2, True, False, False, [{}, {}], False),
(False, 1, None, None, False, False, [{}, {}], False),
(False, 1, 1, None, False, False, [{}, {}], False),
(False, 1, None, True, False, False, [{}, {}], False),
(False, 1, None, False, False, False, [{}, {}], False),
])
def test_packagecontroller_after_show(self, update_via_api, creator_id, user_id, sysadmin, private, fields_expected):
def test_packagecontroller_after_show(self, update_via_api, creator_id, user_id, sysadmin, private, fields_expected, resources, resources_fields):
context = {'updating_via_cb': update_via_api}
if creator_id is not None or sysadmin is not None:
@ -251,7 +284,7 @@ class PluginTest(unittest.TestCase):
user.sysadmin = sysadmin
context['auth_user_obj'] = user
pkg_dict = {'creator_user_id': creator_id, 'allowed_users': ['a', 'b', 'c'], 'searchable': True, 'acquire_url': 'http://google.es', 'private': private}
pkg_dict = {'creator_user_id': creator_id, 'allowed_users': ['a', 'b', 'c'], 'searchable': True, 'acquire_url': 'http://google.es', 'private': private, 'resources': resources, 'num_resources': 2}
# Call the function
result = self.privateDatasets.after_show(context, pkg_dict) # Call the function
@ -264,6 +297,14 @@ class PluginTest(unittest.TestCase):
else:
self.assertFalse(field in result)
fields = ['resources', 'num_resources']
for field in fields:
if resources_fields:
self.assertTrue(field in result)
else:
self.assertFalse(field in result)
@parameterized.expand([
('public', None, 'public'),
('public', 'False', 'private'),
@ -286,11 +327,20 @@ class PluginTest(unittest.TestCase):
package_id = 'package_id'
# Configure mocks
default_dict = {'a': '0', 'b': 1, 'm': True, 'revision_timestamp': '2000'}
revision = {'timestamp': '7888'}
default_dict = {'a': '0', 'b': 1, 'm': True, 'revision_id': 'revision_id_uuidv4'}
expected_dict = default_dict.copy()
expected_dict['metadata_modified'] = default_dict['revision_timestamp']
expected_dict['metadata_modified'] = revision['timestamp']
package_show = MagicMock(return_value=default_dict.copy())
plugin.tk.get_action = MagicMock(return_value=package_show)
revision_show = MagicMock(return_value=revision.copy())
def _get_action(action):
if action == 'package_show':
return package_show
elif action == 'revision_show':
return revision_show
plugin.tk.get_action = MagicMock(side_effect=_get_action)
# Each time 'AllowedUser' is called, we must get a new instance
# and this is the way to get this behaviour
@ -433,3 +483,40 @@ class PluginTest(unittest.TestCase):
self.assertEquals(final_search_results['facets'], search_results['facets'])
self.assertEquals(final_search_results['elements'], search_results['elements'])
@parameterized.expand([
(True,),
(False,)
])
def test_package_controller_before_view(self, user_allowed):
pkg_dict = {'resources': [{'id': 1}, {'id': 2}, {'id': 3}]}
pkg_dict_not_allowed = {'resources': []}
plugin.tk.check_access.side_effect = None if user_allowed else plugin.tk.NotAuthorized
result = self.privateDatasets.before_view(pkg_dict)
if user_allowed:
self.assertEquals(result['resources'], pkg_dict['resources'])
else:
self.assertEquals(result['resources'], pkg_dict_not_allowed['resources'])
@parameterized.expand([
(True,),
(False,)
])
def test_resource_controller_before_show(self, user_allowed):
resource_dict = {'id': 1, 'resource_name': 'resource_test'}
plugin.tk.check_access.side_effect = None if user_allowed else plugin.tk.NotAuthorized
result = self.privateDatasets.before_show(resource_dict)
if user_allowed:
self.assertEquals(result['id'], resource_dict['id'])
self.assertEquals(result['resource_name'], resource_dict['resource_name'])
else:
self.assertNotIn('id', result)
self.assertNotIn('resource_name', result)

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid
# Copyright (c) 2014 - 2017 CoNWeT Lab., Universidad Politécnica de Madrid
# Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L.
# This file is part of CKAN Private Dataset Extension.
@ -17,21 +18,28 @@
# You should have received a copy of the GNU Affero General Public License
# along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>.
from nose_parameterized import parameterized
from selenium import webdriver
from selenium.webdriver.support.ui import Select
from subprocess import Popen
from __future__ import unicode_literals, print_function
import ckan.lib.search.index as search_index
import ckan.model as model
import ckanext.privatedatasets.db as db
import json
import os
import unittest
import re
import requests
from subprocess import Popen
import time
import ckan.lib.search.index as search_index
import ckan.model as model
from parameterized import parameterized
import requests
from selenium import webdriver
from selenium.common.exceptions import NoAlertPresentException
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import Select, WebDriverWait
import ckanext.privatedatasets.db as db
def get_dataset_url(dataset_name):
return dataset_name.replace(' ', '-').lower()
@ -41,14 +49,20 @@ class TestSelenium(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Run CKAN
env = os.environ.copy()
env['DEBUG'] = 'True'
env['OAUTHLIB_INSECURE_TRANSPORT'] = 'True'
env['DEBUG'] = 'False'
cls._process = Popen(['paster', 'serve', 'test.ini'], env=env)
# Init Selenium
cls.driver = webdriver.Firefox()
cls.base_url = 'http://localhost:5000/'
cls.driver.set_window_size(1024, 768)
@classmethod
def tearDownClass(cls):
cls._process.terminate()
cls.driver.quit()
def clearBBDD(self):
# Clean Solr
@ -67,55 +81,68 @@ class TestSelenium(unittest.TestCase):
def setUp(self):
self.clearBBDD()
self.driver = webdriver.Firefox()
self.driver.implicitly_wait(5)
self.driver.set_window_size(1024, 768)
self.base_url = 'http://127.0.0.1:5000/'
def tearDown(self):
self.driver.get(self.base_url)
try: # pragma: no cover
# Accept any "Are you sure to leave?" alert
self.driver.switch_to.alert.accept()
self.driver.switch_to.default_content()
except NoAlertPresentException:
pass
WebDriverWait(self.driver, 10).until(lambda driver: self.base_url == driver.current_url)
self.driver.delete_all_cookies()
self.clearBBDD()
self.driver.quit()
def assert_fields_disabled(self, fields):
for field in fields:
self.assertFalse(self.driver.find_element_by_id(field).is_enabled())
def logout(self):
self.driver.find_element_by_css_selector('i.icon-signout').click()
self.driver.delete_all_cookies()
self.driver.get(self.base_url)
def register(self, username, fullname, mail, password):
def register(self, username, fullname, mail):
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_link_text('Register').click()
driver.find_element_by_id('field-username').clear()
WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.ID, "field-username"))).clear()
driver.find_element_by_id('field-username').send_keys(username)
driver.find_element_by_id('field-fullname').clear()
driver.find_element_by_id('field-fullname').send_keys(fullname)
driver.find_element_by_id('field-email').clear()
driver.find_element_by_id('field-email').send_keys(mail)
driver.find_element_by_id('field-password').clear()
driver.find_element_by_id('field-password').send_keys(password)
driver.find_element_by_id('field-password').send_keys("1234" + username)
driver.find_element_by_id('field-confirm-password').clear()
driver.find_element_by_id('field-confirm-password').send_keys(password)
driver.find_element_by_id('field-confirm-password').send_keys("1234" + username)
driver.find_element_by_name('save').click()
self.logout()
def login(self, username, password):
def login(self, username):
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_link_text('Log in').click()
driver.find_element_by_id('field-login').clear()
login_btn = WebDriverWait(driver, 15).until(
EC.element_to_be_clickable((By.LINK_TEXT, 'Log in'))
)
login_btn.click()
WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.ID, "field-login"))).clear()
driver.find_element_by_id('field-login').send_keys(username)
driver.find_element_by_id('field-password').clear()
driver.find_element_by_id('field-password').send_keys(password)
driver.find_element_by_id('field-password').send_keys("1234" + username)
driver.find_element_by_id('field-remember').click()
driver.find_element_by_css_selector('button.btn.btn-primary').click()
def create_organization(self, name, description, users):
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_link_text('Organizations').click()
driver.find_element_by_link_text('Add Organization').click()
WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Organizations'))).click()
WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Add Organization'))).click()
WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.ID, 'field-name')))
# Wait a bit to let ckan add javascript hooks
time.sleep(0.2)
driver.find_element_by_id('field-name').clear()
driver.find_element_by_id('field-name').send_keys(name)
driver.find_element_by_id('field-description').clear()
@ -123,22 +150,28 @@ class TestSelenium(unittest.TestCase):
driver.find_element_by_name('save').click()
# Add users
driver.find_element_by_link_text('Manage').click()
driver.find_element_by_link_text('Members').click()
WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Manage'))).click()
WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Members'))).click()
for user in users:
driver.find_element_by_link_text('Add Member').click()
driver.find_element_by_id('username').send_keys(user)
WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Add Member'))).click()
WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.ID, "s2id_autogen1"))).send_keys(user + Keys.RETURN)
driver.find_element_by_name('submit').click()
def fill_ds_general_info(self, name, description, tags, private, searchable, allowed_users, acquire_url):
# FIRST PAGE: Dataset properties
driver = self.driver
WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.ID, "field-title")))
# Wait a bit to let ckan add javascript hooks
time.sleep(0.2)
driver.find_element_by_id('field-title').clear()
driver.find_element_by_id('field-title').send_keys(name)
driver.find_element_by_id('field-notes').clear()
driver.find_element_by_id('field-notes').send_keys(description)
driver.find_element_by_id('field-tags').clear()
driver.find_element_by_id('field-tags').send_keys(','.join(tags))
# field-tags
for tag in tags:
driver.find_element_by_id('s2id_autogen1').send_keys(tag + Keys.RETURN)
Select(driver.find_element_by_id('field-private')).select_by_visible_text('Private' if private else 'Public')
# WARN: The organization is set by default
@ -146,8 +179,9 @@ class TestSelenium(unittest.TestCase):
# If the dataset is public, these fields will be disabled (we'll check it)
if private:
Select(driver.find_element_by_id('field-searchable')).select_by_visible_text('True' if searchable else 'False')
driver.find_element_by_id('field-allowed_users_str').clear()
driver.find_element_by_id('field-allowed_users_str').send_keys(','.join(allowed_users))
# field-allowed_users
for user in allowed_users:
driver.find_element_by_css_selector('#s2id_field-allowed_users_str .select2-input').send_keys(user + Keys.RETURN)
driver.find_element_by_id('field-acquire_url').clear()
if acquire_url:
driver.find_element_by_id('field-acquire_url').send_keys(acquire_url)
@ -159,15 +193,20 @@ class TestSelenium(unittest.TestCase):
def create_ds(self, name, description, tags, private, searchable, allowed_users, acquire_url, resource_url, resource_name, resource_description, resource_format):
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_link_text('Datasets').click()
driver.find_element_by_link_text('Add Dataset').click()
WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Datasets'))).click()
WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Add Dataset'))).click()
self.fill_ds_general_info(name, description, tags, private, searchable, allowed_users, acquire_url)
# SECOND PAGE: Add Resources
WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.ID, "field-name")))
# Wait a bit to let ckan add javascript hooks
time.sleep(0.2)
try:
# The link button is only clicked if it's present
driver.find_element_by_link_text('Link').click()
except Exception:
except Exception: # pragma: no cover
pass
driver.find_element_by_id('field-image-url').clear()
@ -176,11 +215,7 @@ class TestSelenium(unittest.TestCase):
driver.find_element_by_id('field-name').send_keys(resource_name)
driver.find_element_by_id('field-description').clear()
driver.find_element_by_id('field-description').send_keys(resource_description)
driver.find_element_by_id('s2id_autogen1').clear()
driver.find_element_by_id('s2id_autogen1').send_keys(resource_format)
driver.find_element_by_css_selector('button.btn.btn-primary').click()
# THIRD PAGE: Metadata
driver.find_element_by_id('s2id_autogen1').send_keys(resource_format + Keys.RETURN)
driver.find_element_by_css_selector('button.btn.btn-primary').click()
def modify_ds(self, url, name, description, tags, private, searchable, allowed_users, acquire_url):
@ -200,11 +235,12 @@ class TestSelenium(unittest.TestCase):
# Test that the allowed users lists is as expected (order is not important)
current_users = driver.find_element_by_css_selector('#s2id_field-allowed_users_str > ul.select2-choices').text.split('\n')
current_users = current_users[0:-1]
# ''.split('\n') ==> ['']
if len(current_users) == 1 and current_users[0] == '':
current_users = []
# if len(current_users) == 1 and current_users[0] == '':
# current_users = []
# Check the array
self.assertEquals(len(allowed_users), len(current_users))
self.assertEqual(len(allowed_users), len(current_users))
for user in current_users:
self.assertIn(user, allowed_users)
else:
@ -214,69 +250,56 @@ class TestSelenium(unittest.TestCase):
driver = self.driver
driver.find_element_by_link_text('Datasets').click()
if searchable:
if searchable or owner or in_org:
xpath = '//div[@id=\'content\']/div[3]/div/section/div/ul/li/div/h3/span'
# Check the label
if owner:
self.assertEqual('OWNER', driver.find_element_by_xpath(xpath).text)
if not acquired and private and not in_org:
self.assertEqual('PRIVATE', driver.find_element_by_xpath(xpath).text)
elif acquired and not owner and private:
self.assertEqual('ACQUIRED', driver.find_element_by_xpath(xpath).text)
elif owner:
self.assertEqual('OWNER', driver.find_element_by_xpath(xpath).text)
# Access the dataset
driver.find_element_by_link_text(dataset).click()
# When a user cannot access a dataset, the link is no longer provided
else:
# If the dataset is not searchable, a link to it could not be found in the dataset search page
self.assertEquals(None, re.search(dataset_url, driver.page_source))
# If the dataset is not searchable and the user is not the owner, a link to it could not be found in the dataset search page
self.assertEqual(None, re.search(dataset_url, driver.page_source))
# Access the dataset
driver.get(self.base_url + 'dataset/' + dataset_url)
# Access the dataset
driver.get(self.base_url + 'dataset/' + dataset_url)
if not acquired and private and not in_org:
xpath = '//div[@id=\'content\']/div/div'
buy_msg = 'This private dataset can be acquired. To do so, please click here'
if acquire_url is not None:
self.assertTrue(driver.find_element_by_xpath(xpath).text.startswith(buy_msg))
self.assertEquals(acquire_url, driver.find_element_by_link_text('here').get_attribute('href'))
xpath += '[2]' # The unauthorized message is in a different Path
else:
src = driver.page_source
self.assertEquals(None, re.search(buy_msg, src))
# If the dataset is private and the user hasnt access to the resources, the field resources dont appear
self.assertTrue('/user/login' in driver.current_url)
self.assertTrue(driver.find_element_by_xpath(xpath).text.startswith('Unauthorized to read package %s' % dataset_url))
self.assertEquals('empty', driver.find_element_by_class_name('empty').get_attribute('class'))
self.assertEqual(self.base_url + 'dataset/%s' % dataset_url, driver.current_url)
else:
self.assertEquals(self.base_url + 'dataset/%s' % dataset_url, driver.current_url)
self.assertEquals('resource-list', driver.find_element_by_class_name('resource-list').get_attribute('class'))
self.assertEqual(self.base_url + 'dataset/%s' % dataset_url, driver.current_url)
def check_acquired(self, dataset, dataset_url, acquired, private):
driver = self.driver
driver.get(self.base_url + 'dashboard')
driver.find_element_by_link_text('Acquired Datasets').click()
WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Acquired Datasets'))).click()
if acquired and private:
# This message could not be shown when the user has acquired at least one dataset
self.assertEquals(None, re.search('You haven\'t acquired any datasets.', driver.page_source))
self.assertEqual(None, re.search('You haven\'t acquired any datasets.', driver.page_source))
# Access the dataset
driver.find_element_by_link_text(dataset).click()
self.assertEquals(self.base_url + 'dataset/%s' % dataset_url, driver.current_url)
self.assertEqual(self.base_url + 'dataset/%s' % dataset_url, driver.current_url)
else:
# If the user has not acquired the dataset, a link to this dataset could not be in the acquired dataset list
self.assertEquals(None, re.search(dataset_url, driver.page_source))
self.assertEqual(None, re.search(dataset_url, driver.page_source))
# When a user has not acquired any dataset, a message will be shown to inform the user
self.assertNotEquals(None, re.search('You haven\'t acquired any datasets.', driver.page_source))
def default_register(self, user):
self.register(user, user, '%s@conwet.com' % user, user)
self.register(user, user, '%s@conwet.com' % user)
@parameterized.expand([
# (['user1', 'user2', 'user3'], True, True, ['user2'], 'http://store.conwet.com/'),
# (['user1', 'user2', 'user3'], True, True, ['user3']),
# (['user1', 'user2', 'user3'], False, True, ['user3']),
# (['user1', 'user2', 'user3'], True, False, ['user2']),
(['user1', 'user2', 'user3'], True, True, [], 'http://store.conwet.com/'),
(['user1', 'user2', 'user3'], True, True, []),
(['user1', 'user2', 'user3'], False, True, []),
@ -292,12 +315,14 @@ class TestSelenium(unittest.TestCase):
self.default_register(user)
# The first user creates a dataset
self.login(users[0], users[0])
self.login(users[0])
pkg_name = 'Dataset 1'
url = get_dataset_url(pkg_name)
self.create_ds(pkg_name, 'Example description', ['tag1', 'tag2', 'tag3'], private, searchable,
allowed_users, acquire_url, 'http://upm.es', 'UPM Main', 'Example Description', 'CSV')
self.check_ds_values(url, private, searchable, allowed_users, acquire_url)
self.check_user_access(pkg_name, url, True, True, False, private, searchable, acquire_url)
self.check_acquired(pkg_name, url, False, private)
@ -305,26 +330,16 @@ class TestSelenium(unittest.TestCase):
rest_users = users[1:]
for user in rest_users:
self.logout()
self.login(user, user)
self.login(user)
acquired = user in allowed_users
self.check_user_access(pkg_name, url, False, acquired, False, private, searchable, acquire_url)
self.check_acquired(pkg_name, url, acquired, private)
@parameterized.expand([
# (['a'] , 'http://upm.es', 'Allowed users: Name must be at least 2 characters long'),
# (['a a'], 'http://upm.es', 'Allowed users: Url must be purely lowercase alphanumeric (ascii) characters and these symbols: -_'),
(['upm', 'a'], 'http://upm.es', 'Allowed users: Name must be at least 2 characters long'),
(['upm', 'a a a'], 'http://upm.es', 'Allowed users: Url must be purely lowercase alphanumeric (ascii) characters and these symbols: -_'),
(['upm', 'a?-vz'], 'http://upm.es', 'Allowed users: Url must be purely lowercase alphanumeric (ascii) characters and these symbols: -_'),
(['thisisaveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylongname'],
'http://upm.es', 'Allowed users: Name must be a maximum of 100 characters long'),
(['conwet'], 'ftp://google.es', 'Acquire URL: The URL "ftp://google.es" is not valid.'),
# (['conwet'], 'http://google*.com', 'Acquire URL: The URL "http://google*.com" is not valid.'),
# (['conwet'], 'http://google+.com', 'Acquire URL: The URL "http://google+.com" is not valid.'),
# (['conwet'], 'http://google/.com', 'Acquire URL: The URL "http://google/.com" is not valid.'),
(['conwet'], 'google', 'Acquire URL: The URL "google" is not valid.'),
(['conwet'], 'http://google', 'Acquire URL: The URL "http://google" is not valid.'),
# (['conwet'], 'http://google:es', 'Acquire URL: The URL "http://google:es" is not valid.'),
(['conwet'], 'www.google.es', 'Acquire URL: The URL "www.google.es" is not valid.')
])
@ -335,21 +350,21 @@ class TestSelenium(unittest.TestCase):
self.default_register(user)
# Create the dataset
self.login(user, user)
self.login(user)
pkg_name = 'Dataset 2'
# Go the page to create the dataset
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_link_text('Datasets').click()
driver.find_element_by_link_text('Add Dataset').click()
WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Datasets'))).click()
WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Add Dataset'))).click()
# Fill the requested information
self.fill_ds_general_info(pkg_name, 'Example description', ['tag1'], True, True, allowed_users, acquire_url)
# Check the error message
msg_error = self.driver.find_element_by_xpath('//div[@id=\'content\']/div[3]/div/section/div/form/div/ul/li').text
self.assertEquals(expected_msg, msg_error)
msg_error = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.XPATH, '//div[@id=\'content\']/div[3]/div/section/div/form/div/ul/li'))).text
self.assertEqual(expected_msg, msg_error)
@parameterized.expand([
('Acquire Dataset', 'dataset'),
@ -359,13 +374,13 @@ class TestSelenium(unittest.TestCase):
# Create a default user
user = 'user1'
self.default_register(user)
self.login(user, user)
self.login(user)
# Enter the acquired dataset tab
driver = self.driver
driver.get(self.base_url + 'dashboard/acquired')
driver.find_element_by_link_text(link).click()
self.assertEquals(self.base_url + 'dataset', self.base_url + expected_url)
WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, link))).click()
self.assertEqual(self.base_url + 'dataset', self.base_url + expected_url)
@parameterized.expand([
@ -390,7 +405,6 @@ class TestSelenium(unittest.TestCase):
([{'private': False, 'searchable': True, 'allowed_users': ['user1', 'user2']}], ['user3', 'user4']),
([{'private': True, 'searchable': False, 'allowed_users': ['user1', 'user2']}], ['user3', 'user4']),
([{'private': False, 'searchable': False, 'allowed_users': ['user1', 'user2']}], ['user3', 'user4']),
# Complex test
([{'private': True, 'searchable': False, 'allowed_users': ['user1', 'user2']},
{'private': True, 'searchable': True, 'allowed_users': ['user5', 'user6']},
@ -402,7 +416,7 @@ class TestSelenium(unittest.TestCase):
# Create a default user
user = 'user1'
self.default_register(user)
self.login(user, user)
self.login(user)
acquire_url = 'http://upm.es'
dataset_default_name = 'Dataset %d'
@ -443,29 +457,14 @@ class TestSelenium(unittest.TestCase):
self.check_ds_values(url_path, dataset['private'], dataset['searchable'], final_users, acquire_url)
@parameterized.expand([
# (['user1', 'user2', 'user3'], [{'name': 'CoNWeT', 'users': ['user2']}], True, True, [], 'http://store.conwet.com/'),
# (['user1', 'user2', 'user3'], [{'name': 'CoNWeT', 'users': ['user2']}], True, True, []),
# (['user1', 'user2', 'user3'], [{'name': 'CoNWeT', 'users': ['user2']}], False, True, []),
# (['user1', 'user2', 'user3'], [{'name': 'CoNWeT', 'users': ['user2']}], True, False, []),
# (['user1', 'user2', 'user3'], [{'name': 'CoNWeT', 'users': ['user2']}], True, True, ['user3'], 'http://store.conwet.com/'),
# (['user1', 'user2', 'user3'], [{'name': 'CoNWeT', 'users': ['user2']}], True, True, ['user3']),
# (['user1', 'user2', 'user3'], [{'name': 'CoNWeT', 'users': ['user2']}], False, True, ['user3']),
# (['user1', 'user2', 'user3'], [{'name': 'CoNWeT', 'users': ['user2']}], True, False, ['user3']),
# More complex
# (['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']}], True, True, ['user4', 'user5'], 'http://store.conwet.com/'),
# (['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']}], True, True, ['user4', 'user5']),
# (['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']}], False, True, ['user4', 'user5']),
# (['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']}], True, False, ['user4', 'user5']),
# Even if user6 is in another organization, he/she won't be able to access the dataset
(['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']},
(['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']},
{'name': 'UPM', 'users': ['user6']}], True, True, ['user4', 'user5'], 'http://store.conwet.com/'),
(['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']},
(['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']},
{'name': 'UPM', 'users': ['user6']}], True, True, ['user4', 'user5']),
(['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']},
(['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']},
{'name': 'UPM', 'users': ['user6']}], True, False, ['user4', 'user5']),
(['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']},
(['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']},
{'name': 'UPM', 'users': ['user6']}], False, True, ['user4', 'user5']),
])
def test_organization(self, users, orgs, private, searchable, adquiring_users, acquire_url=None):
@ -473,7 +472,7 @@ class TestSelenium(unittest.TestCase):
for user in users:
self.default_register(user)
self.login(users[0], users[0])
self.login(users[0])
# Create the organizations
for org in orgs:
@ -492,10 +491,11 @@ class TestSelenium(unittest.TestCase):
rest_users = users[1:]
for user in rest_users:
self.logout()
self.login(user, user)
self.login(user)
acquired = user in adquiring_users
in_org = user in orgs[0]['users']
self.check_user_access(pkg_name, url, False, acquired, in_org, private, searchable, acquire_url)
self.check_acquired(pkg_name, url, acquired, private)
def test_bug_16(self):
@ -506,15 +506,14 @@ class TestSelenium(unittest.TestCase):
self.default_register(user)
# The user creates a dataset
self.login(user, user)
self.login(user)
pkg_name = 'Dataset 1'
description = 'Example Description'
tags = ['tag1', 'tag2', 'tag3']
url = get_dataset_url(pkg_name)
self.create_ds(pkg_name, 'Example description', ['tag1', 'tag2', 'tag3'], True, True,
self.create_ds(pkg_name, 'Example description', [], True, True,
[], 'http://example.com', 'http://upm.es', 'UPM Main', 'Example Description', 'CSV')
self.modify_ds(url, pkg_name, description, tags, False, None, None, None)
expected_url = 'dataset/%s' % url
current_url = self.driver.current_url
self.assertIn(expected_url, current_url) # Maybe the current URL include some parameters
WebDriverWait(self.driver, 20).until(lambda driver: expected_url in driver.current_url)

View File

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid
# Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L.
# This file is part of CKAN Private Dataset Extension.
# CKAN Private Dataset Extension is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# CKAN Private Dataset Extension 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 Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>.
import unittest
from mock import ANY, DEFAULT, MagicMock, patch
from parameterized import parameterized
from ckanext.privatedatasets import views
class ViewsTest(unittest.TestCase):
@parameterized.expand([
('NotFound', 404),
('NotAuthorized', 403),
])
@patch.multiple("ckanext.privatedatasets.views", base=DEFAULT, toolkit=DEFAULT, model=DEFAULT, g=DEFAULT, logic=DEFAULT, _=DEFAULT)
def test_exceptions_loading_users(self, exception, expected_status, base, toolkit, model, g, logic, _):
# Configure the mocks
setattr(logic, exception, ValueError)
toolkit.get_action().side_effect = getattr(logic, exception)
base.abort.side_effect = TypeError
# Call the function
with self.assertRaises(TypeError):
views.acquired_datasets()
# Assertations
expected_context = {
'auth_user_obj': g.userobj,
'for_view': True,
'model': model,
'session': model.Session,
'user': g.user,
}
toolkit.get_action().assert_called_once_with(expected_context, {'user_obj': g.userobj})
base.abort.assert_called_once_with(expected_status, ANY)
@patch.multiple("ckanext.privatedatasets.views", base=DEFAULT, toolkit=DEFAULT, model=DEFAULT, g=DEFAULT, logic=DEFAULT)
def test_no_error_loading_users(self, base, toolkit, model, g, logic):
# actions
default_user = {'user_name': 'test', 'another_val': 'example value'}
user_show = MagicMock(return_value=default_user)
acquisitions_list = MagicMock()
toolkit.get_action = MagicMock(side_effect=lambda action: user_show if action == 'user_show' else acquisitions_list)
# Call the function
returned = views.acquired_datasets()
# User_show called correctly
expected_context = {
'auth_user_obj': g.userobj,
'for_view': True,
'model': model,
'session': model.Session,
'user': g.user,
}
user_show.assert_called_once_with(expected_context, {'user_obj': g.userobj})
acquisitions_list.assert_called_with(expected_context, None)
# Check that the render method has been called
base.render.assert_called_once_with('user/dashboard_acquired.html', {'user_dict': default_user, 'acquired_datasets': acquisitions_list()})
self.assertEqual(returned, base.render())
@patch("ckanext.privatedatasets.views.acquired_datasets")
def test_there_is_a_controller_for_ckan_27(self, acquired_datasets):
controller = views.AcquiredDatasetsControllerUI()
response = controller.acquired_datasets()
acquired_datasets.assert_called_once_with()
self.assertEqual(response, acquired_datasets())

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L.
# This file is part of CKAN Private Dataset Extension.
# CKAN Private Dataset Extension is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# CKAN Private Dataset Extension 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 Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, unicode_literals
from ckan import logic, model
from ckan.common import _, g
from ckan.lib import base
from ckan.plugins import toolkit
from ckanext.privatedatasets import constants
def acquired_datasets():
context = {'auth_user_obj': g.userobj, 'for_view': True, 'model': model, 'session': model.Session, 'user': g.user}
data_dict = {'user_obj': g.userobj}
try:
user_dict = toolkit.get_action('user_show')(context, data_dict)
acquired_datasets = toolkit.get_action(constants.ACQUISITIONS_LIST)(context, None)
except logic.NotFound:
base.abort(404, _('User not found'))
except logic.NotAuthorized:
base.abort(403, _('Not authorized to see this page'))
extra_vars = {
'user_dict': user_dict,
'acquired_datasets': acquired_datasets,
}
return base.render('user/dashboard_acquired.html', extra_vars)
class AcquiredDatasetsControllerUI(base.BaseController):
def acquired_datasets(self):
return acquired_datasets()

View File

@ -1,2 +0,0 @@
nose_parameterized==0.3.3
selenium==2.44.0

21
setup.cfg Normal file
View File

@ -0,0 +1,21 @@
[bdist_wheel]
universal = 1
[flake8]
ignore=E501
[metadata]
description-file = README.md
[nosetests]
ckan=1
with-pylons=test.ini
with-xunit=1
with-coverage=1
cover-package=ckanext.privatedatasets
cover-inclusive=1
cover-erase=1
cover-xml=1
[pep8]
ignore=E501

View File

@ -19,29 +19,38 @@
# along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>.
from setuptools import setup, find_packages
import sys, os
version = '0.2.7'
version = '0.4.1'
setup(
name='ckanext-privatedatasets',
version=version,
description="This extensions allows users to create private datasets only visible to certain users. The extension provides also an API to specify programatically which users can access private datasets",
description='CKAN Extension - Private Datasets',
long_description='''
This CKAN extension allows a user to create private datasets that only certain users will be able to see. When a dataset is being created, it's possible to specify the list of users that can see this dataset. In addition, the extension provides an HTTP API that allows to add users programatically.
''',
classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
keywords='',
author='Aitor Magan',
author_email='amagan@conwet.com',
url='',
keywords='ckan, private, datasets',
author='Aitor Magan, Francisco de la Vega',
author_email='fdelavega@ficodes.com',
url='https://conwet.fi.upm.es',
download_url='https://github.com/conwetlab/ckanext-privatedatasets/tarball/v' + version,
license='',
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
namespace_packages=['ckanext', 'ckanext.privatedatasets'],
include_package_data=True,
zip_safe=False,
setup_requires=[
'nose>=1.3.0'
],
install_requires=[
# -*- Extra requirements: -*-
],
tests_require=[
'parameterized',
'selenium==3.13.0'
],
test_suite='nosetests',
entry_points='''
[ckan.plugins]
# Add plugins here, e.g.

View File

@ -4,10 +4,76 @@ host = 0.0.0.0
port = 5000
[app:main]
# use = config:/usr/lib/ckan/default/src/ckan/test-core.ini
use = config:./ckan/test-core.ini
ckan.legacy_templates = No
ckan.site_id = ckanext.privatedatasets.test
ckan.site_url = http://localhost:5000
package_new_return_url = http://localhost:5000/dataset/<NAME>?test=new
package_edit_return_url = http://localhost:5000/dataset/<NAME>?test=edit
ckan.cache_validation_enabled = True
ckan.cache_enabled = False
ckan.tests.functional.test_cache.expires = 1800
ckan.tests.functional.test_cache.TestCacheBasics.test_get_cache_expires.expires = 3600
# Disable test css as we aren't using it and creates requests to URLs
# raising ValidationErrors. See #46
ckan.template_head_end = <!-- template_head_end -->
ckan.legacy_templates = no
ckan.plugins = privatedatasets
ckan.auth.create_unowned_dataset = true
ckan.auth.create_dataset_if_not_in_organization = true
ckan.auth.user_create_groups = true
ckan.auth.user_create_organizations = true
ckan.privatedatasets.parser = ckanext.privatedatasets.parsers.fiware:FiWareNotificationParser
ckan.privatedatasets.show_acquire_url_on_create = True
ckan.privatedatasets.show_acquire_url_on_edit = True
ckan.privatedatasets.show_acquire_url_on_edit = True
sqlalchemy.url = postgresql://ckan_default:pass@127.0.0.1:5432/ckan_test
ckan.datastore.write_url = postgresql://ckan_default:pass@127.0.0.1:5432/datastore_test
ckan.datastore.read_url = postgresql://datastore_default:pass@127.0.0.1:5432/datastore_test
ckan.storage_path=data/storage
# Logging configuration
[loggers]
keys = root, ckan, ckanext, sqlalchemy
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
[logger_ckan]
qualname = ckan
handlers = console
level = INFO
propagate = 0
[logger_ckanext]
qualname = ckanext
handlers = console
level = DEBUG
propagate = 0
[logger_sqlalchemy]
handlers = console
qualname = sqlalchemy.engine
level = WARN
[handler_console]
class = StreamHandler
args = (sys.stdout,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s