Skip to content

Commit

Permalink
PB-35 Fix django admin with file fields
Browse files Browse the repository at this point in the history
The form didn't allow creating assets anymore. The changes for the file
field only worked if the asset already existed.
  • Loading branch information
schtibe committed Jul 29, 2024
1 parent edd751c commit aacf949
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 953 deletions.
173 changes: 107 additions & 66 deletions app/stac_api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from django.forms import Textarea
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

from stac_api.models import BBOX_CH
from stac_api.models import Asset
Expand Down Expand Up @@ -386,42 +387,21 @@ def get_fieldsets(self, request, obj=None):
return fields


class AssetAdminForm(forms.ModelForm):

def __init__(self, *args, **kwargs):
"""Add some logic to the is_external field"""
super().__init__(*args, **kwargs)

if not getattr(self.instance, 'item', False):
# on creation time, we don't show the flag because we don't know
# the asset yet
are_external_assets_allowed = False
else:
are_external_assets_allowed = self.instance.item.collection.allow_external_assets

if are_external_assets_allowed:
# turn the file field into a char field if it's external
# so the user can set the url
if self.instance.is_external:
self.fields['file'] = forms.CharField()
else:
# collection doesn't allow external assets
# let's hide it from the user
# also see the javascript file for the missing implementation detail
external_field = self.fields['is_external']
external_field.disabled = True


@admin.register(Asset)
class AssetAdmin(admin.ModelAdmin):
form = AssetAdminForm

class Media:
js = ('js/admin/asset_help_search.js', 'js/admin/asset_external_fields.js')
css = {'all': ('style/hover.css',)}

file = forms.FileField(required=False)

autocomplete_fields = ['item']
search_fields = ['name', 'item__name', 'item__collection__name']

# We don't want to move the assets on S3
# That's why some fields like the name of the asset are set readonly here
# for update operations
readonly_fields = [
'item_name',
'collection_name',
Expand All @@ -433,30 +413,7 @@ class Media:
'update_interval'
]
list_display = ['name', 'item_name', 'collection_name', 'collection_published']
fieldsets = (
(None, {
'fields': ('name', 'item', 'created', 'updated', 'etag')
}),
(
'File',
{
'fields': (
'is_external',
'file',
'media_type',
'href',
'checksum_multihash',
'update_interval'
)
}
),
('Description', {
'fields': ('title', 'description', 'roles')
}),
('Attributes', {
'fields': ('eo_gsd', 'proj_epsg', 'geoadmin_variant', 'geoadmin_lang')
}),
)

list_filter = [
AutocompleteFilterFactory('Item name', 'item', use_pk_exact=True),
AutocompleteFilterFactory('Collection name', 'item__collection', use_pk_exact=True)
Expand Down Expand Up @@ -519,27 +476,111 @@ def href(self, instance):
return path
return build_asset_href(self.request, path)

# We don't want to move the assets on S3
# That's why some fields like the name of the asset are set readonly here
# for update operations
def render_change_form(self, request, context, *args, **kwargs):
"""Make the file field be a text input if asset is external"""
obj = kwargs['obj']
if obj is not None:
are_external_assets_allowed = obj.item.collection.allow_external_assets

form_instance = context['adminform'].form

if are_external_assets_allowed:
external_field = form_instance.fields['is_external']
external_field.help_text = (
_('Whether this asset is hosted externally. Save the form in '
'order to toggle the file field between input and file widget.')
)

if obj.is_external:
form_instance.fields['file'].widget = forms.TextInput(
attrs={
'size': 150, 'placeholder': 'https://map.geo.admin.ch/external.jpg'
}
)
return super().render_change_form(request, context, *args, **kwargs)

def get_fieldsets(self, request, obj=None):
# Note: this is a bit hacky and only required to get access
# to the request object in 'href' method.
"""Build the different field sets for the admin page
The create page takes less fields than the edit page. This is because
at creation time we don't know yet if the collection allows for external
assets and thus can't determine whether to show the flag or not
"""

# Save the request for use in the href field
self.request = request # pylint: disable=attribute-defined-outside-init

fields = super().get_fieldsets(request, obj)
fields = []
if obj is None:
# In case a new Asset is added use the normal field 'item' from model that have
# a help text fort the search functionality.
fields[0][1]['fields'] = ('name', 'item', 'created', 'updated', 'etag')
return fields
# Otherwise if this is an update operation only display the read only fields
# without help text
fields[0][1]['fields'] = (
'name', 'item_name', 'collection_name', 'created', 'updated', 'etag'
)
fields.append((None, {'fields': ('name', 'item', 'created', 'updated', 'etag')}))
fields.append(('File', {'fields': ('media_type',)}))
else:
# add one section after another
fields.append((
None, {
'fields':
('name', 'item_name', 'collection_name', 'created', 'updated', 'etag')
}
))

# is_external is only available if the collection allows it
if obj.item.collection.allow_external_assets:
file_fields = (
'is_external',
'file',
'media_type',
'href',
'checksum_multihash',
'update_interval'
)
else:
file_fields = (
'file', 'media_type', 'href', 'checksum_multihash', 'update_interval'
)

fields.append(('File', {'fields': file_fields}))

fields.append(('Description', {'fields': ('title', 'description', 'roles')}))

fields.append((
'Attributes', {
'fields': ('eo_gsd', 'proj_epsg', 'geoadmin_variant', 'geoadmin_lang')
}
))

return fields

def get_form(self, request, obj=None, change=False, **kwargs):
"""Make the file field optional
It is perfectly possible to not specify any file in the file field when saving,
even when the field *isn't* blank=True or null=True. The multipart-upload
process does it too.
We allow the field to be empty in case somebody is setting the is_external flag"""
form = super().get_form(request, obj, change, **kwargs)

if obj:
form.base_fields['file'].required = False
return form

def response_add(self, request, obj, post_url_continue=None):
"""
Determine the HttpResponse for the add_view stage. It mostly defers to
its superclass implementation but is customized because the User model
has a slightly different workflow.
"""
# We should allow further modification of the user just added i.e. the
# 'Save' button should behave like the 'Save and continue editing'
# button except for:
# * The user has pressed the 'Save and add another' button

# stole this from django.contrib.auth.admin.UserAdmin
if "_addanother" not in request.POST:
request.POST = request.POST.copy()
request.POST["_continue"] = 1

return super().response_add(request, obj, post_url_continue)


@admin.register(AssetUpload)
class AssetUploadAdmin(admin.ModelAdmin):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,17 +187,13 @@ def _create_asset_minimal(self, item):
filelike = BytesIO(filecontent)
filelike.name = 'testname.txt'

# we need to create the asset in two steps, since the django admin form
# only takes some values in the creation form, then the rest in the
# change form
data = {
"item": item.id,
"name": "test_asset.txt",
"description": "",
"eo_gsd": "",
"geoadmin_lang": "",
"geoadmin_variant": "",
"proj_epsg": "",
"title": "",
"media_type": "text/plain",
"file": filelike
}

response = self.client.post(reverse('admin:stac_api_asset_add'), data)
Expand All @@ -210,7 +206,24 @@ def _create_asset_minimal(self, item):
Asset.objects.filter(item=item, name=data["name"]).exists(),
msg="Admin page asset added not found in DB"
)

data = {
"item": item.id,
"name": "test_asset.txt",
"description": "",
"eo_gsd": "",
"geoadmin_lang": "",
"geoadmin_variant": "",
"proj_epsg": "",
"title": "",
"media_type": "text/plain",
"file": filelike
}

asset = Asset.objects.get(item=item, name=data["name"])
response = self.client.post(reverse('admin:stac_api_asset_change', args=[asset.id]), data)

asset.refresh_from_db()

# Check the asset values
for key, value in data.items():
Expand Down Expand Up @@ -240,14 +253,7 @@ def _create_asset(self, item, extra=None):
data = {
"item": item.id,
"name": filelike.name,
"description": "This is a description",
"eo_gsd": 10,
"geoadmin_lang": "en",
"geoadmin_variant": "kgrs",
"proj_epsg": 2056,
"title": "My first Asset for test",
"media_type": "application/x.filegdb+zip",
"file": filelike
}
if extra:
data.update(extra)
Expand All @@ -261,7 +267,24 @@ def _create_asset(self, item, extra=None):
Asset.objects.filter(item=item, name=data["name"]).exists(),
msg="Admin page asset added not found in DB"
)

data = {
"item": item.id,
"name": filelike.name,
"description": "This is a description",
"eo_gsd": 10,
"geoadmin_lang": "en",
"geoadmin_variant": "kgrs",
"proj_epsg": 2056,
"title": "My first Asset for test",
"media_type": "application/x.filegdb+zip",
"file": filelike
}

asset = Asset.objects.get(item=item, name=data["name"])
response = self.client.post(reverse('admin:stac_api_asset_change', args=[asset.id]), data)

asset.refresh_from_db()

# Check the asset values
for key, value in data.items():
Expand Down Expand Up @@ -840,23 +863,37 @@ def test_add_update_asset_invalid_media_type(self):
@mock_s3_asset_file
def test_asset_file_metadata(self):
content_type = 'image/tiff; application=geotiff'
sample = self.factory.create_asset_sample(
self.item, name='asset.tiff', media_type=content_type
).attributes

# Admin page doesn't uses the name for foreign key but the internal db id.
sample['item'] = self.item.id
response = self.client.post(reverse('admin:stac_api_asset_add'), sample)
data = {"item": self.item.id, "name": "checksum.tiff", "media_type": content_type}

response = self.client.post(reverse('admin:stac_api_asset_add'), data)
# Status code for successful creation is 302, since in the admin UI
# you're redirected to the list view after successful creation
self.assertEqual(response.status_code, 302)
self.assertTrue(
Asset.objects.filter(item=self.item, name=sample["name"]).exists(),
Asset.objects.filter(item=self.item, name=data["name"]).exists(),
msg="Admin page asset added not found in DB"
)
asset = Asset.objects.get(item=self.item, name=sample["name"])
asset = Asset.objects.get(item=self.item, name=data["name"])

filecontent = b'mybinarydata2'
filelike = BytesIO(filecontent)
filelike.name = 'testname.tiff'

data.update({
"description": "",
"eo_gsd": "",
"geoadmin_lang": "",
"geoadmin_variant": "",
"proj_epsg": "",
"title": "",
"file": filelike
})

response = self.client.post(reverse('admin:stac_api_asset_change', args=[asset.id]), data)
asset.refresh_from_db()

path = f"{self.item.collection.name}/{self.item.name}/{sample['name']}"
path = f"{self.item.collection.name}/{self.item.name}/{data['name']}"
sha256 = parse_multihash(asset.checksum_multihash).digest.hex()
obj = self.get_s3_object(path)
self.assertS3ObjectContentType(obj, path, content_type)
Expand Down
Loading

0 comments on commit aacf949

Please sign in to comment.