Commit 759512a8 by tasawernawaz Committed by Tasawer Nawaz

Preview widget added for Publisher role

ECOM-6077
parent 9ba4792f
"""Publisher API Serializers"""
import re
import waffle
from django.apps import apps
from django.db import transaction
......@@ -11,7 +12,9 @@ from rest_framework import serializers
from course_discovery.apps.core.models import User
from course_discovery.apps.publisher.choices import PublisherUserRole
from course_discovery.apps.publisher.emails import send_email_for_studio_instance_created, send_email_preview_accepted
from course_discovery.apps.publisher.emails import (
send_email_for_studio_instance_created, send_email_preview_accepted, send_email_preview_page_is_available
)
from course_discovery.apps.publisher.models import CourseRun, CourseRunState, CourseState, CourseUserRole
......@@ -45,38 +48,59 @@ class GroupUserSerializer(serializers.ModelSerializer):
return obj.get_full_name() or obj.username
class UpdateCourseKeySerializer(serializers.ModelSerializer):
class CourseRunSerializer(serializers.ModelSerializer):
"""
Serializer for the `CourseRun` model to update 'lms_course_id'.
Serializer for the `CourseRun` model.
"""
class Meta:
model = CourseRun
fields = ('lms_course_id', 'changed_by',)
def validate(self, data):
validated_values = super(UpdateCourseKeySerializer, self).validate(data)
lms_course_id = validated_values.get('lms_course_id')
fields = ('lms_course_id', 'changed_by', 'preview_url',)
def validate_lms_course_id(self, value):
try:
CourseKey.from_string(lms_course_id)
CourseKey.from_string(value)
except InvalidKeyError:
# pylint: disable=no-member
raise serializers.ValidationError(
{'lms_course_id': _('Invalid course key "{lms_course_id}"').format(lms_course_id=lms_course_id)}
{'lms_course_id': _('Invalid course key "{lms_course_id}"').format(lms_course_id=value)}
)
request = self.context.get('request')
if request:
validated_values.update({'changed_by': request.user})
return value
def validate_preview_url(self, value):
if not re.match(r'https?://(?:www)?(?:[\w-]{2,255}(?:\.\w{2,6}){1,2})(?:/[\w&%?#-]{1,300})?', value):
# pylint: disable=no-member
raise serializers.ValidationError(
{'preview_url': _('Invalid URL format "{preview_url}"').format(preview_url=value)}
)
return value
def validate(self, data):
validated_values = super(CourseRunSerializer, self).validate(data)
if validated_values.get('lms_course_id'):
request = self.context.get('request')
if request:
validated_values.update({'changed_by': request.user})
return validated_values
def update(self, instance, validated_data):
instance = super(UpdateCourseKeySerializer, self).update(instance, validated_data)
instance = super(CourseRunSerializer, self).update(instance, validated_data)
preview_url = validated_data.get('preview_url')
lms_course_id = validated_data.get('lms_course_id')
if preview_url:
instance.course_run_state.change_role(PublisherUserRole.CourseTeam)
if waffle.switch_is_active('enable_publisher_email_notifications'):
send_email_for_studio_instance_created(instance)
if preview_url:
send_email_preview_page_is_available(instance)
elif lms_course_id:
send_email_for_studio_instance_created(instance)
return instance
......
......@@ -6,9 +6,9 @@ from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.course_metadata.tests.factories import PersonFactory
from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.api.serializers import (CourseRevisionSerializer, CourseRunStateSerializer,
CourseStateSerializer, CourseUserRoleSerializer,
GroupUserSerializer, UpdateCourseKeySerializer)
from course_discovery.apps.publisher.api.serializers import (CourseRevisionSerializer, CourseRunSerializer,
CourseRunStateSerializer, CourseStateSerializer,
CourseUserRoleSerializer, GroupUserSerializer)
from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.models import CourseState, Seat
from course_discovery.apps.publisher.tests.factories import (CourseFactory, CourseRunFactory, CourseRunStateFactory,
......@@ -60,35 +60,64 @@ class GroupUserSerializerTests(TestCase):
self.assertDictEqual(serializer.data, expected)
class UpdateCourseKeySerializerTests(TestCase):
serializer_class = UpdateCourseKeySerializer
class CourseRunSerializerTests(TestCase):
serializer_class = CourseRunSerializer
def setUp(self):
super(UpdateCourseKeySerializerTests, self).setUp()
super(CourseRunSerializerTests, self).setUp()
self.course_run = CourseRunFactory()
self.course_run.lms_course_id = 'course-v1:edX+DemoX+Demo_Course'
self.request = RequestFactory()
self.user = UserFactory()
self.request.user = self.user
self.course_state = CourseRunStateFactory(course_run=self.course_run, owner_role=PublisherUserRole.Publisher)
def get_expected_data(self):
return {
'lms_course_id': self.course_run.lms_course_id,
'changed_by': self.user
'changed_by': self.user,
'preview_url': self.course_run.preview_url
}
def test_validation(self):
self.course_run.lms_course_id = 'course-v1:edxTest+TC101+2016_Q1'
def test_validate_lms_course_id(self):
""" Verify that serializer raises error if 'lms_course_id' has invalid format. """
self.course_run.lms_course_id = 'invalid-course-id'
self.course_run.save() # pylint: disable=no-member
serializer = self.serializer_class(self.course_run, context={'request': self.request})
expected = serializer.validate(serializer.data)
self.assertEqual(self.get_expected_data(), expected)
serializer = self.serializer_class(self.course_run)
with self.assertRaises(ValidationError):
serializer.validate_lms_course_id(self.course_run.lms_course_id)
def test_validation_error(self):
self.course_run.lms_course_id = 'invalid-course-id'
def test_validate_preview_url(self):
""" Verify that serializer raises error if 'preview_url' has invalid format. """
self.course_run.preview_url = 'invalid-preview-url'
self.course_run.save() # pylint: disable=no-member
serializer = self.serializer_class(self.course_run)
with self.assertRaises(ValidationError):
serializer.validate(serializer.data)
serializer.validate_preview_url(self.course_run.preview_url)
def test_serializer_with_valid_data(self):
""" Verify that serializer validate course_run object. """
serializer = self.serializer_class(self.course_run, context={'request': self.request})
self.assertEqual(self.get_expected_data(), serializer.validate(serializer.data))
def test_update_preview_url(self):
""" Verify that course 'owner_role' will be changed to course_team after updating
course run with preview url.
"""
serializer = self.serializer_class(self.course_run)
serializer.update(self.course_run, serializer.data)
self.assertEqual(self.course_state.owner_role, PublisherUserRole.CourseTeam)
def test_update_lms_course_id(self):
""" Verify that 'changed_by' also updated after updating course_run's lms_course_id."""
self.course_run.preview_url = None
self.course_run.save()
serializer = self.serializer_class(self.course_run, context={'request': self.request})
serializer.update(self.course_run, serializer.validate(serializer.data))
self.assertEqual(self.course_run.lms_course_id, serializer.data['lms_course_id'])
self.assertEqual(self.course_run.changed_by, self.user)
class CourseRevisionSerializerTests(TestCase):
......
......@@ -180,16 +180,16 @@ class OrganizationGroupUserViewTests(TestCase):
expected_results = [
{
"id": self.org_user1.id,
"full_name": self.org_user1.full_name
},
{
"id": self.org_user2.id,
"full_name": self.org_user2.username
},
{
"id": self.org_user1.id,
"full_name": self.org_user1.full_name
}
]
self.assertEqual(json.loads(response.content.decode("utf-8"))["results"], expected_results)
self.assertListEqual(json.loads(response.content.decode("utf-8"))["results"], expected_results)
def test_get_organization_not_found(self):
""" Verify that view returns status=404 if organization is not found
......@@ -205,10 +205,10 @@ class OrganizationGroupUserViewTests(TestCase):
)
class UpdateCourseKeyViewTests(TestCase):
class UpdateCourseRunViewTests(TestCase):
def setUp(self):
super(UpdateCourseKeyViewTests, self).setUp()
super(UpdateCourseRunViewTests, self).setUp()
self.course_run = factories.CourseRunFactory()
self.user = UserFactory()
self.user.groups.add(Group.objects.get(name=INTERNAL_USER_GROUP_NAME))
......@@ -216,8 +216,8 @@ class UpdateCourseKeyViewTests(TestCase):
self.organization_extension = factories.OrganizationExtensionFactory()
self.course_run.course.organizations.add(self.organization_extension.organization)
self.update_course_key_url = reverse(
'publisher:api:update_course_key', kwargs={'pk': self.course_run.id}
self.update_course_run_url = reverse(
'publisher:api:update_course_run', kwargs={'pk': self.course_run.id}
)
factories.CourseUserRoleFactory(
......@@ -236,7 +236,7 @@ class UpdateCourseKeyViewTests(TestCase):
"""
invalid_course_id = 'invalid-course-key'
response = self.client.patch(
self.update_course_key_url,
self.update_course_run_url,
data=json.dumps({'lms_course_id': invalid_course_id}),
content_type=JSON_CONTENT_TYPE
)
......@@ -244,7 +244,7 @@ class UpdateCourseKeyViewTests(TestCase):
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.data.get('lms_course_id'),
['Invalid course key "{lms_course_id}"'.format(lms_course_id=invalid_course_id)]
({'lms_course_id': 'Invalid course key "{lms_course_id}"'.format(lms_course_id=invalid_course_id)})
)
def test_update_course_key_without_permission(self):
......@@ -253,7 +253,7 @@ class UpdateCourseKeyViewTests(TestCase):
"""
self.user.groups.remove(Group.objects.get(name=INTERNAL_USER_GROUP_NAME))
response = self.client.patch(
self.update_course_key_url,
self.update_course_run_url,
data=json.dumps({'lms_course_id': 'course-v1:edxTest+TC12+2050Q1'}),
content_type=JSON_CONTENT_TYPE
)
......@@ -271,7 +271,7 @@ class UpdateCourseKeyViewTests(TestCase):
factories.CourseRunFactory(lms_course_id=lms_course_id)
response = self.client.patch(
self.update_course_key_url,
self.update_course_run_url,
data=json.dumps({'lms_course_id': lms_course_id}),
content_type=JSON_CONTENT_TYPE
)
......@@ -290,7 +290,7 @@ class UpdateCourseKeyViewTests(TestCase):
lms_course_id = 'course-v1:edxTest+TC12+2050Q1'
response = self.client.patch(
self.update_course_key_url,
self.update_course_run_url,
data=json.dumps({'lms_course_id': lms_course_id}),
content_type=JSON_CONTENT_TYPE
)
......@@ -326,6 +326,30 @@ class UpdateCourseKeyViewTests(TestCase):
page_url = 'https://{host}{path}'.format(host=Site.objects.get_current().domain.strip('/'), path=object_path)
self.assertIn(page_url, body)
def test_update_preview_url(self):
"""Verify the user can update course preview url."""
preview_url = 'https://example.com/abc/course'
factories.CourseRunStateFactory.create(course_run=self.course_run, owner_role=PublisherUserRole.Publisher)
response = self._make_request(preview_url)
self.assertEqual(response.status_code, 200)
course_run = CourseRun.objects.get(id=self.course_run.id)
self.assertEqual(course_run.preview_url, preview_url)
def test_update_with_invalid_preview_url(self):
"""Verify the user can't update course preview url if url has invalid format."""
preview_url = 'invalid_url_format'
response = self._make_request(preview_url)
self.assertEqual(response.status_code, 400)
def _make_request(self, preview_url):
""" Helper method to make request. """
return self.client.patch(
self.update_course_run_url,
data=json.dumps({'preview_url': preview_url}),
content_type=JSON_CONTENT_TYPE
)
class CourseRevisionDetailViewTests(TestCase):
......
......@@ -3,7 +3,7 @@ from django.conf.urls import url
from course_discovery.apps.publisher.api.views import (
ChangeCourseRunStateView, ChangeCourseStateView, CourseRevisionDetailView, CourseRoleAssignmentView,
OrganizationGroupUserView, UpdateCourseKeyView
OrganizationGroupUserView, UpdateCourseRunView
)
urlpatterns = [
......@@ -11,7 +11,7 @@ urlpatterns = [
url(r'^admins/organizations/(?P<pk>\d+)/users/$', OrganizationGroupUserView.as_view(),
name='organization_group_users'),
url(r'^course_state/(?P<pk>\d+)/$', ChangeCourseStateView.as_view(), name='change_course_state'),
url(r'^course_runs/(?P<pk>\d+)/$', UpdateCourseKeyView.as_view(), name='update_course_key'),
url(r'^course_runs/(?P<pk>\d+)/$', UpdateCourseRunView.as_view(), name='update_course_run'),
url(r'^course_revisions/(?P<history_id>\d+)/$', CourseRevisionDetailView.as_view(), name='course_revisions'),
url(r'^course_run_state/(?P<pk>\d+)/$', ChangeCourseRunStateView.as_view(), name='change_course_run_state'),
]
......@@ -2,16 +2,13 @@ from rest_framework.generics import ListAPIView, RetrieveAPIView, UpdateAPIView,
from rest_framework.permissions import IsAuthenticated
from course_discovery.apps.core.models import User
from course_discovery.apps.publisher.api.permissions import (
CanViewAssociatedCourse, InternalUserPermission, PublisherUserPermission
)
from course_discovery.apps.publisher.api.serializers import (
CourseRevisionSerializer, CourseRunStateSerializer, CourseStateSerializer, CourseUserRoleSerializer,
GroupUserSerializer, UpdateCourseKeySerializer
)
from course_discovery.apps.publisher.models import (
Course, CourseRun, CourseRunState, CourseState, CourseUserRole, OrganizationExtension
)
from course_discovery.apps.publisher.api.permissions import (CanViewAssociatedCourse, InternalUserPermission,
PublisherUserPermission)
from course_discovery.apps.publisher.api.serializers import (CourseRevisionSerializer, CourseRunSerializer,
CourseRunStateSerializer, CourseStateSerializer,
CourseUserRoleSerializer, GroupUserSerializer)
from course_discovery.apps.publisher.models import (Course, CourseRun, CourseRunState, CourseState, CourseUserRole,
OrganizationExtension)
class CourseRoleAssignmentView(UpdateAPIView):
......@@ -30,10 +27,10 @@ class OrganizationGroupUserView(ListAPIView):
return queryset
class UpdateCourseKeyView(UpdateAPIView):
class UpdateCourseRunView(UpdateAPIView):
permission_classes = (IsAuthenticated, InternalUserPermission,)
queryset = CourseRun.objects.all()
serializer_class = UpdateCourseKeySerializer
serializer_class = CourseRunSerializer
class CourseRevisionDetailView(RetrieveAPIView):
......
......@@ -365,3 +365,47 @@ def send_email_preview_accepted(course_run):
email_msg.send()
except Exception: # pylint: disable=broad-except
logger.exception('Failed to send email notifications for preview approved of course-run %s', course_run.id)
def send_email_preview_page_is_available(course_run):
""" Send email for course preview available to course team.
Arguments:
course_run (Object): CourseRun object
"""
txt_template = 'publisher/email/course_run/preview_available.txt'
html_template = 'publisher/email/course_run/preview_available.html'
run_name = '{pacing_type}: {start_date}'.format(
pacing_type=course_run.get_pacing_type_display(),
start_date=course_run.start.strftime("%B %d, %Y")
)
subject = _('Preview for {run_name} is available').format(run_name=run_name) # pylint: disable=no-member
course_team_user = course_run.course.course_team_admin
try:
if is_email_notification_enabled(course_team_user):
to_addresses = [course_team_user.email]
from_address = settings.PUBLISHER_FROM_EMAIL
project_coordinator = course_run.course.project_coordinator
page_path = reverse('publisher:publisher_course_run_detail', kwargs={'pk': course_run.id})
context = {
'course_name': run_name,
'contact_us_email': project_coordinator.email if project_coordinator else '',
'page_url': 'https://{host}{path}'.format(
host=Site.objects.get_current().domain.strip('/'), path=page_path
)
}
template = get_template(txt_template)
plain_content = template.render(context)
template = get_template(html_template)
html_content = template.render(context)
email_msg = EmailMultiAlternatives(
subject, plain_content, from_address, to=[from_address], bcc=to_addresses
)
email_msg.attach_alternative(html_content, 'text/html')
email_msg.send()
except Exception: # pylint: disable=broad-except
logger.exception('Failed to send email notifications for preview available of course-run %s', course_run.id)
......@@ -709,6 +709,11 @@ class CourseRunState(TimeStampedModel, ChangedByMixin):
self.save()
def change_role(self, role):
self.owner_role = role
self.owner_role_modified = timezone.now()
self.save()
@property
def is_preview_accepted(self):
return self.preview_accepted
......
......@@ -534,3 +534,40 @@ class CourseRunPreviewEmailTests(TestCase):
)
)
)
def test_preview_available_email(self):
"""
Verify that preview available email functionality works fine.
"""
emails.send_email_preview_page_is_available(self.run_state.course_run)
run_name = '{pacing_type}: {start_date}'.format(
pacing_type=self.run_state.course_run.get_pacing_type_display(),
start_date=self.run_state.course_run.start.strftime("%B %d, %Y")
)
subject = 'Preview for {run_name} is available'.format(
run_name=run_name
)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual([self.course.course_team_admin.email], mail.outbox[0].bcc)
self.assertEqual(str(mail.outbox[0].subject), subject)
body = mail.outbox[0].body.strip()
page_path = reverse('publisher:publisher_course_run_detail', kwargs={'pk': self.run_state.course_run.id})
page_url = 'https://{host}{path}'.format(host=Site.objects.get_current().domain.strip('/'), path=page_path)
self.assertIn(page_url, body)
self.assertIn('is available for review.', body)
def test_preview_available_email_with_error(self):
""" Verify that email failure log error message."""
with mock.patch('django.core.mail.message.EmailMessage.send', side_effect=TypeError):
with LogCapture(emails.logger.name) as l:
emails.send_email_preview_page_is_available(self.run_state.course_run)
l.check(
(
emails.logger.name,
'ERROR',
'Failed to send email notifications for preview available of course-run {}'.format(
self.run_state.course_run.id
)
)
)
......@@ -1198,6 +1198,36 @@ class CourseRunDetailTests(TestCase):
)
self.assertContains(response, 'Approved')
def test_course_preview(self):
"""Verify that publisher user can see preview widget."""
factories.CourseUserRoleFactory(course=self.course, user=self.user, role=PublisherUserRole.Publisher)
self.course_run_state.name = CourseStateChoices.Approved
self.course_run_state.save()
self.user.groups.add(self.organization_extension.group)
assign_perm(OrganizationExtension.VIEW_COURSE, self.organization_extension.group, self.organization_extension)
preview_api_url = reverse('publisher:api:update_course_run', args=[self.course_run.id])
response = self.client.get(self.page_url)
self.assertContains(response, 'COURSE PREVIEW')
self.assertContains(
response,
'<button data-url="{url}" class="btn btn-neutral btn-edit-preview-url">'.format(url=preview_api_url)
)
# verify with out preview_url
self.course_run.preview_url = None
self.course_run.save()
response = self.client.get(self.page_url)
self.assertContains(response, 'COURSE PREVIEW')
self.assertContains(
response,
'<button data-url="{url}" class="btn btn-neutral btn-save-preview-url">'.format(url=preview_api_url)
)
self.assertContains(response, '<input id="id-review-url" type="text">')
# pylint: disable=attribute-defined-outside-init
@ddt.ddt
......
......@@ -764,7 +764,9 @@ def get_course_role_widgets_data(user, course, state_object, change_state_url):
if history_record:
role_widget['reviewed'] = history_record.modified
elif state_object.name != CourseStateChoices.Draft:
elif ((state_object.name != CourseStateChoices.Draft and course_role.role != state_object.owner_role) or
state_object.name == CourseRunStateChoices.Approved):
history_record = state_object.history.filter(
name=CourseStateChoices.Review
).order_by('-modified').first()
......
......@@ -7,14 +7,14 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-23 12:44+0500\n"
"POT-Creation-Date: 2017-02-23 17:25+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"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
#: apps/api/filters.py
#, python-brace-format
......@@ -431,6 +431,11 @@ msgstr ""
#: apps/publisher/api/serializers.py
#, python-brace-format
msgid "Invalid URL format \"{preview_url}\""
msgstr ""
#: apps/publisher/api/serializers.py
#, python-brace-format
msgid "Cannot switch from state `{state}` to `{target_state}`"
msgstr ""
......@@ -507,6 +512,11 @@ msgstr ""
msgid "Preview for {run_name} has been approved"
msgstr ""
#: apps/publisher/emails.py
#, python-brace-format
msgid "Preview for {run_name} is available"
msgstr ""
#: apps/publisher/forms.py
msgid "Remove Image"
msgstr ""
......@@ -879,7 +889,9 @@ msgstr ""
msgid "Edit Comment"
msgstr ""
#: templates/comments/edit_comment.html templates/publisher/seat_form.html
#: templates/comments/edit_comment.html
#: templates/publisher/course_run_detail/_widgets.html
#: templates/publisher/seat_form.html
msgid "Save"
msgstr ""
......@@ -2171,6 +2183,15 @@ msgid "Accept"
msgstr ""
#: templates/publisher/course_run_detail/_widgets.html
msgid "Submitted for review"
msgstr ""
#: templates/publisher/course_run_detail/_widgets.html
#: templates/publisher/courses.html
msgid "Edit"
msgstr ""
#: templates/publisher/course_run_detail/_widgets.html
msgid "COURSE PREVIEW"
msgstr ""
......@@ -2238,10 +2259,6 @@ msgstr ""
msgid "Runs"
msgstr ""
#: templates/publisher/courses.html
msgid "Edit"
msgstr ""
#: templates/publisher/dashboard.html
msgid "Course runs"
msgstr ""
......@@ -2344,6 +2361,8 @@ msgstr ""
#: templates/publisher/email/course_created.txt
#: templates/publisher/email/course_run/preview_accepted.html
#: templates/publisher/email/course_run/preview_accepted.txt
#: templates/publisher/email/course_run/preview_available.html
#: templates/publisher/email/course_run/preview_available.txt
msgid "The edX team"
msgstr ""
......@@ -2397,6 +2416,8 @@ msgstr ""
#: templates/publisher/email/course_run/mark_as_reviewed.txt
#: templates/publisher/email/course_run/preview_accepted.html
#: templates/publisher/email/course_run/preview_accepted.txt
#: templates/publisher/email/course_run/preview_available.html
#: templates/publisher/email/course_run/preview_available.txt
#: templates/publisher/email/course_run/send_for_review.html
#: templates/publisher/email/course_run/send_for_review.txt
#: templates/publisher/email/studio_instance_created.html
......@@ -2410,6 +2431,7 @@ msgstr ""
#: templates/publisher/email/course/send_for_review.html
#: templates/publisher/email/course_run/mark_as_reviewed.html
#: templates/publisher/email/course_run/preview_accepted.html
#: templates/publisher/email/course_run/preview_available.html
#: templates/publisher/email/course_run/send_for_review.html
#: templates/publisher/email/studio_instance_created.html
#, python-format
......@@ -2427,6 +2449,7 @@ msgstr ""
#: templates/publisher/email/course/send_for_review.txt
#: templates/publisher/email/course_run/mark_as_reviewed.txt
#: templates/publisher/email/course_run/preview_accepted.txt
#: templates/publisher/email/course_run/preview_available.txt
#: templates/publisher/email/course_run/send_for_review.txt
#: templates/publisher/email/studio_instance_created.txt
#, python-format
......@@ -2498,6 +2521,8 @@ msgstr ""
#: templates/publisher/email/course_run/preview_accepted.html
#: templates/publisher/email/course_run/preview_accepted.txt
#: templates/publisher/email/course_run/preview_available.html
#: templates/publisher/email/course_run/preview_available.txt
msgid "Dear member,"
msgstr ""
......@@ -2514,6 +2539,18 @@ msgid ""
"Preview for %(course_name)s has beed approved by course team. %(page_url)s"
msgstr ""
#: templates/publisher/email/course_run/preview_available.html
#, python-format
msgid ""
"Preview for %(link_start)s%(page_url)s%(link_middle)s %(course_name)s "
"%(link_end)s is available for review."
msgstr ""
#: templates/publisher/email/course_run/preview_available.txt
#, python-format
msgid "Preview for %(course_name)s is available for review. %(page_url)s"
msgstr ""
#: templates/publisher/email/course_run/send_for_review.html
#, python-format
msgid ""
......
......@@ -7,14 +7,14 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-23 12:44+0500\n"
"POT-Creation-Date: 2017-02-23 17:25+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"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
#: static/js/catalogs-change-form.js
msgid "Preview"
......@@ -36,6 +36,14 @@ msgstr ""
msgid "File must be smaller than 1 megabyte in size."
msgstr ""
#: static/js/publisher/publisher.js
msgid "Please enter a valid URL."
msgstr ""
#: static/js/publisher/publisher.js
msgid "Save"
msgstr ""
#: static/js/publisher/views/dashboard.js
msgid ""
"You have successfully created a studio instance ({studioLinkTag}) for "
......
......@@ -7,14 +7,14 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-23 12:44+0500\n"
"POT-Creation-Date: 2017-02-23 17:25+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"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: apps/api/filters.py
......@@ -541,6 +541,11 @@ msgstr "Ìnvälïd çöürsé kéý \"{lms_course_id}\" Ⱡ'σяєм ιρѕυм
#: apps/publisher/api/serializers.py
#, python-brace-format
msgid "Invalid URL format \"{preview_url}\""
msgstr "Ìnvälïd ÛRL förmät \"{preview_url}\" Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢ση#"
#: apps/publisher/api/serializers.py
#, python-brace-format
msgid "Cannot switch from state `{state}` to `{target_state}`"
msgstr ""
"Çännöt swïtçh fröm stäté `{state}` tö `{target_state}` Ⱡ'σяєм ιρѕυм ∂σłσя "
......@@ -630,6 +635,12 @@ msgstr ""
"Prévïéw för {run_name} häs ßéén äpprövéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, "
"¢σηѕє¢тє#"
#: apps/publisher/emails.py
#, python-brace-format
msgid "Preview for {run_name} is available"
msgstr ""
"Prévïéw för {run_name} ïs äväïläßlé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"
#: apps/publisher/forms.py
msgid "Remove Image"
msgstr "Rémövé Ìmägé Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
......@@ -1037,7 +1048,9 @@ msgstr "Çöürsé Rün Förm Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#"
msgid "Edit Comment"
msgstr "Édït Çömmént Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
#: templates/comments/edit_comment.html templates/publisher/seat_form.html
#: templates/comments/edit_comment.html
#: templates/publisher/course_run_detail/_widgets.html
#: templates/publisher/seat_form.html
msgid "Save"
msgstr "Sävé Ⱡ'σяєм ι#"
......@@ -2555,6 +2568,15 @@ msgid "Accept"
msgstr "Àççépt Ⱡ'σяєм ιρѕυ#"
#: templates/publisher/course_run_detail/_widgets.html
msgid "Submitted for review"
msgstr "Süßmïttéd för révïéw Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
#: templates/publisher/course_run_detail/_widgets.html
#: templates/publisher/courses.html
msgid "Edit"
msgstr "Édït Ⱡ'σяєм ι#"
#: templates/publisher/course_run_detail/_widgets.html
msgid "COURSE PREVIEW"
msgstr "ÇÖÛRSÉ PRÉVÌÉW Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#"
......@@ -2629,10 +2651,6 @@ msgstr ""
msgid "Runs"
msgstr "Rüns Ⱡ'σяєм ι#"
#: templates/publisher/courses.html
msgid "Edit"
msgstr "Édït Ⱡ'σяєм ι#"
#: templates/publisher/dashboard.html
msgid "Course runs"
msgstr "Çöürsé rüns Ⱡ'σяєм ιρѕυм ∂σłσя #"
......@@ -2754,6 +2772,8 @@ msgstr "Vïéw Çöürsé Ⱡ'σяєм ιρѕυм ∂σłσя #"
#: templates/publisher/email/course_created.txt
#: templates/publisher/email/course_run/preview_accepted.html
#: templates/publisher/email/course_run/preview_accepted.txt
#: templates/publisher/email/course_run/preview_available.html
#: templates/publisher/email/course_run/preview_available.txt
msgid "The edX team"
msgstr "Thé édX téäm Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
......@@ -2811,6 +2831,8 @@ msgstr ""
#: templates/publisher/email/course_run/mark_as_reviewed.txt
#: templates/publisher/email/course_run/preview_accepted.html
#: templates/publisher/email/course_run/preview_accepted.txt
#: templates/publisher/email/course_run/preview_available.html
#: templates/publisher/email/course_run/preview_available.txt
#: templates/publisher/email/course_run/send_for_review.html
#: templates/publisher/email/course_run/send_for_review.txt
#: templates/publisher/email/studio_instance_created.html
......@@ -2824,6 +2846,7 @@ msgstr "Thänks, Ⱡ'σяєм ιρѕυм #"
#: templates/publisher/email/course/send_for_review.html
#: templates/publisher/email/course_run/mark_as_reviewed.html
#: templates/publisher/email/course_run/preview_accepted.html
#: templates/publisher/email/course_run/preview_available.html
#: templates/publisher/email/course_run/send_for_review.html
#: templates/publisher/email/studio_instance_created.html
#, python-format
......@@ -2845,6 +2868,7 @@ msgstr ""
#: templates/publisher/email/course/send_for_review.txt
#: templates/publisher/email/course_run/mark_as_reviewed.txt
#: templates/publisher/email/course_run/preview_accepted.txt
#: templates/publisher/email/course_run/preview_available.txt
#: templates/publisher/email/course_run/send_for_review.txt
#: templates/publisher/email/studio_instance_created.txt
#, python-format
......@@ -2945,6 +2969,8 @@ msgstr ""
#: templates/publisher/email/course_run/preview_accepted.html
#: templates/publisher/email/course_run/preview_accepted.txt
#: templates/publisher/email/course_run/preview_available.html
#: templates/publisher/email/course_run/preview_available.txt
msgid "Dear member,"
msgstr "Déär mémßér, Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
......@@ -2966,6 +2992,23 @@ msgstr ""
"Prévïéw för %(course_name)s häs ßééd äpprövéd ßý çöürsé téäm. %(page_url)s "
"Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"
#: templates/publisher/email/course_run/preview_available.html
#, python-format
msgid ""
"Preview for %(link_start)s%(page_url)s%(link_middle)s %(course_name)s "
"%(link_end)s is available for review."
msgstr ""
"Prévïéw för %(link_start)s%(page_url)s%(link_middle)s %(course_name)s "
"%(link_end)s ïs äväïläßlé för révïéw. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, "
"¢σηѕє¢тєтυя α#"
#: templates/publisher/email/course_run/preview_available.txt
#, python-format
msgid "Preview for %(course_name)s is available for review. %(page_url)s"
msgstr ""
"Prévïéw för %(course_name)s ïs äväïläßlé för révïéw. %(page_url)s Ⱡ'σяєм "
"ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #"
#: templates/publisher/email/course_run/send_for_review.html
#, python-format
msgid ""
......
......@@ -7,14 +7,14 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-23 12:44+0500\n"
"POT-Creation-Date: 2017-02-23 17:25+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"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: static/js/catalogs-change-form.js
......@@ -39,6 +39,14 @@ msgstr ""
"Fïlé müst ßé smällér thän 1 mégäßýté ïn sïzé. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, "
"¢σηѕє¢тєтυя #"
#: static/js/publisher/publisher.js
msgid "Please enter a valid URL."
msgstr "Pléäsé éntér ä välïd ÛRL. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
#: static/js/publisher/publisher.js
msgid "Save"
msgstr "Sävé Ⱡ'σяєм ι#"
#: static/js/publisher/views/dashboard.js
msgid ""
"You have successfully created a studio instance ({studioLinkTag}) for "
......
......@@ -328,3 +328,40 @@ function clearModalError($modal) {
$('#modal-errors').html('');
$('#modal-errors').hide();
}
$(document).on('click', '.btn-save-preview-url', function (e) {
preview_url = $('#id-review-url').val();
if (!preview_url) {
showInvalidPreviewUrlError();
return
}
$.ajax({
type: "PATCH",
url: $(this).data('url'),
data: JSON.stringify({'preview_url': preview_url}),
contentType: "application/json",
success: function (response) {
location.reload();
},
error: function (response) {
showInvalidPreviewUrlError();
}
});
});
function showInvalidPreviewUrlError() {
$('#id-review-url').addClass('has-error');
$('.error-message').html(gettext("Please enter a valid URL."));
}
$(document).on('click', '.btn-edit-preview-url', function (e) {
var $previewUrl = $('.preview-url'),
currentUrl = $previewUrl.find('a').attr('href'),
html = '<input id="id-review-url" type="text" value='+ currentUrl +'>';
$(this).addClass('btn-save-preview-url').removeClass('btn-edit-preview-url');
$(this).text(gettext("Save"));
$('.preview-status').remove();
$previewUrl.html(html);
$('#id-review-url').focus();
});
......@@ -339,8 +339,8 @@
.approval-widget, .course-widgets {
.btn-course-edit, .btn-courserun-edit, .btn-change-state, .btn-preview {
.btn-course-edit, .btn-courserun-edit, .btn-change-state, .btn-preview, .btn-save-preview-url, .btn-edit-preview-url {
@include padding(2px, 20px, 3px, 20px);
@include float(right);
font-weight: 400;
......@@ -605,3 +605,22 @@
background-color: #F2F2F2;
padding: 20px;
}
.preview-container {
#id-review-url {
width: 360px;
}
.btn-save-preview-url {
margin-top: 25px;
}
.preview-status {
@include float(right);
font-size: 12px;
}
.has-error{
border-color: red;
}
.error-message {
color: red;
}
}
......@@ -14,7 +14,8 @@
<div class="preview-container">
<div class="layout-1q3q layout-reversed">
<div class="layout-col layout-col-a">
{% if object.preview_url and object.course.course_team_admin == request.user and not object.course_run_state.is_preview_accepted %}
{% if object.preview_url %}
{% if object.course.course_team_admin == request.user and not object.course_run_state.is_preview_accepted %}
<button class="btn btn-neutral btn-preview btn-preview-decline" type="button">
{% trans "Decline" %}
</button>
......@@ -27,18 +28,29 @@
{% trans "Approved" %}<br>
{{ preview_accepted_date|date:'m/d/y H:i a' }}
</span>
{% elif object.course.publisher == request.user %}
<span class="preview-status">{% trans "Submitted for review" %}</span>
<button data-url="{% url 'publisher:api:update_course_run' object.id %}" class="btn btn-neutral btn-edit-preview-url">{% trans "Edit" %}</button>
{% endif %}
{% elif object.course.publisher == request.user %}
<button data-url="{% url 'publisher:api:update_course_run' object.id %}" class="btn btn-neutral btn-save-preview-url">{% trans "Save" %}</button>
{% endif %}
</div>
<div class="layout-col layout-col-b">
<span class="preview-heading">
<strong>{% trans "COURSE PREVIEW" %}</strong>
</span>
<div>
<span class="preview-url-heading">{% trans "Preview URL" %} - </span>
<div class="preview-url">
{% if object.preview_url %}
<span class="preview-url-heading">{% trans "Preview URL" %} - </span>
<a href="{{ object.preview_url }}" target="_blank">{% trans "View course preview live" %}</a>
{% else %}
{% trans "Not available" %}
{% if object.course.publisher == request.user %}
<input id="id-review-url" type="text">
<span class="error-message"></span>
{% else %}
<span>{% trans "Not available" %}</span>
{% endif %}
{% endif %}
</div>
</div>
......
......@@ -49,7 +49,7 @@
</td>
<td class="form-group">
<input type="text" class="field-input input-text small" aria-labelledby="course-title-{{ course_run.title }} column-title" />
<button data-update-course-key-url="{% url 'publisher:api:update_course_key' course_run.id %}" class="btn-inline btn-add-course-key">{% trans "Add" %}</button>
<button data-update-course-key-url="{% url 'publisher:api:update_course_run' course_run.id %}" class="btn-inline btn-add-course-key">{% trans "Add" %}</button>
</td>
</tr>
{% endfor %}
......
{% extends "publisher/email/email_base.html" %}
{% load i18n %}
{% block body %}
<!-- Message Body -->
<p>
{% blocktrans trimmed %}
Dear member,
{% endblocktrans %}
<p>
{% blocktrans with link_start='<a href="' link_middle='">' link_end='</a>' trimmed %}
Preview for {{ link_start }}{{ page_url }}{{ link_middle }} {{ course_name }} {{ link_end }} is available for review.
{% endblocktrans %}
</p>
{% comment %}Translators: It's closing of mail.{% endcomment %}
{% trans "Thanks," %}<br>
{% trans "The edX team" %}
{% blocktrans trimmed %}
<p>Note: This email address is unable to receive replies. For questions or comments, contact {{ contact_us_email }}.</p>
{% endblocktrans %}
<!-- End Message Body -->
{% endblock body %}
{% load i18n %}
{% blocktrans trimmed %}
Dear member,
{% endblocktrans %}
{% blocktrans trimmed %}
Preview for {{ course_name }} is available for review. {{ page_url }}
{% endblocktrans %}
{% trans "Thanks," %}
{% trans "The edX team" %}
{% blocktrans trimmed %}
Note: This email address is unable to receive replies. For questions or comments, contact {{ contact_us_email }}.
{% endblocktrans %}
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