Browse Source

add entry CRUD models

pull/342/head
Guillaume Vincent 9 years ago
parent
commit
514e9df72e
12 changed files with 229 additions and 10 deletions
  1. +17
    -7
      api/migrations/0001_initial.py
  2. +2
    -0
      api/models.py
  3. +6
    -0
      api/permissions.py
  4. +39
    -0
      api/serializers.py
  5. +0
    -0
      api/tests/__init__.py
  6. +39
    -0
      api/tests/factories.py
  7. +109
    -0
      api/tests/tests_entries.py
  8. +1
    -0
      api/urls.py
  9. +10
    -1
      api/views.py
  10. +2
    -1
      lesspass/settings.py
  11. +2
    -0
      requirements.dev.txt
  12. +2
    -1
      requirements.txt

+ 17
- 7
api/migrations/0001_initial.py View File

@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.1 on 2016-01-07 19:39
# Generated by Django 1.9.1 on 2016-01-07 19:49
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
@@ -12,6 +13,7 @@ class Migration(migrations.Migration):
initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
@@ -22,14 +24,14 @@ class Migration(migrations.Migration):
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('site', models.CharField(max_length=255)),
('title', models.CharField(default=True, max_length=255, null=True)),
('username', models.CharField(default=True, max_length=255, null=True)),
('email', models.EmailField(default=True, max_length=254, null=True)),
('description', models.TextField(default=True, null=True)),
('url', models.URLField(default=True, null=True)),
('title', models.CharField(blank=True, max_length=255, null=True)),
('username', models.CharField(blank=True, max_length=255, null=True)),
('email', models.EmailField(blank=True, max_length=254, null=True)),
('description', models.TextField(blank=True, null=True)),
('url', models.URLField(blank=True, null=True)),
],
options={
'abstract': False,
'verbose_name_plural': 'Entries',
},
),
migrations.CreateModel(
@@ -40,10 +42,18 @@ class Migration(migrations.Migration):
('settings', models.TextField()),
('length', models.IntegerField(default=12)),
],
options={
'verbose_name_plural': 'Password info',
},
),
migrations.AddField(
model_name='entry',
name='password',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.PasswordInfo'),
),
migrations.AddField(
model_name='entry',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to=settings.AUTH_USER_MODEL),
),
]

+ 2
- 0
api/models.py View File

@@ -1,6 +1,7 @@
import uuid

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


class DateMixin(models.Model):
@@ -26,6 +27,7 @@ class PasswordInfo(models.Model):

class Entry(DateMixin):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='entries')
site = models.CharField(max_length=255)
password = models.ForeignKey(PasswordInfo)



+ 6
- 0
api/permissions.py View File

@@ -0,0 +1,6 @@
from rest_framework import permissions


class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.user == request.user

+ 39
- 0
api/serializers.py View File

@@ -1,8 +1,29 @@
import json

from api import models
from rest_framework import serializers

from django.utils.translation import ugettext_lazy as _


class JsonSettingsField(serializers.Field):
default_error_messages = {
'invalid': _('Value must be valid JSON.')
}

def to_representation(self, value):
return json.loads(value)

def to_internal_value(self, data):
try:
return json.dumps(data)
except (TypeError, ValueError):
self.fail('invalid')


class PasswordInfoSerializer(serializers.ModelSerializer):
settings = JsonSettingsField()

class Meta:
model = models.PasswordInfo
fields = ('counter', 'settings', 'length')
@@ -15,3 +36,21 @@ class EntrySerializer(serializers.ModelSerializer):
model = models.Entry
fields = ('id', 'site', 'password', 'title', 'username', 'email', 'description', 'url', 'created', 'modified')
read_only_fields = ('created', 'modified')

def create(self, validated_data):
password_data = validated_data.pop('password')
user = self.context['request'].user
password_info = models.PasswordInfo.objects.create(**password_data)
return models.Entry.objects.create(user=user, password=password_info, **validated_data)

def update(self, instance, validated_data):
password_data = validated_data.pop('password')
passwordInfo = instance.password
for attr, value in password_data.items():
setattr(passwordInfo, attr, value)
passwordInfo.save()

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

api/tests.py → api/tests/__init__.py View File


+ 39
- 0
api/tests/factories.py View File

@@ -0,0 +1,39 @@
import factory

from api import models


class UserFactory(factory.DjangoModelFactory):
class Meta:
model = models.User

username = factory.Sequence(lambda n: 'username{0}'.format(n))
first_name = factory.Faker('first_name')
last_name = factory.Faker('last_name')
email = factory.LazyAttribute(lambda a: '{0}.{1}@oslab.fr'.format(a.first_name, a.last_name).lower())
password = factory.PostGenerationMethodCall('set_password', 'password')
is_staff = False


class AdminFactory(UserFactory):
is_staff = True


class PasswordInfoFactory(factory.DjangoModelFactory):
class Meta:
model = models.PasswordInfo

settings = '["lowercase", "uppercase", "numbers", "symbols"]'


class EntryFactory(factory.DjangoModelFactory):
class Meta:
model = models.Entry

user = factory.SubFactory(UserFactory)
password = factory.SubFactory(PasswordInfoFactory)

title = 'twitter'
site = 'twitter'
username = 'guillaume20100'
url = 'https://twitter.com/'

+ 109
- 0
api/tests/tests_entries.py View File

@@ -0,0 +1,109 @@
import json

from rest_framework.test import APITestCase, APIClient

from api import models
from api.tests import factories


class LogoutApiTestCase(APITestCase):
def test_get_entries_403(self):
response = self.client.get('/api/entries/')
self.assertEqual(403, response.status_code)


class LoginApiTestCase(APITestCase):
def setUp(self):
self.user = factories.UserFactory()
self.client = APIClient()
self.client.force_authenticate(user=self.user)

def test_get_empty_entries(self):
request = self.client.get('/api/entries/')
self.assertEqual(0, len(request.data['results']))

def test_retrieve_its_own_entries(self):
entry = factories.EntryFactory(user=self.user)
request = self.client.get('/api/entries/')
self.assertEqual(1, len(request.data['results']))
self.assertEqual(entry.site, request.data['results'][0]['site'])

def test_cant_retrieve_other_entries(self):
not_my_entry = factories.EntryFactory(user=factories.UserFactory())
request = self.client.get('/api/entries/%s/' % not_my_entry.id)
self.assertEqual(404, request.status_code)

def test_delete_its_own_entries(self):
entry = factories.EntryFactory(user=self.user)
self.assertEqual(1, models.Entry.objects.all().count())
request = self.client.delete('/api/entries/%s/' % entry.id)
self.assertEqual(204, request.status_code)
self.assertEqual(0, models.Entry.objects.all().count())

def test_cant_delete_other_entry(self):
not_my_entry = factories.EntryFactory(user=factories.UserFactory())
self.assertEqual(1, models.Entry.objects.all().count())
request = self.client.delete('/api/entries/%s/' % not_my_entry.id)
self.assertEqual(404, request.status_code)
self.assertEqual(1, models.Entry.objects.all().count())

def test_create_entry(self):
entry = {
"site": "twitter",
"password": {
"counter": 1,
"settings": [
"lowercase",
"uppercase",
"numbers",
"symbols"
],
"length": 12
},
"title": "twitter",
"username": "guillaume20100",
"email": "guillaume@oslab.fr",
"description": "",
"url": "https://twitter.com/"
}
self.assertEqual(0, models.Entry.objects.count())
self.assertEqual(0, models.PasswordInfo.objects.count())
self.client.post('/api/entries/', entry)
self.assertEqual(1, models.Entry.objects.count())
self.assertEqual(1, models.PasswordInfo.objects.count())

def test_update_entry(self):
entry = factories.EntryFactory(user=self.user)
self.assertNotEqual('facebook', entry.site)
new_entry = {
"site": "facebook",
"password": {
"counter": 1,
"settings": [
"lowercase",
"uppercase",
"numbers"
],
"length": 12
},
"title": "facebook",
"username": "",
"email": "",
"description": "",
"url": "https://facebook.com/"
}
self.client.put('/api/entries/%s/' % entry.id, new_entry)
entry_updated = models.Entry.objects.get(id=entry.id)
self.assertEqual('facebook', entry_updated.site)
self.assertEqual(3, len(json.loads(entry_updated.password.settings)))

def test_cant_update_other_entry(self):
not_my_entry = factories.EntryFactory(user=factories.UserFactory())
self.assertEqual('twitter', not_my_entry.site)
new_entry = {
"site": "facebook",
"password": {"settings": []}
}
request = self.client.put('/api/entries/%s/' % not_my_entry.id, new_entry)
self.assertEqual(404, request.status_code)
self.assertEqual(1, models.Entry.objects.all().count())

+ 1
- 0
api/urls.py View File

@@ -5,6 +5,7 @@ from api import views

router = DefaultRouter()
router.register(r'auth', views.AuthViewSet, base_name="auth")
router.register(r'entries', views.EntryViewSet, base_name='entries')

urlpatterns = [
url(r'^', include(router.urls)),


+ 10
- 1
api/views.py View File

@@ -1,4 +1,5 @@
import json
from api import models, serializers
from api.permissions import IsOwner

from django.contrib.auth import login, authenticate
from rest_framework import status, permissions, viewsets
@@ -36,3 +37,11 @@ class AuthViewSet(viewsets.ViewSet):
login(request, user)
return Response(status=status.HTTP_201_CREATED)
return Response(status=status.HTTP_401_UNAUTHORIZED)


class EntryViewSet(viewsets.ModelViewSet):
serializer_class = serializers.EntrySerializer
permission_classes = (permissions.IsAuthenticated, IsOwner,)

def get_queryset(self):
return models.Entry.objects.filter(user=self.request.user)

+ 2
- 1
lesspass/settings.py View File

@@ -125,7 +125,8 @@ REST_FRAMEWORK = {
),
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
)
),
'TEST_REQUEST_DEFAULT_FORMAT': 'json'
}

AUTHENTICATION_BACKENDS = (


+ 2
- 0
requirements.dev.txt View File

@@ -0,0 +1,2 @@
-r requirements.txt
factory-boy==2.6.0

+ 2
- 1
requirements.txt View File

@@ -1,3 +1,4 @@
Django==1.9.1
djangorestframework==3.3.2
smartconfigparser==0.1.1
smartconfigparser==0.1.1
django-allauth==0.24.1

Loading…
Cancel
Save