Skip to content

Commit

Permalink
add isort/black linting
Browse files Browse the repository at this point in the history
  • Loading branch information
anx-abruckner committed Oct 21, 2022
1 parent 7905d2e commit 5cad687
Show file tree
Hide file tree
Showing 27 changed files with 347 additions and 190 deletions.
12 changes: 12 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
repos:
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
args: [ "--profile", "black", "--filter-files" ]

- repo: https://github.com/psf/black
rev: 22.6.0 # Replace by any tag/version: https://github.com/psf/black/tags
hooks:
- id: black
language_version: python3 # Should be a command that runs python3.6.2+
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,17 @@ def attachment_context_translations():
}
```

### Auto-formatter setup
We use isort (https://github.com/pycqa/isort) and black (https://github.com/psf/black) for local auto-formatting and for linting in the CI pipeline.
The pre-commit framework (https://pre-commit.com) provides GIT hooks for these tools, so they are automatically applied before every commit.

Steps to activate:
* Install the pre-commit framework: `pip install pre-commit` (for alternative installation options see https://pre-commit.com/#install)
* Activate the framework (from the root directory of the repository): `pre-commit install`

Hint: You can also run the formatters manually at any time with the following command: `pre-commit run --all-files`


## Usage

Attachments accept any other Model as content_object and store the uploaded files in their respective directories
Expand Down
4 changes: 3 additions & 1 deletion drf_attachments/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ def content_object(obj):
app_label = entity._meta.app_label
model_name = entity._meta.model_name
try:
admin_url = reverse(f"admin:{app_label}_{model_name}_change", args=(entity.pk,))
admin_url = reverse(
f"admin:{app_label}_{model_name}_change", args=(entity.pk,)
)
return mark_safe(f'<a href="{admin_url}">{entity}</a>')
except NoReverseMatch:
return entity
Expand Down
22 changes: 10 additions & 12 deletions drf_attachments/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def get_callable(cls, setting_key) -> Optional[Callable]:
if not setting:
return None

module_name, callable_name = setting.rsplit('.', maxsplit=1)
module_name, callable_name = setting.rsplit(".", maxsplit=1)
module = importlib.import_module(module_name)
return getattr(module, callable_name)

Expand All @@ -51,7 +51,10 @@ def get_content_object_field(cls) -> GenericRelatedField:

@classmethod
def context_choices(
cls, include_default=True, values_list=True, translated=True,
cls,
include_default=True,
values_list=True,
translated=True,
) -> Union[List[str], Tuple[Tuple[Any, Any]]]:
"""
Extract all unique context definitions from settings "ATTACHMENT_CONTEXT_*" + "ATTACHMENT_DEFAULT_CONTEXT"
Expand All @@ -73,21 +76,16 @@ def context_choices(
def get_contexts(cls, include_default) -> Set[str]:
settings_keys = dir(settings)
return {
getattr(settings, key) for key in settings_keys if cls.__is_context_setting(key, include_default)
getattr(settings, key)
for key in settings_keys
if cls.__is_context_setting(key, include_default)
}

@staticmethod
def __is_context_setting(key, include_default) -> bool:
return (
(
key.startswith("ATTACHMENT_CONTEXT_")
and not key.endswith("_CALLABLE")
)
or (
include_default
and key == DEFAULT_CONTEXT_SETTING
)
)
key.startswith("ATTACHMENT_CONTEXT_") and not key.endswith("_CALLABLE")
) or (include_default and key == DEFAULT_CONTEXT_SETTING)

@classmethod
def translate_context(cls, context):
Expand Down
87 changes: 64 additions & 23 deletions drf_attachments/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,77 @@ class Migration(migrations.Migration):
initial = True

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
("contenttypes", "0002_remove_content_type_name"),
]

operations = [
migrations.CreateModel(
name='Attachment',
name="Attachment",
fields=[
('id',
models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True,
verbose_name='Attachment ID')),
('name', models.CharField(blank=True, max_length=255, verbose_name='name')),
('context',
models.CharField(blank=True, help_text="Additional info about the attachment's context/meaning.",
max_length=255, verbose_name='context')),
('meta', django.db.models.JSONField(
help_text='Additional info about the attachment (e.g. file meta data: mime_type, extension, size).',
verbose_name='meta')),
('file',
models.FileField(storage=storage.AttachmentFileStorage(), upload_to=storage.attachment_upload_path,
verbose_name='file')),
('object_id', models.UUIDField()),
('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')),
('last_modification_date', models.DateTimeField(auto_now=True, verbose_name='Last modification date')),
('content_type',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
verbose_name="Attachment ID",
),
),
(
"name",
models.CharField(blank=True, max_length=255, verbose_name="name"),
),
(
"context",
models.CharField(
blank=True,
help_text="Additional info about the attachment's context/meaning.",
max_length=255,
verbose_name="context",
),
),
(
"meta",
django.db.models.JSONField(
help_text="Additional info about the attachment (e.g. file meta data: mime_type, extension, size).",
verbose_name="meta",
),
),
(
"file",
models.FileField(
storage=storage.AttachmentFileStorage(),
upload_to=storage.attachment_upload_path,
verbose_name="file",
),
),
("object_id", models.UUIDField()),
(
"creation_date",
models.DateTimeField(
auto_now_add=True, verbose_name="Creation date"
),
),
(
"last_modification_date",
models.DateTimeField(
auto_now=True, verbose_name="Last modification date"
),
),
(
"content_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
],
options={
'verbose_name': 'attachment',
'verbose_name_plural': 'attachments',
'ordering': ('creation_date',),
"verbose_name": "attachment",
"verbose_name_plural": "attachments",
"ordering": ("creation_date",),
},
),
]
6 changes: 3 additions & 3 deletions drf_attachments/migrations/0002_attachment_object_id_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
class Migration(migrations.Migration):

dependencies = [
('drf_attachments', '0001_initial'),
("drf_attachments", "0001_initial"),
]

operations = [
migrations.AlterField(
model_name='attachment',
name='object_id',
model_name="attachment",
name="object_id",
field=models.CharField(db_index=True, max_length=64),
),
]
66 changes: 46 additions & 20 deletions drf_attachments/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db.models import CASCADE, CharField, DateTimeField, FileField, ForeignKey, Model, UUIDField
from django.db.models import JSONField
from django.db.models import (
CASCADE,
CharField,
DateTimeField,
FileField,
ForeignKey,
JSONField,
Model,
UUIDField,
)
from django.utils.translation import gettext_lazy as _
from django_userforeignkey.request import get_current_request
from rest_framework.exceptions import ValidationError
Expand All @@ -28,7 +36,7 @@ class Attachment(Model):
objects = AttachmentManager()

id = UUIDField(
_('Attachment ID'),
_("Attachment ID"),
default=uuid.uuid4,
editable=False,
unique=True,
Expand All @@ -50,7 +58,9 @@ class Attachment(Model):

meta = JSONField(
_("meta"),
help_text=_("Additional info about the attachment (e.g. file meta data: mime_type, extension, size)."),
help_text=_(
"Additional info about the attachment (e.g. file meta data: mime_type, extension, size)."
),
blank=False,
null=False,
)
Expand Down Expand Up @@ -96,7 +106,11 @@ def __str__(self):

@property
def is_image(self):
return 'mime_type' in self.meta and self.meta['mime_type'] and self.meta['mime_type'].startswith("image")
return (
"mime_type" in self.meta
and self.meta["mime_type"]
and self.meta["mime_type"].startswith("image")
)

@property
def download_url(self):
Expand Down Expand Up @@ -126,13 +140,13 @@ def is_modified(self):
return self.creation_date != self.last_modification_date

def get_extension(self):
return self.meta.get('extension')
return self.meta.get("extension")

def get_size(self):
return self.meta.get('size')
return self.meta.get("size")

def get_mime_type(self):
return self.meta.get('mime_type')
return self.meta.get("mime_type")

def save(self, *args, **kwargs):
# set computed values for direct and API access
Expand All @@ -152,8 +166,8 @@ def set_and_validate(self):
self.cleanup_file() # remove the old file of a changed Attachment

def set_default_context(self):
""" Set context to settings.ATTACHMENT_DEFAULT_CONTEXT (if defined) if it's still empty """
if not self.context and hasattr(settings, 'ATTACHMENT_DEFAULT_CONTEXT'):
"""Set context to settings.ATTACHMENT_DEFAULT_CONTEXT (if defined) if it's still empty"""
if not self.context and hasattr(settings, "ATTACHMENT_DEFAULT_CONTEXT"):
self.context = self.default_context

def set_attachment_meta(self):
Expand All @@ -166,14 +180,16 @@ def set_attachment_meta(self):
int(settings.ATTACHMENT_MAX_UPLOAD_SIZE),
)
self.unique_upload = getattr(meta, "unique_upload", False)
self.unique_upload_per_context = getattr(meta, "unique_upload_per_context", False)
self.unique_upload_per_context = getattr(
meta, "unique_upload_per_context", False
)

def set_file_meta(self):
if self.meta is None:
self.meta = {}
self.meta['mime_type'] = get_mime_type(self.file)
self.meta['extension'] = get_extension(self.file)
self.meta['size'] = self.file.size
self.meta["mime_type"] = get_mime_type(self.file)
self.meta["extension"] = get_extension(self.file)
self.meta["size"] = self.file.size

def validate_context(self):
"""
Expand Down Expand Up @@ -206,11 +222,14 @@ def _validate_file_mime_type(self):
Validate the mime_type against the AttachmentMeta.valid_mime_types defined in the content_object's model class.
Raise a ValidationError on failure.
"""
if self.valid_mime_types and self.meta['mime_type'] not in self.valid_mime_types:
if (
self.valid_mime_types
and self.meta["mime_type"] not in self.valid_mime_types
):
error_msg = _(
"Invalid mime type {mime_type} detected! It must be one of the following: {valid_mime_types}"
).format(
mime_type=self.meta['mime_type'],
mime_type=self.meta["mime_type"],
valid_mime_types=", ".join(self.valid_mime_types),
)
raise ValidationError(
Expand All @@ -225,11 +244,14 @@ def _validate_file_extension(self):
Validate the extension against the AttachmentMeta.valid_extensions defined in the content_object's model class.
Raise a ValidationError on failure.
"""
if self.valid_extensions and self.meta['extension'] not in self.valid_extensions:
if (
self.valid_extensions
and self.meta["extension"] not in self.valid_extensions
):
error_msg = _(
"Invalid extension {extension} detected! It must be one of the following: {valid_extensions}"
).format(
extension=self.meta['extension'],
extension=self.meta["extension"],
valid_extensions=", ".join(self.valid_extensions),
)
raise ValidationError(
Expand All @@ -247,7 +269,9 @@ def _validate_file_size(self):
Validate the extension and raise a ValidationError on failure.
"""
if self.min_size and self.file.size < self.min_size:
error_msg = _("File size {size} too small! It must be at least {min_size}").format(
error_msg = _(
"File size {size} too small! It must be at least {min_size}"
).format(
size=self.file.size,
min_size=self.min_size,
)
Expand All @@ -260,7 +284,9 @@ def _validate_file_size(self):

# self.max_size is always given (settings.ATTACHMENT_MAX_UPLOAD_SIZE by default and as maximum)
if self.file.size > self.max_size:
error_msg = _("File size {size} too large! It can only be {max_size}").format(
error_msg = _(
"File size {size} too large! It can only be {max_size}"
).format(
size=self.file.size,
max_size=self.max_size,
)
Expand Down
3 changes: 1 addition & 2 deletions drf_attachments/models/querysets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@


class AttachmentQuerySet(QuerySet):

def viewable(self, *args, **kwargs):
callable_ = config.get_filter_callable_for_viewable_content_objects()
return self.__filter_by_callable(callable_)
Expand All @@ -25,7 +24,7 @@ def deletable(self, *args, **kwargs):

def delete(self):
"""Bulk remove files after related Attachments were deleted"""
files = list(self.values_list('file', flat=True))
files = list(self.values_list("file", flat=True))
result = super().delete()

# remove all files that belonged to the deleted attachments
Expand Down
2 changes: 1 addition & 1 deletion drf_attachments/rest/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


class FileDownloadRenderer(BaseRenderer):
""" Return data as download/attachment. """
"""Return data as download/attachment."""

media_type = "application/octet-stream"
format = "binary"
Expand Down
Loading

0 comments on commit 5cad687

Please sign in to comment.