Skip to content

Commit

Permalink
Merge pull request #14 from dnd-al-krk/feature/13-models_for_characters
Browse files Browse the repository at this point in the history
13 - added PlayerCharacter and related models

This Pull Request introduces changes from #13 and also from #11 and #9

**Added:**
+ PlayerCharacter model (and related races, classes and factions)
+ DMNotes model
+ admin interface for all models
+ login with email instead of username
+ username is nickname for the system
+ API endpoints (CRUD) - also for Profile and User
+ permissions: PlayerCharacter can be changed only by owners, but visible to any user
+ permissions: DMNotes can be seen by any DM, but modified only by owner
+ migration for profiles has been changed so dbs should be reseted
  • Loading branch information
ivellios authored Jul 21, 2018
2 parents 231809d + 9fd3c42 commit 6be0035
Show file tree
Hide file tree
Showing 14 changed files with 343 additions and 5 deletions.
Empty file added portal/api/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions portal/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.conf.urls import url, include
from django.urls import path
from rest_framework.routers import DefaultRouter
from profiles.api import views as profiles_views

# Create a router and register our viewsets with it.
router = DefaultRouter()
router.register(r'characters', profiles_views.PlayerCharacterViewSet)
router.register(r'notes', profiles_views.DMNoteViewSet)
router.register(r'profiles', profiles_views.ProfileViewSet)

# The API URLs are now determined automatically by the router.
urlpatterns = [
url(r'^', include(router.urls))
]
4 changes: 3 additions & 1 deletion portal/portal/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.urls import path, include


urlpatterns = [
path('api/', include(('api.urls', 'api'), namespace='api')),
path('admin/', admin.site.urls),
]
37 changes: 36 additions & 1 deletion portal/profiles/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

# Define an inline admin descriptor for Employee model
# which acts a bit like a singleton
from .models import Profile
from .constants import ROLE_DM, ROLE_PLAYER
from .models import Profile, CharacterFaction, CharacterRace, CharacterClass, PlayerCharacter, DMNote


class ProfileInline(admin.StackedInline):
Expand All @@ -23,3 +24,37 @@ class UserAdmin(BaseUserAdmin):

admin.site.unregister(User)
admin.site.register(User, UserAdmin)


@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
list_display = ['__str__', 'role', 'dci']
actions = ['make_dm', 'make_player']

def make_dm(self, request, queryset):
updated = queryset.update(role=ROLE_DM)
self.message_user(request, _("{} successfully changed roles to DM.").format(updated))
make_dm.short_description = _('Change role to DM')

def make_player(self, request, queryset):
updated = queryset.update(role=ROLE_PLAYER)
self.message_user(request, _("{} successfully changed roles to Player.").format(updated))
make_player.short_description = _('Change role to Player')


admin.site.register(CharacterClass)
admin.site.register(CharacterRace)
admin.site.register(CharacterFaction)


@admin.register(PlayerCharacter)
class PlayerCharacterAdmin(admin.ModelAdmin):
list_display = ['name', 'level', 'race', 'pc_class', 'faction']
search_fields = ['name', ]
list_filter = ['level', 'race', 'pc_class', 'faction']


@admin.register(DMNote)
class DMNoteAdmin(admin.ModelAdmin):
list_display = ['__str__', 'dm', 'player', 'created']
list_filter = ['created']
Empty file added portal/profiles/api/__init__.py
Empty file.
34 changes: 34 additions & 0 deletions portal/profiles/api/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):

def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True

return obj.owner == request.user.profile


class IsProfileOwnerOrReadOnly(permissions.BasePermission):

def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True

return obj.user == request.user


class IsDMOwnerOrReadOnly(permissions.BasePermission):

def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True

return obj.dm == request.user.profile


class OnlyDMCanRead(permissions.BasePermission):

def has_permission(self, request, view):
return request.user.profile.is_dm()
47 changes: 47 additions & 0 deletions portal/profiles/api/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from django.contrib.auth.models import User
from rest_framework import serializers
from rest_framework.validators import UniqueValidator

from ..models import PlayerCharacter, DMNote, Profile


class PlayerCharacterSerializer(serializers.ModelSerializer):
class Meta:
model = PlayerCharacter
fields = ('id', 'name', 'pc_class', 'race', 'faction', 'level', )


class DMNoteSerializer(serializers.ModelSerializer):
class Meta:
model = DMNote
fields = ('id', 'player', 'note', )


class UserSerializer(serializers.ModelSerializer):
nickname = serializers.CharField(source='username',
validators=[UniqueValidator(queryset=User.objects.all())])

class Meta:
model = User
fields = ('id', 'first_name', 'last_name', 'nickname', )


class ProfileSerializer(serializers.ModelSerializer):
user = UserSerializer(required=True)

class Meta:
model = Profile
fields = ('id', 'user', 'dci')

def update(self, instance, validated_data):
try:
user_data = validated_data.pop('user')
except KeyError:
pass
else:
instance.dci = validated_data.get('dci', instance.dci)
instance.save()

UserSerializer().update(instance.user, user_data)

return instance
40 changes: 40 additions & 0 deletions portal/profiles/api/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from rest_framework import viewsets, mixins
from rest_framework.permissions import IsAuthenticated

from .permissions import IsOwnerOrReadOnly, IsDMOwnerOrReadOnly, OnlyDMCanRead, IsProfileOwnerOrReadOnly
from .serializers import PlayerCharacterSerializer, DMNoteSerializer, ProfileSerializer
from ..models import PlayerCharacter, DMNote, Profile


class PlayerCharacterViewSet(viewsets.ModelViewSet):
serializer_class = PlayerCharacterSerializer
queryset = PlayerCharacter.objects.all()
permission_classes = [IsAuthenticated,
IsOwnerOrReadOnly]

def get_queryset(self):
return PlayerCharacter.objects.filter(owner=self.request.user.profile)

def perform_create(self, serializer):
serializer.save(owner=self.request.user.profile)


class DMNoteViewSet(viewsets.ModelViewSet):
serializer_class = DMNoteSerializer
queryset = DMNote.objects.all()
permission_classes = [IsAuthenticated,
IsDMOwnerOrReadOnly,
OnlyDMCanRead]

def perform_create(self, serializer):
serializer.save(dm=self.request.user.profile)


class ProfileViewSet(mixins.UpdateModelMixin,
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet):
serializer_class = ProfileSerializer
queryset = Profile.objects.all()
permission_classes = [IsAuthenticated,
IsProfileOwnerOrReadOnly]
15 changes: 15 additions & 0 deletions portal/profiles/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend


class EmailBackend(ModelBackend):
def authenticate(self, username=None, password=None, **kwargs):
UserModel = get_user_model()
try:
user = UserModel.objects.get(email=username)
except UserModel.DoesNotExist:
return None
else:
if user.check_password(password):
return user
return None
86 changes: 84 additions & 2 deletions portal/profiles/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Generated by Django 2.0.6 on 2018-06-25 20:05
# Generated by Django 2.0.6 on 2018-07-16 19:16

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid


class Migration(migrations.Migration):
Expand All @@ -14,13 +15,94 @@ class Migration(migrations.Migration):
]

operations = [
migrations.CreateModel(
name='CharacterClass',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=16, verbose_name='Class Name')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='CharacterFaction',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=16, verbose_name='Faction Name')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='CharacterRace',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=16, verbose_name='Race Name')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='DMNote',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('note', models.TextField(verbose_name='Note on player')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Modified')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='PlayerCharacter',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=255, verbose_name='Character name')),
('level', models.PositiveIntegerField(default=1, verbose_name='Level')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Modified')),
('faction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='profiles.CharacterFaction')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Profile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('dci', models.IntegerField(blank=True, null=True, verbose_name='DCI')),
('role', models.CharField(default='player', max_length=20, verbose_name='Role')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='playercharacter',
name='owner',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='profiles.Profile'),
),
migrations.AddField(
model_name='playercharacter',
name='pc_class',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='profiles.CharacterClass'),
),
migrations.AddField(
model_name='playercharacter',
name='race',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='profiles.CharacterRace'),
),
migrations.AddField(
model_name='dmnote',
name='dm',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dm_notes_given', to='profiles.Profile'),
),
migrations.AddField(
model_name='dmnote',
name='player',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dm_notes', to='profiles.Profile'),
),
]
57 changes: 56 additions & 1 deletion portal/profiles/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import uuid

from django.db import models
from django.contrib.auth.models import User

from django.utils.translation import ugettext_lazy as _

from utils.models import UUIDModel
from .constants import ROLE_DM, ROLE_PLAYER


Expand All @@ -13,6 +16,58 @@ class Profile(models.Model):
(ROLE_PLAYER, 'Player')
)

user = models.ForeignKey(User, on_delete=models.CASCADE)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
dci = models.IntegerField(_('DCI'), blank=True, null=True)
role = models.CharField(_('Role'), max_length=20, default=ROLE_PLAYER)

def __str__(self):
return '{} {}'.format(self.user.first_name, self.user.last_name)

def is_dm(self):
return self.role == ROLE_DM


class CharacterClass(UUIDModel):
name = models.CharField(_('Class Name'), max_length=16)

def __str__(self):
return self.name


class CharacterRace(UUIDModel):
name = models.CharField(_('Race Name'), max_length=16)

def __str__(self):
return self.name


class CharacterFaction(UUIDModel):
name = models.CharField(_('Faction Name'), max_length=16)

def __str__(self):
return self.name


class PlayerCharacter(UUIDModel):
owner = models.ForeignKey(Profile, on_delete=models.CASCADE)
name = models.CharField(_('Character name'), max_length=255)
pc_class = models.ForeignKey(CharacterClass, on_delete=models.CASCADE)
race = models.ForeignKey(CharacterRace, on_delete=models.CASCADE)
faction = models.ForeignKey(CharacterFaction, on_delete=models.CASCADE)
level = models.PositiveIntegerField(_('Level'), default=1)
created = models.DateTimeField(_('Created'), auto_now_add=True)
modified = models.DateTimeField(_('Modified'), auto_now=True)

def __str__(self):
return self.name


class DMNote(UUIDModel):
dm = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='dm_notes_given')
player = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='dm_notes')
note = models.TextField(_('Note on player'))
created = models.DateTimeField(_('Created'), auto_now_add=True)
modified = models.DateTimeField(_('Modified'), auto_now=True)

def __str__(self):
return 'Note by {} for {}'.format(self.dm, self.player)
Loading

0 comments on commit 6be0035

Please sign in to comment.