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