=================
Named File Widget
=================

This package contains two widgets: the NamedFileWidget will be used when
rendering NamedFile objects from plone.namedfile. The NamedImageWidget is used
for NamedImage objects. This includes their sub-classes NamedBlobFile and
NamedBlobImage.

Like any widget, the file widgets provide the ``IWidget`` interface:

  >>> from zope.interface.verify import verifyClass
  >>> from z3c.form import interfaces
  >>> from plone.formwidget.namedfile import NamedFileWidget, NamedImageWidget

  >>> verifyClass(interfaces.IWidget, NamedFileWidget)
  True
  >>> verifyClass(interfaces.IWidget, NamedImageWidget)
  True

There are also more specific interfaces for each widget:

    >>> from plone.formwidget.namedfile.interfaces import INamedFileWidget
    >>> from plone.formwidget.namedfile.interfaces import INamedImageWidget

    >>> verifyClass(INamedFileWidget, NamedFileWidget)
    True
    >>> verifyClass(INamedImageWidget, NamedImageWidget)
    True

The widgets can be instantiated only using the request:

  >>> from z3c.form.testing import TestRequest
  >>> request = TestRequest()

  >>> file_widget = NamedFileWidget(request)
  >>> image_widget = NamedImageWidget(request)

Before rendering a widget, one has to set the name and id of the widget:

  >>> file_widget.id = 'widget.id.file'
  >>> file_widget.name = 'widget.name.file'

  >>> image_widget.id = 'widget.id.image'
  >>> image_widget.name = 'widget.name.image'

We also need to register the templates for the widgets:

  >>> import zope.component
  >>> from zope.pagetemplate.interfaces import IPageTemplate
  >>> from z3c.form.widget import WidgetTemplateFactory

  >>> def getPath(filename):
  ...     import os.path
  ...     import plone.formwidget.namedfile
  ...     return os.path.join(os.path.dirname(plone.formwidget.namedfile.__file__), filename)

  >>> zope.component.provideAdapter(
  ...     WidgetTemplateFactory(getPath('file_input.pt'), 'text/html'),
  ...     (None, None, None, None, INamedFileWidget),
  ...     IPageTemplate, name=interfaces.INPUT_MODE)

  >>> zope.component.provideAdapter(
  ...     WidgetTemplateFactory(getPath('image_input.pt'), 'text/html'),
  ...     (None, None, None, None, INamedImageWidget),
  ...     IPageTemplate, name=interfaces.INPUT_MODE)

If we render the widget as this we get an input element in a simple wrapper.
Later, we will show more advanced functionality when using a field-widget.

  >>> file_widget.update()
  >>> print(file_widget.render())
  <span class="named-file-widget" id="widget.id.file">
    <input type="file" id="widget.id.file-input"
           name="widget.name.file" />
  </span>

  >>> image_widget.update()
  >>> print(image_widget.render())
  <span class="named-image-widget" id="widget.id.image">
      <input type="file" id="widget.id.image-input"
             name="widget.name.image" />
  </span>

We can extract simple file data from the widget like this:

  >>> import cStringIO
  >>> myfile = cStringIO.StringIO('My file contents.')

  >>> file_widget.request = TestRequest(form={'widget.name.file': myfile})
  >>> file_widget.update()
  >>> file_widget.extract()
  <cStringIO.StringI object at ...>

  >>> image_widget.request = TestRequest(form={'widget.name.image': myfile})
  >>> image_widget.update()
  >>> image_widget.extract()
  <cStringIO.StringI object at ...>

If nothing is found in the request, the default is returned:

  >>> file_widget.request = TestRequest()
  >>> file_widget.update()
  >>> file_widget.extract()
  <NO_VALUE>

  >>> image_widget.request = TestRequest()
  >>> image_widget.update()
  >>> image_widget.extract()
  <NO_VALUE>

We can also handle file-upload objects.

  >>> import cStringIO
  >>> from ZPublisher.HTTPRequest import FileUpload

Let's define a FieldStorage stub:

  >>> class FieldStorageStub:
  ...     def __init__(self, file, headers={}, filename='foo.bar'):
  ...         self.file = file
  ...         self.headers = headers
  ...         self.filename = filename

Now build a FileUpload:

  >>> myfile = cStringIO.StringIO('File upload contents.')
  >>> aFieldStorage = FieldStorageStub(myfile)
  >>> myUpload = FileUpload(aFieldStorage)

  >>> file_widget.request = TestRequest(form={'widget.name.file': myUpload})
  >>> file_widget.update()
  >>> file_widget.extract()
  <ZPublisher.HTTPRequest.FileUpload instance at ...>

  >>> image_widget.request = TestRequest(form={'widget.name.image': myUpload})
  >>> image_widget.update()
  >>> image_widget.extract()
  <ZPublisher.HTTPRequest.FileUpload instance at ...>

The rendering is unchanged.

  >>> print(file_widget.render())
  <span class="named-file-widget" id="widget.id.file">
      <input type="file" id="widget.id.file-input"
             name="widget.name.file" />
  </span>

  >>> print(image_widget.render())
  <span class="named-image-widget" id="widget.id.image">
      <input type="file" id="widget.id.image-input"
             name="widget.name.image" />
  </span>

Empty, unnamed FileUploads are treated as having no value:

  >>> emptyfile = cStringIO.StringIO('')
  >>> aFieldStorage = FieldStorageStub(emptyfile, filename='')
  >>> myEmptyUpload = FileUpload(aFieldStorage)

  >>> file_widget.request = TestRequest(form={'widget.name.file': myEmptyUpload})
  >>> file_widget.update()
  >>> file_widget.extract()
  <NO_VALUE>

  >>> image_widget.request = TestRequest(form={'widget.name.image': myEmptyUpload})
  >>> image_widget.update()
  >>> image_widget.extract()
  <NO_VALUE>


Rendering field widgets
-----------------------

If the widgets are used as field widgets for the fields in plone.namedfile,
we get more interesting behaviour: the user may either select to provide a
new file, or keep the existing one.

For this to work, we need a context and a data manager.

  >>> from plone.namedfile import field
  >>> from zope.interface import implements, Interface
  >>> class IContent(Interface):
  ...     file_field = field.NamedFile(title=u"File")
  ...     image_field = field.NamedImage(title=u"Image")

  >>> class Content(object):
  ...     implements(IContent)
  ...     def __init__(self, file, image):
  ...         self.file_field = file
  ...         self.image_field = image
  ...
  ...     def absolute_url(self):
  ...         return 'http://example.com/content1'

  >>> content = Content(None, None)

  >>> from z3c.form.datamanager import AttributeField
  >>> from zope.component import provideAdapter
  >>> provideAdapter(AttributeField)

  >>> from plone.formwidget.namedfile import NamedFileFieldWidget
  >>> from plone.formwidget.namedfile import NamedImageFieldWidget

  >>> file_widget = NamedFileFieldWidget(IContent['file_field'], TestRequest())
  >>> image_widget = NamedImageFieldWidget(IContent['image_field'], TestRequest())

  >>> file_widget.context = content
  >>> image_widget.context = content

  >>> file_widget.id = 'widget.id.file'
  >>> file_widget.name = 'widget.name.file'

  >>> image_widget.id = 'widget.id.image'
  >>> image_widget.name = 'widget.name.image'

At first, there is no value, so the behaviour is much like before:

  >>> file_widget.update()
  >>> print(file_widget.render())
  <span class="named-file-widget required namedfile-field"
        id="widget.id.file">
      <input type="file" id="widget.id.file-input"
             name="widget.name.file" />
  </span>

  >>> image_widget.update()
  >>> print(image_widget.render())
  <span class="named-image-widget required namedimage-field"
        id="widget.id.image">
      <input type="file" id="widget.id.image-input"
             name="widget.name.image" />
  </span>

However, if we now set a value, we will have the option of keeping it,
or changing it.  The filename can handle unicode and international
characters.

  >>> from plone.namedfile import NamedFile, NamedImage
  >>> file_widget.value = NamedFile(data='My file data',
  ...                               filename=unicode('data_深.txt', 'utf-8'))
  >>> image_widget.value = NamedImage(data='My image data', filename=u'faux.png')

  >>> file_widget.update()
  >>> print(file_widget.render())
  <span class="named-file-widget required namedfile-field"
        id="widget.id.file">
      <span>
          <a href="http://127.0.0.1/++widget++widget.name.file/@@download/data_%E6%B7%B1.txt">data_深.txt</a>
          <span class="discreet"> &mdash;
              text/plain,
              1 KB
          </span>
      </span>
      <div style="padding-top: 1em;">
          <input type="radio" value="nochange"
                 class="noborder" checked="checked"
                 name="widget.name.file.action"
                 onclick="document.getElementById('widget.id.file-input').disabled=true"
                 id="widget.id.file-nochange" />
          <label for="widget.id.file-nochange">Keep existing file</label>
  <BLANKLINE>
          <br />
          <input type="radio" value="replace" class="noborder"
                 name="widget.name.file.action"
                 onclick="document.getElementById('widget.id.file-input').disabled=false"
                 id="widget.id.file-replace" />
          <label for="widget.id.file-replace">Replace with new file</label>
      </div>
      <div style="padding-left: 1.5em; padding-top: 0.5em;">
          <input type="file" id="widget.id.file-input"
                 name="widget.name.file" />
          <script type="text/javascript">document.getElementById('widget.id.file-input').disabled=true;</script>
      </div>
  </span>
  <BLANKLINE>

  >>> image_widget.update()
  >>> print(image_widget.render())
  <span class="named-image-widget required namedimage-field"
        id="widget.id.image">
      <span>
          <img src="http://127.0.0.1/++widget++widget.name.image/@@download/faux.png"
               width="-1" /><br />
          <a href="http://127.0.0.1/++widget++widget.name.image/@@download/faux.png">faux.png</a>
          <span class="discreet"> &mdash;
              1 KB
          </span>
      </span>
      <div style="padding-top: 1em;">
          <input type="radio" value="nochange"
                 class="noborder" checked="checked"
                 name="widget.name.image.action"
                 onclick="document.getElementById('widget.id.image-input').disabled=true"
                 id="widget.id.image-nochange" />
          <label for="widget.id.image-nochange">Keep existing image</label>
  <BLANKLINE>
          <br />
          <input type="radio" value="replace" class="noborder"
                 name="widget.name.image.action"
                 onclick="document.getElementById('widget.id.image-input').disabled=false"
                 id="widget.id.image-replace" />
          <label for="widget.id.image-replace">Replace with new image</label>
      </div>
      <div style="padding-left: 1.5em; padding-top: 0.5em;">
          <input type="file" id="widget.id.image-input"
                 name="widget.name.image" />
          <script type="text/javascript">document.getElementById('widget.id.image-input').disabled=true;</script>
      </div>
  </span>
  <BLANKLINE>

(the image height and width are taken from the NamedImage instance, and here
default to -1 since we didn't upload a real image)

Notice how there are radio buttons to decide whether to upload a new file or
keep the existing one. If the '.action' field is not submitted or is
empty, the behaviour is the same as before:

  >>> myfile = cStringIO.StringIO('File upload contents.')
  >>> aFieldStorage = FieldStorageStub(myfile, filename='test2.txt')
  >>> myUpload = FileUpload(aFieldStorage)

  >>> file_widget.request = TestRequest(form={'widget.name.file': myUpload})
  >>> file_widget.update()
  >>> file_widget.extract()
  <ZPublisher.HTTPRequest.FileUpload instance at ...>

  >>> myfile = cStringIO.StringIO('Random image content.')
  >>> aFieldStorage = FieldStorageStub(myfile, filename='faux2.png')
  >>> myUpload = FileUpload(aFieldStorage)

  >>> image_widget.request = TestRequest(form={'widget.name.image': myUpload})
  >>> image_widget.update()
  >>> image_widget.extract()
  <ZPublisher.HTTPRequest.FileUpload instance at ...>

If the widgets are rendered again, the newly uploaded files will be shown.

  >>> print(file_widget.render())
  <span class="named-file-widget required namedfile-field"
        id="widget.id.file">
      <span>
          <a href="http://127.0.0.1/++widget++widget.name.file/@@download/test2.txt">test2.txt</a>
          <span class="discreet"> &mdash;
              0 KB
          </span>
      </span>
      <div style="padding-top: 1em;">
          <input type="radio" value="nochange"
                 class="noborder" checked="checked"
                 name="widget.name.file.action"
                 onclick="document.getElementById('widget.id.file-input').disabled=true"
                 id="widget.id.file-nochange" />
          <label for="widget.id.file-nochange">Keep existing file</label>
  <BLANKLINE>
          <br />
          <input type="radio" value="replace" class="noborder"
                 name="widget.name.file.action"
                 onclick="document.getElementById('widget.id.file-input').disabled=false"
                 id="widget.id.file-replace" />
          <label for="widget.id.file-replace">Replace with new file</label>
      </div>
      <div style="padding-left: 1.5em; padding-top: 0.5em;">
          <input type="file" id="widget.id.file-input"
                 name="widget.name.file" />
          <script type="text/javascript">document.getElementById('widget.id.file-input').disabled=true;</script>
      </div>
  </span>
  <BLANKLINE>


  >>> print(image_widget.render())
  <span class="named-image-widget required namedimage-field"
        id="widget.id.image">
      <span>
          <img src="http://127.0.0.1/++widget++widget.name.image/@@download/faux2.png"
               width="128" /><br />
          <a href="http://127.0.0.1/++widget++widget.name.image/@@download/faux2.png">faux2.png</a>
          <span class="discreet"> &mdash;
              0 KB
          </span>
      </span>
      <div style="padding-top: 1em;">
          <input type="radio" value="nochange"
                 class="noborder" checked="checked"
                 name="widget.name.image.action"
                 onclick="document.getElementById('widget.id.image-input').disabled=true"
                 id="widget.id.image-nochange" />
          <label for="widget.id.image-nochange">Keep existing image</label>
  <BLANKLINE>
          <br />
          <input type="radio" value="replace" class="noborder"
                 name="widget.name.image.action"
                 onclick="document.getElementById('widget.id.image-input').disabled=false"
                 id="widget.id.image-replace" />
          <label for="widget.id.image-replace">Replace with new image</label>
      </div>
      <div style="padding-left: 1.5em; padding-top: 0.5em;">
          <input type="file" id="widget.id.image-input"
                 name="widget.name.image" />
          <script type="text/javascript">document.getElementById('widget.id.image-input').disabled=true;</script>
      </div>
  </span>
  <BLANKLINE>

However, if we provide the '.action' field, we get back the value currently
stored in the field.

  >>> content.file_field = NamedFile(data='My file data', filename=u'data.txt')
  >>> content.image_field = NamedImage(data='My image data', filename=u'faux.png')

  >>> file_widget.value = content.file_field
  >>> image_widget.value = content.image_field

  >>> file_widget.request = TestRequest(form={'widget.name.file': '', 'widget.name.file.action': 'nochange'})
  >>> file_widget.update()
  >>> file_widget.extract() is content.file_field
  True

  >>> myfile = cStringIO.StringIO('Random image content.')
  >>> aFieldStorage = FieldStorageStub(myfile, filename='faux2.png')
  >>> myUpload = FileUpload(aFieldStorage)

  >>> image_widget.request = TestRequest(form={'widget.name.image': '', 'widget.name.image.action': 'nochange'})
  >>> image_widget.update()
  >>> image_widget.extract() is content.image_field
  True

Download view
-------------

The download view extracts the image/file data, the widget template output uses
this view to display the image itself or link to the file.

  >>> from plone.formwidget.namedfile.widget import Download
  >>> request = TestRequest()
  >>> view = Download(image_widget, request)
  >>> view()
  'My image data'
  >>> request.response.getHeader('Content-Disposition')
  'attachment; filename="faux.png"'

  >>> request = TestRequest()
  >>> view = Download(file_widget, request)
  >>> view()
  'My file data'
  >>> request.response.getHeader('Content-Disposition')
  'attachment; filename="data.txt"'

The URL will influence the name of the file as reported to the browser, but
doesn't stop it being found.

  >>> request = TestRequest()
  >>> view = Download(file_widget, request)
  >>> view = view.publishTraverse(request, 'daisy.txt')
  >>> view()
  'My file data'
  >>> request.response.getHeader('Content-Disposition')
  'attachment; filename="daisy.txt"'

Any additional traversal will result in an error.

  >>> request = TestRequest()
  >>> view = Download(file_widget, request)
  >>> view = view.publishTraverse(request, 'cows')
  >>> view = view.publishTraverse(request, 'daisy.txt')
  Traceback (most recent call last):
  ...
  NotFound: ... 'daisy.txt'

The converter
-------------

This package comes with a data converter that can convert a file upload
instance to a named file. It is registered to work on all named file/image
instances and the two named file/image widgets.

  >>> from plone.formwidget.namedfile.converter import NamedDataConverter
  >>> provideAdapter(NamedDataConverter)

  >>> from zope.component import getMultiAdapter
  >>> from z3c.form.interfaces import IDataConverter

  >>> file_converter = getMultiAdapter((IContent['file_field'], file_widget), IDataConverter)
  >>> image_converter = getMultiAdapter((IContent['image_field'], image_widget), IDataConverter)

A value of None or '' results in the field's missing_value being returned.

  >>> file_converter.toFieldValue(u'') is IContent['file_field'].missing_value
  True
  >>> file_converter.toFieldValue(None) is IContent['file_field'].missing_value
  True

  >>> image_converter.toFieldValue(u'') is IContent['image_field'].missing_value
  True
  >>> image_converter.toFieldValue(None) is IContent['image_field'].missing_value
  True

A named file/image instance is returned as-is:

  >>> file_converter.toFieldValue(content.file_field) is content.file_field
  True
  >>> image_converter.toFieldValue(content.image_field) is content.image_field
  True

A data string is converted to the appropriate type:

  >>> file_converter.toFieldValue('some file content')
  <plone.namedfile.file.NamedFile object at ...>

  >>> image_converter.toFieldValue('random data')
  <plone.namedfile.file.NamedImage object at ...>

A FileUpload object is converted to the appropriate type, preserving filename
and content type, and possibly handling international characters in filenames.

  >>> myfile = cStringIO.StringIO('File upload contents.')
  >>> # \xc3\xb8 is UTF-8 for a small letter o with slash
  >>> aFieldStorage = FieldStorageStub(myfile, filename='rand\xc3\xb8m.txt',
  ...     headers={'Content-Type': 'text/x-dummy'})
  >>> file_obj = file_converter.toFieldValue(FileUpload(aFieldStorage))
  >>> file_obj.data
  'File upload contents.'
  >>> file_obj.filename
  u'rand\xf8m.txt'
  >>> file_obj.contentType
  'text/x-dummy'

  >>> myfile = cStringIO.StringIO('Random image content.')
  >>> aFieldStorage = FieldStorageStub(myfile, filename='random.png', headers={'Content-Type': 'image/x-dummy'})
  >>> image_obj = image_converter.toFieldValue(FileUpload(aFieldStorage))
  >>> image_obj.data
  'Random image content.'
  >>> image_obj.filename
  u'random.png'
  >>> image_obj.contentType
  'image/x-dummy'

However, a zero-length, unnamed FileUpload results in the field's missing_value
being returned.

  >>> myfile = cStringIO.StringIO('')
  >>> aFieldStorage = FieldStorageStub(myfile, filename='', headers={'Content-Type': 'application/octet-stream'})
  >>> field_value = file_converter.toFieldValue(FileUpload(aFieldStorage))
  >>> field_value is IContent['file_field'].missing_value
  True
  >>> field_value = image_converter.toFieldValue(FileUpload(aFieldStorage))
  >>> field_value is IContent['file_field'].missing_value
  True

The validator
-------------

If the user clicked 'replace' but did not provide a file, we want to get a
validation error.

  >>> from plone.formwidget.namedfile.validator import NamedFileWidgetValidator

If 'action' is omitted and the value is None, we should get a validation error
only when the field is required.

  >>> request = TestRequest(form={'widget.name.file': myfile})
  >>> validator = NamedFileWidgetValidator(content, request, None, IContent['file_field'], file_widget)
  >>> validator.validate(None) is None
  Traceback (most recent call last):
  ...
  RequiredMissing...
  >>> IContent['file_field'].required = False
  >>> validator.validate(None) is None
  True

However, if it is set to 'replace' and there is no value provided, we get the
InvalidState exception from validator.py (its docstring is displayed to the
user).

  >>> request = TestRequest(form={'widget.name.file': myfile, 'widget.name.file.action': 'replace'})
  >>> validator = NamedFileWidgetValidator(content, request, None, IContent['file_field'], file_widget)
  >>> validator.validate(None)
  Traceback (most recent call last):
  ...
  InvalidState

If we provide a file, all is good.

  >>> request = TestRequest(form={'widget.name.file': myfile, 'widget.name.file.action': 'replace'})
  >>> validator = NamedFileWidgetValidator(content, request, None, IContent['file_field'], file_widget)
  >>> validator.validate(file_obj) is None
  True

Similarly, if we really wanted to remove the file, we won't complain, unless
we again make the field required.

  >>> request = TestRequest(form={'widget.name.file': myfile, 'widget.name.file.action': 'remove'})
  >>> validator = NamedFileWidgetValidator(content, request, None, IContent['file_field'], file_widget)
  >>> validator.validate(None) is None
  True
  >>> IContent['file_field'].required = True
  >>> validator.validate(None) is None
  Traceback (most recent call last):
  ...
  RequiredMissing...
