From 720c12c1ae0e8bb030a0b4b34fb6b31b94806134 Mon Sep 17 00:00:00 2001 From: lukaszkarykowski <54527822+lukaszkarykowski@users.noreply.github.com> Date: Thu, 16 Jan 2020 11:35:18 +0100 Subject: [PATCH] Introduce Access Card management (#3506) Author: @lukaszkarykowski --- src/ralph/access_cards/__init__.py | 0 src/ralph/access_cards/admin.py | 33 ++++++ src/ralph/access_cards/api.py | 25 +++++ .../access_cards/migrations/0001_initial.py | 41 +++++++ src/ralph/access_cards/migrations/__init__.py | 0 src/ralph/access_cards/models.py | 81 ++++++++++++++ src/ralph/access_cards/tests/__init__.py | 0 src/ralph/access_cards/tests/factories.py | 16 +++ src/ralph/access_cards/tests/test_api.py | 101 ++++++++++++++++++ src/ralph/admin/sitetrees.py | 1 + src/ralph/admin/tests/tests_views.py | 1 + src/ralph/settings/base.py | 1 + src/ralph/urls/base.py | 1 + 13 files changed, 301 insertions(+) create mode 100644 src/ralph/access_cards/__init__.py create mode 100644 src/ralph/access_cards/admin.py create mode 100644 src/ralph/access_cards/api.py create mode 100644 src/ralph/access_cards/migrations/0001_initial.py create mode 100644 src/ralph/access_cards/migrations/__init__.py create mode 100644 src/ralph/access_cards/models.py create mode 100644 src/ralph/access_cards/tests/__init__.py create mode 100644 src/ralph/access_cards/tests/factories.py create mode 100644 src/ralph/access_cards/tests/test_api.py diff --git a/src/ralph/access_cards/__init__.py b/src/ralph/access_cards/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/ralph/access_cards/admin.py b/src/ralph/access_cards/admin.py new file mode 100644 index 0000000000..5cd223c229 --- /dev/null +++ b/src/ralph/access_cards/admin.py @@ -0,0 +1,33 @@ +from django.utils.translation import ugettext_lazy as _ + +from ralph.access_cards.models import AccessCard +from ralph.admin import RalphAdmin, register + + +@register(AccessCard) +class AccessCardAdmin(RalphAdmin): + list_display = ['status', 'visual_number', 'system_number', 'user', + 'owner'] + list_select_related = ['user', 'owner'] + raw_id_fields = ['user', 'owner', 'region'] + list_filter = ['status', 'issue_date', 'visual_number', + 'system_number', 'user', 'owner', 'user__segment', + 'user__company', 'user__department', 'user__employee_id'] + search_fields = ['visual_number', 'system_number', 'user__first_name', + 'user__last_name', 'user__username'] + + fieldsets = ( + ( + _('Access Card Info'), + { + 'fields': ('visual_number', 'system_number', + 'status', 'region', 'issue_date', 'notes') + } + ), + ( + _('User Info'), + { + 'fields': ('user', 'owner') + } + ) + ) diff --git a/src/ralph/access_cards/api.py b/src/ralph/access_cards/api.py new file mode 100644 index 0000000000..e74b3da227 --- /dev/null +++ b/src/ralph/access_cards/api.py @@ -0,0 +1,25 @@ +from ralph.access_cards.models import AccessCard +from ralph.accounts.api import RalphUserSimpleSerializer, RegionSerializer +from ralph.api import RalphAPISerializer, RalphAPIViewSet, router + + +class AccessCardSerializer(RalphAPISerializer): + user = RalphUserSimpleSerializer() + owner = RalphUserSimpleSerializer() + region = RegionSerializer() + + class Meta: + model = AccessCard + fields = ['id', 'status', 'user', 'owner', 'created', 'modified', + 'visual_number', 'system_number', 'issue_date', 'notes', + 'region'] + + +class AccessCardViewSet(RalphAPIViewSet): + queryset = AccessCard.objects.order_by('id').all() + select_related = ['user', 'owner', 'region'] + serializer_class = AccessCardSerializer + + +router.register(r'access-card', AccessCardViewSet) +urlpatterns = [] diff --git a/src/ralph/access_cards/migrations/0001_initial.py b/src/ralph/access_cards/migrations/0001_initial.py new file mode 100644 index 0000000000..08df46b355 --- /dev/null +++ b/src/ralph/access_cards/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import dj.choices.fields +from django.conf import settings +import django.db.models.deletion +import ralph.access_cards.models +import ralph.lib.mixins.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0006_remove_ralphuser_gender'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='AccessCard', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), + ('created', models.DateTimeField(verbose_name='date created', auto_now_add=True)), + ('modified', models.DateTimeField(verbose_name='last modified', auto_now=True)), + ('visual_number', models.CharField(max_length=255, unique=True, help_text='Number visible on the access card')), + ('system_number', models.CharField(max_length=255, unique=True, help_text='Internal number in the access system')), + ('issue_date', models.DateField(blank=True, null=True, help_text='Date of issue to the User')), + ('notes', models.TextField(blank=True, null=True, help_text='Optional notes')), + ('status', dj.choices.fields.ChoiceField(default=1, choices=ralph.access_cards.models.AccessCardStatus, help_text='Access card status')), + ('owner', models.ForeignKey(blank=True, null=True, help_text='Owner of the card', related_name='+', on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ('region', models.ForeignKey(to='accounts.Region')), + ('user', models.ForeignKey(blank=True, null=True, help_text='User of the card', related_name='+', on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('-modified', '-created'), + 'abstract': False, + }, + bases=(ralph.lib.mixins.models.AdminAbsoluteUrlMixin, models.Model), + ), + ] diff --git a/src/ralph/access_cards/migrations/__init__.py b/src/ralph/access_cards/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/ralph/access_cards/models.py b/src/ralph/access_cards/models.py new file mode 100644 index 0000000000..7782d2580f --- /dev/null +++ b/src/ralph/access_cards/models.py @@ -0,0 +1,81 @@ +from dj.choices import Choice, Choices +from dj.choices.fields import ChoiceField +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from ralph.accounts.models import RalphUser, Regionalizable +from ralph.lib.mixins.models import AdminAbsoluteUrlMixin, TimeStampMixin + + +class AccessCardStatus(Choices): + _ = Choice + + new = _('new') + in_progress = _('in progress') + lost = _('lost') + damaged = _('damaged') + used = _('in use') + free = _('free') + return_in_progress = _('return in progres') + liquidated = _('liquidated') + + +class AccessCard( + AdminAbsoluteUrlMixin, TimeStampMixin, Regionalizable, models.Model +): + visual_number = models.CharField( + max_length=255, + null=False, + blank=False, + unique=True, + help_text=_('Number visible on the access card') + ) + system_number = models.CharField( + max_length=255, + null=False, + blank=False, + unique=True, + help_text=_('Internal number in the access system') + ) + issue_date = models.DateField( + null=True, + blank=True, + help_text=_('Date of issue to the User') + ) + notes = models.TextField( + null=True, + blank=True, + help_text=_('Optional notes') + ) + user = models.ForeignKey( + RalphUser, + null=True, + blank=True, + related_name='+', + help_text=_('User of the card'), + on_delete=models.SET_NULL + ) + owner = models.ForeignKey( + RalphUser, + null=True, + blank=True, + related_name='+', + help_text=('Owner of the card'), + on_delete=models.SET_NULL + ) + status = ChoiceField( + choices=AccessCardStatus, + default=AccessCardStatus.new.id, + null=False, + blank=False, + help_text=_('Access card status') + ) + + def __str__(self): + return _('Access Card: {}').format(self.visual_number) + + @classmethod + def get_autocomplete_queryset(cls): + return cls._default_manager.exclude( + status=AccessCardStatus.liquidated.id + ) diff --git a/src/ralph/access_cards/tests/__init__.py b/src/ralph/access_cards/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/ralph/access_cards/tests/factories.py b/src/ralph/access_cards/tests/factories.py new file mode 100644 index 0000000000..82c78fa9e8 --- /dev/null +++ b/src/ralph/access_cards/tests/factories.py @@ -0,0 +1,16 @@ +import factory +from factory.django import DjangoModelFactory + +from ralph.access_cards.models import AccessCard, AccessCardStatus +from ralph.accounts.tests.factories import RegionFactory + + +class AccessCardFactory(DjangoModelFactory): + visual_number = factory.Sequence(lambda n: '000000000000{}'.format(n)) + system_number = factory.Sequence(lambda n: '000000000000{}'.format(n)) + region = factory.SubFactory(RegionFactory) + status = AccessCardStatus.new + + class Meta: + model = AccessCard + django_get_or_create = ['visual_number', 'system_number'] diff --git a/src/ralph/access_cards/tests/test_api.py b/src/ralph/access_cards/tests/test_api.py new file mode 100644 index 0000000000..03e7f0d680 --- /dev/null +++ b/src/ralph/access_cards/tests/test_api.py @@ -0,0 +1,101 @@ +from datetime import datetime + +from rest_framework import status +from rest_framework.reverse import reverse + +from ralph.access_cards.tests.factories import AccessCardFactory +from ralph.accounts.tests.factories import RegionFactory +from ralph.api.tests._base import RalphAPITestCase +from ralph.tests.factories import UserFactory + + +class AccessCardTestCase(RalphAPITestCase): + def assertAccessCardHasCertainFieldsAndValues( + self, access_card, response_data + ): + self.assertEqual(response_data['status'], access_card.status.name) + self.assertEqual( + response_data['system_number'], + access_card.system_number + ) + self.assertEqual( + response_data['visual_number'], + access_card.visual_number + ) + self.assertEqual( + response_data['issue_date'], + access_card.issue_date.strftime('%Y-%m-%d') + ) + self.assertEqual(response_data['notes'], access_card.notes) + self.assertEqual( + response_data['user']['username'], + access_card.user.username + ) + self.assertEqual( + response_data['owner']['username'], + access_card.owner.username + ) + self.assertEqual( + response_data['region']['id'], + access_card.region.id + ) + + def test_detail_access_card_returns_expected_fields(self): + access_card = AccessCardFactory( + issue_date=datetime.now(), + user=UserFactory(), + owner=UserFactory(), + notes='test' + ) + + url = reverse('accesscard-detail', args=(access_card.id,)) + response = self.client.get(url) + self.assertEqual(status.HTTP_200_OK, response.status_code) + + self.assertAccessCardHasCertainFieldsAndValues( + access_card, response.data + ) + + def test_list_access_card_returns_expected_fields(self): + access_card1 = AccessCardFactory( + issue_date=datetime.now(), + user=UserFactory(), + owner=UserFactory(), + notes='test' + ) + access_card2 = AccessCardFactory( + issue_date=datetime.now(), + user=UserFactory(), + owner=UserFactory(), + notes='test' + ) + + url = reverse('accesscard-list') + response = self.client.get(url) + self.assertEqual(status.HTTP_200_OK, response.status_code) + + self.assertAccessCardHasCertainFieldsAndValues( + access_card1, response.data['results'][0] + ) + + self.assertAccessCardHasCertainFieldsAndValues( + access_card2, response.data['results'][1] + ) + + def test_class_access_card_test_case(self): + region = RegionFactory() + + access_card = { + 'status': "in use", + 'visual_number': '654321', + 'system_number': 'F9876DSGV', + 'notes': 'test note', + 'issue_date': '2020-01-02', + 'region': region.id + } + url = reverse('accesscard-list') + response = self.client.post(url, data=access_card) + self.assertEqual(status.HTTP_201_CREATED, response.status_code) + + for field in access_card: + self.assertEqual(response.data[field], access_card[field]) diff --git a/src/ralph/admin/sitetrees.py b/src/ralph/admin/sitetrees.py index 42e36f6b11..1741b17b7f 100644 --- a/src/ralph/admin/sitetrees.py +++ b/src/ralph/admin/sitetrees.py @@ -138,6 +138,7 @@ def section(section_name, app, model): url_as_pattern=False, perms_mode_all=False, children=[ + section(_('Access Cards'), 'access_cards', 'AccessCard'), section(_('Hardware'), 'back_office', 'backofficeasset'), section(_('SIM Cards'), 'sim_cards', 'SIMCard'), ] diff --git a/src/ralph/admin/tests/tests_views.py b/src/ralph/admin/tests/tests_views.py index 916cf8dd5a..c278b7e309 100644 --- a/src/ralph/admin/tests/tests_views.py +++ b/src/ralph/admin/tests/tests_views.py @@ -14,6 +14,7 @@ FACTORY_MAP = { 'django.contrib.auth.models.Group': 'ralph.accounts.tests.factories.GroupFactory', # noqa + 'ralph.access_cards.models.AccessCard': 'ralph.access_cards.tests.factories.AccessCardFactory', # noqa 'ralph.accounts.models.RalphUser': 'ralph.accounts.tests.factories.UserFactory', # noqa 'ralph.accounts.models.Region': 'ralph.accounts.tests.factories.RegionFactory', # noqa 'ralph.accounts.models.Team': 'ralph.accounts.tests.factories.TeamFactory', diff --git a/src/ralph/settings/base.py b/src/ralph/settings/base.py index 2204930085..4477366aaf 100644 --- a/src/ralph/settings/base.py +++ b/src/ralph/settings/base.py @@ -66,6 +66,7 @@ def get_sentinels(sentinels_string): 'mptt', 'reversion', 'sitetree', + 'ralph.access_cards', 'ralph.accounts', 'ralph.assets', 'ralph.attachments', diff --git a/src/ralph/urls/base.py b/src/ralph/urls/base.py index e01eee4470..ff910b006e 100644 --- a/src/ralph/urls/base.py +++ b/src/ralph/urls/base.py @@ -16,6 +16,7 @@ # notice that each module should have `urlpatters` variable defined # (as empty list if there is any custom url) api_urls = list(map(lambda u: url(r'^', include(u)), [ + 'ralph.access_cards.api', 'ralph.accounts.api', 'ralph.assets.api.routers', 'ralph.back_office.api',