Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transfer a right #17

Merged
merged 5 commits into from
Feb 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface (runnable in Docker) of the functionalities provided in
[pycoalaip](https://github.com/bigchaindb/pycoalaip).


## Whats the status of this?
## What's the status of this?

Super-pre-alpha. At the moment all you can do is hit against two endpoints that
are being exposed. Minimal error paths are provided.
Expand Down Expand Up @@ -134,6 +134,22 @@ The API server can be configured with a number of environment variables [see
.env_template](./.env_template).


## Quick Guide

Let's assume you have an artwork you'd like to loan to someone else. These are
the steps you'll need to follow in order to register the work and any
transactions relating to the work via COALA IP.

1. First, [create some users](#create-users) for both yourself and those
"someone elses"
1. [Register your artwork as a Manifestation](#reigster-a-manifestation)
1. [Derive a special "usage" Right](#register-a-right-allowed-by-another-right-or-copyright)
from the Copyright that resulted from registering your artwork
1. Finally, [transfer that special "usage" Right](#transfer-a-right) to someone
else, optionally including a contract and other information as part of the
transfer.


## REST API


Expand Down Expand Up @@ -279,7 +295,7 @@ PAYLOAD:
"publicKey": "<base58 string>",
"privateKey": "<base58 string>"
},
"sourceRightId": "<ID of an existing Right that allows for the creation of this new Right>"
"sourceRightId": "<ID of an existing Right that allows for the creation of this new Right; must be held by the user specified in `currentHolder`>"
}

RETURNS:
Expand All @@ -296,3 +312,54 @@ RETURNS:
To check if your POST was successful, follow the steps in [registering a
manifestation](#was-my-post-to-manifestations-successful) and use the returned
Right's data instead.


### Transfer a Right

A contract between a rightsholder and a licensee can be recorded through the
transferring of a Right, via an accompanying RightsAssignment entity. The
transfer transaction recorded on BigchainDB links to that contract (via the
RightsAssignment) and provides a record of the parties, the date of the
transaction, and the contract that applies.

You may only transfer a Right that you are currently holding. RightsAssignment
entities are automatically created for each transfer and may include additional,
arbitrary attributes if a `rightsAssignment` dict is given in the payload.

```
POST /api/v1/rights/transfer
HEADERS {"Content-Type": "application/json"}

PAYLOAD:
{
"rightId": "<ID of an existing Right to transfer; must be held by the user specified in `currentHolder`>",
"rightsAssignment": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we'd add a document or sentence here that would state the conditions under which the Right is transferred, we'd also solve #6.

...
},
"currentHolder": {
"publicKey": "<base58 string>",
"privateKey": "<base58 string>"
},
"to": {
"publicKey": "<base58 string>",
"privateKey": null
}
}

RETURNS:
{
"rightsAssignment": {
"@id": "<currently empty>",
"@type": "RightsTransferAction",
... (provided `rightsAssignment`)
}
}
```

Note that the `to` field in the payload may avoid specifying the new holder's
private details (i.e. `signingKey`), but should still provide the keys needed to
conform to the [user model](#create-users).

To check if your POST was successful, follow the steps in [registering a
manifestation](#was-my-post-to-manifestations-successful) and use the returned
Right's data instead.
40 changes: 37 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,22 @@ def app():


@pytest.fixture
def user(client):
def alice(client):
return client.post(url_for('user_views.userapi')).json


@pytest.fixture
def created_manifestation_resp(client, user):
def bob(client):
return client.post(url_for('user_views.userapi')).json


@pytest.fixture
def carly(client):
return client.post(url_for('user_views.userapi')).json


@pytest.fixture
def created_manifestation_resp(client, alice):
import json
from time import sleep
payload = {
Expand All @@ -25,7 +35,7 @@ def created_manifestation_resp(client, user):
'datePublished': '29-07-1954',
'url': 'http://localhost/lordoftherings.txt',
},
'copyrightHolder': user,
'copyrightHolder': alice,
'work': {
'name': 'The Lord of the Rings Triology',
'author': 'J. R. R. Tolkien',
Expand All @@ -39,3 +49,27 @@ def created_manifestation_resp(client, user):
# Sleep for a bit to let the transaction become valid
sleep(3)
return resp.json


@pytest.fixture
def created_derived_right(client, alice, created_manifestation_resp):
import json
from time import sleep

copyright_id = created_manifestation_resp['copyright']['@id']
copyright_id = copyright_id.split('../rights/')[1]
payload = {
'currentHolder': alice,
'right': {
'license': 'http://www.ascribe.io/terms',
},
'sourceRightId': copyright_id,
}

resp = client.post(url_for('right_views.rightapi'),
data=json.dumps(payload),
headers={'Content-Type': 'application/json'})

# Sleep for a bit to let the transaction become valid
sleep(3)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😴

return resp.json['right']
82 changes: 71 additions & 11 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ def test_create_user(client):
assert resp.json['privateKey']


def test_create_manifestation(client, user):
def test_create_manifestation(client, alice):
payload = {
'manifestation': {
'name': 'The Fellowship of the Ring',
'datePublished': '29-07-1954',
'url': 'http://localhost/lordoftherings.txt',
},
'copyrightHolder': user,
'copyrightHolder': alice,
'work': {
'name': 'The Lord of the Rings Triology',
'author': 'J. R. R. Tolkien',
Expand Down Expand Up @@ -70,13 +70,13 @@ def test_create_manifestation(client, user):
assert resp.status_code == 200


def test_create_manifestation_missing_single_attribute(client, user):
def test_create_manifestation_missing_single_attribute(client, alice):
payload = {
'manifestation': {
'name': 'The Fellowship of the Ring',
'url': 'http://localhost/lordoftherings.txt',
},
'copyrightHolder': user,
'copyrightHolder': alice,
'work': {
'name': 'The Lord of the Rings Triology',
'author': 'J. R. R. Tolkien',
Expand Down Expand Up @@ -108,12 +108,12 @@ def test_create_manifestation_missing_argument_in_body(client):
'Missing required parameter in the JSON body'


def test_create_right(client, user, created_manifestation_resp):
def test_create_right(client, alice, created_manifestation_resp):
copyright_id = created_manifestation_resp['copyright']['@id']
copyright_id = copyright_id.split('../rights/')[1]

payload = {
'currentHolder': user,
'currentHolder': alice,
'right': {
'license': 'http://www.ascribe.io/terms',
},
Expand All @@ -126,7 +126,7 @@ def test_create_right(client, user, created_manifestation_resp):
'@type': 'Right',
'source': payload['sourceRightId'],
'license': 'http://www.ascribe.io/terms',
}
},
}

resp = client.post(url_for('right_views.rightapi'),
Expand All @@ -140,9 +140,9 @@ def test_create_right(client, user, created_manifestation_resp):
assert resp.status_code == 200


def test_create_right_missing_single_attribute(client, user):
def test_create_right_missing_single_attribute(client, alice):
payload = {
'currentHolder': user,
'currentHolder': alice,
'right': {
'notALicense': 'this is not a license',
},
Expand All @@ -156,9 +156,9 @@ def test_create_right_missing_single_attribute(client, user):
"'`license` must be provided'"


def test_create_right_missing_argument_in_body(client, user):
def test_create_right_missing_argument_in_body(client, alice):
payload = {
'currentHolder': user,
'currentHolder': alice,
'right': {
'license': 'http://www.ascribe.io/terms',
},
Expand All @@ -169,3 +169,63 @@ def test_create_right_missing_argument_in_body(client, user):
assert resp.status_code == 400
assert resp.json['message']['sourceRightId'] == \
'Missing required parameter in the JSON body'


def test_transfer_right(client, alice, bob, carly, created_derived_right):
from time import sleep

payload = {
'rightId': created_derived_right['@id'],
'rightsAssignment': {
'action': 'loan',
},
'currentHolder': alice,
'to': {
'publicKey': bob['publicKey'],
'privateKey': None,
}
}

expected = {
'rightsAssignment': {
'@context': ['<coalaip placeholder>', 'http://schema.org/'],
'@type': 'RightsTransferAction',
'@id': '',
'action': 'loan',
},
}

resp = client.post(url_for('right_views.righttransferapi'),
data=json.dumps(payload),
headers={'Content-Type': 'application/json'})
assert resp.status_code == 200
assert resp.json == expected

# Test re-transfer, after waiting for the first transfer to become valid
sleep(3)
retransfer_payload = {
'rightId': created_derived_right['@id'],
'rightsAssignment': {
'action': 'reloan',
},
'currentHolder': bob,
'to': {
'publicKey': carly['publicKey'],
'privateKey': None,
}
}

retransfer_expected = {
'rightsAssignment': {
'@context': ['<coalaip placeholder>', 'http://schema.org/'],
'@type': 'RightsTransferAction',
'@id': '',
'action': 'reloan',
},
}

resp = client.post(url_for('right_views.righttransferapi'),
data=json.dumps(retransfer_payload),
headers={'Content-Type': 'application/json'})
assert resp.status_code == 200
assert resp.json == retransfer_expected
1 change: 1 addition & 0 deletions web/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


user_model = parse_model(['publicKey', 'privateKey'])
public_user_model = parse_model(['publicKey'])
manifestation_model = parse_model(['name', 'datePublished', 'url'])
work_model = parse_model(['name', 'author'])
right_model = parse_model(['license'])
2 changes: 1 addition & 1 deletion web/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def _parse_model(inputs):
value = inputs[field]
except KeyError:
raise KeyError('`{}` must be provided'.format(field))
if bool(value) is not True:
if not value:
raise ValueError("`{}`'s value must be defined".format(field))
return inputs
return _parse_model
49 changes: 47 additions & 2 deletions web/views/rights.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from flask import Blueprint
from flask_restful import reqparse, Resource, Api

from coalaip import CoalaIp
from coalaip import CoalaIp, ModelDataError, entities
from coalaip_bigchaindb.plugin import Plugin
from web.models import right_model, user_model
from web.models import right_model, public_user_model, user_model
from web.utils import get_bigchaindb_api_url


Expand Down Expand Up @@ -42,4 +42,49 @@ def post(self):
return res


class RightTransferApi(Resource):
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('rightId', type=str, required=True,
location='json')
parser.add_argument('currentHolder', type=user_model, required=True,
location='json')
parser.add_argument('to', type=public_user_model, required=True,
location='json')
parser.add_argument('rightsAssignment', type=dict, location='json')
args = parser.parse_args()

right_id = args['rightId']
current_holder = args['currentHolder']
to = args['to']
rights_assignment = args['rightsAssignment']

for user in [current_holder, to]:
user['public_key'] = user.pop('publicKey')
user['private_key'] = user.pop('privateKey')

# We can't be sure of the type of Right that's given by using just the
# id, so let's assume it's a normal Right first before trying to make a
# Copyright
try:
right = entities.Right.from_persist_id(right_id,
plugin=coalaip.plugin,
force_load=True)
except ModelDataError:
right = entities.Copyright.from_persist_id(right_id,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, we can transfer a copyright as well!

plugin=coalaip.plugin,
force_load=True)

res = coalaip.transfer_right(right=right,
rights_assignment_data=rights_assignment,
current_holder=current_holder,
to=to)

res = {'rightsAssignment': res.to_jsonld()}

return res


right_api.add_resource(RightApi, '/rights', strict_slashes=False)
right_api.add_resource(RightTransferApi, '/rights/transfer',
strict_slashes=False)