diff --git a/backend/fleet_management/admin.py b/backend/fleet_management/admin.py index 65e13e8c..3861d831 100644 --- a/backend/fleet_management/admin.py +++ b/backend/fleet_management/admin.py @@ -4,12 +4,12 @@ from import_export.admin import ImportExportModelAdmin from import_export.fields import Field from import_export import resources -from .models import Car, Passenger, Drive, User, Project +from .models import Car, Drive, User, Project class DriveResource(resources.ModelResource): - def dehydrate_passengers(self, drive): - return " / ".join(str(passenger) for passenger in drive.passengers.all()) + def dehydrate_passenger(self, drive): + return str(drive.passenger) def dehydrate_driver(self, drive): return str(drive.driver) # required, because import-export prints PK by default @@ -23,7 +23,7 @@ class Meta: model = Drive fields = ( "id", - "passengers", + "passenger", "date", "start_mileage", "end_mileage", @@ -68,7 +68,6 @@ class CustomUserAdmin(UserAdmin): admin.site.register(Car) -admin.site.register(Passenger) admin.site.register(Drive, DriveAdmin) admin.site.register(User, CustomUserAdmin) admin.site.register(Project) diff --git a/backend/fleet_management/api.py b/backend/fleet_management/api.py index 90249096..fe1c4837 100644 --- a/backend/fleet_management/api.py +++ b/backend/fleet_management/api.py @@ -2,7 +2,7 @@ from rest_framework.response import Response from .permissions import GroupPermission, all_driver_methods -from .models import Car, Drive, Passenger, Project +from .models import User, Car, Drive, Project from .serializers import ( CarSerializer, DriveSerializer, @@ -10,7 +10,7 @@ UserSerializer, ProjectSerializer, ) - +from .constants import Groups class CurrentUserRetrieveView(views.APIView): @@ -32,9 +32,10 @@ class PassengerListView(generics.ListAPIView): ordering = ('first_name', 'last_name') def get_queryset(self): - return Passenger.objects.filter( + return User.objects.filter( + groups__name=Groups.Passenger.name, country=self.request.user.country - ) | Passenger.objects.filter(country=None) + ) | User.objects.filter(country=None, groups__name=Groups.Passenger.name) class CarListView(generics.ListAPIView): diff --git a/backend/fleet_management/factories.py b/backend/fleet_management/factories.py index ea26ff39..d8413830 100644 --- a/backend/fleet_management/factories.py +++ b/backend/fleet_management/factories.py @@ -8,7 +8,7 @@ from fleet_management.models import ( - Car, Drive, Passenger, Project, User + Car, Drive, Project, User ) COUNTRIES = ('UA', 'SS') @@ -31,7 +31,11 @@ class Meta: @classmethod def _create(cls, model_class, *args, **kwargs): manager = cls._get_manager(model_class) - return manager.create_user(*args, **kwargs) + groups = kwargs.pop("groups", []) + user = manager.create_user(*args, **kwargs) + for g in groups: + user.groups.add(g) + return user class CarFactory(DjangoModelFactory): @@ -111,16 +115,6 @@ def description(self): ) -class PassengerFactory(DjangoModelFactory): - - class Meta: - model = Passenger - - first_name = Faker('first_name', locale='pl_PL') - last_name = Faker('last_name', locale='pl_PL') - country = fuzzy.FuzzyChoice(COUNTRIES) - - class ProjectFactory(DjangoModelFactory): class Meta: @@ -147,6 +141,7 @@ class Meta: driver = SubFactory(UserFactory) project = SubFactory(ProjectFactory) + passenger = SubFactory(UserFactory) car = SubFactory(CarFactory) date = fuzzy.FuzzyDate((now() - timedelta(days=1000)).date()) start_mileage = fuzzy.FuzzyInteger(1000000) @@ -160,11 +155,3 @@ class Meta: def end_mileage(self): return random.randint(self.start_mileage, 1000000) - @post_generation - def passengers(self, create, extracted, **kwargs): - if not create: - return - - if extracted: - for passenger in extracted: - self.passengers.add(passenger) diff --git a/backend/fleet_management/management/commands/populate_database.py b/backend/fleet_management/management/commands/populate_database.py index 0feae8bc..1ddaaaca 100644 --- a/backend/fleet_management/management/commands/populate_database.py +++ b/backend/fleet_management/management/commands/populate_database.py @@ -10,11 +10,10 @@ from fleet_management.factories import ( CarFactory, DriveFactory, - PassengerFactory, ProjectFactory, UserFactory, ) -from ...models import Passenger, Project, User +from ...models import Project, User class Command(BaseCommand): @@ -23,33 +22,37 @@ class Command(BaseCommand): help = "Populates database with fake items." def handle(self, *args, **options): + driver_group = Group.objects.get(name=Groups.Driver.name) + passenger_group = Group.objects.get(name=Groups.Passenger.name) + self.stdout.write(self.style.SUCCESS('Creating 5 cars')) for _ in tqdm(range(5)): CarFactory.create() - self.stdout.write(self.style.SUCCESS('Creating 5 users')) - usernames = [] + self.stdout.write(self.style.SUCCESS('Creating 5 drivers')) + drivers = [] for _ in tqdm(range(5)): - usernames.append(UserFactory.create().username) + drivers.append(UserFactory.create(groups=[driver_group]).username) + passengers = [] self.stdout.write(self.style.SUCCESS('Creating 10 passengers')) for _ in tqdm(range(10)): - PassengerFactory.create() + passengers.append(UserFactory.create(groups=[passenger_group]).username) self.stdout.write(self.style.SUCCESS('Creating 5 projects')) for _ in tqdm(range(5)): ProjectFactory.create() self.stdout.write(self.style.SUCCESS('Creating 50 drives')) - all_users = list(User.objects.all()) - all_passengers = list(Passenger.objects.all()) + all_drivers = list(User.objects.filter(groups=driver_group)) + all_passengers = list(User.objects.filter(groups=passenger_group)) all_projects = list(Project.objects.all()) for _ in tqdm(range(50)): DriveFactory.create( - passengers=random.sample(all_passengers, random.randint(1, 4)), + passenger=random.choice(all_passengers), project=random.choice(all_projects), - driver=random.choice(all_users), + driver=random.choice(all_drivers), ) self.stdout.write( @@ -57,8 +60,15 @@ def handle(self, *args, **options): ) self.stdout.write('=' * 50) - self.stdout.write(self.style.SUCCESS('Newly created users:')) - for index, username in enumerate(usernames): + self.stdout.write(self.style.SUCCESS('Newly created drivers:')) + for index, username in enumerate(drivers): + self.stdout.write(self.style.SUCCESS('{index}. {username}'.format( + index=index, + username=username, + ))) + + self.stdout.write(self.style.SUCCESS('Newly created passengers:')) + for index, username in enumerate(passengers): self.stdout.write(self.style.SUCCESS('{index}. {username}'.format( index=index, username=username, @@ -66,7 +76,5 @@ def handle(self, *args, **options): default_user = User.objects.filter(email='hello@codeforpoznan.pl').first() - driver_group = Group.objects.get(name=Groups.Driver.name) driver_group.user_set.add(default_user) - passenger_group = Group.objects.get(name=Groups.Passenger.name) passenger_group.user_set.add(default_user) diff --git a/backend/fleet_management/migrations/0011_add_default_groups.py b/backend/fleet_management/migrations/0011_add_default_groups.py index 4f3c61f7..052b222b 100644 --- a/backend/fleet_management/migrations/0011_add_default_groups.py +++ b/backend/fleet_management/migrations/0011_add_default_groups.py @@ -1,19 +1,18 @@ -from django.db import migrations, connections -from django.contrib.auth.models import Group -from ..models import User - - -from ..constants import Groups +from django.db import migrations def create_groups(apps, schema_editor): - Group.objects.create(name=Groups.Passenger.name) - Group.objects.create(name=Groups.Driver.name) + Group = apps.get_model("auth", "Group") + + Group.objects.create(name="Passenger") + Group.objects.create(name="Driver") def insert_to_groups(apps, schema_editor): - g = Group.objects.get(name=Groups.Driver.name) + User = apps.get_model("fleet_management", "User") + Group = apps.get_model("auth", "Group") + g = Group.objects.get(name="Driver") users = User.objects.all() for u in users: g.user_set.add(u) diff --git a/backend/fleet_management/migrations/0012_auto_20190810_2109.py b/backend/fleet_management/migrations/0012_auto_20190810_2109.py index 36e1a6a6..e58b19f4 100644 --- a/backend/fleet_management/migrations/0012_auto_20190810_2109.py +++ b/backend/fleet_management/migrations/0012_auto_20190810_2109.py @@ -2,10 +2,9 @@ from django.db import migrations, models -from ..models import Drive - def default_is_verified(apps, schema_editor): + Drive = apps.get_model("fleet_management", "Drive") drives = Drive.objects.all() for drive in drives: drive.is_verified = True diff --git a/backend/fleet_management/migrations/0016_auto_20190825_2051.py b/backend/fleet_management/migrations/0016_auto_20190825_2051.py index bdb46260..0a651f63 100644 --- a/backend/fleet_management/migrations/0016_auto_20190825_2051.py +++ b/backend/fleet_management/migrations/0016_auto_20190825_2051.py @@ -1,8 +1,8 @@ from django.db import migrations -from ..models import Project def default_country_to_project(apps, schema_editor): + Project = apps.get_model("fleet_management", "Project") projects = Project.objects.all() for project in projects: project.country = 'UA' diff --git a/backend/fleet_management/migrations/0017_auto_20191002_1858.py b/backend/fleet_management/migrations/0017_auto_20191002_1858.py new file mode 100644 index 00000000..653d7408 --- /dev/null +++ b/backend/fleet_management/migrations/0017_auto_20191002_1858.py @@ -0,0 +1,31 @@ +# Generated by Django 2.1.2 on 2019-10-02 18:58 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django_countries.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('fleet_management', '0016_auto_20190825_2051'), + ] + + operations = [ + migrations.AddField( + model_name='drive', + name='passenger', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='drives_taken', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='drive', + name='driver', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='drives_driven', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='project', + name='country', + field=django_countries.fields.CountryField(default=None, max_length=2), + ), + ] diff --git a/backend/fleet_management/migrations/0018_passenger_to_user.py b/backend/fleet_management/migrations/0018_passenger_to_user.py new file mode 100644 index 00000000..174bbfb2 --- /dev/null +++ b/backend/fleet_management/migrations/0018_passenger_to_user.py @@ -0,0 +1,44 @@ +# Generated by Django 2.1.2 on 2019-10-02 19:00 + +from django.db import migrations + + +def migrate_passenger_to_user(apps, schema_editor): + Passenger = apps.get_model("fleet_management", "Passenger") + User = apps.get_model("fleet_management", "User") + Group = apps.get_model("auth", "Group") + Drive = apps.get_model("fleet_management", "Drive") + g = Group.objects.get(name="Passenger") + passengers = Passenger.objects.all() + for p in passengers: + u, created = User.objects.get_or_create( + username=p.email, + defaults={ + 'email': p.email, + 'is_superuser': False, + 'is_staff': False, + 'country': p.country, + 'is_active': True, + 'first_name': p.first_name, + 'last_name': p.last_name + } + ) + u.save() + g.user_set.add(u) + + drives = Drive.objects.filter(passengers=p.id) + for d in drives: + assert d.passengers.all().count() == 1, f"Too many passengers in a drive {d.id}" + d.passenger = u + d.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('fleet_management', '0017_auto_20191002_1858'), + ] + + operations = [ + migrations.RunPython(migrate_passenger_to_user), + ] diff --git a/backend/fleet_management/migrations/0019_remove_passenger_model.py b/backend/fleet_management/migrations/0019_remove_passenger_model.py new file mode 100644 index 00000000..bb7e928f --- /dev/null +++ b/backend/fleet_management/migrations/0019_remove_passenger_model.py @@ -0,0 +1,20 @@ +# Generated by Django 2.1.2 on 2019-10-02 21:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('fleet_management', '0018_passenger_to_user'), + ] + + operations = [ + migrations.RemoveField( + model_name='drive', + name='passengers', + ), + migrations.DeleteModel( + name='Passenger', + ), + ] diff --git a/backend/fleet_management/models.py b/backend/fleet_management/models.py index 0cf57663..faa6b7e1 100644 --- a/backend/fleet_management/models.py +++ b/backend/fleet_management/models.py @@ -28,16 +28,6 @@ def __str__(self): return self.plates -class Passenger(models.Model): - first_name = models.CharField(max_length=60, blank=False) - last_name = models.CharField(max_length=60, blank=False) - email = models.EmailField(blank=False) - country = CountryField(blank_label='(select country)', null=True, default=None) - - def __str__(self): - return f"{self.first_name} {self.last_name}" - - class Project(models.Model): title = models.CharField(max_length=50, blank=False) description = models.CharField(max_length=1000, blank=False) @@ -48,9 +38,8 @@ def __str__(self): class Drive(models.Model): - driver = models.ForeignKey(User, on_delete=models.CASCADE) + driver = models.ForeignKey(User, on_delete=models.CASCADE, related_name="drives_driven") car = models.ForeignKey(Car, null=False, on_delete=models.CASCADE) - passengers = models.ManyToManyField(Passenger) date = models.DateField(default=now, blank=False) start_mileage = models.IntegerField(null=False) end_mileage = models.IntegerField(null=False) @@ -60,6 +49,7 @@ class Drive(models.Model): timestamp = models.IntegerField(blank=False, default=get_current_timestamp_in_gmt) project = models.ForeignKey(Project, on_delete=models.CASCADE) is_verified = models.BooleanField(default=False) + passenger = models.ForeignKey(User, on_delete=models.CASCADE, related_name="drives_taken", null=True, blank=True) class Meta: unique_together = [ diff --git a/backend/fleet_management/serializers.py b/backend/fleet_management/serializers.py index c4465a66..19160c23 100644 --- a/backend/fleet_management/serializers.py +++ b/backend/fleet_management/serializers.py @@ -4,7 +4,7 @@ from rest_framework.exceptions import ValidationError -from .models import Car, Drive, Passenger, User, Project +from .models import Car, Drive, User, Project class GroupSerializer(serializers.ModelSerializer): @@ -28,7 +28,7 @@ class PassengerSerializer(serializers.ModelSerializer): last_name = fields.CharField(read_only=True) class Meta: - model = Passenger + model = User fields = ('id', 'first_name', 'last_name') @@ -56,10 +56,18 @@ class Meta: fields = ('title', 'description', 'id') +class PassengersField(serializers.Field): + def to_representation(self, value): + return [PassengerSerializer(value).data] + + def to_internal_value(self, data): + return data[0] + + class DriveSerializer(serializers.ModelSerializer): driver = UserSerializer(read_only=True) car = CarSerializer() - passengers = PassengerSerializer(many=True) + passengers = PassengersField(source="passenger") project = ProjectSerializer() class Meta: @@ -73,14 +81,12 @@ class Meta: read_only_fields = ('is_verified',) def create(self, validated_data): - passengers_data = validated_data.pop('passengers') + passenger_data = validated_data.pop('passenger') car_data = validated_data.pop('car') car = Car.objects.get(pk=car_data['id']) project_data = validated_data.pop('project') project = Project.objects.get(pk=project_data['id']) - passengers = Passenger.objects.filter( - id__in=[p['id'] for p in passengers_data], - ).all() + passenger = User.objects.get(pk=passenger_data['id']) with transaction.atomic(): drive = Drive.objects.create( @@ -89,9 +95,9 @@ def create(self, validated_data): is_verified=True, driver=self.context['driver'], car=car, - project=project + project=project, + passenger=passenger ) - drive.passengers.set(passengers) drive.save() return drive diff --git a/backend/fleet_management/test_drives.py b/backend/fleet_management/test_drives.py index eb4dc607..c65992d7 100644 --- a/backend/fleet_management/test_drives.py +++ b/backend/fleet_management/test_drives.py @@ -9,24 +9,14 @@ from rest_framework.test import APITestCase from fleet_management.constants import Groups -from fleet_management.factories import DriveFactory -from fleet_management.models import Car, Drive, Passenger, Project +from fleet_management.factories import DriveFactory, UserFactory +from fleet_management.models import Car, Drive, Project class DrivesApiTest(APITestCase): - def create_passenger(self, first_name, last_name, email): - return Passenger.objects.create( - first_name=first_name, - last_name=last_name, - email=email, - ) - def setUp(self): self.url = reverse('drives') - self.passengers = [ - self.create_passenger('Mike', 'Melnik', 'mike@melnik.com'), - self.create_passenger('Mykhailo', 'Возняк', 'mik@bo.uk'), - ] + self.passenger = UserFactory.create(groups=[Group.objects.get(name="Passenger")]) self.car = Car.objects.create( plates='FOO 129338', fuel_consumption=8.2, @@ -36,7 +26,6 @@ def setUp(self): description='Project description', country="UA", ) - self.driver = get_user_model().objects.create_user( username='Admin', first_name='John', @@ -49,6 +38,7 @@ def setUp(self): Drive.objects.create( car=self.car, driver=self.driver, + passenger=self.passenger, date=date.today(), start_mileage=200, end_mileage=12123, @@ -58,7 +48,6 @@ def setUp(self): project=self.project, ) ] - self.drives[0].passengers.set(self.passengers) self.drives[0].save() def test_401_for_unlogged_user(self): @@ -71,9 +60,7 @@ def test_get_all_drives(self): self.assertEqual(res.status_code, status.HTTP_200_OK) drives = json.loads(res.content) - self.assertEqual( - drives[0], - { + expected_drive = { 'id': self.drives[0].id, 'date': self.drives[0].date.isoformat(), 'startMileage': self.drives[0].start_mileage, @@ -87,14 +74,9 @@ def test_get_all_drives(self): }, 'passengers': [ { - 'id': self.passengers[0].id, - 'lastName': self.passengers[0].last_name, - 'firstName': self.passengers[0].first_name, - }, - { - 'id': self.passengers[1].id, - 'lastName': self.passengers[1].last_name, - 'firstName': self.passengers[1].first_name, + 'id': self.passenger.id, + 'lastName': self.passenger.last_name, + 'firstName': self.passenger.first_name, }, ], 'driver': { @@ -111,6 +93,9 @@ def test_get_all_drives(self): }, "timestamp": self.drives[0].timestamp, } + self.assertEqual( + drives[0], + expected_drive ) def test_can_retrieve_only_my_drives(self): @@ -149,8 +134,7 @@ def test_can_create_a_drive(self): 'id': self.car.id, }, 'passengers': [ - {'id': self.passengers[0].id}, - {'id': self.passengers[1].id}, + {'id': self.passenger.id}, ], 'date': date.today().isoformat(), 'startMileage': 180000, @@ -168,10 +152,7 @@ def test_can_create_a_drive(self): self.assertEqual(res.status_code, status.HTTP_201_CREATED) drive = Drive.objects.get(pk=res.data['id']) - self.assertSetEqual( - {p.id for p in drive.passengers.all()}, - {p.id for p in self.passengers}, - ) + self.assertEqual(drive.passenger.id, self.passenger.id) self.assertEqual(drive.car.id, self.car.id) self.assertEqual(drive.date.isoformat(), res.data['date']) self.assertEqual(drive.start_mileage, res.data['start_mileage']) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0ba28845..1a7d2861 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2201,7 +2201,7 @@ }, "babel-plugin-syntax-object-rest-spread": { "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "resolved": "http://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", "dev": true },