diff --git a/.gitignore b/.gitignore index 440bdf6..0cd8bef 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,5 @@ docs/conf.py _docs_tmp* docs/test-ourprojects/ test-ourprojects/ +test.ipynb +run-tests diff --git a/ourprojects/__init__.py b/ourprojects/__init__.py index 423fead..c5099df 100644 --- a/ourprojects/__init__.py +++ b/ourprojects/__init__.py @@ -14,7 +14,9 @@ .. autosummary:: :toctree: . + Person Project + Reference """ __version__ = "0.0.1" # denote a pre-release for 0.1.0 with 0.1rc1 @@ -32,4 +34,4 @@ def __getattr__(name): import lamindb del __getattr__ # delete so that imports work out - from .models import Project + from .models import Person, Project, Reference diff --git a/ourprojects/migrations/0001_initial.py b/ourprojects/migrations/0001_initial.py index 0963da7..b9ce5ad 100644 --- a/ourprojects/migrations/0001_initial.py +++ b/ourprojects/migrations/0001_initial.py @@ -1,5 +1,6 @@ -# Generated by Django 5.2 on 2024-11-21 10:57 +# Generated by Django 5.1.3 on 2024-11-21 16:47 +import django.core.validators import django.db.models.deletion import lnschema_core.fields import lnschema_core.ids @@ -42,7 +43,6 @@ class Migration(migrations.Migration): "artifact", lnschema_core.fields.ForeignKey( blank=True, - default=None, on_delete=django.db.models.deletion.CASCADE, related_name="links_project", to="lnschema_core.artifact", @@ -83,6 +83,149 @@ class Migration(migrations.Migration): ], bases=(lnschema_core.models.LinkORM, models.Model), ), + migrations.CreateModel( + name="ArtifactReference", + fields=[ + ( + "created_at", + lnschema_core.fields.DateTimeField( + auto_now_add=True, db_index=True + ), + ), + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ( + "label_ref_is_name", + lnschema_core.fields.BooleanField( + blank=True, default=None, null=True + ), + ), + ( + "feature_ref_is_name", + lnschema_core.fields.BooleanField( + blank=True, default=None, null=True + ), + ), + ( + "artifact", + lnschema_core.fields.ForeignKey( + blank=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="links_reference", + to="lnschema_core.artifact", + ), + ), + ( + "created_by", + lnschema_core.fields.ForeignKey( + blank=True, + default=lnschema_core.users.current_user_id, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="lnschema_core.user", + ), + ), + ( + "feature", + lnschema_core.fields.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="links_artifactreference", + to="lnschema_core.feature", + ), + ), + ( + "run", + lnschema_core.fields.ForeignKey( + blank=True, + default=lnschema_core.models.current_run, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="lnschema_core.run", + ), + ), + ], + bases=(lnschema_core.models.LinkORM, models.Model), + ), + migrations.CreateModel( + name="Person", + fields=[ + ( + "created_at", + lnschema_core.fields.DateTimeField( + auto_now_add=True, db_index=True + ), + ), + ( + "updated_at", + lnschema_core.fields.DateTimeField(auto_now=True, db_index=True), + ), + ("id", models.AutoField(primary_key=True, serialize=False)), + ( + "uid", + lnschema_core.fields.CharField( + blank=True, + db_index=True, + default=lnschema_core.ids.base62_8, + max_length=8, + unique=True, + ), + ), + ( + "name", + lnschema_core.fields.CharField( + blank=True, db_index=True, default=None, max_length=255 + ), + ), + ( + "email", + lnschema_core.fields.EmailField( + blank=True, default=None, max_length=254, null=True + ), + ), + ( + "external", + lnschema_core.fields.BooleanField( + blank=True, db_index=True, default=True + ), + ), + ( + "_previous_runs", + models.ManyToManyField(related_name="+", to="lnschema_core.run"), + ), + ( + "created_by", + lnschema_core.fields.ForeignKey( + blank=True, + default=lnschema_core.users.current_user_id, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="lnschema_core.user", + ), + ), + ( + "run", + lnschema_core.fields.ForeignKey( + blank=True, + default=lnschema_core.models.current_run, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="lnschema_core.run", + ), + ), + ], + options={ + "abstract": False, + }, + bases=( + lnschema_core.models.CanCurate, + models.Model, + lnschema_core.models.ValidateFields, + ), + ), migrations.CreateModel( name="Project", fields=[ @@ -101,6 +244,7 @@ class Migration(migrations.Migration): "uid", lnschema_core.fields.CharField( blank=True, + db_index=True, default=lnschema_core.ids.base62_12, max_length=12, unique=True, @@ -136,11 +280,17 @@ class Migration(migrations.Migration): ( "artifacts", models.ManyToManyField( - related_name="Projects", + related_name="projects", through="ourprojects.ArtifactProject", to="lnschema_core.artifact", ), ), + ( + "contributors", + models.ManyToManyField( + related_name="projects", to="ourprojects.person" + ), + ), ( "created_by", lnschema_core.fields.ForeignKey( @@ -177,14 +327,175 @@ class Migration(migrations.Migration): name="project", field=lnschema_core.fields.ForeignKey( blank=True, - default=None, on_delete=django.db.models.deletion.PROTECT, related_name="links_artifact", to="ourprojects.project", ), ), + migrations.CreateModel( + name="Reference", + fields=[ + ( + "created_at", + lnschema_core.fields.DateTimeField( + auto_now_add=True, db_index=True + ), + ), + ( + "updated_at", + lnschema_core.fields.DateTimeField(auto_now=True, db_index=True), + ), + ("id", models.AutoField(primary_key=True, serialize=False)), + ( + "uid", + lnschema_core.fields.CharField( + blank=True, + db_index=True, + default=lnschema_core.ids.base62_12, + max_length=12, + unique=True, + ), + ), + ( + "name", + lnschema_core.fields.CharField( + blank=True, db_index=True, default=None, max_length=255 + ), + ), + ( + "abbr", + lnschema_core.fields.CharField( + blank=True, + db_index=True, + default=None, + max_length=32, + null=True, + unique=True, + ), + ), + ("url", lnschema_core.fields.URLField(blank=True, null=True)), + ( + "pubmed_id", + lnschema_core.fields.BigIntegerField( + blank=True, db_index=True, null=True + ), + ), + ( + "doi", + lnschema_core.fields.CharField( + blank=True, + db_index=True, + default=None, + max_length=255, + null=True, + validators=[ + django.core.validators.RegexValidator( + message="Must be a DOI (e.g., 10.1000/xyz123 or https://doi.org/10.1000/xyz123)", + regex="^(?:https?://(?:dx\\.)?doi\\.org/|doi:|DOI:)?10\\.\\d+/.*$", + ) + ], + ), + ), + ( + "preprint", + lnschema_core.fields.BooleanField( + blank=True, db_index=True, default=False + ), + ), + ( + "public", + lnschema_core.fields.BooleanField( + blank=True, db_index=True, default=True + ), + ), + ( + "journal", + lnschema_core.fields.TextField(blank=True, default=None, null=True), + ), + ( + "description", + lnschema_core.fields.TextField(blank=True, default=None, null=True), + ), + ( + "text", + lnschema_core.fields.TextField(blank=True, default=None, null=True), + ), + ( + "published_at", + lnschema_core.fields.DateField(blank=True, default=None, null=True), + ), + ( + "_previous_runs", + models.ManyToManyField(related_name="+", to="lnschema_core.run"), + ), + ( + "artifacts", + models.ManyToManyField( + related_name="references", + through="ourprojects.ArtifactReference", + to="lnschema_core.artifact", + ), + ), + ( + "authors", + models.ManyToManyField( + related_name="references", to="ourprojects.person" + ), + ), + ( + "created_by", + lnschema_core.fields.ForeignKey( + blank=True, + default=lnschema_core.users.current_user_id, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="lnschema_core.user", + ), + ), + ( + "run", + lnschema_core.fields.ForeignKey( + blank=True, + default=lnschema_core.models.current_run, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="lnschema_core.run", + ), + ), + ], + options={ + "abstract": False, + }, + bases=( + lnschema_core.models.CanCurate, + models.Model, + lnschema_core.models.ValidateFields, + ), + ), + migrations.AddField( + model_name="project", + name="references", + field=models.ManyToManyField( + related_name="projects", to="ourprojects.reference" + ), + ), + migrations.AddField( + model_name="artifactreference", + name="reference", + field=lnschema_core.fields.ForeignKey( + blank=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="links_artifact", + to="ourprojects.reference", + ), + ), migrations.AlterUniqueTogether( name="artifactproject", unique_together={("artifact", "project", "feature")}, ), + migrations.AlterUniqueTogether( + name="artifactreference", + unique_together={("artifact", "reference", "feature")}, + ), ] diff --git a/ourprojects/models.py b/ourprojects/models.py index 477a893..465a1c0 100644 --- a/ourprojects/models.py +++ b/ourprojects/models.py @@ -1,9 +1,21 @@ from __future__ import annotations +from datetime import date # noqa + +from django.core.validators import RegexValidator from django.db import models from django.db.models import CASCADE, PROTECT from lnschema_core import ids -from lnschema_core.fields import BooleanField, CharField, ForeignKey, URLField +from lnschema_core.fields import ( + BigIntegerField, + BooleanField, + CharField, + DateField, + EmailField, + ForeignKey, + TextField, + URLField, +) from lnschema_core.models import ( Artifact, CanCurate, @@ -16,12 +28,40 @@ ) +class Person(Record, CanCurate, TracksRun, TracksUpdates, ValidateFields): + """Internal and external people that can be a part of projects or references. + + Example: + >>> person = Person( + ... name="Jane Doe", + ... email="jane.doe@example.com", + ... internal=True, + ... ).save() + """ + + class Meta(Record.Meta, TracksRun.Meta, TracksUpdates.Meta): + abstract = False + + id: int = models.AutoField(primary_key=True) + """Internal id, valid only in one DB instance.""" + uid: str = CharField(unique=True, max_length=8, db_index=True, default=ids.base62_8) + """Universal id, valid across DB instances.""" + name: str = CharField(db_index=True) + """Name of the person (forename(s) lastname).""" + email: str | None = EmailField(null=True, default=None) + """Email of the person.""" + external: bool = BooleanField(default=True, db_index=True) + """Whether the person is external to the organization or not.""" + + class Project(Record, CanCurate, TracksRun, TracksUpdates, ValidateFields): - """Projects. + """Projects with associated people and references. Example: - >>> Project = Project( - ... name="My project name", + >>> project = Project( + ... name="My Project Name", + ... abbr="MPN", + ... url="https://example.com/my_project", ... ).save() """ @@ -30,7 +70,9 @@ class Meta(Record.Meta, TracksRun.Meta, TracksUpdates.Meta): id: int = models.AutoField(primary_key=True) """Internal id, valid only in one DB instance.""" - uid: str = CharField(unique=True, max_length=12, default=ids.base62_12) + uid: str = CharField( + unique=True, max_length=12, db_index=True, default=ids.base62_12 + ) """Universal id, valid across DB instances.""" name: str = CharField(db_index=True) """Title or name of the Project.""" @@ -38,12 +80,106 @@ class Meta(Record.Meta, TracksRun.Meta, TracksUpdates.Meta): """A unique abbreviation.""" url: str | None = URLField(max_length=255, null=True, default=None) """A URL to view.""" + contributors: Person = models.ManyToManyField(Person, related_name="projects") + """Contributors associated with this project.""" + references: Reference = models.ManyToManyField("Reference", related_name="projects") + """References associated with this project.""" artifacts: Artifact = models.ManyToManyField( - Artifact, through="ArtifactProject", related_name="Projects" + Artifact, through="ArtifactProject", related_name="projects" ) """Artifacts labeled with this Project.""" +class Reference(Record, CanCurate, TracksRun, TracksUpdates, ValidateFields): + """References such as a publication or document, with unique identifiers and metadata. + + Example: + >>> reference = Reference( + ... name="A Paper Title", + ... abbr="APT", + ... url="https://doi.org/10.1000/xyz123", + ... pubmed_id=12345678, + ... doi="10.1000/xyz123", + ... preprint=False, + ... journal="Nature Biotechnology", + ... description="A groundbreaking research paper.", + ... text="A really informative abstract.", + ... published_at=date(2023, 11, 21), + ... ).save() + """ + + class Meta(Record.Meta, TracksRun.Meta, TracksUpdates.Meta): + abstract = False + + id: int = models.AutoField(primary_key=True) + """Internal id, valid only in one DB instance.""" + uid: str = CharField( + unique=True, max_length=12, db_index=True, default=ids.base62_12 + ) + """Universal id, valid across DB instances.""" + name: str = CharField(db_index=True) + """Title or name of the reference document.""" + abbr: str | None = CharField( + max_length=32, + db_index=True, + unique=True, + null=True, + ) + """A unique abbreviation for the reference.""" + url: str | None = URLField(null=True) + """URL linking to the reference.""" + pubmed_id: int | None = BigIntegerField(null=True, db_index=True) + """A PudMmed ID.""" + doi: str | None = CharField( + null=True, + db_index=True, + validators=[ + RegexValidator( + regex=r"^(?:https?://(?:dx\.)?doi\.org/|doi:|DOI:)?10\.\d+/.*$", + message="Must be a DOI (e.g., 10.1000/xyz123 or https://doi.org/10.1000/xyz123)", + ) + ], + ) + """Digital Object Identifier (DOI) for the reference.""" + preprint: bool = BooleanField(default=False, db_index=True) + """Whether the reference is from a preprint.""" + public: bool = BooleanField(default=True, db_index=True) + """Whether the reference is public or not.""" + journal: str | None = TextField(null=True) + """Name of the journal.""" + description: str | None = TextField(null=True) + """Description of the reference.""" + text: str | None = TextField(null=True) + """Abstract or full text of the reference.""" + published_at: date | None = DateField(null=True, default=None) + """Publication date.""" + authors: Person = models.ManyToManyField(Person, related_name="references") + """All people associated with this reference.""" + artifacts: Artifact = models.ManyToManyField( + Artifact, through="ArtifactReference", related_name="references" + ) + """Artifacts labeled with this reference.""" + + +class ArtifactReference(Record, LinkORM, TracksRun): + id: int = models.BigAutoField(primary_key=True) + artifact: Artifact = ForeignKey(Artifact, CASCADE, related_name="links_reference") + reference: Reference = ForeignKey(Reference, PROTECT, related_name="links_artifact") + feature: Feature | None = ForeignKey( + Feature, + PROTECT, + null=True, + default=None, + related_name="links_artifactreference", + ) + label_ref_is_name: bool | None = BooleanField(null=True, default=None) + feature_ref_is_name: bool | None = BooleanField(null=True, default=None) + + class Meta: + # can have the same label linked to the same artifact if the feature is different + unique_together = ("artifact", "reference", "feature") + + class ArtifactProject(Record, LinkORM, TracksRun): id: int = models.BigAutoField(primary_key=True) artifact: Artifact = ForeignKey(Artifact, CASCADE, related_name="links_project") @@ -59,6 +195,5 @@ class ArtifactProject(Record, LinkORM, TracksRun): feature_ref_is_name: bool | None = BooleanField(null=True, default=None) class Meta: - # can have the same label linked to the same artifact if the feature is - # different + # can have the same label linked to the same artifact if the feature is different unique_together = ("artifact", "project", "feature")