diff --git a/.gitignore b/.gitignore index 5d381cc..2b47c14 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ __pycache__/ *.py[cod] *$py.class +.DS_Store +*/.DS_Store +**/*/.DS* + # C extensions *.so diff --git a/padelclub_backend/settings.py b/padelclub_backend/settings.py index 6a82666..fe90e45 100644 --- a/padelclub_backend/settings.py +++ b/padelclub_backend/settings.py @@ -32,16 +32,19 @@ ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [ - "tournaments.apps.TournamentsConfig", + 'tournaments', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'rest_framework' + 'rest_framework', + 'rest_framework.authtoken', ] +AUTH_USER_MODEL = "tournaments.CustomUser" + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -136,3 +139,16 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static/') # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Rest Framework configuration +REST_FRAMEWORK = { + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + # 'DEFAULT_PERMISSION_CLASSES': [ + # 'rest_framework.permissions.IsAuthenticated', + # ], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.TokenAuthentication', + ] +} diff --git a/padelclub_backend/urls.py b/padelclub_backend/urls.py index 40966f3..4f49ac6 100644 --- a/padelclub_backend/urls.py +++ b/padelclub_backend/urls.py @@ -1,4 +1,4 @@ -"""padelclub_backend URL Configuration +"""storage_poc URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/4.1/topics/http/urls/ @@ -14,17 +14,22 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path, include -from tournaments import views +from django.urls import include, path from rest_framework import routers +from tournaments import views +from rest_framework.authtoken.views import obtain_auth_token router = routers.DefaultRouter() router.register(r'users', views.UserViewSet) router.register(r'clubs', views.ClubViewSet) - +router.register(r'tournaments', views.TournamentViewSet) urlpatterns = [ path('api/', include(router.urls)), path("tournaments/", include("tournaments.urls")), path('admin/', admin.site.urls), + path('api-auth/', include('rest_framework.urls')), + path('api/plus/api-token-auth/', obtain_auth_token, name='api_token_auth'), + path("api/plus/user-by-token/", views.user_by_token, name="user_by_token"), + path("api/plus/change-password/", views.ChangePasswordView.as_view(), name="change_password"), ] diff --git a/tournaments/admin.py b/tournaments/admin.py index 1432b05..c2a610f 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -1,6 +1,29 @@ from django.contrib import admin +from .models import Club, Tournament +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.forms import UserCreationForm, UserChangeForm -# Register your models here. -from .models import Club +from .forms import CustomUserCreationForm, CustomUserChangeForm +from .models import CustomUser +class CustomUserAdmin(UserAdmin): + form = CustomUserChangeForm + add_form = CustomUserCreationForm + model = CustomUser + list_display = ["email", "username", "umpire_code"] + fieldsets = [ + (None, {"fields": ["username", "email", "password", "club", "umpire_code"]}), + ] + add_fieldsets = [ + ( + None, + { + "classes": ["wide"], + "fields": ["username", "email", "password1", "password2", "club", "umpire_code"], + }, + ), + ] + +admin.site.register(CustomUser, CustomUserAdmin) admin.site.register(Club) +admin.site.register(Tournament) diff --git a/tournaments/forms.py b/tournaments/forms.py new file mode 100644 index 0000000..f9900bf --- /dev/null +++ b/tournaments/forms.py @@ -0,0 +1,14 @@ +from django.contrib.auth.forms import UserCreationForm, UserChangeForm +from .models import CustomUser + +class CustomUserCreationForm(UserCreationForm): + + class Meta: + model = CustomUser + fields = UserCreationForm.Meta.fields + ("umpire_code", ) + +class CustomUserChangeForm(UserChangeForm): + + class Meta: + model = CustomUser + fields = UserCreationForm.Meta.fields + ("umpire_code", ) diff --git a/tournaments/migrations/0001_initial.py b/tournaments/migrations/0001_initial.py index 56f51f3..be7256a 100644 --- a/tournaments/migrations/0001_initial.py +++ b/tournaments/migrations/0001_initial.py @@ -1,6 +1,11 @@ -# Generated by Django 4.1.1 on 2024-01-19 10:42 +# Generated by Django 4.1.1 on 2024-02-23 09:08 +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 +import uuid class Migration(migrations.Migration): @@ -8,14 +13,52 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ migrations.CreateModel( name='Club', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('name', models.CharField(max_length=200)), + ('address', models.CharField(blank=True, max_length=200, null=True)), + ], + ), + migrations.CreateModel( + name='Tournament', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200)), + ('club', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tournaments.club')), + ], + ), + migrations.CreateModel( + name='CustomUser', + fields=[ + ('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')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('umpire_code', models.CharField(blank=True, max_length=200, null=True)), + ('club', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='tournaments.club')), + ('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')), + ('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={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), ], ), ] diff --git a/tournaments/models.py b/tournaments/models.py index 3a29b18..a70cf63 100644 --- a/tournaments/models.py +++ b/tournaments/models.py @@ -1,6 +1,27 @@ from django.db import models +from django.contrib.auth.models import AbstractUser +import uuid class Club(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) + name = models.CharField(max_length=200) + address = models.CharField(max_length=200, null=True, blank=True) + + def __str__(self): + return self.name + +class CustomUser(AbstractUser): + pass + id = models.UUIDField(primary_key=True, default=uuid.uuid4) + umpire_code = models.CharField(max_length=200, blank=True, null=True) + club = models.ForeignKey(Club, null=True, on_delete=models.SET_NULL) + + def __str__(self): + return self.username + +class Tournament(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + club = models.ForeignKey(Club, on_delete=models.CASCADE) name = models.CharField(max_length=200) def __str__(self): diff --git a/tournaments/serializers.py b/tournaments/serializers.py index 2a730db..c19e6ca 100644 --- a/tournaments/serializers.py +++ b/tournaments/serializers.py @@ -1,13 +1,57 @@ -from django.contrib.auth.models import User from rest_framework import serializers -from .models import Club +from .models import Club, Tournament, CustomUser +from django.contrib.auth import password_validation +from django.utils.translation import gettext_lazy as _ class UserSerializer(serializers.HyperlinkedModelSerializer): + + password = serializers.CharField(write_only=True) + + def create(self, validated_data): + user = CustomUser.objects.create_user( + username=validated_data['username'], + password=validated_data['password'], + ) + return user + class Meta: - model = User - fields = ['url', 'username', 'email'] + club_id = serializers.PrimaryKeyRelatedField(queryset=Club.objects.all()) + model = CustomUser + fields = ['id', 'username', 'password', 'club_id', 'umpire_code'] class ClubSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Club - fields = ['id', 'name'] + fields = ['id', 'name', 'address'] + +class TournamentSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + club_id = serializers.PrimaryKeyRelatedField(queryset=Club.objects.all()) + model = Tournament + fields = ['id', 'name', 'club_id'] + +class ChangePasswordSerializer(serializers.Serializer): + old_password = serializers.CharField(max_length=128, write_only=True, required=True) + new_password1 = serializers.CharField(max_length=128, write_only=True, required=True) + new_password2 = serializers.CharField(max_length=128, write_only=True, required=True) + + def validate_old_password(self, value): + user = self.context['request'].user + if not user.check_password(value): + raise serializers.ValidationError( + _('Your old password was entered incorrectly. Please enter it again.') + ) + return value + + def validate(self, data): + if data['new_password1'] != data['new_password2']: + raise serializers.ValidationError({'new_password2': _("The two password fields didn't match.")}) + password_validation.validate_password(data['new_password1'], self.context['request'].user) + return data + + def save(self, **kwargs): + password = self.validated_data['new_password1'] + user = self.context['request'].user + user.set_password(password) + user.save() + return user diff --git a/tournaments/views.py b/tournaments/views.py index dda867f..f054c67 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -1,26 +1,48 @@ from django.shortcuts import render from django.http import HttpResponse +from .serializers import ClubSerializer, TournamentSerializer, UserSerializer, ChangePasswordSerializer +from .models import Club, Tournament, CustomUser from rest_framework import viewsets, permissions -from django.contrib.auth.models import User -from .serializers import UserSerializer, ClubSerializer -from .models import Club +from rest_framework.authtoken.models import Token +from rest_framework.response import Response +from rest_framework.decorators import api_view +from rest_framework import status +from rest_framework.generics import UpdateAPIView def index(request): - return HttpResponse("Hello, world. You're at the tournaments index.") + return HttpResponse("Hello, you're at the top of the world.") +@api_view(['GET']) +def user_by_token(request): + # return Response({"message": "Hello for today! See you tomorrow!"}) + # key = request.data['token'] + # token = Token.objects.get(key=key) + # user = CustomUser.objects.get(username=token.user) + serializer = UserSerializer(request.user) + return Response(serializer.data, status=status.HTTP_200_OK) class UserViewSet(viewsets.ModelViewSet): - """ - API endpoint that allows users to be viewed or edited. - """ - queryset = User.objects.all().order_by('-date_joined') + queryset = CustomUser.objects.all() serializer_class = UserSerializer - permission_classes = [permissions.IsAuthenticated] class ClubViewSet(viewsets.ModelViewSet): - """ - API endpoint that allows clubs to be viewed or edited. - """ - queryset = Club.objects.all().order_by('id') + queryset = Club.objects.all() serializer_class = ClubSerializer - permission_classes = [permissions.IsAuthenticated] + +class TournamentViewSet(viewsets.ModelViewSet): + queryset = Tournament.objects.all() + serializer_class = TournamentSerializer + +class ChangePasswordView(UpdateAPIView): + serializer_class = ChangePasswordSerializer + + def update(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.save() + # if using drf authtoken, create a new token + if hasattr(user, 'auth_token'): + user.auth_token.delete() + token, created = Token.objects.get_or_create(user=user) + # return new token + return Response({'token': token.key}, status=status.HTTP_200_OK)