"""Base material for download views: :class:`DownloadMixin` and :class:`BaseDownloadView`""" import calendar from django.http import Http404, HttpResponseNotModified from django.views.generic.base import View from django.views.static import was_modified_since from django_downloadview import exceptions from django_downloadview.response import DownloadResponse class DownloadMixin(object): """Placeholders and base implementation to create file download views. .. note:: This class does not inherit from :py:class:`django.views.generic.base.View`. The :py:meth:`get_file` method is a placeholder subclasses must implement. Base implementation raises ``NotImplementedError``. Other methods provide a base implementation that use the file wrapper returned by :py:meth:`get_file`. """ #: Response class, to be used in :py:meth:`render_to_response`. response_class = DownloadResponse #: Whether to return the response as attachment or not. #: #: When ``True`` (the default), the view returns file "as attachment", #: which usually triggers a "Save the file as ..." prompt. #: #: When ``False``, the view returns file "inline", as if it was an element #: of the current page. #: #: .. note:: #: #: The actual behaviour client-side depends on the browser and its #: configuration. #: #: In fact, affects the "Content-Disposition" header via :attr:`response's #: attachment attribute #: `. attachment = True #: Client-side filename, if only file is returned as attachment. basename = None #: File's mime type. #: If ``None`` (the default), then the file's mime type will be guessed via #: :mod:`mimetypes`. mimetype = None #: File's encoding. #: If ``None`` (the default), then the file's encoding will be guessed via #: :mod:`mimetypes`. encoding = None def get_file(self): """Return a file wrapper instance. Raises :class:`~django_downloadview.exceptions.FileNotFound` if file does not exist. """ raise NotImplementedError() def get_basename(self): """Return :attr:`basename`. Override this method if you need more dynamic basename. """ return self.basename def get_mimetype(self): """Return :attr:`mimetype`. Override this method if you need more dynamic mime type. """ return self.mimetype def get_encoding(self): """Return :attr:`encoding`. Override this method if you need more dynamic encoding. """ return self.encoding def was_modified_since(self, file_instance, since): """Return True if ``file_instance`` was modified after ``since``. Uses file wrapper's ``was_modified_since`` if available, with value of ``since`` as positional argument. Else, fallbacks to default implementation, which uses :py:func:`django.views.static.was_modified_since`. Django's ``was_modified_since`` function needs a datetime. It is passed the ``modified_time`` attribute from file wrapper. If file wrapper does not support this attribute (``AttributeError`` or ``NotImplementedError`` is raised), then the file is considered as modified and ``True`` is returned. """ try: return file_instance.was_modified_since(since) except (AttributeError, NotImplementedError): try: modification_time = calendar.timegm( file_instance.modified_time.utctimetuple() ) except (AttributeError, NotImplementedError) as e: print("!=======!", e) return True else: return was_modified_since(since, modification_time) def not_modified_response(self, *response_args, **response_kwargs): """Return :class:`django.http.HttpResponseNotModified` instance.""" return HttpResponseNotModified(*response_args, **response_kwargs) def download_response(self, *response_args, **response_kwargs): """Return :class:`~django_downloadview.response.DownloadResponse`.""" response_kwargs.setdefault("file_instance", self.file_instance) response_kwargs.setdefault("attachment", self.attachment) response_kwargs.setdefault("basename", self.get_basename()) response_kwargs.setdefault("file_mimetype", self.get_mimetype()) response_kwargs.setdefault("file_encoding", self.get_encoding()) response = self.response_class(*response_args, **response_kwargs) return response def file_not_found_response(self): """Raise Http404.""" raise Http404() def render_to_response(self, *response_args, **response_kwargs): """Return "download" response (if everything is ok). Return :meth:`file_not_found_response` if file does not exist. Respects the "HTTP_IF_MODIFIED_SINCE" header if any. In that case, uses :py:meth:`was_modified_since` and :py:meth:`not_modified_response`. Else, uses :py:meth:`download_response` to return a download response. """ try: self.file_instance = self.get_file() except exceptions.FileNotFound: return self.file_not_found_response() # Respect the If-Modified-Since header. since = self.request.headers.get("if-modified-since", None) if since is not None: if not self.was_modified_since(self.file_instance, since): return self.not_modified_response(**response_kwargs) # Return download response. return self.download_response(*response_args, **response_kwargs) class BaseDownloadView(DownloadMixin, View): """A base :class:`DownloadMixin` that implements :meth:`get`.""" def get(self, request, *args, **kwargs): """Handle GET requests: stream a file.""" return self.render_to_response()