Skip to content

Commit

Permalink
extend functionality + README update
Browse files Browse the repository at this point in the history
  • Loading branch information
anx-abruckner committed Nov 23, 2021
1 parent e2da62e commit 273f283
Show file tree
Hide file tree
Showing 9 changed files with 354 additions and 89 deletions.
195 changes: 157 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ Django rest framework module to manage any model's file up-/downloads by relatin

## Installation

Install using pip:
1. Install using pip:

```
pip install git+https://github.com/anexia-it/drf-attachments@main
```

Add model_prefix to your INSTALLED_APPS list. Make sure it is the first app in the list
2. Add model_prefix to your INSTALLED_APPS list. Make sure it is the first app in the list

```
INSTALLED_APPS = [
Expand All @@ -20,38 +20,102 @@ INSTALLED_APPS = [
]
```

Add the `ATTACHMENT_MAX_UPLOAD_SIZE` to your `settings.py`:
3. Add a helper/utils file to your project's source code (e.g. `attachments.py`) and prepare the methods `attachment_content_object_field`, `attachment_context_translatables`, `filter_viewable_content_types`, `filter_editable_content_types` and `filter_deletable_content_types` there.

```
# within app/your_app_name/attachments.py
def attachment_content_object_field():
"""
Manually define all relations using a GenericRelation to Attachment (teach the package how to map the relation)
"""
pass
def attachment_context_translatables():
"""
Manually define all translatable strings of all known context types
(defined in settings.py via "ATTACHMENT_CONTEXT_x" and "ATTACHMENT_DEFAULT_CONTEXT"
"""
return {}
def filter_viewable_content_types(queryset):
"""
Override to return viewable related content_types.
"""
return queryset
def filter_editable_content_types(queryset):
"""
Override to return editable related content_types.
"""
return queryset
def filter_deletable_content_types(queryset):
"""
Override to return deletable related content_types.
"""
return queryset
```

4. Define the helper/utils methods' paths within your `settings.py` as `ATTACHMENT_CONTENT_OBJECT_FIELD_CALLABLE` and `ATTACHMENT_CONTEXT_TRANSLATABLES_CALLABLE`:
```
# settings relevant for attachments app
# within settings.py
ATTACHMENT_CONTENT_OBJECT_FIELD_CALLABLE = "your_app_name.attachments.attachment_content_object_field"
ATTACHMENT_CONTEXT_TRANSLATABLES_CALLABLE = "your_app_name.attachments.attachment_context_translatables"
```

5. Add the `ATTACHMENT_MAX_UPLOAD_SIZE` to your `settings.py`:
```
# within settings.py
ATTACHMENT_MAX_UPLOAD_SIZE = env.int("ATTACHMENT_MAX_UPLOAD_SIZE", default=1024 * 1024 * 25)
```

Define any context choices your attachment files might represent (e.g. "driver's license", "offer", "contract", ...)
as `ATTACHMENT_CONTEXT_CHOICES` in the `settings.py`. We recommend defining each context type as constant before
adding them to the "choices" dict, e.g.:
6. Define any context choices your attachment files might represent (e.g. "driver's license", "offer", "contract", ...) as `ATTACHMENT_CONTEXT_CHOICES` in the `settings.py`. We recommend defining each context type as constant before adding them to the "choices" dict, e.g.:
```
# within settings.py
ATTACHMENT_CONTEXT_DRIVERS_LICENSE = 'DRIVERS_LICENSE'
ATTACHMENT_CONTEXT_OFFER = 'OFFER'
ATTACHMENT_CONTEXT_CONTRACT = 'CONTRACT'
ATTACHMENT_CONTEXT_OTHER = 'OTHER'
ATTACHMENT_CONTEXT_CHOICES = (
("ATTACHMENT_CONTEXT_DRIVERS_LICENSE", _("Driver's license")),
("ATTACHMENT_CONTEXT_OFFER", _("Offer")),
("ATTACHMENT_CONTEXT_CONTRACT", _("Contract")),
("ATTACHMENT_CONTEXT_OTHER", _("Other")),
)
```

You may also define a default context in the `settings.py` that will be set automatically each time you save an
attachment without explicitely defining a context yourself. This `ATTACHMENT_DEFAULT_CONTEXT` must then be included in
the `ATTACHMENT_CONTEXT_CHOICES`:
7. Optionally define a default context in the `settings.py` that will be set automatically each time you save an attachment without explicitly defining a context yourself, e.g.:
```
# within settings.py
...
ATTACHMENT_DEFAULT_CONTEXT = 'ATTACHMENT'
```

8. Add all possible context choices (and the default value, if defined) to the `attachment_context_translatables` method to make them translatable and detectable via the `makemessages` command, e.g.:
```
# within app/your_app_name/attachments.py
from django.utils.translation import ugettext_lazy as _
...
def attachment_context_translatables():
"""
Manually define all translatable strings of all known context types
(defined in settings.py via "ATTACHMENT_CONTEXT_x" and "ATTACHMENT_DEFAULT_CONTEXT"
"""
return {
settings.ATTACHMENT_CONTEXT_DRIVERS_LICENSE: _("Driver's license")
settings.ATTACHMENT_CONTEXT_OFFER: _("Offer")
settings.ATTACHMENT_CONTEXT_CONTRACT: _("Contract")
settings.ATTACHMENT_CONTEXT_OTHER: _("Other")
settings.ATTACHMENT_DEFAULT_CONTEXT: _("Attachment"),
}
...
ATTACHMENT_CONTEXT_CHOICES = (
...
("ATTACHMENT_DEFAULT_CONTEXT", _("Attachment")),
)
```

## Usage
Expand All @@ -62,14 +126,18 @@ Attachments accept any other Model as content_object and store the uploaded file
To manage file uploads for any existing model you must create a one-to-many "attachments" relation to it, via following these steps:
1. Add a generic relation in the model class that is supposed to manage file uploads, e.g. users.UserVehicle:
```
# related_query_name enables filtering for Attachment.objects.filter(users_uservehicles=...)
# within app/your_app_name/users/models/models.py UserVehicle class
# NOTE: since Attachment.object_id is of type CharField, any filters for user vehicle attachments will need to look for its content_type and pk, e.g.:
# user_vehicle_attachments = AttachmentQuerySet.filter(content_type=user_vehicle_content_type, object_id__in=user_vehicle_pk_list)
attachments = GenericRelation(
"attachments.Attachment",
related_query_name="%(app_label)s_%(class)ss", # "users_uservehicles"
)
```
2. Add the AttachmentMeta class with the relevant restrictions to the newly referenced model class (e.g. users.UserVehicle). If not defined otherwise, the default settings will be used for validation:
```
# within app/your_app_name/users/models/models.py UserVehicle class
class AttachmentMeta:
valid_mime_types = [] # allow all mime types
valid_extensions = [] # allow all extensions
Expand All @@ -83,37 +151,88 @@ To manage file uploads for any existing model you must create a one-to-many "att
context at a time (when adding any further Attachments, previous ones with the same context will be
deleted permanently); unique_upload=True trumps unique_upload_per_context=True, so if you want this
config, make sure to have unique_upload=False
```
E.g. in users.UserVehicle model class to allow only a single Attachment (driver's license) that must be an image
(jpg/png):
```
# within app/your_app_name/users/models/models.py UserVehicle class
class AttachmentMeta:
valid_mime_types = ['image/jpeg', 'image/png']
valid_extensions = ['.jpg', '.jpeg', '.jpe', '.png']
unique_upload = True
```
3. Add the newly referenced model (e.g. users.UserVehicle) as HyperlinkedRelatedField to the AttachmentSerializer's content_object, e.g.:
3. Add the newly referenced model (e.g. users.UserVehicle) as HyperlinkedRelatedField to the helper/util file's `attachment_content_object_field` method, e.g.:
```
content_object = GenericRelatedField({
UserVehicle: serializers.HyperlinkedRelatedField(
queryset=UserVehicle.objects.all(),
view_name='vehicle-detail',
), # user-vehicle
...
)
# within app/your_app_name/attachments.py
def attachment_content_object_field():
"""
Manually define all relations using a GenericRelation to Attachment (teach the package how to map the relation)
"""
return GenericRelatedField({
UserVehicle: serializers.HyperlinkedRelatedField(
queryset=UserVehicle.objects.all(),
view_name='vehicle-detail',
), # user-vehicle
...
)
...
```
4. Optional: Add the newly referenced model (e.g. users.UserVehicle) as OR-filter to any relevant AttachmentQuerySet methods, e.g.:
4. Optional: Add the newly referenced model (e.g. users.UserVehicle) as OR-filter to any relevant queryset filter method within the helper/utils file, e.g.:
```
queryset = self.filter(
Q(
users_uservehicles__user=user, # user's vehicle registrations
),
...
)
# within app/your_app_name/attachments.py
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django_userforeignkey.request import get_current_user
...
def filter_viewable_content_types(queryset):
"""
Return only attachments related to uservehicles belonging to the currently logged in user.
"""
user = get_current_user()
# NOTE: since Attachment.object_id is of type CharField, we can not directly filter the UserVehicleQuerySet and need to filter for its content_type and pk instead
user_vehicle_content_type = ContentType.objects.get_for_model(UserVehicle)
viewable_user_vehicle_ids = list(UserVehicle.objects.filter(user=user).values_list('pk', flat=True))
queryset = queryset.filter(
Q(
content_type=my_cars_content_type,
object_id__in=viewable_user_vehicle_ids, # user's own vehicles' attachments
),
)
return queryset
def filter_editable_content_types(queryset):
"""
No attachments are editable
"""
return queryset.none()
def filter_deletable_content_types(queryset):
"""
Attachments are only deletable for admin (superuser)
"""
user = get_current_user()
if user.is_superuser:
return queryset
return queryset.none()
```
5. Add attachment DRF route
```
# app/your_app_name/within urls.py

from drf_attachments.rest.views import AttachmentViewSet

router = get_api_router()
Expand Down
59 changes: 44 additions & 15 deletions drf_attachments/admin.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,61 @@
from django.contrib import admin
from django.contrib.contenttypes.admin import GenericTabularInline
from django.forms import ChoiceField, ModelForm
from django.forms.utils import ErrorList
from django.utils.translation import ugettext_lazy as _

from drf_attachments.models.models import Attachment
from drf_attachments.models.models import Attachment, attachment_context_choices, attachment_context_translatable

__all__ = [
"AttachmentInlineAdmin",
]

class AttachmentAdminMixin(object):
def size(self, obj):
return obj.get_size()

def mime_type(self, obj):
return obj.get_mime_type()

def extension(self, obj):
return obj.get_extension()

def context_label(self, obj):
return obj.context_label


class AttachmentForm(ModelForm):
context = ChoiceField(choices=attachment_context_choices(values_list=False))


@admin.register(Attachment)
class AttachmentAdmin(admin.ModelAdmin):
pass
class AttachmentAdmin(admin.ModelAdmin, AttachmentAdminMixin):
form = AttachmentForm
list_display = ["pk", "name", "content_object", "context_label"]
fields = (
"name",
"context",
"content_type",
"object_id",
"file",
"size",
"mime_type",
"extension",
"creation_date",
)
readonly_fields = (
"size",
"mime_type",
"extension",
"creation_date",
)


class AttachmentInlineAdmin(GenericTabularInline):
class AttachmentInlineAdmin(GenericTabularInline, AttachmentAdminMixin):
model = Attachment
extra = 0
fields = (
"context",
"context_label",
"name",
"download_url",
"file",
Expand All @@ -27,7 +65,7 @@ class AttachmentInlineAdmin(GenericTabularInline):
"creation_date",
)
readonly_fields = (
"context",
"context_label",
"name",
"download_url",
"file",
Expand All @@ -38,15 +76,6 @@ class AttachmentInlineAdmin(GenericTabularInline):
)
show_change_link = False

def size(self, obj):
return obj.get_size()

def mime_type(self, obj):
return obj.get_mime_type()

def extension(self, obj):
return obj.get_extension()

def has_add_permission(self, request, obj=None):
return False

Expand Down
18 changes: 18 additions & 0 deletions drf_attachments/migrations/0002_attachment_object_id_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.9 on 2021-11-23 07:31

from django.db import migrations, models


class Migration(migrations.Migration):

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

operations = [
migrations.AlterField(
model_name='attachment',
name='object_id',
field=models.CharField(db_index=True, max_length=64),
),
]
Loading

0 comments on commit 273f283

Please sign in to comment.