Skip to content

Commit

Permalink
✨(models/api) add models and endpoints to user wishlist
Browse files Browse the repository at this point in the history
This commit is the first part of resolving issue 196 (course
wishes). We add a CourseWish model and an API endpoint as
action on the existing course endpoint.

co-authored with Morgane Alonso <[email protected]>
  • Loading branch information
sampaccoud committed Jul 7, 2023
1 parent 20c8465 commit 71a58e4
Show file tree
Hide file tree
Showing 8 changed files with 623 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ and this project adheres to
- Rename certificate field into certificate_definition for the ProductSerializer
- Improve certificate serializer
- Upgrade to Django 4.2
- Add model and API endpoint for course wishes

### Removed

Expand Down
19 changes: 19 additions & 0 deletions src/backend/joanie/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,3 +548,22 @@ def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
extra_context["subtitle"] = _("To get results, choose an owner on the right")
return super().changelist_view(request, extra_context=extra_context)


@admin.register(models.CourseWish)
class CourseWishAdmin(admin.ModelAdmin):
"""Admin class for the CourseWish model"""

list_display = (
"course",
"owner",
)
list_filter = [CourseFilter, OwnerFilter]
readonly_fields = ("id",)
search_fields = [
"owner__last_name",
"owner__username",
"owner__email",
"course__code",
"course__title",
]
39 changes: 34 additions & 5 deletions src/backend/joanie/core/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,24 +629,24 @@ class CourseAccessViewSet(
"""
API ViewSet for all interactions with course accesses.
GET /api/course/<course_id>/accesses/:<course_access_id>
GET /api/courses/<course_id>/accesses/:<course_access_id>
Return list of all course accesses related to the logged-in user or one
course access if an id is provided.
POST /api/<course_id>/accesses/ with expected data:
POST /api/courses/<course_id>/accesses/ with expected data:
- user: str
- role: str [owner|admin|member]
Return newly created course access
PUT /api/<course_id>/accesses/<course_access_id>/ with expected data:
PUT /api/courses/<course_id>/accesses/<course_access_id>/ with expected data:
- role: str [owner|admin|member]
Return updated course access
PATCH /api/<course_id>/accesses/<course_access_id>/ with expected data:
PATCH /api/courses/<course_id>/accesses/<course_access_id>/ with expected data:
- role: str [owner|admin|member]
Return partially updated course access
DELETE /api/<course_id>/accesses/<course_access_id>/
DELETE /api/courses/<course_id>/accesses/<course_access_id>/
Delete targeted course access
"""

Expand Down Expand Up @@ -704,9 +704,19 @@ class CourseViewSet(
GET /api/courses/:<course_id>
Return one course if an id is provided.
GET /api/courses/:<course_id>/wish
Return wish status on this course for the authenticated user
POST /api/courses/:<course_id>/wish
Confirm a wish on this course for the authenticated user
DELETE /api/courses/:<course_id>/wish
Delete any existing wish on this course for the authenticated user
"""

lookup_field = "pk"
lookup_value_regex = "[0-9a-z-]*"
filterset_class = filters.CourseViewSetFilter
pagination_class = Pagination
permission_classes = [permissions.AccessPermission]
Expand Down Expand Up @@ -737,3 +747,22 @@ def get_queryset(self):
return courses.annotate(user_role=Subquery(user_role_query)).prefetch_related(
"organizations", "products", "course_runs"
)

@action(
detail=True,
methods=["post", "get", "delete"],
permission_classes=[permissions.IsAuthenticated],
)
# pylint: disable=invalid-name
def wish(self, request, pk=None):
"""Action to handle the wish on this course for the logged-in user."""
params = {"course": models.Course(pk=pk), "owner": request.user}
if request.method == "POST":
models.CourseWish.objects.get_or_create(**params)
is_wished = True
elif request.method == "DELETE":
models.CourseWish.objects.filter(**params).delete()
is_wished = False
else:
is_wished = models.CourseWish.objects.filter(**params).exists()
return Response({"status": is_wished})
10 changes: 10 additions & 0 deletions src/backend/joanie/core/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,3 +474,13 @@ def certificate_definition(self):
Return the order product certificate definition.
"""
return self.order.product.certificate_definition


class CourseWishFactory(factory.django.DjangoModelFactory):
"""A factory to create a course wish for a user."""

class Meta:
model = models.CourseWish

course = factory.SubFactory(CourseFactory)
owner = factory.SubFactory(UserFactory)
257 changes: 257 additions & 0 deletions src/backend/joanie/core/migrations/0006_add_coursewish.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
# Generated by Django 4.2.2 on 2023-07-04 17:22

import uuid

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models

import joanie.core.fields.multiselect


class Migration(migrations.Migration):
dependencies = [
("core", "0005_courseproductrelation_add_max_validated_orders"),
]

operations = [
migrations.CreateModel(
name="CourseWish",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_on",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_on",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
],
options={
"verbose_name": "Course Wish",
"verbose_name_plural": "Course Wishes",
"db_table": "joanie_course_wish",
},
),
migrations.AlterModelOptions(
name="certificatedefinition",
options={
"ordering": ["-created_on"],
"verbose_name": "Certificate definition",
"verbose_name_plural": "Certificate definitions",
},
),
migrations.AlterModelOptions(
name="courseaccess",
options={
"ordering": ["-created_on"],
"verbose_name": "Course access",
"verbose_name_plural": "Course accesses",
},
),
migrations.AlterModelOptions(
name="courseproductrelation",
options={
"ordering": ["-created_on"],
"verbose_name": "Course relation to a product",
"verbose_name_plural": "Courses relations to products",
},
),
migrations.AlterModelOptions(
name="courserun",
options={
"ordering": ["-created_on"],
"verbose_name": "Course run",
"verbose_name_plural": "Course runs",
},
),
migrations.AlterModelOptions(
name="organization",
options={
"ordering": ["-created_on"],
"verbose_name": "Organization",
"verbose_name_plural": "Organizations",
},
),
migrations.AlterModelOptions(
name="organizationaccess",
options={
"ordering": ["-created_on"],
"verbose_name": "Organization access",
"verbose_name_plural": "Organization accesses",
},
),
migrations.AlterModelOptions(
name="product",
options={
"ordering": ["-created_on"],
"verbose_name": "Product",
"verbose_name_plural": "Products",
},
),
migrations.RemoveConstraint(
model_name="order",
name="unique_owner_product_not_canceled",
),
migrations.AlterField(
model_name="courserun",
name="languages",
field=joanie.core.fields.multiselect.MultiSelectField(
choices=[
("af", "Afrikaans"),
("ar", "Arabic"),
("ar-dz", "Algerian Arabic"),
("ast", "Asturian"),
("az", "Azerbaijani"),
("bg", "Bulgarian"),
("be", "Belarusian"),
("bn", "Bengali"),
("br", "Breton"),
("bs", "Bosnian"),
("ca", "Catalan"),
("ckb", "Central Kurdish (Sorani)"),
("cs", "Czech"),
("cy", "Welsh"),
("da", "Danish"),
("de", "German"),
("dsb", "Lower Sorbian"),
("el", "Greek"),
("en", "English"),
("en-au", "Australian English"),
("en-gb", "British English"),
("eo", "Esperanto"),
("es", "Spanish"),
("es-ar", "Argentinian Spanish"),
("es-co", "Colombian Spanish"),
("es-mx", "Mexican Spanish"),
("es-ni", "Nicaraguan Spanish"),
("es-ve", "Venezuelan Spanish"),
("et", "Estonian"),
("eu", "Basque"),
("fa", "Persian"),
("fi", "Finnish"),
("fr", "French"),
("fy", "Frisian"),
("ga", "Irish"),
("gd", "Scottish Gaelic"),
("gl", "Galician"),
("he", "Hebrew"),
("hi", "Hindi"),
("hr", "Croatian"),
("hsb", "Upper Sorbian"),
("hu", "Hungarian"),
("hy", "Armenian"),
("ia", "Interlingua"),
("id", "Indonesian"),
("ig", "Igbo"),
("io", "Ido"),
("is", "Icelandic"),
("it", "Italian"),
("ja", "Japanese"),
("ka", "Georgian"),
("kab", "Kabyle"),
("kk", "Kazakh"),
("km", "Khmer"),
("kn", "Kannada"),
("ko", "Korean"),
("ky", "Kyrgyz"),
("lb", "Luxembourgish"),
("lt", "Lithuanian"),
("lv", "Latvian"),
("mk", "Macedonian"),
("ml", "Malayalam"),
("mn", "Mongolian"),
("mr", "Marathi"),
("ms", "Malay"),
("my", "Burmese"),
("nb", "Norwegian Bokmål"),
("ne", "Nepali"),
("nl", "Dutch"),
("nn", "Norwegian Nynorsk"),
("os", "Ossetic"),
("pa", "Punjabi"),
("pl", "Polish"),
("pt", "Portuguese"),
("pt-br", "Brazilian Portuguese"),
("ro", "Romanian"),
("ru", "Russian"),
("sk", "Slovak"),
("sl", "Slovenian"),
("sq", "Albanian"),
("sr", "Serbian"),
("sr-latn", "Serbian Latin"),
("sv", "Swedish"),
("sw", "Swahili"),
("ta", "Tamil"),
("te", "Telugu"),
("tg", "Tajik"),
("th", "Thai"),
("tk", "Turkmen"),
("tr", "Turkish"),
("tt", "Tatar"),
("udm", "Udmurt"),
("uk", "Ukrainian"),
("ur", "Urdu"),
("uz", "Uzbek"),
("vi", "Vietnamese"),
("zh-hans", "Simplified Chinese"),
("zh-hant", "Traditional Chinese"),
],
help_text="The list of languages in which the course content is available.",
max_choices=50,
max_length=255,
),
),
migrations.AddConstraint(
model_name="order",
constraint=models.UniqueConstraint(
condition=models.Q(("state", "canceled"), _negated=True),
fields=("course", "owner", "product"),
name="unique_owner_product_not_canceled",
violation_error_message="An order for this product and course already exists.",
),
),
migrations.AddField(
model_name="coursewish",
name="course",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="wishes",
to="core.course",
verbose_name="Course",
),
),
migrations.AddField(
model_name="coursewish",
name="owner",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="course_wishes",
to=settings.AUTH_USER_MODEL,
verbose_name="Owner",
),
),
migrations.AlterUniqueTogether(
name="coursewish",
unique_together={("owner", "course")},
),
]
1 change: 1 addition & 0 deletions src/backend/joanie/core/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@

from .accounts import *
from .certifications import *
from .course_wishes import *
from .courses import *
from .products import *
Loading

0 comments on commit 71a58e4

Please sign in to comment.