From 8e8cf0ea538616ed9cf538c291df8f958627a1fb Mon Sep 17 00:00:00 2001 From: Andrew Conti Date: Fri, 30 Aug 2024 16:51:09 -0400 Subject: [PATCH] chore: remove node-progressive (#2375) * chore: remove node-progressive * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * chore: update ci options * chore: update ci settings location --- .github/workflows/push.yaml | 5 +++-- README.md | 6 ++--- .../Dockerfile | 17 +++++++++----- .../conftest.py | 7 ++++++ .../docker-compose.yml | 9 ++++---- .../requirements.txt | 5 +---- .../{{cookiecutter.app_name}}/config/local.py | 10 --------- .../{{cookiecutter.app_name}}/pytest.ini | 4 ++++ .../{{cookiecutter.app_name}}/urls.py | 3 +-- .../users/permissions.py | 14 ++++++++---- .../users/test/test_serializers.py | 11 +++++----- .../users/test/test_views.py | 18 +++++++-------- .../{{cookiecutter.app_name}}/users/views.py | 22 ++++++++----------- 13 files changed, 70 insertions(+), 61 deletions(-) create mode 100644 {{cookiecutter.github_repository_name}}/conftest.py create mode 100644 {{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/pytest.ini diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 246ad4e14..0120105bd 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -31,6 +31,7 @@ jobs: - name: Run Web tests run: docker compose run --rm web bash -c "python3 -m flake8 . && - python3 wait_for_postgres.py && python3 -Wall ./manage.py check --deploy && - python3 -m pytest --ds=piedpiper.settings --dc=Local" + python3 wait_for_postgres.py && + python3 -m pytest --ds=piedpiper.config --dc=Local -p no:cacheprovider -s" + diff --git a/README.md b/README.md index 7d2171e77..f7ef4d3b1 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,10 @@ You need to make a scalable api on a deadline. You deeply care about the quality ## Highlights -- Modern Python development with Python 3.8+ -- Bleeding edge Django 3.0+ +- Modern Python development with Python 3.12+ +- Bleeding edge Django 5.0+ - Fully dockerized, local development via docker-compose. -- PostgreSQL 11.6+ +- PostgreSQL 16.4+ - Start off with full test coverage and [continuous integration](https://github.com/agconti/cookiecutter-django-rest/blob/master/%7B%7Bcookiecutter.github_repository_name%7D%7D/.travis.yml). - Complete [Django Rest Framework](http://www.django-rest-framework.org/) integration - Always current dependencies and security updates enforced by [pyup.io](https://pyup.io/). diff --git a/{{cookiecutter.github_repository_name}}/Dockerfile b/{{cookiecutter.github_repository_name}}/Dockerfile index b81d0ccd0..0c424b7a8 100644 --- a/{{cookiecutter.github_repository_name}}/Dockerfile +++ b/{{cookiecutter.github_repository_name}}/Dockerfile @@ -1,15 +1,22 @@ -FROM python:3.8 -ENV PYTHONUNBUFFERED 1 +FROM python:3.12-slim as base +FROM base as builder # Allows docker to cache installed dependencies between builds +RUN apt-get update && apt-get -y install libpq-dev gcc COPY ./requirements.txt requirements.txt -RUN pip install -r requirements.txt +RUN pip3 install --no-cache-dir --target=packages -r requirements.txt + +FROM base as runtime +COPY --from=builder packages /usr/lib/python3.12/site-packages +ENV PYTHONPATH=/usr/lib/python3.12/site-packages + +# Security Context +RUN useradd -m nonroot +USER nonroot -# Adds our application code to the image COPY . code WORKDIR code EXPOSE 8000 - # Run the production server CMD newrelic-admin run-program gunicorn --bind 0.0.0.0:$PORT --access-logfile - {{cookiecutter.app_name}}.wsgi:application diff --git a/{{cookiecutter.github_repository_name}}/conftest.py b/{{cookiecutter.github_repository_name}}/conftest.py new file mode 100644 index 000000000..7fd006b13 --- /dev/null +++ b/{{cookiecutter.github_repository_name}}/conftest.py @@ -0,0 +1,7 @@ +import pytest + +# Ensures pytest waits for the database to load +# https://pytest-django.readthedocs.io/en/latest/faq.html#how-can-i-give-database-access-to-all-my-tests-without-the-django-db-marker +@pytest.fixture(autouse=True) +def enable_db_access_for_all_tests(db): + pass diff --git a/{{cookiecutter.github_repository_name}}/docker-compose.yml b/{{cookiecutter.github_repository_name}}/docker-compose.yml index d0cb0d49f..969d8e54f 100644 --- a/{{cookiecutter.github_repository_name}}/docker-compose.yml +++ b/{{cookiecutter.github_repository_name}}/docker-compose.yml @@ -1,14 +1,15 @@ -version: '3' services: postgres: - image: postgres:11.6 + environment: + - POSTGRES_HOST_AUTH_METHOD=trust + image: postgres:16.4 web: restart: always environment: - DJANGO_SECRET_KEY=local build: ./ command: > - bash -c "python wait_for_postgres.py && + bash -c "python3 wait_for_postgres.py && ./manage.py migrate && ./manage.py runserver 0.0.0.0:8000" volumes: @@ -20,7 +21,7 @@ services: documentation: restart: always build: ./ - command: "mkdocs serve" + command: "python3 -m mkdocs serve" volumes: - ./:/code ports: diff --git a/{{cookiecutter.github_repository_name}}/requirements.txt b/{{cookiecutter.github_repository_name}}/requirements.txt index cb87f58f4..a1ade54e2 100644 --- a/{{cookiecutter.github_repository_name}}/requirements.txt +++ b/{{cookiecutter.github_repository_name}}/requirements.txt @@ -1,5 +1,4 @@ # Core -pytz==2024.1 Django==5.1 django-configurations==2.5.1 gunicorn==23.0.0 @@ -15,7 +14,6 @@ django_unique_upload==0.2.1 # Rest apis djangorestframework==3.15.2 -Markdown==3.7 django-filter==24.3 # Developer Tools @@ -27,8 +25,7 @@ flake8==7.1.1 # Testing mock==5.1.0 factory-boy==3.3.1 -django-nose==1.4.7 -nose-progressive==1.5.2 +pytest-django==4.8.0 coverage==7.6.1 # Static and Media Storage diff --git a/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/config/local.py b/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/config/local.py index 17c071fc0..ca125405b 100755 --- a/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/config/local.py +++ b/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/config/local.py @@ -8,16 +8,6 @@ class Local(Common): # Testing INSTALLED_APPS = Common.INSTALLED_APPS - INSTALLED_APPS += ('django_nose',) - TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' - NOSE_ARGS = [ - BASE_DIR, - '-s', - '--nologcapture', - '--with-coverage', - '--with-progressive', - '--cover-package={{cookiecutter.app_name}}' - ] # Mail EMAIL_HOST = 'localhost' diff --git a/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/pytest.ini b/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/pytest.ini new file mode 100644 index 000000000..5cc5bff9b --- /dev/null +++ b/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +DJANGO_CONFIGURATION = Local +DJANGO_SETTINGS_MODULE = {{cookiecutter.app_name}}.config +python_files = tests.py test_*.py *_tests.py \ No newline at end of file diff --git a/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/urls.py b/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/urls.py index 8ea4edbfc..b921da1ab 100755 --- a/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/urls.py +++ b/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/urls.py @@ -5,11 +5,10 @@ from django.views.generic.base import RedirectView from rest_framework.routers import DefaultRouter from rest_framework.authtoken import views -from .users.views import UserViewSet, UserCreateViewSet +from .users.views import UserViewSet router = DefaultRouter() router.register(r'users', UserViewSet) -router.register(r'users', UserCreateViewSet) urlpatterns = [ path('admin/', admin.site.urls), diff --git a/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/permissions.py b/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/permissions.py index 0596a5e4f..04dce0cc2 100644 --- a/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/permissions.py +++ b/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/permissions.py @@ -1,14 +1,20 @@ from rest_framework import permissions -class IsUserOrReadOnly(permissions.BasePermission): +class IsUserOrCreatingAccountOrReadOnly(permissions.BasePermission): """ - Object-level permission to only allow owners of an object to edit it. + Object-level permission that allows users to create accounts or edit their + own accounts. """ def has_object_permission(self, request, view, obj): + user_is_making_new_account = view.action == 'create' + if user_is_making_new_account: + return True - if request.method in permissions.SAFE_METHODS: + is_read_only_action = request.method in permissions.SAFE_METHODS + if is_read_only_action: return True - return obj == request.user + is_accessing_their_own_user_object = obj == request.user + return is_accessing_their_own_user_object diff --git a/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/test/test_serializers.py b/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/test/test_serializers.py index 22ec9b2aa..bf61a6dbe 100644 --- a/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/test/test_serializers.py +++ b/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/test/test_serializers.py @@ -1,11 +1,12 @@ from django.test import TestCase from django.forms.models import model_to_dict from django.contrib.auth.hashers import check_password -from nose.tools import eq_, ok_ +import pytest from .factories import UserFactory from ..serializers import CreateUserSerializer +@pytest.mark.django_db class TestCreateUserSerializer(TestCase): def setUp(self): @@ -13,15 +14,15 @@ def setUp(self): def test_serializer_with_empty_data(self): serializer = CreateUserSerializer(data={}) - eq_(serializer.is_valid(), False) + assert serializer.is_valid() is False def test_serializer_with_valid_data(self): serializer = CreateUserSerializer(data=self.user_data) - ok_(serializer.is_valid()) + assert serializer.is_valid() def test_serializer_hashes_password(self): serializer = CreateUserSerializer(data=self.user_data) - ok_(serializer.is_valid()) + assert serializer.is_valid() user = serializer.save() - ok_(check_password(self.user_data.get('password'), user.password)) + assert check_password(self.user_data.get('password'), user.password) diff --git a/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/test/test_views.py b/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/test/test_views.py index 58bfe8b71..7945a5647 100644 --- a/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/test/test_views.py +++ b/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/test/test_views.py @@ -1,16 +1,16 @@ from django.urls import reverse from django.contrib.auth.hashers import check_password -from nose.tools import ok_, eq_ from rest_framework.test import APITestCase from rest_framework import status from faker import Faker import factory +import pytest from ..models import User from .factories import UserFactory fake = Faker() - +@pytest.mark.django_db class TestUserListTestCase(APITestCase): """ Tests /users list operations. @@ -22,15 +22,15 @@ def setUp(self): def test_post_request_with_no_data_fails(self): response = self.client.post(self.url, {}) - eq_(response.status_code, status.HTTP_400_BAD_REQUEST) + assert response.status_code == status.HTTP_400_BAD_REQUEST def test_post_request_with_valid_data_succeeds(self): response = self.client.post(self.url, self.user_data) - eq_(response.status_code, status.HTTP_201_CREATED) + assert response.status_code == status.HTTP_201_CREATED user = User.objects.get(pk=response.data.get('id')) - eq_(user.username, self.user_data.get('username')) - ok_(check_password(self.user_data.get('password'), user.password)) + assert user.username == self.user_data.get('username') + assert check_password(self.user_data.get('password'), user.password) class TestUserDetailTestCase(APITestCase): @@ -45,13 +45,13 @@ def setUp(self): def test_get_request_returns_a_given_user(self): response = self.client.get(self.url) - eq_(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK def test_put_request_updates_a_user(self): new_first_name = fake.first_name() payload = {'first_name': new_first_name} response = self.client.put(self.url, payload) - eq_(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK user = User.objects.get(pk=self.user.id) - eq_(user.first_name, new_first_name) + assert user.first_name == new_first_name diff --git a/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/views.py b/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/views.py index 4ab74d634..ca6c05dae 100644 --- a/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/views.py +++ b/{{cookiecutter.github_repository_name}}/{{cookiecutter.app_name}}/users/views.py @@ -1,11 +1,11 @@ from rest_framework import viewsets, mixins -from rest_framework.permissions import AllowAny from .models import User -from .permissions import IsUserOrReadOnly +from .permissions import IsUserOrCreatingAccountOrReadOnly from .serializers import CreateUserSerializer, UserSerializer -class UserViewSet(mixins.RetrieveModelMixin, +class UserViewSet(mixins.CreateModelMixin, + mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet): """ @@ -13,14 +13,10 @@ class UserViewSet(mixins.RetrieveModelMixin, """ queryset = User.objects.all() serializer_class = UserSerializer - permission_classes = (IsUserOrReadOnly,) + permission_classes = (IsUserOrCreatingAccountOrReadOnly,) - -class UserCreateViewSet(mixins.CreateModelMixin, - viewsets.GenericViewSet): - """ - Creates user accounts - """ - queryset = User.objects.all() - serializer_class = CreateUserSerializer - permission_classes = (AllowAny,) + def get_serializer_class(self): + is_creating_a_new_user = self.action == 'create' + if is_creating_a_new_user: + return CreateUserSerializer + return self.serializer_class