Commit cb0663cc authored by Mickaël Desfrênes's avatar Mickaël Desfrênes
Browse files

add notion of project properties as a general key->value store

parent 8e4ea6ff
Loading
Loading
Loading
Loading
+40 −0
Original line number Diff line number Diff line
# Generated by Django 4.0 on 2022-01-21 10:23

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


class Migration(migrations.Migration):

    dependencies = [
        ("resources", "0024_alter_metadata_options"),
    ]

    operations = [
        migrations.CreateModel(
            name="ProjectProperty",
            fields=[
                (
                    "id",
                    models.AutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                ("key", models.CharField(max_length=64)),
                ("value", models.JSONField()),
                (
                    "project",
                    models.ForeignKey(
                        on_delete=django.db.models.deletion.CASCADE,
                        to="resources.project",
                    ),
                ),
            ],
            options={
                "unique_together": {("project", "key")},
            },
        ),
    ]
+35 −0
Original line number Diff line number Diff line
# Generated by Django 4.0 on 2022-01-21 10:25

from django.db import migrations
from resources import models


def add_fixtures(*args):
    create_right, _ = models.Permission.objects.get_or_create(
        label="project_property.create"
    )
    read_right, _ = models.Permission.objects.get_or_create(
        label="project_property.read"
    )
    update_right, _ = models.Permission.objects.get_or_create(
        label="project_property.update"
    )
    delete_right, _ = models.Permission.objects.get_or_create(
        label="project_property.delete"
    )
    for project in models.Project.objects.all():
        admin_role = models.Role.objects.filter(project=project, label="admin").first()
        if admin_role:
            admin_role.permissions.add(create_right)
            admin_role.permissions.add(read_right)
            admin_role.permissions.add(update_right)
            admin_role.permissions.add(delete_right)


class Migration(migrations.Migration):

    dependencies = [
        ("resources", "0025_projectproperty"),
    ]

    operations = [migrations.RunPython(add_fixtures)]
+18 −0
Original line number Diff line number Diff line
@@ -71,6 +71,24 @@ class Project(models.Model):
        return col


class ProjectProperty(models.Model):
    """
    Generic key/value store for the project.

    Store anything that is needed by the client application,
    like user prefs, field labels or other useful data.

    Be extra careful with access rights here.
    """

    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    key = models.CharField(max_length=64, null=False, blank=False)
    value = models.JSONField()

    class Meta:
        unique_together = (("project", "key"),)


class Tag(models.Model):
    uid = models.TextField()
    ark = models.TextField(null=True, blank=True)
+6 −0
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ NO_SUCH_RESOURCE = "no such resource"
NO_SUCH_TAG = "no such tag"
NO_SUCH_USER = "no such user"
NO_PROJECT_ROOT = "project has no root"
NO_SUCH_PROJECT_PROPERTY = "project has no such property"
NOT_A_FILE = "not a file"
PERM_COLLECTION_CREATE = "collection.create"
PERM_COLLECTION_DELETE = "collection.delete"
@@ -34,6 +35,10 @@ PERM_RESOURCE_READ = "resource.read"
PERM_RESOURCE_UPDATE = "resource.update"
PERM_ROLE_READ = "role.read"
PERM_TAG_DELETE = "tag.delete"
PERM_PROJECT_PROPERTY_CREATE = "project_property.create"
PERM_PROJECT_PROPERTY_READ = "project_property.read"
PERM_PROJECT_PROPERTY_UPDATE = "project_property.update"
PERM_PROJECT_PROPERTY_DELETE = "project_property.delete"
ROOT_COLLECTION_TITLE = "root"
SEARCH_TERMS_LIMIT = 10
SUPERUSER_NEEDED = "only for superusers"
@@ -41,3 +46,4 @@ TOO_MANY_SEARCH_TERMS = "Too many search terms."
WRONG_ARGUMENT = "wrong argument"
NO_FOOTGUNS = "Can't shoot your own foot"
PROJECT_MISMATCH = "Can't mix items from different collections"
INVALID_PROPERTY_KEY = "invalid property key"
+84 −0
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@ from resources.models import (
    Role,
    Permission,
    APIKey,
    ProjectProperty,
)
from django.contrib.auth.models import User
from django.db.models import QuerySet
@@ -2666,3 +2667,86 @@ def public_collections(user: User, project_id: int) -> List[dict]:
    for col in Collection.objects.filter(public_access=True, project_id=project_id):
        data.append(serializers.collection(col, cache=serializer_cache))
    return data


@_log_call
@_rpc_groups(["Projects"])
def set_project_property(
    user: User, project_id: int, property_key: str, property_value: dict
) -> dict:
    """
    Set a property value to the project.

    property_key is NOT case sensitive, ie. "ProPertY" is the same as "pRoperTy" or "property".
    """
    _check_project_permission(user, project_id, PERM_PROJECT_PROPERTY_CREATE)
    _check_project_permission(user, project_id, PERM_PROJECT_PROPERTY_UPDATE)
    property_key = str(property_key)
    property_key = property_key.strip()
    property_key = property_key.lower()
    if not property_key or len(property_key) > 64:
        raise ServiceException(INVALID_PROPERTY_KEY)
    prop, _ = ProjectProperty.objects.update_or_create(
        project_id=project_id, key=property_key, defaults={"value": property_value}
    )
    return serializers.project_property(prop)


@_log_call
@_rpc_groups(["Projects"])
def delete_project_property(user: User, project_id: int, property_key: str) -> bool:
    """
    Delete a property from the project.

    property_key is NOT case sensitive, ie. "ProPertY" is the same as "pRoperTy" or "property".
    """
    _check_project_permission(user, project_id, PERM_PROJECT_PROPERTY_DELETE)
    property_key = str(property_key)
    property_key = property_key.strip()
    property_key = property_key.lower()
    if not property_key or len(property_key) > 64:
        raise ServiceException(INVALID_PROPERTY_KEY)
    prop = ProjectProperty.objects.filter(
        project_id=project_id, key=property_key
    ).first()
    if prop:
        prop.delete()
        return True
    raise ServiceException(NO_SUCH_PROJECT_PROPERTY)


@_log_call
@_rpc_groups(["Projects"])
def project_property(user: User, project_id: int, property_key: str) -> dict:
    """
    Get a property value from the project.

    property_key is NOT case sensitive, ie. "ProPertY" is the same as "pRoperTy" or "property".

    Will raise an exception if property does not exist.
    """
    _check_project_permission(user, project_id, PERM_PROJECT_PROPERTY_READ)
    property_key = str(property_key)
    property_key = property_key.strip()
    property_key = property_key.lower()
    if not property_key or len(property_key) > 64:
        raise ServiceException(INVALID_PROPERTY_KEY)
    prop = ProjectProperty.objects.filter(
        project_id=project_id, key=property_key
    ).first()
    if prop:
        return serializers.project_property(prop)
    raise ServiceException(NO_SUCH_PROJECT_PROPERTY)


@_log_call
@_rpc_groups(["Projects"])
def project_properties(user: User, project_id: int) -> List[dict]:
    """
    Get ALL properties from a project.
    """
    _check_project_permission(user, project_id, PERM_PROJECT_PROPERTY_READ)
    data = []
    for prop in ProjectProperty.objects.filter(project_id=project_id):
        data.append(serializers.project_property(prop))
    return data
Loading