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:
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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']),
|
||||
]
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -5283,3 +5283,5 @@ ENTERPRISE_PLOTLY_SECRET = "I am a secret"
|
||||
############## PLOTLY ##############
|
||||
|
||||
ENTERPRISE_MANUAL_REPORTING_CUSTOMER_UUIDS = []
|
||||
|
||||
AVAILABLE_DISCUSSION_TOURS = []
|
||||
|
||||
@@ -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', [])
|
||||
|
||||
Reference in New Issue
Block a user