-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge jasmin_metadata into jasmin_services. (#134)
* Merge jasmin_metadata into jasmin_services. * Fix import problems. * Add jasmin-metadata to coverage.
- Loading branch information
Showing
15 changed files
with
1,764 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
Oops, something went wrong.