Commit 4e0569de by tasawernawaz Committed by Tasawer Nawaz

Instructor module added on course form

ECOM-6051
parent e5f5def2
from django.db.models import Q from django.db.models import Q
from django.template.loader import render_to_string
from dal import autocomplete from dal import autocomplete
from .models import Course, CourseRun, Organization, Video from .models import Course, CourseRun, Organization, Video, Person
class CourseAutocomplete(autocomplete.Select2QuerySetView): class CourseAutocomplete(autocomplete.Select2QuerySetView):
...@@ -51,3 +52,25 @@ class VideoAutocomplete(autocomplete.Select2QuerySetView): ...@@ -51,3 +52,25 @@ class VideoAutocomplete(autocomplete.Select2QuerySetView):
return qs return qs
return [] return []
class PersonAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if self.request.user.is_authenticated() and self.request.user.is_staff:
qs = Person.objects.all()
if self.q:
qs = qs.filter(Q(given_name__icontains=self.q) | Q(family_name__icontains=self.q))
return qs
return []
def get_result_label(self, result):
context = {
'id': result.id,
'profile_image': result.profile_image_url,
'full_name': result.full_name,
'position': result.position if hasattr(result, 'position') else None
}
return render_to_string('publisher/_personLookup.html', context=context)
...@@ -9,6 +9,9 @@ from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWOR ...@@ -9,6 +9,9 @@ from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWOR
# pylint: disable=no-member # pylint: disable=no-member
from course_discovery.apps.course_metadata.tests.factories import PositionFactory
@ddt.ddt @ddt.ddt
class AutocompleteTests(TestCase): class AutocompleteTests(TestCase):
""" Tests for autocomplete lookups.""" """ Tests for autocomplete lookups."""
...@@ -20,6 +23,9 @@ class AutocompleteTests(TestCase): ...@@ -20,6 +23,9 @@ class AutocompleteTests(TestCase):
for course in self.courses: for course in self.courses:
factories.CourseRunFactory(course=course) factories.CourseRunFactory(course=course)
self.organizations = factories.OrganizationFactory.create_batch(3) self.organizations = factories.OrganizationFactory.create_batch(3)
first_instructor = factories.PersonFactory(given_name="First Instructor")
second_instructor = factories.PersonFactory(given_name="Second Instructor")
self.instructors = [first_instructor, second_instructor]
@ddt.data('dum', 'ing') @ddt.data('dum', 'ing')
def test_course_autocomplete(self, search_key): def test_course_autocomplete(self, search_key):
...@@ -134,3 +140,42 @@ class AutocompleteTests(TestCase): ...@@ -134,3 +140,42 @@ class AutocompleteTests(TestCase):
self.user = UserFactory(is_staff=False) self.user = UserFactory(is_staff=False)
self.user.save() self.user.save()
self.client.login(username=self.user.username, password=USER_PASSWORD) self.client.login(username=self.user.username, password=USER_PASSWORD)
def test_instructor_autocomplete(self):
""" Verify instructor autocomplete returns the data. """
response = self.client.get(
reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q='ins')
)
self._assert_response(response, 2)
# update first instructor's name
self.instructors[0].given_name = 'dummy_name'
self.instructors[0].save()
response = self.client.get(
reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q='dummy')
)
self._assert_response(response, 1)
def test_instructor_autocomplete_un_authorize_user(self):
""" Verify instructor autocomplete returns empty list for un-authorized users. """
self._make_user_non_staff()
response = self.client.get(reverse('admin_metadata:person-autocomplete'))
self._assert_response(response, 0)
def test_instructor_label(self):
""" Verify that instructor label contains position of instructor if it exists."""
position_title = 'professor'
PositionFactory.create(person=self.instructors[0], title=position_title, organization=self.organizations[0])
response = self.client.get(reverse('admin_metadata:person-autocomplete'))
self.assertContains(response, '<p>{position} at {organization}</p>'.format(
position=position_title,
organization=self.organizations[0].name))
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)
...@@ -5,8 +5,8 @@ from django.conf.urls import url ...@@ -5,8 +5,8 @@ from django.conf.urls import url
from course_discovery.apps.course_metadata.views import CourseRunSelectionAdmin from course_discovery.apps.course_metadata.views import CourseRunSelectionAdmin
from course_discovery.apps.course_metadata.lookups import ( from course_discovery.apps.course_metadata.lookups import (
CourseAutocomplete, CourseRunAutocomplete, OrganizationAutocomplete, VideoAutocomplete CourseAutocomplete, CourseRunAutocomplete, OrganizationAutocomplete, VideoAutocomplete,
) PersonAutocomplete)
urlpatterns = [ urlpatterns = [
url(r'^update_course_runs/(?P<pk>\d+)/$', CourseRunSelectionAdmin.as_view(), name='update_course_runs',), url(r'^update_course_runs/(?P<pk>\d+)/$', CourseRunSelectionAdmin.as_view(), name='update_course_runs',),
...@@ -14,4 +14,5 @@ urlpatterns = [ ...@@ -14,4 +14,5 @@ urlpatterns = [
url(r'^course-run-autocomplete/$', CourseRunAutocomplete.as_view(), name='course-run-autocomplete',), url(r'^course-run-autocomplete/$', CourseRunAutocomplete.as_view(), name='course-run-autocomplete',),
url(r'^organisation-autocomplete/$', OrganizationAutocomplete.as_view(), name='organisation-autocomplete',), url(r'^organisation-autocomplete/$', OrganizationAutocomplete.as_view(), name='organisation-autocomplete',),
url(r'^video-autocomplete/$', VideoAutocomplete.as_view(), name='video-autocomplete',), url(r'^video-autocomplete/$', VideoAutocomplete.as_view(), name='video-autocomplete',),
url(r'^person-autocomplete/$', PersonAutocomplete.as_view(), name='person-autocomplete',),
] ]
...@@ -3,6 +3,7 @@ Course publisher forms. ...@@ -3,6 +3,7 @@ Course publisher forms.
""" """
from dal import autocomplete from dal import autocomplete
from django import forms from django import forms
from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from course_discovery.apps.course_metadata.choices import CourseRunPacing from course_discovery.apps.course_metadata.choices import CourseRunPacing
...@@ -19,6 +20,15 @@ class UserModelChoiceField(forms.ModelChoiceField): ...@@ -19,6 +20,15 @@ class UserModelChoiceField(forms.ModelChoiceField):
return obj.get_full_name() return obj.get_full_name()
class PersonModelMultipleChoice(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
context = {
'profile_image': obj.profile_image_url,
'full_name': obj.full_name
}
return str(render_to_string('publisher/_personFieldLabel.html', context=context))
class BaseCourseForm(forms.ModelForm): class BaseCourseForm(forms.ModelForm):
""" Base Course Form. """ """ Base Course Form. """
...@@ -159,8 +169,17 @@ class CustomCourseRunForm(CourseRunForm): ...@@ -159,8 +169,17 @@ class CustomCourseRunForm(CourseRunForm):
start = forms.DateTimeField(label=_('Course Start Date'), required=True) start = forms.DateTimeField(label=_('Course Start Date'), required=True)
end = forms.DateTimeField(label=_('Course End Date'), required=True) end = forms.DateTimeField(label=_('Course End Date'), required=True)
staff = forms.ModelMultipleChoiceField( staff = PersonModelMultipleChoice(
queryset=Person.objects.all(), widget=forms.SelectMultiple, required=False label=_('instructor'),
queryset=Person.objects.all(),
widget=autocomplete.ModelSelect2Multiple(
url='admin_metadata:person-autocomplete',
attrs={
'data-minimum-input-length': 2,
'data-html': 'true',
}
),
required=False,
) )
target_content = forms.BooleanField( target_content = forms.BooleanField(
widget=forms.RadioSelect( widget=forms.RadioSelect(
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-01-24 10:18-0500\n" "POT-Creation-Date: 2017-01-25 12:05+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
#: apps/api/filters.py #: apps/api/filters.py
#, python-brace-format #, python-brace-format
...@@ -526,6 +526,10 @@ msgid "Course End Date" ...@@ -526,6 +526,10 @@ msgid "Course End Date"
msgstr "" msgstr ""
#: apps/publisher/forms.py #: apps/publisher/forms.py
msgid "instructor"
msgstr ""
#: apps/publisher/forms.py
msgid "Pacing" msgid "Pacing"
msgstr "" msgstr ""
...@@ -1202,7 +1206,7 @@ msgid "" ...@@ -1202,7 +1206,7 @@ msgid ""
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
msgid "Limited to the primary instructors a learner will encounter in videos" msgid "Limited to the primary instructors a learner will encounter in videos "
msgstr "" msgstr ""
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-01-24 10:18-0500\n" "POT-Creation-Date: 2017-01-25 12:05+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
#: static/js/catalogs-change-form.js #: static/js/catalogs-change-form.js
msgid "Preview" msgid "Preview"
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-01-24 10:18-0500\n" "POT-Creation-Date: 2017-01-25 12:05+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: apps/api/filters.py #: apps/api/filters.py
...@@ -640,6 +640,10 @@ msgid "Course End Date" ...@@ -640,6 +640,10 @@ msgid "Course End Date"
msgstr "Çöürsé Énd Däté Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" msgstr "Çöürsé Énd Däté Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#"
#: apps/publisher/forms.py #: apps/publisher/forms.py
msgid "instructor"
msgstr "ïnstrüçtör Ⱡ'σяєм ιρѕυм ∂σłσ#"
#: apps/publisher/forms.py
msgid "Pacing" msgid "Pacing"
msgstr "Päçïng Ⱡ'σяєм ιρѕυ#" msgstr "Päçïng Ⱡ'σяєм ιρѕυ#"
...@@ -1443,10 +1447,10 @@ msgstr "" ...@@ -1443,10 +1447,10 @@ msgstr ""
" ïnstrüçtörs shöüld ßé lïstéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт #" " ïnstrüçtörs shöüld ßé lïstéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт #"
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
msgid "Limited to the primary instructors a learner will encounter in videos" msgid "Limited to the primary instructors a learner will encounter in videos "
msgstr "" msgstr ""
"Lïmïtéd tö thé prïmärý ïnstrüçtörs ä léärnér wïll énçöüntér ïn vïdéös Ⱡ'σяєм" "Lïmïtéd tö thé prïmärý ïnstrüçtörs ä léärnér wïll énçöüntér ïn vïdéös "
" ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #" "Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #"
#: templates/publisher/add_course_form.html #: templates/publisher/add_course_form.html
msgid "Add New" msgid "Add New"
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-01-24 10:18-0500\n" "POT-Creation-Date: 2017-01-25 12:05+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: static/js/catalogs-change-form.js #: static/js/catalogs-change-form.js
......
...@@ -48,6 +48,15 @@ $(document).ready(function(){ ...@@ -48,6 +48,15 @@ $(document).ready(function(){
$('.closeModal').click(function (e) { $('.closeModal').click(function (e) {
closeModal(e, $('#addInstructorModal')); closeModal(e, $('#addInstructorModal'));
}); });
$("#id_staff").find('option:selected').each(function(){
var id = this.value,
label = $.parseHTML(this.label),
image_source = $(label[0]).attr('src'),
name = $(label[1]).text();
renderSelectedInstructor(id, name, image_source);
});
}); });
$(document).on('change', '#id_organization', function (e) { $(document).on('change', '#id_organization', function (e) {
...@@ -94,3 +103,34 @@ function closeModal(event, modal) { ...@@ -94,3 +103,34 @@ function closeModal(event, modal) {
modal.hide(); modal.hide();
$('body').removeClass('stopScroll'); $('body').removeClass('stopScroll');
} }
$(document).on('change', '#id_staff', function (e) {
var id = this.value,
$instructorSelector = $('.instructor-select'),
$instructor = $instructorSelector.find('.select2-selection__choice'),
image_source,
name;
$instructorSelector.find('.select2-selection__clear').remove();
image_source = $instructor.find('img').last().attr('src');
name = $instructor.find('b').last().text();
renderSelectedInstructor(id, name, image_source);
$instructor.remove();
});
$(document).on('click', '.selected-instructor a', function (e) {
e.preventDefault();
var id = this.id,
option = $('#id_staff').find('option[value="' + id + '"]');
option.prop("selected", false);
this.closest('.selected-instructor, .instructor').remove();
});
function renderSelectedInstructor(id, name, image) {
var instructorHtml = '<div class="instructor"><div><img src="' + image + '"></div><div><a id="' + id + '" ' +
'href="#"><i class="fa fa-trash-o fa-fw"></i></a><b>' + name + '</b></div></div>';
$('.selected-instructor').append(instructorHtml);
}
...@@ -822,3 +822,46 @@ select { ...@@ -822,3 +822,46 @@ select {
width: 480px; width: 480px;
height: 200px; height: 200px;
} }
.selected-instructor {
display: inline-block;
padding-top: 10px;
}
#id_staff {
width: 500px;
}
.instructor-option {
img {
width: 80px;
height: 80px;
}
td:nth-child(2) {
@include padding-left(5px);
@include padding-right(5px);
}
}
.instructor {
@include float(left);
font-size: smaller;
width: 125px;
padding-bottom: 15px;
img {
width: 80px;
height: 80px;
}
div {
text-align: center;
}
a {
color: red;
}
}
.instructor-select {
.select2-selection__choice, .select2-selection__clear{
display: none;
}
}
<img src="{{ profile_image }}"/><span>{{ full_name }}</span>
<table class="instructor-option" id="{{ id }}">
<tr>
<td><img src="{{ profile_image }}" alt="profile image"/></td>
<td>
<b>{{ full_name }}</b>
{% if position %}
<p>{{ position }}</p>
{% endif %}
</td>
</tr>
</table>
<hr>
...@@ -316,16 +316,18 @@ ...@@ -316,16 +316,18 @@
<div class="row"> <div class="row">
<div class="col col-6 help-text"> <div class="col col-6 help-text">
<ul> <ul>
<li>{% trans "If there is more than one instructor, please indicate the order in which the instructors should be listed" %} <li>{% trans "If there is more than one instructor, please indicate the order in which the instructors should be listed" %}</li>
</li> <li>{% trans "Limited to the primary instructors a learner will encounter in videos " %}</li>
<li>{% trans "Limited to the primary instructors a learner will encounter in videos" %}</li>
</ul> </ul>
</div> </div>
<div class="col col-6"> <div class="col col-6 instructor-select">
<label class="field-label ">{{ run_form.staff.label_tag }}</label>
{{ run_form.staff }} {{ run_form.staff }}
{% if publisher_add_instructor_feature %} {% if publisher_add_instructor_feature %}
<button type="button" id="add-new-instructor">{% trans "Add New" %}<br>{% trans "Instructor" %}</button> <button type="button" id="add-new-instructor">{% trans "Add New" %}<br>{% trans "Instructor" %}</button>
{% endif %} {% endif %}
<div class="selected-instructor">
</div>
</div> </div>
</div> </div>
......
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