Skip to content

Commit

Permalink
Add ruff and pre-commit config and fix issues
Browse files Browse the repository at this point in the history
  • Loading branch information
jochenklar committed Aug 30, 2023
1 parent 1fc068e commit 32cbd6f
Show file tree
Hide file tree
Showing 22 changed files with 177 additions and 60 deletions.
18 changes: 18 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
exclude: \/migrations\/

repos:
- repo: meta
hooks:
- id: check-hooks-apply
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-ast
- id: end-of-file-fixer
- id: trailing-whitespace
- id: debug-statements
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.284
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ django-datacite
[![pytest Workflow Status](https://github.com/ISI-MIP/django-datacite/actions/workflows/pytest.yml/badge.svg)](https://github.com/ISI-MIP/django-datacite/actions/workflows/pytest.yml)
[![Coverage Status](https://coveralls.io/repos/github/ISI-MIP/django-datacite/badge.svg?branch=master)](https://coveralls.io/github/ISI-MIP/django-datacite?branch=master)

A Django app to properly model the [DataCite Metadata Schema](https://schema.datacite.org/) in a relational database, with full integration into the Django admin interface. The app is *slightly* opinionated in a way to make it better usable as a DOI registration database. Names (`creators` and `contributors`) and identifiers (for the resources, but also for `alternativeIdentifiers` and `relatedIdentifiers`) are stored in separate database tables and can be reused for different resources.
A Django app to properly model the [DataCite Metadata Schema](https://schema.datacite.org/) in a relational database, with full integration into the Django admin interface. The app is *slightly* opinionated in a way to make it better usable as a DOI registration database. Names (`creators` and `contributors`) and identifiers (for the resources, but also for `alternativeIdentifiers` and `relatedIdentifiers`) are stored in separate database tables and can be reused for different resources.

This app provides a set of [models](django_datacite/models.py), [admin classes](django_datacite/admin.py), [views](django_datacite/views.py), and utility functions to export and import DataCite files. All options and default settings can be customized in the Django `settings` using the same syntax as in [django_datacite/settings.py](django_datacite/settings.py). Identifiers have an additional `citation` field to store the human readable citation for reuse in other tools.

Expand Down
1 change: 1 addition & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pathlib import Path

import pytest

from django.conf import settings
from django.core.management import call_command

Expand Down
63 changes: 43 additions & 20 deletions django_datacite/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import json

import requests
from django import forms
from django.contrib import admin
from django.core.exceptions import ValidationError
Expand All @@ -10,13 +9,31 @@
from django.urls import path
from django.utils.translation import gettext as _

import requests

from .exports import export_resource
from .imports import import_resource
from .models import (AlternateIdentifier, Contributor, Creator, Date,
Description, FundingReference, GeoLocation,
GeoLocationBox, GeoLocationPoint, GeoLocationPolygon,
Identifier, Name, NameIdentifier, RelatedIdentifier,
RelatedItem, Resource, Rights, Subject, Title)
from .models import (
AlternateIdentifier,
Contributor,
Creator,
Date,
Description,
FundingReference,
GeoLocation,
GeoLocationBox,
GeoLocationPoint,
GeoLocationPolygon,
Identifier,
Name,
NameIdentifier,
RelatedIdentifier,
RelatedItem,
Resource,
Rights,
Subject,
Title,
)
from .renderers import XMLRenderer
from .utils import render_bibtex

Expand Down Expand Up @@ -184,25 +201,25 @@ def clean(self):
elif cleaned_data.get('file'):
try:
cleaned_data['data'] = json.load(cleaned_data['file'])
except json.JSONDecodeError:
raise ValidationError(_('Please provide a valid JSON.'))
except json.JSONDecodeError as e:
raise ValidationError(_('Please provide a valid JSON.')) from e

elif cleaned_data.get('url'):
response = requests.get(cleaned_data['url'])
response.raise_for_status()

try:
cleaned_data['data'] = response.json()
except json.JSONDecodeError:
raise ValidationError(_('Please provide a valid JSON.'))
except json.JSONDecodeError as e:
raise ValidationError(_('Please provide a valid JSON.')) from e

else:
raise ValidationError(_('Please provide a file OR a URL.'))


# Inlines

class NoExtraInlineMixin(object):
class NoExtraInlineMixin:
extra = 0


Expand Down Expand Up @@ -334,33 +351,39 @@ def get_queryset(self, request):

def get_urls(self):
return [
path('<int:pk>/export/<str:format>/', self.admin_site.admin_view(self.datacite_resource_export),
path('<int:pk>/export/<str:format>/',
self.admin_site.admin_view(self.datacite_resource_export),
name='datacite_resource_export'),
path('import/', self.admin_site.admin_view(self.datacite_resource_import),
path('import/',
self.admin_site.admin_view(self.datacite_resource_import),
name='datacite_resource_import'),
path('<int:pk>/import/', self.admin_site.admin_view(self.datacite_resource_import),
path('<int:pk>/import/',
self.admin_site.admin_view(self.datacite_resource_import),
name='datacite_resource_import'),
path('<int:pk>/copy/', self.admin_site.admin_view(self.datacite_resource_copy),
path('<int:pk>/copy/',
self.admin_site.admin_view(self.datacite_resource_copy),
name='datacite_resource_copy'),
path('<int:pk>/validate/', self.admin_site.admin_view(self.datacite_resource_validate),
path('<int:pk>/validate/',
self.admin_site.admin_view(self.datacite_resource_validate),
name='datacite_resource_validate'),
] + super().get_urls()
*super().get_urls()
]

def datacite_resource_export(self, request, pk=None, format=None):
resource = get_object_or_404(Resource, id=pk)

if format == 'json':
resource_json = json.dumps(export_resource(resource), indent=2)
response = HttpResponse(resource_json, content_type="application/json")
response['Content-Disposition'] = 'filename="{}.json"'.format(resource.identifier)
response['Content-Disposition'] = f'filename="{resource.identifier}.json"'
elif format == 'xml':
resource_xml = XMLRenderer().render(export_resource(resource))
response = HttpResponse(resource_xml, content_type="application/xml")
response['Content-Disposition'] = 'filename="{}.xml"'.format(resource.identifier)
response['Content-Disposition'] = f'filename="{resource.identifier}.xml"'
elif format == 'bibtex':
resource_bibtex = render_bibtex(resource)
response = HttpResponse(resource_bibtex, content_type='application/x-bibtex')
response['Content-Disposition'] = 'filename="{}.bib"'.format(resource.identifier)
response['Content-Disposition'] = f'filename="{resource.identifier}.bib"'
else:
raise Http404

Expand Down
14 changes: 9 additions & 5 deletions django_datacite/exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ def export_resource(resource):
# contributors
contributors = resource.contributor_set.all()
if contributors:
data['contributors'] = [export_name(contributor.name, contributor.contributor_type) for contributor in contributors]
data['contributors'] = [export_name(contributor.name, contributor.contributor_type)
for contributor in contributors]

# dates
dates = resource.dates.all()
Expand All @@ -72,7 +73,8 @@ def export_resource(resource):
# related identifiers
related_identifiers = resource.relatedidentifier_set.all()
if related_identifiers:
data['relatedIdentifiers'] = [export_related_identifiers(related_identifier) for related_identifier in related_identifiers]
data['relatedIdentifiers'] = [export_related_identifiers(related_identifier)
for related_identifier in related_identifiers]

# size
if resource.size:
Expand Down Expand Up @@ -113,7 +115,8 @@ def export_resource(resource):
# funding reference
funding_references = resource.fundingreference_set.all()
if funding_references:
data['fundingReferences'] = [export_funding_references(funding_reference) for funding_reference in funding_references]
data['fundingReferences'] = [export_funding_references(funding_reference)
for funding_reference in funding_references]

# related items
related_items = resource.relateditem_set.all()
Expand Down Expand Up @@ -261,7 +264,7 @@ def export_geo_location(geo_location):
}
})
if geo_location_polygons:
data['geoLocationPolygons'] = [geo_location_polygon for geo_location_polygon in geo_location_polygons]
data['geoLocationPolygons'] = list(geo_location_polygons)

return data

Expand Down Expand Up @@ -354,6 +357,7 @@ def export_related_item(related_item):
# contributors
contributors = related_item.item.contributor_set.all()
if contributors:
data['contributors'] = [export_name(contributor.name, contributor.contributor_type) for contributor in contributors]
data['contributors'] = [export_name(contributor.name, contributor.contributor_type)
for contributor in contributors]

return data
35 changes: 27 additions & 8 deletions django_datacite/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,27 @@

from django.utils.dateparse import parse_date

from .models import (AlternateIdentifier, Contributor, Creator, Date,
Description, FundingReference, GeoLocation,
GeoLocationBox, GeoLocationPoint, GeoLocationPolygon,
Identifier, Name, NameIdentifier, RelatedIdentifier,
RelatedItem, Resource, Rights, Subject, Title)
from .models import (
AlternateIdentifier,
Contributor,
Creator,
Date,
Description,
FundingReference,
GeoLocation,
GeoLocationBox,
GeoLocationPoint,
GeoLocationPolygon,
Identifier,
Name,
NameIdentifier,
RelatedIdentifier,
RelatedItem,
Resource,
Rights,
Subject,
Title,
)
from .utils import get_settings

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -170,7 +186,8 @@ def import_resource(resource_instance, data):
'order': order
}
)
logger.info('AlternateIdentifier="%s" %s', alternate_identifier_instance, 'created' if created else 'updated')
logger.info('AlternateIdentifier="%s" %s',
alternate_identifier_instance, 'created' if created else 'updated')

# relatedIdentifiers
related_identifier_nodes = data.get('relatedIdentifiers')
Expand Down Expand Up @@ -200,7 +217,8 @@ def import_resource(resource_instance, data):
'resource_type_general': resource_type_general
}
)
logger.info('RelatedIdentifier="%s" %s', related_identifier_instance, 'created' if created else 'updated')
logger.info('RelatedIdentifier="%s" %s',
related_identifier_instance, 'created' if created else 'updated')

# rightsList
right_list_node = data.get('rightsList')
Expand Down Expand Up @@ -251,7 +269,8 @@ def import_resource(resource_instance, data):
'award_title': funding_reference_node.get('awardTitle', '')
}
)
logger.info('FundingReference="%s" %s', funding_reference_instance, 'created' if created else 'updated')
logger.info('FundingReference="%s" %s',
funding_reference_instance, 'created' if created else 'updated')

# related_items
related_item_nodes = data.get('relatedItems')
Expand Down
7 changes: 4 additions & 3 deletions django_datacite/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ def citation(self):
def get_absolute_url(self):
try:
return reverse('django_datacite:resource', args=[self.identifier.identifier])
except (NoReverseMatch, AttributeError):
raise Http404
except (NoReverseMatch, AttributeError) as e:
raise Http404 from e

@staticmethod
def get_default_public():
Expand Down Expand Up @@ -718,7 +718,8 @@ class GeoLocationBox(models.Model):
)

def __str__(self):
return '{west_bound_longitude}, {east_bound_longitude}, {south_bound_latitude}, {north_bound_latitude}'.format(**vars(self))
return '{west_bound_longitude}, {east_bound_longitude}, ' \
'{south_bound_latitude}, {north_bound_latitude}'.format(**vars(self))


class GeoLocationPolygon(models.Model):
Expand Down
4 changes: 2 additions & 2 deletions django_datacite/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from xml.sax.saxutils import XMLGenerator


class XMLRenderer(object):
class XMLRenderer:

def __init__(self):
self.stream = io.StringIO()
Expand All @@ -19,7 +19,7 @@ def render(self, data):
def render_node(self, tag, attrs, value):
if value is not None:
# remove None values from attrs
attrs = dict((k, v) for k, v in attrs.items() if v is not None)
attrs = {k: v for k, v in attrs.items() if v is not None}

self.xml.startElement(tag, attrs)
self.xml.characters(str(value))
Expand Down
15 changes: 10 additions & 5 deletions django_datacite/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,16 @@

DATACITE_DEFAULT_RIGHTS_IDENTIFIER = 'CC0-1.0'
DATACITE_RIGHTS_IDENTIFIERS = (
('CC0-1.0', _('CC0 1.0 Universal Public Domain Dedication')),
('CC-BY-4.0', _('Creative Commons Attribution 4.0 International (CC BY 4.0)')),
('CC-BY-SA-4.0', _('Creative Commons Attribution Share Alike 4.0 International (CC BY-SA 4.0)')),
('CC-BY-NC-4.0', _('Creative Commons Attribution Non Commercial 4.0 International (CC BY-NC 4.0)')),
('CC-BY-NC-SA-4.0', _('Creative Commons Attribution Non Commercial Share Alike 4.0 International (CC BY-NC-SA 4.0)')),
('CC0-1.0',
_('CC0 1.0 Universal Public Domain Dedication')),
('CC-BY-4.0',
_('Creative Commons Attribution 4.0 International (CC BY 4.0)')),
('CC-BY-SA-4.0',
_('Creative Commons Attribution Share Alike 4.0 International (CC BY-SA 4.0)')),
('CC-BY-NC-4.0',
_('Creative Commons Attribution Non Commercial 4.0 International (CC BY-NC 4.0)')),
('CC-BY-NC-SA-4.0',
_('Creative Commons Attribution Non Commercial Share Alike 4.0 International (CC BY-NC-SA 4.0)')),
)

DATACITE_RIGHTS_IDENTIFIER_URIS = {
Expand Down
2 changes: 1 addition & 1 deletion django_datacite/tests/test_admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
import json
import re

from django.conf import settings
from django.urls import reverse
Expand Down
2 changes: 1 addition & 1 deletion django_datacite/tests/test_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.conf import settings

from django_datacite.imports import import_resource
from django_datacite.models import Resource, Name, Identifier, Subject
from django_datacite.models import Identifier, Name, Resource, Subject

resource_id = 1

Expand Down
3 changes: 1 addition & 2 deletions django_datacite/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

from django.http import Http404

from django_datacite.models import Resource, NameIdentifier, GeoLocation

from django_datacite.models import GeoLocation, NameIdentifier, Resource

resource_id = 1
name_identifier_id = 1
Expand Down
2 changes: 1 addition & 1 deletion django_datacite/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.urls import re_path

from django_datacite.views import resource, resource_json, resource_xml, resource_bibtex
from django_datacite.views import resource, resource_bibtex, resource_json, resource_xml

app_name = 'django_datacite'

Expand Down
4 changes: 2 additions & 2 deletions django_datacite/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ def validate_polygon_points(value):
]
}
})
except jsonschema.exceptions.ValidationError:
raise ValidationError('JSON is not of the form [[lon, lat], ...] with at least 4 items.')
except jsonschema.exceptions.ValidationError as e:
raise ValidationError('JSON is not of the form [[lon, lat], ...] with at least 4 items.') from e
Loading

0 comments on commit 32cbd6f

Please sign in to comment.