Skip to content
This repository has been archived by the owner on May 15, 2020. It is now read-only.

Commit

Permalink
Merge pull request #8 from kpn/feature/credentials-model
Browse files Browse the repository at this point in the history
NEW - Added Credential model/view
  • Loading branch information
mjholtkamp authored Feb 11, 2019
2 parents 1ec5ede + 1105853 commit 581152d
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 19 deletions.
4 changes: 2 additions & 2 deletions katka/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,5 @@ def pre_save(self, model_instance, add):


class KatkaSlugField(models.SlugField):
def __init__(self, *args, max_length=10, unique=True, **kwargs):
super().__init__(*args, max_length=max_length, unique=unique, **kwargs)
def __init__(self, *args, max_length=10, blank=False, null=False, **kwargs):
super().__init__(*args, max_length=max_length, blank=blank, null=null, **kwargs)
46 changes: 46 additions & 0 deletions katka/migrations/0003_credential_and_unique_slugs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Generated by Django 2.1.5 on 2019-02-11 02:52

import uuid

import django.db.models.deletion
from django.db import migrations, models

import katka.fields


class Migration(migrations.Migration):

dependencies = [
('katka', '0002_add_project_and_slug'),
]

operations = [
migrations.CreateModel(
name='Credential',
fields=[
('created', models.DateTimeField(auto_now_add=True)),
('created_username', katka.fields.AutoUsernameField(max_length=50)),
('modified', models.DateTimeField(auto_now=True)),
('modified_username', katka.fields.AutoUsernameField(max_length=50)),
('status', models.CharField(choices=[('active', 'active'), ('inactive', 'inactive')], default='active', max_length=50)),
('public_identifier', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('slug', katka.fields.KatkaSlugField(max_length=10)),
('name', models.CharField(max_length=100)),
('credential_type', models.CharField(max_length=50)),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='katka.Team')),
],
),
migrations.AlterField(
model_name='project',
name='slug',
field=katka.fields.KatkaSlugField(max_length=10),
),
migrations.AlterUniqueTogether(
name='project',
unique_together={('team', 'slug')},
),
migrations.AlterUniqueTogether(
name='credential',
unique_together={('team', 'slug')},
),
]
19 changes: 18 additions & 1 deletion katka/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

class Team(AuditedModel):
public_identifier = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
slug = KatkaSlugField()
slug = KatkaSlugField(unique=True)
name = models.CharField(max_length=100)
group = models.ForeignKey(Group, on_delete=models.CASCADE)

Expand All @@ -23,5 +23,22 @@ class Project(AuditedModel):
name = models.CharField(max_length=100)
team = models.ForeignKey(Team, on_delete=models.CASCADE)

class Meta:
unique_together = ('team', 'slug')

def __str__(self): # pragma: no cover
return f'{self.name}'


class Credential(AuditedModel):
public_identifier = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
slug = KatkaSlugField()
name = models.CharField(max_length=100)
credential_type = models.CharField(max_length=50)
team = models.ForeignKey(Team, on_delete=models.CASCADE)

class Meta:
unique_together = ('team', 'slug')

def __str__(self): # pragma: no cover
return f'{self.name}'
29 changes: 20 additions & 9 deletions katka/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib.auth.models import Group

from katka.models import Project, Team
from katka.models import Credential, Project, Team
from rest_framework import serializers
from rest_framework.exceptions import NotFound, PermissionDenied

Expand Down Expand Up @@ -41,16 +41,11 @@ class EmbeddedTeamSerializer(serializers.Serializer):
slug = serializers.SlugField(required=False)


class ProjectSerializer(serializers.ModelSerializer):
team = EmbeddedTeamSerializer(required=False, read_only=True)

class Meta:
model = Project
fields = ('public_identifier', 'slug', 'name', 'team')

class TeamChildMixin:
def to_internal_value(self, data):
# Can't put this on the EmbeddedTeamSerializer because it will never be called (team is not required and
# read-only, so it's not allowed to pass the team when modifying something).
data = super().to_internal_value(data)
try:
data['team'] = Team.objects.get(public_identifier=self.context['view'].kwargs['team_public_identifier'])
except Team.DoesNotExist:
Expand All @@ -66,4 +61,20 @@ def validate(self, attrs):
if not self.context['request'].user.groups.filter(name=attrs['team'].group.name).exists():
raise PermissionDenied('User is not a member of this group')

return attrs
return super().validate(attrs)


class ProjectSerializer(TeamChildMixin, serializers.ModelSerializer):
team = EmbeddedTeamSerializer(required=False, read_only=True)

class Meta:
model = Project
fields = ('public_identifier', 'slug', 'name', 'team')


class CredentialSerializer(TeamChildMixin, serializers.ModelSerializer):
team = EmbeddedTeamSerializer(required=False, read_only=True)

class Meta:
model = Credential
fields = ('name', 'slug', 'team')
4 changes: 4 additions & 0 deletions katka/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
project_router = NestedSimpleRouter(router, 'team', lookup='team')
project_router.register('project', views.ProjectViewSet, basename='project')

credential_router = NestedSimpleRouter(router, 'team', lookup='team')
credential_router.register('credential', views.CredentialViewSet, basename='credential')

urlpatterns = [
path('admin/', admin.site.urls),
path('', include(router.urls)),
path('', include(project_router.urls)),
path('', include(credential_router.urls)),
]
13 changes: 11 additions & 2 deletions katka/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from katka.models import Project, Team
from katka.serializers import ProjectSerializer, TeamSerializer
from katka.models import Credential, Project, Team
from katka.serializers import CredentialSerializer, ProjectSerializer, TeamSerializer
from katka.viewsets import AuditViewSet


Expand All @@ -22,3 +22,12 @@ class ProjectViewSet(AuditViewSet):
def get_queryset(self):
user_groups = self.request.user.groups.all()
return super().get_queryset().filter(team__group__in=user_groups)


class CredentialViewSet(AuditViewSet):
model = Credential
serializer_class = CredentialSerializer

def get_queryset(self):
user_groups = self.request.user.groups.all()
return super().get_queryset().filter(team__group__in=user_groups)
3 changes: 2 additions & 1 deletion requirements/requirements-base.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Django>=2.1,<3.0
#Django>=2.1,<3.0
Django==2.1.5 # pin to this version because 2.1.6 has a missing migration which breaks the build: https://code.djangoproject.com/ticket/30174#ticket
djangorestframework>=3.9.0,<4.0.0
django-encrypted-model-fields>=0.5.8,<1.0.0
drf-nested-routers
9 changes: 9 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ def deactivated_project(team, project):
return project


@pytest.fixture
def credential(team):
credential = models.Credential(name='System user X', slug='SUX', team=team)
with username_on_model(models.Credential, 'initial'):
credential.save()

return credential


@pytest.fixture
def user(group):
u = User.objects.create_user('test_user', None, None)
Expand Down
112 changes: 112 additions & 0 deletions tests/integration/test_credential_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from uuid import UUID

import pytest
from katka import models
from katka.constants import STATUS_INACTIVE


@pytest.mark.django_db
class TestCredentialViewSetUnauthenticated:
"""
When a user is not logged in, no group information is available, so nothing is returned.
For listing, that would be an empty list for other operations, an error like the object could
not be found, except on create (you need to be part of a group and anonymous users do not have any)
"""

def test_list(self, client, team, credential):
response = client.get(f'/team/{team.public_identifier}/credential/')
assert response.status_code == 200
parsed = response.json()
assert len(parsed) == 0

def test_list_unknown_team(self, client, team, credential):
response = client.get('/team/00000000-0000-0000-0000-000000000000/credential/')
assert response.status_code == 200
parsed = response.json()
assert len(parsed) == 0

def test_get(self, client, team, credential):
response = client.get(f'/team/{team.public_identifier}/credential/{credential.public_identifier}/')
assert response.status_code == 404

def test_delete(self, client, team, credential):
response = client.delete(f'/team/{team.public_identifier}/credential/{credential.public_identifier}/')
assert response.status_code == 404

def test_update(self, client, team, credential):
url = f'/team/{team.public_identifier}/credential/{credential.public_identifier}/'
data = {'name': 'B-Team', 'group': 'group1'}
response = client.put(url, data, content_type='application/json')
assert response.status_code == 404

def test_partial_update(self, client, team, credential):
url = f'/team/{team.public_identifier}/credential/{credential.public_identifier}/'
response = client.patch(url, {'name': 'B-Team'}, content_type='application/json')
assert response.status_code == 404

def test_create(self, client, team):
url = f'/team/{team.public_identifier}/credential/'
data = {'name': 'System User Y', 'slug': 'SUY'}
response = client.post(url, data, content_type='application/json')
assert response.status_code == 403


@pytest.mark.django_db
class TestCredentialViewSet:
def test_list(self, client, logged_in_user, team, credential):
response = client.get(f'/team/{team.public_identifier}/credential/')
assert response.status_code == 200
parsed = response.json()
assert len(parsed) == 1
assert parsed[0]['name'] == 'System user X'
parsed_team = parsed[0]['team']
assert UUID(parsed_team['public_identifier']) == team.public_identifier
assert parsed_team['slug'] == 'ATM'

def test_get(self, client, logged_in_user, team, credential):
response = client.get(f'/team/{team.public_identifier}/credential/{credential.public_identifier}/')
assert response.status_code == 200
parsed = response.json()
assert parsed['name'] == 'System user X'
assert parsed['team']['slug'] == 'ATM'
assert UUID(parsed['team']['public_identifier']) == team.public_identifier

def test_get_excludes_inactive(self, client, logged_in_user, deactivated_team):
response = client.get(f'/team/{deactivated_team.public_identifier}/')
assert response.status_code == 404

def test_delete(self, client, logged_in_user, team, credential):
response = client.delete(f'/team/{team.public_identifier}/credential/{credential.public_identifier}/')
assert response.status_code == 204
p = models.Credential.objects.get(pk=credential.public_identifier)
assert p.status == STATUS_INACTIVE

def test_update(self, client, logged_in_user, team, credential):
url = f'/team/{team.public_identifier}/credential/{credential.public_identifier}/'
data = {'name': 'System user Y', 'slug': 'SUY'}
response = client.put(url, data, content_type='application/json')
assert response.status_code == 200
p = models.Credential.objects.get(pk=credential.public_identifier)
assert p.name == 'System user Y'

def test_update_nonexistent_team(self, client, logged_in_user, team, credential):
url = f'/team/00000000-0000-0000-0000-000000000000/credential/{credential.public_identifier}/'
data = {'name': 'System User Y', 'slug': 'SUY'}
response = client.put(url, data, content_type='application/json')
assert response.status_code == 404

def test_partial_update(self, client, logged_in_user, team, credential):
url = f'/team/{team.public_identifier}/credential/{credential.public_identifier}/'
data = {'name': 'System User Y'}
response = client.patch(url, data, content_type='application/json')
assert response.status_code == 200
p = models.Credential.objects.get(pk=credential.public_identifier)
assert p.name == 'System User Y'

def test_create(self, client, logged_in_user, team, credential):
url = f'/team/{team.public_identifier}/credential/'
response = client.post(url, {'name': 'System User Y', 'slug': 'SUY'}, content_type='application/json')
assert response.status_code == 201
models.Credential.objects.get(name='System User Y')
assert models.Credential.objects.count() == 2
9 changes: 5 additions & 4 deletions tests/integration/test_project_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def test_partial_update(self, client, team, project):

def test_create(self, client, team):
url = f'/team/{team.public_identifier}/project/'
data = {'name': 'Project D'}
data = {'name': 'Project D', 'slug': 'PRJD'}
response = client.post(url, data, content_type='application/json')
assert response.status_code == 403

Expand Down Expand Up @@ -76,6 +76,7 @@ def test_get(self, client, logged_in_user, team, project):
assert response.status_code == 200
parsed = response.json()
assert parsed['name'] == 'Project D'
assert parsed['slug'] == 'PRJD'
assert parsed['team']['slug'] == 'ATM'
assert UUID(parsed['team']['public_identifier']) == team.public_identifier

Expand All @@ -91,15 +92,15 @@ def test_delete(self, client, logged_in_user, team, project):

def test_update(self, client, logged_in_user, team, project):
url = f'/team/{team.public_identifier}/project/{project.public_identifier}/'
data = {'name': 'Project X'}
data = {'name': 'Project X', 'slug': 'PRJX'}
response = client.put(url, data, content_type='application/json')
assert response.status_code == 200
p = models.Project.objects.get(pk=project.public_identifier)
assert p.name == 'Project X'

def test_update_nonexistent_team(self, client, logged_in_user, team, project):
url = f'/team/00000000-0000-0000-0000-000000000000/project/{project.public_identifier}/'
data = {'name': 'Project X'}
data = {'name': 'Project X', 'slug': 'PRJX'}
response = client.put(url, data, content_type='application/json')
assert response.status_code == 404

Expand All @@ -113,7 +114,7 @@ def test_partial_update(self, client, logged_in_user, team, project):

def test_create(self, client, logged_in_user, team, project):
url = f'/team/{team.public_identifier}/project/'
response = client.post(url, {'name': 'Project X'}, content_type='application/json')
response = client.post(url, {'name': 'Project X', 'slug': 'PRJX'}, content_type='application/json')
assert response.status_code == 201
p = models.Project.objects.get(name='Project X')
assert p.name == 'Project X'
Expand Down

0 comments on commit 581152d

Please sign in to comment.