Hi. I created a django app with
settings.py
"""
Django settings for restapi project.
Generated by 'django-admin startproject' using Django 5.2.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-b0jt)bxzsmm-#8a7zbpr0=3xe!kh4vqu80(gnn22-=#h9x*=)#'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["*"]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'api.apps.ApiConfig',
'drf_spectacular',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'restapi.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'restapi.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
],
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
views.py
from django.contrib.auth.models import Group, User
from rest_framework import permissions, viewsets
from rest_framework.exceptions import PermissionDenied
from .serializers import RoleSerializer, UserSerializer, ContestSerializer, PhotoSerializer, VoteSerializer
from api.models import Contest, Photo, Vote
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all().order_by('-date_joined')
serializer_class = UserSerializer
permission_classes = [permissions.IsAdminUser]
# Create a new user and set the password if provided.
def perform_create(self, serializer):
user = serializer.save()
user.set_password(self.request.data['password'])
user.save()
class RoleViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows roles to be viewed or edited.
"""
queryset = Group.objects.all().order_by('name')
serializer_class = RoleSerializer
permission_classes = [permissions.IsAdminUser]
class ContestViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows contests to be viewed or edited.
"""
queryset = Contest.objects.all().order_by('name')
serializer_class = ContestSerializer
permission_classes = [permissions.IsAdminUser]
class PhotoViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows photos to be viewed or edited.
"""
queryset = Photo.objects.all().order_by('upload_date')
serializer_class = PhotoSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
def perform_update(self, serializer, *args, **kwargs):
UPDATE_ERROR_MSG = "You cannot update this photo because you are not the owner."
if self.request.user != serializer.owner:
raise PermissionDenied(UPDATE_ERROR_MSG)
return super().perform_update(serializer)
def perform_destroy(self, serializer, *args, **kwargs):
DELETE_ERROR_MSG = "You cannot delete this photo because you are not the owner."
if self.request.user != serializer.owner:
raise PermissionDenied(DELETE_ERROR_MSG)
return super().perform_destroy(serializer)
class VoteViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows votes to be viewed or edited.
"""
queryset = Vote.objects.all().order_by('timestamp')
serializer_class = VoteSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def perform_create(self, serializer):
serializer.save(user=self.request.user)
def perform_update(self, serializer, *args, **kwargs):
UPDATE_ERROR_MSG = "You cannot update this vote because you are not the owner."
if self.request.user != serializer:
raise PermissionDenied(UPDATE_ERROR_MSG)
return super().perform_update(serializer)
def perform_destroy(self, serializer, *args, **kwargs):
DELETE_ERROR_MSG = "You cannot delete this vote because you are not the owner."
if self.request.user != serializer:
raise PermissionDenied(DELETE_ERROR_MSG)
return super().perform_destroy(serializer)
And testing with a client, autogenerated with the schema with the tool https://openapi-generator.tech/ , I got 403 when request get to /votes/ or /photos/.
[1399] NetworkUtility.shouldRetryException: Unexpected response code 403 for http://10.0.2.2:8000/votes/
[1399] NetworkUtility.shouldRetryException: Unexpected response code 403 for http://10.0.2.2:8000/votes/
ApiException{code=403, message=null}
at client.api.VotesApi.votesList(VotesApi.java:361)
at .LaunchActivity.lambda$test$3$es-alejandromarmol-rallyfotografico-LaunchActivity(LaunchActivity.java:53)
at .LaunchActivity$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)
at java.lang.Thread.run(Thread.java:923)
Which I have the log:
Forbidden: /votes/
[22/Apr/2025 13:00:19] "GET /votes/ HTTP/1.1" 403 39
Forbidden: /votes/
[22/Apr/2025 13:00:19] "GET /votes/ HTTP/1.1" 403 39
Basically I need to know why, it’s supposed to allow me to request get anonymously, and my API client should be well autogenerated. Please check if CORS or anything is required, but other error would be raised, right?