Martín Miranda

Merge branch 'feature/#9_crear_nuevas_app_usuarios' into 'develop'

Feature/#9 crear nuevas app usuarios



See merge request !5
import requests
from rest_framework.response import Response
from rest_framework.decorators import api_view
from django.conf import settings
@api_view(['POST'])
def recaptcha(request):
r = requests.post(
'https://www.google.com/recaptcha/api/siteverify',
data={
'secret': settings.SECRET_KEY_CAPTCHA,
'response': request.data['captcha_value'],
}
)
return Response({'captcha': r.json()})
... ...
from rest_framework_json_api.pagination import JsonApiPageNumberPagination
class LargePagination(JsonApiPageNumberPagination):
max_page_size = 300
page_size_query_param = 'page_size'
\ No newline at end of file
... ...
from rest_framework.permissions import DjangoModelPermissions
class CustomModelPermissions(DjangoModelPermissions):
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': [],
'HEAD': [],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
... ...
from django.test import TestCase
# Create your tests here.
... ... @@ -3,6 +3,8 @@ import json
import pytest
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from oauth2_provider.models import get_application_model
from rest_framework.test import APIClient
... ... @@ -11,27 +13,82 @@ User = get_user_model()
CONTENT_TYPE_JSON = 'application/json'
@pytest.fixture
def create_user(username, first_name='Admin', last_name='Root', email=None):
def create_user(username, first_name='Admin', last_name='Root', email=None, *, is_active=True,
documento_identidad='24262155'):
user, created = User.objects.get_or_create(
username=username,
documento_identidad=documento_identidad,
email='{}@root.com'.format(username) if email is None else email,
defaults=dict(
first_name=first_name,
last_name=last_name,
password='password'
password='password',
is_active=is_active
)
)
return user
def crear_grupo_administrador():
grupo, _ = Group.objects.get_or_create(name='administrador')
PERMISSIONS = {
'auth': {
'group': ['add', 'change', 'view', 'delete']
},
'organismo': {
'organismo': ['add', 'change', 'view', 'delete'],
},
'usuario': {
'usuario': ['add', 'change', 'view', 'delete'],
}
}
agregar_varios_permisos_grupo(PERMISSIONS, grupo)
return grupo
def agregar_permisos_grupo(permisos_por_modelo, grupo, app):
# Loop models in group
for model_name in permisos_por_modelo:
# Loop permissions in group/model
model_ct = ContentType.objects.get(app_label=app, model=model_name)
for perm_name in permisos_por_modelo[model_name]:
# Generate permission name as Django would generate it
codename = perm_name + "_" + model_name
name = "Can " + perm_name + " " + model_name
permission, _ = Permission.objects.get_or_create(codename=codename,
content_type=model_ct,
defaults={'name': name})
grupo.permissions.add(permission)
def agregar_varios_permisos_grupo(permisos_por_app, grupo):
# iterar sobre nombres de apps
for nombre_app in permisos_por_app:
agregar_permisos_grupo(permisos_por_app[nombre_app], grupo, nombre_app)
@pytest.fixture
def get_default_test_user():
test_user = create_user(username='test_user', first_name='Test', last_name='User', email='test@user')
test_user = create_user(username='test_user', first_name='Test', last_name='User', email='test@user', documento_identidad='12345678')
return test_user
@pytest.fixture
def crear_usuarios():
usuario1 = create_user(username='usuario1', first_name='Usuario', last_name='J',
email='test@user1', documento_identidad='12345777')
usuario2 = create_user(username='usuario2', first_name='Usuario', last_name='S',
email='test@user2', documento_identidad='12345679')
usuario3 = create_user(username='usuario3', first_name='Usuario', last_name='P',
email='test@user3', documento_identidad='12345699')
return usuario1, usuario2, usuario3
def get_client_application():
Application = get_application_model()
application, _ = Application.objects.get_or_create(
... ...
from django.urls import path
from . import api
app_name = 'core'
urlpatterns = [
path('verificar-key/', api.recaptcha)
]
\ No newline at end of file
... ...
from django.contrib import admin
from core.admin import PublicadoAdmin
from organismo.models import Organismo
@admin.register(Organismo)
class OrganismoAdmin(PublicadoAdmin):
list_display = ('nombre', 'descripcion')
list_filter = ('nombre', )
search_fields = ('nombre', )
list_per_page = 10
\ No newline at end of file
... ...
from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.permissions import IsAuthenticated
from core.permissions import CustomModelPermissions
from organismo.filters import OrganismoFilter
from organismo.models import Organismo
from organismo.serializers import OrganismoSerializer
class OrganismoViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Organismo.objects.all()
permission_classes = (IsAuthenticated, CustomModelPermissions)
serializer_class = OrganismoSerializer
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
filterset_class = OrganismoFilter
ordering_fields = ('id',)
... ...
from django.apps import AppConfig
class OrganismoConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'organismo'
... ...
from django_filters import rest_framework as filters
from organismo.models import Organismo
class OrganismoFilter(filters.FilterSet):
class Meta:
model = Organismo
fields = ('nombre', 'descripcion',)
... ...
# Generated by Django 3.2.8 on 2023-06-07 13:24
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Organismo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('publicado', models.DateTimeField(blank=True, null=True)),
('nombre', models.CharField(max_length=150, unique=True)),
('descripcion', models.TextField(blank=True)),
('domicilio', models.CharField(blank=True, max_length=200)),
('telefono', models.CharField(blank=True, max_length=100)),
('email', models.EmailField(blank=True, max_length=100)),
],
options={
'ordering': ('nombre', 'descripcion'),
},
),
]
... ...
# Generated by Django 3.2.8 on 2023-06-13 13:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('organismo', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='organismo',
name='es_publico',
field=models.BooleanField(default=False),
),
]
... ...
from django.db import models
from core.models import Publicado
class Organismo(Publicado):
class Meta:
ordering = ('nombre', 'descripcion')
nombre = models.CharField(max_length=150, unique=True)
descripcion = models.TextField(blank=True)
domicilio = models.CharField(max_length=200, blank=True)
telefono = models.CharField(max_length=100, blank=True)
email = models.EmailField(max_length=100, blank=True)
es_publico = models.BooleanField(default=False)
def __str__(self):
return f'{self.nombre}'
... ...
from rest_framework_json_api import serializers
from organismo.models import Organismo
class OrganismoSerializer(serializers.ModelSerializer):
class Meta:
model = Organismo
fields = (
'nombre',
'descripcion',
'domicilio',
'telefono',
'email',
)
... ...
import pytest
from organismo.models import Organismo
@pytest.fixture
def crear_organismo():
osep, _ = Organismo.objects.get_or_create(
nombre='Osep',
descripcion='Obra Social de la Provincia de Catamarca',
domicilio='San Fernando del Valle de Catamarca',
telefono='0303456',
email='osep@catamarca.gob.ar'
)
juzgado_primer_instancia, _ = Organismo.objects.get_or_create(
nombre='Secretaria de Modernizacion',
descripcion='Secretaria de Modernizacion',
domicilio='San Fernando del Valle de Catamarca',
telefono='0303458',
email='modernizacion_del_estado@catamarca.gob.ar'
)
return osep, juzgado_primer_instancia
... ...
import pytest
from core.tests.utils import get
from organismo.tests.fixture_organismo import crear_organismo
from core.tests.fixtures import (
get_default_test_user,
create_user,
crear_grupo_administrador
)
@pytest.mark.django_db
def test_listado_de_organismo(get_default_test_user, crear_organismo):
endpoint = '/api/v1/organismo/'
usuario = get_default_test_user
grupo_admin = crear_grupo_administrador()
usuario.groups.add(grupo_admin)
response = get(endpoint, user_logged=usuario)
assert response.status_code == 200
meta = response.json()['meta']
assert meta['pagination']['count'] == 2
@pytest.mark.django_db
def test_detalle_de_organismo(get_default_test_user, crear_organismo):
osep, juzgado_primer_instancia = crear_organismo
endpoint = f'/api/v1/organismo/{osep.id}/'
usuario = get_default_test_user
grupo_admin = crear_grupo_administrador()
usuario.groups.add(grupo_admin)
response = get(endpoint, user_logged=usuario)
assert response.status_code == 200
data = response.json()['data']
assert data['id'] == str(osep.id)
assert data['attributes']['nombre'] == osep.nombre
assert data['attributes']['descripcion'] == osep.descripcion
assert data['attributes']['domicilio'] == osep.domicilio
assert data['attributes']['telefono'] == osep.telefono
... ...
from django.shortcuts import render
# Create your views here.
... ...
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext as _
from usuario.models import Usuario
@admin.register(Usuario)
class UsuarioAdmin(UserAdmin):
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'),
{
'fields': (
'first_name',
'last_name',
'email',
'documento_identidad'
)
}),
(_('Organismo'), {'fields': ('organismo',)}),
(_('Permissions'), {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'),
}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': (
'first_name', 'last_name', 'username', "password1", "password2", 'email', 'documento_identidad',
'organismo', 'is_staff',)}
),
)
search_fields = ('email', 'documento_identidad', 'username',)
autocomplete_fields = ('organismo',)
... ...
from django.contrib.auth.tokens import default_token_generator
from rest_framework import mixins, viewsets, status
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.parsers import JSONParser
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from usuario.models import Usuario
from usuario.serializers import UsuarioSerializer, CambiarClaveSecretaSerializer
class UsuarioViewSet(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
queryset = Usuario.objects.all()
permission_classes = (IsAuthenticated,)
serializer_class = UsuarioSerializer
def get_object(self, base_method=False):
user = self.request.user
if base_method:
user = super().get_object()
return user
@action(
methods=('patch',),
detail=False,
url_path='cambiar-clave-secreta',
parser_classes=(JSONParser,),
serializer_class=CambiarClaveSecretaSerializer
)
def cambiar_clave_secreta(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(status=status.HTTP_200_OK)
... ...
from django.apps import AppConfig
class UsuarioConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'usuario'
... ...
# Generated by Django 3.2.8 on 2023-06-08 11:10
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('organismo', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Usuario',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('documento_identidad', models.CharField(max_length=15, unique=True, verbose_name='Número de documento')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('organismo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='organismo.organismo')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'db_table': 'auth_user',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]
... ...
from django.contrib.auth.models import AbstractUser
from django.db import models
class Usuario(AbstractUser):
class Meta:
db_table = 'auth_user'
documento_identidad = models.CharField(max_length=15, verbose_name='Número de documento', unique=True)
organismo = models.ForeignKey('organismo.Organismo', on_delete=models.SET_NULL, blank=True, null=True)
def __str__(self):
return f'{self.username}'
def obtener_nombre_completo(self):
nombre_completo = f'{self.last_name}, {self.first_name}'
return nombre_completo.strip()
obtener_nombre_completo.short_description = 'Nombre Completo'
... ...
from django.contrib.auth import password_validation
from rest_framework_json_api import serializers
from rest_framework.serializers import Serializer as DRFSerializer
from usuario.models import Usuario
class UsuarioSerializer(serializers.ModelSerializer):
permisos_usuario = serializers.SerializerMethodField()
class Meta:
model = Usuario
fields = (
'first_name',
'last_name',
'email',
'documento_identidad',
'permisos_usuario',
)
@staticmethod
def get_permisos_usuario(instance):
return instance.get_all_permissions()
included_serializers = {
'organismo': 'organismo.serializers.OrganismoSerializer',
}
class CambiarClaveSecretaSerializer(DRFSerializer):
clave = serializers.CharField(max_length=128, write_only=True, required=True)
clave_nueva = serializers.CharField(max_length=128, write_only=True, required=True)
clave_nueva_2 = serializers.CharField(max_length=128, write_only=True, required=True)
def validate_clave(self, value):
user = self.context['request'].user
if not user.check_password(value):
raise serializers.ValidationError("La contraseña anterior no es válida. ¡Intentalo nuevamente!")
return value
def validate(self, data):
if data['clave_nueva'] != data['clave_nueva_2']:
raise serializers.ValidationError({'clave_nueva_2': "Los nuevos campos de contraseñas no coinciden"})
password_validation.validate_password(data['clave_nueva'], self.context['request'].user)
return data
def save(self, **kwargs):
clave = self.validated_data['clave_nueva']
usuario = self.context['request'].user
usuario.set_password(clave)
usuario.save()
return usuario
... ...
import pytest
from django.contrib.auth import get_user_model
from core.tests.fixtures import create_user, CONTENT_TYPE_JSON
from core.tests.utils import post, get, JSON_CONTENT_TYPE, patch
@pytest.mark.django_db
def test_usuario_cambio_password_satisfactoriamente():
usuario_autenticado = create_user(username='mbarrera')
usuario_autenticado.set_password('ultima_contraseña')
data = {
"clave": "ultima_contraseña",
"clave_nueva": "nueva_contraseña",
"clave_nueva_2": "nueva_contraseña"
}
endpoint = "/api/v1/usuario/cambiar-clave-secreta/"
response = patch(endpoint, data=data, content_type=CONTENT_TYPE_JSON, user_logged=usuario_autenticado)
assert response.status_code == 200
debianitram = get_user_model().objects.get(username='mbarrera')
assert debianitram.check_password('nueva_contraseña')
@pytest.mark.django_db
def test_usuario_cambio_password_falla_con_clave():
usuario_autenticado = create_user(username='mbarrera')
usuario_autenticado.set_password('ultima_contraseña')
data = {
"clave": "ultima.-.",
"clave_nueva": "nueva_contraseña",
"clave_nueva_2": "nueva_contraseña"
}
endpoint = "/api/v1/usuario/cambiar-clave-secreta/"
response = patch(endpoint, data=data, content_type=CONTENT_TYPE_JSON, user_logged=usuario_autenticado)
assert response.status_code == 400
errors = response.json()['errors']
assert errors[0]['detail'] == 'La contraseña anterior no es válida. ¡Intentalo nuevamente!'
@pytest.mark.django_db
def test_usuario_cambio_password_falla_no_coinciden_nuevas_password():
usuario_autenticado = create_user(username='mbarrera')
usuario_autenticado.set_password('ultima_contraseña')
data = {
"clave": "ultima_contraseña",
"clave_nueva": "NuevaContraseña",
"clave_nueva_2": "nueva_contraseña"
}
endpoint = "/api/v1/usuario/cambiar-clave-secreta/"
response = patch(endpoint, data=data, content_type=CONTENT_TYPE_JSON, user_logged=usuario_autenticado)
assert response.status_code == 400
errors = response.json()['errors']
assert errors[0]['detail'] == 'Los nuevos campos de contraseñas no coinciden'
... ...
import requests
from django.conf import settings
from oauth2_provider.views import TokenView
class CustomTokenView(TokenView):
def post(self, request, *args, **kwargs):
# realizar validaciones de recaptcha
if 'captcha_value' not in request.POST:
raise AttributeError(
"El campo Valor de Captcha es obligatorio"
)
r = requests.post(
'https://www.google.com/recaptcha/api/siteverify',
data={
'secret': settings.SECRET_KEY_CAPTCHA,
'response': request.POST['captcha_value'],
}
)
respuesta = r.json()
if not respuesta["success"]:
raise AttributeError(
"Captcha inválido: " + respuesta['error-codes'][0]
)
return super().post(request, *args, **kwargs)
... ...
from rest_framework import routers
from organismo import api as organismo_api
from usuario import api as usuario_api
# Define routes
router = routers.DefaultRouter()
router.register(prefix='usuario', viewset=usuario_api.UsuarioViewSet)
router.register(prefix='organismo', viewset=organismo_api.OrganismoViewSet)
\ No newline at end of file
... ...
import sys
import os
import environ
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
ROOT_DIR = environ.Path(__file__) - 3
PROJECT_DIR = ROOT_DIR.path('project')
... ... @@ -33,7 +33,6 @@ DATABASES = {
'default': env.db('DATABASE_URL')
}
DJANGO_APPS = (
'django.contrib.admin',
'django.contrib.auth',
... ... @@ -47,10 +46,14 @@ THIRD_PARTY_APPS = (
'rest_framework',
'django_filters',
'corsheaders',
'oauth2_provider',
'requests',
)
PROJECT_APPS = (
'core',
'organismo',
'usuario',
)
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS
... ... @@ -64,11 +67,15 @@ MIDDLEWARE = (
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware'
)
ROOT_URLCONF = 'project.urls'
# Usuario Personalizado
AUTH_USER_MODEL = 'usuario.Usuario'
# Python dotted path to the WSGI application used by Django's runserver.
WSGI_APPLICATION = 'project.wsgi.application'
... ... @@ -131,18 +138,37 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
REST_FRAMEWORK = {
'PAGE_SIZE': 50,
'EXCEPTION_HANDLER': 'rest_framework_json_api.exceptions.exception_handler',
'DEFAULT_PAGINATION_CLASS': 'core.paginations.LargePagination',
'DEFAULT_PARSER_CLASSES': (
'rest_framework_json_api.parsers.JSONParser',
'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser'
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_RENDERER_CLASSES': ('rest_framework_json_api.renderers.JSONRenderer',),
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
'NON_FIELD_ERRORS_KEY': 'error_messages'
}
ACTIVAR_HERRAMIENTAS_DEBUGGING = env.bool('ACTIVAR_HERRAMIENTAS_DEBUGGING', default=False)
if ACTIVAR_HERRAMIENTAS_DEBUGGING:
INTERNAL_IPS = ['127.0.0.1']
INSTALLED_APPS += ('debug_toolbar', 'django_extensions')
MIDDLEWARE = ('debug_toolbar.middleware.DebugToolbarMiddleware',) + MIDDLEWARE
REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] += ('rest_framework.renderers.BrowsableAPIRenderer',)
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] += ('rest_framework.authentication.SessionAuthentication',)
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
)
# Secret Key para Captcha.
SECRET_KEY_CAPTCHA = '6LczX70hAAAAANBGK03-2l48lmn7QAftwQkR7vUI'
ACTIVAR_HERRAMIENTAS_DEBBUGING = env.bool('ACTIVAR_HERRAMIENTAS_DEBBUGING', default=False)
if ACTIVAR_HERRAMIENTAS_DEBBUGING:
INSTALLED_APPS += (
'debug_toolbar',
'django_extensions',
)
MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
... ...
... ... @@ -16,6 +16,9 @@ Including another URLconf
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from oauth2_provider.urls import base_urlpatterns
from usuario.views import CustomTokenView
from .router import router
... ... @@ -25,5 +28,16 @@ admin.site.site_title = getattr(settings, 'PROJECT_NAME_TITLE')
urlpatterns = [
path('admin/', admin.site.urls),
path('oauth2/token/', CustomTokenView.as_view(), name="token"),
path('oauth2/', include((base_urlpatterns, 'oauth2_provider'), namespace='oauth2_provider')),
path('api/v1/', include(router.urls)),
]
path('recaptcha/', include('core.urls', namespace='core')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.ACTIVAR_HERRAMIENTAS_DEBUGGING:
import debug_toolbar
urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)),
path('api-auth/', include('rest_framework.urls')),
] + urlpatterns
... ...
[pytest]
DJANGO_SETTINGS_MODULE=PROJECTproject-NAME.settings.testing
DJANGO_SETTINGS_MODULE=project.settings.testing
norecursedirs = requirements deployment
testpaths = tests
addopts = --capture=fd --nomigrations
... ...
# Requeriments base.
Django==3.2.8
Django==4.1.9
django-cors-headers==3.10.0
django-filter==21.1
djangorestframework==3.14.0
djangorestframework-jsonapi==6.0.0
django-environ==0.10.0
django-crispy-forms==2.0
django-oauth-toolkit==2.3.0
Pillow==9.5.0
# Requests and SSL.
requests_toolbelt==0.9.1
requests==2.27.1
pyOpenSSL==22.0.0
# database
psycopg2==2.9.6
psycopg2-binary==2.7.4
psycopg2==2.9.1
psycopg2-binary==2.9.1
... ...