Skip to content

Commit

Permalink
Merge jasmin_metadata into jasmin_services. (#134)
Browse files Browse the repository at this point in the history
* Merge jasmin_metadata into jasmin_services.
* Fix import problems.
* Add jasmin-metadata to coverage.
  • Loading branch information
amanning9 authored Mar 28, 2024
1 parent e91f120 commit ca95407
Show file tree
Hide file tree
Showing 15 changed files with 1,764 additions and 1 deletion.
10 changes: 10 additions & 0 deletions jasmin_metadata/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Main module for the JASMIN metadata app.
This app provides generic functionality for attaching metadata to objects.
"""

__author__ = "Matt Pryor"
__copyright__ = "Copyright 2015 UK Science and Technology Facilities Council"

__version__ = "0.1"
348 changes: 348 additions & 0 deletions jasmin_metadata/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
"""
Module containing classes for integration of metadata with the Django admin site.
"""

__author__ = "Matt Pryor"
__copyright__ = "Copyright 2015 UK Science and Technology Facilities Council"

import django.utils.encoding
from django import forms
from django.contrib import admin
from django.contrib.admin import helpers
from django.contrib.contenttypes.models import ContentType
from polymorphic.admin import PolymorphicInlineSupportMixin, StackedPolymorphicInline

from .models import *


@admin.register(UserChoice)
class UserChoiceAdmin(admin.ModelAdmin):
prepopulated_fields = {"display": ("value",)}


class FieldInline(StackedPolymorphicInline):
model = Field
child_inlines = []


class BooleanFieldInline(StackedPolymorphicInline.Child):
model = BooleanField


FieldInline.child_inlines.append(BooleanFieldInline)


class SingleLineTextFieldInline(StackedPolymorphicInline.Child):
model = SingleLineTextField


FieldInline.child_inlines.append(SingleLineTextFieldInline)


class MultiLineTextFieldInline(StackedPolymorphicInline.Child):
model = MultiLineTextField


FieldInline.child_inlines.append(MultiLineTextFieldInline)


class EmailFieldInline(StackedPolymorphicInline.Child):
model = EmailField


FieldInline.child_inlines.append(EmailFieldInline)


class IPv4FieldInline(StackedPolymorphicInline.Child):
model = IPv4Field


FieldInline.child_inlines.append(IPv4FieldInline)


class RegexFieldInline(StackedPolymorphicInline.Child):
model = RegexField


FieldInline.child_inlines.append(RegexFieldInline)


class SlugFieldInline(StackedPolymorphicInline.Child):
model = SlugField


FieldInline.child_inlines.append(SlugFieldInline)


class URLFieldInline(StackedPolymorphicInline.Child):
model = URLField


FieldInline.child_inlines.append(URLFieldInline)


class IntegerFieldInline(StackedPolymorphicInline.Child):
model = IntegerField


FieldInline.child_inlines.append(IntegerFieldInline)


class FloatFieldInline(StackedPolymorphicInline.Child):
model = FloatField


FieldInline.child_inlines.append(FloatFieldInline)


class DateFieldInline(StackedPolymorphicInline.Child):
model = DateField


FieldInline.child_inlines.append(DateFieldInline)


class DateTimeFieldInline(StackedPolymorphicInline.Child):
model = DateTimeField


FieldInline.child_inlines.append(DateTimeFieldInline)


class TimeFieldInline(StackedPolymorphicInline.Child):
model = TimeField


FieldInline.child_inlines.append(TimeFieldInline)


class ChoiceFieldInline(StackedPolymorphicInline.Child):
model = ChoiceField
filter_horizontal = ("choices",)


FieldInline.child_inlines.append(ChoiceFieldInline)


class MultipleChoiceFieldInline(StackedPolymorphicInline.Child):
model = MultipleChoiceField
filter_horizontal = ("choices",)


FieldInline.child_inlines.append(MultipleChoiceFieldInline)


@admin.register(Form)
class FormAdmin(PolymorphicInlineSupportMixin, admin.ModelAdmin):
class Media:
js = (
# mutationobserver polyfill from cloudflare CDN
"https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/0.7.23/MutationObserver.min.js",
# mutationobserver plugin for jQuery
"admin/metadata/jquery.initialize.js",
"admin/metadata/formfield_inlines.js",
)

inlines = (FieldInline,)
list_display = ("name", "n_fields")
# Allow "Save as new" for quick duplication of forms
save_as = True

def n_fields(self, obj):
return obj.fields.count()

n_fields.short_description = "# fields"


################################################################################
################################################################################


class HasMetadataModelAdmin(admin.ModelAdmin):
"""
``ModelAdmin`` for use with models that may have metadata attached.
"""

#: The metadata form class to use
#: Must inherit from :py:class:`~.models.MetadataForm`
metadata_form_class = None

change_form_template = "admin/change_form_metadata.html"

def get_metadata_form_class(self, request, obj):
"""
Returns the metadata form to use for the given object.
The returned form must inherit from :py:class:`~.forms.MetadataForm`.
"""
return self.metadata_form_class

def get_metadata_form_initial_data(self, request, obj):
"""
Gets the initial data for the metadata form. By default, this just
returns the metadata currently attached to the object.
"""
ctype = ContentType.objects.get_for_model(obj)
metadata = Metadatum.objects.filter(content_type=ctype, object_id=obj.pk)
return {d.key: d.value for d in metadata.all()}

def save_model(self, request, obj, form, change):
#####
## Override save_model to only save the model if the metadata is also valid
#####
metadata_form_class = self.get_metadata_form_class(request, obj)
# If there is no metadata form, behave as normal
if not metadata_form_class:
return super().save_model(request, obj, form, change)
# If the metadata is valid, save the object and the metadata
if "_has_metadata" in request.POST:
metadata_form = metadata_form_class(data=request.POST, prefix="metadata")
if metadata_form.is_valid():
super().save_model(request, obj, form, change)
metadata_form.save(obj)

def response_add(self, request, obj, post_url_continue=None):
#####
## Override response_add to collect metadata after the object is fully
## specified
##
## This is primarily to allow us to deal with the situation where the
## required metadata is dependent on an objects state in an intuitive way
#####
# If there is no metadata form, behave as normal
metadata_form_class = self.get_metadata_form_class(request, obj)
# If there is no metadata form, behave as normal
if not metadata_form_class:
return super().response_add(request, obj, post_url_continue)
if "_has_metadata" in request.POST:
# If the submit supposedly has metadata, validate it
# If the metadata is valid (and hence has been saved), behave as normal
metadata_form = metadata_form_class(data=request.POST, prefix="metadata")
if metadata_form.is_valid():
return super().response_add(request, obj, post_url_continue)
else:
# If there is no metadata in the submit, create the form
metadata_form = metadata_form_class(
initial=self.get_metadata_form_initial_data(request, obj),
prefix="metadata",
)
#######
## THIS CODE IS SIMILAR TO changeform_view
#######
# When rendering the metadata form, we also render the object form with
# all the elements hidden
parent_form_class = self.get_form(request)
parent_form = parent_form_class(request.POST, request.FILES)
# Make all the fields in the parent form hidden
for field in parent_form.fields:
parent_form.fields[field].widget = forms.HiddenInput()
admin_form = helpers.AdminForm(
parent_form,
list(self.get_fieldsets(request, obj)),
self.get_prepopulated_fields(request, obj),
self.get_readonly_fields(request, obj),
model_admin=self,
)
media = self.media + admin_form.media
metadata_admin_form = helpers.AdminForm(
metadata_form,
# Put all the fields in one fieldset
[(None, {"fields": list(metadata_form.fields.keys())})],
# No pre-populated fields
{},
)
errors = helpers.AdminErrorList(parent_form, [])
if metadata_form.errors:
errors.extend(metadata_form.errors.values())
context = dict(
self.admin_site.each_context(request),
title="Set metadata for {}".format(
django.utils.encoding.force_str(self.model._meta.verbose_name)
),
adminform=admin_form,
metadata_form=metadata_admin_form,
object_id=obj.pk,
original=obj,
is_popup=("_popup" in request.POST or "_popup" in request.GET),
to_field=request.POST.get("_to_field", request.GET.get("_to_field")),
media=media,
inline_admin_formsets=[],
errors=errors,
preserved_filters=self.get_preserved_filters(request),
)
return self.render_change_form(request, context, add=True, change=False, obj=obj)

def response_change(self, request, obj):
#####
## Override response_change to ensure that the metadata is valid before
## proceeding with the normal action
#####
# If there is no metadata form, behave as normal
metadata_form_class = self.get_metadata_form_class(request, obj)
# If there is no metadata form, behave as normal
if not metadata_form_class:
return super().response_change(request, obj)
if "_has_metadata" in request.POST:
# If the submit supposedly has metadata, validate it
# If the metadata is valid (and hence has been saved), behave as normal
metadata_form = metadata_form_class(data=request.POST, prefix="metadata")
if metadata_form.is_valid():
return super().response_change(request, obj)
#######
## If metadata is invalid, we essentially need to replicate part of
## changeform_view to re-display the form
#######
parent_form_class = self.get_form(request)
parent_form = parent_form_class(request.POST, request.FILES, instance=obj)
admin_form = helpers.AdminForm(
parent_form,
list(self.get_fieldsets(request, obj)),
self.get_prepopulated_fields(request, obj),
self.get_readonly_fields(request, obj),
model_admin=self,
)
media = self.media + admin_form.media
context = dict(
self.admin_site.each_context(request),
title="Change {}".format(
django.utils.encoding.force_str(self.model._meta.verbose_name)
),
adminform=admin_form,
object_id=obj.pk,
original=obj,
is_popup=("_popup" in request.POST or "_popup" in request.GET),
to_field=request.POST.get("_to_field", request.GET.get("_to_field")),
media=media,
inline_admin_formsets=[],
errors=helpers.AdminErrorList(parent_form, []),
preserved_filters=self.get_preserved_filters(request),
)
return self.render_change_form(request, context, add=False, change=True, obj=obj)

def render_change_form(self, request, context, add=False, change=False, form_url="", obj=None):
#####
## Override render_change_form to show the metadata form as an additional
## fieldset on change pages
#####
if change:
metadata_form_class = self.get_metadata_form_class(request, obj)
if metadata_form_class:
if request.method == "POST":
metadata_form = metadata_form_class(data=request.POST, prefix="metadata")
# Force a validation - we don't really care about the result here
metadata_form.is_valid()
else:
# If there is no metadata in the submit, create the form
metadata_form = metadata_form_class(
initial=self.get_metadata_form_initial_data(request, obj),
prefix="metadata",
)
context["metadata_form"] = helpers.AdminForm(
metadata_form,
# Put all the fields in one fieldset
[("Metadata", {"fields": list(metadata_form.fields.keys())})],
# No pre-populated fields
{},
)
context["errors"].extend(metadata_form.errors.values())
return super().render_change_form(request, context, add, change, form_url, obj)
17 changes: 17 additions & 0 deletions jasmin_metadata/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
Module containing custom application configuration for the JASMIN metadata app.
"""

__author__ = "Matt Pryor"
__copyright__ = "Copyright 2015 UK Science and Technology Facilities Council"

from django.apps import AppConfig as BaseAppConfig


class AppConfig(BaseAppConfig):
"""
Application configuration object for the JASMIN metadata app.
"""

name = ".".join(__name__.split(".")[:-1])
verbose_name = "JASMIN Metadata"
Loading

0 comments on commit ca95407

Please sign in to comment.