Commit 41ec68db by Waheed Ahmed

Create course run from dashboard.

ECOM-7758
parent b08c6361
# pylint: disable=no-member
import json
from urllib.parse import quote
import ddt
from django.contrib.auth.models import Group
......@@ -20,7 +21,7 @@ from course_discovery.apps.course_metadata.tests.factories import OrganizationFa
from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.api import views
from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.constants import INTERNAL_USER_GROUP_NAME
from course_discovery.apps.publisher.constants import ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME
from course_discovery.apps.publisher.models import (Course, CourseRun, CourseRunState, CourseState,
OrganizationExtension, Seat)
from course_discovery.apps.publisher.tests import JSON_CONTENT_TYPE, factories
......@@ -825,3 +826,70 @@ class RevertCourseByRevisionTests(TestCase):
'publisher:api:course_revision_revert', kwargs={'history_id': revision_id}
)
return self.client.put(path=course_revision_path)
class CoursesAutoCompleteTests(TestCase):
""" Tests for course autocomplete."""
def setUp(self):
super(CoursesAutoCompleteTests, self).setUp()
self.user = UserFactory()
self.course = factories.CourseFactory(title='Test course 1')
self.course2 = factories.CourseFactory(title='Test course 2')
self.organization_extension = factories.OrganizationExtensionFactory()
self.course.organizations.add(self.organization_extension.organization)
self.user.groups.add(self.organization_extension.group)
assign_perm(
OrganizationExtension.VIEW_COURSE, self.organization_extension.group, self.organization_extension
)
self.client.login(username=self.user.username, password=USER_PASSWORD)
self.course_autocomplete_url = reverse('publisher:api:course-autocomplete') + '?q={title}'
def test_course_autocomplete_without_login(self):
""" Verify course autocomplete without login. """
self.client.logout()
self.course_autocomplete_url = self.course_autocomplete_url.format(title='test')
response = self.client.get(self.course_autocomplete_url)
self.assertRedirects(
response,
expected_url='{url}?next={next}'.format(
url=reverse('login'),
next=quote(self.course_autocomplete_url)
),
status_code=302,
target_status_code=302
)
def test_course_autocomplete_with_course_team(self):
""" Verify course autocomplete returns data for course team user. """
response = self.client.get(self.course_autocomplete_url.format(title='test'))
self._assert_response(response, 1)
response = self.client.get(
self.course_autocomplete_url.format(title='dummy')
)
self._assert_response(response, 0)
def test_course_autocomplete_with_admin(self):
""" Verify course autocomplete returns all courses for publisher admin. """
self.user.groups.remove(self.organization_extension.group)
self.user.groups.add(Group.objects.get(name=ADMIN_GROUP_NAME))
response = self.client.get(self.course_autocomplete_url.format(title='test'))
self._assert_response(response, 2)
def test_course_autocomplete_with_internal_user(self):
""" Verify course autocomplete returns all courses for publisher admin. """
self.user.groups.remove(self.organization_extension.group)
self.user.groups.add(Group.objects.get(name=INTERNAL_USER_GROUP_NAME))
factories.CourseUserRoleFactory(course=self.course2, user=self.user, role=PublisherUserRole.MarketingReviewer)
response = self.client.get(self.course_autocomplete_url.format(title='test'))
self._assert_response(response, 1)
def _assert_response(self, response, expected_length):
""" Assert autocomplete response. """
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode('utf-8'))
self.assertEqual(len(data['results']), expected_length)
......@@ -3,8 +3,8 @@ from django.conf.urls import url
from course_discovery.apps.publisher.api.views import (ChangeCourseRunStateView, ChangeCourseStateView,
CourseRevisionDetailView, CourseRoleAssignmentView,
OrganizationGroupUserView, RevertCourseRevisionView,
UpdateCourseRunView)
CoursesAutoComplete, OrganizationGroupUserView,
RevertCourseRevisionView, UpdateCourseRunView)
urlpatterns = [
url(r'^course_role_assignments/(?P<pk>\d+)/$', CourseRoleAssignmentView.as_view(), name='course_role_assignments'),
......@@ -18,4 +18,5 @@ urlpatterns = [
r'^course/revision/(?P<history_id>\d+)/revert/$',
RevertCourseRevisionView.as_view(), name='course_revision_revert'
),
url(r'^course-autocomplete/$', CoursesAutoComplete.as_view(), name='course-autocomplete'),
]
import logging
from dal import autocomplete
from django.apps import apps
from django.contrib.auth.mixins import LoginRequiredMixin
from guardian.shortcuts import get_objects_for_user
from rest_framework import status
from rest_framework.generics import ListAPIView, RetrieveAPIView, UpdateAPIView, get_object_or_404
from rest_framework.permissions import IsAuthenticated
......@@ -16,6 +19,7 @@ from course_discovery.apps.publisher.api.serializers import (CourseRevisionSeria
from course_discovery.apps.publisher.forms import CustomCourseForm
from course_discovery.apps.publisher.models import (Course, CourseRun, CourseRunState, CourseState, CourseUserRole,
OrganizationExtension)
from course_discovery.apps.publisher.utils import is_internal_user, is_publisher_admin
logger = logging.getLogger(__name__)
......@@ -88,3 +92,28 @@ class RevertCourseRevisionView(APIView):
return Response(status=status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT)
class CoursesAutoComplete(LoginRequiredMixin, autocomplete.Select2QuerySetView):
""" Course Autocomplete. """
def get_queryset(self):
if self.q:
user = self.request.user
if is_publisher_admin(user):
qs = Course.objects.filter(title__icontains=self.q)
elif is_internal_user(user):
qs = Course.objects.filter(title__icontains=self.q, course_user_roles__user=user).distinct()
else:
organizations = get_objects_for_user(
user,
OrganizationExtension.VIEW_COURSE,
OrganizationExtension,
use_groups=True,
with_superuser=False
).values_list('organization')
qs = Course.objects.filter(title__icontains=self.q, organizations__in=organizations)
return qs
return []
......@@ -192,6 +192,21 @@ class CustomCourseForm(CourseForm):
self.fields['video_link'].widget = forms.HiddenInput()
class CourseSearchForm(forms.Form):
""" Course Type ahead Search Form. """
course = forms.ModelChoiceField(
label=_('Find Course By Title'),
queryset=Course.objects.all(),
widget=autocomplete.ModelSelect2(
url='publisher:api:course-autocomplete',
attrs={
'data-minimum-input-length': 3,
}
),
required=True,
)
class CourseRunForm(BaseCourseForm):
""" Course Run Form. """
......
......@@ -2764,3 +2764,97 @@ class CourseRevisionViewTests(TestCase):
kwargs={'pk': course_id, 'revision_id': revision_id})
return self.client.get(path=revision_path)
class CreateRunFromDashboardViewTests(TestCase):
""" Tests for the publisher `CreateRunFromDashboardView`. """
def setUp(self):
super(CreateRunFromDashboardViewTests, self).setUp()
self.user = UserFactory()
self.course = factories.CourseFactory()
factories.CourseStateFactory(course=self.course)
factories.CourseUserRoleFactory.create(course=self.course, role=PublisherUserRole.CourseTeam, user=self.user)
self.organization_extension = factories.OrganizationExtensionFactory()
self.course.organizations.add(self.organization_extension.organization)
self.user.groups.add(self.organization_extension.group)
assign_perm(
OrganizationExtension.VIEW_COURSE_RUN, self.organization_extension.group, self.organization_extension
)
self.client.login(username=self.user.username, password=USER_PASSWORD)
self.create_course_run_url = reverse('publisher:publisher_create_run_from_dashboard')
def test_courserun_form_without_login(self):
""" Verify that user can't access new course run form page when not logged in. """
self.client.logout()
response = self.client.get(self.create_course_run_url)
self.assertRedirects(
response,
expected_url='{url}?next={next}'.format(
url=reverse('login'),
next=self.create_course_run_url
),
status_code=302,
target_status_code=302
)
self.client.login(username=self.user.username, password=USER_PASSWORD)
response = self.client.get(self.create_course_run_url)
self.assertEqual(response.status_code, 200)
def _post_data(self):
return {
'course': self.course.id,
'start': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'end': (datetime.now() + timedelta(days=60)).strftime('%Y-%m-%d %H:%M:%S'),
'pacing_type': 'self_paced',
'type': Seat.VERIFIED,
'price': 450
}
def test_create_course_run_and_seat_without_parent_course(self):
""" Verify that user cannot create course run without selecting parent course.
"""
post_data = self._post_data()
post_data.pop('course')
response = self.client.post(self.create_course_run_url, post_data)
self.assertContains(response, 'Please fill all required fields.', status_code=400)
def test_create_course_run_and_seat(self):
""" Verify that we can create a new course run with seat. """
self.assertEqual(self.course.course_runs.count(), 0)
new_user = factories.UserFactory()
new_user.groups.add(self.organization_extension.group)
factories.CourseUserRoleFactory.create(
course=self.course, role=PublisherUserRole.ProjectCoordinator, user=factories.UserFactory()
)
self.assertEqual(self.course.course_team_admin, self.user)
post_data = self._post_data()
response = self.client.post(self.create_course_run_url, self._post_data())
self.assertEqual(self.course.course_runs.count(), 1)
new_seat = Seat.objects.get(type=post_data['type'], price=post_data['price'])
self.assertRedirects(
response,
expected_url=reverse('publisher:publisher_course_run_detail', kwargs={'pk': new_seat.course_run.id}),
status_code=302,
target_status_code=200
)
self.assertEqual(new_seat.type, Seat.VERIFIED)
self.assertEqual(new_seat.price, post_data['price'])
# Verify that and email is sent for studio instance request to project coordinator.
self.assertEqual(len(mail.outbox), 1)
self.assertEqual([self.course.project_coordinator.email], mail.outbox[0].to)
expected_subject = 'New Studio instance request for {title}'.format(title=self.course.title)
self.assertEqual(str(mail.outbox[0].subject), expected_subject)
......@@ -20,6 +20,12 @@ urlpatterns = [
url(r'^course_runs/(?P<pk>\d+)/$', views.CourseRunDetailView.as_view(), name='publisher_course_run_detail'),
url(r'^course_runs/(?P<pk>\d+)/edit/$', views.CourseRunEditView.as_view(), name='publisher_course_runs_edit'),
url(
r'^course_runs/new/$',
views.CreateRunFromDashboardView.as_view(),
name='publisher_create_run_from_dashboard'
),
url(
r'^user/toggle/email_settings/$',
views.ToggleEmailNotification.as_view(),
name='publisher_toggle_email_settings'
......
......@@ -24,7 +24,8 @@ 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.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.forms import CustomCourseForm, CustomCourseRunForm, CustomSeatForm
from course_discovery.apps.publisher.forms import (CourseSearchForm, CustomCourseForm, CustomCourseRunForm,
CustomSeatForm)
from course_discovery.apps.publisher.models import (Course, CourseRun, CourseRunState, CourseState, CourseUserRole,
OrganizationExtension, Seat, UserAttributes)
from course_discovery.apps.publisher.utils import (get_internal_users, has_role_for_course, is_internal_user,
......@@ -565,6 +566,39 @@ class CreateCourseRunView(mixins.LoginRequiredMixin, CreateView):
return render(request, self.template_name, context, status=400)
class CreateRunFromDashboardView(CreateCourseRunView):
""" Create Course Run From Dashboard With Type ahead Search For Parent Course."""
course_form = CourseSearchForm
def get_context_data(self, **kwargs):
context = {
'from_dashboard': True,
'course_form': self.course_form(),
'run_form': self.run_form(),
'seat_form': self.seat_form()
}
return context
def post(self, request, *args, **kwargs):
course_form = self.course_form(request.POST)
run_form = self.run_form(request.POST)
seat_form = self.seat_form(request.POST)
if course_form.is_valid() and run_form.is_valid() and seat_form.is_valid():
self.parent_course = course_form.cleaned_data.get('course')
return super(CreateRunFromDashboardView, self).post(request, *args, **kwargs)
messages.error(request, _('Please fill all required fields.'))
context = self.get_context_data()
context.update(
{
'course_form': course_form,
'run_form': run_form,
'seat_form': seat_form,
}
)
return render(request, self.template_name, context, status=400)
class CourseRunEditView(mixins.LoginRequiredMixin, mixins.PublisherPermissionMixin, UpdateView):
""" Course Run Edit View."""
model = CourseRun
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-09 15:18+0500\n"
"POT-Creation-Date: 2017-05-09 17:22+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"
......@@ -616,6 +616,10 @@ msgid "Syllabus"
msgstr ""
#: apps/publisher/forms.py
msgid "Find Course By Title"
msgstr ""
#: apps/publisher/forms.py
msgid "Course Start Date"
msgstr ""
......@@ -1415,6 +1419,18 @@ msgid ""
msgstr ""
#: templates/publisher/add_courserun_form.html
msgid "FIND COURSE BY TITLE"
msgstr ""
#: templates/publisher/add_courserun_form.html
#, python-format
msgid ""
"\n"
" Find the course that you are creating a run for by typing in the title. If you don't find a match, please check that the name is correct or %(link_start)s%(new_course_url)s%(link_middle)screate a new course%(link_end)s.\n"
" "
msgstr ""
#: templates/publisher/add_courserun_form.html
#: templates/publisher/course_run/edit_run_form.html
msgid "COURSE START DATE"
msgstr ""
......@@ -2494,6 +2510,10 @@ msgid "Add a New Course"
msgstr ""
#: templates/publisher/dashboard.html
msgid "Add a Course Run"
msgstr ""
#: templates/publisher/dashboard.html
msgid ""
"EdX Publisher is a companion to edX Studio. Course teams enter About page "
"information in Publisher, and course content in Studio."
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-09 15:18+0500\n"
"POT-Creation-Date: 2017-05-09 17:22+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"
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-09 15:18+0500\n"
"POT-Creation-Date: 2017-05-09 17:22+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"
......@@ -742,6 +742,10 @@ msgid "Syllabus"
msgstr "Sýlläßüs Ⱡ'σяєм ιρѕυм ∂#"
#: apps/publisher/forms.py
msgid "Find Course By Title"
msgstr "Fïnd Çöürsé Bý Tïtlé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
#: apps/publisher/forms.py
msgid "Course Start Date"
msgstr "Çöürsé Stärt Däté Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмє#"
......@@ -1631,6 +1635,21 @@ msgstr ""
" Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм, qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ ¢σηѕєqυαт. ∂υιѕ αυтє#"
#: templates/publisher/add_courserun_form.html
msgid "FIND COURSE BY TITLE"
msgstr "FÌND ÇÖÛRSÉ BÝ TÌTLÉ Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
#: templates/publisher/add_courserun_form.html
#, python-format
msgid ""
"\n"
" Find the course that you are creating a run for by typing in the title. If you don't find a match, please check that the name is correct or %(link_start)s%(new_course_url)s%(link_middle)screate a new course%(link_end)s.\n"
" "
msgstr ""
"\n"
" Fïnd thé çöürsé thät ýöü äré çréätïng ä rün för ßý týpïng ïn thé tïtlé. Ìf ýöü dön't fïnd ä mätçh, pléäsé çhéçk thät thé nämé ïs çörréçt ör %(link_start)s%(new_course_url)s%(link_middle)sçréäté ä néw çöürsé%(link_end)s.\n"
" Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм, qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ ¢σηѕєqυαт. ∂υιѕ αυтє#"
#: templates/publisher/add_courserun_form.html
#: templates/publisher/course_run/edit_run_form.html
msgid "COURSE START DATE"
msgstr "ÇÖÛRSÉ STÀRT DÀTÉ Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмє#"
......@@ -2940,6 +2959,10 @@ msgid "Add a New Course"
msgstr "Àdd ä Néw Çöürsé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#"
#: templates/publisher/dashboard.html
msgid "Add a Course Run"
msgstr "Àdd ä Çöürsé Rün Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#"
#: templates/publisher/dashboard.html
msgid ""
"EdX Publisher is a companion to edX Studio. Course teams enter About page "
"information in Publisher, and course content in Studio."
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-09 15:18+0500\n"
"POT-Creation-Date: 2017-05-09 17:22+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"
......
......@@ -116,3 +116,16 @@ $btn-filter-color: rgba(228, 228, 228, 1);
border-radius: 8px;
}
.btn-add-course {
background-color: #34495E;
border: solid 1px #243342;
color: #F5F9FD;
font-size: 16px;
}
.btn-add-courserun {
background-color: #00A0E3;
border: solid 1px #0491CB;
color: #F5F9FD;
font-size: 16px;
}
......@@ -179,3 +179,7 @@
@include text-align(right);
width: 100%;
}
.error-text {
color: #b20610;
}
......@@ -435,15 +435,8 @@
display: inline-block;
}
.btn-add-course {
background-color: #34495E;
border: solid 1px #243342;
color: #F5F9FD;
font-size: 16px;
}
.about-publisher {
max-width: 75%;
max-width: 60%;
}
.about-and-button {
......
......@@ -30,6 +30,30 @@
<div class="course-form">
<div class="course-information">
<fieldset class="form-group grid-container grid-manual">
{% if from_dashboard %}
<div class="field-title">{% trans "FIND COURSE BY TITLE" %}</div>
<div class="row">
<div class="col col-6 help-text">
<p>
{% url 'publisher:publisher_courses_new' as new_course_url %}
{% blocktrans with link_start='<a href="' link_middle='">' link_end='</a>' %}
Find the course that you are creating a run for by typing in the title. If you don't find a match, please check that the name is correct or {{ link_start }}{{ new_course_url }}{{ link_middle }}create a new course{{ link_end }}.
{% endblocktrans%}
</p>
</div>
<div class="col col-6">
<label class="field-label ">
{{ course_form.course.label }}
<span class="required">*</span>
</label>
{{ course_form.course }}
{% if course_form.course.errors %}
<div class="error-text">{{ course_form.course.errors.as_text }}</div>
{% endif %}
</div>
</div>
{% endif %}
<div class="field-title">{% trans "COURSE START DATE" %}</div>
<div class="row">
<div class="col col-6 help-text">
......@@ -127,8 +151,10 @@
{% endblock %}
{% block extra_js %}
<script src="{% static 'js/publisher/organizations.js' %}"></script>
<script src="{% static 'js/publisher/course-tabs.js' %}"></script>
<script src="{% static 'js/publisher/seat-type-change.js' %}"></script>
<script src="{% static 'js/publisher/change-admin.js' %}"></script>
{% endblock %}
{% block js_without_compress %}
{{ course_form.media }}
{% endblock %}
......@@ -12,12 +12,15 @@
<div class="about-and-button">
<p class="about-publisher">{% trans "EdX Publisher is used to create course About pages. Users enter, review, and approve content in Publisher. Publisher keeps track of the details and sends email updates when actions are necessary." %}</p>
<a href="{% url 'publisher:publisher_courses_new' %}" class="btn btn-brand btn-add-course">
{% trans "Add a New Course" %}
{% trans "Add a New Course" %}
</a>
<a href="{% url 'publisher:publisher_create_run_from_dashboard' %}" class="btn btn-brand btn-add-courserun">
{% trans "Add a Course Run" %}
</a>
</div>
<p>{% trans "EdX Publisher is a companion to edX Studio. Course teams enter About page information in Publisher, and course content in Studio." %}</p>
{% with studio_count=studio_request_courses|length published_count=published_course_runs|length preview_count=preview_course_runs|length in_progress_count=in_progress_course_runs|length %}
<h2 class="hd-2 emphasized">{% trans "Course runs" %}</h2>
<h2 class="hd-2 emphasized course-runs-heading">{% trans "Course runs" %}</h2>
<ul role="tablist" class="tabs">
<li role="tab" id="tab-progress" class="tab" aria-selected="true" aria-expanded="false"
aria-controls="progress" tabindex="0">
......
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