From 95d395a594fa1eb0c70067864c4c4116bd2c7130 Mon Sep 17 00:00:00 2001 From: Sandeep Rawat Date: Fri, 30 Jun 2023 20:40:43 +0530 Subject: [PATCH 1/3] Closes #25: Add editors permission to doc --- openapi_documentor/conftest.py | 18 ++++++- openapi_documentor/openapi/admin.py | 16 +++++- .../migrations/0007_auto_20230630_1414.py | 30 +++++++++++ openapi_documentor/openapi/models.py | 5 +- openapi_documentor/openapi/tests/factories.py | 10 ++++ .../openapi/tests/fixtures/hello.yaml | 15 ++++++ .../openapi/tests/test_admin.py | 52 +++++++++++++++++++ 7 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 openapi_documentor/openapi/migrations/0007_auto_20230630_1414.py create mode 100644 openapi_documentor/openapi/tests/factories.py create mode 100644 openapi_documentor/openapi/tests/fixtures/hello.yaml create mode 100644 openapi_documentor/openapi/tests/test_admin.py 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..96c2fd9 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)) + + 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..0e47431 100644 --- a/openapi_documentor/openapi/models.py +++ b/openapi_documentor/openapi/models.py @@ -45,8 +45,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: 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) From 34a79b09ca2907593f062e09cd5ae68470b15030 Mon Sep 17 00:00:00 2001 From: Sandeep Rawat Date: Thu, 14 Sep 2023 14:31:22 +0530 Subject: [PATCH 2/3] Fix list documents page --- openapi_documentor/openapi/admin.py | 2 +- openapi_documentor/openapi/models.py | 2 +- requirements/local.txt | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openapi_documentor/openapi/admin.py b/openapi_documentor/openapi/admin.py index 96c2fd9..0b79e24 100644 --- a/openapi_documentor/openapi/admin.py +++ b/openapi_documentor/openapi/admin.py @@ -22,7 +22,7 @@ def get_queryset(self, request): qs = super(DocumentAdmin, self).get_queryset(request) if request.user.is_superuser: return qs - return qs.filter(Q(owner=request.user) | Q(editors=request.user)) + return qs.filter(Q(owner=request.user) | Q(editors=request.user)).distinct() def change_view( self, request: HttpRequest, object_id: str, **kwargs diff --git a/openapi_documentor/openapi/models.py b/openapi_documentor/openapi/models.py index 0e47431..51b2cf0 100644 --- a/openapi_documentor/openapi/models.py +++ b/openapi_documentor/openapi/models.py @@ -4,7 +4,7 @@ import yaml try: - from yaml import CLoader as YamlLoader + from yaml import Loader as YamlLoader except ImportError: from yaml import YamlLoader 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 From 5fdcaac3604125b059504ebe6b7c0b535ada5801 Mon Sep 17 00:00:00 2001 From: Kuldeep Date: Fri, 14 Nov 2025 12:15:46 +0000 Subject: [PATCH 3/3] Update redocly js uri --- compose/production/django/Dockerfile | 2 +- config/settings/base.py | 1 + openapi_documentor/openapi/models.py | 5 ++++- openapi_documentor/templates/openapi/document_detail.html | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) 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/openapi/models.py b/openapi_documentor/openapi/models.py index 51b2cf0..0d070b9 100644 --- a/openapi_documentor/openapi/models.py +++ b/openapi_documentor/openapi/models.py @@ -11,6 +11,7 @@ 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 @@ -57,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 @@ -65,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/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 %}