diff --git a/compose/production/django/Dockerfile b/compose/production/django/Dockerfile
index 87abf65..f14b81e 100644
--- a/compose/production/django/Dockerfile
+++ b/compose/production/django/Dockerfile
@@ -1,5 +1,5 @@
-FROM python:3.8-slim-buster
+FROM python:3.8-slim
ENV PYTHONUNBUFFERED 1
diff --git a/config/settings/base.py b/config/settings/base.py
index d29cb09..aa891a4 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -287,3 +287,4 @@
CORS_URLS_REGEX = r"^/api/.*$"
# Your stuff...
# ------------------------------------------------------------------------------
+VALIDATE_SPEC = env.bool("VALIDATE_SPEC", True)
diff --git a/openapi_documentor/conftest.py b/openapi_documentor/conftest.py
index 2e88c50..be85299 100644
--- a/openapi_documentor/conftest.py
+++ b/openapi_documentor/conftest.py
@@ -1,7 +1,13 @@
import pytest
-
+from django.conf import settings
from openapi_documentor.users.models import User
from openapi_documentor.users.tests.factories import UserFactory
+from openapi_documentor.openapi.tests.factories import DocumentFactory
+
+
+open_api_file = settings.APPS_DIR / "openapi/tests/fixtures/hello.yaml"
+with open(open_api_file) as fout:
+ open_api_doc = fout.read()
@pytest.fixture(autouse=True)
@@ -12,3 +18,13 @@ def media_storage(settings, tmpdir):
@pytest.fixture
def user() -> User:
return UserFactory()
+
+
+@pytest.fixture
+def document(openapi) -> User:
+ return DocumentFactory(doc=openapi)
+
+
+@pytest.fixture
+def openapi() -> str:
+ return open_api_doc
diff --git a/openapi_documentor/openapi/admin.py b/openapi_documentor/openapi/admin.py
index 76684e1..0b79e24 100644
--- a/openapi_documentor/openapi/admin.py
+++ b/openapi_documentor/openapi/admin.py
@@ -1,4 +1,7 @@
from django.contrib import admin
+from django.db.models import Q
+from django.http.request import HttpRequest
+from django.http.response import HttpResponse
# Register your models here.
# from guardian.admin import GuardedModelAdmin
@@ -19,4 +22,15 @@ def get_queryset(self, request):
qs = super(DocumentAdmin, self).get_queryset(request)
if request.user.is_superuser:
return qs
- return qs.filter(owner=request.user)
+ return qs.filter(Q(owner=request.user) | Q(editors=request.user)).distinct()
+
+ def change_view(
+ self, request: HttpRequest, object_id: str, **kwargs
+ ) -> HttpResponse:
+ if not (
+ request.user.is_superuser
+ or request.user.document_set.filter(pk=object_id).exists()
+ ):
+ self.exclude = ["editors"]
+ self.readonly_fields = ["owner"]
+ return super().change_view(request, object_id, **kwargs)
diff --git a/openapi_documentor/openapi/migrations/0007_auto_20230630_1414.py b/openapi_documentor/openapi/migrations/0007_auto_20230630_1414.py
new file mode 100644
index 0000000..fbe4d55
--- /dev/null
+++ b/openapi_documentor/openapi/migrations/0007_auto_20230630_1414.py
@@ -0,0 +1,30 @@
+# Generated by Django 3.1.7 on 2023-06-30 14:14
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('openapi', '0006_auto_20210321_1802'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='document',
+ name='editors',
+ field=models.ManyToManyField(blank=True, default=None, related_name='editors', to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AlterField(
+ model_name='document',
+ name='created',
+ field=models.DateTimeField(auto_now_add=True, verbose_name='date published'),
+ ),
+ migrations.AlterField(
+ model_name='document',
+ name='modified',
+ field=models.DateTimeField(auto_now=True, verbose_name='date modified'),
+ ),
+ ]
diff --git a/openapi_documentor/openapi/models.py b/openapi_documentor/openapi/models.py
index afee421..0d070b9 100644
--- a/openapi_documentor/openapi/models.py
+++ b/openapi_documentor/openapi/models.py
@@ -4,13 +4,14 @@
import yaml
try:
- from yaml import CLoader as YamlLoader
+ from yaml import Loader as YamlLoader
except ImportError:
from yaml import YamlLoader
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
+from django.conf import settings
from django.utils.translation import gettext_lazy as _
from openapi_spec_validator import validate_spec
from taggit.managers import TaggableManager
@@ -45,8 +46,9 @@ class Document(models.Model):
default=OpenapiVersions.THREEZERO,
) # oas 3.0
owner = models.ForeignKey(User, on_delete=models.DO_NOTHING, default=None)
- created = models.DateTimeField("date published")
- modified = models.DateTimeField("date modified")
+ created = models.DateTimeField("date published", auto_now_add=True)
+ modified = models.DateTimeField("date modified", auto_now=True)
+ editors = models.ManyToManyField(User, default=None, related_name="editors", blank=True)
tags = TaggableManager(through=TaggedDocument)
class Meta:
@@ -56,6 +58,8 @@ def clean(self):
parsed_doc = self._parse_doc(self.doc)
if not parsed_doc:
raise ValidationError(_("Only Json and Yaml are allowed"))
+ if not settings.VALIDATE_SPEC:
+ return
try:
validate_spec(parsed_doc)
except: # noqa: E722
@@ -64,7 +68,7 @@ def clean(self):
def save(self, *args, **kwargs):
parsed_doc = self._parse_doc(self.doc)
if parsed_doc:
- self.formatted = json.dumps(parsed_doc)
+ self.formatted = json.dumps(parsed_doc, default=str)
super().save(*args, **kwargs)
def get_absolute_url(self):
diff --git a/openapi_documentor/openapi/tests/factories.py b/openapi_documentor/openapi/tests/factories.py
new file mode 100644
index 0000000..ac1f964
--- /dev/null
+++ b/openapi_documentor/openapi/tests/factories.py
@@ -0,0 +1,10 @@
+from factory import Faker
+from factory.django import DjangoModelFactory
+from openapi_documentor.openapi.models import Document
+
+
+class DocumentFactory(DjangoModelFactory):
+ title = Faker("user_name")
+
+ class Meta:
+ model = Document
diff --git a/openapi_documentor/openapi/tests/fixtures/hello.yaml b/openapi_documentor/openapi/tests/fixtures/hello.yaml
new file mode 100644
index 0000000..7da2aed
--- /dev/null
+++ b/openapi_documentor/openapi/tests/fixtures/hello.yaml
@@ -0,0 +1,15 @@
+openapi: 3.0.0
+info:
+ title: Minimal API
+ version: 1.0.0
+paths:
+ /hello:
+ get:
+ summary: Retrieve a greeting message
+ responses:
+ "200":
+ description: Successful response
+ content:
+ text/plain:
+ schema:
+ type: string
diff --git a/openapi_documentor/openapi/tests/test_admin.py b/openapi_documentor/openapi/tests/test_admin.py
new file mode 100644
index 0000000..dfea5f5
--- /dev/null
+++ b/openapi_documentor/openapi/tests/test_admin.py
@@ -0,0 +1,52 @@
+from unittest.mock import Mock
+
+import pytest
+from django.urls import reverse
+from django.contrib.admin.sites import AdminSite
+
+from openapi_documentor.openapi.models import Document
+from openapi_documentor.openapi.admin import DocumentAdmin
+from openapi_documentor.openapi.tests.factories import DocumentFactory
+from openapi_documentor.users.tests.factories import UserFactory
+
+pytestmark = pytest.mark.django_db
+
+
+class TestDocumentAdmin:
+ def test_add(self, admin_client, user, openapi):
+ url = reverse("admin:openapi_document_add")
+ response = admin_client.get(url)
+ assert response.status_code == 200
+
+ response = admin_client.post(
+ url,
+ data={
+ "title": "MyApiDoc",
+ "doc": openapi,
+ "owner": user.pk,
+ "version": "3.0.0",
+ "rev": "1.0.0",
+ "tags": ["test"],
+ },
+ )
+ assert response.status_code == 302
+ assert Document.objects.filter(title="MyApiDoc", owner=user).exists()
+
+ def test_list_docs_with_editor_access(self):
+ user1 = UserFactory()
+ user2 = UserFactory()
+ u1_private_doc = DocumentFactory.create_batch(3, owner=user1)
+ u1_shared = DocumentFactory.create_batch(2, owner=user1)
+ # user1 shared editor access with user2
+ for d in u1_shared:
+ d.editors.add(user2)
+
+ u2_private_doc = DocumentFactory.create_batch(2, owner=user2)
+
+ doc_admin = DocumentAdmin(model=Document, admin_site=AdminSite())
+ qs1 = doc_admin.get_queryset(request=Mock(user=user1))
+
+ assert qs1.count() == len(u1_private_doc + u1_shared)
+
+ qs2 = doc_admin.get_queryset(request=Mock(user=user2))
+ assert qs2.count() == len(u2_private_doc + u1_shared)
diff --git a/openapi_documentor/templates/openapi/document_detail.html b/openapi_documentor/templates/openapi/document_detail.html
index 50896d8..1833827 100644
--- a/openapi_documentor/templates/openapi/document_detail.html
+++ b/openapi_documentor/templates/openapi/document_detail.html
@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% load static %}
{% block custom_javascript %}
-
+
{% endblock custom_javascript %}
{% block title %}Api: {{ object.title }}{% endblock %}
diff --git a/requirements/local.txt b/requirements/local.txt
index c898704..df106a6 100644
--- a/requirements/local.txt
+++ b/requirements/local.txt
@@ -5,7 +5,7 @@ ipdb==0.13.5 # https://github.com/gotcha/ipdb
# Testing
# ------------------------------------------------------------------------------
-mypy==0.812 # https://github.com/python/mypy
+mypy==1.5.1 # https://github.com/python/mypy
django-stubs==1.7.0 # https://github.com/typeddjango/django-stubs
pytest==6.2.2 # https://github.com/pytest-dev/pytest
pytest-sugar==0.9.4 # https://github.com/Frozenball/pytest-sugar
@@ -32,3 +32,4 @@ django-debug-toolbar==3.2 # https://github.com/jazzband/django-debug-toolbar
django-extensions==3.1.1 # https://github.com/django-extensions/django-extensions
django-coverage-plugin==1.8.0 # https://github.com/nedbat/django_coverage_plugin
pytest-django==4.1.0 # https://github.com/pytest-dev/pytest-django
+typed-ast==1.5.4