-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
task/DES-2707: add filemeta app (#1194)
* Add filemeta app * Add AuthenticatedApiView to main from f19997b#diff-f1bbb364f4c92d6f513be2ef3c46a1a4e14453cadd8b18ef25bc124125c0d927 * Update designsafe/apps/api/filemeta/models.py Co-authored-by: Jake Rosenberg <[email protected]> * Add app to INSTALLED_APPS * Rename data to value * Fix url's by path * Add access check * Add unit tests * Extend tests * Fix pylint issues * Fix black formatting issues * Add migration * Rework model so all info is in json value column * Add test * Update docstring for publications * Add todo * Add mising migration --------- Co-authored-by: Jake Rosenberg <[email protected]>
- Loading branch information
1 parent
d168f1d
commit 6a15778
Showing
14 changed files
with
422 additions
and
4 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
"""File Meta app config""" | ||
from django.apps import AppConfig | ||
|
||
|
||
class FileMetaAppConfig(AppConfig): | ||
"""App config for File Meta""" | ||
|
||
name = "designsafe.apps.api.filemeta" | ||
label = "filemeta_api" | ||
verbose_name = "Designsafe File Meta" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Generated by Django 4.2.6 on 2024-04-18 16:44 | ||
|
||
from django.db import migrations, models | ||
import django.utils.timezone | ||
|
||
|
||
class Migration(migrations.Migration): | ||
initial = True | ||
|
||
dependencies = [] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="FileMetaModel", | ||
fields=[ | ||
( | ||
"id", | ||
models.AutoField( | ||
auto_created=True, | ||
primary_key=True, | ||
serialize=False, | ||
verbose_name="ID", | ||
), | ||
), | ||
("value", models.JSONField()), | ||
("created", models.DateTimeField(default=django.utils.timezone.now)), | ||
("last_updated", models.DateTimeField(auto_now=True)), | ||
], | ||
options={ | ||
"indexes": [ | ||
models.Index(models.F("value__system"), name="idx_value_system"), | ||
models.Index(models.F("value__path"), name="idx_value_path"), | ||
], | ||
}, | ||
), | ||
] |
21 changes: 21 additions & 0 deletions
21
designsafe/apps/api/filemeta/migrations/0002_auto_20240418_1650.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Generated by Django 4.2.6 on 2024-04-18 16:50 | ||
# Custom migration to handle unique constraint of system/path in "value" | ||
from django.db import migrations, models | ||
import django.db.models.expressions as expressions | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("filemeta_api", "0001_initial"), | ||
] | ||
|
||
operations = [ | ||
migrations.AddConstraint( | ||
model_name="filemetamodel", | ||
constraint=models.UniqueConstraint( | ||
expressions.F("value__system"), | ||
expressions.F("value__path"), | ||
name="unique_system_path", | ||
), | ||
), | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
"""File Meta model""" | ||
|
||
from django.db import models, transaction | ||
from django.db.models import Q | ||
from django.utils import timezone | ||
|
||
|
||
class FileMetaModel(models.Model): | ||
"""Model for File Meta""" | ||
|
||
value = models.JSONField() # 'path' and 'system' keys are always in value | ||
created = models.DateTimeField(default=timezone.now) | ||
last_updated = models.DateTimeField(auto_now=True) | ||
|
||
class Meta: | ||
indexes = [ | ||
models.Index( | ||
models.F( | ||
"value__system" | ||
), # Functional index on 'system' within 'value' | ||
name="idx_value_system", | ||
), | ||
models.Index( | ||
models.F("value__path"), # Functional index on 'path' within 'value' | ||
name="idx_value_path", | ||
), | ||
] | ||
|
||
def __str__(self): | ||
return f"{self.value}" | ||
|
||
@classmethod | ||
def create_or_update_file_meta(cls, value): | ||
""" | ||
Create or update a FileMetaModel instance based on the provided value dict containing 'system' and 'path'. | ||
Parameters: | ||
- value (dict): A dictionary containing file metadata including required 'system' and 'path'. | ||
Returns: | ||
- tuple (instance, created): The FileMetaModel instance and a boolean indicating if it was created (True) or updated (False). | ||
""" | ||
system = value.get("system") | ||
path = value.get("path") | ||
|
||
# Use a transaction to ensure atomicity | ||
with transaction.atomic(): | ||
try: | ||
file_meta = FileMetaModel.objects.select_for_update().get( | ||
Q(value__system=system) & Q(value__path=path) | ||
) | ||
file_meta.value = value | ||
file_meta.save() | ||
return file_meta, False | ||
except FileMetaModel.DoesNotExist: | ||
file_meta = FileMetaModel.objects.create(value=value) | ||
return file_meta, True | ||
|
||
@classmethod | ||
def get_by_path_and_system(cls, system, path): | ||
""" | ||
Retrieve file metadata by 'system' and 'path'. | ||
Raises: | ||
- DoesNotExist: if file metadata entry not found | ||
""" | ||
return cls.objects.get(value__system=system, value__path=path) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import pytest | ||
import json | ||
from django.db import IntegrityError | ||
from django.test import Client | ||
from designsafe.apps.api.filemeta.models import FileMetaModel | ||
|
||
|
||
@pytest.fixture | ||
def filemeta_value_mock(): | ||
value = {"system": "project-1234", "path": "/test/path.txt"} | ||
return value | ||
|
||
|
||
@pytest.fixture | ||
def filemeta_db_mock(filemeta_value_mock): | ||
system_id = filemeta_value_mock["system"] | ||
path = filemeta_value_mock["path"] | ||
value = filemeta_value_mock | ||
file_meta_obj = FileMetaModel.objects.create(value=value) | ||
return system_id, path, file_meta_obj | ||
|
||
|
||
@pytest.fixture | ||
def mock_access_success(mocker): | ||
"""Fixture to mock the listing function to always succeed.""" | ||
mocker.patch("designsafe.apps.api.filemeta.views.listing") | ||
|
||
|
||
@pytest.fixture | ||
def mock_access_failure(mocker): | ||
"""Fixture to mock the listing function to always raise an exception.""" | ||
mocker.patch( | ||
"designsafe.apps.api.filemeta.views.listing", | ||
side_effect=Exception("Access Denied"), | ||
) | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_database_constraint(filemeta_value_mock): | ||
FileMetaModel.objects.create(value=filemeta_value_mock) | ||
with pytest.raises(IntegrityError) as excinfo: | ||
FileMetaModel.objects.create(value=filemeta_value_mock) | ||
assert "Unique" in str(excinfo.value) | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_database_get_by_path_system(filemeta_db_mock, filemeta_value_mock): | ||
FileMetaModel.get_by_path_and_system( | ||
system=filemeta_value_mock["system"], path=filemeta_value_mock["path"] | ||
) | ||
|
||
with pytest.raises(FileMetaModel.DoesNotExist): | ||
FileMetaModel.get_by_path_and_system(system="foo", path="bar/baz.txt") | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_database_constraint(filemeta_value_mock): | ||
FileMetaModel.objects.create(value=filemeta_value_mock) | ||
with pytest.raises(IntegrityError) as excinfo: | ||
FileMetaModel.objects.create(value=filemeta_value_mock) | ||
assert "Unique" in str(excinfo.value) | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_get_file_meta_unauthenticated(client, filemeta_db_mock, mock_access_success): | ||
system_id, path, file_meta = filemeta_db_mock | ||
response = client.get(f"/api/filemeta/{system_id}/{path}") | ||
assert response.status_code == 401 | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_get_file_meta( | ||
client, authenticated_user, filemeta_db_mock, mock_access_success | ||
): | ||
system_id, path, file_meta = filemeta_db_mock | ||
response = client.get(f"/api/filemeta/{system_id}/{path}") | ||
assert response.status_code == 200 | ||
|
||
assert response.json() == { | ||
"value": file_meta.value, | ||
"name": "designsafe.file", | ||
"lastUpdated": file_meta.last_updated.isoformat( | ||
timespec="milliseconds" | ||
).replace("+00:00", "Z"), | ||
} | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_create_file_meta_no_access( | ||
client, authenticated_user, filemeta_value_mock, mock_access_failure | ||
): | ||
response = client.post( | ||
"/api/filemeta/", | ||
data=json.dumps(filemeta_value_mock), | ||
content_type="application/json", | ||
) | ||
assert response.status_code == 403 | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_create_file_meta_unauthenticated(client, filemeta_value_mock): | ||
response = client.post( | ||
"/api/filemeta/", | ||
data=json.dumps(filemeta_value_mock), | ||
content_type="application/json", | ||
) | ||
assert response.status_code == 401 | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_create_file_meta( | ||
client, authenticated_user, filemeta_value_mock, mock_access_success | ||
): | ||
response = client.post( | ||
"/api/filemeta/", | ||
data=json.dumps(filemeta_value_mock), | ||
content_type="application/json", | ||
) | ||
assert response.status_code == 200 | ||
|
||
file_meta = FileMetaModel.objects.first() | ||
assert file_meta.value == filemeta_value_mock | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_create_file_meta_update_existing_entry( | ||
client, | ||
authenticated_user, | ||
filemeta_db_mock, | ||
filemeta_value_mock, | ||
mock_access_success, | ||
): | ||
updated_value = {**filemeta_value_mock, "new_key": "new_value"} | ||
|
||
response = client.post( | ||
"/api/filemeta/", | ||
data=json.dumps(updated_value), | ||
content_type="application/json", | ||
) | ||
assert response.status_code == 200 | ||
|
||
file_meta = FileMetaModel.objects.first() | ||
assert file_meta.value == updated_value | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_create_file_metadata_missing_system_or_path( | ||
client, | ||
authenticated_user, | ||
filemeta_db_mock, | ||
filemeta_value_mock, | ||
mock_access_success, | ||
): | ||
value_missing_system_path = {"foo": "bar"} | ||
|
||
response = client.post( | ||
"/api/filemeta/", | ||
data=json.dumps(value_missing_system_path), | ||
content_type="application/json", | ||
) | ||
assert response.status_code == 400 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
"""File Meta API routes""" | ||
|
||
from django.urls import path | ||
from .views import FileMetaView, CreateFileMetaView | ||
|
||
urlpatterns = [ | ||
path("<str:system_id>/<path:path>", FileMetaView.as_view(), name="filemeta-get"), | ||
path("", CreateFileMetaView.as_view(), name="filemeta-create"), | ||
] |
Oops, something went wrong.