diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..5f94e17d5627a0b7ad7c6dd1e0ba601b3889e236 --- /dev/null +++ b/.gitignore @@ -0,0 +1,154 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/django +# Edit at https://www.toptal.com/developers/gitignore?templates=django + +### Django ### +*.log +*.pot +*.pyc +__pycache__/ +local_settings.py +db.sqlite3 +db.sqlite3-journal +media + +# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ +# in your Git repository. Update and uncomment the following line accordingly. +# <django-project-name>/staticfiles/ + +### Django.Python Stack ### +# Byte-compiled / optimized / DLL files +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +pytestdebug.log + +# Translations +*.mo + +# Django stuff: + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +doc/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +pythonenv* + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# profiling data +.prof + +# End of https://www.toptal.com/developers/gitignore/api/django + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..388caae75beb195eb807c3258c8e22b4c7d3fa23 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2020 Institut Pasteur + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000000000000000000000000000000000000..d3f0593d57b5ae59c512352f59910272503ce681 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE +include README.md +recursive-include docs * \ No newline at end of file diff --git a/README.md b/README.md index 92169061ae1d4d339df3a55c7d6525f850daf759..baeec3c08f5717d49b635ceb8a4bfe5969fbafbd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,42 @@ -# django-biodblinks +BioDBLinks +========== + +BioDBLinks is a Django app to help connect a bio-related django-based database +with other existing references databases. + + +Quick start +----------- + +1. Add "polls" to your INSTALLED_APPS setting like this: + +```python +INSTALLED_APPS = [ + ... + 'biodblinks', +] +``` + +2. Add the various configuration variables to the settings: + +```python +# Django BioDbLinks settings +LABLINKS_ID_PREFIX = "https://ippidb.pasteur.fr" +LABLINKS_PROVIDER_ID = "2079" +LABLINKS_RESOURCE_NAME = "iPPI-DB" +LABLINKS_RESOURCE_DESCRIPTION = "iPPI-DB : An interactive database of protein-protein interactions modulators" +LABLINKS_CONTACT_EMAIL = "ippidb@pasteur.fr" +``` + +3. Create a LabsLink export command for your data, in the `management/commands` + folder of the relevant application. The only thing you need to declare is a + subclass of LabsLinkCommand with a queryset class property that generates + for each link the following fields, to be used in the _links_ file: + - title + - url + - record_id and record_source + - or doi + An example command is available in the `biodblinks/tests/test_lablinks.py` + file. + -BioDBLinks is a Django app to help connect bio-related django-based databases with other existing references databases. diff --git a/biodblinks/__init__.py b/biodblinks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/biodblinks/admin.py b/biodblinks/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/biodblinks/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/biodblinks/apps.py b/biodblinks/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..ff780c6ac050d064e3233a06ed7cd2c1f8cccdcf --- /dev/null +++ b/biodblinks/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class BiodblinksConfig(AppConfig): + name = 'biodblinks' diff --git a/biodblinks/lablinks/__init__.py b/biodblinks/lablinks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/biodblinks/lablinks/command.py b/biodblinks/lablinks/command.py new file mode 100644 index 0000000000000000000000000000000000000000..a435bb1253898e628a621829319b2ee89ced4ff3 --- /dev/null +++ b/biodblinks/lablinks/command.py @@ -0,0 +1,70 @@ +# coding: utf-8 +import xml.dom.minidom + +from django.conf import settings +from django.core import serializers +from django.core.management import BaseCommand +from django.core.serializers import register_serializer + +register_serializer("labslink-xml", "biodblinks.lablinks.serializer") + + +class LabsLinkCommand(BaseCommand): + + help = "Create the LabsLink files to be uploaded to PMC Europe" + + def add_arguments(self, parser): + parser.add_argument( + "profile_file", type=str, help="Path to the Profile file to be generated" + ) + parser.add_argument( + "links_file", type=str, help="Path to the Links file to be generated" + ) + + def generate_profile_file(self, profile_file_path): + self.stdout.write( + self.style.SUCCESS(f"Generating the Profile file in {profile_file_path}...") + ) + provider_id = settings.LABLINKS_PROVIDER_ID + resource_name = settings.LABLINKS_RESOURCE_NAME + resource_description = settings.LABLINKS_RESOURCE_DESCRIPTION + contact_email = settings.LABLINKS_CONTACT_EMAIL + xml_string = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?> + <providers> + <provider> + <id>{provider_id}</id> + <resourceName>{resource_name}</resourceName> + <description>{resource_description}</description> + <email>{contact_email}</email> + </provider> + </providers> + """ + xml_string = xml.dom.minidom.parseString(xml_string).toprettyxml() + with open(profile_file_path, "w") as profile_file: + profile_file.write(xml_string) + self.stdout.write( + self.style.SUCCESS( + f"Successfully generated the Profile file in {profile_file_path}..." + ) + ) + + def generate_links_file(self, links_file_path): + self.stdout.write( + self.style.SUCCESS(f"Generating the Links file in {links_file_path}...") + ) + XMLSerializer = serializers.get_serializer("labslink-xml") + xml_serializer = XMLSerializer() + xml_serializer.serialize(self.queryset) + data = xml_serializer.getvalue() + xml_string = xml.dom.minidom.parseString(data).toprettyxml() + with open(links_file_path, "w") as links_file: + links_file.write(xml_string) + self.stdout.write( + self.style.SUCCESS( + f"Successfully generated the Links file in {links_file_path}..." + ) + ) + + def handle(self, *args, **options): + self.generate_profile_file(options["profile_file"]) + self.generate_links_file(options["links_file"]) diff --git a/biodblinks/lablinks/serializer.py b/biodblinks/lablinks/serializer.py new file mode 100644 index 0000000000000000000000000000000000000000..73e853656c2598cb8179a39915ce05eddb66684e --- /dev/null +++ b/biodblinks/lablinks/serializer.py @@ -0,0 +1,94 @@ +# coding: utf-8 + +from django.conf import settings +from django.core.serializers.xml_serializer import Serializer as BaseSerializer +from django.core.serializers import base +from django.utils.xmlutils import ( + SimplerXMLGenerator, + UnserializableContentError, +) + + +class Serializer(BaseSerializer): + def start_serialization(self): + """ + Start serialization -- open the XML document and the root element. + """ + self.xml = SimplerXMLGenerator( + self.stream, self.options.get("encoding", settings.DEFAULT_CHARSET) + ) + self.xml.startDocument() + self.xml.startElement("links", {}) + + def end_serialization(self): + """ + End serialization -- end the document. + """ + self.indent(0) + self.xml.endElement("links") + self.xml.endDocument() + + def start_object(self, obj): + """ + Called as each object is handled. + """ + if not hasattr(obj, "_meta"): + raise base.SerializationError( + "Non-model object (%s) encountered during serialization" % type(obj) + ) + + self.indent(1) + attrs = {"model": str(obj._meta)} + if not self.use_natural_primary_keys or not hasattr(obj, "natural_key"): + obj_pk = obj.pk + if obj_pk is not None: + attrs["pk"] = str(obj_pk) + self.xml.startElement("link", {}) + self.xml.startElement("resource", {}) + self.indent(3) + self.xml.startElement("title", {}) + self.xml.characters(obj.resource_title) + self.xml.endElement("title") + self.xml.startElement("url", {}) + self.xml.characters(obj.resource_url) + self.xml.endElement("url") + self.xml.endElement("resource") + if obj.doi is not None: + self.xml.startElement("doi", {}) + self.xml.characters(obj.doi) + self.xml.endElement("doi") + else: + self.xml.startElement("record", {}) + self.indent(3) + self.xml.startElement("source", {}) + self.xml.characters(obj.record_source) + self.xml.endElement("source") + self.xml.startElement("id", {}) + self.xml.characters(obj.record_id) + self.xml.endElement("id") + self.xml.endElement("record") + + def end_object(self, obj): + """ + Called after handling all fields for an object. + """ + self.indent(1) + self.xml.endElement("link") + + def handle_field(self, obj, field): + """ + Handle each field on an object (except for ForeignKeys and + ManyToManyFields). + """ + return + + def handle_fk_field(self, obj, field): + """ + Handle a ForeignKey (they need to be treated slightly + differently from regular fields). + """ + return + + def _start_relational_field(self, field): + """Output the <field> element for relational fields.""" + return diff --git a/biodblinks/migrations/0001_initial.py b/biodblinks/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..72ae7a16fe17c19ca774c8731a67ba56bfaf41b3 --- /dev/null +++ b/biodblinks/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 2.2.1 on 2020-12-03 20:51 +import sys + +from django.conf import settings +from django.db import migrations, models + +IS_TEST_DB = 'test' in sys.argv + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + ] + + if IS_TEST_DB: + operations.extend([ + migrations.CreateModel( + name='Resource', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.TextField(verbose_name='Resource Title')), + ('url', models.TextField(verbose_name='Resource URL')), + ('record_id', models.TextField(verbose_name='Record ID')), + ('record_source', models.TextField(verbose_name='Record source')), + ], + ), + ]) diff --git a/biodblinks/migrations/__init__.py b/biodblinks/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/biodblinks/models.py b/biodblinks/models.py new file mode 100644 index 0000000000000000000000000000000000000000..a9abea0dbe02bb05c4ad8b69fa653f5235a6a215 --- /dev/null +++ b/biodblinks/models.py @@ -0,0 +1,7 @@ +from django.db import models + +class Resource(models.Model): + title = models.TextField("Resource Title") + url = models.TextField("Resource URL") + record_id = models.TextField("Record ID") + record_source = models.TextField("Record source") \ No newline at end of file diff --git a/biodblinks/tests/__init__.py b/biodblinks/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/biodblinks/tests/test_lablinks.py b/biodblinks/tests/test_lablinks.py new file mode 100644 index 0000000000000000000000000000000000000000..eb0c0f41cdd9aab621cc4708a908a668ae304814 --- /dev/null +++ b/biodblinks/tests/test_lablinks.py @@ -0,0 +1,35 @@ +import tempfile + +from django.core.management import call_command +from django.db.models import F, Value as V, TextField +from django.test import TestCase + +from biodblinks.lablinks.command import LabsLinkCommand +from biodblinks.models import Resource + + +class TestCommand(LabsLinkCommand): + + queryset = ( + Resource.objects.annotate(resource_title=F("title")) + .annotate(resource_url=F("url")) + .annotate(doi=V("", output_field=TextField())) + ) + + +test_command = TestCommand() + + +class TestRunLabsLinkCommand(TestCase): + def setUp(self): + r = Resource() + r.title = "test title" + r.url = "http://example.com" + r.record_id = 1 + r.record_source = "MED" + r.save() + + def test_run_cmd(self): + profile_file = tempfile.NamedTemporaryFile() + links_file = tempfile.NamedTemporaryFile() + call_command(test_command, profile_file.name, links_file.name) diff --git a/biodblinks/views.py b/biodblinks/views.py new file mode 100644 index 0000000000000000000000000000000000000000..91ea44a218fbd2f408430959283f0419c921093e --- /dev/null +++ b/biodblinks/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000000000000000000000000000000000..81887e648a815e49250191e03d56b8be06cc42ec --- /dev/null +++ b/setup.cfg @@ -0,0 +1,29 @@ +[metadata] +name = django-biodblinks +version = 0.1 +description = A Django app to link your Bio DB to other databases +long_description = file: README.rst +url = http://github.com/??/django-biodblinks +author = Hervé Ménager +author_email = herve.menager@pasteur.fr +license = MIT +classifiers = + Environment :: Web Environment + Framework :: Django :: X.Y # Replace "X.Y" as appropriate + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Topic :: Database + Topic :: Internet :: WWW/HTTP + Topic :: Internet :: WWW/HTTP :: Dynamic Content + Topic :: Scientific/Engineering :: Bio-Informatics + +[options] +include_package_data = true +packages = find: \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..fc1f76c84d17b458f7090667d495592c9abda034 --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() \ No newline at end of file