Skip to content

Commit

Permalink
Merge branch 'master' into zope.security
Browse files Browse the repository at this point in the history
  • Loading branch information
kroman0 committed Dec 12, 2014
2 parents d704c9c + 699a192 commit 98eb4a6
Show file tree
Hide file tree
Showing 9 changed files with 787 additions and 28 deletions.
54 changes: 31 additions & 23 deletions src/openprocurement/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from datetime import datetime, timedelta
from iso8601 import parse_date, ParseError
from pyramid.security import Allow
from schematics.exceptions import ConversionError, ValidationError
from schematics.exceptions import ConversionError
from schematics.models import Model
from schematics.transforms import whitelist, blacklist
from schematics.types import StringType, FloatType, IntType, URLType, BooleanType, BaseType, EmailType
Expand Down Expand Up @@ -55,28 +55,6 @@ def to_primitive(self, value, context=None):
return value.isoformat()


class AmendmentInformation(Model):
"""Amendment information"""
class Options:
serialize_when_none = False

amendmentDate = IsoDateTimeType()
amendedFields = StringType() # Comma-seperated list of affected fields.
justification = StringType() # An explanation / justification for the amendment.


class Notice(Model):
"""The notice is a published document that notifies the public at various stages of the contracting process."""
class Options:
serialize_when_none = False

id = StringType() # The identifier that identifies the notice to the publisher. This may be the same or different from the OCID.
uri = URLType() # A permanent uri that provides access to the notice.
publishedDate = IsoDateTimeType() # The date this version of the notice was published. In the case of notice amendments, it is the date that reflects to this version of the data.
isAmendment = BooleanType() # If true, then amendment information should be provided.
amendment = ModelType(AmendmentInformation) # Amendment information


class Value(Model):
class Options:
serialize_when_none = False
Expand Down Expand Up @@ -132,6 +110,15 @@ class Options:
countryName_ru = StringType()


class Location(Model):
class Options:
serialize_when_none = False

latitude = BaseType(required=True)
longitudee = BaseType(required=True)
elevation = BaseType()


class Item(Model):
"""A good, service, or work to be contracted."""
class Options:
Expand All @@ -146,6 +133,7 @@ class Options:
quantity = IntType() # The number of units required
deliveryDate = ModelType(Period)
deliveryAddress = ModelType(Address)
deliveryLocation = ModelType(Location)


class Document(Model):
Expand Down Expand Up @@ -302,6 +290,25 @@ class Options:
documents = ListType(ModelType(Document), default=list())


class Contract(Model):
class Options:
serialize_when_none = False

id = StringType(required=True, default=lambda: uuid4().hex)
awardID = StringType()
title = StringType() # Contract title
title_en = StringType()
title_ru = StringType()
description = StringType() # Contract description
description_en = StringType()
description_ru = StringType()
status = StringType(required=True, choices=['pending', 'terminated', 'active', 'cancelled'], default='pending')
period = ModelType(Period)
value = ModelType(Value)
dateSigned = IsoDateTimeType(default=get_now)
documents = ListType(ModelType(Document), default=list())


class Award(Model):
""" An award for the given procurement. There may be more than one award
per contracting process e.g. because the contract is split amongst
Expand Down Expand Up @@ -329,6 +336,7 @@ class Options:
items = ListType(ModelType(Item))
documents = ListType(ModelType(Document), default=list())
complaints = ListType(ModelType(Complaint), default=list())
contracts = ListType(ModelType(Contract), default=list())


plain_role = (blacklist('owner_token', '_attachments', 'revisions', 'dateModified') + schematics_embedded_role)
Expand Down
526 changes: 525 additions & 1 deletion src/openprocurement/api/tests/award.py

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion src/openprocurement/api/traversal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from pyramid.security import (
ALL_PERMISSIONS,
Allow,
Authenticated,
Everyone,
)

Expand Down Expand Up @@ -69,6 +68,14 @@ def factory(request):
return get_item(complaint, 'document', request, root)
else:
return complaint
elif request.matchdict.get('contract_id'):
contract = get_item(award, 'contract', request, root)
if contract == root:
return root
elif request.matchdict.get('document_id'):
return get_item(contract, 'document', request, root)
else:
return contract
elif request.matchdict.get('document_id'):
return get_item(award, 'document', request, root)
else:
Expand Down
10 changes: 9 additions & 1 deletion src/openprocurement/api/validation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from openprocurement.api.models import Tender, Bid, Award, Document, Question, Complaint
from openprocurement.api.models import Tender, Bid, Award, Document, Question, Complaint, Contract
from schematics.exceptions import ModelValidationError, ModelConversionError
from zope.security.proxy import isinstance

Expand Down Expand Up @@ -101,6 +101,14 @@ def validate_patch_complaint_data(request):
return validate_data(request, Complaint, True)


def validate_contract_data(request):
return validate_data(request, Contract)


def validate_patch_contract_data(request):
return validate_data(request, Contract, True)


def validate_file_upload(request):
if 'file' not in request.POST:
request.errors.add('body', 'file', 'Not Found')
Expand Down
3 changes: 2 additions & 1 deletion src/openprocurement/api/views/award.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
from cornice.resource import resource, view
from openprocurement.api.models import Award, get_now
from openprocurement.api.models import Award, Contract, get_now
from openprocurement.api.utils import (
apply_data_patch,
save_tender,
Expand Down Expand Up @@ -296,6 +296,7 @@ def patch(self):
src = tender.serialize("plain")
award.import_data(apply_data_patch(award.serialize(), award_data))
if award.status == 'active':
award.contracts.append(Contract({'awardID': award.id}))
tender.awardPeriod.endDate = get_now()
tender.status = 'active.awarded'
elif award.status == 'unsuccessful':
Expand Down
2 changes: 2 additions & 0 deletions src/openprocurement/api/views/award_complaint.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ def patch(self):
for j in i.complaints:
if j.status == 'pending':
j.status = 'cancelled'
for i in award.contracts:
i.status = 'cancelled'
award.status = 'cancelled'
unsuccessful_awards = [i.bid_id for i in tender.awards if i.status == 'unsuccessful']
bids = [i for i in sorted(tender.bids, key=lambda i: (i.value.amount, i.date)) if i.id not in unsuccessful_awards]
Expand Down
70 changes: 70 additions & 0 deletions src/openprocurement/api/views/award_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
from cornice.resource import resource, view
from openprocurement.api.models import Contract
from openprocurement.api.utils import (
apply_data_patch,
save_tender,
)
from openprocurement.api.validation import (
validate_contract_data,
validate_patch_contract_data,
)


@resource(name='Tender Award Contracts',
collection_path='/tenders/{tender_id}/awards/{award_id}/contracts',
path='/tenders/{tender_id}/awards/{award_id}/contracts/{contract_id}',
description="Tender award contracts")
class TenderAwardContractResource(object):

def __init__(self, request):
self.request = request
self.db = request.registry.db

@view(content_type="application/json", permission='create_award_contract', validators=(validate_contract_data,), renderer='json')
def collection_post(self):
"""Post a contract for award
"""
tender = self.request.validated['tender']
if tender.status not in ['active.awarded', 'complete']:
self.request.errors.add('body', 'data', 'Can\'t add contract in current tender status')
self.request.errors.status = 403
return
contract_data = self.request.validated['data']
contract = Contract(contract_data)
contract.awardID = self.request.validated['award_id']
src = tender.serialize("plain")
self.request.validated['award'].contracts.append(contract)
save_tender(tender, src, self.request)
self.request.response.status = 201
self.request.response.headers['Location'] = self.request.route_url('Tender Award Contracts', tender_id=tender.id, award_id=self.request.validated['award_id'], contract_id=contract['id'])
return {'data': contract.serialize()}

@view(renderer='json', permission='view_tender')
def collection_get(self):
"""List contracts for award
"""
return {'data': [i.serialize() for i in self.request.validated['award'].contracts]}

@view(renderer='json', permission='view_tender')
def get(self):
"""Retrieving the contract for award
"""
return {'data': self.request.validated['contract'].serialize()}

@view(content_type="application/json", permission='edit_tender', validators=(validate_patch_contract_data,), renderer='json')
def patch(self):
"""Update of contract
"""
tender = self.request.validated['tender']
if tender.status not in ['active.awarded', 'complete']:
self.request.errors.add('body', 'data', 'Can\'t update contract in current tender status')
self.request.errors.status = 403
return
contract = self.request.validated['contract']
contract_data = self.request.validated['data']
if contract_data:
src = tender.serialize("plain")
contract.import_data(apply_data_patch(contract.serialize(), contract_data))
save_tender(tender, src, self.request)
return {'data': contract.serialize()}
139 changes: 139 additions & 0 deletions src/openprocurement/api/views/award_contract_document.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
from cornice.resource import resource, view
from openprocurement.api.models import Document
from openprocurement.api.utils import (
generate_id,
get_file,
save_tender,
upload_file,
)
from openprocurement.api.validation import (
validate_file_update,
validate_file_upload,
validate_patch_document_data,
)


@resource(name='Tender Award Contract Documents',
collection_path='/tenders/{tender_id}/awards/{award_id}/contracts/{contract_id}/documents',
path='/tenders/{tender_id}/awards/{award_id}/contracts/{contract_id}/documents/{document_id}',
description="Tender award contract documents")
class TenderAwardContractDocumentResource(object):

def __init__(self, request):
self.request = request
self.db = request.registry.db

@view(renderer='json', permission='view_tender')
def collection_get(self):
"""Tender Award Contract Documents List"""
contract = self.request.validated['contract']
if self.request.params.get('all', ''):
collection_data = [i.serialize("view") for i in contract['documents']]
else:
collection_data = sorted(dict([
(i.id, i.serialize("view"))
for i in contract['documents']
]).values(), key=lambda i: i['dateModified'])
return {'data': collection_data}

@view(renderer='json', permission='edit_tender', validators=(validate_file_upload,))
def collection_post(self):
"""Tender Award Contract Document Upload
"""
tender = self.request.validated['tender']
if tender.status not in ['active.awarded', 'complete']:
self.request.errors.add('body', 'data', 'Can\'t add document in current tender status')
self.request.errors.status = 403
return
contract = self.request.validated['contract']
if contract.status not in ['pending', 'active']:
self.request.errors.add('body', 'data', 'Can\'t add document in current contract status')
self.request.errors.status = 403
return
src = tender.serialize("plain")
data = self.request.validated['file']
document = Document()
document.id = generate_id()
document.title = data.filename
document.format = data.type
key = generate_id()
document.url = self.request.route_url('Tender Award Contract Documents', tender_id=tender.id, award_id=self.request.validated['award_id'], contract_id=self.request.validated['contract_id'], document_id=document.id, _query={'download': key})
self.request.validated['contract'].documents.append(document)
upload_file(tender, document, key, data.file, self.request)
save_tender(tender, src, self.request)
self.request.response.status = 201
self.request.response.headers['Location'] = self.request.route_url('Tender Award Contract Documents', tender_id=tender.id, award_id=self.request.validated['award_id'], contract_id=self.request.validated['contract_id'], document_id=document.id)
return {'data': document.serialize("view")}

@view(renderer='json', permission='view_tender')
def get(self):
"""Tender Award Contract Document Read"""
document = self.request.validated['document']
key = self.request.params.get('download')
if key:
return get_file(self.request.validated['tender'], document, key, self.db, self.request)
document_data = document.serialize("view")
document_data['previousVersions'] = [
i.serialize("view")
for i in self.request.validated['documents']
if i.url != document.url
]
return {'data': document_data}

@view(renderer='json', validators=(validate_file_update,), permission='edit_tender')
def put(self):
"""Tender Award Contract Document Update"""
tender = self.request.validated['tender']
if tender.status not in ['active.awarded', 'complete']:
self.request.errors.add('body', 'data', 'Can\'t update document in current tender status')
self.request.errors.status = 403
return
contract = self.request.validated['contract']
if contract.status not in ['pending', 'active']:
self.request.errors.add('body', 'data', 'Can\'t update document in current contract status')
self.request.errors.status = 403
return
first_document = self.request.validated['documents'][0]
src = tender.serialize("plain")
if self.request.content_type == 'multipart/form-data':
data = self.request.validated['file']
filename = data.filename
content_type = data.type
in_file = data.file
else:
filename = first_document.title
content_type = self.request.content_type
in_file = self.request.body_file
document = Document()
document.id = self.request.validated['id']
document.title = filename
document.format = content_type
document.datePublished = first_document.datePublished
key = generate_id()
document.url = self.request.route_url('Tender Award Contract Documents', tender_id=tender.id, award_id=self.request.validated['award_id'], contract_id=self.request.validated['contract_id'], document_id=document.id, _query={'download': key})
self.request.validated['contract'].documents.append(document)
upload_file(tender, document, key, in_file, self.request)
save_tender(tender, src, self.request)
return {'data': document.serialize("view")}

@view(renderer='json', validators=(validate_patch_document_data,), permission='edit_tender')
def patch(self):
"""Tender Award Contract Document Update"""
tender = self.request.validated['tender']
if tender.status not in ['active.awarded', 'complete']:
self.request.errors.add('body', 'data', 'Can\'t update document in current tender status')
self.request.errors.status = 403
return
contract = self.request.validated['contract']
if contract.status not in ['pending', 'active']:
self.request.errors.add('body', 'data', 'Can\'t update document in current contract status')
self.request.errors.status = 403
return
document = self.request.validated['document']
document_data = self.request.validated['data']
if document_data:
src = tender.serialize("plain")
document.import_data(document_data)
save_tender(tender, src, self.request)
return {'data': document.serialize("view")}
2 changes: 1 addition & 1 deletion src/openprocurement/api/views/complaint.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def collection_post(self):
"""Post a complaint
"""
tender = self.request.validated['tender']
if tender.status != 'active.enquiries':
if tender.status not in ['active.enquiries', 'active.tendering']:
self.request.errors.add('body', 'data', 'Can\'t add complaint in current tender status')
self.request.errors.status = 403
return
Expand Down

0 comments on commit 98eb4a6

Please sign in to comment.