Skip to content

Commit

Permalink
Merge pull request #247 from maxgraziano/3.1-model-refactor
Browse files Browse the repository at this point in the history
Fixes Issues #233 and #245.
  • Loading branch information
domdelorenzo authored May 16, 2024
2 parents 4ed9d02 + 32a667d commit 02de90e
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 22 deletions.
5 changes: 4 additions & 1 deletion web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Three Django models are used for this process.
| Delete Existing Coop | ![](./img/DeleteOperation_Proposal.png) | ![](./img/DeleteOperation_ReviewRejected.png) | ![](./img/DeleteOperation_ReviewApproved.png) |

## Testing
Tests are stored in the [directory/tests](./directory/tests/) directory using Python's `unittest` framework with additional add-ins from Django and Django Rest Framework. Tests can be run via Django's [manage.py](./manage.py) script.
Tests are stored in the [directory/tests](./directory/tests/) directory using Python's `unittest` framework with additional add-ins from Django and Django Rest Framework. Tests can be run via Django's [manage.py](./manage.py) script. If using Development Mode via Docker, you can access manage.py via `docker exec -it web-dev /bin/bash`. If using Production Mode via Docker, you can access manage.py via `docker exec -it web-prod /bin/bash`.

Run all tests:
```
Expand Down Expand Up @@ -121,4 +121,7 @@ python manage.py test directory.tests.test_coop_list.TestCoopList.test_list_all
- [x] Filter out CoopCSV
- [x] Filter out CoopDetail
- [x] Remove is_filter param from cooplist
- [x] Clean up loose TODOs
- [ ] What should Coop.scope do?
- [ ] What should Coop.tags do?
- [ ] HTTPS Certificates
5 changes: 3 additions & 2 deletions web/directory/admin.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from django.contrib import admin
from .models import Coop, CoopType, CoopAddressTags, ContactMethod, Address, Person
from .models import Coop, CoopType, CoopAddressTags, ContactMethod, Address, Person, UserProfile

admin.site.register(Coop)
admin.site.register(CoopType)
admin.site.register(CoopAddressTags)
admin.site.register(ContactMethod)
admin.site.register(Address)
admin.site.register(Person)
admin.site.register(Person)
admin.site.register(UserProfile)
26 changes: 26 additions & 0 deletions web/directory/migrations/0036_userprofile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 3.2.24 on 2024-05-01 20:32

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import phonenumber_field.modelfields


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('directory', '0035_alter_cooptype_name'),
]

operations = [
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('phone', phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None)),
('github_username', models.CharField(blank=True, max_length=165, null=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
6 changes: 6 additions & 0 deletions web/directory/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
from phonenumber_field.modelfields import PhoneNumberField
from django.db.models import Prefetch
from django.utils.timezone import now
from django.conf import settings

class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
phone = PhoneNumberField(blank=True)
github_username = models.CharField(max_length=165, blank=True)

class ContactMethod(models.Model):
class ContactTypes(models.TextChoices):
Expand Down
5 changes: 5 additions & 0 deletions web/directory/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from rest_framework import permissions

class IsOwnerOrAdmin(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return bool(request.user and request.user.is_authenticated and (obj.user == request.user or request.user.is_staff))
51 changes: 49 additions & 2 deletions web/directory/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.contrib.auth import get_user_model
from django.db import transaction
from rest_framework import serializers, exceptions
from directory.models import CoopType, ContactMethod, CoopAddressTags, Address, CoopProposal, CoopPublic, Coop, Person
from directory.models import CoopType, ContactMethod, CoopAddressTags, Address, CoopProposal, CoopPublic, Coop, Person, UserProfile
from django.utils.timezone import now
from directory.services.location_service import LocationService
import json
Expand Down Expand Up @@ -80,15 +80,62 @@ class Meta:
extra_kwargs = {'password': {'write_only': True}}

def create(self, validated_data):
password = validated_data.pop('password')
user = User.objects.create_user(
username=validated_data['username'],
email=validated_data['email'],
password=validated_data['password'],
first_name=validated_data.get('first_name', ''),
last_name=validated_data.get('last_name', ''),
)
if password:
user.set_password(password)

return user

class UserProfileSerializer(serializers.ModelSerializer):
username = serializers.CharField(source='user.username')
email = serializers.EmailField(source='user.email')
first_name = serializers.CharField(source='user.first_name')
last_name = serializers.CharField(source='user.last_name')
phone = serializers.CharField(allow_blank=True, required=False)
github_username = serializers.CharField(allow_blank=True, required=False, max_length=165)
password = serializers.CharField(source='user.password', write_only=True)

class Meta:
model = UserProfile
fields = ['username', 'email', 'first_name', 'last_name', 'phone', 'github_username', 'password']

def create(self, validated_data):
user_data = validated_data.pop('user')
password = user_data.pop('password')
user = User.objects.create(
username=user_data['username'],
email=user_data['email'],
first_name=user_data['first_name'],
last_name=user_data['last_name']
)
user.set_password(password)
user.save()
user_profile = UserProfile.objects.create(user=user, **validated_data)
return user_profile

def update(self, instance, validated_data):
user_data = validated_data.pop('user', {})
user = instance.user
for key, value in user_data.items():
if key == 'password':
user.set_password(value)
else:
setattr(user, key, value)
user.save()

for key, value in validated_data.items():
setattr(instance, key, value)
instance.save()

return instance


#============================================================================

class CoopPublicListSerializer(serializers.ModelSerializer):
Expand Down
8 changes: 4 additions & 4 deletions web/directory/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
path('api/v1/admin/', admin.site.urls),

path('api/v1/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/v1/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('api/v1/token/refresh/', TokenRefreshView.as_view(), name='token-refresh'),

path('api/v1/users/', views.UserList.as_view(), name='user-list'),
path('api/v1/users/<int:pk>/', views.UserDetail.as_view(), name='user-detail'),
path('api/v1/register/', views.CreateUserView.as_view(), name='register'),
path('api/v1/password_reset/', views.PasswordResetRequestView.as_view(), name='password-reset-request'),
path('api/v1/register/', views.UserRegister.as_view(), name='register'),

path('api/v1/password-reset/', views.PasswordResetRequestView.as_view(), name='password-reset-request'),
path('api/v1/password-reset-confirm/<str:uidb64>/<str:token>/', views.PasswordResetConfirmView.as_view(), name='password-reset-confirm'),

path('api/v1/coops/', views.CoopList.as_view(), name='coop-list'),
Expand Down
43 changes: 30 additions & 13 deletions web/directory/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.views import APIView
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.tokens import RefreshToken
from drf_spectacular.utils import extend_schema_view, extend_schema, OpenApiParameter
from drf_spectacular.utils import extend_schema_view, extend_schema, OpenApiParameter, inline_serializer

from directory.models import ContactMethod, CoopType, Address, AddressCache, CoopAddressTags, CoopPublic, Coop, CoopProposal, Person
from directory.serializers import *
from directory.renderers import CSVRenderer
from directory.permissions import IsOwnerOrAdmin

@extend_schema_view(
get=extend_schema(
Expand Down Expand Up @@ -66,20 +68,19 @@ def get(self, request, *args, **kwargs):
})

return Response(data)

class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [IsAdminUser]

class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [IsAdminUser]
class UserDetail(generics.RetrieveUpdateAPIView):
queryset = UserProfile.objects.all()
serializer_class = UserProfileSerializer
permission_classes = [IsOwnerOrAdmin]

class CreateUserView(APIView):
@extend_schema(
request=UserProfileSerializer,
responses=TokenObtainPairSerializer
)
class UserRegister(APIView):
def post(self, request):
serializer = UserSerializer(data=request.data)
serializer = UserProfileSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
if user:
Expand Down Expand Up @@ -224,8 +225,24 @@ def get(self, request, *args, **kwargs):
return Response(states_data[input_country_code], status.HTTP_200_OK)
else:
return Response("Country not found.", status.HTTP_404_NOT_FOUND)

class PasswordResetRequestView(APIView):
@extend_schema(
request=inline_serializer(
name='PasswordResetRequestSerializer',
fields={
'email': serializers.EmailField()
}
),
responses={
status.HTTP_200_OK: inline_serializer(
name='PasswordResetResponseSerializer',
fields={
'message': serializers.CharField()
}
)
}
)
def post(self, request):
email = request.data.get("email")
user = User.objects.filter(email=email).first()
Expand Down

0 comments on commit 02de90e

Please sign in to comment.