Commit edec9391 by muhammad-ammar Committed by Muhammad Ammar

paginate courses list page on backend

EDUCATOR-882
parent 81a485b0
"""
Publisher courses serializers.
"""
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from django.utils.translation import ugettext as _
from rest_framework import serializers
from course_discovery.apps.publisher.mixins import check_course_organization_permission
from course_discovery.apps.publisher.models import OrganizationExtension
from course_discovery.apps.publisher.utils import has_role_for_course
class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Publisher courses list serializer.
"""
course_title = serializers.SerializerMethodField()
organization_name = serializers.SerializerMethodField()
project_coordinator_name = serializers.SerializerMethodField()
publisher_course_runs_count = serializers.SerializerMethodField()
course_team_status = serializers.SerializerMethodField()
internal_user_status = serializers.SerializerMethodField()
edit_url = serializers.SerializerMethodField()
def get_course_title(self, course):
"""
Returns a dict containing course `title` and `url`.
"""
publisher_hide_features_for_pilot = self.context['publisher_hide_features_for_pilot']
return {
'title': course.title,
'url': None if publisher_hide_features_for_pilot else reverse(
'publisher:publisher_course_detail', kwargs={'pk': course.id}
)
}
def get_organization_name(self, course):
"""
Returns course organization name.
"""
return course.organization_name
def get_project_coordinator_name(self, course):
"""
Returns course project coordinator name.
"""
project_coordinator = course.project_coordinator
return project_coordinator.full_name if project_coordinator else ''
def get_publisher_course_runs_count(self, course):
"""
Returns count of course runs for a course.
"""
try:
return course.publisher_course_runs.count()
except ObjectDoesNotExist:
return 0
def get_course_team_status(self, course):
"""
Returns a dict containing `status` and `date` for course team status.
"""
try:
course_team_status = course.course_state.course_team_status
except ObjectDoesNotExist:
return {
'status': '',
'date': ''
}
course_team_status_date = course_team_status.get('date', '')
return {
'status': course_team_status.get('status_text', ''),
'date': course_team_status_date and course_team_status_date.strftime('%m/%d/%y')
}
def get_internal_user_status(self, course):
"""
Returns a dict containing `status` and `date` for internal user status.
"""
try:
internal_user_status = course.course_state.internal_user_status
except ObjectDoesNotExist:
return {
'status': '',
'date': ''
}
internal_user_status_date = internal_user_status.get('date', '')
return {
'status': internal_user_status.get('status_text', ''),
'date': internal_user_status_date and internal_user_status_date.strftime('%m/%d/%y')
}
def get_edit_url(self, course):
"""
Returns a dict containing `title` and `url` to edit a course.
"""
courses_edit_url = None
publisher_hide_features_for_pilot = self.context['publisher_hide_features_for_pilot']
if not publisher_hide_features_for_pilot and self.can_edit_course(course, self.context['user']):
courses_edit_url = reverse('publisher:publisher_courses_edit', kwargs={'pk': course.id})
return {
'title': _('Edit'),
'url': courses_edit_url
}
@classmethod
def can_edit_course(cls, course, user):
"""
Check if user has permissions on course.
Arguments:
course: course instance to be serialized
user: currently logedin user
Returns:
bool: Whether the logedin user has permission or not.
"""
try:
return check_course_organization_permission(
user, course, OrganizationExtension.EDIT_COURSE
) and has_role_for_course(course, user)
except ObjectDoesNotExist:
return False
from django import template
from course_discovery.apps.publisher.mixins import check_course_organization_permission
from course_discovery.apps.publisher.models import OrganizationExtension
from course_discovery.apps.publisher.utils import has_role_for_course
register = template.Library()
def can_edit(course, user, permission):
return check_course_organization_permission(
user, course, permission
) and has_role_for_course(course, user)
@register.filter
def can_edit_course(course, user):
return can_edit(course, user, OrganizationExtension.EDIT_COURSE)
# pylint: disable=no-member
import json
import random
from datetime import datetime, timedelta
import ddt
import factory
import mock
from bs4 import BeautifulSoup
from django.conf import settings
from django.contrib.auth.models import Group
......@@ -14,7 +16,6 @@ from django.forms import model_to_dict
from django.test import TestCase
from django.urls import reverse
from guardian.shortcuts import assign_perm
from mock import patch
from opaque_keys.edx.keys import CourseKey
from pytz import timezone
from testfixtures import LogCapture
......@@ -37,7 +38,8 @@ from course_discovery.apps.publisher.tests import factories
from course_discovery.apps.publisher.tests.utils import create_non_staff_user_and_login
from course_discovery.apps.publisher.utils import is_email_notification_enabled
from course_discovery.apps.publisher.views import logger as publisher_views_logger
from course_discovery.apps.publisher.views import CourseRunDetailView, get_course_role_widgets_data
from course_discovery.apps.publisher.views import (COURSES_ALLOWED_PAGE_SIZES, CourseRunDetailView,
get_course_role_widgets_data)
from course_discovery.apps.publisher.wrappers import CourseRunWrapper
from course_discovery.apps.publisher_comments.models import CommentTypeChoices
from course_discovery.apps.publisher_comments.tests.factories import CommentFactory
......@@ -157,7 +159,7 @@ class CreateCourseViewTests(SiteMixin, TestCase):
self._assert_records(1)
data = {'number': 'course_2', 'image': make_image_file('test_banner.jpg')}
course_dict = self._post_data(data, self.course)
with patch.object(Course, "save") as mock_method:
with mock.patch.object(Course, "save") as mock_method:
mock_method.side_effect = IntegrityError
response = self.client.post(reverse('publisher:publisher_courses_new'), course_dict, files=data['image'])
......@@ -355,7 +357,7 @@ class CreateCourseRunViewTests(SiteMixin, TestCase):
)
self.assertEqual(response.status_code, 400)
with patch('django.forms.models.BaseModelForm.is_valid') as mocked_is_valid:
with mock.patch('django.forms.models.BaseModelForm.is_valid') as mocked_is_valid:
mocked_is_valid.return_value = True
with LogCapture(publisher_views_logger.name) as log_capture:
response = self.client.post(
......@@ -792,7 +794,7 @@ class CourseRunDetailTests(SiteMixin, TestCase):
"""
non_staff_user, group = create_non_staff_user_and_login(self) # pylint: disable=unused-variable
page_url = reverse('publisher:publisher_course_run_detail', args=[self.course_run.id])
with patch.object(CourseRunDetailView, 'get_object', return_value=non_staff_user):
with mock.patch.object(CourseRunDetailView, 'get_object', return_value=non_staff_user):
response = self.client.get(page_url)
self.assertEqual(response.status_code, 403)
......@@ -1603,7 +1605,41 @@ class ToggleEmailNotificationTests(SiteMixin, TestCase):
self.assertEqual(is_email_notification_enabled(user), is_enabled)
class CourseListViewTests(SiteMixin, TestCase):
class PaginationMixin(object):
"""
Common methods to be used for Paginated views.
"""
def get_courses(self, status_code=200, content_type='application/json', **kwargs):
"""
Make get request with specified params.
Arguments:
status_code (int): used to verify the received response status code
content_type (st): content type of get request
kwargs (dict): extra kwargs like `query_params` to be used in get request
Returns:
courses (list): list of courses
"""
query_params = kwargs.get('query_params', {})
# draw query parameter is send by jquery DataTables in all ajax requests
# https://datatables.net/manual/server-side
draw = 1
query_params['draw'] = draw
response = self.client.get(self.courses_url, query_params, HTTP_ACCEPT=content_type)
self.assertEqual(response.status_code, status_code)
if content_type == 'application/json':
json_response = response.json()
self.assertEqual(json_response['draw'], draw)
return json_response['courses']
else:
return json.loads(response.context_data['courses'].decode('utf-8'))
class CourseListViewTests(SiteMixin, PaginationMixin, TestCase):
""" Tests for `CourseListView` """
def setUp(self):
......@@ -1612,6 +1648,9 @@ class CourseListViewTests(SiteMixin, TestCase):
self.course = self.courses[0]
self.user = UserFactory()
for course in self.courses:
factories.CourseStateFactory(course=course, owner_role=PublisherUserRole.MarketingReviewer)
self.client.login(username=self.user.username, password=USER_PASSWORD)
self.courses_url = reverse('publisher:publisher_courses')
......@@ -1622,7 +1661,7 @@ class CourseListViewTests(SiteMixin, TestCase):
def test_courses_with_admin(self):
""" Verify that admin user can see all courses on course list page. """
self.user.groups.add(Group.objects.get(name=ADMIN_GROUP_NAME))
self.assert_course_list_page(course_count=10, queries_executed=32)
self.assert_course_list_page(course_count=10, queries_executed=33)
def test_courses_with_course_user_role(self):
""" Verify that internal user can see course on course list page. """
......@@ -1630,7 +1669,7 @@ class CourseListViewTests(SiteMixin, TestCase):
for course in self.courses:
factories.CourseUserRoleFactory(course=course, user=self.user, role=InternalUserRole.Publisher)
self.assert_course_list_page(course_count=10, queries_executed=33)
self.assert_course_list_page(course_count=10, queries_executed=34)
def test_courses_with_permission(self):
""" Verify that user can see course with permission on course list page. """
......@@ -1641,7 +1680,7 @@ class CourseListViewTests(SiteMixin, TestCase):
course.organizations.add(organization_extension.organization)
assign_perm(OrganizationExtension.VIEW_COURSE, organization_extension.group, organization_extension)
self.assert_course_list_page(course_count=10, queries_executed=65)
self.assert_course_list_page(course_count=10, queries_executed=66)
def assert_course_list_page(self, course_count, queries_executed):
""" Dry method to assert course list page content. """
......@@ -1651,27 +1690,32 @@ class CourseListViewTests(SiteMixin, TestCase):
self.assertContains(response, '{} Courses'.format(course_count))
self.assertContains(response, 'Create New Course')
if course_count > 0:
self.assertContains(response, self.course.title)
self.assertEqual(response.status_code, 200)
courses = json.loads(response.context_data['courses'].decode('utf-8'))
self.assertTrue(self.course.title in [course['course_title']['title'] for course in courses])
def test_page_with_enable_waffle_switch(self):
"""
Verify that edit button will not be shown if 'publisher_hide_features_for_pilot' activated.
"""
edit_url = {'title': 'Edit', 'url': reverse('publisher:publisher_courses_edit', args=[self.course.id])}
factories.CourseUserRoleFactory(course=self.course, user=self.user, role=PublisherUserRole.CourseTeam)
organization_extension = factories.OrganizationExtensionFactory()
self.course.organizations.add(organization_extension.organization)
self.user.groups.add(organization_extension.group)
assign_perm(OrganizationExtension.VIEW_COURSE, organization_extension.group, organization_extension)
assign_perm(OrganizationExtension.EDIT_COURSE, organization_extension.group, organization_extension)
response = self.client.get(self.courses_url)
self.assertContains(response, 'Edit')
response = self.get_courses()
self.assertEqual(response[0]['edit_url'], edit_url)
toggle_switch('publisher_hide_features_for_pilot', True)
with self.assertNumQueries(18):
response = self.client.get(self.courses_url)
with self.assertNumQueries(19):
response = self.get_courses()
self.assertNotContains(response, 'Edit')
edit_url['url'] = None
self.assertEqual(response[0]['edit_url'], edit_url)
def test_page_with_disable_waffle_switch(self):
"""
......@@ -1687,13 +1731,233 @@ class CourseListViewTests(SiteMixin, TestCase):
toggle_switch('publisher_hide_features_for_pilot', False)
with self.assertNumQueries(22):
with self.assertNumQueries(23):
response = self.client.get(self.courses_url)
self.assertContains(response, 'Edit')
class CourseDetailViewTests(SiteMixin, TestCase):
@ddt.ddt
@mock.patch('course_discovery.apps.publisher.views.COURSES_DEFAULT_PAGE_SIZE', 2)
@mock.patch('course_discovery.apps.publisher.views.COURSES_ALLOWED_PAGE_SIZES', (2, 3, 4))
class CourseListViewPaginationTests(PaginationMixin, TestCase):
""" Pagination tests for `CourseListView` """
def setUp(self):
super(CourseListViewPaginationTests, self).setUp()
self.courses = []
self.course_titles = [
'course title 16', 'course title 37', 'course title 19', 'course title 37', 'course title 25',
'course title 25', 'course title 10', 'course title 13', 'course title 28', 'course title 13'
]
self.course_organizations = [
'zeroX', 'deepX', 'fuzzyX', 'arkX', 'maX', 'pizzaX', 'maX', 'arkX', 'fuzzyX', 'zeroX',
]
self.course_dates = [
datetime(2017, 1, 10), datetime(2019, 2, 25), datetime(2017, 3, 20), datetime(2018, 3, 24),
datetime(2017, 2, 21), datetime(2015, 1, 22), datetime(2018, 2, 23), datetime(2017, 1, 21),
datetime(2019, 1, 24), datetime(2017, 2, 11),
]
# create 10 courses with related objects
for index in range(10):
course = factories.CourseFactory(title=self.course_titles[index])
for _ in range(random.randrange(1, 10)):
factories.CourseRunFactory(course=course)
course_state = factories.CourseStateFactory(course=course, owner_role=PublisherUserRole.MarketingReviewer)
course_state.owner_role_modified = self.course_dates[index]
course_state.save()
course.organizations.add(OrganizationFactory(key=self.course_organizations[index]))
self.courses.append(course)
self.course = self.courses[0]
self.user = UserFactory()
self.user.groups.add(Group.objects.get(name=ADMIN_GROUP_NAME))
self.client.login(username=self.user.username, password=USER_PASSWORD)
self.courses_url = reverse('publisher:publisher_courses')
self.sort_directions = {
'asc': False,
'desc': True,
}
@ddt.data(
{'page_size': '', 'expected': 2},
{'page_size': 2, 'expected': 2},
{'page_size': 3, 'expected': 3},
{'page_size': 4, 'expected': 4},
{'page_size': -1, 'expected': 2},
)
@ddt.unpack
def test_page_size(self, page_size, expected):
""" Verify that page size is working as expected. """
courses = self.get_courses(query_params={'pageSize': page_size})
self.assertEqual(len(courses), expected)
@ddt.data(
{'page': '', 'expected': 1},
{'page': 2, 'expected': 2},
{'page': 10, 'expected': None},
)
@ddt.unpack
def test_page_number(self, page, expected):
"""
Verify that page number is working as expected.
Note: We have total 3 pages. If page number is invalid than 404 response will be received.
"""
response = self.client.get(self.courses_url, {'pageSize': 4, 'page': page})
if response.status_code == 200:
self.assertEqual(response.context_data['page_obj'].number, expected)
else:
self.assertEqual(response.status_code, 404)
def test_content_type_text_html(self):
"""
Verify that get request with text/html content type is working as expected.
"""
courses = self.get_courses(content_type='text/html')
self.assertEqual(len(courses), 2)
@ddt.data(
{'field': 'title', 'column': 0, 'direction': 'asc'},
{'field': 'title', 'column': 0, 'direction': 'desc'},
)
@ddt.unpack
def test_ordering_by_title(self, field, column, direction):
""" Verify that ordering by title is working as expected. """
for page in (1, 2, 3):
courses = self.get_courses(
query_params={'sortColumn': column, 'sortDirection': direction, 'pageSize': 4, 'page': page}
)
course_titles = [course['course_title'][field] for course in courses]
self.assertEqual(sorted(course_titles, reverse=self.sort_directions[direction]), course_titles)
@ddt.data(
{'field': 'course_team_status', 'column': 4, 'direction': 'asc'},
{'field': 'course_team_status', 'column': 4, 'direction': 'desc'},
{'field': 'internal_user_status', 'column': 5, 'direction': 'asc'},
{'field': 'internal_user_status', 'column': 5, 'direction': 'desc'},
)
@ddt.unpack
def test_ordering_by_date(self, field, column, direction):
""" Verify that ordering by date is working as expected. """
for page in (1, 2, 3):
courses = self.get_courses(
query_params={'sortColumn': column, 'sortDirection': direction, 'pageSize': 4, 'page': page}
)
course_dates = [course[field]['date'] for course in courses]
self.assertEqual(
sorted(
course_dates,
key=lambda x: datetime.strptime(x, '%m/%d/%y'),
reverse=self.sort_directions[direction]
),
course_dates
)
@ddt.data(
{'field': 'publisher_course_runs_count', 'column': 3, 'direction': 'asc'},
{'field': 'publisher_course_runs_count', 'column': 3, 'direction': 'desc'},
)
@ddt.unpack
def test_ordering_by_course_runs(self, field, column, direction):
""" Verify that ordering by course runs is working as expected. """
for page in (1, 2, 3):
courses = self.get_courses(
query_params={'sortColumn': column, 'sortDirection': direction, 'pageSize': 4, 'page': page}
)
course_runs = [course[field] for course in courses]
self.assertEqual(sorted(course_runs, reverse=self.sort_directions[direction]), course_runs)
@ddt.data(
{'query': 'course title', 'results_count': 10},
{'query': 'course 13 title ', 'results_count': 2},
{'query': 'maX title course', 'results_count': 2},
{'query': 'course title arkX', 'results_count': 2},
{'query': 'course 03/24/18 title arkX', 'results_count': 1},
{'query': 'zeroX 01/10/17 course', 'results_count': 1},
{'query': 'blah blah', 'results_count': 0},
)
@ddt.unpack
def test_filtering(self, query, results_count):
""" Verify that filtering is working as expected. """
with mock.patch('course_discovery.apps.publisher.views.COURSES_ALLOWED_PAGE_SIZES', (10,)):
courses = self.get_courses(query_params={'pageSize': 10, 'searchText': query})
self.assertEqual(len(courses), results_count)
for course in courses:
title_org_dates = '{} {} {} {}'.format(
course['course_title']['title'], course['organization_name'],
course['course_team_status']['date'], course['internal_user_status']['date'],
)
for token in query.split():
self.assertTrue(token in title_org_dates)
def test_filtering_with_multiple_dates(self):
""" Verify that filtering is working as expected. """
query = 'zeroX 01/10/17 course 02/11/17 title'
courses = self.get_courses(query_params={'pageSize': 10, 'searchText': query})
self.assertEqual(len(courses), 2)
dates = []
for course in courses:
dates.extend(
[course['course_team_status']['date'], course['internal_user_status']['date']]
)
query_without_dates = query
# verify that dates for each course record should be present on query
for date in dates:
self.assertTrue(date in query)
query_without_dates = query_without_dates.replace(date, '')
# verify that non date query keywords exist in returned courses
for course in courses:
title_and_org = '{} {}'.format(course['course_title']['title'], course['organization_name'])
for token in query_without_dates.split():
self.assertTrue(token in title_and_org)
def test_pagination_for_internal_user(self):
""" Verify that pagination works for internal user. """
with mock.patch('course_discovery.apps.publisher.views.is_publisher_admin', return_value=False):
self.user.groups.add(Group.objects.get(name=INTERNAL_USER_GROUP_NAME))
self.course_team_role = factories.CourseUserRoleFactory(
course=self.courses[0], user=self.user, role=PublisherUserRole.CourseTeam
)
self.course_team_role = factories.CourseUserRoleFactory(
course=self.courses[1], user=self.user, role=PublisherUserRole.CourseTeam
)
courses = self.get_courses()
self.assertEqual(len(courses), 2)
def test_pagination_for_user_organizations(self):
""" Verify that pagination works for user organizations. """
with mock.patch('course_discovery.apps.publisher.views.is_publisher_admin', return_value=False):
with mock.patch('course_discovery.apps.publisher.views.is_internal_user', return_value=False):
organization_extension = factories.OrganizationExtensionFactory(
organization=self.courses[0].organizations.all()[0] # zeroX
)
self.user.groups.add(organization_extension.group)
assign_perm(OrganizationExtension.VIEW_COURSE, organization_extension.group, organization_extension)
courses = self.get_courses()
self.assertEqual(len(courses), 1)
def test_context(self):
""" Verify that required data is present in context. """
with mock.patch('course_discovery.apps.publisher.views.COURSES_ALLOWED_PAGE_SIZES', COURSES_ALLOWED_PAGE_SIZES):
response = self.client.get(self.courses_url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context_data['publisher_courses_url'], reverse('publisher:publisher_courses'))
self.assertEqual(response.context_data['allowed_page_sizes'], json.dumps(COURSES_ALLOWED_PAGE_SIZES))
class CourseDetailViewTests(TestCase):
""" Tests for the course detail view. """
def setUp(self):
......@@ -2676,7 +2940,7 @@ class CourseRunEditViewTests(SiteMixin, TestCase):
""" Verify that in case of any error transactions roll back and no object
updated in db.
"""
with patch.object(CourseRun, "save") as mock_method:
with mock.patch.object(CourseRun, "save") as mock_method:
mock_method.side_effect = IntegrityError
response = self.client.post(self.edit_page_url, self.updated_dict)
......@@ -2720,7 +2984,7 @@ class CourseRunEditViewTests(SiteMixin, TestCase):
def test_logging(self):
""" Verify view logs the errors in case of errors. """
with patch('django.forms.models.BaseModelForm.is_valid') as mocked_is_valid:
with mock.patch('django.forms.models.BaseModelForm.is_valid') as mocked_is_valid:
mocked_is_valid.return_value = True
with LogCapture(publisher_views_logger.name) as log_capture:
# pop the
......
......@@ -4,12 +4,15 @@ Course publisher views.
import json
import logging
from datetime import datetime, timedelta
from functools import reduce
import waffle
from django.contrib import messages
from django.contrib.sites.models import Site
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models import Count, Q
from django.db.models.functions import Lower
from django.forms import model_to_dict
from django.http import Http404, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render
......@@ -22,7 +25,7 @@ from guardian.shortcuts import get_objects_for_user
from course_discovery.apps.core.models import User
from course_discovery.apps.course_metadata.models import Person
from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher import emails, mixins
from course_discovery.apps.publisher import emails, mixins, serializers
from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.dataloader.create_courses import process_course
from course_discovery.apps.publisher.emails import send_email_for_published_course_run_editing
......@@ -57,6 +60,9 @@ DEFAULT_ROLES = [
COURSE_ROLES = [PublisherUserRole.CourseTeam]
COURSE_ROLES.extend(DEFAULT_ROLES)
COURSES_DEFAULT_PAGE_SIZE = 25
COURSES_ALLOWED_PAGE_SIZES = (25, 50, 100)
class Dashboard(mixins.LoginRequiredMixin, ListView):
""" Create Course View."""
......@@ -796,6 +802,7 @@ class ToggleEmailNotification(mixins.LoginRequiredMixin, View):
class CourseListView(mixins.LoginRequiredMixin, ListView):
""" Course List View."""
template_name = 'publisher/courses.html'
paginate_by = COURSES_DEFAULT_PAGE_SIZE
def get_queryset(self):
user = self.request.user
......@@ -817,6 +824,9 @@ class CourseListView(mixins.LoginRequiredMixin, ListView):
).values_list('organization')
courses = courses.filter(organizations__in=organizations)
courses = self.do_ordering(courses)
courses = self.do_filtering(courses)
return courses
def get_context_data(self, **kwargs):
......@@ -824,8 +834,167 @@ class CourseListView(mixins.LoginRequiredMixin, ListView):
context['publisher_hide_features_for_pilot'] = waffle.switch_is_active('publisher_hide_features_for_pilot')
site = Site.objects.first()
context['site_name'] = 'edX' if 'edx' in site.name.lower() else site.name
context['publisher_courses_url'] = reverse('publisher:publisher_courses')
context['allowed_page_sizes'] = json.dumps(COURSES_ALLOWED_PAGE_SIZES)
return context
def get_paginate_by(self, queryset):
"""
Get the number of items to paginate by.
"""
try:
page_size = int(self.request.GET.get('pageSize', COURSES_DEFAULT_PAGE_SIZE))
page_size = page_size if page_size in COURSES_ALLOWED_PAGE_SIZES else COURSES_DEFAULT_PAGE_SIZE
except ValueError:
page_size = COURSES_DEFAULT_PAGE_SIZE
return page_size
def do_ordering(self, queryset):
"""
Perform ordering on queryset
"""
# commented fields are multi-valued so ordering is not reliable becuase a single
# record can be returned multiple times. We are not doing ordering for these fields
ordering_fields = {
0: 'title',
# 1: 'organizations__key',
# 2: 'course_user_roles__user__full_name',
3: 'course_runs_count',
4: 'course_state__owner_role_modified',
5: 'course_state__owner_role_modified',
}
try:
ordering_field_index = int(self.request.GET.get('sortColumn', 0))
if ordering_field_index not in ordering_fields.keys():
raise ValueError
except ValueError:
ordering_field_index = 0
ordering_direction = self.request.GET.get('sortDirection', 'asc')
ordering_field = ordering_fields.get(ordering_field_index)
if ordering_field == 'course_runs_count':
queryset = queryset.annotate(course_runs_count=Count('publisher_course_runs'))
if ordering_direction == 'asc':
queryset = queryset.order_by(Lower(ordering_field).asc())
else:
queryset = queryset.order_by(Lower(ordering_field).desc())
return queryset
def do_filtering(self, queryset):
"""
Perform filtering on queryset
"""
filter_text = self.request.GET.get('searchText', '').strip()
if not filter_text:
return queryset
keywords, dates = self.extract_keywords_and_dates(filter_text)
query_filters = []
keywords_filter = None
for keyword in keywords:
keyword_filter = Q(title__icontains=keyword) | Q(organizations__key__icontains=keyword)
keywords_filter = (keyword_filter & keywords_filter) if bool(keywords_filter) else keyword_filter
if keywords_filter:
query_filters.append(keywords_filter)
if dates:
query_filters.append(
Q(reduce(lambda x, y: x | y, [
Q(course_state__owner_role_modified__gte=date['first']) &
Q(course_state__owner_role_modified__lt=date['second'])
for date in dates
]))
)
# Filtering is based on keywords and dates. Providing any one of them or both will filter the results.
# if both are provided then filtering happens according to the below algorithm
# << select records that contains all the keywords AND the record also contains any of the date >>
# if user enters multiple dates then we will fetch all records matching any date provided that
# those records contains all the keywords too. See the below sample records and query results
#
# {'title': 'Ops', 'org': 'arbi', 'date': '07/04/17'},
# {'title': 'Ops'", 'org': 'arbi', 'date': '07/04/17'},
# {'title': 'Ops', 'org': 'arbi', 'date': '07/10/18'},
# {'title': 'Ops', 'org': 'arbi', 'date': '07/04/17'},
# {'title': 'awesome', 'org': 'me', 'date': '07/10/18'},
#
# arbi ops << select first 4 records
# arbi 07/04/17 ops << select 1st, 2nd and 4th record
# ops 07/04/17 arbi 07/10/18 << select first 4 records
# ops 07/04/17 arbi 07/10/18 nope << no record matches -- all keywords must be present with any of the date
# 07/10/18 << select 3rd and last record
# awesome << select last record
# awesome 07/04/17 << no record matches
# distinct is used here to remove duplicate records in case a course has multiple organizations
# Note: currently this will not happen because each course has one organization only
return queryset.filter(*query_filters).distinct()
@staticmethod
def extract_keywords_and_dates(filter_text):
"""
Check each keyword in provided list of keywords and parse dates.
Arguments:
filter_text (str): input text entered by user like 'intro to python 07/04/17'
Returns:
tuple: tuple of two lists, first list contains keywords without dates and
second contains list of dicts where each dict has two date objects
"""
dates = []
keywords = []
tokens = filter_text.split()
for token in tokens:
try:
dt = datetime.strptime(token, '%m/%d/%y')
dates.append({
'first': dt,
'second': dt + timedelta(days=1),
})
except ValueError:
keywords.append(token)
return keywords, dates
def get(self, request):
"""
HTTP GET handler for publisher courses.
"""
self.object_list = self.get_queryset()
context = self.get_context_data()
context['publisher_total_courses_count'] = self.object_list.count()
courses = serializers.CourseSerializer(
context['object_list'],
many=True,
context={
'user': request.user,
'publisher_hide_features_for_pilot': context['publisher_hide_features_for_pilot'],
}
).data
if 'application/json' in request.META.get('HTTP_ACCEPT', ''):
count = self.object_list.count()
return JsonResponse({
'draw': int(self.request.GET['draw']),
'recordsTotal': count,
'recordsFiltered': count,
'courses': courses,
})
else:
context['courses'] = JsonResponse(courses, safe=False).content
return self.render_to_response(context)
class CourseRevisionView(mixins.LoginRequiredMixin, DetailView):
"""Course revisions view """
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-07-13 17:02-0400\n"
"POT-Creation-Date: 2017-07-21 17:33+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -875,6 +875,12 @@ msgstr ""
msgid "n/a"
msgstr ""
#: apps/publisher/serializers.py templates/publisher/_approval_widget.html
#: templates/publisher/course_detail/_edit_warning_popup.html
#: templates/publisher/course_run_detail/_edit_warning.html
msgid "Edit"
msgstr ""
#: apps/publisher/validators.py
#, python-format
msgid ""
......@@ -1205,13 +1211,6 @@ msgid "Submitted for review"
msgstr ""
#: templates/publisher/_approval_widget.html
#: templates/publisher/course_detail/_edit_warning_popup.html
#: templates/publisher/course_run_detail/_edit_warning.html
#: templates/publisher/courses.html
msgid "Edit"
msgstr ""
#: templates/publisher/_approval_widget.html
msgid "ABOUT PAGE PREVIEW"
msgstr ""
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-07-13 17:03-0400\n"
"POT-Creation-Date: 2017-07-21 17:33+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -74,6 +74,10 @@ msgstr ""
msgid "Please enter a valid URL."
msgstr ""
#: static/js/publisher/views/courses.js
msgid "No courses have been created."
msgstr ""
#: static/js/publisher/views/dashboard.js
msgid ""
"You have successfully created a Studio URL ({studioLinkTag}) for "
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-07-13 17:02-0400\n"
"POT-Creation-Date: 2017-07-21 17:33+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -1026,6 +1026,12 @@ msgstr "Ìn Révïéw sïnçé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#"
msgid "n/a"
msgstr "n/ä Ⱡ'σяєм#"
#: apps/publisher/serializers.py templates/publisher/_approval_widget.html
#: templates/publisher/course_detail/_edit_warning_popup.html
#: templates/publisher/course_run_detail/_edit_warning.html
msgid "Edit"
msgstr "Édït Ⱡ'σяєм ι#"
#: apps/publisher/validators.py
#, python-format
msgid ""
......@@ -1401,13 +1407,6 @@ msgid "Submitted for review"
msgstr "Süßmïttéd för révïéw Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
#: templates/publisher/_approval_widget.html
#: templates/publisher/course_detail/_edit_warning_popup.html
#: templates/publisher/course_run_detail/_edit_warning.html
#: templates/publisher/courses.html
msgid "Edit"
msgstr "Édït Ⱡ'σяєм ι#"
#: templates/publisher/_approval_widget.html
msgid "ABOUT PAGE PREVIEW"
msgstr "ÀBÖÛT PÀGÉ PRÉVÌÉW Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#"
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-07-13 17:03-0400\n"
"POT-Creation-Date: 2017-07-21 17:33+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -83,6 +83,10 @@ msgstr "Sävé Ⱡ'σяєм ι#"
msgid "Please enter a valid URL."
msgstr "Pléäsé éntér ä välïd ÛRL. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
#: static/js/publisher/views/courses.js
msgid "No courses have been created."
msgstr "Nö çöürsés hävé ßéén çréätéd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"
#: static/js/publisher/views/dashboard.js
msgid ""
"You have successfully created a Studio URL ({studioLinkTag}) for "
......
$(document).ready(function() {
var data = $('.course-count-heading').data();
var $coursesTable = $('#dataTableCourse').DataTable({
'autoWidth': false,
'processing': true,
'serverSide': true,
'lengthMenu': $('.course-count-heading').data('publisherCoursesAllowedPageSizes'),
'deferLoading': $('.course-count-heading').data('publisherTotalCoursesCount'),
'data': $('.course-count-heading').data('publisherCourses'),
'ajax': {
'url': $('.course-count-heading').data('publisherCoursesUrl'),
'data': function(d) {
var table = $('#dataTableCourse').DataTable();
return {
draw: d.draw,
pageSize: d.length,
page: table.page.info().page + 1,
sortColumn: d.order[0].column,
sortDirection: d.order[0].dir,
searchText: d.search.value.trim()
};
},
'dataSrc': 'courses'
},
"columnDefs": [
{
"targets": 0,
"data": "course_title",
"render": function ( data, type, full, meta ) {
if (data.url) {
return '<a href="'+data.url+'">' + data.title + '</a>';
} else {
return data.title;
}
}
},
{
"targets": 1,
"data": "organization_name",
"sortable": false
},
{
"targets": 2,
"data": "project_coordinator_name",
"sortable": false
},
{
"targets": 3,
"data": "publisher_course_runs_count"
},
{
"targets": 4,
"data": "course_team_status",
"render": function ( data, type, full, meta ) {
return data.status + '<br>' + data.date;
}
},
{
"targets": 5,
"data": "internal_user_status",
"render": function ( data, type, full, meta ) {
return data.status + '<br>' + data.date;
}
},
{
"targets": 6,
"data": "edit_url",
"sortable": false,
"render": function ( data, type, full, meta ) {
if (data.url) {
return '<a href="'+data.url+ '" class="btn btn-brand btn-small btn-course-edit">' + data.title + '</a>'
} else {
return null;
}
}
}
],
'oLanguage': { 'sEmptyTable': gettext('No courses have been created.') }
});
$('div.dataTables_filter input').unbind();
$('div.dataTables_filter input').bind('keyup', function(e) {
if(e.keyCode == 13) {
$coursesTable.search( this.value ).draw();
}
});
});
{% extends 'publisher/base.html' %}
{% load i18n %}
{% load publisher_extras %}
{% load static %}
{% load compress %}
{% block title %}
{% trans "Courses" %}
{% endblock title %}
{% block page_content %}
<h2 class="hd-2 course-count-heading">{{ object_list.count }} Courses</h2>
<h2 class="hd-2 course-count-heading"
data-publisher-total-courses-count="{{publisher_total_courses_count}}"
data-publisher-courses="{{courses}}"
data-publisher-courses-url="{{publisher_courses_url}}"
data-publisher-courses-allowed-page-sizes="{{allowed_page_sizes}}"
>{{publisher_total_courses_count}} Courses</h2>
<a href="{% url 'publisher:publisher_courses_new' %}" class="btn btn-brand btn-small btn-course-add">
{% trans "Create New Course" %}
</a>
......@@ -37,56 +43,11 @@
<th></th>
</tr>
</thead>
<tbody>
{% for course in object_list %}
<tr>
<td>
{% if publisher_hide_features_for_pilot %}
{{ course.title }}
{% else %}
<a href="{% url 'publisher:publisher_course_detail' course.id %}">
{{ course.title }}
</a>
{% endif %}
</td>
<td>
{{ course.organization_name }}
</td>
<td>
{{ course.project_coordinator.full_name }}
</td>
<td>
{{ course.publisher_course_runs.count }}
</td>
<td>
{{ course.course_state.course_team_status.status_text }}<br>
{{ course.course_state.course_team_status.date|date:'m/d/y' }}
</td>
<td>
{{ course.course_state.internal_user_status.status_text }}<br>
{{ course.course_state.internal_user_status.date|date:'m/d/y' }}
</td>
<td>
{% if not publisher_hide_features_for_pilot and course|can_edit_course:request.user %}
<a href="{% url 'publisher:publisher_courses_edit' course.id %}" class="btn btn-brand btn-small btn-course-edit">
{% trans "Edit" %}
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
<tbody></tbody>
</table>
</div>
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function() {
$('#dataTableCourse').DataTable({
"autoWidth": false,
"oLanguage": { "sEmptyTable": gettext("No courses have been created.") }
});
});
</script>
<script src="{% static 'js/publisher/views/courses.js' %}"></script>
{% endblock %}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment