Skip to content
2 changes: 1 addition & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ This folder contains a Django + PostgreSQL backend supporting the EMBER Archive
1. Enter postgres shell
```
psql -U postgres
```
````
1. Inside postgres shell run:
```
CREATE USER ember WITH PASSWORD 'test123'; # password is up to you
Expand Down
3 changes: 1 addition & 2 deletions backend/apps/core/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
# TODO: Import all models here
# from .project import EmberProject
# Import all models here
7 changes: 7 additions & 0 deletions backend/apps/projects/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
from django.contrib import admin

from apps.projects.models import Contributor, EmberProject, Funding, Publication

# Register your models here.

admin.site.register(EmberProject)
admin.site.register(Contributor)
admin.site.register(Funding)
admin.site.register(Publication)
91 changes: 91 additions & 0 deletions backend/apps/projects/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Generated by Django 6.0 on 2026-01-08 16:27

import django.core.validators
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Contributor',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('external_identifiers', models.JSONField(blank=True, default=dict, help_text='External identifiers such as ORCID')),
('name', models.CharField(help_text='Full name of the contributor')),
('email', models.EmailField(blank=True, help_text='Contact email address', max_length=254)),
('institution', models.CharField(blank=True, help_text='Institutional affiliation')),
],
),
migrations.CreateModel(
name='Funding',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('funding_institute', models.CharField(help_text='Name of the funding institute (e.g., NIH, NSF, DARPA)')),
('award_number', models.CharField(help_text='Funding award or grant number')),
('award_title', models.CharField(blank=True, help_text='Title of the funded award')),
('funding_url', models.URLField(blank=True, help_text='URL to the funding announcement or award page')),
],
),
migrations.CreateModel(
name='Publication',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('publication_doi', models.CharField(blank=True, help_text='DOI for the publication', validators=[django.core.validators.RegexValidator(message='This field must be formatted as DOI', regex='^10\\\\.\\\\d{4,9}/[-._;()/:A-Z0-9]+$')])),
('title', models.CharField(help_text='Title of the publication')),
('journal', models.CharField(blank=True, help_text='Journal or venue where the work was published')),
('year', models.IntegerField(blank=True, help_text='Year of publication', validators=[django.core.validators.RegexValidator(message='This field must be a valid year', regex='\\d{4}')])),
('publication_url', models.URLField(blank=True, help_text='URL to the publication landing page or preprint')),
],
),
migrations.CreateModel(
name='EmberProject',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('project_id', models.CharField(help_text='Project identifier')),
('project_title', models.CharField(help_text='Title of the project')),
('project_description', models.TextField(help_text='Text description of the project')),
('data_use_agreement', models.TextField(blank=True, help_text='Description or reference to the data use agreement governing access to restricted or controlled data')),
('data_use_agreement_required', models.BooleanField(default=False, help_text='Indicates whether a data use agreement is required to access any project data')),
('data_availability_emberdandi', models.BooleanField(default=False, help_text='Indicates if data is available in EMBER-DANDI', verbose_name='Data in EMBER-DANDI?')),
('data_availability_emberrestricted', models.BooleanField(default=False, help_text='Indicates if data is available in EMBERrestricted', verbose_name='Data in EMBERrestrictred?')),
('data_availability_embervault', models.BooleanField(default=False, help_text='Indicates if data is available in EMBERvault', verbose_name='Data in EMBERvault?')),
('access_tier_summary', models.TextField(blank=True, help_text='Text description of different data tiers')),
('last_metadata_update', models.DateTimeField(help_text='Date and time of last metadata update')),
('metadata_version', models.CharField(default='1', help_text='Version of the metadata')),
('ember_doi', models.CharField(blank=True, help_text='DOI in EMBER system (empty string allowed until assigned)', validators=[django.core.validators.RegexValidator(message='This field must be formatted as DOI', regex='^10\\\\.\\\\d{4,9}/[-._;()/:A-Z0-9]+$')])),
('access_level_emberdandisets', models.JSONField(blank=True, default=list, help_text='EMBER-DANDI dandiset ids', validators=[django.core.validators.RegexValidator(message='This field must be a valid EMBER-DANDI id', regex='EMBER-DANDI:\\\\d{6}$')])),
('access_level_restricted_datasets', models.JSONField(blank=True, default=list, help_text='Restricted dataset ids')),
('access_level_access_vault_ids', models.JSONField(blank=True, default=list, help_text='EMBERvault dataset ids')),
('related_repositories', models.JSONField(blank=True, default=list, help_text='Identifiers for related repositories')),
('related_dandisets', models.JSONField(blank=True, default=list, help_text='DOIs for related dandisets')),
('related_data', models.JSONField(blank=True, default=list, help_text='Identifiers for other related datasets')),
('data_administrator', models.ForeignKey(help_text='Primary point of contact for data management', on_delete=django.db.models.deletion.RESTRICT, to='projects.contributor')),
('funding', models.ManyToManyField(blank=True, help_text='Funding sources supporting this project', related_name='projects', to='projects.funding')),
('related_publications', models.ManyToManyField(blank=True, help_text='Publications associated with this project', to='projects.publication')),
],
options={
'verbose_name': 'EMBER Project',
'verbose_name_plural': 'EMBER Projects',
},
),
migrations.CreateModel(
name='PublicationContributor',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('contributor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.contributor')),
('publication', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.publication')),
],
),
migrations.AddField(
model_name='publication',
name='authors',
field=models.ManyToManyField(blank=True, help_text='Authors of the publication', through='projects.PublicationContributor', to='projects.contributor'),
),
]
8 changes: 6 additions & 2 deletions backend/apps/projects/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# TODO: Import all models here
# from .project import EmberProject
from .contributor import Contributor, PublicationContributor
from .funding import Funding
from .project import EmberProject
from .publication import Publication

__all__ = ["EmberProject", "Contributor", "PublicationContributor", "Funding", "Publication"]
26 changes: 26 additions & 0 deletions backend/apps/projects/models/contributor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from django.db import models


class Contributor(models.Model):
"""
Person or Organization metadata.
"""

id = models.BigAutoField(primary_key=True)
external_identifiers = models.JSONField(
blank=True, default=dict, help_text="External identifiers such as ORCID"
)
name = models.CharField(help_text="Full name of the contributor")
email = models.EmailField(blank=True, help_text="Contact email address")
institution = models.CharField(blank=True, help_text="Institutional affiliation")


class PublicationContributor(models.Model):
"""
Person or organization contributing to a project.
"""

publication = models.ForeignKey("Publication", on_delete=models.CASCADE)
contributor = models.ForeignKey(Contributor, on_delete=models.CASCADE)
# TODO: Add role (?)
# role = models.CharField(max_length=100, blank=True, help_text="Role of the contributor in this project")
17 changes: 17 additions & 0 deletions backend/apps/projects/models/funding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.db import models


class Funding(models.Model):
"""
Funding source metadata.
"""

id = models.BigAutoField(primary_key=True)
funding_institute = models.CharField(
help_text="Name of the funding institute (e.g., NIH, NSF, DARPA)"
)
award_number = models.CharField(help_text="Funding award or grant number")
award_title = models.CharField(blank=True, help_text="Title of the funded award")
funding_url = models.URLField(
blank=True, help_text="URL to the funding announcement or award page"
)
87 changes: 86 additions & 1 deletion backend/apps/projects/models/project.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,88 @@
from django.db import models

# TODO: Define EmberProject
from apps.projects.validators import doi_validator, ember_dandi_id_validator


class EmberProject(models.Model):
"""
EMBER Project metadata.
"""

id = models.BigAutoField(primary_key=True)
project_id = models.CharField(help_text="Project identifier")
project_title = models.CharField(help_text="Title of the project")
project_description = models.TextField(help_text="Text description of the project")
data_use_agreement = models.TextField(
blank=True,
help_text="Description or reference to the data use agreement governing access to restricted or controlled data",
)
data_use_agreement_required = models.BooleanField(
default=False,
help_text="Indicates whether a data use agreement is required to access any project data",
)
data_availability_emberdandi = models.BooleanField(
default=False,
verbose_name="Data in EMBER-DANDI?",
help_text="Indicates if data is available in EMBER-DANDI",
)
data_availability_emberrestricted = models.BooleanField(
default=False,
verbose_name="Data in EMBERrestrictred?",
help_text="Indicates if data is available in EMBERrestricted",
)
data_availability_embervault = models.BooleanField(
default=False,
verbose_name="Data in EMBERvault?",
help_text="Indicates if data is available in EMBERvault",
)
access_tier_summary = models.TextField(
blank=True, help_text="Text description of different data tiers"
)
data_administrator = models.ForeignKey(
"Contributor",
on_delete=models.RESTRICT,
help_text="Primary point of contact for data management",
)
last_metadata_update = models.DateTimeField(
help_text="Date and time of last metadata update"
) # default=timezone.now
metadata_version = models.CharField(default="1", help_text="Version of the metadata")
ember_doi = models.CharField(
blank=True,
help_text="DOI in EMBER system (empty string allowed until assigned)",
validators=[doi_validator],
)
access_level_emberdandisets = models.JSONField(
default=list,
blank=True,
help_text="EMBER-DANDI dandiset ids",
validators=[ember_dandi_id_validator],
)
access_level_restricted_datasets = models.JSONField(
default=list, blank=True, help_text="Restricted dataset ids"
)
access_level_access_vault_ids = models.JSONField(
default=list, blank=True, help_text="EMBERvault dataset ids"
)
related_publications = models.ManyToManyField(
"Publication", blank=True, help_text="Publications associated with this project"
)
related_repositories = models.JSONField(
default=list, blank=True, help_text="Identifiers for related repositories"
)
related_dandisets = models.JSONField(
default=list, blank=True, help_text="DOIs for related dandisets"
)
related_data = models.JSONField(
default=list, blank=True, help_text="Identifiers for other related datasets"
)
funding = models.ManyToManyField(
"Funding",
related_name="projects",
blank=True,
help_text="Funding sources supporting this project",
)

class Meta:
verbose_name = "EMBER Project"
verbose_name_plural = "EMBER Projects"
30 changes: 30 additions & 0 deletions backend/apps/projects/models/publication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.db import models

from apps.projects.validators import doi_validator, year_validator


class Publication(models.Model):
"""
Publication metadata.
"""

id = models.BigAutoField(primary_key=True)
publication_doi = models.CharField(
blank=True, help_text="DOI for the publication", validators=[doi_validator]
)
title = models.CharField(help_text="Title of the publication")
authors = models.ManyToManyField(
"Contributor",
through="PublicationContributor",
blank=True,
help_text="Authors of the publication",
)
journal = models.CharField(
blank=True, help_text="Journal or venue where the work was published"
)
year = models.IntegerField(
blank=True, help_text="Year of publication", validators=[year_validator]
)
publication_url = models.URLField(
blank=True, help_text="URL to the publication landing page or preprint"
)
42 changes: 31 additions & 11 deletions backend/apps/projects/serializers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
# from rest_framework import serializers

# TODO: Project Serializer
# class ProjectSerializer(serializers.ModelSerializer):
# class Meta:
# model = EmberProject
# fields = [
# "project_id",
# "project_title",
# ...
# ]
from rest_framework import serializers

from apps.projects.models import Contributor, EmberProject, Funding, Publication


class ContributorSerializer(serializers.ModelSerializer):
class Meta:
model = Contributor
fields = "__all__"


class FundingSerializer(serializers.ModelSerializer):
class Meta:
model = Funding
fields = "__all__"


class PublicationSerializer(serializers.ModelSerializer):
class Meta:
model = Publication
fields = "__all__"


class ProjectSerializer(serializers.ModelSerializer):
data_administrator = ContributorSerializer(read_only=True)
related_publications = PublicationSerializer(many=True, read_only=True)
funding = FundingSerializer(many=True, read_only=True)

class Meta:
model = EmberProject
fields = "__all__"
11 changes: 11 additions & 0 deletions backend/apps/projects/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.core.validators import RegexValidator

doi_validator = RegexValidator(
regex=r"^10\\.\\d{4,9}/[-._;()/:A-Z0-9]+$", message="This field must be formatted as DOI"
)

ember_dandi_id_validator = RegexValidator(
regex=r"EMBER-DANDI:\\d{6}$", message="This field must be a valid EMBER-DANDI id"
)

year_validator = RegexValidator(regex=r"\d{4}", message="This field must be a valid year")
19 changes: 11 additions & 8 deletions backend/apps/projects/views.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework.response import Response
from rest_framework.views import APIView

from apps.core.serializers import MessageSerializer
from apps.projects.models.project import EmberProject
from apps.projects.serializers import ProjectSerializer


@extend_schema(tags=["projects"])
class ProjectsView(APIView):
@extend_schema(
operation_id="projects_list",
responses={200: MessageSerializer}, # TODO: ProjectSerializer(many=True)
responses={200: ProjectSerializer(many=True), 404: MessageSerializer},
description="Get all EMBER Projects",
)
def get(self, request):
return Response(
{"message": "Placeholder: List all Projects"},
)
projects_list = EmberProject.objects
serializer = ProjectSerializer(projects_list, many=True)
return Response(serializer.data)


@extend_schema(tags=["projects"])
class ProjectView(APIView):
@extend_schema(
operation_id="projects_retrieve",
responses={200: MessageSerializer}, # TODO: ProjectSerializer
responses={200: ProjectSerializer, 404: MessageSerializer},
description="Get an EMBER Project by its project id",
parameters=[
OpenApiParameter(
Expand All @@ -34,6 +37,6 @@ class ProjectView(APIView):
],
)
def get(self, request, project_id: str):
return Response(
{"message": f"Placeholder: Get Project with id={project_id}"},
)
project = get_object_or_404(EmberProject, project_id=project_id)
serializer = ProjectSerializer(project)
return Response(serializer.data)