Skip to content

Commit

Permalink
Merge branch 'master' into use_python_version
Browse files Browse the repository at this point in the history
  • Loading branch information
tisto committed Mar 6, 2019
2 parents c21a4f4 + be3f3ba commit 390214a
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 62 deletions.
10 changes: 8 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@ Changelog
.. towncrier release notes start
3.7.1 (unreleased)
3.7.1 (2019-03-06)
------------------

- Nothing changed yet.
Bugfixes:

- Fix release to not create universal (Python 2/3) wheels.
[gforcada]

- Install zestreleaser.towncrier in the buildout to the changelog is updated correctly. (#684)
[maurits]


3.7.0 (2019-03-04)
Expand Down
2 changes: 2 additions & 0 deletions base.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ scripts = dependencychecker
recipe = zc.recipe.egg
eggs =
zest.releaser[recommended]
zestreleaser.towncrier
towncrier
readme
docutils

Expand Down
1 change: 0 additions & 1 deletion news/684.bugfix

This file was deleted.

3 changes: 3 additions & 0 deletions news/689.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix TUS upload events `#689 <https://github.com/plone/plone.restapi/issues/689>`_.
[buchi]

2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# https://github.com/plone/buildout.coredev/blob/5.2/requirements.txt
setuptools==40.4.3
setuptools==40.6.3
zc.buildout==2.12.2
122 changes: 69 additions & 53 deletions src/plone/restapi/services/content/tus.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# -*- coding: utf-8 -*-
from Acquisition import aq_base
from Acquisition.interfaces import IAcquirer
from AccessControl.SecurityManagement import getSecurityManager
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.utils import base_hasattr
from Products.CMFPlone.utils import safe_hasattr
from base64 import b64decode
from email.utils import formatdate
from fnmatch import fnmatch
Expand Down Expand Up @@ -217,62 +220,11 @@ def reply(self):
if hasattr(request_body, 'raw'): # Unwrap io.BufferedRandom
request_body = request_body.raw
tus_upload.write(request_body, offset)
offset = tus_upload.offset()

if tus_upload.finished:
offset = tus_upload.offset()
filename = metadata.get('filename', '')
content_type = metadata.get('content-type',
'application/octet-stream')
mode = metadata.get('mode', 'create')
fieldname = metadata.get('fieldname')

if mode == 'create':
type_ = metadata.get('@type')
if type_ is None:
ctr = getToolByName(self.context, 'content_type_registry')
type_ = ctr.findTypeName(
filename.lower(), content_type, '') or 'File'

obj = create(self.context, type_)
else:
obj = self.context

if not fieldname:
info = IPrimaryFieldInfo(obj, None)
if info is not None:
fieldname = info.fieldname
elif base_hasattr(obj, 'getPrimaryField'):
field = obj.getPrimaryField()
fieldname = field.getName()

if not fieldname:
return self.error('Bad Request', 'Fieldname required', 400)

# Update field with file data
deserializer = queryMultiAdapter(
(obj, self.request), IDeserializeFromJson)
if deserializer is None:
return self.error(
'Not Implemented',
'Cannot deserialize type {}'.format(
obj.portal_type),
501)
try:
deserializer(data={fieldname: tus_upload})
except DeserializationError as e:
return self.error(
'Deserialization Error', str(e), 400)

if mode == 'create':
if not getattr(deserializer, 'notifies_create', False):
notify(ObjectCreatedEvent(obj))
obj = add(self.context, obj)

tus_upload.close()
tus_upload.cleanup()
self.request.response.setHeader('Location', obj.absolute_url())
self.create_or_modify_content(tus_upload)
else:
offset = tus_upload.offset()
self.request.response.setHeader(
'Upload-Expires', tus_upload.expires())

Expand All @@ -281,6 +233,70 @@ def reply(self):
self.request.response.setStatus(204, lock=1)
return super(UploadPatch, self).reply()

def create_or_modify_content(self, tus_upload):
metadata = tus_upload.metadata()
filename = metadata.get('filename', '')
content_type = metadata.get('content-type',
'application/octet-stream')
mode = metadata.get('mode', 'create')
fieldname = metadata.get('fieldname')

if mode == 'create':
type_ = metadata.get('@type')
if type_ is None:
ctr = getToolByName(self.context, 'content_type_registry')
type_ = ctr.findTypeName(
filename.lower(), content_type, '') or 'File'

obj = create(self.context, type_)
else:
obj = self.context

if not fieldname:
info = IPrimaryFieldInfo(obj, None)
if info is not None:
fieldname = info.fieldname
elif base_hasattr(obj, 'getPrimaryField'):
field = obj.getPrimaryField()
fieldname = field.getName()

if not fieldname:
return self.error('Bad Request', 'Fieldname required', 400)

# Acquisition wrap temporarily for deserialization
temporarily_wrapped = False
if IAcquirer.providedBy(obj) and not safe_hasattr(obj, 'aq_base'):
obj = obj.__of__(self.context)
temporarily_wrapped = True

# Update field with file data
deserializer = queryMultiAdapter(
(obj, self.request), IDeserializeFromJson)
if deserializer is None:
return self.error(
'Not Implemented',
'Cannot deserialize type {}'.format(
obj.portal_type),
501)
try:
deserializer(
data={fieldname: tus_upload}, create=mode == 'create')
except DeserializationError as e:
return self.error(
'Deserialization Error', str(e), 400)

if temporarily_wrapped:
obj = aq_base(obj)

if mode == 'create':
if not getattr(deserializer, 'notifies_create', False):
notify(ObjectCreatedEvent(obj))
obj = add(self.context, obj)

tus_upload.close()
tus_upload.cleanup()
self.request.response.setHeader('Location', obj.absolute_url())


class TUSUpload(object):

Expand Down
122 changes: 117 additions & 5 deletions src/plone/restapi/tests/test_tus.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
# -*- coding: utf-8 -*-
from DateTime import DateTime
from six import BytesIO
from base64 import b64encode
from DateTime import DateTime
from OFS.interfaces import IObjectWillBeAddedEvent
from plone import api
from plone.app.testing import login
from plone.app.testing import setRoles
from plone.app.testing import SITE_OWNER_NAME
from plone.app.testing import SITE_OWNER_PASSWORD
from plone.app.testing import TEST_USER_ID
from plone.app.testing import TEST_USER_NAME
from plone.app.testing import TEST_USER_PASSWORD
from plone.app.testing import login
from plone.app.testing import setRoles
from plone.rest.cors import CORSPolicy
from plone.rest.interfaces import ICORSPolicy
from plone.restapi.services.content.tus import TUSUpload
from plone.restapi import HAS_AT
from plone.restapi.services.content.tus import TUSUpload
from plone.restapi.testing import PLONE_RESTAPI_AT_FUNCTIONAL_TESTING
from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING
from plone.restapi.testing import RelativeSession
from six import BytesIO
from zope.component import getGlobalSiteManager
from zope.component import provideAdapter
from zope.interface import Interface
from zope.lifecycleevent.interfaces import IObjectAddedEvent
from zope.lifecycleevent.interfaces import IObjectCreatedEvent
from zope.lifecycleevent.interfaces import IObjectModifiedEvent
from zope.publisher.interfaces.browser import IBrowserRequest

import os
Expand Down Expand Up @@ -399,6 +403,114 @@ def test_tus_can_replace_pdf_file(self):
self.assertEqual(UPLOAD_PDF_FILENAME, self.file.file.filename)
self.assertEqual(pdf_file_size, self.file.file.size)

def test_create_with_tus_fires_proper_events(self):
sm = getGlobalSiteManager()
fired_events = []

def record_event(event):
fired_events.append(event.__class__.__name__)

sm.registerHandler(record_event, (IObjectCreatedEvent,))
sm.registerHandler(record_event, (IObjectWillBeAddedEvent,))
sm.registerHandler(record_event, (IObjectAddedEvent,))
sm.registerHandler(record_event, (IObjectModifiedEvent,))

# initialize the upload with POST
pdf_file_path = os.path.join(os.path.dirname(__file__),
UPLOAD_PDF_FILENAME)
pdf_file_size = os.path.getsize(pdf_file_path)
metadata = _prepare_metadata(UPLOAD_PDF_FILENAME, UPLOAD_PDF_MIMETYPE)
response = self.api_session.post(
self.upload_url,
headers={'Tus-Resumable': '1.0.0',
'Upload-Length': str(pdf_file_size),
'Upload-Metadata': metadata}
)
self.assertEqual(response.status_code, 201)
location = response.headers['Location']

# upload the data with PATCH
with open(pdf_file_path, 'rb') as pdf_file:
response = self.api_session.patch(
location,
headers={
'Content-Type': 'application/offset+octet-stream',
'Upload-Offset': '0',
'Tus-Resumable': '1.0.0'
},
data=pdf_file)
self.assertEqual(response.status_code, 204)

self.assertEqual(
fired_events,
[
'ObjectCreatedEvent',
'ObjectWillBeAddedEvent',
'ObjectAddedEvent',
'ContainerModifiedEvent',
])

sm.unregisterHandler(record_event, (IObjectCreatedEvent,))
sm.unregisterHandler(record_event, (IObjectWillBeAddedEvent,))
sm.unregisterHandler(record_event, (IObjectAddedEvent,))
sm.unregisterHandler(record_event, (IObjectModifiedEvent,))

def test_replace_with_tus_fires_proper_events(self):
# Create a test file
self.file = api.content.create(container=self.portal,
type='File',
id='testfile',
title='Testfile')
transaction.commit()

sm = getGlobalSiteManager()
fired_events = []

def record_event(event):
fired_events.append(event.__class__.__name__)

sm.registerHandler(record_event, (IObjectCreatedEvent,))
sm.registerHandler(record_event, (IObjectWillBeAddedEvent,))
sm.registerHandler(record_event, (IObjectAddedEvent,))
sm.registerHandler(record_event, (IObjectModifiedEvent,))

# initialize the upload with POST
pdf_file_path = os.path.join(os.path.dirname(__file__),
UPLOAD_PDF_FILENAME)
pdf_file_size = os.path.getsize(pdf_file_path)
metadata = _prepare_metadata(UPLOAD_PDF_FILENAME, UPLOAD_PDF_MIMETYPE)
response = self.api_session.post(
'{}/@tus-replace'.format(self.file.absolute_url()),
headers={'Tus-Resumable': '1.0.0',
'Upload-Length': str(pdf_file_size),
'Upload-Metadata': metadata}
)
self.assertEqual(response.status_code, 201)
location = response.headers['Location']

# upload the data with PATCH
with open(pdf_file_path, 'rb') as pdf_file:
response = self.api_session.patch(
location,
headers={
'Content-Type': 'application/offset+octet-stream',
'Upload-Offset': '0',
'Tus-Resumable': '1.0.0'
},
data=pdf_file)
self.assertEqual(response.status_code, 204)

self.assertEqual(
fired_events,
[
'ObjectModifiedEvent',
])

sm.unregisterHandler(record_event, (IObjectCreatedEvent,))
sm.unregisterHandler(record_event, (IObjectWillBeAddedEvent,))
sm.unregisterHandler(record_event, (IObjectAddedEvent,))
sm.unregisterHandler(record_event, (IObjectModifiedEvent,))

def tearDown(self):
self.api_session.close()
client_home = os.environ.get('CLIENT_HOME')
Expand Down

0 comments on commit 390214a

Please sign in to comment.