Commit aae91ad9 by Clinton Blackburn Committed by Clinton Blackburn

Updated state display on Publisher course list view

- Split handoff date into its own column
- Clarified status text
parent 0e5c1ed0
......@@ -700,22 +700,22 @@ class CourseState(TimeStampedModel, ChangedByMixin):
@property
def course_team_status(self):
if self.is_draft and self.owner_role == PublisherUserRole.CourseTeam and not self.marketing_reviewed:
return {'status_text': _('In Draft since'), 'date': self.owner_role_modified}
return _('Draft')
elif self.owner_role == PublisherUserRole.MarketingReviewer:
return {'status_text': _('Submitted on'), 'date': self.owner_role_modified}
return _('Submitted for Marketing Review')
elif self.owner_role == PublisherUserRole.CourseTeam and self.is_approved:
return {'status_text': _('Reviewed on'), 'date': self.owner_role_modified}
return _('Approved by Course Team')
elif self.marketing_reviewed and self.owner_role == PublisherUserRole.CourseTeam:
return {'status_text': _('In Review since'), 'date': self.owner_role_modified}
return _('Awaiting Course Team Review')
@property
def internal_user_status(self):
if self.is_draft and self.owner_role == PublisherUserRole.CourseTeam:
return {'status_text': _('n/a'), 'date': ''}
return _('N/A')
elif self.owner_role == PublisherUserRole.MarketingReviewer and (self.is_in_review or self.is_draft):
return {'status_text': _('In Review since'), 'date': self.owner_role_modified}
return _('Awaiting Marketing Review')
elif self.marketing_reviewed:
return {'status_text': _('Reviewed on'), 'date': self.owner_role_modified}
return _('Approved by Marketing')
class CourseRunState(TimeStampedModel, ChangedByMixin):
......
......@@ -6,6 +6,7 @@ from django.urls import reverse
from django.utils.translation import ugettext as _
from rest_framework import serializers
from course_discovery.apps.core.utils import serialize_datetime
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
......@@ -20,6 +21,7 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth
course_team_status = serializers.SerializerMethodField()
internal_user_status = serializers.SerializerMethodField()
edit_url = serializers.SerializerMethodField()
last_state_change = serializers.SerializerMethodField()
def get_number(self, course):
return course.number
......@@ -47,40 +49,19 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth
return 0
def get_course_team_status(self, course):
default_status = {
'status': '',
'date': ''
}
try:
course_team_status = course.course_state.course_team_status
return course.course_state.course_team_status
except ObjectDoesNotExist:
return default_status
course_team_status = default_status if course_team_status is None else course_team_status
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')
}
return ''
def get_internal_user_status(self, course):
default_status = {
'status': '',
'date': ''
}
try:
internal_user_status = course.course_state.internal_user_status
return course.course_state.internal_user_status
except ObjectDoesNotExist:
return default_status
return ''
internal_user_status = default_status if internal_user_status is None else internal_user_status
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_last_state_change(self, course):
return serialize_datetime(course.course_state.owner_role_modified)
def get_edit_url(self, course):
courses_edit_url = None
......
......@@ -38,10 +38,13 @@
{% trans "Runs" %}
</th>
<th role="button">
{% trans "Course Team" %}
{% trans "Course Team Status" %}
</th>
<th role="button">
{{ site_name }}
{% blocktrans with site_name=site_name trimmed %}{{ site_name }} Status{% endblocktrans %}
</th>
<th role="button">
{% trans "Last Handoff" %}
</th>
<th></th>
</tr>
......
......@@ -583,13 +583,6 @@ class CourseStateTests(TestCase):
self.course_state.change_owner_role(role)
self.assertEqual(self.course_state.owner_role, role)
def _assert_course_run_status(self, actual_status, expected_status_text, expected_date):
"""
Assert course statuses.
"""
expected_status = {'status_text': expected_status_text, 'date': expected_date}
self.assertEqual(actual_status, expected_status)
def _change_state_and_owner(self, course_state):
"""
Change course state to review and ownership to marketing.
......@@ -598,48 +591,30 @@ class CourseStateTests(TestCase):
course_state.change_owner_role(PublisherUserRole.MarketingReviewer)
def test_course_team_status(self):
"""
Verify that course_team_status returns right statuses.
"""
course_state = factories.CourseStateFactory(owner_role=PublisherUserRole.CourseTeam)
self._assert_course_run_status(
course_state.course_team_status, 'In Draft since', course_state.owner_role_modified
)
assert course_state.course_team_status == 'Draft'
self._change_state_and_owner(course_state)
self._assert_course_run_status(
course_state.course_team_status, 'Submitted on', course_state.owner_role_modified
)
assert course_state.course_team_status == 'Submitted for Marketing Review'
course_state.marketing_reviewed = True
course_state.change_owner_role(PublisherUserRole.CourseTeam)
self._assert_course_run_status(
course_state.course_team_status, 'In Review since', course_state.owner_role_modified
)
assert course_state.course_team_status == 'Awaiting Course Team Review'
course_state.approved()
course_state.save()
self._assert_course_run_status(
course_state.course_team_status, 'Reviewed on', course_state.owner_role_modified
)
assert course_state.course_team_status == 'Approved by Course Team'
def test_internal_user_status(self):
"""
Verify that internal_user_status returns right statuses.
"""
course_state = factories.CourseStateFactory(owner_role=PublisherUserRole.CourseTeam)
self._assert_course_run_status(course_state.internal_user_status, 'n/a', '')
assert course_state.internal_user_status == 'N/A'
self._change_state_and_owner(course_state)
self._assert_course_run_status(
course_state.internal_user_status, 'In Review since', course_state.owner_role_modified
)
assert course_state.internal_user_status == 'Awaiting Marketing Review'
course_state.marketing_reviewed = True
course_state.change_owner_role(PublisherUserRole.CourseTeam)
self._assert_course_run_status(
course_state.internal_user_status, 'Reviewed on', course_state.owner_role_modified
)
assert course_state.internal_user_status == 'Approved by Marketing'
@ddt.ddt
......
......@@ -1820,29 +1820,6 @@ class CourseListViewPaginationTests(PaginationMixin, TestCase):
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'},
)
......@@ -1856,56 +1833,6 @@ class CourseListViewPaginationTests(PaginationMixin, TestCase):
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):
......@@ -1942,18 +1869,6 @@ class CourseListViewPaginationTests(PaginationMixin, TestCase):
@mock.patch('course_discovery.apps.publisher.models.CourseState.course_team_status', new_callable=mock.PropertyMock)
@mock.patch('course_discovery.apps.publisher.models.CourseState.internal_user_status',
new_callable=mock.PropertyMock)
def test_course_state_statuses(self, mocked_internal_user_status, mocked_course_team_status):
""" Verify that course_state statuses raise no exception. """
mocked_internal_user_status.return_value = None
mocked_course_team_status.return_value = None
courses = self.get_courses()
for course in courses:
self.assertEqual(course['course_team_status'], {'status': '', 'date': ''})
self.assertEqual(course['internal_user_status'], {'status': '', 'date': ''})
@mock.patch('course_discovery.apps.publisher.models.CourseState.course_team_status', new_callable=mock.PropertyMock)
@mock.patch('course_discovery.apps.publisher.models.CourseState.internal_user_status',
new_callable=mock.PropertyMock)
def test_course_state_exceptions(self, mocked_internal_user_status, mocked_course_team_status):
"""
Verify that course_team_status and internal_user_status return
......@@ -1963,8 +1878,8 @@ class CourseListViewPaginationTests(PaginationMixin, TestCase):
mocked_course_team_status.side_effect = ObjectDoesNotExist
courses = self.get_courses()
for course in courses:
self.assertEqual(course['course_team_status'], {'status': '', 'date': ''})
self.assertEqual(course['internal_user_status'], {'status': '', 'date': ''})
assert course['course_team_status'] == ''
assert course['internal_user_status'] == ''
class CourseDetailViewTests(TestCase):
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-28 00:07-0400\n"
"POT-Creation-Date: 2017-09-28 00:11-0400\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"
......@@ -515,12 +515,11 @@ msgid "Publisher"
msgstr ""
#: apps/publisher/choices.py
#: apps/publisher/templates/publisher/course_list.html
#: apps/publisher/templates/publisher/dashboard/_in_progress.html
msgid "Course Team"
msgstr ""
#: apps/publisher/choices.py
#: apps/publisher/choices.py apps/publisher/models.py
msgid "Draft"
msgstr ""
......@@ -908,24 +907,28 @@ msgid ""
"also create a corresponding course run in Studio."
msgstr ""
#: apps/publisher/models.py apps/publisher/wrappers.py
msgid "In Draft since"
#: apps/publisher/models.py
msgid "Submitted for Marketing Review"
msgstr ""
#: apps/publisher/models.py apps/publisher/wrappers.py
msgid "Submitted on"
#: apps/publisher/models.py
msgid "Approved by Course Team"
msgstr ""
#: apps/publisher/models.py apps/publisher/wrappers.py
msgid "Reviewed on"
#: apps/publisher/models.py
msgid "Awaiting Course Team Review"
msgstr ""
#: apps/publisher/models.py apps/publisher/wrappers.py
msgid "In Review since"
#: apps/publisher/models.py
msgid "N/A"
msgstr ""
#: apps/publisher/models.py apps/publisher/wrappers.py
msgid "n/a"
#: apps/publisher/models.py
msgid "Awaiting Marketing Review"
msgstr ""
#: apps/publisher/models.py
msgid "Approved by Marketing"
msgstr ""
#: apps/publisher/serializers.py
......@@ -1970,6 +1973,19 @@ msgstr ""
msgid "Runs"
msgstr ""
#: apps/publisher/templates/publisher/course_list.html
msgid "Course Team Status"
msgstr ""
#: apps/publisher/templates/publisher/course_list.html
#, python-format
msgid "%(site_name)s Status"
msgstr ""
#: apps/publisher/templates/publisher/course_list.html
msgid "Last Handoff"
msgstr ""
#: apps/publisher/templates/publisher/course_revision_history.html
msgid "Revision History"
msgstr ""
......@@ -3241,6 +3257,26 @@ msgstr ""
msgid "Course run updated successfully."
msgstr ""
#: apps/publisher/wrappers.py
msgid "In Draft since"
msgstr ""
#: apps/publisher/wrappers.py
msgid "Submitted on"
msgstr ""
#: apps/publisher/wrappers.py
msgid "In Review since"
msgstr ""
#: apps/publisher/wrappers.py
msgid "n/a"
msgstr ""
#: apps/publisher/wrappers.py
msgid "Reviewed on"
msgstr ""
#: apps/publisher_comments/emails.py
msgid "Comment added:"
msgstr ""
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-28 00:07-0400\n"
"POT-Creation-Date: 2017-09-28 00:11-0400\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"
......@@ -634,12 +634,11 @@ msgid "Publisher"
msgstr "Püßlïshér Ⱡ'σяєм ιρѕυм ∂σł#"
#: apps/publisher/choices.py
#: apps/publisher/templates/publisher/course_list.html
#: apps/publisher/templates/publisher/dashboard/_in_progress.html
msgid "Course Team"
msgstr "Çöürsé Téäm Ⱡ'σяєм ιρѕυм ∂σłσя #"
#: apps/publisher/choices.py
#: apps/publisher/choices.py apps/publisher/models.py
msgid "Draft"
msgstr "Dräft Ⱡ'σяєм ιρѕ#"
......@@ -1065,25 +1064,29 @@ msgstr ""
"Whén thïs fläg ïs énäßléd, çréätïön öf ä néw çöürsé rün ïn Püßlïshér wïll "
"älsö çréäté ä çörréspöndïng çöürsé rün ïn Stüdïö. Ⱡ'σяєм ιρ#"
#: apps/publisher/models.py apps/publisher/wrappers.py
msgid "In Draft since"
msgstr "Ìn Dräft sïnçé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#"
#: apps/publisher/models.py
msgid "Submitted for Marketing Review"
msgstr "Süßmïttéd för Märkétïng Révïéw Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#"
#: apps/publisher/models.py apps/publisher/wrappers.py
msgid "Submitted on"
msgstr "Süßmïttéd ön Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
#: apps/publisher/models.py
msgid "Approved by Course Team"
msgstr "Àpprövéd ßý Çöürsé Téäm Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σ#"
#: apps/publisher/models.py apps/publisher/wrappers.py
msgid "Reviewed on"
msgstr "Révïéwéd ön Ⱡ'σяєм ιρѕυм ∂σłσя #"
#: apps/publisher/models.py
msgid "Awaiting Course Team Review"
msgstr "Àwäïtïng Çöürsé Téäm Révïéw Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#"
#: apps/publisher/models.py apps/publisher/wrappers.py
msgid "In Review since"
msgstr "Ìn Révïéw sïnçé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#"
#: apps/publisher/models.py
msgid "N/A"
msgstr "N/À Ⱡ'σяєм#"
#: apps/publisher/models.py apps/publisher/wrappers.py
msgid "n/a"
msgstr "n/ä Ⱡ'σяєм#"
#: apps/publisher/models.py
msgid "Awaiting Marketing Review"
msgstr "Àwäïtïng Märkétïng Révïéw Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
#: apps/publisher/models.py
msgid "Approved by Marketing"
msgstr "Àpprövéd ßý Märkétïng Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
#: apps/publisher/serializers.py
#: apps/publisher/templates/publisher/_approval_widget.html
......@@ -2352,6 +2355,19 @@ msgstr "Çöürsé Nämé Ⱡ'σяєм ιρѕυм ∂σłσя #"
msgid "Runs"
msgstr "Rüns Ⱡ'σяєм ι#"
#: apps/publisher/templates/publisher/course_list.html
msgid "Course Team Status"
msgstr "Çöürsé Téäm Stätüs Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#"
#: apps/publisher/templates/publisher/course_list.html
#, python-format
msgid "%(site_name)s Status"
msgstr "%(site_name)s Stätüs Ⱡ'σяєм ιρѕυм ∂σłσ#"
#: apps/publisher/templates/publisher/course_list.html
msgid "Last Handoff"
msgstr "Läst Händöff Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
#: apps/publisher/templates/publisher/course_revision_history.html
msgid "Revision History"
msgstr "Révïsïön Hïstörý Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#"
......@@ -3939,6 +3955,26 @@ msgid "Course run updated successfully."
msgstr ""
"Çöürsé rün üpdätéd süççéssfüllý. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тє#"
#: apps/publisher/wrappers.py
msgid "In Draft since"
msgstr "Ìn Dräft sïnçé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#"
#: apps/publisher/wrappers.py
msgid "Submitted on"
msgstr "Süßmïttéd ön Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
#: apps/publisher/wrappers.py
msgid "In Review since"
msgstr "Ìn Révïéw sïnçé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#"
#: apps/publisher/wrappers.py
msgid "n/a"
msgstr "n/ä Ⱡ'σяєм#"
#: apps/publisher/wrappers.py
msgid "Reviewed on"
msgstr "Révïéwéd ön Ⱡ'σяєм ιρѕυм ∂σłσя #"
#: apps/publisher_comments/emails.py
msgid "Comment added:"
msgstr "Çömmént äddéd: Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#"
......
......@@ -54,20 +54,21 @@ $(document).ready(function() {
},
{
"targets": 5,
"data": "course_team_status",
"render": function ( data, type, full, meta ) {
return data.status + '<br>' + data.date;
}
"data": "course_team_status"
},
{
"targets": 6,
"data": "internal_user_status",
"render": function ( data, type, full, meta ) {
return data.status + '<br>' + data.date;
}
"data": "internal_user_status"
},
{
"targets": 7,
"data": "last_state_change",
"render": function(data){
return moment(data).startOf('hour').fromNow();
}
},
{
"targets": 8,
"data": "edit_url",
"sortable": false,
"render": function ( data, type, full, meta ) {
......
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