Skip to content

Commit

Permalink
Merge pull request #30 from Hafnernuss/release/0.5.0
Browse files Browse the repository at this point in the history
Release 0.5.0
  • Loading branch information
Hafnernuss authored Jul 7, 2023
2 parents 78d644e + 69939ce commit 964f452
Show file tree
Hide file tree
Showing 25 changed files with 362 additions and 77 deletions.
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,5 +131,6 @@ dmypy.json
# user configured:
_build/
test_files

cov.xml
static/

cov.xml
15 changes: 15 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.. _api:

######################
API
######################

.. currentmodule:: dynamic_file.models

================
DynamicFile
================
.. autoclass:: DynamicFile
:members:
:inherited-members: Model
:noindex:
32 changes: 32 additions & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.. _changelog:

######################
Changelog
######################
This document provides an overview about breaking changes and new features.


***************************************************
0.5.0
***************************************************

Changes
****************************************************
The setting ``DYNAMIC_FILE_UPLOADED_BY_RELATED_NAME`` has been removed.
If you want to know the uploaded files from your ``uploader entity``, use the following:

.. code-block:: python3
DynamicFile.objects.filter(uploaded_by=your_entity_id)
Features
****************************************************

* If the file is an image, a preview will be shown in the admin (relies on ``mimetypes``)
* Added a field ``display_name`` to provide a human-readable name for dynamic files
* Added some ``base64`` utilities for commonly used tasks

***************************************************
0.4.0
***************************************************
First useable version
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@


project = 'django-dynamic-file'
copyright = '2022, Philipp Hafner'
copyright = '2023, Philipp Hafner'
author = 'Philipp Hafner'

release = '0.0'
Expand Down
5 changes: 4 additions & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ This library aims at providing an easy interface for handling files in conjuncti
methods for applying generic permissions to serving, updating and deleting files.

This is done by wrapping `FileField` in their own model.

The following documents provide a guide on how to install, configure and use this library.


.. toctree::
:maxdepth: 1

installation
configuration
usage
changelog
api

8 changes: 0 additions & 8 deletions docs/source/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,6 @@ DYNAMIC_FILE_UPLOADED_BY_MODEL
This setting defines the foreign key that determines which entity has uploaded the file. It defaults to the user model.


DYNAMIC_FILE_UPLOADED_BY_RELATED_NAME
****************************************************
.. code-block:: python3
DYNAMIC_FILE_UPLOADED_BY_RELATED_NAME = 'uploaded_files'
Defines the related name for all uploaded files that is added to the model specified by ``DYNAMIC_FILE_UPLOADED_BY_MODEL``.

DYNAMIC_FILE_UPLOADED_BY_MODEL_MIGRATION_DEPENDENCY
****************************************************
.. code-block:: python3
Expand Down
47 changes: 46 additions & 1 deletion docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,49 @@ There is a provided default view which handles file serving via a passed ``pk``,
]
Now, this view name (``serve_default``) can be used in serializers, as described above.
Now, this view name (``serve_default``) can be used in serializers, as described above.


***************************************************
Admin integration
***************************************************
``DynamicFile`` has a pretty basic but useful admin integration.
It supports a preview rendering in case the file is an image.
In case it's not an image, no preview will be shown.


.. code-block:: python3
from .models import Company
from django.contrib import admin
from dynamic_file.admin import preview as image_preview
@admin.register(Company)
class CompanyAdmin(admin.ModelAdmin):
fields = ['image', 'preview']
readonly_fields = ['preview']
def preview(self, instance):
return image_preview(instance.image)
Files can also be downloaded from within the admin with a provided view.
Simply add the following to your ``urls.py`` and make sure that you place it
**before** the actual import of the admin pages:

.. code-block:: python3
from dynamic_file.views import ServeDynamicFileAdmin
urlpatterns = [
#... your other includes
path('admin/download/<str:name>', ServeDynamicFileAdmin.as_view()),
path('admin/', admin.site.urls),
]
This enables downloadable files for every user that has access to the admin page.
20 changes: 19 additions & 1 deletion dynamic_file/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
from django.contrib import admin

from .models import DynamicFile
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _


def preview(dynamic_file):
try:
mimetype = dynamic_file.mimetype
if mimetype and 'image' in mimetype:
src = dynamic_file.to_base64_src()
return mark_safe(f'<img src="{src}" width="150" />')
else:
return _('No preview available')
except Exception as e:
return _(f'No preview available: {str(e)}')


@admin.register(DynamicFile)
class CourseGroupAdmin(admin.ModelAdmin):
class DynamicFileAdmin(admin.ModelAdmin):
list_display = ['id', 'name']
readonly_fields = ['preview']

def preview(self, instance):
return preview(instance)
3 changes: 0 additions & 3 deletions dynamic_file/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,3 @@

if not hasattr(settings, 'DYNAMIC_FILE_UPLOADED_BY_MIGRATION_DEPENDENCY'):
setattr(settings, 'DYNAMIC_FILE_UPLOADED_BY_MIGRATION_DEPENDENCY', '__first__')

if not hasattr(settings, 'DYNAMIC_FILE_UPLOADED_BY_RELATED_NAME'):
setattr(settings, 'DYNAMIC_FILE_UPLOADED_BY_RELATED_NAME', 'uploaded_files')
6 changes: 4 additions & 2 deletions dynamic_file/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# Heavily inspired by https://github.com/dj-stripe/dj-stripe/blob/master/djstripe/migrations/0001_initial.py

DYNAMIC_FILE_UPLOADED_BY_MODEL = settings.DYNAMIC_FILE_UPLOADED_BY_MODEL
DYNAMIC_FILE_UPLOADED_BY_RELATED_NAME = settings.DYNAMIC_FILE_UPLOADED_BY_RELATED_NAME
DYNAMIC_FILE_UPLOADED_BY_RELATED_NAME = '+'
DYNAMIC_FILE_UPLOADED_BY_MODEL_MIGRATION_DEPENDENCY = settings.DYNAMIC_FILE_UPLOADED_BY_MIGRATION_DEPENDENCY


Expand All @@ -27,6 +27,7 @@
DYNAMIC_FILE_UPLOADED_BY_MODEL,
)


class Migration(migrations.Migration):

initial = True
Expand All @@ -42,7 +43,8 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.TextField(blank=True, help_text='A description for this file')),
('file', models.FileField(help_text='The uploaded file', upload_to='')),
('uploaded_by', models.ForeignKey(help_text='The owner/uploader of this file', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name=DYNAMIC_FILE_UPLOADED_BY_RELATED_NAME, to=DYNAMIC_FILE_UPLOADED_BY_MODEL)),
('uploaded_by', models.ForeignKey(help_text='The owner/uploader of this file', null=True,
on_delete=django.db.models.deletion.SET_NULL, related_name=DYNAMIC_FILE_UPLOADED_BY_RELATED_NAME, to=DYNAMIC_FILE_UPLOADED_BY_MODEL)),
],
options={
'abstract': False,
Expand Down
30 changes: 30 additions & 0 deletions dynamic_file/migrations/0003_alter_dynamicfile_uploaded_by.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 4.1.3 on 2023-07-07 07:34

from django.db import migrations, models
import django.db.models.deletion


from django.conf import settings
DYNAMIC_FILE_UPLOADED_BY_MODEL = settings.DYNAMIC_FILE_UPLOADED_BY_MODEL


class Migration(migrations.Migration):

dependencies = [
('dynamic_file', '0002_alter_dynamicfile_file'),
]

operations = [
migrations.AlterField(
model_name='dynamicfile',
name='uploaded_by',
field=models.ForeignKey(blank=True, help_text='The owner/uploader of this file', null=True,
on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=DYNAMIC_FILE_UPLOADED_BY_MODEL),
),
migrations.AddField(
model_name='dynamicfile',
name='display_name',
field=models.CharField(blank=True, default='',
help_text='An optional displayable name for this file.', max_length=128),
),
]
83 changes: 76 additions & 7 deletions dynamic_file/models/base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
from django.db import models
from django.utils.translation import gettext as _
from django.conf import settings
import mimetypes

import base64


class DynamicFileBase(models.Model):
'''
Base model for handling dynamic files. Should not be used on it's own by
consuming applications.
Base model for handling dynamic files. Abstract, cannot be used directly.
'''

display_name = models.CharField(
blank=True,
default='',
max_length=128,
help_text=_('An optional displayable name for this file.')
)
'''
A concise and optional description of the uploaded file.
'''

description = models.TextField(
Expand All @@ -21,17 +33,74 @@ class DynamicFileBase(models.Model):
settings.DYNAMIC_FILE_UPLOADED_BY_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name=settings.DYNAMIC_FILE_UPLOADED_BY_RELATED_NAME,
blank=True,
related_name='+',
help_text=_('The owner/uploader of this file')
)
'''
Specifies the uploader of this file. This foreign key defaults to AUTH_USER_MODEL
but can be customized by changing DYNAMIC_FILE_UPLOADED_BY_MODEL.
The reverse accessor defaults to 'uploaded_files' and can be customized by changing
DYNAMIC_FILE_UPLOADED_BY_RELATED_NAME.
Specifies the uploader of this file. This foreign key defaults to ``AUTH_USER_MODEL``
but can be customized by changing ``DYNAMIC_FILE_UPLOADED_BY_MODEL``.
The reverse accessor is unused.
NOTE: Changing those settings is only supported _before_ running the first migration
'''

@property
def name(self):
'''
Returns the filename of this instance. If ``file`` is ``None``, an empty string is returned.
'''
if self.file:
return self.file.name
return ''

@property
def read(self):
'''
Wrapper for the read method of the ``FileField`` instance
'''
return self.file.read

@property
def mimetype(self):
'''
Guesses the ``mimetype`` from the file extension. Returns null if unknown
'''
return mimetypes.MimeTypes().guess_type(self.name)[0]

def to_base64(self):
'''
Converts the file to base64. Undefined if the file is ``None``
'''
return base64.b64encode(self.read())

def to_base64_utf8(self):
'''
Converts the file to base64 and returns it in ``utf-8`` encoding.
'''
return self.to_base64().decode('utf-8')

def to_base64_src(self):
'''
Converts the file to base64 and returns it in ``utf-8`` encoding. In addition,
the content is wrapped to be used within html ``src=`` attributes
'''
src = self.to_base64_utf8()
return f'data:;base64,{src}'

def __str__(self):
'''
String representation of this model.
If a display name is set, this will be used as representation.
If no display name is set, the filename is used.
If no filename is set, the pk and a `_nofile` suffix is used.
'''
if len(self.display_name) > 0:
return self.display_name
elif len(self.name) > 0:
return self.name
else:
return f'{self.pk}_nofile'

class Meta:
abstract = True
12 changes: 1 addition & 11 deletions dynamic_file/models/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from dynamic_file.models.base import DynamicFileBase
from dynamic_file.storage import DynamicFileSystemStorage


fs = DynamicFileSystemStorage()


Expand All @@ -14,14 +15,3 @@ class DynamicFile(DynamicFileBase):
'''
The concrete file for this model.
'''

@property
def name(self):
return self.file.name

def __str__(self):
'''
String representation of this model. Defaults to the name of the file.
If no file has been set, returnes the pk with a fixed suffix.
'''
return self.file.name if self.file.name else f'{self.pk}_nofile'
1 change: 0 additions & 1 deletion dynamic_file/serializers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
from .fields import * # noqa: F401,F403
from .dynamic_file import * # noqa: F401,F403
Empty file.
2 changes: 1 addition & 1 deletion dynamic_file/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

class DynamicFileSystemStorage(FileSystemStorage):
def __init__(self, location=settings.DYNAMIC_FILE_STORAGE_LOCATION):
super().__init__(location=location, base_url='')
super().__init__(location=location, base_url='/admin/download/')
Loading

0 comments on commit 964f452

Please sign in to comment.