feat: added apis for discussions tours (#31495)

* feat: added apis for discussions tours

* fix: resolved linter issues

* feat: moved config list to settings and fixed unit tests

* refactor: merged migrations in 1 file
This commit is contained in:
Ahtisham Shahid
2023-01-17 15:59:26 +05:00
committed by GitHub
parent cee6f22acd
commit 0870e1a758
8 changed files with 245 additions and 15 deletions

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.2.16 on 2023-01-10 09:05
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('user_tours', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='UserDiscussionsTours',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tour_name', models.CharField(max_length=255)),
('show_tour', models.BooleanField(default=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddIndex(
model_name='userdiscussionstours',
index=models.Index(fields=['user', 'tour_name'], name='user_tours__user_id_0397d5_idx'),
),
]

View File

@@ -25,3 +25,17 @@ class UserTour(models.Model):
)
show_courseware_tour = models.BooleanField(default=True)
user = models.OneToOneField(User, related_name='tour', on_delete=models.CASCADE)
class UserDiscussionsTours(models.Model):
"""
Model to track which discussions tours a user has seen.
"""
tour_name = models.CharField(max_length=255)
show_tour = models.BooleanField(default=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
indexes = [
models.Index(fields=['user', 'tour_name']),
]

View File

@@ -3,9 +3,10 @@
from django.conf import settings
from django.urls import re_path
from lms.djangoapps.user_tours.v1.views import UserTourView
from lms.djangoapps.user_tours.v1.views import UserTourView, UserDiscussionsToursView
urlpatterns = [
re_path(fr'^v1/{settings.USERNAME_PATTERN}$', UserTourView.as_view(), name='user-tours'),
re_path(r'^discussion_tours/(?P<tour_id>\d+)?/?$', UserDiscussionsToursView.as_view(), name='discussion-tours'),
]

View File

@@ -2,10 +2,29 @@
from rest_framework import serializers
from lms.djangoapps.user_tours.models import UserTour
from lms.djangoapps.user_tours.models import UserTour, UserDiscussionsTours
class UserTourSerializer(serializers.ModelSerializer):
class Meta:
model = UserTour
fields = ['course_home_tour_status', 'show_courseware_tour']
class UserDiscussionsToursSerializer(serializers.ModelSerializer):
"""
Serializer for UserDiscussionsTours model.
"""
id = serializers.IntegerField(read_only=True)
tour_name = serializers.CharField(read_only=True)
user = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = UserDiscussionsTours
fields = ['id', 'tour_name', 'show_tour', 'user']
def to_representation(self, instance):
# Convert the status field to a boolean value
instance.show_tour = bool(instance.show_tour)
return super().to_representation(instance)

View File

@@ -3,21 +3,29 @@
import ddt
from django.contrib.auth import get_user_model
from django.db.models.signals import post_save
from django.test import TestCase
from django.test import TestCase, override_settings
from django.urls import reverse
from rest_framework import status
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.user_tours.handlers import init_user_tour
from lms.djangoapps.user_tours.models import UserTour
from lms.djangoapps.user_tours.models import UserTour, UserDiscussionsTours
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
User = get_user_model()
def build_jwt_headers(user):
""" Helper function for creating headers for the JWT authentication. """
token = create_jwt_for_user(user)
headers = {'HTTP_AUTHORIZATION': f'JWT {token}'}
return headers
@ddt.ddt
class TestUserTourView(TestCase):
""" Tests for the v1 User Tour views. """
def setUp(self):
""" Test set up. """
super().setUp()
@@ -30,15 +38,9 @@ class TestUserTourView(TestCase):
self.staff_user = UserFactory(is_staff=True)
self.new_user_tour = self.staff_user.tour
def build_jwt_headers(self, user):
""" Helper function for creating headers for the JWT authentication. """
token = create_jwt_for_user(user)
headers = {'HTTP_AUTHORIZATION': f'JWT {token}'}
return headers
def send_request(self, jwt_user, request_user, method, data=None):
""" Helper function to call API. """
headers = self.build_jwt_headers(jwt_user)
headers = build_jwt_headers(jwt_user)
url = reverse('user-tours', args=[request_user.username])
if method == 'GET':
return self.client.get(url, **headers)
@@ -142,7 +144,74 @@ class TestUserTourView(TestCase):
def test_put_not_supported(self):
""" Test PUT request returns method not supported. """
headers = self.build_jwt_headers(self.staff_user)
headers = build_jwt_headers(self.staff_user)
url = reverse('user-tours', args=[self.staff_user.username])
response = self.client.put(url, data={}, content_type='application/json', **headers)
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
@override_settings(AVAILABLE_DISCUSSION_TOURS=['not_responded_filter'])
class UserDiscussionsToursViewTestCase(TestCase):
"""
Tests for the UserDiscussionsToursView view.
"""
def setUp(self):
# Create a user and a tour for testing
self.user = User.objects.create_user(
username='testuser',
password='testpass'
)
# create a UserDiscussionsTour for the user
self.tour = UserDiscussionsTours.objects.create(
tour_name='Test Tour',
show_tour=False,
user=self.user
)
self.url = reverse('discussion-tours')
def test_get_tours(self):
"""
Test GET request for a user's discussion tours.
"""
# create a header with our user's credentials
headers = build_jwt_headers(self.user)
# Send a GET request to the view
response = self.client.get(self.url, **headers)
# Check that the response status code is correct
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Check that the returned data is correct
self.assertEqual(len(response.data), 2)
self.assertEqual(response.data[0]['tour_name'], 'Test Tour')
self.assertFalse(response.data[0]['show_tour'])
self.assertEqual(response.data[1]['tour_name'], 'not_responded_filter')
self.assertTrue(response.data[1]['show_tour'])
def test_get_tours_unauthenticated(self):
"""
Test that an unauthenticated user cannot access the discussion tours endpoint.
"""
# Send a GET request to the view without logging in
response = self.client.get(self.url)
# Check that the response status code is correct
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_update_tour(self):
"""
Test that a user can update their discussion tour status.
"""
headers = build_jwt_headers(self.user)
# Send a PUT request to the view with the updated tour data
updated_data = {'show_tour': False}
url = reverse('discussion-tours', args=[self.tour.id])
response = self.client.put(url, updated_data, content_type='application/json', **headers)
# Check that the response status code is correct
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Check that the tour was updated in the database
updated_tour = UserDiscussionsTours.objects.get(id=self.tour.id)
self.assertEqual(updated_tour.show_tour, False)

View File

@@ -1,13 +1,18 @@
""" API for User Tours. """
from django.conf import settings
from django.db import transaction, IntegrityError
from django.shortcuts import get_object_or_404
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from rest_framework.authentication import SessionAuthentication
from rest_framework.generics import RetrieveUpdateAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from lms.djangoapps.user_tours.models import UserTour
from lms.djangoapps.user_tours.v1.serializers import UserTourSerializer
from lms.djangoapps.user_tours.models import UserTour, UserDiscussionsTours
from lms.djangoapps.user_tours.v1.serializers import UserTourSerializer, UserDiscussionsToursSerializer
from rest_framework.views import APIView
class UserTourView(RetrieveUpdateAPIView):
@@ -77,3 +82,91 @@ class UserTourView(RetrieveUpdateAPIView):
def put(self, *_args, **_kwargs):
""" Unsupported method. """
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
class UserDiscussionsToursView(APIView):
"""
Supports retrieving and patching the UserDiscussionsTours model
returns a list of available tours and their status
**Example Requests**
GET /api/user_tours/v1/discussions/
PUT /api/user_tours/v1/discussions/{tour_id}
**Example Response**:
[
{
"id": 1,
"tour_name": "discussions",
"show_tour": true,
"user": 1
}
]
"""
authentication_classes = (JwtAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
def get(self, request, tour_id=None):
"""
Return a list of all tours in the database.
Parameters:
request (Request): The request object
tour_id (int): The ID of the tour to be retrieved.
Returns:
200: A list of tours, serialized using the UserDiscussionsToursSerializer
[
{
"id": 1,
"tour_name": "discussions",
"show_tour": true,
"user": 1
}
]
"""
try:
with transaction.atomic():
tours = UserDiscussionsTours.objects.filter(user=request.user)
tours_to_create = []
for tour_name in settings.AVAILABLE_DISCUSSION_TOURS:
if tour_name not in [tour.tour_name for tour in tours]:
tours_to_create.append(UserDiscussionsTours(
tour_name=tour_name,
user=request.user,
show_tour=True
))
UserDiscussionsTours.objects.bulk_create(tours_to_create)
tours = UserDiscussionsTours.objects.filter(user=request.user)
serializer = UserDiscussionsToursSerializer(tours, many=True)
return Response(serializer.data)
except IntegrityError:
return Response(status=status.HTTP_409_CONFLICT)
def put(self, request, tour_id):
"""
Update an existing tour with the data in the request body.
Parameters:
request (Request): The request object
tour_id (int): The ID of the tour to be updated.
Returns:
200: The updated tour, serialized using the UserDiscussionsToursSerializer
404: If the tour does not exist
403: If the user does not have permission to update the tour
400: Validation error
"""
tour = get_object_or_404(UserDiscussionsTours, pk=tour_id)
if tour.user != request.user:
return Response(status=status.HTTP_403_FORBIDDEN)
serializer = UserDiscussionsToursSerializer(tour, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@@ -5283,3 +5283,5 @@ ENTERPRISE_PLOTLY_SECRET = "I am a secret"
############## PLOTLY ##############
ENTERPRISE_MANUAL_REPORTING_CUSTOMER_UUIDS = []
AVAILABLE_DISCUSSION_TOURS = []

View File

@@ -1083,3 +1083,6 @@ COURSE_LIVE_GLOBAL_CREDENTIALS["BIG_BLUE_BUTTON"] = {
############## Settings for survey report ##############
SURVEY_REPORT_EXTRA_DATA = ENV_TOKENS.get('SURVEY_REPORT_EXTRA_DATA', {})
AVAILABLE_DISCUSSION_TOURS = ENV_TOKENS.get('AVAILABLE_DISCUSSION_TOURS', [])