Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨(models/api) add models and API endpoints for contract #358

Merged
merged 3 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to

## Added

- Add contract and contract definition models with related API endpoints
- Add `instructions` markdown field to `Product` model
- Add filter course by product type
- Enroll as "verified" mode in OpenEdX when enrolling via an order
Expand Down
25 changes: 25 additions & 0 deletions src/backend/joanie/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,31 @@ class CourseRunFilter(AutocompleteFilter):
# Admin registers


@admin.register(models.ContractDefinition)
class ContractDefinitionAdmin(TranslatableAdmin):
"""Admin class for the ContractDefinition model"""

list_display = ("title", "language")


@admin.register(models.Contract)
class ContractAdmin(admin.ModelAdmin):
"""Admin class for the Contract model"""

list_display = ("order", "owner", "signed_on")
readonly_fields = (
"definition",
"definition_checksum",
"signed_on",
"order",
"owner",
)

def owner(self, obj): # pylint: disable=no-self-use
"""Retrieve the owner of the contract from the related order."""
return obj.order.owner


@admin.register(models.CertificateDefinition)
class CertificateDefinitionAdmin(TranslatableAdmin):
"""Admin class for the CertificateDefinition model"""
Expand Down
11 changes: 11 additions & 0 deletions src/backend/joanie/core/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,14 @@ def reorder(
product=product_id, course=target_course_id
).update(position=index)
return Response(status=201)


class ContractDefinitionViewSet(viewsets.ModelViewSet):
"""
Admin Contract Definition ViewSet
"""

authentication_classes = [SessionAuthenticationWithAuthenticateHeader]
permission_classes = [permissions.IsAdminUser & permissions.DjangoModelPermissions]
serializer_class = serializers.AdminContractDefinitionSerializer
queryset = models.ContractDefinition.objects.all()
35 changes: 33 additions & 2 deletions src/backend/joanie/core/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from joanie.core import enums, filters, models, permissions, serializers
from joanie.payment.models import Invoice

# pylint: disable=too-many-ancestors


class Pagination(pagination.PageNumberPagination):
"""Pagination to display no more than 100 objects per page sorted by creation date."""
Expand Down Expand Up @@ -85,6 +87,7 @@ class CourseProductRelationViewSet(
.select_related(
"course",
"product",
sampaccoud marked this conversation as resolved.
Show resolved Hide resolved
"product__contract_definition",
"product__certificate_definition",
)
.prefetch_related("organizations")
Expand Down Expand Up @@ -228,7 +231,7 @@ def get_queryset(self):
Prefetch(
"course_run__course__product_relations",
queryset=models.CourseProductRelation.objects.select_related(
"product"
"product", "product__contract_definition"
).filter(product__type=enums.PRODUCT_TYPE_CERTIFICATE),
to_attr="certificate_product_relations",
),
Expand Down Expand Up @@ -286,7 +289,7 @@ def get_queryset(self):
else self.request.user.username
)
return models.Order.objects.filter(owner__username=username).select_related(
"owner", "product", "certificate", "course"
"certificate", "contract", "course", "owner", "product"
)

def perform_create(self, serializer):
Expand Down Expand Up @@ -920,3 +923,31 @@ def get_me(self, request):
"""
context = {"request": request}
return Response(self.serializer_class(request.user, context=context).data)


class ContractViewSet(
mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
"""
API views to get all contracts for a user

GET /api/contracts/:contract_id
Return list of all contracts for a user or one contract if an id is
provided.
"""

lookup_field = "pk"
sampaccoud marked this conversation as resolved.
Show resolved Hide resolved
pagination_class = Pagination
permission_classes = [permissions.IsAuthenticated]
serializer_class = serializers.ContractSerializer

def get_queryset(self):
"""
Custom queryset to get user contracts
"""
username = (
self.request.auth["username"]
if self.request.auth
else self.request.user.username
)
return models.Contract.objects.filter(order__owner__username=username)
5 changes: 5 additions & 0 deletions src/backend/joanie/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,8 @@
pgettext_lazy("As in: the enrollment failed on the LMS.", "Failed"),
),
)

# For contract names choices
CONTRACT_DEFINITION = "contract_definition"

CONTRACT_NAME_CHOICES = ((CONTRACT_DEFINITION, _("Contract Definition")),) # default
58 changes: 55 additions & 3 deletions src/backend/joanie/core/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ class Meta:
template = settings.MARION_CERTIFICATE_DOCUMENT_ISSUER


class ContractDefinitionFactory(factory.django.DjangoModelFactory):
"""A factory to create a contract definition"""

class Meta:
model = models.ContractDefinition

body = factory.Faker("paragraphs", nb=3)
description = factory.Faker("paragraph", nb_sentences=5)
language = factory.fuzzy.FuzzyChoice([lang[0] for lang in settings.LANGUAGES])
name = factory.fuzzy.FuzzyChoice([name[0] for name in enums.CONTRACT_NAME_CHOICES])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder : I should do the same for certificate_definition

title = factory.Sequence(lambda n: f"Contract definition {n}")


class OrganizationFactory(factory.django.DjangoModelFactory):
"""A factory to create an organization"""

Expand Down Expand Up @@ -198,7 +211,7 @@ class Params:
"""Parameters for the factory."""

state = None
ref_date = django_timezone.now()
ref_date = factory.LazyAttribute(lambda o: django_timezone.now())

class Meta:
model = models.CourseRun
Expand Down Expand Up @@ -295,11 +308,18 @@ def end(self):
elif self.state in [CourseState.ONGOING_OPEN, CourseState.ONGOING_CLOSED]:
# The course run is on going, end date must be greater than ref_date
min_date = self.ref_date
max_date = self.ref_date + period
max_date = min_date + period
elif self.state in [
CourseState.FUTURE_NOT_YET_OPEN,
CourseState.FUTURE_OPEN,
CourseState.FUTURE_CLOSED,
]:
min_date = max(self.ref_date, self.start)
max_date = min_date + period
else:
# Otherwise, we just want end date to be greater than start date
min_date = self.start
max_date = self.start + period
max_date = min_date + period

return datetime.utcfromtimestamp(
random.randrange( # nosec
Expand Down Expand Up @@ -460,6 +480,19 @@ def certificate_definition(self):

return CertificateDefinitionFactory()

@factory.lazy_attribute
def contract_definition(self):
"""
Return a ContractDefinition object with a random title
if the product type is credential or certificate.
"""
if self.type in [
enums.PRODUCT_TYPE_CREDENTIAL,
enums.PRODUCT_TYPE_CERTIFICATE,
]:
return ContractDefinitionFactory()
return None


class CourseProductRelationFactory(factory.django.DjangoModelFactory):
"""A factory to create CourseProductRelation object"""
Expand Down Expand Up @@ -614,3 +647,22 @@ class Meta:

course = factory.SubFactory(CourseFactory)
owner = factory.SubFactory(UserFactory)


class ContractFactory(factory.django.DjangoModelFactory):
"""A factory to create a contract"""

class Meta:
model = models.Contract

order = factory.SubFactory(
OrderFactory,
product__type=enums.PRODUCT_TYPE_CREDENTIAL,
)

@factory.lazy_attribute
def definition(self):
"""
Return the order product contract definition.
"""
return self.order.product.contract_definition
Loading