Skip to content
This repository has been archived by the owner on Jul 29, 2023. It is now read-only.

Commit

Permalink
Merge pull request #33 from davidcr01/2.0
Browse files Browse the repository at this point in the history
Release v2.1.0
  • Loading branch information
davidcr01 authored Jul 3, 2023
2 parents 2dd6fe3 + 51bf9ba commit e3cd592
Show file tree
Hide file tree
Showing 64 changed files with 1,406 additions and 103 deletions.
Binary file added Wordle+/django/avatars/avatars/guts-icon.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions Wordle+/django/djangoproject/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'djapi',
'corsheaders',
'djapi'
]

REST_FRAMEWORK = {
Expand Down Expand Up @@ -148,9 +148,13 @@
AUTH_USER_MODEL = 'djapi.CustomUser'
TOKEN_EXPIRED_AFTER_SECONDS = 3600

MEDIA_URL = '/avatars/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'avatars')

## Conection with the FrontEnd

CORS_ALLOWED_ORIGINS = [
'http://localhost:8100', # Ionic in local (dev)
'http://localhost:8080', # Ionic in Docker
]
]
CORS_ALLOW_REDIRECTS = False
11 changes: 7 additions & 4 deletions Wordle+/django/djangoproject/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@
from django.contrib import admin
from rest_framework import routers
from djapi import views
from djapi.views import CustomObtainAuthToken, CheckTokenExpirationView
from rest_framework.authtoken.views import ObtainAuthToken
from djapi.views import CustomObtainAuthToken, CheckTokenExpirationView, AvatarView, UserInfoAPIView
from django.conf import settings
from django.conf.urls.static import static

router = routers.DefaultRouter()
router.register(r'api/users', views.CustomUserViewSet)
router.register(r'api/players', views.PlayerViewSet)
router.register(r'api/groups', views.GroupViewSet)
router.register(r'api/classicwordles', views.ClassicWordleViewSet)
router.register(r'api/notifications', views.NotificationsViewSet)

# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
Expand All @@ -34,5 +36,6 @@
path('admin/', admin.site.urls),
path('api-token-auth/', CustomObtainAuthToken.as_view()),
path('check-token-expiration/', CheckTokenExpirationView.as_view(), name='check-token-expiration'),

]
path('api/avatar/<int:user_id>/', AvatarView.as_view(), name='avatar'),
path('api/users-info/', UserInfoAPIView.as_view(), name='user-detail'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
6 changes: 5 additions & 1 deletion Wordle+/django/djapi/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.contrib.auth.models import Permission, Group
from django.contrib.contenttypes.models import ContentType

from .models import CustomUser, Player, StaffCode, ClassicWordle
from .models import CustomUser, Player, StaffCode, ClassicWordle, Notifications

class CustomUserAdmin(UserAdmin):
model = CustomUser
Expand Down Expand Up @@ -84,6 +84,10 @@ def has_add_permission(self, request):
class ClassicWordleAdmin(admin.ModelAdmin):
list_display = ['id', 'player', 'word', 'date_played']

class NotificationsAdmin(admin.ModelAdmin):
list_display = ('id', 'player', 'text', 'link', 'timestamp')

admin.site.register(Notifications, NotificationsAdmin)
admin.site.register(ClassicWordle, ClassicWordleAdmin)
admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(Player, PlayerAdmin)
Expand Down
7 changes: 7 additions & 0 deletions Wordle+/django/djapi/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ class ClassicWordle(models.Model):
date_played = models.DateTimeField(default=timezone.now)
win = models.BooleanField(default=False)

class Notifications(models.Model):
player = models.ForeignKey(Player, on_delete=models.CASCADE, related_name='notifications')
text = models.CharField(max_length=200)
link = models.URLField(blank=True)
timestamp = models.DateTimeField(auto_now_add=True)

# Method to add the 'Staff' group automatically when creating an administrator
@receiver(post_save, sender=CustomUser)
def assign_permissions(sender, instance, created, **kwargs):
if created and instance.is_staff:
Expand Down
10 changes: 5 additions & 5 deletions Wordle+/django/djapi/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ def has_permission(self, request, view):
"""
class IsOwnerPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):

# Allows if the owner of the object is the user
if hasattr(obj, 'user'):
return obj.user == request.user

return obj.player.user == request.user

def has_permission(self, request, view):
print(request)
return request.user and request.user.is_authenticated
"""
Permission that allows access if the user is the owner of the account or is an admin.
"""
Expand Down
17 changes: 15 additions & 2 deletions Wordle+/django/djapi/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.contrib.auth.models import Group
from .models import CustomUser, Player, StaffCode, ClassicWordle
from .models import CustomUser, Player, StaffCode, ClassicWordle, Notifications
from rest_framework import serializers
from django.contrib.auth.hashers import make_password

Expand Down Expand Up @@ -74,6 +74,14 @@ class Meta:
model = CustomUser
fields = ('id', 'username', 'last_login', 'date_joined')


# Used to update and get the user information partially
class UserInfoPartialSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = ['email', 'first_name', 'last_name']


# Serializer related to the Player model. It considers all the fields, but naming
# them specifically (other way)
class PlayerInfoSerializer(serializers.ModelSerializer):
Expand All @@ -96,7 +104,7 @@ def get_user(self, obj):
class ClassicWordleSerializer(serializers.ModelSerializer):
class Meta:
model = ClassicWordle
fields = ['player', 'word', 'time_consumed', 'attempts', 'xp_gained', 'date_played', 'win']
fields = ['word', 'time_consumed', 'attempts', 'xp_gained', 'date_played', 'win']

def create(self, validated_data):
player = validated_data['player']
Expand All @@ -111,6 +119,11 @@ def create(self, validated_data):

return ClassicWordle.objects.create(**validated_data)

class NotificationsSerializer(serializers.ModelSerializer):
class Meta:
model = Notifications
fields = ['text', 'link']

class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Group
Expand Down
115 changes: 105 additions & 10 deletions Wordle+/django/djapi/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import base64
from django.contrib.auth.models import Group
from .models import CustomUser, Player, ClassicWordle
from rest_framework import viewsets, permissions, status
from rest_framework.response import Response
from djapi.serializers import *
from djapi.permissions import *
from djapi.permissions import IsOwnerOrAdminPermission, IsOwnerPermission
from rest_framework.permissions import IsAdminUser
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
Expand All @@ -30,15 +31,37 @@ def get_permissions(self):
return []
elif self.action in ['list',]:
# List available only for the Event Managers.
return [IsAdminUser()]
return [IsOwnerOrAdminPermission()]
elif self.action in ['update', 'partial_update', 'destroy']:
# Edition and destruction available only for the Event Managers. Needed for the Event Managers
# to edit the personal info of the players.
return [IsOwnerOrAdminPermission()]
else:
# Authentication is needed for the rest of the operations.
return [permissions.IsAuthenticated()]

def get_queryset(self):
queryset = CustomUser.objects.all()
if self.action == 'retrieve':
# Return only the authenticated user's data if the action is 'retrieve'
queryset = queryset.filter(id=self.request.user.id)
return queryset

class UserInfoAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]

def get(self, request):
user = request.user
serializer = UserInfoPartialSerializer(user)
return Response(serializer.data)

def patch(self, request):
user = request.user
serializer = UserInfoPartialSerializer(user, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=400)

class PlayerViewSet(viewsets.ModelViewSet):
"""
Expand Down Expand Up @@ -112,7 +135,12 @@ def post(self, request, *args, **kwargs):
response_data = {
'token': token.key,
'user_id': user.id,
'player_id': user.player.id if hasattr(user, 'player') else None # Include the player ID if it exists
'username': user.username,
'player_id': user.player.id if hasattr(user, 'player') else None, # Include the player ID if it exists
'wins': user.player.wins if hasattr(user, 'player') else None, # Include wins if player exists
'wins_pvp': user.player.wins_pvp if hasattr(user, 'player') else None, # Include wins_pvp if player exists
'wins_tournament': user.player.wins_tournament if hasattr(user, 'player') else None, # Include wins_tournament if player exists
'xp': user.player.xp if hasattr(user, 'player') else None # Include xp if player exists
}

return Response(response_data, status=status.HTTP_200_OK)
Expand All @@ -137,19 +165,86 @@ class ClassicWordleViewSet(viewsets.GenericViewSet):
serializer_class = ClassicWordleSerializer

def list(self, request):
player_id = request.query_params.get('player_id')
if not player_id:
return Response({'error': 'player_id parameter is required'}, status=400)

player = get_object_or_404(Player, id=player_id)
player = getattr(request.user, 'player', None)
if not player:
return Response({'error': 'Player not found'}, status=404)

queryset = ClassicWordle.objects.filter(player=player).order_by('-date_played')
serializer = ClassicWordleSerializer(queryset, many=True)
return Response(serializer.data)

def create(self, request):
serializer = ClassicWordleSerializer(data=request.data)
player = getattr(request.user, 'player', None)

if not player:
return Response({'error': 'Player not found'}, status=404)

serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(player=request.user.player)
serializer.save(player=player)
return Response(serializer.data, status=201)



class AvatarView(APIView):
permission_classes = [permissions.IsAuthenticated]

def get(self, request, user_id):
try:
user = get_object_or_404(CustomUser, id=user_id)
if request.user == user:
if user.avatar:
with open(user.avatar.path, 'rb') as f:
image_data = f.read()
base64_image = base64.b64encode(image_data).decode('utf-8')
return Response({'avatar': base64_image}, status=200)
else:
return Response({'detail': 'Avatar not available.'}, status=404)
else:
return Response({'detail': 'You do not have permission to get the avatar.'}, status=403)
except CustomUser.DoesNotExist:
return Response({'detail': 'The specified user does not exist.'}, status=404)

def post(self, request, user_id):
try:
user = get_object_or_404(CustomUser, id=user_id)
if request.user == user:
avatar = request.FILES.get('avatar')
if avatar:
user.avatar = avatar
user.save()
return Response({'detail': 'Avatar uploaded correctly.'}, status=200)
else:
return Response({'detail': 'No avatar image attached.'}, status=400)
else:
return Response({'detail': 'You do not have permission to upload an avatar.'}, status=403)
except CustomUser.DoesNotExist:
return Response({'detail': 'The specified user does not exist.'}, status=404)

class NotificationsViewSet(viewsets.ModelViewSet):
queryset = Notifications.objects.all()
serializer_class = NotificationsSerializer
permission_classes = [permissions.IsAuthenticated, IsOwnerPermission]

def list(self, request):
limit = int(request.query_params.get('limit', 10))
player = getattr(request.user, 'player', None)

if not player:
return Response({'error': 'Player not found'}, status=404)

notifications = self.queryset.filter(player=player).order_by('-timestamp')[:limit]
serializer = self.serializer_class(notifications, many=True)

return Response(serializer.data)

def create(self, request):
player = getattr(request.user, 'player', None)

if not player:
return Response({'error': 'Player not found'}, status=404)

serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(player=player)
return Response(serializer.data, status=201)
11 changes: 9 additions & 2 deletions Wordle+/ionic/ionic-app/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,20 @@ const routes: Routes = [
},
{
path: 'home',
loadChildren: () => import('./pages/home/home.module').then( m => m.HomePageModule)
loadChildren: () => import('./pages/home/home.module').then( m => m.HomePageModule),
canActivate: [AuthGuard]
},
{
path: 'classic-wordle',
path: 'classic-wordle/:length',
loadChildren: () => import('./pages/classic-wordle/classic-wordle.module').then( m => m.ClassicWordlePageModule),
canActivate: [AuthGuard]
},
{
path: 'edit-user',
loadChildren: () => import('./pages/edit-user/edit-user.module').then( m => m.EditUserPageModule),
canActivate: [AuthGuard]
},




Expand Down
8 changes: 4 additions & 4 deletions Wordle+/ionic/ionic-app/src/app/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,18 @@ export class AuthGuard implements CanActivate {
if (accessToken) {
return (await this.apiService.checkTokenExpiration()).pipe(
catchError((error: HttpErrorResponse) => {
console.log('in error block');
if (error.status === 401 && error.error && error.error.detail === 'Invalid token.') {
if (error.status === 401 && error.error) {
this.storageService.destroyAll();
this.router.navigate(['/login']);
this.router.navigate(['/login'], { queryParams: { expired: 'true' } });
} else {
console.error('Error al comprobar el token de acceso:', error);
}
return of(false);
})
).toPromise();
} else {
this.router.navigate(['/login']);
this.storageService.destroyAll();
this.router.navigate(['/login'], { queryParams: { expired: 'true' } });
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<ion-content>
<h1>Notifications</h1>
<ion-list>
<ion-item *ngFor="let notification of notifications">
<a [href]="notification.link">{{ notification.text }}</a>
</ion-item>
</ion-list>
</ion-content>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
h1 {
text-align: center;
border-bottom: 2px solid;
font-style: oblique;
font-weight: bolder;
color: var(--ion-color-purple);
}

ion-content {
a {
text-decoration: none;
color: inherit;
}
}


.empty-message {
color: gray;
font-size: 18px;
text-align: center;
padding: 16px;
}

Loading

0 comments on commit e3cd592

Please sign in to comment.