diff --git a/config/settings/base.py b/config/settings/base.py index 451f115..8042ec4 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -92,7 +92,6 @@ "treebeard", "corsheaders", "rules.apps.AutodiscoverRulesConfig", - "organizations", "taggit", "django_filters", ] @@ -353,3 +352,4 @@ PROJECTS_PROJECT_MODEL = "nina.Project" +PROJECTS_PROJECTMEMBERSHIP_MODEL = "nina.ProjectMembership" diff --git a/metadata_catalogue/nina/admin.py b/metadata_catalogue/nina/admin.py index ac6d2d4..2328d95 100644 --- a/metadata_catalogue/nina/admin.py +++ b/metadata_catalogue/nina/admin.py @@ -5,19 +5,19 @@ @register(Project) class ProjectAdmin(ModelAdmin): - pass + search_fields = ["name", "slug", "description"] @register(Department) class DepartmentAdmin(ModelAdmin): - pass + search_fields = ["name", "slug", "description"] @register(Category) class CategoryAdmin(ModelAdmin): - pass + search_fields = ["name"] @register(Topic) class TopicAdmin(ModelAdmin): - pass + search_fields = ["name"] diff --git a/metadata_catalogue/nina/libs/harvesters.py b/metadata_catalogue/nina/libs/harvesters.py index 444e4b7..73f80cd 100644 --- a/metadata_catalogue/nina/libs/harvesters.py +++ b/metadata_catalogue/nina/libs/harvesters.py @@ -34,16 +34,13 @@ def _fetch_paginated_project(url: str, limit=50): def _process_project(project: dict): p, created = Project.objects.get_or_create( - extid=project.get("id"), + id=project.get("id"), defaults={ - "extid": project.get("id"), "name": project.get("title"), "description": project.get("notes"), + "slug": f'prj-{project.get("id")}', }, ) - if created: - slug = f'prj-{project.get("id")}' - p.slug = slug p.status = project.get("project_state") p.budget = project.get("budget") @@ -54,17 +51,14 @@ def _process_project(project: dict): p.customer, _ = Organization.objects.get_or_create(name=project.get("customer")) for group in project.get("groups"): - slug = f'dpt-{project.get("id")}' d, created = Department.objects.get_or_create( - extid=group.get("id"), + id=group.get("id"), defaults={ "name": group.get("title"), "description": group.get("description"), + "slug": f'dpt-{project.get("id")}', }, ) - if created: - d.slug = slug - d.save() p.departments.add(d) @@ -76,7 +70,7 @@ def _process_project(project: dict): u.set_unusable_password() u.save() - p.get_or_add_user(u) + p.members.add(u) def prosjektoversikt(url: str, limit=50): diff --git a/metadata_catalogue/nina/migrations/0001_initial.py b/metadata_catalogue/nina/migrations/0001_initial.py index 3af5ff9..94faa80 100644 --- a/metadata_catalogue/nina/migrations/0001_initial.py +++ b/metadata_catalogue/nina/migrations/0001_initial.py @@ -1,7 +1,9 @@ -# Generated by Django 4.2.8 on 2024-01-09 12:01 +# Generated by Django 4.2.8 on 2024-01-10 07:47 +from django.conf import settings from django.db import migrations, models import django.db.models.deletion +import django_extensions.db.fields import taggit.managers @@ -9,9 +11,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("organizations", "0006_alter_organization_slug"), - ("datasets", "0006_content_valid"), ("taggit", "0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx"), + ("datasets", "0006_content_valid"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -25,42 +27,48 @@ class Migration(migrations.Migration): migrations.CreateModel( name="Department", fields=[ + ("id", models.CharField(max_length=50, primary_key=True, serialize=False)), + ("name", models.CharField(max_length=250)), ( - "organization_ptr", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to="organizations.organization", - ), + "slug", + django_extensions.db.fields.AutoSlugField(blank=True, editable=False, populate_from=["name"]), ), - ("extid", models.CharField(max_length=100)), ("description", models.TextField(blank=True, null=True)), ], - bases=("organizations.organization",), + ), + migrations.CreateModel( + name="DepartmentMembership", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ], ), migrations.CreateModel( name="Project", fields=[ + ("name", models.CharField(max_length=250)), ( - "organization_ptr", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to="organizations.organization", - ), + "slug", + django_extensions.db.fields.AutoSlugField(blank=True, editable=False, populate_from=["name"]), ), - ("extid", models.CharField(max_length=100)), + ("id", models.CharField(max_length=50, primary_key=True, serialize=False)), ("description", models.TextField(blank=True, null=True)), ("start_date", models.DateField(blank=True, null=True)), ("end_date", models.DateField(blank=True, null=True)), ("budget", models.BigIntegerField(blank=True, null=True)), ("status", models.CharField(blank=True, max_length=50, null=True)), ], - bases=("organizations.organization",), + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="ProjectMembership", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ], + options={ + "abstract": False, + }, ), migrations.CreateModel( name="Topic", @@ -73,6 +81,20 @@ class Migration(migrations.Migration): model_name="topic", constraint=models.UniqueConstraint(models.F("name"), name="unique topic name"), ), + migrations.AddField( + model_name="projectmembership", + name="project", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.PROJECTS_PROJECT_MODEL), + ), + migrations.AddField( + model_name="projectmembership", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="projects_membership", + to=settings.AUTH_USER_MODEL, + ), + ), migrations.AddField( model_name="project", name="category", @@ -92,6 +114,11 @@ class Migration(migrations.Migration): name="departments", field=models.ManyToManyField(blank=True, related_name="projects", to="nina.department"), ), + migrations.AddField( + model_name="project", + name="members", + field=models.ManyToManyField(blank=True, through="nina.ProjectMembership", to=settings.AUTH_USER_MODEL), + ), migrations.AddField( model_name="project", name="tags", @@ -107,16 +134,25 @@ class Migration(migrations.Migration): name="topics", field=models.ManyToManyField(blank=True, to="nina.topic"), ), - migrations.AddConstraint( + migrations.AddField( + model_name="departmentmembership", + name="department", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="nina.department"), + ), + migrations.AddField( + model_name="departmentmembership", + name="user", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( model_name="department", - constraint=models.UniqueConstraint(models.F("extid"), name="unique departemt extid"), + name="members", + field=models.ManyToManyField( + blank=True, related_name="members", through="nina.DepartmentMembership", to=settings.AUTH_USER_MODEL + ), ), migrations.AddConstraint( model_name="category", constraint=models.UniqueConstraint(models.F("name"), name="unique category name"), ), - migrations.AddConstraint( - model_name="project", - constraint=models.UniqueConstraint(models.F("extid"), name="unique project code"), - ), ] diff --git a/metadata_catalogue/nina/models.py b/metadata_catalogue/nina/models.py index 280172e..4843b8d 100644 --- a/metadata_catalogue/nina/models.py +++ b/metadata_catalogue/nina/models.py @@ -1,18 +1,14 @@ from django.db import models -from organizations.models import Organization +from django_extensions.db.fields import AutoSlugField from taggit.managers import TaggableManager -from metadata_catalogue.projects.models import BaseProject +from metadata_catalogue.projects.models import BaseProject, BaseProjectMembership + +from .conf import settings class Project(BaseProject): - organization_ptr = models.OneToOneField( - "organizations.Organization", - on_delete=models.CASCADE, - parent_link=True, - primary_key=True, - ) - extid = models.CharField(max_length=100) + id = models.CharField(max_length=50, primary_key=True) description = models.TextField(null=True, blank=True) start_date = models.DateField(null=True, blank=True) end_date = models.DateField(null=True, blank=True) @@ -25,13 +21,14 @@ class Project(BaseProject): tags = TaggableManager() - class Meta: - constraints = [models.UniqueConstraint("extid", name="unique project code")] - def __str__(self) -> str: return self.name +class ProjectMembership(BaseProjectMembership): + pass + + class Topic(models.Model): name = models.CharField(max_length=150) @@ -52,18 +49,19 @@ def __str__(self) -> str: return self.name -class Department(Organization): - organization_ptr = models.OneToOneField( - "organizations.Organization", - on_delete=models.CASCADE, - parent_link=True, - primary_key=True, - ) - extid = models.CharField(max_length=100) +class Department(models.Model): + id = models.CharField(max_length=50, primary_key=True) + name = models.CharField(max_length=250) + slug = AutoSlugField(populate_from=["name"]) description = models.TextField(blank=True, null=True) - - class Meta: - constraints = [models.UniqueConstraint("extid", name="unique departemt extid")] + members = models.ManyToManyField( + settings.AUTH_USER_MODEL, blank=True, through="nina.DepartmentMembership", related_name="members" + ) def __str__(self) -> str: return self.name + + +class DepartmentMembership(models.Model): + department = models.ForeignKey("nina.Department", on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) diff --git a/metadata_catalogue/nina/urls.py b/metadata_catalogue/nina/urls.py index de02ca4..3c2a781 100644 --- a/metadata_catalogue/nina/urls.py +++ b/metadata_catalogue/nina/urls.py @@ -4,8 +4,8 @@ urlpatterns = [ path("projects/", ProjectListView.as_view(), name="projects-list"), - path("projects//", ProjectDetailView.as_view(), name="projects-detail"), - path("projects//edit/", ProjectUpdateView.as_view(), name="projects-edit"), + path("projects//", ProjectDetailView.as_view(), name="projects-detail"), + path("projects//edit/", ProjectUpdateView.as_view(), name="projects-edit"), path("departments/", DepartmentListView.as_view(), name="departments-list"), - path("departments//", DepartmentDetailView.as_view(), name="departments-detail"), + path("departments//", DepartmentDetailView.as_view(), name="departments-detail"), ] diff --git a/metadata_catalogue/projects/admin.py b/metadata_catalogue/projects/admin.py index 7a94ac4..73cafa5 100644 --- a/metadata_catalogue/projects/admin.py +++ b/metadata_catalogue/projects/admin.py @@ -1,12 +1,22 @@ from django.contrib.admin import ModelAdmin, site from .conf import settings -from .models import Project +from .models import Project, ProjectMembership class ProjectAdmin(ModelAdmin): pass +class ProjectMembershipAdmin(ModelAdmin): + pass + + if not settings.PROJECTS_PROJECT_MODEL or settings.PROJECTS_PROJECT_MODEL == "projects.Project": site.register(Project, ProjectAdmin) + +if ( + not settings.PROJECTS_PROJECTMEMBERSHIP_MODEL + or settings.PROJECTS_PROJECTMEMBERSHIP_MODEL == "projects.ProjectMembership" +): + site.register(ProjectMembershipAdmin, ProjectMembershipAdmin) diff --git a/metadata_catalogue/projects/migrations/0001_initial.py b/metadata_catalogue/projects/migrations/0001_initial.py index 4d424f9..3026b0c 100644 --- a/metadata_catalogue/projects/migrations/0001_initial.py +++ b/metadata_catalogue/projects/migrations/0001_initial.py @@ -1,35 +1,63 @@ -# Generated by Django 4.2.8 on 2024-01-05 14:10 +# Generated by Django 4.2.8 on 2024-01-09 15:11 +import swapper +from django.conf import settings from django.db import migrations, models import django.db.models.deletion -import swapper +import django_extensions.db.fields + class Migration(migrations.Migration): initial = True dependencies = [ - ("organizations", "0006_alter_organization_slug"), + swapper.dependency('projects', 'Project'), + swapper.dependency('projects', 'ProjectMembership'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name="Project", + name="ProjectMembership", fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ( - "organization_ptr", - models.OneToOneField( - auto_created=True, + "project", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=swapper.get_model_name('projects', 'Project')), + ), + ( + "user", + models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to="organizations.organization", + related_name="projects_membership", + to=settings.AUTH_USER_MODEL, ), ), ], + options={ + "swappable": swapper.swappable_setting('projects', 'ProjectMembership'), + }, + ), + migrations.CreateModel( + name="Project", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=250)), + ( + "slug", + django_extensions.db.fields.AutoSlugField(blank=True, editable=False, populate_from=["name"]), + ), + ( + "members", + models.ManyToManyField(blank=True, through=swapper.get_model_name('projects', 'ProjectMembership'), to=settings.AUTH_USER_MODEL), + ), + ], options={ "swappable": swapper.swappable_setting('projects', 'Project'), }, - bases=("organizations.organization",), + ), + migrations.AddConstraint( + model_name="projectmembership", + constraint=models.UniqueConstraint(["project", "user"], name="unique user per project"), ), ] diff --git a/metadata_catalogue/projects/models.py b/metadata_catalogue/projects/models.py index 2fe3fb0..380bc27 100644 --- a/metadata_catalogue/projects/models.py +++ b/metadata_catalogue/projects/models.py @@ -1,9 +1,26 @@ import swapper +from django.db import models from django.urls import reverse -from organizations.models import Organization +from django_extensions.db.fields import AutoSlugField +from .conf import settings + + +class BaseProject(models.Model): + name = models.CharField(max_length=250) + slug = AutoSlugField(populate_from=["name"]) + members = models.ManyToManyField( + settings.AUTH_USER_MODEL, blank=True, through=swapper.get_model_name("projects", "ProjectMembership") + ) + + class Meta: + abstract = True + + +class BaseProjectMembership(models.Model): + project = models.ForeignKey(swapper.get_model_name("projects", "Project"), on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="projects_membership") -class BaseProject(Organization): class Meta: abstract = True @@ -14,3 +31,9 @@ class Meta: def get_absolute_url(self): return reverse("projects-detail", kwargs={"slug": self.slug}) + + +class ProjectMembership(BaseProjectMembership): + class Meta: + swappable = swapper.swappable_setting("projects", "ProjectMembership") + constraints = [models.UniqueConstraint(["project", "user"], name="unique user per project")] diff --git a/metadata_catalogue/templates/nina/department_detail.html b/metadata_catalogue/templates/nina/department_detail.html index 65afba1..83db5b5 100644 --- a/metadata_catalogue/templates/nina/department_detail.html +++ b/metadata_catalogue/templates/nina/department_detail.html @@ -4,6 +4,6 @@

{{ object.name }}

{{ object.description|default_if_none:"No description" }}

- Back + Back
{% endblock content %} diff --git a/metadata_catalogue/templates/nina/department_list.html b/metadata_catalogue/templates/nina/department_list.html index e7b7fc5..1d951b3 100644 --- a/metadata_catalogue/templates/nina/department_list.html +++ b/metadata_catalogue/templates/nina/department_list.html @@ -7,7 +7,7 @@

Departments

{% for project in object_list %}
  • - {{ project.name }} + {{ project.name }}

    {{ project.description|default_if_none:'No description' }}

  • diff --git a/metadata_catalogue/templates/nina/project_filter.html b/metadata_catalogue/templates/nina/project_filter.html index 2c9e443..0b69c4d 100644 --- a/metadata_catalogue/templates/nina/project_filter.html +++ b/metadata_catalogue/templates/nina/project_filter.html @@ -11,7 +11,7 @@

    Projects

    {% for project in object_list %}
  • - {{ project.name }} + {{ project.name }}

    {{ project.description|default_if_none:'No description' }}

  • diff --git a/metadata_catalogue/templates/nina/project_form.html b/metadata_catalogue/templates/nina/project_form.html index f1ece03..6ed265f 100644 --- a/metadata_catalogue/templates/nina/project_form.html +++ b/metadata_catalogue/templates/nina/project_form.html @@ -11,7 +11,7 @@

    {{ object.name }}

    Back + href="{% url 'projects-detail' pk=object.pk %}">Back
    diff --git a/pdm.lock b/pdm.lock index 4bc8f0c..e8499b6 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev", "docs"] strategy = ["cross_platform"] lock_version = "4.4" -content_hash = "sha256:f6ba5f395509aa64d27f299177c3a448670ce3c43ce92e504a735f20fe22bf59" +content_hash = "sha256:b9f1278f1c99bb897e125227117fab9da130bdc7748ef31723c6473636a1a4fc" [[package]] name = "affine" @@ -873,20 +873,6 @@ files = [ {file = "django_ninja-1.1.0.tar.gz", hash = "sha256:87bff046416a2653ed2fbef1408e101292bf8170684821bac82accfd73bef059"}, ] -[[package]] -name = "django-organizations" -version = "2.3.1" -requires_python = ">=3.8" -summary = "Group accounts for Django" -dependencies = [ - "Django>=3.2.0", - "django-extensions>=2.0.8", -] -files = [ - {file = "django-organizations-2.3.1.tar.gz", hash = "sha256:e692177ddf1a9fb55a66e97ed8a51778569d28af013cde4952b8101ce25004b9"}, - {file = "django_organizations-2.3.1-py2.py3-none-any.whl", hash = "sha256:14c44905bdec5b928f80c1686306fe6909cced84eae22ea4a16130f9236af49d"}, -] - [[package]] name = "django-picklefield" version = "3.1" diff --git a/pyproject.toml b/pyproject.toml index 9049f69..03c2bf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -170,7 +170,6 @@ dependencies = [ "django-appconf>=1.0.6", "django-cors-headers>=4.3.1", "rules>=3.3", - "django-organizations>=2.3.1", "swapper>=1.3.0", "django-taggit>=5.0.1", "django-filter>=23.5",