diff --git a/api/fixtures/agreement_signatures.json b/api/fixtures/agreement_signatures.json new file mode 100644 index 0000000..8a29b8b --- /dev/null +++ b/api/fixtures/agreement_signatures.json @@ -0,0 +1,29 @@ +[ + { + "model": "api.agreementsignature", + "pk": 1, + "fields": { + "contributor": 1, + "agreement_id": "g3d3d3s8dgd3vc37232fef32232df3f3f31fgawf", + "signed_at": "2024-01-02T12:00:00Z" + } + }, + { + "model": "api.agreementsignature", + "pk": 2, + "fields": { + "contributor": 2, + "agreement_id": "g3d3d3s8dgd43vc37232fef0898df3f3f31fgawf", + "signed_at": "2024-01-02T12:00:00Z" + } + }, + { + "model": "api.agreementsignature", + "pk": 3, + "fields": { + "contributor": 3, + "agreement_id": "g379tuehr8dgd43vc37232fef0898df3f3f31fga", + "signed_at": "2024-01-02T12:00:00Z" + } + } +] diff --git a/api/fixtures/contributors.json b/api/fixtures/contributors.json new file mode 100644 index 0000000..da3019f --- /dev/null +++ b/api/fixtures/contributors.json @@ -0,0 +1,35 @@ +[ + { + "model": "api.contributor", + "pk": 1, + "fields": { + "email": "contributor1@gmail.com", + "name": "contributor1", + "location": "Hatfield", + "html_url": "https://github.com/contributor1", + "avatar_url": "https://contributor1.github.io/gravatar-url-generator/#/" + } + }, + { + "model": "api.contributor", + "pk": 2, + "fields": { + "email": "contributor2@gmail.com", + "name": "contributor2", + "location": "Hatfield", + "html_url": "https://github.com/contributor2", + "avatar_url": "https://contributor2.github.io/gravatar-url-generator/#/" + } + }, + { + "model": "api.contributor", + "pk": 3, + "fields": { + "email": "contributor3@gmail.com", + "name": "contributor3", + "location": "Hatfield", + "html_url": "https://github.com/contributor3", + "avatar_url": "https://contributor3.github.io/gravatar-url-generator/#/" + } + } +] diff --git a/api/fixtures/fruits.json b/api/fixtures/fruits.json deleted file mode 100644 index 01d1dd4..0000000 --- a/api/fixtures/fruits.json +++ /dev/null @@ -1,29 +0,0 @@ -[ - { - "model": "api.fruit", - "pk": 1, - "fields": { - "name": "apple", - "is_citrus": false, - "expires_on": "2023-01-01" - } - }, - { - "model": "api.fruit", - "pk": 2, - "fields": { - "name": "banana", - "is_citrus": false, - "expires_on": "2023-01-02" - } - }, - { - "model": "api.fruit", - "pk": 3, - "fields": { - "name": "orange", - "is_citrus": true, - "expires_on": "2023-01-03" - } - } -] diff --git a/api/fixtures/repositories.json b/api/fixtures/repositories.json new file mode 100644 index 0000000..07545f3 --- /dev/null +++ b/api/fixtures/repositories.json @@ -0,0 +1,30 @@ +[ + { + "model": "api.repository", + "pk": 1, + "fields": { + "contributor": 1, + "points": 10, + "gh_id": "10274252" + } + }, + { + "model": "api.repository", + "pk": 2, + "fields": { + "contributor": 2, + "points": 20, + "gh_id": "102097552" + + } + }, + { + "model": "api.repository", + "pk": 3, + "fields": { + "contributor": 3, + "points": 30, + "gh_id": "890732552" + } + } +] diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py index c169fca..b2d21ab 100644 --- a/api/migrations/0001_initial.py +++ b/api/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 3.2.25 on 2024-07-02 15:57 +# Generated by Django 3.2.25 on 2024-07-12 15:33 from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): @@ -12,16 +13,46 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Fruit', + name='Contributor', + fields=[ + ('id', models.IntegerField(help_text="The contributor's GitHub user-ID.", primary_key=True, serialize=False)), + ('email', models.EmailField(max_length=254, verbose_name='email')), + ('name', models.TextField(verbose_name='name')), + ('location', models.TextField(verbose_name='location')), + ('html_url', models.TextField(verbose_name='html url')), + ('avatar_url', models.TextField(verbose_name='avatar url')), + ], + options={ + 'verbose_name': 'contributor', + 'verbose_name_plural': 'contributors', + }, + ), + migrations.CreateModel( + name='Repository', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('gh_id', models.IntegerField(help_text='Github ID of the repo a contributor has contributed to.', verbose_name='GitHub ID')), + ('points', models.IntegerField(default=0, help_text='Story points the contributor closed for this repository.', verbose_name='points')), + ('contributor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.contributor')), + ], + options={ + 'verbose_name': 'repository', + 'verbose_name_plural': 'repositories', + 'unique_together': {('contributor', 'gh_id')}, + }, + ), + migrations.CreateModel( + name='AgreementSignature', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50, verbose_name='name')), - ('is_citrus', models.BooleanField(verbose_name='is citrus')), - ('expires_on', models.DateField(verbose_name='expires on')), + ('agreement_id', models.CharField(help_text='Commit ID of the contribution agreement in workspace.', max_length=40, verbose_name='agreement id')), + ('signed_at', models.DateTimeField(verbose_name='signed at')), + ('contributor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.contributor')), ], options={ - 'verbose_name': 'fruit', - 'verbose_name_plural': 'fruits', + 'verbose_name': 'agreement signature', + 'verbose_name_plural': 'agreement signatures', + 'unique_together': {('contributor', 'agreement_id')}, }, ), ] diff --git a/api/models/__init__.py b/api/models/__init__.py index e1098b7..55bea85 100644 --- a/api/models/__init__.py +++ b/api/models/__init__.py @@ -3,4 +3,6 @@ Created on 02/07/2024 at 11:57:31(+01:00). """ -from .fruit import Fruit +from .agreement_signature import AgreementSignature +from .contributor import Contributor +from .repository import Repository diff --git a/api/models/agreement_signature.py b/api/models/agreement_signature.py new file mode 100644 index 0000000..a8f1815 --- /dev/null +++ b/api/models/agreement_signature.py @@ -0,0 +1,40 @@ +""" +© Ocado Group +Created on 08/07/2024 at 10:48:44(+01:00). +""" + +import typing as t + +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from .contributor import Contributor + +if t.TYPE_CHECKING: + from django_stubs_ext.db.models import TypedModelMeta # pragma: no cover +else: + TypedModelMeta = object + + +class AgreementSignature(models.Model): + """Signature of a contributor signing the agreement""" + + contributor_id: int + contributor = models.ForeignKey(Contributor, on_delete=models.CASCADE) + + agreement_id = models.CharField( + _("agreement id"), + max_length=40, + help_text=_("Commit ID of the contribution agreement in workspace."), + ) + signed_at = models.DateTimeField(_("signed at")) + + class Meta(TypedModelMeta): + unique_together = ["contributor", "agreement_id"] + verbose_name = _("agreement signature") + verbose_name_plural = _("agreement signatures") + + def __str__(self): + cont = f"Contributor {self.contributor.pk} signed" + repo = f"{self.agreement_id[:7]} at {self.signed_at}" + return f"{cont} {repo}" diff --git a/api/models/agreement_signature_test.py b/api/models/agreement_signature_test.py new file mode 100644 index 0000000..22c50b8 --- /dev/null +++ b/api/models/agreement_signature_test.py @@ -0,0 +1,31 @@ +""" +© Ocado Group +Created on 09/07/2024 at 11:43:42(+01:00). +""" + +from codeforlife.tests import ModelTestCase + +from .agreement_signature import AgreementSignature + + +class TestAgreementSignature(ModelTestCase[AgreementSignature]): + """Test the AgreementSignature Model""" + + fixtures = ["contributors", "agreement_signatures"] + + def setUp(self): + self.agreement_signature = AgreementSignature.objects.get(pk=1) + + def test_str(self): + """ + Parsing an agreement-signature instance to a string + that returns the contributor's primary key, + the first 7 characters of the agreement's commit ID + and the timestamp of when the agreement was signed. + """ + commit_id = self.agreement_signature.agreement_id[:7] + time = self.agreement_signature.signed_at + cont = f"Contributor {self.agreement_signature.contributor.pk} signed" + repo = f"{commit_id} at {time}" + expected_str = f"{cont} {repo}" + assert str(self.agreement_signature) == expected_str diff --git a/api/models/contributor.py b/api/models/contributor.py new file mode 100644 index 0000000..124ac8d --- /dev/null +++ b/api/models/contributor.py @@ -0,0 +1,34 @@ +""" +© Ocado Group +Created on 05/07/2024 at 16:18:48(+01:00). +""" + +import typing as t + +from django.db import models +from django.utils.translation import gettext_lazy as _ + +if t.TYPE_CHECKING: + from django_stubs_ext.db.models import TypedModelMeta # pragma: no cover +else: + TypedModelMeta = object + + +class Contributor(models.Model): + """A contributor that contributes to a repo""" + + id = models.IntegerField( + primary_key=True, help_text=_("The contributor's GitHub user-ID.") + ) + email = models.EmailField(_("email")) + name = models.TextField(_("name")) + location = models.TextField(_("location")) + html_url = models.TextField(_("html url")) + avatar_url = models.TextField(_("avatar url")) + + class Meta(TypedModelMeta): + verbose_name = _("contributor") + verbose_name_plural = _("contributors") + + def __str__(self): + return f"{self.name} <{self.email}>" diff --git a/api/models/contributor_test.py b/api/models/contributor_test.py new file mode 100644 index 0000000..909eda1 --- /dev/null +++ b/api/models/contributor_test.py @@ -0,0 +1,25 @@ +""" +© Ocado Group +Created on 09/07/2024 at 09:39:50(+01:00). +""" + +from codeforlife.tests import ModelTestCase + +from .contributor import Contributor + + +class TestContributor(ModelTestCase[Contributor]): + """Test the Contributor Model""" + + fixtures = ["contributors"] + + def setUp(self): + self.contributor = Contributor.objects.get(pk=1) + + def test_str(self): + """ + Parsing a contributor instance to a string returns its name and email. + """ + name = self.contributor.name + email = self.contributor.email + assert str(self.contributor) == f"{name} <{email}>" diff --git a/api/models/fruit.py b/api/models/fruit.py deleted file mode 100644 index 0ab3db2..0000000 --- a/api/models/fruit.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -© Ocado Group -Created on 02/07/2024 at 15:12:59(+01:00). -""" - -import typing as t - -from django.db import models -from django.utils.translation import gettext_lazy as _ - -if t.TYPE_CHECKING: - from django_stubs_ext.db.models import TypedModelMeta -else: - TypedModelMeta = object - - -class Fruit(models.Model): - """A piece of fruit.""" - - name = models.CharField(_("name"), max_length=50) - is_citrus = models.BooleanField(_("is citrus")) - expires_on = models.DateField(_("expires on")) - - class Meta(TypedModelMeta): - verbose_name = _("fruit") - verbose_name_plural = _("fruits") - - def __str__(self): - return self.name - - def __lt__(self, other): - return isinstance(other, Fruit) and self.expires_on < other.expires_on diff --git a/api/models/fruit_test.py b/api/models/fruit_test.py deleted file mode 100644 index b2229be..0000000 --- a/api/models/fruit_test.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -© Ocado Group -Created on 02/07/2024 at 16:17:04(+01:00). -""" - -from codeforlife.tests import ModelTestCase - -from .fruit import Fruit - - -# pylint: disable-next=missing-class-docstring -class TestFruit(ModelTestCase[Fruit]): - fixtures = ["fruits"] - - def setUp(self): - self.apple = Fruit.objects.get(name="apple") - self.banana = Fruit.objects.get(name="banana") - - def test_str(self): - """Parsing a fruit object instance to string returns its name.""" - assert str(self.apple) == self.apple.name - - def test_lt(self): - """ - Comparing if one fruit is less than another compares their expiry dates. - """ - assert self.apple < self.banana - assert not self.banana < self.apple diff --git a/api/models/repository.py b/api/models/repository.py new file mode 100644 index 0000000..80b2e87 --- /dev/null +++ b/api/models/repository.py @@ -0,0 +1,41 @@ +""" +© Ocado Group +Created on 05/07/2024 at 16:39:14(+01:00). +""" + +import typing as t + +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from .contributor import Contributor + +if t.TYPE_CHECKING: + from django_stubs_ext.db.models import TypedModelMeta # pragma: no cover +else: + TypedModelMeta = object + + +class Repository(models.Model): + """A repository that a contributor has contributed to.""" + + contributor_id: int + contributor = models.ForeignKey(Contributor, on_delete=models.CASCADE) + + gh_id = models.IntegerField( + _("GitHub ID"), + help_text=_("Github ID of the repo a contributor has contributed to."), + ) + points = models.IntegerField( + _("points"), + default=0, + help_text=_("Story points the contributor closed for this repository."), + ) + + class Meta(TypedModelMeta): + unique_together = ["contributor", "gh_id"] + verbose_name = _("repository") + verbose_name_plural = _("repositories") + + def __str__(self): + return f"{self.contributor.pk}:{self.gh_id}" diff --git a/api/models/repository_test.py b/api/models/repository_test.py new file mode 100644 index 0000000..145add2 --- /dev/null +++ b/api/models/repository_test.py @@ -0,0 +1,26 @@ +""" +© Ocado Group +Created on 09/07/2024 at 11:43:31(+01:00). +""" + +from codeforlife.tests import ModelTestCase + +from .repository import Repository + + +class TestRepository(ModelTestCase[Repository]): + """Test the Repository Model""" + + fixtures = ["contributors", "repositories"] + + def setUp(self): + self.repository = Repository.objects.get(pk=1) + + def test_str(self): + """ + Parsing a repository instance to a string + returns the contributor's primary key and + the repository's GitHub ID. + """ + expected = f"{self.repository.contributor.pk}:{self.repository.gh_id}" + assert str(self.repository) == expected diff --git a/api/serializers/__init__.py b/api/serializers/__init__.py index 49eb27f..7e05fbc 100644 --- a/api/serializers/__init__.py +++ b/api/serializers/__init__.py @@ -2,5 +2,3 @@ © Ocado Group Created on 02/07/2024 at 12:00:12(+01:00). """ - -from .fruit import FruitSerializer diff --git a/api/serializers/fruit.py b/api/serializers/fruit.py deleted file mode 100644 index 906d6ea..0000000 --- a/api/serializers/fruit.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -© Ocado Group -Created on 02/07/2024 at 15:21:47(+01:00). -""" - -from datetime import timedelta - -from codeforlife.serializers import ModelSerializer -from codeforlife.user.models import User -from django.utils import timezone - -from ..models import Fruit - - -# pylint: disable-next=missing-class-docstring -class FruitSerializer(ModelSerializer[User, Fruit]): - class Meta: - model = Fruit - fields = ["id", "name", "is_citrus", "expires_on"] - extra_kwargs = { - "id": {"read_only": True}, - "expires_on": {"read_only": True}, - } - - def create(self, validated_data): - validated_data["expires_on"] = timezone.now().date() + timedelta(days=3) - return super().create(validated_data) diff --git a/api/serializers/fruit_test.py b/api/serializers/fruit_test.py deleted file mode 100644 index cf84019..0000000 --- a/api/serializers/fruit_test.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -© Ocado Group -Created on 02/07/2024 at 16:27:55(+01:00). -""" - -from datetime import timedelta -from unittest.mock import patch - -from codeforlife.tests import ModelSerializerTestCase -from codeforlife.user.models import User -from django.utils import timezone - -from ..models import Fruit -from .fruit import FruitSerializer - - -# pylint: disable-next=missing-class-docstring -class TestFruitSerializer(ModelSerializerTestCase[User, Fruit]): - model_serializer_class = FruitSerializer - - def test_create(self): - """Creating a fruit instance sets the expiry date to 3 days from now.""" - now = timezone.now() - with patch.object(timezone, "now", return_value=now): - self.assert_create( - validated_data={"name": "kiwi", "is_citrus": False}, - new_data={"expires_on": now.date() + timedelta(days=3)}, - ) diff --git a/api/urls.py b/api/urls.py index 735d6b8..13b885f 100644 --- a/api/urls.py +++ b/api/urls.py @@ -19,13 +19,6 @@ from codeforlife.urls import get_urlpatterns from rest_framework.routers import DefaultRouter -from .views import FruitViewSet - router = DefaultRouter() -router.register( - "fruits", - FruitViewSet, - basename="fruit", -) urlpatterns = get_urlpatterns(router.urls) diff --git a/api/views/__init__.py b/api/views/__init__.py index 435c50f..e7f7f62 100644 --- a/api/views/__init__.py +++ b/api/views/__init__.py @@ -2,5 +2,3 @@ © Ocado Group Created on 02/07/2024 at 11:59:45(+01:00). """ - -from .fruit import FruitViewSet diff --git a/api/views/fruit.py b/api/views/fruit.py deleted file mode 100644 index 092b1d5..0000000 --- a/api/views/fruit.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -© Ocado Group -Created on 02/07/2024 at 15:24:20(+01:00). -""" - -from codeforlife.permissions import AllowAny -from codeforlife.user.models import User -from codeforlife.views import ModelViewSet - -from ..models import Fruit -from ..serializers import FruitSerializer - - -# pylint: disable-next=missing-class-docstring,too-many-ancestors -class FruitViewSet(ModelViewSet[User, Fruit]): - http_method_names = ["get", "post"] - permission_classes = [AllowAny] - serializer_class = FruitSerializer - queryset = Fruit.objects.all() diff --git a/api/views/fruit_test.py b/api/views/fruit_test.py deleted file mode 100644 index 4b4094b..0000000 --- a/api/views/fruit_test.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -© Ocado Group -Created on 02/07/2024 at 16:38:57(+01:00). -""" - -from codeforlife.tests import ModelViewSetTestCase -from codeforlife.user.models import User - -from ..models import Fruit -from .fruit import FruitViewSet - - -# pylint: disable-next=missing-class-docstring,too-many-ancestors -class TestFruitViewSet(ModelViewSetTestCase[User, Fruit]): - basename = "fruit" - model_view_set_class = FruitViewSet - fixtures = ["fruits"] - - def setUp(self): - self.apple = Fruit.objects.get(name="apple") - self.banana = Fruit.objects.get(name="banana") - self.orange = Fruit.objects.get(name="orange") - - def test_list(self): - """Can list all fruits.""" - self.client.list(models=[self.apple, self.banana, self.orange]) - - def test_retrieve(self): - """Can retrieve a single fruit.""" - self.client.retrieve(model=self.apple) - - def test_create(self): - """Can create a fruit.""" - self.client.create(data={"name": "kiwi", "is_citrus": False})