diff --git a/biz/djangoapps/ga_achievement/management/commands/check_playback_log.py b/biz/djangoapps/ga_achievement/management/commands/check_playback_log.py new file mode 100644 index 000000000000..93a02df9bc9f --- /dev/null +++ b/biz/djangoapps/ga_achievement/management/commands/check_playback_log.py @@ -0,0 +1,86 @@ +import logging +from collections import Counter +from datetime import datetime, timedelta +from optparse import make_option + +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError + +from biz.djangoapps.util import datetime_utils +from biz.djangoapps.util.biz_mongo_connection import BizMongoConnection +from biz.djangoapps.util.decorators import handle_command_exception + +log = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = """ + Usage: python manage.py lms --settings=aws check_playback_log [--target_date=] + """ + + option_list = BaseCommand.option_list + ( + make_option('--target_date', + default=None, + action='store', + help="target_date should be 'yyyymmdd' format."), + ) + + @handle_command_exception('/edx/var/log/biz/check_playback_log_command.txt') + def handle(self, *args, **options): + + if len(args) > 0: + raise CommandError("This command requires no arguments. Use target_date option if you want.") + + target_date = options.get('target_date') + if target_date: + try: + target_date = datetime.strptime(target_date, '%Y%m%d') + except ValueError: + raise CommandError("target_date should be 'yyyymmdd' format.") + else: + # By default, check for today (JST) + target_date = datetime_utils.timezone_today() + + log.info("Command check_playback_log started for {}.".format(target_date.strftime('%Y/%m/%d'))) + + # NOTE: By default, PyMongo deals with only naive datetimes. + # NOTE: emr_aggregate_playback_log sets `created_at` to yesterday's midnight. + # For example, when it runs at 2017-11-13 06:06:07+9:00, it sets as below: + # - u'created_at': datetime.datetime(2017, 11, 12, 0, 0, tzinfo=) + target_datetime = datetime.combine(target_date + timedelta(days=-1), datetime.min.time()) + + try: + store_config = settings.BIZ_MONGO['playback_log'] + db_connection = BizMongoConnection(**store_config) + db = db_connection.database + collection = db[store_config['collection']] + except Exception as e: + raise e + + try: + result = collection.aggregate([ + {'$group': { + '_id': { + 'course_id': '$course_id', + 'vertical_id': '$vertical_id', + 'target_id': '$target_id', + 'created_at': '$created_at', + }, + 'total': {'$sum': 1}, + }}, + {'$match': { + '_id.created_at': target_datetime, + 'total': {'$gt': 1}, + }}, + ], + allowDiskUse=True, + )['result'] + + except Exception as e: + raise e + + if result: + messages = ["{} duplicated documents are detected in playback_log for {}.".format(len(result), target_date.strftime('%Y/%m/%d'))] + for course_id, count in Counter([x['_id']['course_id'] for x in result]).most_common(): + messages.append('{},{}'.format(course_id, count)) + raise Exception('\n'.join(messages)) diff --git a/biz/djangoapps/ga_contract/admin.py b/biz/djangoapps/ga_contract/admin.py index c0d996f7b6a0..c2b4f9c5ebba 100644 --- a/biz/djangoapps/ga_contract/admin.py +++ b/biz/djangoapps/ga_contract/admin.py @@ -21,11 +21,15 @@ def __init__(self, *args, **kargs): super(ContractAuthForm, self).__init__(*args, **kargs) def clean(self): - url_code = self.cleaned_data['url_code'] - if not re.match(r'^{url_code}$'.format(url_code=URL_CODE_PATTERN), url_code): + url_code = self.cleaned_data['url_code'] if 'url_code' in self.cleaned_data else None + if url_code is None or not re.match(r'^{url_code}$'.format(url_code=URL_CODE_PATTERN), url_code): raise forms.ValidationError( _("Url code is invalid. Please enter alphanumeric {min_length}-{max_length} characters.").format( min_length=URL_CODE_MIN_LENGTH, max_length=URL_CODE_MAX_LENGTH)) + + if 'contract' in self.cleaned_data and ContractAuth.objects.filter(url_code=url_code).exclude(contract=self.cleaned_data['contract']).exists(): + raise forms.ValidationError(_("Url code is duplicated. Please change url code.")) + return self.cleaned_data class Meta: diff --git a/biz/djangoapps/ga_contract/forms.py b/biz/djangoapps/ga_contract/forms.py index 095cf6787673..15b8ea7fdbed 100644 --- a/biz/djangoapps/ga_contract/forms.py +++ b/biz/djangoapps/ga_contract/forms.py @@ -86,19 +86,12 @@ def clean(self): self.errors["contract"] = ErrorList([_("Contract end date is before contract start date.")]) # check contract detail inputs - course_id_list = self.data.getlist('detail_course') - detail_delete_list = self.data.getlist('detail_delete') - valid_course_id_list = [v for i, v in enumerate(course_id_list) if not detail_delete_list[i]] - if len(valid_course_id_list) != len(set(valid_course_id_list)): - self.errors["contract_detail"] = ErrorList( - [_("You can not enter duplicate values in {0}.").format(_('Contract Detail Info'))]) - - # check contract detail inputs - display_name_list = self.data.getlist('additional_info_display_name') - additional_info_delete_list = self.data.getlist('additional_info_delete') - valid_display_name_list = [v for i, v in enumerate(display_name_list) if not additional_info_delete_list[i]] - if len(valid_display_name_list) != len(set(valid_display_name_list)): - self.errors["additional_info"] = ErrorList( - [_("You can not enter duplicate values in {0}.").format(_('Additional Info'))]) + if len(self.data.getlist('detail_id')) == len(self.data.getlist('detail_course')) == len(self.data.getlist('detail_delete')): + course_id_list = self.data.getlist('detail_course') + detail_delete_list = self.data.getlist('detail_delete') + valid_course_id_list = [v for i, v in enumerate(course_id_list) if not detail_delete_list[i]] + if len(valid_course_id_list) != len(set(valid_course_id_list)): + self.errors["contract_detail"] = ErrorList( + [_("You can not enter duplicate values in {0}.").format(_('Contract Detail Info'))]) return cleaned_data diff --git a/biz/djangoapps/ga_contract/models.py b/biz/djangoapps/ga_contract/models.py index 80d744b7cc08..ce5ad0f10f05 100644 --- a/biz/djangoapps/ga_contract/models.py +++ b/biz/djangoapps/ga_contract/models.py @@ -359,6 +359,11 @@ def find_by_contract_id(cls, contract_id): """ return cls.objects.filter(contract_id=contract_id).order_by('id') + @classmethod + def validate_and_find_by_ids(cls, contract, additional_info_ids): + additional_info_list = cls.objects.filter(contract=contract) + return None if cls.objects.filter(contract=contract).exclude(id__in=additional_info_ids).exists() else additional_info_list + class ContractAuth(models.Model): """ diff --git a/biz/djangoapps/ga_contract/tests/test_admin.py b/biz/djangoapps/ga_contract/tests/test_admin.py new file mode 100644 index 000000000000..85b857de155f --- /dev/null +++ b/biz/djangoapps/ga_contract/tests/test_admin.py @@ -0,0 +1,70 @@ +from mock import patch + +from django.core.urlresolvers import reverse + +from biz.djangoapps.util.tests.testcase import BizTestBase +from student.tests.factories import UserFactory + + +class ContractAuthAdminTest(BizTestBase): + + def setUp(self): + super(ContractAuthAdminTest, self).setUp() + + user = UserFactory.create(is_staff=True, is_superuser=True) + user.save() + + self.client.login(username=user.username, password='test') + + self.contract = self._create_contract() + self.contract_other = self._create_contract() + self.url_code = 'testUrlCode' + + def test_initial_url_code(self): + with patch( + 'biz.djangoapps.ga_contract.admin.get_random_string', + return_value=self.url_code + ): + response = self.client.get(reverse('admin:ga_contract_contractauth_add')) + self.assertEqual(200, response.status_code) + self.assertIn('value="{url_code}"'.format(url_code=self.url_code), response.content) + + def test_add(self): + response = self.client.post(reverse('admin:ga_contract_contractauth_add'), data={ + 'contract': self.contract.id, + 'url_code': self.url_code, + }) + self.assertRedirects(response, reverse('admin:ga_contract_contractauth_changelist')) + + response = self.client.get(reverse('admin:ga_contract_contractauth_changelist')) + self.assertEqual(200, response.status_code) + self.assertIn('{contract_name}({url_code})'.format(contract_name=self.contract.contract_name, url_code=self.url_code), response.content) + + def test_contract_required(self): + response = self.client.post(reverse('admin:ga_contract_contractauth_add'), data={ + 'url_code': self.url_code, + }) + self.assertEqual(200, response.status_code) + self.assertIn('This field is required.'.format(url_code=self.url_code), response.content) + + def test_url_code_required(self): + response = self.client.post(reverse('admin:ga_contract_contractauth_add'), data={ + 'contract': self.contract.id, + }) + self.assertEqual(200, response.status_code) + self.assertIn('This field is required.'.format(url_code=self.url_code), response.content) + self.assertIn('Url code is invalid. Please enter alphanumeric 8-255 characters.'.format(url_code=self.url_code), response.content) + + def test_url_code_duplicate(self): + response = self.client.post(reverse('admin:ga_contract_contractauth_add'), data={ + 'contract': self.contract_other.id, + 'url_code': self.url_code, + }) + self.assertRedirects(response, reverse('admin:ga_contract_contractauth_changelist')) + + response = self.client.post(reverse('admin:ga_contract_contractauth_add'), data={ + 'contract': self.contract.id, + 'url_code': self.url_code, + }) + self.assertEqual(200, response.status_code) + self.assertIn('Url code is duplicated. Please change url code.'.format(url_code=self.url_code), response.content) diff --git a/biz/djangoapps/ga_contract/tests/test_views.py b/biz/djangoapps/ga_contract/tests/test_views.py index 6a6a51133763..4daa6939131f 100644 --- a/biz/djangoapps/ga_contract/tests/test_views.py +++ b/biz/djangoapps/ga_contract/tests/test_views.py @@ -1,20 +1,22 @@ """ Test for contract feature """ +import ddt from datetime import date, timedelta from biz.djangoapps.ga_invitation.models import REGISTER_INVITATION_CODE from biz.djangoapps.ga_invitation.tests.factories import ContractRegisterFactory -from biz.djangoapps.ga_contract.models import Contract, ContractDetail, AdditionalInfo, CONTRACT_TYPE_GACCO_SERVICE, REGISTER_TYPE_DISABLE_REGISTER_BY_STUDENT, REGISTER_TYPE_ENABLE_REGISTER_BY_STUDENT -from biz.djangoapps.ga_contract.tests.factories import ContractFactory, ContractDetailFactory, AdditionalInfoFactory +from biz.djangoapps.ga_contract.models import Contract, ContractDetail, CONTRACT_TYPE_GACCO_SERVICE, REGISTER_TYPE_DISABLE_REGISTER_BY_STUDENT, REGISTER_TYPE_ENABLE_REGISTER_BY_STUDENT +from biz.djangoapps.ga_contract.tests.factories import ContractFactory, ContractDetailFactory from biz.djangoapps.util.tests.testcase import BizViewTestBase from django.core.urlresolvers import reverse from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory +@ddt.ddt class ContractViewTest(BizViewTestBase, ModuleStoreTestCase): def _index_view(self): return reverse('biz:contract:index') @@ -38,9 +40,6 @@ def _create_contract(self, name, contractor, owner, created_by, invitation_code) def _create_contract_detail(self, contract, course_id): return ContractDetailFactory.create(course_id=course_id, contract=contract) - def _create_additional_info(self, contract, display_name): - return AdditionalInfoFactory.create(display_name=display_name, contract=contract) - def _setup_course_data(self): self.course_gacco1 = CourseFactory.create(org='gacco', number='course1', run='run1') self.course_gacco2 = CourseFactory.create(org='gacco', number='course2', run='run2') @@ -67,9 +66,6 @@ def _setup_contract_data(self): self._create_contract_detail(self.contract_b, 'gacco/course1/run1') self._create_contract_detail(self.contract_b, 'gacco/course2/run2') - self._create_additional_info(self.contract_b, 'first name') - self._create_additional_info(self.contract_b, 'last name') - def test_index_for_aggregator(self): # Create account and logged in. self.setup_user() @@ -202,7 +198,7 @@ def test_register_invitation_code_is_used_error(self): self.assertIn('Contract Detail', response.content) self.assertIn('The invitation code has been used.', response.content) - def test_register_contract_detail_field_invalid_error(self): + def test_register_contract_detail_field_duplicate(self): # Create account and logged in. self.setup_user() @@ -228,7 +224,16 @@ def test_register_contract_detail_field_invalid_error(self): self.assertIn('Contract Detail Info', response.content) self.assertIn('You can not enter duplicate values in Contract Detail Info.', response.content) - def test_register_additional_info__field_invalid_error(self): + @ddt.data( + (['course-v1:edX+k01+2016_03', 'course-v1:edX+k01+2016_02'], [''], ['']), + (['course-v1:edX+k01+2016_03'], ['', ''], ['']), + (['course-v1:edX+k01+2016_03'], [''], ['', '']), + (['course-v1:edX+k01+2016_03'], ['', ''], ['', '']), + (['course-v1:edX+k01+2016_03', 'course-v1:edX+k01+2016_02'], [''], ['', '']), + (['course-v1:edX+k01+2016_03', 'course-v1:edX+k01+2016_02'], ['', ''], ['']), + ) + @ddt.unpack + def test_register_contract_detail_field_invalid(self, data_detail_course, data_detail_delete, data_detail_id): # Create account and logged in. self.setup_user() @@ -242,20 +247,17 @@ def test_register_additional_info__field_invalid_error(self): 'contractor_organization': self.org_tac.id, 'start_date': '2016/03/01', 'end_date': '2016/03/31', - 'detail_course': ['course-v1:edX+k01+2016_03', 'course-v1:edX+k02+2016_03'], - 'detail_delete': ['', ''], - 'detail_id': ['', ''], - 'additional_info_display_name': ['english name', 'english name'], - 'additional_info_delete': ['', ''], - 'additional_info_id': ['', ''] + 'detail_course': data_detail_course, + 'detail_delete': data_detail_delete, + 'detail_id': data_detail_id } with self.skip_check_course_selection(current_organization=self.gacco_organization): response = self.client.post(self._register_view(), data=data) self.assertEqual(200, response.status_code) - self.assertIn('Contract Detail', response.content) - self.assertIn('You can not enter duplicate values in Additional Info.', response.content) + self.assertIn('Contract Detail Info', response.content) + self.assertIn('Invalid contract details.', response.content) def test_register_by_platfomer_success(self): # Create account and logged in. @@ -274,9 +276,6 @@ def test_register_by_platfomer_success(self): 'detail_course': ['course-v1:edX+k01+2016_03', 'course-v1:edX+k02+2016_03'], 'detail_delete': ['', ''], 'detail_id': ['', ''], - 'additional_info_display_name': ['first name', 'family name'], - 'additional_info_delete': ['', ''], - 'additional_info_id': ['', ''] } with self.skip_check_course_selection(current_organization=self.gacco_organization): @@ -305,9 +304,6 @@ def test_register_by_aggregator_success(self): 'detail_course': ['course-v1:edX+k01+2016_03', 'course-v1:edX+k02+2016_03'], 'detail_delete': ['', ''], 'detail_id': ['', ''], - 'additional_info_display_name': ['first name', 'last name'], - 'additional_info_delete': ['', ''], - 'additional_info_id': ['', ''] } with self.skip_check_course_selection(current_organization=self.org_tac): @@ -426,7 +422,7 @@ def test_edit_contract_field_invalid_error(self): self.assertIn('Ensure this value has at least 8 characters (it has 6).', response.content) self.assertIn('Contract end date is before contract start date.', response.content) - def test_edit_contract_detail_field_invalid_error(self): + def test_edit_contract_detail_field_duplicate(self): # Create account and logged in. self.setup_user() @@ -454,7 +450,16 @@ def test_edit_contract_detail_field_invalid_error(self): self.assertIn('Contract Detail Info', response.content) self.assertIn('You can not enter duplicate values in Contract Detail Info.', response.content) - def test_edit_additional_info__field_invalid_error(self): + @ddt.data( + (['course-v1:edX+k01+2016_03', 'course-v1:edX+k01+2016_02'], [''], ['']), + (['course-v1:edX+k01+2016_03'], ['', ''], ['']), + (['course-v1:edX+k01+2016_03'], [''], ['', '']), + (['course-v1:edX+k01+2016_03'], ['', ''], ['', '']), + (['course-v1:edX+k01+2016_03', 'course-v1:edX+k01+2016_02'], [''], ['', '']), + (['course-v1:edX+k01+2016_03', 'course-v1:edX+k01+2016_02'], ['', ''], ['']), + ) + @ddt.unpack + def test_edit_contract_detail_field_invalid(self, data_detail_course, data_detail_delete, data_detail_id): # Create account and logged in. self.setup_user() @@ -464,26 +469,23 @@ def test_edit_additional_info__field_invalid_error(self): data = { 'contract_name': 'owner contract for tac', - 'contract_type': 'O', + 'contract_type': 'PF', 'register_type': REGISTER_TYPE_ENABLE_REGISTER_BY_STUDENT[0], 'invitation_code': 'invitationcodea', 'contractor_organization': self.org_a.id, 'start_date': '2016/03/01', 'end_date': '2016/03/31', - 'detail_course': ['gacco/course1/run1', 'gacco/course2/run2'], - 'detail_delete': ['', ''], - 'detail_id': ['', ''], - 'additional_info_display_name': ['english name', 'english name'], - 'additional_info_delete': ['', ''], - 'additional_info_id': ['', ''] + 'detail_course': data_detail_course, + 'detail_delete': data_detail_delete, + 'detail_id': data_detail_id } with self.skip_check_course_selection(current_organization=self.gacco_organization): response = self.client.post(self._edit_view(self.contract_a.id), data=data) self.assertEqual(200, response.status_code) - self.assertIn('Contract Detail', response.content) - self.assertIn('You can not enter duplicate values in Additional Info.', response.content) + self.assertIn('Contract Detail Info', response.content) + self.assertIn('Invalid contract details.', response.content) def test_edit_update_contract(self): # Create account and logged in. @@ -520,7 +522,7 @@ def test_edit_update_contract(self): self.assertIn(_new_start_date.strftime("%Y/%m/%d"), response.content) self.assertIn(_new_end_date.strftime("%Y/%m/%d"), response.content) - def test_edit_add_contract_detail_and_additional_info(self): + def test_edit_add_contract_detail(self): # Create account and logged in. self.setup_user() @@ -545,9 +547,6 @@ def test_edit_add_contract_detail_and_additional_info(self): 'detail_course': ['gacco/course1/run1', 'gacco/course2/run2'], 'detail_delete': ['', ''], 'detail_id': ['', ''], - 'additional_info_display_name': ['first name', 'last name'], - 'additional_info_delete': ['', ''], - 'additional_info_id': ['', ''] } with self.skip_check_course_selection(current_organization=self.gacco_organization): @@ -555,10 +554,9 @@ def test_edit_add_contract_detail_and_additional_info(self): self.assertRedirects(response, self._index_view()) self.assertIn('The contract changes have been saved.', response.content) - self.assertEqual(2, len(AdditionalInfo.find_by_contract_id(self.contract_b))) self.assertEqual(2, len(ContractDetail.find_enabled_by_contractor_and_contract_id(self.org_b.id, self.contract_b.id))) - def test_edit_remove_contract_detail_and_additional_info(self): + def test_edit_remove_contract_detail(self): # Create account and logged in. self.setup_user() @@ -583,9 +581,6 @@ def test_edit_remove_contract_detail_and_additional_info(self): 'detail_course': ['gacco/course1/run1', 'gacco/course2/run2'], 'detail_delete': ['', '1'], 'detail_id': ['1', '2'], - 'additional_info_display_name': ['first name', 'last name'], - 'additional_info_delete': ['', '1'], - 'additional_info_id': ['1', '2'] } with self.skip_check_course_selection(current_organization=self.gacco_organization): @@ -594,10 +589,7 @@ def test_edit_remove_contract_detail_and_additional_info(self): self.assertRedirects(response, self._index_view()) self.assertIn('The contract changes have been saved.', response.content) - after_additional_info_list = AdditionalInfo.find_by_contract_id(self.contract_b) after_contract_detail_list = ContractDetail.find_enabled_by_contractor_and_contract_id(self.org_b.id, self.contract_b.id) - self.assertEqual(1, len(after_additional_info_list)) - self.assertEqual('first name', after_additional_info_list[0].display_name) self.assertEqual(1, len(after_contract_detail_list)) self.assertEqual('gacco/course1/run1', unicode(after_contract_detail_list[0].course_id)) @@ -620,9 +612,6 @@ def test_edit_remove_contract(self): 'detail_course': ['gacco/course1/run1', 'gacco/course2/run2'], 'detail_delete': ['', ''], 'detail_id': ['1', '2'], - 'additional_info_display_name': ['first name', 'last name'], - 'additional_info_delete': ['', ''], - 'additional_info_id': ['1', '2'], 'action_name': 'delete' } diff --git a/biz/djangoapps/ga_contract/views.py b/biz/djangoapps/ga_contract/views.py index 78422ce5cedb..b8a72316fb6f 100644 --- a/biz/djangoapps/ga_contract/views.py +++ b/biz/djangoapps/ga_contract/views.py @@ -2,6 +2,7 @@ Views for contract feature """ import json +import logging from django.contrib import messages from django.contrib.auth.decorators import login_required @@ -9,15 +10,17 @@ from django.shortcuts import get_object_or_404, redirect from django.utils.translation import ugettext as _ from django.views.decorators.http import require_GET, require_POST -from xmodule.modulestore.django import modulestore from biz.djangoapps.ga_contract.forms import ContractForm -from biz.djangoapps.ga_contract.models import Contract, ContractDetail, AdditionalInfo, CONTRACT_TYPE, REGISTER_TYPE +from biz.djangoapps.ga_contract.models import Contract, ContractDetail, CONTRACT_TYPE, REGISTER_TYPE from biz.djangoapps.ga_organization.models import Organization from biz.djangoapps.util.datetime_utils import format_for_w2ui from biz.djangoapps.util.decorators import check_course_selection from biz.djangoapps.util.json_utils import EscapedEdxJSONEncoder, LazyEncoder from edxmako.shortcuts import render_to_response +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview + +log = logging.getLogger(__name__) class ContractEncoder(LazyEncoder, EscapedEdxJSONEncoder): @@ -35,12 +38,11 @@ def index(request): :return: HttpResponse """ search_contract_list = Contract.find_by_owner(request.current_organization).select_related( - 'contractor_organization', 'owner_organization', 'created_by').prefetch_related('additional_info') + 'contractor_organization', 'owner_organization', 'created_by') - show_contract_list = [] - for i, contract in enumerate(search_contract_list): - show_contract_list.append({ - 'recid': i + 1, + return render_to_response("ga_contract/index.html", { + 'contract_show_list': json.dumps([{ + 'recid': i, 'contract_name': contract.contract_name, 'contract_type': dict(CONTRACT_TYPE).get(contract.contract_type, contract.contract_type), 'register_type': dict(REGISTER_TYPE).get(contract.register_type, contract.register_type), @@ -52,14 +54,9 @@ def index(request): 'created_by': contract.created_by.profile.name, 'created': format_for_w2ui(contract.created), 'course_count': contract.details.count(), - 'additional_info_count': contract.additional_info.count(), 'detail_url': reverse('biz:contract:detail', kwargs={'selected_contract_id': contract.id}), - }) - - context = { - 'contract_show_list': json.dumps(show_contract_list, cls=ContractEncoder) - } - return render_to_response("ga_contract/index.html", context) + } for i, contract in enumerate(search_contract_list, start=1)], cls=ContractEncoder) + }) @require_GET @@ -79,14 +76,11 @@ def show_register(request): messages.error(request, _("You need to create an organization first.")) return redirect(reverse('biz:contract:index')) - form = ContractForm(current_org) - context = { - 'course_list': _get_course_name_list(current_org.org_code), - 'form': form, + return render_to_response("ga_contract/detail.html", { + 'form': ContractForm(current_org), 'detail_list': [], - 'additional_info_list': [], - } - return render_to_response("ga_contract/detail.html", context) + 'course_list': _get_course_name_list(current_org.org_code), + }) @require_POST @@ -103,9 +97,15 @@ def register(request): form = ContractForm(current_org, request.POST) # contract detail list - detail_input_list = _get_detail_input_list(request) - # additional info list - additional_info_input_list = _get_additional_info_input_list(request) + detail_list = _get_detail_input_list(request) + if detail_list is None: + # validation failed + messages.error(request, _("Invalid contract details.")) + return render_to_response("ga_contract/detail.html", { + 'form': form, + 'detail_list': [], + 'course_list': _get_course_name_list(current_org.org_code), + }) if form.is_valid(): # validation successful @@ -117,28 +117,20 @@ def register(request): contract.save() # add new contract detail info - for detail_info in detail_input_list: + for detail_info in detail_list: contract_detail = ContractDetail(contract=contract, course_id=detail_info['course_id']) contract_detail.save() - # add new additional info - for additional_info in additional_info_input_list: - contract_additional_info = AdditionalInfo(contract=contract, display_name=additional_info['display_name']) - contract_additional_info.save() - # return to list view messages.info(request, _('The new contract has been added.')) return redirect(reverse('biz:contract:index')) else: # validation failed - context = { + return render_to_response("ga_contract/detail.html", { 'form': form, - 'detail_list': json.dumps(detail_input_list), + 'detail_list': json.dumps(detail_list), 'course_list': _get_course_name_list(current_org.org_code), - 'additional_info_list': json.dumps(additional_info_input_list, cls=ContractEncoder), - } - - return render_to_response("ga_contract/detail.html", context) + }) @require_GET @@ -154,32 +146,17 @@ def detail(request, selected_contract_id): """ current_org = request.current_organization selected_contract = get_object_or_404(Contract, pk=selected_contract_id, owner_organization=current_org) - form = ContractForm(current_org, instance=selected_contract) - detail_list = [] - for contract_detail in selected_contract.details.all(): - detail_list.append({ + return render_to_response("ga_contract/detail.html", { + 'selected_contract_id': selected_contract_id, + 'form': ContractForm(current_org, instance=selected_contract), + 'detail_list': json.dumps([{ 'id': contract_detail.id, 'course_id': unicode(contract_detail.course_id), 'delete_flg': '', - }) - - additional_info_list = [] - for additional_info in selected_contract.additional_info.all(): - additional_info_list.append({ - 'id': additional_info.id, - 'display_name': additional_info.display_name, - 'delete_flg': '', - }) - - context = { - 'form': form, - 'selected_contract_id': selected_contract_id, - 'detail_list': json.dumps(detail_list), + } for contract_detail in selected_contract.details.all()]), 'course_list': _get_course_name_list(current_org.org_code), - 'additional_info_list': json.dumps(additional_info_list, cls=ContractEncoder), - } - return render_to_response("ga_contract/detail.html", context) + }) @require_POST @@ -195,18 +172,29 @@ def edit(request, selected_contract_id): """ current_org = request.current_organization selected_contract = get_object_or_404(Contract, pk=selected_contract_id, owner_organization=current_org) + form = ContractForm(request.current_organization, request.POST, instance=selected_contract) + # contract detail list detail_list = _get_detail_input_list(request) - # additional info list - additional_info_list = _get_additional_info_input_list(request) - form = ContractForm(request.current_organization, request.POST, instance=selected_contract) + if detail_list is None: + # validation failed + messages.error(request, _("Invalid contract details.")) + return render_to_response("ga_contract/detail.html", { + 'selected_contract_id': selected_contract_id, + 'form': form, + 'detail_list': json.dumps([{ + 'id': contract_detail.id, + 'course_id': unicode(contract_detail.course_id), + 'delete_flg': '', + } for contract_detail in selected_contract.details.all()]), + 'course_list': _get_course_name_list(current_org.org_code), + }) context = { - 'form': form, 'selected_contract_id': selected_contract_id, + 'form': form, 'detail_list': json.dumps(detail_list), 'course_list': _get_course_name_list(request.current_organization.org_code), - 'additional_info_list': json.dumps(additional_info_list, cls=ContractEncoder), 'url': reverse('biz:contract:edit', kwargs={'selected_contract_id': selected_contract_id}), } @@ -242,17 +230,6 @@ def edit(request, selected_contract_id): else: contract_detail.save() - # Update contract detail info - for additional_info in additional_info_list: - contract_additional_info = AdditionalInfo(contract=selected_contract, - display_name=additional_info['display_name']) - if additional_info['id']: - contract_additional_info.id = additional_info['id'] - if additional_info['delete_flg']: - contract_additional_info.delete() - else: - contract_additional_info.save() - # return to list view messages.info(request, _("The contract changes have been saved.")) return redirect(reverse('biz:contract:index')) @@ -268,35 +245,23 @@ def _get_detail_input_list(request): :param request: HttpRequest :return: course list """ - detail_list = [] - for num in range(len(request.POST.getlist('detail_id'))): - detail_info = { - 'id': request.POST.getlist('detail_id')[num], - 'course_id': request.POST.getlist('detail_course')[num], - 'delete_flg': request.POST.getlist('detail_delete')[num], - } - detail_list.append(detail_info) - - return detail_list - - -def _get_additional_info_input_list(request): - """ - return the list of additional info input - - :param request: HttpRequest - :return: additional info list - """ - additional_info_list = [] - for num in range(len(request.POST.getlist('additional_info_id'))): - additional_info = { - 'id': request.POST.getlist('additional_info_id')[num], - 'display_name': request.POST.getlist('additional_info_display_name')[num], - 'delete_flg': request.POST.getlist('additional_info_delete')[num], - } - additional_info_list.append(additional_info) - - return additional_info_list + if not (len(request.POST.getlist('detail_id')) == len(request.POST.getlist('detail_course')) == len(request.POST.getlist('detail_delete'))): + log.warning('Invalid contract details. detail_id:{}, detail_course:{}, detail_delete:{}.'.format( + request.POST.getlist('detail_id'), + request.POST.getlist('detail_course'), + request.POST.getlist('detail_delete'), + )) + return None + + return [{ + 'id': detail_id, + 'course_id': course_id, + 'delete_flg': detail_delete, + } for detail_id, course_id, detail_delete in zip( + request.POST.getlist('detail_id'), + request.POST.getlist('detail_course'), + request.POST.getlist('detail_delete') + )] def _get_course_name_list(org_code): @@ -306,10 +271,7 @@ def _get_course_name_list(org_code): :param org_code: organization code :return: course list """ - course_list = [] - courses = modulestore().get_courses(org=org_code) - # courses = sorted(courses, key=lambda c: c.id) - for course in courses: - name = course.course_canonical_name or course.display_name_with_default - course_list.append((unicode(course.id), u'{} ({})'.format(name, unicode(course.id)))) - return course_list + return [ + (unicode(c.id), u'{} ({})'.format(c.display_name, c.id)) + for c in CourseOverview.objects.filter(org=org_code).order_by('id') + ] diff --git a/biz/djangoapps/ga_contract_operation/additionalinfo.py b/biz/djangoapps/ga_contract_operation/additionalinfo.py new file mode 100644 index 000000000000..1781e38ddf2e --- /dev/null +++ b/biz/djangoapps/ga_contract_operation/additionalinfo.py @@ -0,0 +1,137 @@ +import logging +import time + +from django.contrib.auth.models import User +from django.db import transaction +from django.utils.translation import ugettext as _ + +from biz.djangoapps.ga_contract.models import AdditionalInfo +from biz.djangoapps.ga_contract_operation.models import ContractTaskHistory, AdditionalInfoUpdateTaskTarget +from biz.djangoapps.ga_invitation.models import AdditionalInfoSetting, ContractRegister +from openedx.core.djangoapps.ga_task.models import Task +from openedx.core.djangoapps.ga_task.task import TaskProgress + +log = logging.getLogger(__name__) + + +def _validate_and_get_arguments(task_id, task_input): + if 'contract_id' not in task_input or 'history_id' not in task_input or 'additional_info_ids' not in task_input: + raise ValueError("Task {task_id}: Missing required value {task_input}".format(task_id=task_id, task_input=task_input)) + + try: + history_id = task_input['history_id'] + task_history = ContractTaskHistory.objects.select_related('contract__contractauth').get(pk=history_id) + except ContractTaskHistory.DoesNotExist: + # The ContactTaskHistory object should be committed in the view function before the task + # is submitted and reaches this point. + log.warning( + "Task {task_id}: Failed to get ContractTaskHistory with id {history_id}".format( + task_id=task_id, history_id=history_id + ) + ) + raise + + contract_id = task_input['contract_id'] + if task_history.contract.id != task_input['contract_id']: + _msg = "Contract id conflict: submitted value {task_history_contract_id} does not match {contract_id}".format( + task_history_contract_id=task_history.contract.id, contract_id=contract_id + ) + log.warning("Task {task_id}: {msg}".format(task_id=task_id, msg=_msg)) + raise ValueError(_msg) + + contract = task_history.contract + targets = AdditionalInfoUpdateTaskTarget.find_by_history_id(task_history.id) + + additional_info_list = AdditionalInfo.validate_and_find_by_ids(contract, task_input['additional_info_ids']) + if additional_info_list is None: + raise ValueError("Task {task_id}: Additional item is changed".format(task_id=task_id)) + + return (contract, targets, additional_info_list) + + +def perform_delegate_additionalinfo_update(entry_id, task_input, action_name): + """ + Executes to update additional info. This function is called by run_main_task. + """ + entry = Task.objects.get(pk=entry_id) + task_id = entry.task_id + contract, targets, additional_info_list = _validate_and_get_arguments(task_id, task_input) + + task_progress = TaskProgress(action_name, len(targets), time.time()) + task_progress.update_task_state() + + def _fail(message): + return (message, None, None) + + def _validate(inputline): + inputline_columns = inputline.split(',') if inputline else [] + len_inputline_columns = len(inputline_columns) + if not inputline: + # skip + return (None, None, None) + + if len_inputline_columns != 1 + len(additional_info_list): + # e-mail + additional_info* 1 line + return _fail(_("Number of [emails] and [new items] must be the same.")) + + return _validate_email_and_additional_info(*inputline_columns) + + def _validate_email_and_additional_info(email, *additional_info_values): + # validate user + try: + user = User.objects.get(email=email) + + if not ContractRegister.objects.filter(user=user, contract=contract).exists(): + return _fail(_("Could not find target user.")) + + except User.DoesNotExist: + return _fail(_("The user does not exist. ({email})").format(email=email)) + + # validate additional info value + value_max_length = AdditionalInfoSetting._meta.get_field('value').max_length + for additional_info_value in additional_info_values: + if len(additional_info_value) > value_max_length: + return _fail(_("Please enter the name of item within {max_number} characters.").format(max_number=value_max_length)) + + return (None, user, additional_info_values) + + for line_number, target in enumerate(targets, start=1): + task_progress.attempt() + try: + with transaction.atomic(): + message, user, additional_info_values = _validate(target.inputline) + + if user is None: + if message is None: + task_progress.skip() + else: + task_progress.fail() + else: + if additional_info_values: + # Additional info + for i, additional_info in enumerate(additional_info_list): + additional_info_setting, created = AdditionalInfoSetting.objects.get_or_create( + contract=contract, + display_name=additional_info.display_name, + user=user, + defaults={'value': additional_info_values[i]} + ) + if not created: + additional_info_setting.value = additional_info_values[i] + additional_info_setting.save() + + log.info("Task {task_id}: Success to process of register to User {user_id}".format(task_id=task_id, user_id=user.id)) + task_progress.success() + + target.complete(_("Line {line_number}:{message}").format(line_number=line_number, message=message) if message else "") + + except: + # If an exception occur, logging it and to continue processing next target. + log.exception(u"Task {task_id}: Failed to register {input}".format(task_id=task_id, input=target.inputline)) + task_progress.fail() + target.incomplete(_("Line {line_number}:{message}").format( + line_number=line_number, + message=_("Failed to register. Please operation again after a time delay."), + )) + + return task_progress.update_task_state() diff --git a/biz/djangoapps/ga_contract_operation/migrations/0005_additionalinfoupdatetasktarget.py b/biz/djangoapps/ga_contract_operation/migrations/0005_additionalinfoupdatetasktarget.py new file mode 100644 index 000000000000..60d9d820fa8a --- /dev/null +++ b/biz/djangoapps/ga_contract_operation/migrations/0005_additionalinfoupdatetasktarget.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ga_contract_operation', '0004_contractremindermail'), + ] + + operations = [ + migrations.CreateModel( + name='AdditionalInfoUpdateTaskTarget', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('message', models.CharField(max_length=1024, null=True)), + ('completed', models.BooleanField(default=False)), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('inputline', models.TextField()), + ('history', models.ForeignKey(to='ga_contract_operation.ContractTaskHistory')), + ], + options={ + 'ordering': ['id'], + 'abstract': False, + }, + ), + ] diff --git a/biz/djangoapps/ga_contract_operation/models.py b/biz/djangoapps/ga_contract_operation/models.py index f555bb6931c4..8ceeda0d5724 100644 --- a/biz/djangoapps/ga_contract_operation/models.py +++ b/biz/djangoapps/ga_contract_operation/models.py @@ -168,6 +168,25 @@ def find_by_history_id(cls, history_id): ) +class AdditionalInfoUpdateTaskTarget(ContractTaskTargetBase): + + inputline = models.TextField() + + @classmethod + def bulk_create(cls, history, inputline_list): + targets = [cls( + history=history, + inputline=inputline, + ) for inputline in inputline_list] + cls.objects.bulk_create(targets) + + @classmethod + def find_by_history_id(cls, history_id): + return cls.objects.filter( + history_id=history_id, + ) + + class ContractMailBase(models.Model): """ Abstract base class for mail settings for contract diff --git a/biz/djangoapps/ga_contract_operation/tasks.py b/biz/djangoapps/ga_contract_operation/tasks.py index dfd0fc417e37..91e9a4e28d2f 100644 --- a/biz/djangoapps/ga_contract_operation/tasks.py +++ b/biz/djangoapps/ga_contract_operation/tasks.py @@ -5,6 +5,7 @@ from celery import task from django.utils.translation import ugettext as _ +from biz.djangoapps.ga_contract_operation.additionalinfo import perform_delegate_additionalinfo_update from biz.djangoapps.ga_contract_operation.personalinfo import perform_delegate_personalinfo_mask from biz.djangoapps.ga_contract_operation.student_register import perform_delegate_student_register from biz.djangoapps.ga_contract_operation.student_unregister import perform_delegate_sutudent_unregister @@ -14,10 +15,12 @@ PERSONALINFO_MASK = 'personalinfo_mask' STUDENT_REGISTER = 'student_register' STUDENT_UNREGISTER = 'student_unregister' +ADDITIONALINFO_UPDATE = 'additionalinfo_update' TASKS = { PERSONALINFO_MASK: _("Personal Information Mask"), STUDENT_REGISTER: _("Student Register"), STUDENT_UNREGISTER: _("Student Unregister"), + ADDITIONALINFO_UPDATE: _("Additional Item Update"), } @@ -40,3 +43,10 @@ def student_unregister(entry_id): action_name = STUDENT_UNREGISTER visit_func = perform_delegate_sutudent_unregister return run_main_task(entry_id, visit_func, action_name) + + +@task(base=BaseTask) +def additional_info_update(entry_id): + action_name = ADDITIONALINFO_UPDATE + visit_func = perform_delegate_additionalinfo_update + return run_main_task(entry_id, visit_func, action_name) diff --git a/biz/djangoapps/ga_contract_operation/tests/factories.py b/biz/djangoapps/ga_contract_operation/tests/factories.py index af57f5861fe2..50a811a90061 100644 --- a/biz/djangoapps/ga_contract_operation/tests/factories.py +++ b/biz/djangoapps/ga_contract_operation/tests/factories.py @@ -5,7 +5,7 @@ from student.tests.factories import UserFactory from biz.djangoapps.ga_contract_operation.models import ( - ContractMail, ContractReminderMail, + ContractMail, ContractReminderMail, AdditionalInfoUpdateTaskTarget, ContractTaskHistory, ContractTaskTarget, StudentRegisterTaskTarget, StudentUnregisterTaskTarget, ) @@ -40,6 +40,13 @@ class Meta(object): model = StudentUnregisterTaskTarget +class AdditionalInfoUpdateTaskTargetFactory(DjangoModelFactory): + """Factory for the AdditionalInfoUpdateTaskTarget model""" + + class Meta(object): + model = AdditionalInfoUpdateTaskTarget + + class ContractMailFactory(DjangoModelFactory): """Factory for the ContractMail model""" diff --git a/biz/djangoapps/ga_contract_operation/tests/test_additionalinfo.py b/biz/djangoapps/ga_contract_operation/tests/test_additionalinfo.py new file mode 100644 index 000000000000..2fd2751e7190 --- /dev/null +++ b/biz/djangoapps/ga_contract_operation/tests/test_additionalinfo.py @@ -0,0 +1,338 @@ +""" +Tests for additional item task +""" +from mock import patch + +from django.utils.crypto import get_random_string + +from biz.djangoapps.ga_contract_operation.models import AdditionalInfoUpdateTaskTarget +from biz.djangoapps.ga_contract_operation.tasks import additional_info_update +from biz.djangoapps.ga_contract_operation.tests.factories import AdditionalInfoUpdateTaskTargetFactory +from biz.djangoapps.ga_invitation.models import AdditionalInfoSetting +from biz.djangoapps.util.tests.testcase import BizViewTestBase +from openedx.core.djangoapps.ga_task.tests.test_task import TaskTestMixin +from student.tests.factories import UserFactory + +ADDITIONAL_INFO = ['setting1', 'setting2'] + + +class AdditionalInfoTaskTest(BizViewTestBase, TaskTestMixin): + + def setUp(self): + super(AdditionalInfoTaskTest, self).setUp() + self.contract = self._create_contract() + self.registers = [self._create_user_and_contract_register(self.contract) for _ in range(3)] + self.history = self._create_task_history(contract=self.contract) + self.additional_infos = [self._create_additional_info(contract=self.contract, display_name=d) for d in ADDITIONAL_INFO] + self.additional_setting_value1 = get_random_string(8) + self.additional_setting_value2 = get_random_string(8) + + @staticmethod + def _create_targets(history, inputlines, completed=False): + for inputline in inputlines: + AdditionalInfoUpdateTaskTargetFactory.create(history=history, inputline=inputline, completed=completed) + + def _create_entry(self, contract=None, history=None, additional_infos=[]): + task_input = {} + if contract is not None: + task_input['contract_id'] = contract.id + if history is not None: + task_input['history_id'] = history.id + if additional_infos is not None: + task_input['additional_info_ids'] = [a.id for a in additional_infos] + return TaskTestMixin._create_input_entry(self, task_input=task_input) + + def _assert_additional_info_setting(self, user, display_name, value, contract=None): + if contract is None: + contract = self.contract + additional_info_setting = AdditionalInfoSetting.objects.filter( + contract=contract, user=user, display_name=display_name) + self.assertEqual(value, additional_info_setting[0].value) + + def test_success_update_additional_info(self): + # ---------------------------------------------------------- + # Setup test data + # ---------------------------------------------------------- + input_line = ['{},{},{}'.format(self.registers[0].user.email, + self.additional_setting_value1, + self.additional_setting_value2)] + self._create_targets(self.history, input_line) + + # ---------------------------------------------------------- + # Execute task + # ---------------------------------------------------------- + self._test_run_with_task( + additional_info_update, + 'additionalinfo_update', + task_entry=self._create_entry( + contract=self.contract, history=self.history, additional_infos=self.additional_infos), + expected_attempted=1, + expected_num_succeeded=1, + expected_total=1, + ) + + # ---------------------------------------------------------- + # Assertion + # ---------------------------------------------------------- + self.assertEqual(0, AdditionalInfoUpdateTaskTarget.objects.filter(history=self.history, completed=False).count()) + self.assertEqual(1, AdditionalInfoUpdateTaskTarget.objects.filter(history=self.history, completed=True).count()) + self._assert_additional_info_setting(user=self.registers[0].user, + display_name=ADDITIONAL_INFO[0], + value=self.additional_setting_value1) + self._assert_additional_info_setting(user=self.registers[0].user, + display_name=ADDITIONAL_INFO[1], + value=self.additional_setting_value2) + + def test_user_does_not_exist_in_gacco(self): + # ---------------------------------------------------------- + # Setup test data + # ---------------------------------------------------------- + email = '{}@gacco.com'.format(get_random_string(8)) + input_line = ['{},{},{}'.format(email, + self.additional_setting_value1, + self.additional_setting_value2)] + self._create_targets(self.history, input_line) + + # ---------------------------------------------------------- + # Execute task + # ---------------------------------------------------------- + self._test_run_with_task( + additional_info_update, + 'additionalinfo_update', + task_entry=self._create_entry( + contract=self.contract, history=self.history, additional_infos=self.additional_infos), + expected_attempted=1, + expected_num_failed=1, + expected_total=1, + ) + + # ---------------------------------------------------------- + # Assertion + # ---------------------------------------------------------- + self.assertEqual(0, AdditionalInfoUpdateTaskTarget.objects.filter(history=self.history, completed=False).count()) + self.assertEqual(1, AdditionalInfoUpdateTaskTarget.objects.filter(history=self.history, completed=True).count()) + self.assertEqual( + "Line 1:The user does not exist. ({email})".format(email=email), + AdditionalInfoUpdateTaskTarget.objects.get(history=self.history).message, + ) + + def test_user_does_not_exist_in_contract(self): + # ---------------------------------------------------------- + # Setup test data + # ---------------------------------------------------------- + user = UserFactory.create() + input_line = ['{},{},{}'.format(user.email, + self.additional_setting_value1, + self.additional_setting_value2)] + self._create_targets(self.history, input_line) + + # ---------------------------------------------------------- + # Execute task + # ---------------------------------------------------------- + self._test_run_with_task( + additional_info_update, + 'additionalinfo_update', + task_entry=self._create_entry( + contract=self.contract, history=self.history, additional_infos=self.additional_infos), + expected_attempted=1, + expected_num_failed=1, + expected_total=1, + ) + + # ---------------------------------------------------------- + # Assertion + # ---------------------------------------------------------- + self.assertEqual(0, AdditionalInfoUpdateTaskTarget.objects.filter(history=self.history, completed=False).count()) + self.assertEqual(1, AdditionalInfoUpdateTaskTarget.objects.filter(history=self.history, completed=True).count()) + self.assertEqual( + "Line 1:Could not find target user.", + AdditionalInfoUpdateTaskTarget.objects.get(history=self.history).message, + ) + + def test_additional_info_over_max_char_length(self): + # ---------------------------------------------------------- + # Setup test data + # ---------------------------------------------------------- + over_max_char_length = get_random_string(256) + input_line = ['{},{},{}'.format(self.registers[0].user.email, + self.additional_setting_value1, + over_max_char_length)] + self._create_targets(self.history, input_line) + + # ---------------------------------------------------------- + # Execute task + # ---------------------------------------------------------- + self._test_run_with_task( + additional_info_update, + 'additionalinfo_update', + task_entry=self._create_entry( + contract=self.contract, history=self.history, additional_infos=self.additional_infos), + expected_attempted=1, + expected_num_failed=1, + expected_total=1, + ) + + # ---------------------------------------------------------- + # Assertion + # ---------------------------------------------------------- + self.assertEqual(0, AdditionalInfoUpdateTaskTarget.objects.filter(history=self.history, completed=False).count()) + self.assertEqual(1, AdditionalInfoUpdateTaskTarget.objects.filter(history=self.history, completed=True).count()) + self.assertEqual( + "Line 1:Please enter the name of item within 255 characters.", + AdditionalInfoUpdateTaskTarget.objects.get(history=self.history).message, + ) + + def test_number_of_args_does_not_match(self): + # ---------------------------------------------------------- + # Setup test data + # ---------------------------------------------------------- + input_line = ['{},{}'.format(self.registers[0].user.email, + self.additional_setting_value1)] + self._create_targets(self.history, input_line) + + # ---------------------------------------------------------- + # Execute task + # ---------------------------------------------------------- + self._test_run_with_task( + additional_info_update, + 'additionalinfo_update', + task_entry=self._create_entry( + contract=self.contract, history=self.history, additional_infos=self.additional_infos), + expected_attempted=1, + expected_num_failed=1, + expected_total=1, + ) + + # ---------------------------------------------------------- + # Assertion + # ---------------------------------------------------------- + self.assertEqual(0, AdditionalInfoUpdateTaskTarget.objects.filter(history=self.history, completed=False).count()) + self.assertEqual(1, AdditionalInfoUpdateTaskTarget.objects.filter(history=self.history, completed=True).count()) + self.assertEqual( + "Line 1:Number of [emails] and [new items] must be the same.", + AdditionalInfoUpdateTaskTarget.objects.get(history=self.history).message, + ) + + def test_additional_info_changed(self): + # ---------------------------------------------------------- + # Setup test data + # ---------------------------------------------------------- + additional_info = ['setting3'] + additional_infos = [self._create_additional_info(contract=self.contract, display_name=d) for d in additional_info] + input_line = ['{},{},{}'.format(self.registers[0].user.email, + self.additional_setting_value1, + self.additional_setting_value2)] + self._create_targets(self.history, input_line) + + # ---------------------------------------------------------- + # Execute task, Assertion + # ---------------------------------------------------------- + entry = self._create_entry(contract=self.contract, history=self.history, additional_infos=additional_infos) + with self.assertRaises(ValueError) as cm: + self._test_run_with_task( + additional_info_update, + 'additionalinfo_update', + task_entry=entry, + expected_attempted=1, + expected_num_failed=1, + expected_total=1, + ) + # ---------------------------------------------------------- + # Assertion + # ---------------------------------------------------------- + self.assertEqual("Task {}: Additional item is changed".format(entry.task_id), cm.exception.message) + self._assert_task_failure(entry.id) + + def test_contract_id_does_not_exist(self): + # ---------------------------------------------------------- + # Setup test data + # ---------------------------------------------------------- + input_line = ['{},{},{}'.format(self.registers[0].user.email, + self.additional_setting_value1, + self.additional_setting_value2)] + self._create_targets(self.history, input_line) + contract = self._create_contract() + + # ---------------------------------------------------------- + # Execute task, Assertion + # ---------------------------------------------------------- + entry = self._create_entry(contract=contract, history=self.history, additional_infos=self.additional_infos) + with self.assertRaises(ValueError): + self._test_run_with_failure( + additional_info_update, + "Contract id conflict: submitted value {} does not match {}".format(self.contract.id, contract.id), + task_entry=entry, + ) + + def test_unexpected_exception(self): + # ---------------------------------------------------------- + # Setup test data + # ---------------------------------------------------------- + input_line = ['{},{},{}'.format(self.registers[0].user.email, + self.additional_setting_value1, + self.additional_setting_value2)] + self._create_targets(self.history, input_line) + + # ---------------------------------------------------------- + # Execute task, Assertion + # ---------------------------------------------------------- + with patch('biz.djangoapps.ga_contract_operation.additionalinfo.AdditionalInfoSetting.save', side_effect=Exception): + self._test_run_with_task( + additional_info_update, + 'additionalinfo_update', + task_entry=self._create_entry( + contract=self.contract, history=self.history, additional_infos=self.additional_infos), + expected_attempted=1, + expected_num_failed=1, + expected_total=1, + ) + + # ---------------------------------------------------------- + # Assertion + # ---------------------------------------------------------- + self.assertEqual(1, AdditionalInfoUpdateTaskTarget.objects.filter(history=self.history, completed=False).count()) + self.assertEqual(0, AdditionalInfoUpdateTaskTarget.objects.filter(history=self.history, completed=True).count()) + self.assertEqual( + "Line 1:Failed to register. Please operation again after a time delay.", + AdditionalInfoUpdateTaskTarget.objects.get(history=self.history).message + ) + + def test_multiple_line_succeeded_skipped_failed(self): + # ---------------------------------------------------------- + # Setup test data + # ---------------------------------------------------------- + line1 = '' + line2 = '{},{},{}'.format(self.registers[0].user.email, + self.additional_setting_value1, + self.additional_setting_value2) + line3 = '{},{}'.format(self.registers[1].user.email, + self.additional_setting_value1) + input_line = [line1, line2, line3] + self._create_targets(self.history, input_line) + + # ---------------------------------------------------------- + # Execute task + # ---------------------------------------------------------- + self._test_run_with_task( + additional_info_update, + 'additionalinfo_update', + task_entry=self._create_entry( + contract=self.contract, history=self.history, additional_infos=self.additional_infos), + expected_attempted=3, + expected_num_succeeded=1, + expected_num_skipped=1, + expected_num_failed=1, + expected_total=3, + ) + + # ---------------------------------------------------------- + # Assertion + # ---------------------------------------------------------- + self.assertEqual(0, AdditionalInfoUpdateTaskTarget.objects.filter(history=self.history, completed=False).count()) + self.assertEqual(3, AdditionalInfoUpdateTaskTarget.objects.filter(history=self.history, completed=True).count()) + self._assert_additional_info_setting(user=self.registers[0].user, + display_name=ADDITIONAL_INFO[0], + value=self.additional_setting_value1) + self._assert_additional_info_setting(user=self.registers[0].user, + display_name=ADDITIONAL_INFO[1], + value=self.additional_setting_value2) diff --git a/biz/djangoapps/ga_contract_operation/tests/test_views.py b/biz/djangoapps/ga_contract_operation/tests/test_views.py index f5d54c60cfc1..27b6c80b6f85 100644 --- a/biz/djangoapps/ga_contract_operation/tests/test_views.py +++ b/biz/djangoapps/ga_contract_operation/tests/test_views.py @@ -13,9 +13,12 @@ from django.core.urlresolvers import reverse from django.db import IntegrityError from django.test.utils import override_settings +from django.utils.crypto import get_random_string from biz.djangoapps.ga_achievement.management.commands.update_biz_score_status import get_grouped_target_sections -from biz.djangoapps.ga_contract_operation.models import ContractMail, ContractReminderMail, ContractTaskHistory +from biz.djangoapps.ga_contract.models import AdditionalInfo +from biz.djangoapps.ga_contract_operation.models import ContractMail, ContractReminderMail, ContractTaskHistory, AdditionalInfoUpdateTaskTarget +from biz.djangoapps.ga_contract_operation.tasks import ADDITIONALINFO_UPDATE from biz.djangoapps.ga_contract_operation.tests.factories import ContractTaskHistoryFactory, ContractTaskTargetFactory, StudentRegisterTaskTargetFactory, StudentUnregisterTaskTargetFactory from biz.djangoapps.ga_invitation.models import ContractRegister, INPUT_INVITATION_CODE, REGISTER_INVITATION_CODE, UNREGISTER_INVITATION_CODE from biz.djangoapps.ga_invitation.tests.factories import ContractRegisterFactory @@ -28,6 +31,8 @@ from student.tests.factories import UserFactory from xmodule.modulestore.tests.factories import ItemFactory +ERROR_MSG = "Test Message" + @ddt.ddt class ContractOperationViewTest(BizContractTestBase): @@ -58,6 +63,20 @@ def test_register_contract_unmatch(self): data = json.loads(response.content) self.assertEquals(data['error'], 'Current contract is changed. Please reload this page.') + def test_register_validate_task_error(self): + self.setup_user() + csv_content = u"test_student1@example.com,test_student_1,テスター1\n" \ + u"test_student2@example.com,test_student_1,テスター2" + + with self.skip_check_course_selection(current_contract=self.contract), patch( + 'biz.djangoapps.ga_contract_operation.views.validate_task') as mock_validate_task: + mock_validate_task.return_value = ERROR_MSG + response = self.client.post(self._url_register_students_ajax(), {'contract_id': self.contract.id, 'students_list': csv_content}) + + self.assertEqual(400, response.status_code) + data = json.loads(response.content) + self.assertEqual(data['error'], ERROR_MSG) + def test_register_no_param(self): self.setup_user() csv_content = u"test_student1@example.com,test_student_1,テスター1\n" \ @@ -171,7 +190,9 @@ def test_register_student_submit_duplicated(self): csv_content = u"test_student1@example.com,test_student_1,テスター1\n" \ u"test_student2@example.com,test_student_1,テスター2" - with self.skip_check_course_selection(current_contract=self.contract): + with self.skip_check_course_selection(current_contract=self.contract), patch( + 'biz.djangoapps.ga_contract_operation.views.validate_task') as mock_validate_task: + mock_validate_task.return_value = None response = self.client.post(self._url_register_students_ajax(), {'contract_id': self.contract.id, 'students_list': csv_content}) self.assertEqual(400, response.status_code) @@ -352,6 +373,21 @@ def test_unregister_spoc_staff(self): self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_spoc1.id)) self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_spoc2.id)) + def test_personalinfo_mask_validate_task_error(self): + self.setup_user() + register = ContractRegisterFactory.create(user=self.user, contract=self.contract, status=REGISTER_INVITATION_CODE) + CourseEnrollment.enroll(self.user, self.course_spoc1.id) + CourseEnrollment.enroll(self.user, self.course_spoc2.id) + + with self.skip_check_course_selection(current_contract=self.contract), patch( + 'biz.djangoapps.ga_contract_operation.views.validate_task') as mock_validate_task: + mock_validate_task.return_value = ERROR_MSG + response = self.client.post(self._url_unregister_students_ajax(), {'contract_id': self.contract.id, 'target_list': [register.id]}) + + self.assertEqual(response.status_code, 400) + data = json.loads(response.content) + self.assertEquals(ERROR_MSG, data['error']) + def test_unregister_spoc_staff(self): self.setup_user() # to be staff @@ -442,7 +478,9 @@ def test_personalinfo_mask_submit_duplicated(self): self.setup_user() - with self.skip_check_course_selection(current_contract=self.contract): + with self.skip_check_course_selection(current_contract=self.contract), patch( + 'biz.djangoapps.ga_contract_operation.views.validate_task') as mock_validate_task: + mock_validate_task.return_value = None response = self.client.post(self._url_personalinfo_mask, params) self.assertEqual(400, response.status_code) @@ -451,6 +489,25 @@ def test_personalinfo_mask_submit_duplicated(self): # assert not to be created new Task instance. self.assertEqual(1, Task.objects.count()) + def test_personalinfo_mask_validate_task_error(self): + registers = [ + self.create_contract_register(UserFactory.create(), self.contract), + self.create_contract_register(UserFactory.create(), self.contract), + ] + params = {'target_list': [register.id for register in registers], 'contract_id': self.contract.id} + TaskFactory.create(task_type='personalinfo_mask', task_key=hashlib.md5(str(self.contract.id)).hexdigest()) + + self.setup_user() + + with self.skip_check_course_selection(current_contract=self.contract), patch( + 'biz.djangoapps.ga_contract_operation.views.validate_task') as mock_validate_task: + mock_validate_task.return_value = ERROR_MSG + response = self.client.post(self._url_personalinfo_mask, params) + + self.assertEqual(400, response.status_code) + data = json.loads(response.content) + self.assertEqual(ERROR_MSG, data['error']) + @ddt.data('target_list', 'contract_id') def test_personalinfo_mask_missing_params(self, param): params = {'target_list': [1, 2], 'contract_id': 1, } @@ -1615,6 +1672,20 @@ def test_bulk_unregister_no_param(self): data = json.loads(response.content) self.assertEqual(data['error'], 'Unauthorized access.') + def test_bulk_unregister_validate_task_error(self): + self.setup_user() + csv_content = u"test_student_1\n" \ + u"test_student_2" + + with self.skip_check_course_selection(current_contract=self.contract), patch( + 'biz.djangoapps.ga_contract_operation.views.validate_task') as mock_validate_task: + mock_validate_task.return_value = ERROR_MSG + response = self.client.post(self._url_bulk_unregister_students_ajax, {'contract_id': self.contract.id, 'students_list': csv_content}) + + self.assertEqual(400, response.status_code) + data = json.loads(response.content) + self.assertEqual(data['error'], ERROR_MSG) + def test_bulk_unregister_no_param_students_list(self): self.setup_user() @@ -1737,7 +1808,9 @@ def test_bulk_unregister_student_submit_duplicated(self): self.setup_user() csv_content = self.user.username - with self.skip_check_course_selection(current_contract=self.contract): + with self.skip_check_course_selection(current_contract=self.contract), patch( + 'biz.djangoapps.ga_contract_operation.views.validate_task') as mock_validate_task: + mock_validate_task.return_value = None response = self.client.post(self._url_bulk_unregister_students_ajax, {'contract_id': self.contract.id, 'students_list': csv_content}) self.assertEqual(400, response.status_code) @@ -1795,6 +1868,20 @@ def test_bulk_personalinfo_mask_no_param(self): data = json.loads(response.content) self.assertEqual(data['error'], 'Unauthorized access.') + def test_bulk_personalinfo_mask_validate_task_error(self): + self.setup_user() + csv_content = u"test_student_1\n" \ + u"test_student_2" + + with self.skip_check_course_selection(current_contract=self.contract), patch( + 'biz.djangoapps.ga_contract_operation.views.validate_task') as mock_validate_task: + mock_validate_task.return_value = ERROR_MSG + response = self.client.post(self._url_bulk_personalinfo_mask_students_ajax, {'contract_id': self.contract.id, 'students_list': csv_content}) + + self.assertEqual(400, response.status_code) + data = json.loads(response.content) + self.assertEqual(data['error'], ERROR_MSG) + def test_bulk_personalinfo_mask_no_param_students_list(self): self.setup_user() @@ -1872,7 +1959,9 @@ def test_bulk_personalinfo_mask_submit_duplicated(self): self.setup_user() csv_content = self.user.username - with self.skip_check_course_selection(current_contract=self.contract): + with self.skip_check_course_selection(current_contract=self.contract), patch( + 'biz.djangoapps.ga_contract_operation.views.validate_task') as mock_validate_task: + mock_validate_task.return_value = None response = self.client.post(self._url_bulk_personalinfo_mask_students_ajax, {'contract_id': self.contract.id, 'students_list': csv_content}) self.assertEqual(400, response.status_code) @@ -1945,7 +2034,7 @@ def test_task_history(self): self._create_task('personalinfo_mask', 'key1', 'task_id1', 'SUCCESS', 1, 1, 1, 0, 0), self._create_task('student_unregister', 'key2', 'task_id2', 'FAILURE', 1, 1, 0, 1, 0), self._create_task('student_register', 'key3', 'task_id3', 'QUEUING', 1, 1, 0, 0, 1), - self._create_task('dummy_task', 'key4', 'task_id4', 'PROGRESS'), + self._create_task('additionalinfo_update', 'key4', 'task_id4', 'PROGRESS'), self._create_task('dummy_task', 'key5', 'tesk_id5', 'DUMMY'), ] # Create histories for target contract @@ -1971,7 +2060,415 @@ def test_task_history(self): self.assertEqual(5, data['total']) records = data['records'] self._assert_task_history(records[0], 1, 'Unknown', 'Unknown', self.user.username, histories[4].created) - self._assert_task_history(records[1], 2, 'Unknown', 'In Progress', self.user.username, histories[3].created) + self._assert_task_history(records[1], 2, 'Additional Item Update', 'In Progress', self.user.username, histories[3].created) self._assert_task_history(records[2], 3, 'Student Register', 'Waiting', self.user.username, histories[2].created, 1, 0, 0, 1, task_target_register.id, 'message3') self._assert_task_history(records[3], 4, 'Student Unregister', 'Complete', self.user.username, histories[1].created, 1, 0, 1, 0, task_target_unregister.id, 'message2') self._assert_task_history(records[4], 5, 'Personal Information Mask', 'Complete', self.user.username, histories[0].created, 1, 1, 0, 0, task_target_mask.id, 'message1') + + +@ddt.ddt +class ContractOperationViewAdditionalInfoTest(BizContractTestBase): + + def setUp(self): + super(ContractOperationViewAdditionalInfoTest, self).setUp() + self.setup_user() + + def _register_additional_info_ajax(self, params): + return self.client.post(reverse('biz:contract_operation:register_additional_info_ajax'), params) + + def _edit_additional_info_ajax(self, params): + return self.client.post(reverse('biz:contract_operation:edit_additional_info_ajax'), params) + + def _delete_additional_info_ajax(self, params): + return self.client.post(reverse('biz:contract_operation:delete_additional_info_ajax'), params) + + def _update_additional_info_ajax(self, params): + return self.client.post(reverse('biz:contract_operation:update_additional_info_ajax'), params) + + def _assert_response_error_json(self, response, error_message): + self.assertEqual(400, response.status_code) + self.assertEqual(error_message, json.loads(response.content)['error']) + + # ------------------------------------------------------------ + # register_additional_info_ajax + # ------------------------------------------------------------ + @ddt.data('display_name', 'contract_id') + def test_register_additional_info_no_param(self, param): + params = {'display_name': 'test', + 'contract_id': self.contract.id, } + del params[param] + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._register_additional_info_ajax(params), + "Unauthorized access.") + + def test_register_additional_info_different_contract(self): + params = {'display_name': 'test', + 'contract_id': self._create_contract().id, } + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._register_additional_info_ajax(params), + "Current contract is changed. Please reload this page.") + + def test_register_additional_info_validate_task_error(self): + params = {'display_name': 'test', + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract), patch( + 'biz.djangoapps.ga_contract_operation.views.validate_task') as mock_validate_task: + mock_validate_task.return_value = ERROR_MSG + self._assert_response_error_json(self._register_additional_info_ajax(params), ERROR_MSG) + + def test_register_additional_info_empty_display_name(self): + params = {'display_name': '', + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._register_additional_info_ajax(params), + "Please enter the name of item you wish to add.") + + def test_register_additional_info_over_length_display_name(self): + max_length = AdditionalInfo._meta.get_field('display_name').max_length + params = {'display_name': get_random_string(max_length + 1), + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._register_additional_info_ajax(params), + "Please enter the name of item within {max_number} characters.".format(max_number=max_length)) + + def test_register_additional_info_same_display_name(self): + self._create_additional_info(contract=self.contract, display_name='test') + params = {'display_name': 'test', + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._register_additional_info_ajax(params), + "The same item has already been registered.") + + @override_settings(BIZ_MAX_REGISTER_ADDITIONAL_INFO=1) + def test_register_additional_info_max_additional_info(self): + self._create_additional_info(contract=self.contract, display_name='hoge') + params = {'display_name': 'test', + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._register_additional_info_ajax(params), + "Up to {max_number} number of additional item is created.".format(max_number=1)) + + def test_register_additional_info_db_error(self): + params = {'display_name': 'test', + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract), patch.object(AdditionalInfo, 'objects') as patched_manager: + patched_manager.create.side_effect = Exception + patched_manager.filter.return_value.exists.return_value = False + patched_manager.filter.return_value.count.return_value = 0 + + self._assert_response_error_json(self._register_additional_info_ajax(params), + "Failed to register item.") + + def test_register_additional_info_success(self): + params = {'display_name': 'test', + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract): + response = self._register_additional_info_ajax(params) + self.assertEqual(200, response.status_code) + self.assertEqual("New item has been registered.", json.loads(response.content)['info']) + + # ------------------------------------------------------------ + # edit_additional_info_ajax + # ------------------------------------------------------------ + @ddt.data('additional_info_id', 'display_name', 'contract_id') + def test_edit_additional_info_no_param(self, param): + additional_info = self._create_additional_info(contract=self.contract) + params = {'additional_info_id': additional_info.id, + 'display_name': 'test', + 'contract_id': self.contract.id, } + del params[param] + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._edit_additional_info_ajax(params), + "Unauthorized access.") + + def test_edit_additional_info_different_contract(self): + additional_info = self._create_additional_info(contract=self.contract) + params = {'additional_info_id': additional_info.id, + 'display_name': 'test', + 'contract_id': self._create_contract().id, } + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._edit_additional_info_ajax(params), + "Current contract is changed. Please reload this page.") + + def test_edit_additional_info_validate_task_error(self): + additional_info = self._create_additional_info(contract=self.contract) + params = {'additional_info_id': additional_info.id, + 'display_name': 'test', + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract), patch( + 'biz.djangoapps.ga_contract_operation.views.validate_task') as mock_validate_task: + mock_validate_task.return_value = ERROR_MSG + self._assert_response_error_json(self._edit_additional_info_ajax(params), ERROR_MSG) + + def test_edit_additional_info_empty_display_name(self): + additional_info = self._create_additional_info(contract=self.contract) + params = {'additional_info_id': additional_info.id, + 'display_name': '', + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._edit_additional_info_ajax(params), + "Please enter the name of item you wish to add.") + + def test_edit_additional_info_over_length_display_name(self): + max_length = AdditionalInfo._meta.get_field('display_name').max_length + additional_info = self._create_additional_info(contract=self.contract) + params = {'additional_info_id': additional_info.id, + 'display_name': get_random_string(max_length + 1), + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._edit_additional_info_ajax(params), + "Please enter the name of item within {max_number} characters.".format(max_number=max_length)) + + def test_edit_additional_info_same_display_name(self): + self._create_additional_info(contract=self.contract, display_name='test') + additional_info = self._create_additional_info(contract=self.contract) + params = {'additional_info_id': additional_info.id, + 'display_name': 'test', + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._edit_additional_info_ajax(params), + "The same item has already been registered.") + + def test_edit_additional_info_db_error(self): + additional_info = self._create_additional_info(contract=self.contract) + params = {'additional_info_id': additional_info.id, + 'display_name': 'test', + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract), patch.object(AdditionalInfo, 'objects') as patched_manager: + patched_manager.filter.return_value.exclude.return_value.exists.return_value = False + patched_manager.filter.return_value.update.side_effect = Exception + + self._assert_response_error_json(self._edit_additional_info_ajax(params), + "Failed to edit item.") + + def test_edit_additional_info_success(self): + additional_info = self._create_additional_info(contract=self.contract) + params = {'additional_info_id': additional_info.id, + 'display_name': 'test', + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract): + response = self._edit_additional_info_ajax(params) + self.assertEqual(200, response.status_code) + self.assertEqual("New item has been updated.", json.loads(response.content)['info']) + + # ------------------------------------------------------------ + # delete_additional_info_ajax + # ------------------------------------------------------------ + @ddt.data('additional_info_id', 'contract_id') + def test_delete_additional_info_no_param(self, param): + additional_info = self._create_additional_info(contract=self.contract) + params = {'additional_info_id': additional_info.id, + 'contract_id': self.contract.id, } + del params[param] + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._delete_additional_info_ajax(params), + "Unauthorized access.") + + def test_delete_additional_info_different_contract(self): + additional_info = self._create_additional_info(contract=self.contract) + params = {'additional_info_id': additional_info.id, + 'contract_id': self._create_contract().id, } + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._delete_additional_info_ajax(params), + "Current contract is changed. Please reload this page.") + + def test_delete_additional_info_validate_task_error(self): + additional_info = self._create_additional_info(contract=self.contract) + params = {'additional_info_id': additional_info.id, + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract), patch( + 'biz.djangoapps.ga_contract_operation.views.validate_task') as mock_validate_task: + mock_validate_task.return_value = ERROR_MSG + self._assert_response_error_json(self._delete_additional_info_ajax(params), ERROR_MSG) + + def test_delete_additional_info_does_not_exist(self): + additional_info = self._create_additional_info(contract=self.contract) + params = {'additional_info_id': additional_info.id, + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract), patch.object(AdditionalInfo, 'objects') as patched_manager: + patched_manager.get.side_effect = AdditionalInfo.DoesNotExist + + self._assert_response_error_json(self._delete_additional_info_ajax(params), + "Already deleted.") + + def test_delete_additional_info_db_error(self): + additional_info = self._create_additional_info(contract=self.contract) + params = {'additional_info_id': additional_info.id, + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract), patch.object(AdditionalInfo, 'objects') as patched_manager: + patched_manager.get.return_value.delete.side_effect = Exception + + self._assert_response_error_json(self._delete_additional_info_ajax(params), + "Failed to deleted item.") + + def test_delete_additional_info_success(self): + additional_info = self._create_additional_info(contract=self.contract) + params = {'additional_info_id': additional_info.id, + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract): + response = self._delete_additional_info_ajax(params) + self.assertEqual(200, response.status_code) + self.assertEqual("New item has been deleted.", json.loads(response.content)['info']) + + +class ContractOperationViewUpdateAdditionalInfoTest(BizContractTestBase): + + def setUp(self): + super(ContractOperationViewUpdateAdditionalInfoTest, self).setUp() + self.setup_user() + + def _update_additional_info_ajax(self, params): + return self.client.post(reverse('biz:contract_operation:update_additional_info_ajax'), params) + + def _assert_response_error_json(self, response, error_message): + self.assertEqual(400, response.status_code) + self.assertEqual(error_message, json.loads(response.content)['error']) + + # ------------------------------------------------------------ + # update_additional_info_ajax + # ------------------------------------------------------------ + def test_update_additional_info_success(self): + contract = self._create_contract(contract_name='test_update_additional_info_success') + self._create_user_and_contract_register(contract=contract, email='test_student1@example.com') + self._create_user_and_contract_register(contract=contract, email='test_student2@example.com') + additional_info1 = self._create_additional_info(contract=contract) + additional_info2 = self._create_additional_info(contract=contract) + input_line = u"test_student1@example.com,add1,add2\n" \ + u"test_student2@example.com,add1,add2" + + params = {'additional_info': [additional_info1.id, additional_info2.id], + 'contract_id': contract.id, + 'update_students_list': input_line} + + with self.skip_check_course_selection(current_contract=contract): + response = self._update_additional_info_ajax(params) + + self.assertEqual(200, response.status_code) + data = json.loads(response.content) + + # get latest task and assert + task = Task.objects.all().order_by('-id')[0] + self.assertEqual(ADDITIONALINFO_UPDATE, task.task_type) + task_input = json.loads(task.task_input) + self.assertEqual(contract.id, task_input['contract_id']) + history = ContractTaskHistory.objects.get(pk=task_input['history_id']) + self.assertEqual(history.task_id, task.task_id) + for target in AdditionalInfoUpdateTaskTarget.objects.filter(history=history): + self.assertIn(target.inputline, input_line) + self.assertEqual( + "Began the processing of Additional Item Update.Execution status, please check from the task history.", + data['info']) + + def test_update_additional_info_contract_id_no_param(self): + params = {'update_students_list': 'test', + 'additional_info': 'test', } + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._update_additional_info_ajax(params), + "Unauthorized access.") + + def test_update_additional_info_student_list_no_param(self): + params = {'contract_id': self.contract.id, + 'additional_info': 'test', } + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._update_additional_info_ajax(params), + "Unauthorized access.") + + def test_update_additional_info_additional_info_no_param(self): + params = {'update_students_list': 'test', + 'contract_id': self.contract.id, } + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._update_additional_info_ajax(params), + "No additional item registered.") + + def test_update_additional_info_different_contract(self): + params = {'update_students_list': 'test', + 'contract_id': self._create_contract().id, + 'additional_info': 'test'} + + with self.skip_check_course_selection(current_contract=self.contract): + self._assert_response_error_json(self._update_additional_info_ajax(params), + "Current contract is changed. Please reload this page.") + + def test_update_additional_info_validate_task_error(self): + contract = self._create_contract(contract_name='test_update_additional_info_success') + self._create_user_and_contract_register(contract=contract, email='test_student1@example.com') + self._create_user_and_contract_register(contract=contract, email='test_student2@example.com') + additional_info1 = self._create_additional_info(contract=contract) + additional_info2 = self._create_additional_info(contract=contract) + input_line = u"test_student1@example.com,add1,add2\n" \ + u"test_student2@example.com,add1,add2" + + params = {'additional_info': [additional_info1.id, additional_info2.id], + 'contract_id': contract.id, + 'update_students_list': input_line} + + with self.skip_check_course_selection(current_contract=contract), patch( + 'biz.djangoapps.ga_contract_operation.views.validate_task') as mock_validate_task: + mock_validate_task.return_value = ERROR_MSG + self._assert_response_error_json(self._update_additional_info_ajax(params), ERROR_MSG) + + def test_update_additional_info_not_find_student_list(self): + params = {'update_students_list': '', + 'contract_id': self.contract.id, + 'additional_info': 'test'} + + self._assert_response_error_json(self._update_additional_info_ajax(params), + "Could not find student list.") + + @override_settings(BIZ_MAX_REGISTER_NUMBER=2) + def test_update_additional_info_over_max_line(self): + input_line = u"test_student1@example.com,add1,add2\n" \ + u"test_student2@example.com,add1,add2\n" \ + u"test_student3@example.com,add1,add2" + + params = {'update_students_list': input_line, + 'contract_id': self.contract.id, + 'additional_info': 'test'} + + self._assert_response_error_json(self._update_additional_info_ajax(params), + "It has exceeded the number(2) of cases that can be a time of registration.") + + def test_update_additional_info_over_max_char_length(self): + input_line = get_random_string(3001) + params = {'update_students_list': input_line, + 'contract_id': self.contract.id, + 'additional_info': 'test'} + + self._assert_response_error_json(self._update_additional_info_ajax(params), + "The number of lines per line has exceeded the 3000 characters.") + + def test_update_additional_info_different_id(self): + additional_info1 = self._create_additional_info(contract=self.contract) + input_line = u"test_student1@example.com,add1" + params = {'additional_info': additional_info1.id, + 'contract_id': self.contract.id, + 'update_students_list': input_line} + + self._assert_response_error_json(self._update_additional_info_ajax(params), + "New item registered. Please reload browser.") diff --git a/biz/djangoapps/ga_contract_operation/urls.py b/biz/djangoapps/ga_contract_operation/urls.py index 18142ca22ef2..6b6eebf43506 100644 --- a/biz/djangoapps/ga_contract_operation/urls.py +++ b/biz/djangoapps/ga_contract_operation/urls.py @@ -8,6 +8,10 @@ url(r'^students$', 'students', name='students'), url(r'^register_students$', 'register_students', name='register_students'), + url(r'^register_students/register_additional_info$', 'register_additional_info_ajax', name='register_additional_info_ajax'), + url(r'^register_students/edit_additional_info$', 'edit_additional_info_ajax', name='edit_additional_info_ajax'), + url(r'^register_students/delete_additional_info$', 'delete_additional_info_ajax', name='delete_additional_info_ajax'), + url(r'^register_students/update_additional_info$', 'update_additional_info_ajax', name='update_additional_info_ajax'), url(r'^bulk_students$', 'bulk_students', name='bulk_students'), url(r'^register_students_ajax$', 'register_students_ajax', name='register_students_ajax'), url(r'^unregister_students_ajax$', 'unregister_students_ajax', name='unregister_students_ajax'), diff --git a/biz/djangoapps/ga_contract_operation/views.py b/biz/djangoapps/ga_contract_operation/views.py index 7d5bdda1a31e..b2144854a905 100644 --- a/biz/djangoapps/ga_contract_operation/views.py +++ b/biz/djangoapps/ga_contract_operation/views.py @@ -3,7 +3,6 @@ """ from collections import defaultdict from functools import wraps -import hashlib import json import logging @@ -16,12 +15,16 @@ from django.views.decorators.http import require_GET, require_POST from biz.djangoapps.ga_achievement.management.commands.update_biz_score_status import get_grouped_target_sections +from biz.djangoapps.ga_contract.models import AdditionalInfo from biz.djangoapps.ga_contract_operation.models import ( ContractMail, ContractReminderMail, - ContractTaskHistory, ContractTaskTarget, StudentRegisterTaskTarget, StudentUnregisterTaskTarget, + ContractTaskHistory, ContractTaskTarget, StudentRegisterTaskTarget, + StudentUnregisterTaskTarget, AdditionalInfoUpdateTaskTarget, +) +from biz.djangoapps.ga_contract_operation.tasks import ( + personalinfo_mask, student_register, student_unregister, additional_info_update, + TASKS, STUDENT_REGISTER, STUDENT_UNREGISTER, PERSONALINFO_MASK, ADDITIONALINFO_UPDATE, ) -from biz.djangoapps.ga_contract_operation.tasks import personalinfo_mask, student_register, student_unregister, \ - TASKS, STUDENT_REGISTER, STUDENT_UNREGISTER, PERSONALINFO_MASK from biz.djangoapps.ga_contract_operation.utils import send_mail from biz.djangoapps.ga_invitation.models import ( AdditionalInfoSetting, @@ -34,7 +37,7 @@ from biz.djangoapps.util.access_utils import has_staff_access from biz.djangoapps.util.decorators import check_course_selection from biz.djangoapps.util.json_utils import EscapedEdxJSONEncoder -from biz.djangoapps.util.task_utils import submit_task +from biz.djangoapps.util.task_utils import submit_task, validate_task, get_task_key from edxmako.shortcuts import render_to_response from openedx.core.djangoapps.ga_task.api import AlreadyRunningError @@ -60,6 +63,7 @@ def wrapper(request, *args, **kwargs): return _error_response(_("Unauthorized access.")) if str(request.current_contract.id) != request.POST['contract_id']: return _error_response(_("Current contract is changed. Please reload this page.")) + target_list = request.POST.getlist('target_list') if not target_list: return _error_response(_("Please select a target.")) @@ -158,6 +162,11 @@ def unregister_students_ajax(request, registers): valid_register_list = [] warning_register_list = [] + # Check the task running within the same contract. + validate_task_message = validate_task(request.current_contract) + if validate_task_message: + return _error_response(validate_task_message) + # validate for register in registers: # Validate status @@ -201,7 +210,9 @@ def register_students(request): return render_to_response( 'ga_contract_operation/register_students.html', { - 'max_register_number': settings.BIZ_MAX_REGISTER_NUMBER, + 'max_register_number': "{:,d}".format(int(settings.BIZ_MAX_REGISTER_NUMBER)), + 'additional_info_list': AdditionalInfo.objects.filter(contract=request.current_contract), + 'max_length_additional_info_display_name': AdditionalInfo._meta.get_field('display_name').max_length, } ) @@ -270,16 +281,23 @@ def bulk_students(request): ) -def _submit_task(request, task_type, task_class, history): +def _submit_task(request, task_type, task_class, history, additional_info_list=None): try: task_input = { 'contract_id': request.current_contract.id, 'history_id': history.id, } + if additional_info_list: + task_input['additional_info_ids'] = [a.id for a in additional_info_list] + + # Check the task running within the same contract. + validate_task_message = validate_task(request.current_contract) + if validate_task_message: + return _error_response(validate_task_message) + # task prevents duplicate execution by contract_id - task_key = hashlib.md5(str(request.current_contract.id)).hexdigest() - task = submit_task(request, task_type, task_class, task_input, task_key) + task = submit_task(request, task_type, task_class, task_input, get_task_key(request.current_contract)) history.link_to_task(task) except AlreadyRunningError: return _error_response( @@ -327,6 +345,8 @@ def _task_message(task, history): _task_targets = StudentUnregisterTaskTarget.find_by_history_id_and_message(history.id) elif task.task_type == PERSONALINFO_MASK: _task_targets = ContractTaskTarget.find_by_history_id_and_message(history.id) + elif task.task_type == ADDITIONALINFO_UPDATE: + _task_targets = AdditionalInfoUpdateTaskTarget.find_by_history_id_and_message(history.id) return [ { 'recid': task_target.id, @@ -670,3 +690,164 @@ def bulk_personalinfo_mask_ajax(request, students): ContractTaskTarget.bulk_create_by_text(history, students) return _submit_task(request, PERSONALINFO_MASK, personalinfo_mask, history) + + +@require_POST +@login_required +@check_course_selection +def register_additional_info_ajax(request): + + if any(k not in request.POST for k in ['display_name', 'contract_id']): + return _error_response(_("Unauthorized access.")) + + if str(request.current_contract.id) != request.POST['contract_id']: + return _error_response(_("Current contract is changed. Please reload this page.")) + + validate_task_message = validate_task(request.current_contract) + if validate_task_message: + return _error_response(validate_task_message) + + display_name = request.POST['display_name'] + if not display_name: + return _error_response(_("Please enter the name of item you wish to add.")) + + max_length_display_name = AdditionalInfo._meta.get_field('display_name').max_length + if len(display_name) > max_length_display_name: + return _error_response(_("Please enter the name of item within {max_number} characters.").format(max_number=max_length_display_name)) + + if AdditionalInfo.objects.filter(contract=request.current_contract, display_name=display_name).exists(): + return _error_response(_("The same item has already been registered.")) + + max_additional_info = settings.BIZ_MAX_REGISTER_ADDITIONAL_INFO + if AdditionalInfo.objects.filter(contract=request.current_contract).count() >= max_additional_info: + return _error_response(_("Up to {max_number} number of additional item is created.").format(max_number=max_additional_info)) + + try: + additional_info = AdditionalInfo.objects.create( + contract=request.current_contract, + display_name=display_name, + ) + except: + log.exception('Failed to register the display-name of an additional-info.') + return _error_response(_("Failed to register item.")) + + return JsonResponse({ + 'info': _("New item has been registered."), + 'id': additional_info.id, + }) + + +@require_POST +@login_required +@check_course_selection +def edit_additional_info_ajax(request): + + if any(k not in request.POST for k in ['additional_info_id', 'display_name', 'contract_id']): + return _error_response(_("Unauthorized access.")) + + if str(request.current_contract.id) != request.POST['contract_id']: + return _error_response(_("Current contract is changed. Please reload this page.")) + + validate_task_message = validate_task(request.current_contract) + if validate_task_message: + return _error_response(validate_task_message) + + display_name = request.POST['display_name'] + if not display_name: + return _error_response(_("Please enter the name of item you wish to add.")) + + max_length_display_name = AdditionalInfo._meta.get_field('display_name').max_length + if len(display_name) > max_length_display_name: + return _error_response(_("Please enter the name of item within {max_number} characters.").format(max_number=max_length_display_name)) + + additional_info_id = request.POST['additional_info_id'] + if AdditionalInfo.objects.filter(contract=request.current_contract, display_name=display_name).exclude(id=additional_info_id).exists(): + return _error_response(_("The same item has already been registered.")) + + try: + AdditionalInfo.objects.filter( + id=additional_info_id, + contract=request.current_contract, + ).update( + display_name=display_name, + ) + except: + log.exception('Failed to edit the display-name of an additional-info id:{}.'.format(request.POST['additional_info_id'])) + return _error_response(_("Failed to edit item.")) + + return JsonResponse({ + 'info': _("New item has been updated."), + }) + + +@require_POST +@login_required +@check_course_selection +def delete_additional_info_ajax(request): + + if any(k not in request.POST for k in ['additional_info_id', 'contract_id']): + return _error_response(_("Unauthorized access.")) + + if str(request.current_contract.id) != request.POST['contract_id']: + return _error_response(_("Current contract is changed. Please reload this page.")) + + validate_task_message = validate_task(request.current_contract) + if validate_task_message: + return _error_response(validate_task_message) + + try: + additional_info = AdditionalInfo.objects.get(id=request.POST['additional_info_id'], contract=request.current_contract) + AdditionalInfoSetting.objects.filter(contract=request.current_contract, display_name=additional_info.display_name).delete() + additional_info.delete() + except AdditionalInfo.DoesNotExist: + log.info('Already deleted additional-info id:{}.'.format(request.POST['additional_info_id'])) + return _error_response(_("Already deleted.")) + except: + log.exception('Failed to delete the display-name of an additional-info id:{}.'.format(request.POST['additional_info_id'])) + return _error_response(_("Failed to deleted item.")) + + return JsonResponse({ + 'info': _("New item has been deleted."), + }) + + +@transaction.non_atomic_requests +@require_POST +@login_required +@check_course_selection +def update_additional_info_ajax(request): + + if any(k not in request.POST for k in ['update_students_list', 'contract_id']): + return _error_response(_("Unauthorized access.")) + + if any(k not in request.POST for k in ['additional_info']): + return _error_response(_("No additional item registered.")) + + if str(request.current_contract.id) != request.POST['contract_id']: + return _error_response(_("Current contract is changed. Please reload this page.")) + + students = request.POST['update_students_list'].splitlines() + if not students: + return _error_response(_("Could not find student list.")) + + if len(students) > settings.BIZ_MAX_REGISTER_NUMBER: + return _error_response(_( + "It has exceeded the number({max_register_number}) of cases that can be a time of registration." + ).format(max_register_number=settings.BIZ_MAX_REGISTER_NUMBER)) + + if any([len(s) > settings.BIZ_MAX_CHAR_LENGTH_REGISTER_ADD_INFO_LINE for s in students]): + return _error_response(_( + "The number of lines per line has exceeded the {biz_max_char_length_register_line} characters." + ).format(biz_max_char_length_register_line=settings.BIZ_MAX_CHAR_LENGTH_REGISTER_ADD_INFO_LINE)) + + additional_info_list = AdditionalInfo.validate_and_find_by_ids( + request.current_contract, + request.POST.getlist('additional_info') if 'additional_info' in request.POST else [] + ) + if additional_info_list is None: + return _error_response(_("New item registered. Please reload browser.")) + + history = ContractTaskHistory.create(request.current_contract, request.user) + AdditionalInfoUpdateTaskTarget.bulk_create(history, students) + + return _submit_task(request, ADDITIONALINFO_UPDATE, additional_info_update, history, additional_info_list) diff --git a/biz/djangoapps/ga_course_operation/tests/test_views.py b/biz/djangoapps/ga_course_operation/tests/test_views.py index cac2435a1f56..16fc57c3a1e3 100644 --- a/biz/djangoapps/ga_course_operation/tests/test_views.py +++ b/biz/djangoapps/ga_course_operation/tests/test_views.py @@ -15,12 +15,18 @@ def test_survey(self): class CourseOperationSurveyDownloadTest(InstructorAPISurveyDownloadTestMixin, BizContractTestBase): """ - Test instructor survey for biz endpoint. + Test instructor survey utf16 for biz endpoint. """ def get_url(self): return reverse('biz:course_operation:survey_download') + def validate_bom(self, content): + return self.validate_bom_utf16(content) + + def get_survey_csv_rows_unicode(self, content): + return self.get_survey_csv_rows_unicode_from_utf16(content) + def test_get_survey_not_allowed_method(self): with self.skip_check_course_selection(current_contract=self.contract, current_course=self.course): response = self.client.get(self.get_url()) @@ -39,9 +45,24 @@ def test_get_survey_when_data_is_broken(self): super(CourseOperationSurveyDownloadTest, self).test_get_survey_when_data_is_broken() +class CourseOperationSurveyDownloadUTF8Test(CourseOperationSurveyDownloadTest, BizContractTestBase): + """ + Test instructor survey utf8 for biz endpoint. + """ + + def get_url(self): + return reverse('biz:course_operation:survey_download_utf8') + + def validate_bom(self, content): + return self.validate_bom_utf8(content) + + def get_survey_csv_rows_unicode(self, content): + return self.get_survey_csv_rows_unicode_from_utf8(content) + + class LoginCodeEnabledCourseOperationSurveyDownloadTest(LoginCodeEnabledInstructorAPISurveyDownloadTestMixin, BizContractTestBase): """ - Test instructor survey for biz endpoint. + Test instructor survey utf16 for biz endpoint. """ def get_url(self): @@ -50,6 +71,12 @@ def get_url(self): def enable_login_code_check(self): return True + def validate_bom(self, content): + return self.validate_bom_utf16(content) + + def get_survey_csv_rows_unicode(self, content): + return self.get_survey_csv_rows_unicode_from_utf16(content) + def test_get_survey_not_allowed_method(self): with self.skip_check_course_selection(current_contract=self.contract, current_course=self.course): response = self.client.get(self.get_url()) @@ -65,4 +92,23 @@ def test_get_survey_when_data_is_empty(self): def test_get_survey_when_data_is_broken(self): with self.skip_check_course_selection(current_contract=self.contract, current_course=self.course): - super(LoginCodeEnabledCourseOperationSurveyDownloadTest, self).test_get_survey_when_data_is_broken() \ No newline at end of file + super(LoginCodeEnabledCourseOperationSurveyDownloadTest, self).test_get_survey_when_data_is_broken() + + +class LoginCodeEnabledCourseOperationSurveyDownloadUTF8Test(LoginCodeEnabledCourseOperationSurveyDownloadTest, BizContractTestBase): + """ + Test instructor survey utf8 for biz endpoint. + """ + + def get_url(self): + return reverse('biz:course_operation:survey_download_utf8') + + def enable_login_code_check(self): + return True + + def validate_bom(self, content): + return self.validate_bom_utf8(content) + + def get_survey_csv_rows_unicode(self, content): + return self.get_survey_csv_rows_unicode_from_utf8(content) + diff --git a/biz/djangoapps/ga_course_operation/urls.py b/biz/djangoapps/ga_course_operation/urls.py index f923081551cc..8cdf037617dc 100644 --- a/biz/djangoapps/ga_course_operation/urls.py +++ b/biz/djangoapps/ga_course_operation/urls.py @@ -9,4 +9,5 @@ 'biz.djangoapps.ga_course_operation.views', url(r'^survey$', 'survey', name='survey'), url(r'^survey/download$', 'survey_download', name='survey_download'), + url(r'^survey/download_utf8$', 'survey_download_utf8', name='survey_download_utf8'), ) diff --git a/biz/djangoapps/ga_course_operation/views.py b/biz/djangoapps/ga_course_operation/views.py index b35ea49bf0ff..43fdf15a6b37 100644 --- a/biz/djangoapps/ga_course_operation/views.py +++ b/biz/djangoapps/ga_course_operation/views.py @@ -28,4 +28,12 @@ def survey(request): @check_course_selection @require_survey def survey_download(request): - return create_survey_response(request, unicode(request.current_course.id)) + return create_survey_response(request, unicode(request.current_course.id), 'utf-16') + + +@require_POST +@login_required +@check_course_selection +@require_survey +def survey_download_utf8(request): + return create_survey_response(request, unicode(request.current_course.id), 'utf-8') diff --git a/biz/djangoapps/ga_invitation/views.py b/biz/djangoapps/ga_invitation/views.py index b96f9685b060..ab207ea6d3ef 100644 --- a/biz/djangoapps/ga_invitation/views.py +++ b/biz/djangoapps/ga_invitation/views.py @@ -142,7 +142,7 @@ def confirm(request, invitation_code): additionals.append({ 'name': ADDITIONAL_NAME.format(additional_id=additional.id), 'display_name': additional.display_name, - 'value': AdditionalInfoSetting.get_value(request.user, contract, additional) if contract_register.is_registered() else '' + 'value': AdditionalInfoSetting.get_value(request.user, contract, additional), }) context = { diff --git a/biz/djangoapps/util/task_utils.py b/biz/djangoapps/util/task_utils.py index efd3cb8de35d..b9cfe9d4e375 100644 --- a/biz/djangoapps/util/task_utils.py +++ b/biz/djangoapps/util/task_utils.py @@ -1,10 +1,41 @@ +from celery.states import READY_STATES +import hashlib +import logging from django.conf import settings +from django.utils.translation import ugettext as _ +from biz.djangoapps.ga_contract_operation.tasks import TASKS from openedx.core.djangoapps.ga_task import api as task_api +from openedx.core.djangoapps.ga_task.models import Task + +log = logging.getLogger(__name__) def submit_task(request, task_type, task_class, task_input, task_key, queue=None): if queue is None: queue = settings.BIZ_CELERY_DEFAULT_QUEUE return task_api.submit_task(request, task_type, task_class, task_input, task_key, queue) + + +def validate_task(contract): + """ + Check specific task is already running. + """ + running_tasks = Task.objects.filter(task_key=get_task_key(contract)).exclude(task_state__in=READY_STATES) + if not running_tasks: + return None + + if len(running_tasks) == 1: + log.warning("Running of {task_type} is running id({task_id}).".format(task_type=running_tasks[0].task_type, + task_id=running_tasks[0].task_id)) + else: + log.warning("Running task is too many({task_ids}).".format(task_ids=[r.task_id for r in running_tasks])) + + return _( + "{task_type_name} is being executed. Please check task history, leave time and try again." + ).format(task_type_name=TASKS[running_tasks[0].task_type]) + + +def get_task_key(contract): + return hashlib.md5(str(contract.id)).hexdigest() diff --git a/biz/djangoapps/util/tests/test_task_utils.py b/biz/djangoapps/util/tests/test_task_utils.py new file mode 100644 index 000000000000..ce91779a2e86 --- /dev/null +++ b/biz/djangoapps/util/tests/test_task_utils.py @@ -0,0 +1,98 @@ +""" +Tests for task utilities +""" +from celery.states import FAILURE, REVOKED, SUCCESS, PENDING, STARTED +import ddt +import hashlib +from mock import patch + +from django.utils.crypto import get_random_string + +from biz.djangoapps.ga_contract.tests.factories import ContractFactory +from biz.djangoapps.ga_contract_operation.tasks import ( + TASKS, PERSONALINFO_MASK, STUDENT_REGISTER, STUDENT_UNREGISTER, ADDITIONALINFO_UPDATE +) +from biz.djangoapps.ga_organization.tests.factories import OrganizationFactory +from biz.djangoapps.util import task_utils +from biz.djangoapps.util.tests.testcase import BizTestBase + +from openedx.core.djangoapps.ga_task.tests.factories import TaskFactory +from student.tests.factories import UserFactory + + +@ddt.ddt +class TaskUtilsTest(BizTestBase): + """ + Test for task utilities + """ + + def setUp(self): + super(TaskUtilsTest, self).setUp() + + self.user = UserFactory.create() + org = OrganizationFactory.create( + org_name='docomo gacco', + org_code='gacco', + creator_org_id=1, + created_by=self.user, + ) + self.contract = ContractFactory.create( + contract_name=get_random_string(8), + contract_type='PF', + invitation_code=get_random_string(8), + contractor_organization=org, + owner_organization=org, + created_by=UserFactory.create(), + ) + patcher_log = patch('biz.djangoapps.util.task_utils.log') + self.mock_log = patcher_log.start() + self.addCleanup(patcher_log.stop) + + def test_get_task_key(self): + self.assertEqual('c4ca4238a0b923820dcc509a6f75849b', task_utils.get_task_key(self.contract)) + + def _create_ready_task(self, task_type): + TaskFactory.create(task_type=task_type, + task_key=hashlib.md5(str(self.contract.id)).hexdigest(), + task_state=SUCCESS) + TaskFactory.create(task_type=task_type, + task_key=hashlib.md5(str(self.contract.id)).hexdigest(), + task_state=REVOKED) + TaskFactory.create(task_type=task_type, + task_key=hashlib.md5(str(self.contract.id)).hexdigest(), + task_state=FAILURE) + + @ddt.data(PERSONALINFO_MASK, STUDENT_REGISTER, STUDENT_UNREGISTER, ADDITIONALINFO_UPDATE) + def test_success_validate_task(self, task_type): + self._create_ready_task(task_type) + self.assertIsNone(task_utils.validate_task(self.contract)) + + @ddt.data(PERSONALINFO_MASK, STUDENT_REGISTER, STUDENT_UNREGISTER, ADDITIONALINFO_UPDATE) + def test_already_running_task(self, task_type): + self._create_ready_task(task_type) + task = TaskFactory.create(task_type=task_type, + task_key=hashlib.md5(str(self.contract.id)).hexdigest(), + task_state=STARTED) + response = task_utils.validate_task(self.contract) + self.mock_log.warning.assert_called_with( + "Running of {task_type} is running id({task_id}).".format(task_type=task_type, task_id=task.task_id)) + self.assertEquals( + "{} is being executed. Please check task history, leave time and try again.".format(TASKS[task_type]), + response) + + @ddt.data(PERSONALINFO_MASK, STUDENT_REGISTER, STUDENT_UNREGISTER, ADDITIONALINFO_UPDATE) + def test_already_running_multi_task(self, task_type): + self._create_ready_task(task_type) + task1 = TaskFactory.create(task_type=task_type, + task_key=hashlib.md5(str(self.contract.id)).hexdigest(), + task_state=PENDING) + task2 = TaskFactory.create(task_type=task_type, + task_key=hashlib.md5(str(self.contract.id)).hexdigest(), + task_state=STARTED) + task_ids = "[u'{}', u'{}']".format(task1.task_id, task2.task_id) + response = task_utils.validate_task(self.contract) + self.mock_log.warning.assert_called_with( + "Running task is too many({task_ids}).".format(task_ids=task_ids)) + self.assertEquals( + "{} is being executed. Please check task history, leave time and try again.".format(TASKS[task_type]), + response) diff --git a/biz/djangoapps/util/tests/testcase.py b/biz/djangoapps/util/tests/testcase.py index 0140146e3f33..0139939ab261 100644 --- a/biz/djangoapps/util/tests/testcase.py +++ b/biz/djangoapps/util/tests/testcase.py @@ -6,6 +6,7 @@ from django.db.models.query import QuerySet from django.test import TestCase from django.test.utils import override_settings +from django.utils.crypto import get_random_string from mock import patch from biz.djangoapps.ga_contract.tests.factories import ( @@ -84,7 +85,7 @@ def _create_contract(self, contract_name='test contract', contract_type='PF', re course_id = c.id if isinstance(c, CourseDescriptor) else c ContractDetailFactory.create(contract=contract, course_id=course_id) for d in additional_display_names: - AdditionalInfoFactory.create(contract=contract, display_name=d) + self._create_additional_info(contract=contract, display_name=d) if url_code: ContractAuthFactory.create(contract=contract, url_code=url_code, send_mail=send_mail) if customize_mail or send_submission_reminder: @@ -92,6 +93,19 @@ def _create_contract(self, contract_name='test contract', contract_type='PF', re send_submission_reminder=send_submission_reminder) return contract + def _create_additional_info(self, contract=None, display_name=None): + return AdditionalInfoFactory.create( + contract=contract or self._create_contract(), + display_name=display_name or get_random_string(8), + ) + + def _create_user_and_contract_register(self, contract, email=None): + if email: + user = UserFactory.create(email=email) + else: + user = UserFactory.create() + return self._input_contract(user=user, contract=contract) + def _input_contract(self, contract, user): register = ContractRegister.get_by_user_contract(user, contract) if register: diff --git a/biz/envs/aws.py b/biz/envs/aws.py index e191d0f6820b..bb520b379624 100644 --- a/biz/envs/aws.py +++ b/biz/envs/aws.py @@ -14,6 +14,8 @@ BIZ_MAX_CHAR_LENGTH_REGISTER_LINE, BIZ_MAX_BULK_STUDENTS_NUMBER, BIZ_MAX_CHAR_LENGTH_BULK_STUDENTS_LINE, + BIZ_MAX_REGISTER_ADDITIONAL_INFO, + BIZ_MAX_CHAR_LENGTH_REGISTER_ADD_INFO_LINE, ) @@ -63,3 +65,9 @@ """ BIZ_MAX_BULK_STUDENTS_NUMBER = ENV_TOKENS.get('BIZ_MAX_BULK_STUDENTS_NUMBER', BIZ_MAX_BULK_STUDENTS_NUMBER) BIZ_MAX_CHAR_LENGTH_BULK_STUDENTS_LINE = ENV_TOKENS.get('BIZ_MAX_CHAR_LENGTH_BULK_STUDENTS_LINE', BIZ_MAX_CHAR_LENGTH_BULK_STUDENTS_LINE) + +""" +Additional info +""" +BIZ_MAX_REGISTER_ADDITIONAL_INFO = ENV_TOKENS.get('BIZ_MAX_REGISTER_ADDITIONAL_INFO', BIZ_MAX_REGISTER_ADDITIONAL_INFO) +BIZ_MAX_CHAR_LENGTH_REGISTER_ADD_INFO_LINE = ENV_TOKENS.get('BIZ_MAX_CHAR_LENGTH_REGISTER_ADD_INFO_LINE', BIZ_MAX_CHAR_LENGTH_REGISTER_ADD_INFO_LINE) diff --git a/biz/envs/common.py b/biz/envs/common.py index 52183719a57c..63e14b2f75a6 100644 --- a/biz/envs/common.py +++ b/biz/envs/common.py @@ -70,11 +70,17 @@ """ Register student """ -BIZ_MAX_REGISTER_NUMBER = 1000 +BIZ_MAX_REGISTER_NUMBER = 10000 BIZ_MAX_CHAR_LENGTH_REGISTER_LINE = 300 """ Bulk student Management """ -BIZ_MAX_BULK_STUDENTS_NUMBER = 1000 +BIZ_MAX_BULK_STUDENTS_NUMBER = 10000 BIZ_MAX_CHAR_LENGTH_BULK_STUDENTS_LINE = 30 + +""" +Additional info +""" +BIZ_MAX_REGISTER_ADDITIONAL_INFO = 10 +BIZ_MAX_CHAR_LENGTH_REGISTER_ADD_INFO_LINE = 3000 diff --git a/biz/static/css/main_biz.css b/biz/static/css/main_biz.css index 4521b4492454..3e71590ed2c7 100644 --- a/biz/static/css/main_biz.css +++ b/biz/static/css/main_biz.css @@ -657,6 +657,13 @@ w2ui-popup .w2ui-msg-title { padding: 0.3rem 1rem; } +#form p.operation, #form span.operation { font-size: 1rem; +} + +#form label.encoding-utf8 { + font-size: 0.8rem; + font-weight: normal; + font-style: normal; } \ No newline at end of file diff --git a/biz/templates/ga_contract/detail.html b/biz/templates/ga_contract/detail.html index fd2a71b1cfe0..225e488f3904 100644 --- a/biz/templates/ga_contract/detail.html +++ b/biz/templates/ga_contract/detail.html @@ -35,21 +35,6 @@ resize_form($("#detail-container").height() - height_before); } - function show_additional_info(items) { - var height_before = $("#additional-container").height(); - $.each(items, function (i, item) { - var clone = $("#additional-info-template").clone(); - clone.find("a").on('click', delete_additional_info); - clone.find("input[name=additional_info_display_name]").val(item.display_name); - clone.find("input[name=additional_info_id]").val(item.id); - clone.find("input[name=additional_info_delete]").val(item.delete_flg); - clone.find(".err-msg").text(item.error); - $('#additional-container .w2ui-group').append(clone); - clone.show(); - }); - resize_form($("#additional-container").height() - height_before); - } - function delete_detail() { var height_before = $("#detail-container").height(); var detail = $(this).parent().parent(); @@ -144,7 +129,6 @@ $('#id_end_date').w2field('date', {format: 'yyyy/mm/dd'}); show_detail(${detail_list}); - show_additional_info(${additional_info_list}); }); @@ -219,7 +203,7 @@ .add-plus { text-align: right; - cursor: pointer; + font-size: 1rem; } @@ -267,13 +251,17 @@

${_('Contract Detail Info')}

% if form.errors.has_key('contract_detail'): ${form.errors['contract_detail']} % endif + % if course_list: + % else: +
${_('Not found course for Contract Detail Info')}
+ % endif
-
-
-
-
-

${_('Additional Info')}

- % if form.errors.has_key('additional_info'): - ${form.errors['additional_info']} - % endif - -
- -
-
diff --git a/biz/templates/ga_contract/index.html b/biz/templates/ga_contract/index.html index 5fabbd2fe663..b50cd17fa1a3 100644 --- a/biz/templates/ga_contract/index.html +++ b/biz/templates/ga_contract/index.html @@ -54,7 +54,6 @@ {field: 'start_date', caption: '${_("Contract Start Date")}', size: '20%', sortable: true, hidden: false, render: 'date:yyyy/mm/dd'}, {field: 'end_date', caption: '${_("Contract End Date")}', size: '20%', sortable: true, hidden: false, render: 'date:yyyy/mm/dd'}, {field: 'course_count', caption: '${_("Total Course Count")}', size: '20%', sortable: true, hidden: true, attr: 'align=right'}, - {field: 'additional_info_count', caption: '${_("Additional Info Count")}', size: '20%', sortable: true, hidden: true, attr: 'align=right'}, {field: 'created_by', caption: '${_("Created By Name")}', size: '20%', sortable: true, hidden: true}, {field: 'created', caption: '${_("Created Date")}', size: '20%', sortable: true, hidden: true, render: 'date:yyyy/mm/dd'} ], diff --git a/biz/templates/ga_contract_operation/_task_history.html b/biz/templates/ga_contract_operation/_task_history.html index b9ba27646166..f902a27e2f4e 100644 --- a/biz/templates/ga_contract_operation/_task_history.html +++ b/biz/templates/ga_contract_operation/_task_history.html @@ -23,7 +23,7 @@

${_("Task History")}

-

${_("To see the status of the tasks that have been run in the background, please click this button.")}

+

${_("To see the status of tasks running in the background, click the button below.")}

diff --git a/biz/templates/ga_contract_operation/additional_info.underscore b/biz/templates/ga_contract_operation/additional_info.underscore new file mode 100644 index 000000000000..83ece17d0c1e --- /dev/null +++ b/biz/templates/ga_contract_operation/additional_info.underscore @@ -0,0 +1,4 @@ +
+ + +
diff --git a/biz/templates/ga_contract_operation/bulk_students.html b/biz/templates/ga_contract_operation/bulk_students.html index ac57c4c26633..85ed6b8eef49 100644 --- a/biz/templates/ga_contract_operation/bulk_students.html +++ b/biz/templates/ga_contract_operation/bulk_students.html @@ -64,7 +64,7 @@
- +
<%include file="_task_history.html" /> diff --git a/biz/templates/ga_contract_operation/register_students.html b/biz/templates/ga_contract_operation/register_students.html index 023f187d2be2..48b7d7524f9b 100644 --- a/biz/templates/ga_contract_operation/register_students.html +++ b/biz/templates/ga_contract_operation/register_students.html @@ -6,12 +6,23 @@ from biz.djangoapps.ga_invitation.models import REGISTER_INVITATION_CODE %> -<%block name="pagetitle">${_("Register Students")} +<%namespace name='static' file='/static_content.html'/> +<%block name="pagetitle">${_("Enrolling New Learners")} <%block name="js_extra"> + + + <%block name="custom_content"> -
+
- %if hasattr(request.current_contract, 'contractauth'): - ${_("To register a list of users in this course, contains the following columns in this exact order: e-mail, username, name, login code, and password. Please include one user per row and do not include any headers, footers, or blank lines.")} + %if request.current_contract.has_auth: +

${_(u"Enroll each Learner by a single command-line using comma(,), in the order of [E-mail address], [User name], [Name], [Login code], [Password].
These Items cannot be changed.")}

+

${_(u"ex. username1@domain.com,gaccotarou,gaccoutarou,gacco,Gacco12345")}

%else: - ${_("To register a list of users in this course, contains the following columns in this exact order: e-mail, username, and name. Please include one user per row and do not include any headers, footers, or blank lines.")} +

${_(u"Enroll each Learner by a single command-line using comma(,), in the order of [E-mail address], [User name], [Name]. These Items cannot be changed.")}

+

${_(u"ex. username1@domain.com,gaccotarou,gaccoutarou")}

%endif
+
- ${_("Once can be registered number is {max_register_number}.").format(max_register_number=max_register_number)} +

${_(u"< Note >")}

+

${_(u"Please be aware that once you press the \"Register Signup\" button, a welcome e-mail of the course will be sent automatically. This welcome e-mail cannot be reissued.")}

+
+

${_(u"· Should not include headers, footers or blank lines.")}

+

${_(u"· Up to {max_students_number} Learners can be enrolled at a time.").format(max_students_number=max_register_number)}

+
+
+
+
+

${_(u"< Entry item >")}

+

${_(u"· E-mail address: Half-width alphanumeric characters, hyphen (-), underscore (_), period (.).")}

+

${_(u"· User name: Two or more half-width alphabets, up to 30 characters. (It will be the User name displayed during the discussion. Once registered, it can not be changed.)")}

+

${_(u"· Name: \"Name\" will be included in the certificate for the course if the Learner successfully gets one.")}

+ %if request.current_contract.has_auth: +

${_(u"· Login code: 2 or more half-width alphabets and 30 or less.")}

+

${_(u"· Password: 8 characters or more. (Must include at least one upper case letter, one lower case letter, and one number, all in one byte characters.)")}

+ % endif
@@ -85,9 +283,62 @@
+
+ +
+
+ +
+
+

${_(u"If the course instructor wishes to Enroll more information to the existing list, add/delete item as follows:")}

+
+
+
+

${_(u"Add new item here:")}


+
+ + +
+
+
+
+ ${_(u"Delete item here:
*If deleted item, each user input the additional item will be delete.")}

+ % for a in additional_info_list: +
+ + +
+ % endfor +
-
- + +
+
+

${_(u"To enroll additional item of the Learners separately from the first registration, enroll each Learner by a single command-line using comma(,), in the order.")}

+

${_(u"ex. username1@domain.com,system department,manager")}

+
+
+
+

${_(u"< Note >")}

+

${_(u"· Should not include headers, footers or blank lines.")}

+

${_(u"· Additional item use comma(,) after the E-mail address, then enter the additional item.")}

+

${_(u"· Up to {max_students_number} Learners can be enrolled at a time.").format(max_students_number=max_register_number)}

+
+
+
+
+

${_(u"< Entry item >")}

+

${_(u"· E-mail address: The same e-mail address as the Learners already registered.")}

+ % for a in additional_info_list: +

${_(u"· {additional_item_name}: {max_length} characters or less.").format(additional_item_name=a.display_name, max_length=max_length_additional_info_display_name)}

+ % endfor +
+
+ +
+
+ +
<%include file="_task_history.html" /> diff --git a/biz/templates/ga_contract_operation/students.html b/biz/templates/ga_contract_operation/students.html index eb7c8463f692..459b81924d97 100644 --- a/biz/templates/ga_contract_operation/students.html +++ b/biz/templates/ga_contract_operation/students.html @@ -1,108 +1,108 @@ -<%inherit file="../main_biz.html" /> -<%! -from django.utils.translation import ugettext as _ -from django.core.urlresolvers import reverse -%> - -<%block name="pagetitle">${_("Users List")} - -<%block name="js_extra"> - - - - -<%block name="custom_content"> -
- - - - -<%include file="_task_history.html" /> - +<%inherit file="../main_biz.html" /> +<%! +from django.utils.translation import ugettext as _ +from django.core.urlresolvers import reverse +%> + +<%block name="pagetitle">${_("Users List")} + +<%block name="js_extra"> + + + + +<%block name="custom_content"> +
+ + + + +<%include file="_task_history.html" /> + diff --git a/biz/templates/ga_course_operation/survey.html b/biz/templates/ga_course_operation/survey.html index d8f019a43bc1..42cde10d71d6 100644 --- a/biz/templates/ga_course_operation/survey.html +++ b/biz/templates/ga_course_operation/survey.html @@ -11,9 +11,28 @@ $(function () { $('#form').w2form({name: 'form'}); $('.w2ui-buttons').on('click', '#download-btn', function () { - downloadFileUsingPost($(this).data('endpoint')); + if ($('#encoding-utf8').prop('checked')) { + saveInstructorSurveyEncodingUTF8('true'); + downloadFileUsingPost($(this).data('endpointUtf8')); + } else { + saveInstructorSurveyEncodingUTF8('false'); + downloadFileUsingPost($(this).data('endpoint')); + } }); + $('#encoding-utf8').prop('checked', getInstructorSurveyEncodingUTF8()); }); + + function saveInstructorSurveyEncodingUTF8(value) { + if (window.localStorage) { + window.localStorage.setItem('biz.survey.encodingUTF8', value); + } + } + + function getInstructorSurveyEncodingUTF8() { + if (window.localStorage) { + return window.localStorage.getItem('biz.survey.encodingUTF8') === 'true'; + } + } @@ -21,11 +40,12 @@
- ${_("The survey result can be downloaded as a CSV file.
When you try to open a CSV file as it is in Excel, because that would garbled in the case that contains the Japanese, once you save the file, please re-open it in a text editor that corresponds to the 'UTF-8'.")}
+ ${_("You can download the survey results.")}
- +
+
diff --git a/biz/templates/ga_invitation/confirm.html b/biz/templates/ga_invitation/confirm.html index ddef788567e1..0d4325afcb3a 100644 --- a/biz/templates/ga_invitation/confirm.html +++ b/biz/templates/ga_invitation/confirm.html @@ -264,7 +264,7 @@

${_("{contract_name} contains the course of the following. Pleas % endfor
% if additionals: -

${_("To take {contract_name} are required to enter the additional information below.").format(contract_name=contract.contract_name) | h}

+

${_("To take {contract_name} are required to enter the additional item below.").format(contract_name=contract.contract_name) | h}

% for additional in additionals:
diff --git a/biz/templates/main_biz.html b/biz/templates/main_biz.html index d452ac6930d3..0289aade51bf 100644 --- a/biz/templates/main_biz.html +++ b/biz/templates/main_biz.html @@ -45,7 +45,8 @@ } .w2ui-overlay { - min-width: 80%; + min-width: 40%; + max-width: 60%; margin-left: -15px; } @@ -193,6 +194,81 @@ border-bottom: 1px solid #fff; padding-top: 10px; } + + /* Biz icon */ + .biz-icon:before, .biz-icon:after { + font-family: FontAwesome; + font-style: normal; + margin: 0 5px 0 0; + color: #126f9a; + } + .biz-icon.contract:before { + content: ' \f0f6 '; + } + .biz-icon.course:before { + content: ' \f19d '; + margin: 0 2px 0 0; + } + .biz-manager { + margin: 0 0 5px 10px; + top: 2px; + } + + /* Biz button */ + .biz-wrap .biz-btn { + background-color: #777; + background-image: none; + border-bottom: 2px solid #333; + color: #fff; + font-family: FontAwesome; + font-weight: initial; + min-height: 2rem; + min-width: 6rem; + padding: 0.1rem 1rem; + -moz-transition: .3s; + -o-transition: .3s; + -webkit-transition: .3s; + transition: .3s; + } + .biz-wrap .biz-btn:disabled { + background-color: #777777; + border-color: #333333; + pointer-events: auto; + } + .biz-btn.register-btn { + background-color: #43ac6a; + border-color: #2f8a7d; + } + .biz-btn.register-btn:hover:not(:disabled) { + background-color: green; + } + .biz-btn.remove-btn { + background-color: #cc3333; + border-color: #8a2f2f; + } + .biz-btn.remove-btn:hover:not(:disabled) { + background-color: #c30707; + } + .biz-btn.small-btn { + min-width: 2rem; + } + .w2ui-tabs~.w2ui-page .w2ui-buttons { + position: inherit; + padding: 0 !important; + } + .w2ui-tabs .w2ui-tab { + background-color: #777777; + color: #DDDDDD; + font-size: 1rem; + margin: 1px 1px -2px 0; + cursor: pointer; + } + /* Etc. */ + .operation { + font-size: 1rem; + line-height: 1.3rem; + } + @@ -246,19 +322,26 @@

${_("Organization not specified")}

% elif current_manager.is_director() or current_manager.is_manager(): - % if not current_course: + % if not current_contract:
-

${_("Course not specified")}

+

${_("Contract not specified")}

% else: -
- -
${current_contract.contract_name | h}
-
-
- -
${current_course.course_canonical_name or current_course.display_name | h}
-
+
+ +
${current_contract.contract_name | h}
+
+ % if not current_course: +
+ +
--
+
+ % else: +
+ +
${current_course.course_canonical_name or current_course.display_name | h}
+
+ % endif % endif % endif

diff --git a/common/djangoapps/student/tests/test_bulk_email_settings.py b/common/djangoapps/student/tests/test_bulk_email_settings.py index 4db331183e34..43de01dbd617 100644 --- a/common/djangoapps/student/tests/test_bulk_email_settings.py +++ b/common/djangoapps/student/tests/test_bulk_email_settings.py @@ -41,12 +41,13 @@ def setUp(self): # URL for email settings modal self.email_modal_link = ( 'Email Settings' ).format( org=self.course.org, num=self.course.number, name=self.course.display_name.replace(' ', '_'), + display_name=self.course.display_name, ) @patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False}) diff --git a/common/djangoapps/student/tests/test_views.py b/common/djangoapps/student/tests/test_views.py index b7f6816aa618..acba126daaf8 100644 --- a/common/djangoapps/student/tests/test_views.py +++ b/common/djangoapps/student/tests/test_views.py @@ -150,7 +150,8 @@ def test_cant_unenroll_status(self): self.assertEqual(response.status_code, 200) -YESTERDAY = datetime.now(pytz.UTC) - timedelta(days=1) +TODAY = datetime.now(pytz.UTC) +YESTERDAY = TODAY - timedelta(days=1) @ddt.ddt @@ -299,3 +300,45 @@ def test_user_is_not_active(self): self.client.login(username=self.USERNAME, password=self.PASSWORD) response = self.client.get(reverse('notice_unactivated')) self.assertRedirects(response, '/login?unactivated=true', status_code=302, target_status_code=200) + + +@ddt.ddt +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +class TestOrderGlobalCourse(ModuleStoreTestCase): + + def setUp(self): + super(TestOrderGlobalCourse, self).setUp() + + user = UserFactory.create() + + course = CourseFactory.create() + self.course_enrollment = CourseEnrollmentFactory.create(course_id=course.id, user=user) + self.course_id = str(course.id) + + global_course = CourseFactory.create() + CourseGlobalSettingFactory.create(course_id=global_course.id) + self.global_course_enrollment = CourseEnrollmentFactory.create(course_id=global_course.id, user=user) + self.global_course_id = str(global_course.id) + + self.client.login(username=user.username, password='test') + + @ddt.data( + (YESTERDAY, TODAY), + (TODAY, YESTERDAY), + ) + @ddt.unpack + def test_order(self, course_enrollment_created, global_course_enrollment_created): + # mod created of course_enrollment + self.course_enrollment.created = course_enrollment_created + self.course_enrollment.save() + + # mod created of global_course_enrollment + self.global_course_enrollment.created = global_course_enrollment_created + self.global_course_enrollment.save() + + response = self.client.get(reverse('dashboard')) + + self.assertIn(self.course_id, response.content) + self.assertIn(self.global_course_id, response.content) + + self.assertTrue(response.content.find(self.course_id) < response.content.find(self.global_course_id)) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 94843d264be3..a6c6735f4535 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -560,6 +560,11 @@ def dashboard(request): # sort the enrollment pairs by the enrollment date course_enrollments.sort(key=lambda x: x.created, reverse=True) + # mod order non-global-course before global-course + global_courses = CourseGlobalSetting.all_course_id() + course_enrollments = [c for c in course_enrollments if c.course_id not in global_courses] + [ + c for c in course_enrollments if c.course_id in global_courses] + # Retrieve the course modes for each course enrolled_course_ids = [enrollment.course_id for enrollment in course_enrollments] __, unexpired_course_modes = CourseMode.all_and_unexpired_modes_for_courses(enrolled_course_ids) @@ -644,7 +649,6 @@ def dashboard(request): } # only show email settings for Mongo course and when bulk email is turned on - global_courses = CourseGlobalSetting.all_course_id() show_email_settings_for = frozenset( enrollment.course_id for enrollment in course_enrollments if ( settings.FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and diff --git a/common/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py index a7453d387278..ddcf9531b853 100644 --- a/common/lib/xmodule/xmodule/conditional_module.py +++ b/common/lib/xmodule/xmodule/conditional_module.py @@ -168,7 +168,7 @@ def get_icon_class(self): new_class = 'other' # HACK: This shouldn't be hard-coded to two types # OBSOLETE: This obsoletes 'type' - class_priority = ['video', 'problem'] + class_priority = ['problem', 'video'] child_classes = [self.system.get_module(child_descriptor).get_icon_class() for child_descriptor in self.descriptor.get_children()] diff --git a/common/lib/xmodule/xmodule/css/sequence/display.scss b/common/lib/xmodule/xmodule/css/sequence/display.scss index 621a183b72e4..248c02a1541a 100644 --- a/common/lib/xmodule/xmodule/css/sequence/display.scss +++ b/common/lib/xmodule/xmodule/css/sequence/display.scss @@ -94,9 +94,13 @@ $sequence--border-color: #C8C8C8; @extend .block-link; } + li.over-unit-limit-display-text { + min-width: 20px; + } + li { display: table-cell; - min-width: 20px; + min-width: 60px; a { @extend %ui-fake-link; @@ -117,22 +121,35 @@ $sequence--border-color: #C8C8C8; color: rgb(90, 90, 90); -webkit-font-smoothing: antialiased; // Clear up the lines on the icons } + .copy { + line-height: 42px; // same as the icons. + font-size: 90%; // same as the icons. + color: rgb(90, 90, 90); + } &.inactive { - .icon { color: rgb(90, 90, 90); } + + .copy { + color: rgb(90, 90, 90); + } } &.active { @extend %ui-depth1; background-color: $white; + border-bottom: solid 3px #cc0033; .icon { color: rgb(10, 10, 10); } + .copy { + color: rgb(10, 10, 10); + } + &:hover, &:focus { background-color: $white; background-repeat: no-repeat; @@ -141,6 +158,10 @@ $sequence--border-color: #C8C8C8; .icon { color: rgb(10, 10, 10); } + + .copy { + color: rgb(10, 10, 10); + } } } @@ -148,13 +169,14 @@ $sequence--border-color: #C8C8C8; background-color: $white; background-repeat: no-repeat; background-position: center 14px; + border-bottom: solid 3px #cc0033; } //video &.seq_video { .icon:before { - content: "\f008"; // .fa-film + content: "\f16a"; // .fa-youtube-play } } @@ -250,7 +272,8 @@ $sequence--border-color: #C8C8C8; width: ($baseline*2); height: 46px; padding: 0; - background: #282c2e; + background: #cc0033; + box-shadow: none; &.button-previous { @include border-radius(35px, 0, 0, 35px); @@ -264,12 +287,18 @@ $sequence--border-color: #C8C8C8; &:hover, &:active { - background: #eef1f4; + background: #8a0023; + + &.disabled { + cursor: normal; + background: #eef1f4; + opacity: 1.0; + } } &.disabled { cursor: normal; - background: #50575b; + background: #eef1f4; opacity: 1.0; } } diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 0775b2eef026..43337c2f1924 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -25,7 +25,7 @@ # HACK: This shouldn't be hard-coded to two types # OBSOLETE: This obsoletes 'type' -class_priority = ['video', 'problem'] +class_priority = ['problem', 'video'] # Make '_' a no-op so we can scrape strings. Using lambda instead of # `django.utils.translation.ugettext_noop` because Django cannot be imported in this file diff --git a/common/lib/xmodule/xmodule/vertical_block.py b/common/lib/xmodule/xmodule/vertical_block.py index edeccd9b75fd..74e6273a73e0 100644 --- a/common/lib/xmodule/xmodule/vertical_block.py +++ b/common/lib/xmodule/xmodule/vertical_block.py @@ -17,7 +17,7 @@ # HACK: This shouldn't be hard-coded to two types # OBSOLETE: This obsoletes 'type' -CLASS_PRIORITY = ['video', 'problem'] +CLASS_PRIORITY = ['problem', 'video'] class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParserMixin, MakoTemplateBlockBase, XBlock): diff --git a/common/lib/xmodule/xmodule/wrapper_module.py b/common/lib/xmodule/xmodule/wrapper_module.py index 63e132716d8e..a63692700965 100644 --- a/common/lib/xmodule/xmodule/wrapper_module.py +++ b/common/lib/xmodule/xmodule/wrapper_module.py @@ -5,7 +5,7 @@ # HACK: This shouldn't be hard-coded to two types # OBSOLETE: This obsoletes 'type' -class_priority = ['video', 'problem'] +class_priority = ['problem', 'video'] class WrapperBlock(VerticalBlock): diff --git a/common/test/acceptance/pages/biz/ga_contract.py b/common/test/acceptance/pages/biz/ga_contract.py index 81561366ef8a..d8c5cd0be61d 100644 --- a/common/test/acceptance/pages/biz/ga_contract.py +++ b/common/test/acceptance/pages/biz/ga_contract.py @@ -97,21 +97,6 @@ def input_detail_info(self, value, index): self.q(css='select[name=detail_course]>option[value="{}"]'.format(value)).nth(index).first.click() return self - def add_additional_info(self, value, index): - """ - Click add additional info link and input value for it - """ - self.q(css='a#add-additional').click() - self.input_additional_info(value, index) - return self - - def input_additional_info(self, value, index): - """ - Input additional info - """ - self.q(css='input[name=additional_info_display_name]').nth(index).fill(value) - return self - def click_register_button(self): """ Click the register button @@ -140,8 +125,3 @@ def main_info_error(self): def detail_info_error(self): """Return a error displayed to the contract detail info. """ return self.q(css='div#detail-container .errorlist li').text[0] - - @property - def additional_info_error(self): - """Return a error displayed to the contract additional_info. """ - return self.q(css='div#additional-container .errorlist li').text[0] diff --git a/common/test/acceptance/pages/biz/ga_contract_operation.py b/common/test/acceptance/pages/biz/ga_contract_operation.py index a9dc0586b88f..45016cc3583a 100644 --- a/common/test/acceptance/pages/biz/ga_contract_operation.py +++ b/common/test/acceptance/pages/biz/ga_contract_operation.py @@ -95,7 +95,7 @@ def is_browser_on_page(self): """ Check if browser is showing the page. """ - return 'Register Students' in self.browser.title + return 'Enrolling New Learners' in self.browser.title def input_students(self, value): """ @@ -104,21 +104,21 @@ def input_students(self, value): Arguments: value : target students """ - self.q(css='textarea#list-students').fill(value) + self.q(css='.w2ui-page.page-0 textarea#list-students').fill(value) return self def click_register_status(self): """ Click the register status checkbox """ - self.q(css='#register-status').click() + self.q(css='.w2ui-page.page-0 #register-status').click() return self def click_register_button(self): """ Click the register button """ - self.q(css='#register-btn').click() + self.q(css='.w2ui-page.page-0 #register-btn').click() self.wait_for_ajax() return self @@ -127,6 +127,57 @@ def messages(self): """Return a list of errors displayed to the list view. """ return self.q(css="div.main ul.messages li").text + def click_tab_register_student(self): + self.click_tab('Enrolling Learners') + self.wait_for_ajax() + return self + + def click_tab_additional_info(self): + self.click_tab('Editing Additional Item') + self.wait_for_ajax() + return self + + def click_tab_update_additional_info(self): + self.click_tab('Enrolling Learners to Additional Item') + self.wait_for_ajax() + return self + + def click_additional_info_register_button(self): + self.q(css='.w2ui-page.page-1 .add-additional-info-btn').click() + self.wait_for_ajax() + return self + + def click_additional_info_delete_button(self, index=0): + self.q(css='.w2ui-page.page-1 .remove-btn').nth(index).click() + self.wait_for_ajax() + return self + + def click_additional_info_update_button(self): + self.q(css='.w2ui-page.page-2 .biz-btn.register-btn.update-btn').click() + self.wait_for_ajax() + return self + + def edit_additional_info_value(self, value, index=0): + self.q(css='.w2ui-page.page-1 [name="display_name"]').nth(index).fill(value) + # focus out. + self.q(css='.w2ui-page.page-1 [name="register_display_name"]').click() + self.wait_for_ajax() + return self + + def input_additional_info_register(self, value): + self.q(css='.w2ui-page.page-1 [name="register_display_name"]').fill(value) + return self + + def input_additional_info_list(self, value): + self.q(css='.w2ui-page.page-2 textarea#additional-info-list').fill(value) + return self + + def get_display_name_values(self, index=0): + return self.q(css='[name="display_name"]').nth(index).attrs('value') + + def get_additional_infos(self): + return self.q(css='.operation.setting-list').text + class BizMailPage(BizNavPage, W2uiMixin): """ diff --git a/common/test/acceptance/pages/biz/ga_survey.py b/common/test/acceptance/pages/biz/ga_survey.py index f6d037434cb7..b60f386a046c 100644 --- a/common/test/acceptance/pages/biz/ga_survey.py +++ b/common/test/acceptance/pages/biz/ga_survey.py @@ -23,3 +23,13 @@ def click_download_button(self): """ self.q(css='input#download-btn').first.click() return self.wait_for_page() + + def check_encoding_utf8(self, checked): + checkbox = self.q(css='input#encoding-utf8') + if checkbox.selected != checked: + checkbox.click() + self.wait_for(lambda: checked == checkbox.selected, 'Checkbox is not clicked') + return self + + def is_encoding_utf8_selected(self): + return self.q(css='input#encoding-utf8').selected diff --git a/common/test/acceptance/pages/lms/ga_instructor_dashboard.py b/common/test/acceptance/pages/lms/ga_instructor_dashboard.py index 59ab44b461b6..c19fff082ff6 100644 --- a/common/test/acceptance/pages/lms/ga_instructor_dashboard.py +++ b/common/test/acceptance/pages/lms/ga_instructor_dashboard.py @@ -48,6 +48,16 @@ def click_download_button(self): self.wait_for_ajax() return self + def check_encoding_utf8(self, checked): + checkbox = self.q(css='input#encoding-utf8') + if checkbox.selected != checked: + checkbox.click() + self.wait_for(lambda: checked == checkbox.selected, 'Checkbox is not clicked') + return self + + def is_encoding_utf8_selected(self): + return self.q(css='input#encoding-utf8').selected + class SendEmailPage(PageObject): """ diff --git a/common/test/acceptance/tests/biz/__init__.py b/common/test/acceptance/tests/biz/__init__.py index e8921b5f510f..9057e7a834f0 100644 --- a/common/test/acceptance/tests/biz/__init__.py +++ b/common/test/acceptance/tests/biz/__init__.py @@ -113,8 +113,7 @@ def assert_grid_row_equal(self, grid_rows_a, grid_rows_b): ) def create_contract(self, biz_contract_page, contract_type, start_date, end_date, contractor_organization='', contractor_organization_name=None, - detail_info=None, - additional_info=None, register_type='ERS'): + detail_info=None, register_type='ERS'): """ Register a contract. """ @@ -132,10 +131,6 @@ def create_contract(self, biz_contract_page, contract_type, start_date, end_date for i, course_id in enumerate(detail_info): biz_contract_detail_page.add_detail_info(course_id, i + 1) - if additional_info: - for i, additional_name in enumerate(additional_info): - biz_contract_detail_page.add_additional_info(additional_name, i + 1) - biz_contract_detail_page.click_register_button() BizContractPage(self.browser).wait_for_page() @@ -189,7 +184,7 @@ def grant(self, operator, organization_name, permission, grant_to_user_info): self.assertIn(grant_to_user_info['email'], biz_manager_page.emails) def register_contract(self, operator, contractor_organization_name, contract_type='PF', register_type='ERS', - start_date='2000/01/01', end_date='2100/12/31', detail_info=None, additional_info=None): + start_date='2000/01/01', end_date='2100/12/31', detail_info=None): self.switch_to_user(operator) return self.create_contract( DashboardPage(self.browser).visit().click_biz().click_contract(), @@ -198,7 +193,6 @@ def register_contract(self, operator, contractor_organization_name, contract_typ end_date, contractor_organization_name=contractor_organization_name, detail_info=detail_info, - additional_info=additional_info, register_type=register_type, ) diff --git a/common/test/acceptance/tests/biz/test_ga_contract.py b/common/test/acceptance/tests/biz/test_ga_contract.py index 0eafb45e9f69..18385b1d15c4 100644 --- a/common/test/acceptance/tests/biz/test_ga_contract.py +++ b/common/test/acceptance/tests/biz/test_ga_contract.py @@ -96,8 +96,6 @@ def test_register_all_as_owner(self): start_date=start_date, end_date=end_date, contractor_organization=self.CONTRACTOR_ORGANIZATION) \ .add_detail_info(self.course_id, 1) \ - .add_additional_info(u'部署', 1) \ - .add_additional_info(u'社員番号', 2) \ .click_register_button() contract_page = BizContractPage(self.browser).wait_for_page() bok_choy.browser.save_screenshot(self.browser, 'test_register_all_as_owner_1') @@ -181,8 +179,6 @@ def test_register_contract_and_additinaol_by_owner(self): invitation_code=invitation_code, start_date=start_date, end_date=end_date, contractor_organization=self.CONTRACTOR_ORGANIZATION) \ - .add_additional_info(u'部署', 1) \ - .add_additional_info(u'社員番号', 2) \ .click_register_button() contract_page = BizContractPage(self.browser).wait_for_page() bok_choy.browser.save_screenshot(self.browser, 'test_register_contract_and_additinaol_by_owner_1') @@ -214,7 +210,7 @@ def test_register_required_error(self): detail_page = BizContractDetailPage(self.browser).wait_for_page() # Register a contract - detail_page.add_additional_info('name', 1).click_register_button() + detail_page.click_register_button().wait_for_page() bok_choy.browser.save_screenshot(self.browser, 'test_register_required_error_1') # Verify that required errors are displayed @@ -267,7 +263,6 @@ def test_register_invitation_code_field_error(self): invitation_code=invitation_code, start_date=start_date, end_date=end_date, contractor_organization=self.CONTRACTOR_ORGANIZATION) \ - .add_additional_info(u'部署', 1) \ .click_register_button() contract_page = BizContractPage(self.browser).wait_for_page() contract_page.click_register_button() @@ -357,31 +352,6 @@ def test_register_detail_info_error(self): # Verify that error is displayed self.assertEqual(detail_page.detail_info_error, 'You can not enter duplicate values in Contract Detail Info.') - def test_register_additional_info_error(self): - """ - Tests contract additional validation. - """ - # Visit register view - BizNavPage(self.browser).visit().click_contract().click_register_button() - detail_page = BizContractDetailPage(self.browser).wait_for_page() - - # Case 51 - # input duplicate detail info - contract_name = self.CONTRACTOR_ORGANIZATION_NAME + self.unique_id[0:8] - invitation_code = self.unique_id[0:8] - detail_page.input(contract_name=contract_name, - contract_type=self.CONTRACT_TYPE, - register_type=self.REGISTER_TYPE, - invitation_code=invitation_code, - start_date='2016/01/01', - end_date='2100/01/01') \ - .add_additional_info(u'部署', 1) \ - .add_additional_info(u'部署', 2).click_register_button().wait_for_page() - bok_choy.browser.save_screenshot(self.browser, 'test_register_additional_info_error_1') - - # Verify that error is displayed - self.assertEqual(detail_page.additional_info_error, 'You can not enter duplicate values in Additional Info.') - def test_edit_success(self): """ Tests edit contract. @@ -405,7 +375,6 @@ def test_edit_success(self): invitation_code=invitation_code, start_date=start_date, end_date=end_date) \ .add_detail_info(self.course_id, 1) \ - .add_additional_info(u'部署', 1) \ .click_register_button() contract_page = BizContractPage(self.browser).wait_for_page() bok_choy.browser.save_screenshot(self.browser, 'test_edit_success_1') @@ -425,7 +394,6 @@ def test_edit_success(self): invitation_code=invitation_code, start_date=start_date, end_date=end_date) \ .input_detail_info(course2._course_key, 1) \ - .input_additional_info(u'社員番号', 1) \ .click_register_button() contract_page = contract_page.wait_for_page() bok_choy.browser.save_screenshot(self.browser, 'test_edit_success_3') @@ -468,7 +436,6 @@ def test_delete_success(self): invitation_code=invitation_code, start_date=start_date, end_date=end_date) \ .add_detail_info(self.course_id, 1) \ - .add_additional_info(u'部署', 1) \ .click_register_button() contract_page = BizContractPage(self.browser).wait_for_page() bok_choy.browser.save_screenshot(self.browser, 'test_delete_success_1') diff --git a/common/test/acceptance/tests/biz/test_ga_contract_operation.py b/common/test/acceptance/tests/biz/test_ga_contract_operation.py index f1c0f32742bf..4da1530cc4bd 100644 --- a/common/test/acceptance/tests/biz/test_ga_contract_operation.py +++ b/common/test/acceptance/tests/biz/test_ga_contract_operation.py @@ -91,7 +91,7 @@ def _assert_cannot_register_invitation_code(self): self.assertIn('Please ask your administrator to register the invitation code.', self.invitation_page.messages) -@attr('shard_ga_biz_3') +@attr('shard_ga_biz_2') @flaky class BizStudentRegisterTest(WebAppTest, GaccoBizTestMixin, BizStudentRegisterMixin): """ @@ -1172,8 +1172,7 @@ def setUp(self): self.new_course_key, _ = self.install_course(PLAT_COMPANY_CODE) self.new_contract = self.register_contract( PLATFORMER_USER_INFO, new_org_info['Organization Name'], - detail_info=[self.new_course_key], - additional_info=['info1', 'info2'] + detail_info=[self.new_course_key] ) self.users = [self.register_user() for _ in range(3)] @@ -1203,15 +1202,11 @@ def _register_student(self, user, do_register=False): def _assert_grid_row(self, grid_row, expected_user, expected_status=None): if expected_status is None: expected_status = expected_user['status'] - expected_info1 = 'info1-{}'.format(expected_user['username']) if expected_user['status'] == 'Register Invitation' else '' - expected_info2 = 'info2-{}'.format(expected_user['username']) if expected_user['status'] == 'Register Invitation' else '' self.assert_grid_row(grid_row, { 'Register Status': expected_status, 'Email Address': expected_user['email'], 'Username': expected_user['username'], 'Full Name': expected_user['username'], - 'info1': expected_info1, - 'info2': expected_info2, }) def _assert_task_history(self, grid_row, task_type, state, username, total=0, success=0, skipped=0, failed=0): @@ -1241,7 +1236,7 @@ def test_students_grid_column(self): # Check default columns grid_columns = self.students_page.student_grid.grid_columns self.assertItemsEqual(grid_columns, [ - '', 'Register Status', 'Full Name', 'Username', 'Email Address', 'info1', 'info2', + '', 'Register Status', 'Full Name', 'Username', 'Email Address', ]) # Check icon columns on/off @@ -1256,14 +1251,14 @@ def test_students_grid_column(self): grid_columns = self.students_page.student_grid.grid_columns self.assertItemsEqual(grid_columns, [ - '', 'Full Name', 'Username', 'Email Address', 'info1', 'info2', + '', 'Full Name', 'Username', 'Email Address', ]) self.students_page.student_grid.click_grid_icon_columns_checkbox('Register Status') grid_columns = self.students_page.student_grid.grid_columns self.assertItemsEqual(grid_columns, [ - '', 'Register Status', 'Full Name', 'Username', 'Email Address', 'info1', 'info2', + '', 'Register Status', 'Full Name', 'Username', 'Email Address', ]) # Check columns of grid for search @@ -1275,7 +1270,7 @@ def test_students_grid_column(self): self.students_page.student_grid.click_grid_icon_search() grid_icon_search = self.students_page.student_grid.grid_icon_search self.assertItemsEqual(grid_icon_search, [ - u'\u5168\u3066\u306e\u691c\u7d22\u9805\u76ee', 'Register Status', 'Full Name', 'Username', 'Email Address', 'info1', 'info2', + u'\u5168\u3066\u306e\u691c\u7d22\u9805\u76ee', 'Register Status', 'Full Name', 'Username', 'Email Address', ]) self.assertTrue(self.students_page.student_grid.is_checked_grid_icon_search(u'\u5168\u3066\u306e\u691c\u7d22\u9805\u76ee')) @@ -1442,7 +1437,7 @@ def test_success(self): self._assert_access_course_about(self.users[0], False) -@attr('shard_ga_biz_1') +@attr('shard_ga_biz_2') @flaky class BizStudentRegisterWithDisableRegisterStudentSelfTest(WebAppTest, GaccoBizTestMixin, BizStudentRegisterMixin): """ @@ -1774,7 +1769,7 @@ def test_register_students_nochecked_register_status(self): ], self.biz_login_page.error_messages) -@attr('shard_ga_biz_3') +@attr('shard_ga_biz_2') class BizMailTest(WebAppTest, GaccoBizTestMixin, BizStudentRegisterMixin): """ Tests that the mail functionality of biz works @@ -2019,7 +2014,7 @@ def test_normal_existing_user(self): self.email_client.clear_messages() # Register - register = nav.click_register_students() + register_page = nav.click_register_students() register_page.input_students(self._make_students([ self.new_user, ])).click_register_button().click_popup_yes() @@ -2096,7 +2091,7 @@ def test_auth_existing_user(self): self.email_client.clear_messages() # Register - register = nav.click_register_students() + register_page = nav.click_register_students() register_page.input_students(self._make_students_auth([ self.new_user, ])).click_register_button().click_popup_yes() diff --git a/common/test/acceptance/tests/biz/test_ga_contract_operation_additional_info.py b/common/test/acceptance/tests/biz/test_ga_contract_operation_additional_info.py new file mode 100644 index 000000000000..150c8a27093e --- /dev/null +++ b/common/test/acceptance/tests/biz/test_ga_contract_operation_additional_info.py @@ -0,0 +1,385 @@ +# -*- coding: utf-8 -*- +""" + End-to-end tests for contract operation additional info of biz feature +""" +from bok_choy.web_app_test import WebAppTest +from datetime import datetime +from nose.plugins.attrib import attr + +from django.utils.crypto import get_random_string + +from . import GaccoBizTestMixin, PLAT_COMPANY_CODE, PLATFORMER_USER_INFO +from .test_ga_contract_operation import BizStudentRegisterMixin +from ...pages.biz.ga_navigation import BizNavPage + + +@attr('shard_ga_biz_1') +class BizAdditionalInfoTest(WebAppTest, GaccoBizTestMixin): + + def setUp(self): + super(BizAdditionalInfoTest, self).setUp() + + # Register organization + self.org_info = self.register_organization(PLATFORMER_USER_INFO) + + # Register user as director + self.director = self.register_user() + self.grant(PLATFORMER_USER_INFO, self.org_info['Organization Name'], 'director', self.director) + + # Register contract + course_key, course_name = self.install_course(PLAT_COMPANY_CODE) + self.course_name = course_name + self.course_key = course_key + self.contract = self.register_contract(PLATFORMER_USER_INFO, + self.org_info['Organization Name'], + detail_info=[self.course_key]) + + def test_success(self): + self.switch_to_user(self.director) + nav = BizNavPage(self.browser).visit() + nav.change_manage_target(self.org_info['Organization Name'], self.contract['Contract Name'], self.course_key) + + # Go to additional info page + self.register_students_page = nav.click_register_students() + self.additional_info_page = self.register_students_page.click_tab_additional_info() + + # Check error message + self.additional_info_page.click_additional_info_register_button() + self.assertEqual( + [u"Please enter the name of item you wish to add."], + self.additional_info_page.messages, + ) + + # Register additional info + additional_input_value1 = get_random_string(10) + self.additional_info_page.input_additional_info_register(additional_input_value1) + self.additional_info_page.click_additional_info_register_button() + self.assertEqual( + [u"New item has been registered."], + self.additional_info_page.messages, + ) + + additional_input_value2 = get_random_string(10) + self.additional_info_page.input_additional_info_register(additional_input_value2) + self.additional_info_page.click_additional_info_register_button() + self.assertEqual( + [u"New item has been registered."], + self.additional_info_page.messages, + ) + + # Check additional info value + self.assertEqual( + [additional_input_value1], + self.additional_info_page.get_display_name_values(0), + ) + self.assertEqual( + [additional_input_value2], + self.additional_info_page.get_display_name_values(1), + ) + + # Edit additional info + # Check error message + self.additional_info_page.edit_additional_info_value('') + self.assertEqual( + [u"Please enter the name of item you wish to add."], + self.additional_info_page.messages, + ) + + additional_input_value3 = get_random_string(10) + self.additional_info_page.edit_additional_info_value(additional_input_value3) + self.assertEqual( + [u"New item has been updated."], + self.additional_info_page.messages, + ) + + # Check additional info value + self.assertEqual( + [additional_input_value3], + self.additional_info_page.get_display_name_values(0), + ) + self.assertEqual( + [additional_input_value2], + self.additional_info_page.get_display_name_values(1), + ) + + # Delete additional info + self.additional_info_page.click_additional_info_delete_button().click_popup_yes() + self.assertEqual( + [u"New item has been deleted."], + self.additional_info_page.messages, + ) + + # Check additional info value + self.assertEqual( + [additional_input_value2], + self.additional_info_page.get_display_name_values(0), + ) + + +@attr('shard_ga_biz_1') +class BizBulkAdditionalInfoRegisterTest(WebAppTest, GaccoBizTestMixin, BizStudentRegisterMixin): + + def setUp(self): + super(BizBulkAdditionalInfoRegisterTest, self).setUp() + + # Register organization + self.org_info = self.register_organization(PLATFORMER_USER_INFO) + + # Register user as director + self.director = self.register_user() + self.grant(PLATFORMER_USER_INFO, self.org_info['Organization Name'], 'director', self.director) + + # Register contract + course_key, course_name = self.install_course(PLAT_COMPANY_CODE) + self.course_name = course_name + self.course_key = course_key + self.contract = self.register_contract(PLATFORMER_USER_INFO, + self.org_info['Organization Name'], + detail_info=[self.course_key]) + + # Register users + self.new_users = [self.register_user() for _ in range(3)] + + @staticmethod + def _make_additional_info_csv(email, add_info_x, add_info_y): + return u'{},{},{}\r\n'.format(email, add_info_x, add_info_y) + + def _assert_additional_info_update_task_history(self, grid_row, total, success, skipped, failed, user_info): + self.assertEqual(u"Additional Item Update", grid_row['Task Type']) + self.assertEqual(u"Complete", grid_row['State']) + self.assertEqual(u"Total: {}, Success: {}, Skipped: {}, Failed: {}".format( + total, success, skipped, failed), grid_row['Execution Result']) + self.assertEqual(user_info['username'], grid_row['Execution Username']) + self.assertIsNotNone(datetime.strptime(grid_row['Execution Datetime'], '%Y/%m/%d %H:%M:%S')) + + def test_success(self): + self.switch_to_user(self.director) + nav = BizNavPage(self.browser).visit() + nav.change_manage_target(self.org_info['Organization Name'], self.contract['Contract Name'], self.course_key) + + # Register students + register_students_page = nav.click_register_students() + input_lines = self._make_students([self.new_users[0], self.new_users[1], self.new_users[2]]) + register_students_page.input_students(input_lines) + register_students_page.click_register_button().click_popup_yes() + + # Add additional info + additional_info_page = nav.click_register_students().click_tab_additional_info() + additional_input_x = get_random_string(10) + additional_info_page.input_additional_info_register(additional_input_x) + additional_info_page.click_additional_info_register_button() + additional_input_y = get_random_string(10) + additional_info_page.input_additional_info_register(additional_input_y) + additional_info_page.click_additional_info_register_button() + + # Go to update additional info page + update_additional_info_page = register_students_page.click_tab_update_additional_info() + self.assertIn( + u"· {}: 255 characters or less.".format(additional_input_x), + update_additional_info_page.get_additional_infos(), + ) + self.assertIn( + u"· {}: 255 characters or less.".format(additional_input_y), + update_additional_info_page.get_additional_infos(), + ) + + # Check error message + update_additional_info_page.click_additional_info_update_button() + self.assertEqual( + [u"Could not find student list."], + update_additional_info_page.messages, + ) + + # Update additional info + additional_input_x_user1 = get_random_string(10) + additional_input_y_user1 = get_random_string(10) + additional_input_x_user2 = get_random_string(10) + additional_input_y_user2 = get_random_string(10) + additional_input_x_user3 = get_random_string(10) + additional_input_y_user3 = get_random_string(10) + csv_list = self._make_additional_info_csv(self.new_users[0]['email'], + additional_input_x_user1, + additional_input_y_user1) + csv_list += self._make_additional_info_csv(self.new_users[1]['email'], + additional_input_x_user2, + additional_input_y_user2) + csv_list += self._make_additional_info_csv(self.new_users[2]['email'], + additional_input_x_user3, + additional_input_y_user3) + update_additional_info_page.input_additional_info_list(csv_list) + update_additional_info_page.click_additional_info_update_button().click_popup_yes() + + # Check message + self.assertEqual( + [u"Began the processing of Additional Item Update.Execution status, please check from the task history."], + update_additional_info_page.messages, + ) + + # Check task history + update_additional_info_page.click_show_history() + self._assert_additional_info_update_task_history( + update_additional_info_page.task_history_grid_row, + 3, 3, 0, 0, + self.director, + ) + self.assertEqual( + [u"No messages."], + update_additional_info_page.task_messages, + ) + + def test_no_additional_item(self): + self.switch_to_user(self.director) + nav = BizNavPage(self.browser).visit() + nav.change_manage_target(self.org_info['Organization Name'], self.contract['Contract Name'], self.course_key) + + # Register students + register_students_page = nav.click_register_students() + input_lines = self._make_students([self.new_users[0]]) + register_students_page.input_students(input_lines) + register_students_page.click_register_button().click_popup_yes() + + # Update additional info + update_additional_info_page = nav.click_register_students().click_tab_update_additional_info() + additional_input_x_user1 = get_random_string(10) + additional_input_y_user1 = get_random_string(10) + csv_list = self._make_additional_info_csv(self.new_users[0]['email'], + additional_input_x_user1, + additional_input_y_user1) + + update_additional_info_page.input_additional_info_list(csv_list) + update_additional_info_page.click_additional_info_update_button().click_popup_yes() + + # Confirm message + self.assertEqual( + [u"No additional item registered."], + update_additional_info_page.messages + ) + + def test_errors_user(self): + self.switch_to_user(self.director) + nav = BizNavPage(self.browser).visit() + nav.change_manage_target(self.org_info['Organization Name'], self.contract['Contract Name'], self.course_key) + + # Register students + register_students_page = nav.click_register_students() + input_lines = self._make_students([self.new_users[0]]) + register_students_page.input_students(input_lines) + register_students_page.click_register_button().click_popup_yes() + + # Add additional info + additional_info_page = nav.click_register_students().click_tab_additional_info() + additional_input_x = get_random_string(10) + additional_info_page.input_additional_info_register(additional_input_x) + additional_info_page.click_additional_info_register_button() + additional_input_y = get_random_string(10) + additional_info_page.input_additional_info_register(additional_input_y) + additional_info_page.click_additional_info_register_button() + + # Update additional info + update_additional_info_page = nav.click_register_students().click_tab_update_additional_info() + additional_input_x_user1 = get_random_string(10) + additional_input_y_user1 = get_random_string(10) + + # Note: + # Line1 : success + # Line2 : user does not exists + # Line3 : user not registered in contract + csv_list = self._make_additional_info_csv(self.new_users[0]['email'], + additional_input_x_user1, + additional_input_y_user1) + email = 'test_user_does_not_exist@gacco.org' + csv_list += self._make_additional_info_csv(email, + additional_input_x_user1, + additional_input_y_user1) + csv_list += self._make_additional_info_csv(self.new_users[1]['email'], + additional_input_x_user1, + additional_input_y_user1) + + update_additional_info_page.input_additional_info_list(csv_list) + update_additional_info_page.click_additional_info_update_button().click_popup_yes() + + # Confirm message + self.assertEqual( + [u"Began the processing of Additional Item Update.Execution status, please check from the task history."], + update_additional_info_page.messages + ) + + # Check task history + update_additional_info_page.click_show_history() + self._assert_additional_info_update_task_history( + update_additional_info_page.task_history_grid_row, + 3, 1, 0, 2, + self.director + ) + self.assertEqual( + [ + u"Line 2:The user does not exist. ({})".format(email), + u"Line 3:Could not find target user.", + ], + update_additional_info_page.task_messages + ) + + def test_errors_other(self): + self.switch_to_user(self.director) + nav = BizNavPage(self.browser).visit() + nav.change_manage_target(self.org_info['Organization Name'], self.contract['Contract Name'], self.course_key) + + # Register students + register_students_page = nav.click_register_students() + input_lines = self._make_students([self.new_users[0], self.new_users[1], self.new_users[2]]) + register_students_page.input_students(input_lines) + register_students_page.click_register_button().click_popup_yes() + + # Add additional info + additional_info_page = nav.click_register_students().click_tab_additional_info() + additional_input_x = get_random_string(10) + additional_info_page.input_additional_info_register(additional_input_x) + additional_info_page.click_additional_info_register_button() + additional_input_y = get_random_string(10) + additional_info_page.input_additional_info_register(additional_input_y) + additional_info_page.click_additional_info_register_button() + + # Update additional info + update_additional_info_page = nav.click_register_students().click_tab_update_additional_info() + additional_input_x_user1 = get_random_string(10) + additional_input_y_user1 = get_random_string(10) + + # Note: + # Line1 : success + # Line2 : Too many columns + # Line3 : Input blank line + # Line4 : Over max character + csv_list = self._make_additional_info_csv(self.new_users[0]['email'], + additional_input_x_user1, + additional_input_y_user1) + csv_list += self._make_additional_info_csv(self.new_users[1]['email'], + additional_input_x_user1, + ',') + csv_list += u'\r\n' + csv_list += self._make_additional_info_csv(self.new_users[2]['email'], + additional_input_x_user1, + get_random_string(256)) + + update_additional_info_page.input_additional_info_list(csv_list) + update_additional_info_page.click_additional_info_update_button().click_popup_yes() + + # Confirm message + self.assertEqual( + [u"Began the processing of Additional Item Update.Execution status, please check from the task history."], + update_additional_info_page.messages + ) + + # Check task history + update_additional_info_page.click_show_history() + self._assert_additional_info_update_task_history( + update_additional_info_page.task_history_grid_row, + 4, 1, 1, 2, + self.director + ) + self.assertEqual( + [ + u"Line 2:Number of [emails] and [new items] must be the same.", + u"Line 4:Please enter the name of item within 255 characters.", + ], + update_additional_info_page.task_messages + ) diff --git a/common/test/acceptance/tests/biz/test_ga_course_about.py b/common/test/acceptance/tests/biz/test_ga_course_about.py index e7eee07bcbd0..13c635d9b294 100644 --- a/common/test/acceptance/tests/biz/test_ga_course_about.py +++ b/common/test/acceptance/tests/biz/test_ga_course_about.py @@ -41,7 +41,7 @@ def test_opening_course(self): contract = self.create_contract(BizContractPage(self.browser).visit(), self.CONTRACT_TYPE_PF, '2016/01/01', '2100/01/01', contractor_organization=A_COMPANY, - detail_info=[course._course_key], additional_info=[u'部署']) + detail_info=[course._course_key]) invitation_code = contract['Invitation Code'] # Logout @@ -66,10 +66,9 @@ def test_opening_course(self): CourseAboutPage(self.browser, course._course_key).visit() # Case 68 - # Register invitation code and additional info + # Register invitation code BizInvitationPage(self.browser).visit().input_invitation_code(invitation_code).click_register_button() - BizInvitationConfirmPage(self.browser, invitation_code).wait_for_page().input_additional_info(u'マーケティング部', 0) \ - .click_register_button() + BizInvitationConfirmPage(self.browser, invitation_code).wait_for_page().click_register_button() DashboardPage(self.browser).wait_for_page() CourseAboutPage(self.browser, course._course_key).visit() @@ -87,8 +86,7 @@ def test_wait_start_course(self): # Register a contract with platfomer self.switch_to_user(PLATFORMER_USER_INFO) self.create_contract(BizContractPage(self.browser).visit(), self.CONTRACT_TYPE_PF, '2016/01/01', '2100/01/01', - contractor_organization=A_COMPANY, detail_info=[course._course_key], - additional_info=[u'部署']) + contractor_organization=A_COMPANY, detail_info=[course._course_key]) # Case 70 # Login with gacco staff @@ -116,8 +114,7 @@ def test_closed_course(self): # Register a contract with platfomer self.switch_to_user(PLATFORMER_USER_INFO) self.create_contract(BizContractPage(self.browser).visit(), self.CONTRACT_TYPE_PF, '2016/01/01', '2100/01/01', - contractor_organization=A_COMPANY, detail_info=[course._course_key], - additional_info=[u'部署']) + contractor_organization=A_COMPANY, detail_info=[course._course_key]) # Case 72 # Add course staff role for A company director diff --git a/common/test/acceptance/tests/biz/test_ga_invitation.py b/common/test/acceptance/tests/biz/test_ga_invitation.py index da8b3668bbab..b84270f59527 100644 --- a/common/test/acceptance/tests/biz/test_ga_invitation.py +++ b/common/test/acceptance/tests/biz/test_ga_invitation.py @@ -25,7 +25,6 @@ class BizInvitationTest(WebAppTest, GaccoBizTestMixin): CONTRACT_TYPE_GACCO_SERVICE = 'GS' CONTRACT_TYPE_OWNER_SERVICE = 'OS' - @flaky def test_register_owner_service_course(self): """ Tests register invitation code of owner service contract. @@ -45,22 +44,19 @@ def test_register_owner_service_course(self): self.CONTRACT_TYPE_OWNER_SERVICE, '2100/01/01', '2100/01/01', contractor_organization=C_COMPANY, - detail_info=[self.course._course_key], - additional_info=[u'部署']) + detail_info=[self.course._course_key]) contract_expired = self.create_contract(BizContractPage(self.browser).visit(), self.CONTRACT_TYPE_OWNER_SERVICE, '2000/01/01', '2000/01/01', contractor_organization=C_COMPANY, - detail_info=[self.course._course_key], - additional_info=[u'部署']) + detail_info=[self.course._course_key]) contract_effective = self.create_contract(BizContractPage(self.browser).visit(), self.CONTRACT_TYPE_OWNER_SERVICE, '2000/01/01', '2100/01/01', contractor_organization=C_COMPANY, - detail_info=[self.course._course_key], - additional_info=[u'部署']) + detail_info=[self.course._course_key]) # Login as C company director self.switch_to_user(C_DIRECTOR_USER_INFO) @@ -98,7 +94,8 @@ def test_register_owner_service_course(self): contract_effective['Invitation Code']).click_register_button() confirm_page = BizInvitationConfirmPage(self.browser, contract_effective['Invitation Code']).wait_for_page().click_register_button() confirm_page.wait_for_ajax() - self.assertIn(u'部署 is required.', confirm_page.additional_messages) + # TODO Fix by Hara. + # self.assertIn(u'部署 is required.', confirm_page.additional_messages) # Case 54, 103 # Input invitation code @@ -115,8 +112,7 @@ def test_register_owner_service_course(self): AccountSettingsPage(self.browser).visit().click_on_link_in_link_field('invitation_code') BizInvitationPage(self.browser).wait_for_page().input_invitation_code( contract_effective['Invitation Code']).click_register_button() - BizInvitationConfirmPage(self.browser, contract_effective['Invitation Code']).wait_for_page().input_additional_info(u'マーケティング部', - 0).click_register_button() + BizInvitationConfirmPage(self.browser, contract_effective['Invitation Code']).wait_for_page().click_register_button() # Verify that course is registered dashboard = DashboardPage(self.browser).wait_for_page() @@ -136,8 +132,7 @@ def test_register_platfomer_service_course(self): contract = self.create_contract(BizContractPage(self.browser).visit(), self.CONTRACT_TYPE_GACCO_SERVICE, '2016/01/01', '2100/01/01', contractor_organization=B_COMPANY, - detail_info=[self.course._course_key], - additional_info=[u'部署', u'社員番号']) + detail_info=[self.course._course_key]) # Change login user self.switch_to_user(B_DIRECTOR_USER_INFO) diff --git a/common/test/acceptance/tests/biz/test_ga_survey.py b/common/test/acceptance/tests/biz/test_ga_survey.py index 44842538d0e0..e43a812cfceb 100644 --- a/common/test/acceptance/tests/biz/test_ga_survey.py +++ b/common/test/acceptance/tests/biz/test_ga_survey.py @@ -2,11 +2,11 @@ """ End-to-end tests for survey of biz feature """ +import codecs import csv import os import shutil -import bok_choy from bok_choy.web_app_test import WebAppTest from django.utils.crypto import get_random_string from nose.plugins.attrib import attr @@ -50,7 +50,7 @@ def setUp(self): self.switch_to_user(PLATFORMER_USER_INFO) self.contract = self.create_contract(BizContractPage(self.browser).visit(), 'PF', '2016/01/01', '2100/01/01', contractor_organization=A_COMPANY, - detail_info=[self.course._course_key], additional_info=[u'部署']) + detail_info=[self.course._course_key]) # Change login user and answer survey acom_employees = [self.register_user(), self.register_user()] @@ -68,10 +68,18 @@ def test_survey_as_director(self): Tests that director of contractor can download survey. """ self.switch_to_user(A_DIRECTOR_USER_INFO) - BizNavPage(self.browser).visit().change_role(A_COMPANY, self.contract['Contract Name'], - self.course._course_key).click_survey().click_download_button() - bok_choy.browser.save_screenshot(self.browser, 'test_survey_as_director__1') - self._verify_csv_answers(self.answers) + BizNavPage(self.browser).visit().change_role(A_COMPANY, self.contract['Contract Name'], self.course._course_key)\ + .click_survey().check_encoding_utf8(False).click_download_button() + self._verify_csv_answers(self.answers, 'utf16') + + def test_survey_as_director_utf8(self): + """ + Tests that director of contractor can download survey. + """ + self.switch_to_user(A_DIRECTOR_USER_INFO) + BizNavPage(self.browser).visit().change_role(A_COMPANY, self.contract['Contract Name'], self.course._course_key)\ + .click_survey().check_encoding_utf8(True).click_download_button() + self._verify_csv_answers(self.answers, 'utf8') def test_survey_as_staff(self): """ @@ -80,9 +88,54 @@ def test_survey_as_staff(self): self.switch_to_user(SUPER_USER_INFO) instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course._course_key).visit() survey_page_section = instructor_dashboard_page.select_survey() - survey_page_section.click_download_button() - bok_choy.browser.save_screenshot(self.browser, 'test_survey_as_staff__1') - self._verify_csv_answers(self.answers) + survey_page_section.check_encoding_utf8(False).click_download_button() + self._verify_csv_answers(self.answers, 'utf16') + + def test_survey_as_staff_utf8(self): + """ + Tests that staff of platfomer can download survey. + """ + self.switch_to_user(SUPER_USER_INFO) + instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course._course_key).visit() + survey_page_section = instructor_dashboard_page.select_survey() + survey_page_section.check_encoding_utf8(True).click_download_button() + self._verify_csv_answers(self.answers, 'utf8') + + def test_utf8_checkbox_is_saved_as_biz(self): + """ + Test that the value of the checkbox is saved as biz. + """ + # checked + self.switch_to_user(A_DIRECTOR_USER_INFO) + bizSurveyPage = BizNavPage(self.browser).visit().change_role(A_COMPANY, self.contract['Contract Name'], + self.course._course_key).click_survey() + bizSurveyPage.check_encoding_utf8(True).click_download_button() + bizSurveyPage = BizNavPage(self.browser).visit().change_role(A_COMPANY, self.contract['Contract Name'], + self.course._course_key).click_survey() + self.assertTrue(bizSurveyPage.is_encoding_utf8_selected()) + # unchecked + bizSurveyPage.check_encoding_utf8(False).click_download_button() + bizSurveyPage = BizNavPage(self.browser).visit().change_role(A_COMPANY, self.contract['Contract Name'], + self.course._course_key).click_survey() + self.assertFalse(bizSurveyPage.is_encoding_utf8_selected()) + + def test_utf8_checkbox_is_saved_as_instructor(self): + """ + Test that the value of the checkbox is saved as instructor. + """ + # checked + self.switch_to_user(SUPER_USER_INFO) + instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course._course_key).visit() + survey_page_section = instructor_dashboard_page.select_survey() + survey_page_section.check_encoding_utf8(True).click_download_button() + instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course._course_key).visit() + survey_page_section = instructor_dashboard_page.select_survey() + self.assertTrue(survey_page_section.is_encoding_utf8_selected()) + # unchecked + survey_page_section.check_encoding_utf8(False).click_download_button() + instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course._course_key).visit() + survey_page_section = instructor_dashboard_page.select_survey() + self.assertFalse(survey_page_section.is_encoding_utf8_selected()) def _answer_survey(self, login_user, answer): """ @@ -109,21 +162,21 @@ def _answer_survey(self, login_user, answer): self.assertIn(u"ご回答ありがとうございました。", survey_page.wait_for_messages()) self.assertFalse(survey_page.is_submit_button_enabled()) - def _verify_csv_answers(self, expect_data): + def _verify_csv_answers(self, expect_data, encoding): """ Verify csv file. """ - # Get csv file + # Get csv file. (Content_type is 'text/tab-separated-values') tmp_file = max( - [os.path.join(DOWNLOAD_DIR, f) for f in os.listdir(DOWNLOAD_DIR) if f.count('.csv')], + [os.path.join(DOWNLOAD_DIR, f) for f in os.listdir(DOWNLOAD_DIR) if f.count('.tsv')], key=os.path.getctime) csv_file = os.path.join(os.environ.get('SELENIUM_DRIVER_LOG_DIR', ''), self._testMethodName + '.csv') shutil.move(os.path.join(DOWNLOAD_DIR, tmp_file), csv_file) # Read csv - reader = csv.DictReader(open(csv_file)) + with codecs.open(csv_file, encoding=encoding) as f: + reader = csv.DictReader([row.encode('utf8') for row in f], delimiter="\t") csv_data = [[row.get('Q1').decode('utf8'), row.get('Q2').decode('utf8'), row.get('Q3').decode('utf8'), row.get('Q4').decode('utf8'), row.get('User Name').decode('utf8')] for row in reader] - self.assertEqual(csv_data, expect_data) @@ -162,7 +215,7 @@ def setUp(self): self.switch_to_user(PLATFORMER_USER_INFO) self.contract = self.create_contract(BizContractPage(self.browser).visit(), 'PF', '2016/01/01', '2100/01/01', contractor_organization=A_COMPANY, - detail_info=[self.course._course_key], additional_info=[u'部署']) + detail_info=[self.course._course_key]) # Make contract auth self.django_admin_page = DjangoAdminPage(self.browser) @@ -222,10 +275,18 @@ def test_survey_as_director(self): Tests that director of contractor can download survey. """ self.switch_to_user(A_DIRECTOR_USER_INFO) - BizNavPage(self.browser).visit().change_role(A_COMPANY, self.contract['Contract Name'], - self.course._course_key).click_survey().click_download_button() - bok_choy.browser.save_screenshot(self.browser, 'test_survey_as_director__1') - self._verify_csv_answers(self.expect_datas_as_director,True) + BizNavPage(self.browser).visit().change_role(A_COMPANY, self.contract['Contract Name'], self.course._course_key)\ + .click_survey().check_encoding_utf8(False).click_download_button() + self._verify_csv_answers(self.expect_datas_as_director, True, 'utf16') + + def test_survey_as_director_utf8(self): + """ + Tests that director of contractor can download survey. + """ + self.switch_to_user(A_DIRECTOR_USER_INFO) + BizNavPage(self.browser).visit().change_role(A_COMPANY, self.contract['Contract Name'], self.course._course_key)\ + .click_survey().check_encoding_utf8(True).click_download_button() + self._verify_csv_answers(self.expect_datas_as_director, True, 'utf8') def test_survey_as_staff(self): """ @@ -234,9 +295,18 @@ def test_survey_as_staff(self): self.switch_to_user(SUPER_USER_INFO) instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course._course_key).visit() survey_page_section = instructor_dashboard_page.select_survey() - survey_page_section.click_download_button() - bok_choy.browser.save_screenshot(self.browser, 'test_survey_as_staff__1') - self._verify_csv_answers(self.expect_datas_as_staff, False) + survey_page_section.check_encoding_utf8(False).click_download_button() + self._verify_csv_answers(self.expect_datas_as_staff, False, 'utf16') + + def test_survey_as_staff_utf8(self): + """ + Tests that staff of platfomer can download survey. + """ + self.switch_to_user(SUPER_USER_INFO) + instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course._course_key).visit() + survey_page_section = instructor_dashboard_page.select_survey() + survey_page_section.check_encoding_utf8(True).click_download_button() + self._verify_csv_answers(self.expect_datas_as_staff, False, 'utf8') def _answer_survey(self, login_user, answer): """ @@ -263,18 +333,19 @@ def _answer_survey(self, login_user, answer): self.assertIn(u"ご回答ありがとうございました。", survey_page.wait_for_messages()) self.assertFalse(survey_page.is_submit_button_enabled()) - def _verify_csv_answers(self, expect_data, enable_login_code_check): + def _verify_csv_answers(self, expect_data, enable_login_code_check, encoding): """ Verify csv file. """ - # Get csv file + # Get csv file. (Content_type is 'text/tab-separated-values') tmp_file = max( - [os.path.join(DOWNLOAD_DIR, f) for f in os.listdir(DOWNLOAD_DIR) if f.count('.csv')], + [os.path.join(DOWNLOAD_DIR, f) for f in os.listdir(DOWNLOAD_DIR) if f.count('.tsv')], key=os.path.getctime) csv_file = os.path.join(os.environ.get('SELENIUM_DRIVER_LOG_DIR', ''), self._testMethodName + '.csv') shutil.move(os.path.join(DOWNLOAD_DIR, tmp_file), csv_file) # Read csv - reader = csv.DictReader(open(csv_file)) + with codecs.open(csv_file, encoding=encoding) as f: + reader = csv.DictReader([row.encode('utf8') for row in f], delimiter="\t") if enable_login_code_check: csv_data = [[row.get('Login Code').decode('utf8'), row.get('Q1').decode('utf8'), row.get('Q2').decode('utf8'), row.get('Q3').decode('utf8'), row.get('Q4').decode('utf8'), row.get('User Name').decode('utf8') diff --git a/common/test/acceptance/tests/lms/test_ga_instructor_dashboard.py b/common/test/acceptance/tests/lms/test_ga_instructor_dashboard.py index 127d1a45b2fd..2096704724eb 100644 --- a/common/test/acceptance/tests/lms/test_ga_instructor_dashboard.py +++ b/common/test/acceptance/tests/lms/test_ga_instructor_dashboard.py @@ -382,7 +382,7 @@ def _setup_spoc_course_with_jwplayer(self): self.create_contract(BizContractPage(self.browser).visit(), 'PF', '2016/01/01', '2100/01/01', contractor_organization=A_COMPANY, - detail_info=[self.course_key], additional_info=[u'部署']) + detail_info=[self.course_key]) # Logout LogoutPage(self.browser).visit() diff --git a/ga/conf/locale/ja_JP/LC_MESSAGES/biz.po b/ga/conf/locale/ja_JP/LC_MESSAGES/biz.po index 5fdf75716e70..afa842e02648 100644 --- a/ga/conf/locale/ja_JP/LC_MESSAGES/biz.po +++ b/ga/conf/locale/ja_JP/LC_MESSAGES/biz.po @@ -213,10 +213,6 @@ msgstr "合計ユニークユーザー数" msgid "Total Course Count" msgstr "講座数" -#: biz/templates/ga_contract/index.html -msgid "Additional Info Count" -msgstr "追加情報数" - #: biz/templates/ga_contract/index.html msgid "Created By Name" msgstr "作成者名" @@ -245,6 +241,10 @@ msgstr "追加情報" msgid "Add Contract Detail Info" msgstr "新しい講座情報を追加" +#: biz/templates/ga_contract/detail.html +msgid "Not found course for Contract Detail Info" +msgstr "追加できる講座情報がありません" + #: biz/templates/ga_contract/detail.html msgid "Add Additional Info" msgstr "新しい追加情報を追加" @@ -257,10 +257,18 @@ msgstr "登録" msgid "Url code is invalid. Please enter alphanumeric {min_length}-{max_length} characters." msgstr "URLコードが不正です。英字で{min_length}~{max_length}文字で入力してください。" +#: biz/djangoapps/ga_contract/admin.py +msgid "Url code is duplicated. Please change url code." +msgstr "URLコードが重複しています。URLコードを変更してください。" + #: biz/djangoapps/ga_contract/views.py msgid "You need to create an organization first." msgstr "組織を先に作成する必要があります。" +#: biz/djangoapps/ga_contract/views.py +msgid "Invalid contract details." +msgstr "講座情報が不正です。再度、入力してください。" + #: biz/djangoapps/ga_contract/views.py msgid "The new contract has been added." msgstr "新しい契約が追加されました。" @@ -432,7 +440,7 @@ msgid "Confirm Course" msgstr "コース内容を確認する" #: biz/templates/ga_invitation/confirm.html -msgid "To take {contract_name} are required to enter the additional information below." +msgid "To take {contract_name} are required to enter the additional item below." msgstr "{contract_name}を受講するには下記の追加情報の入力が必要です。" #: biz/templates/ga_invitation/confirm.html @@ -579,24 +587,133 @@ msgstr "登録済の契約が存在するため、組織を削除できません ##### Contract Operation #: biz/templates/ga_contract_operation/register_students.html -msgid "To register a list of users in this course, contains the following columns in this exact order: e-mail, username, and name. Please include one user per row and do not include any headers, footers, or blank lines." -msgstr "区切り文字カンマ(,)で、次に示す項目の順序に従い値を指定してください。なお、1受講者につき1行で記載し、ヘッダー、フッターや空行を含んではいけません。
" -"・メールアドレス:半角英数字、ハイフン(-)、アンダーバー(_)、ピリオド(.)。
" -"・ユーザー名:半角英数2文字以上、30文字以下。ディスカッションの際に表示されるユーザー名となります。(一度、登録すると変更できません)
" -"・氏名:修了証に表示するために用いる情報です。" +msgid "Enrolling Learners" +msgstr "受講者登録" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "Enroll each Learner by a single command-line using comma(,), in the order of [E-mail address], [User name], [Name]. These Items cannot be changed." +msgstr "1受講者につき1行で【メールアドレス】,【ユーザー名】,【氏名】の順に区切り文字カンマ(,)で区切って記入ください。" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "Enroll each Learner by a single command-line using comma(,), in the order of [E-mail address], [User name], [Name], [Login code], [Password].
These Items cannot be changed." +msgstr "1受講者につき1行で【メールアドレス】,【ユーザー名】,【氏名】,【ログインコード】,【パスワード】の順に区切り文字カンマ(,)で区切って記入ください。" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "ex. username1@domain.com,gaccotarou,gaccoutarou" +msgstr "(例)username1@domain.com,gaccotarou,学校 太郎" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "ex. username1@domain.com,gaccotarou,gaccoutarou,gacco,Gacco12345" +msgstr "(例)username1@domain.com,gaccotarou,学校 太郎,gacco,Gacco12345" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "< Note >" +msgstr "<記入する際の注意点>" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "< Entry item >" +msgstr "<記入情報>" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "Please be aware that once you press the \"Register Signup\" button, a welcome e-mail of the course will be sent automatically. This welcome e-mail cannot be reissued." +msgstr "「登録」を押下すると、受講者へ即時メールが自動送信されますのでご注意ください。自動送信メールは再発行できません。" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "· Should not include headers, footers or blank lines." +msgstr "・ヘッダーやフッター、空行を含めないでください" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "· E-mail address: Half-width alphanumeric characters, hyphen (-), underscore (_), period (.)." +msgstr "・メールアドレス:半角英数字、ハイフン(-)、アンダーバー(_)、ピリオド(.)" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "· User name: Two or more half-width alphabets, up to 30 characters. (It will be the User name displayed during the discussion. Once registered, it can not be changed.)" +msgstr "・ユーザー名:半角英数2文字以上、30文字以下(ディスカッションの際に表示されるユーザー名です、一度登録すると変更できません)" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "· Name: \"Name\" will be included in the certificate for the course if the Learner successfully gets one." +msgstr "・氏名:修了証に表示するために用いる情報です" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "· Login code: 2 or more half-width alphabets and 30 or less." +msgstr "・ログインコード:半角英数2文字以上、30文字以下" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "· Password: 8 characters or more. (Must include at least one upper case letter, one lower case letter, and one number, all in one byte characters.)" +msgstr "・パスワード:半角英数8文字以上(半角アルファベットの大文字、小文字、数字を1文字以上含めてください)" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "" +"Delete item here:
" +"*If deleted item, each user input the additional item will be delete." +msgstr "" +"削除する追加情報を選択してください。
" +"※追加情報を削除すると、受講者がすでに登録した追加情報もすべて削除されます。" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "Add new item here:" +msgstr "追加情報の名称を入力してください。" + +#: biz/templates/ga_contract_operation/register_students.html +#: biz/templates/ga_contract_operation/views.py +msgid "Please enter the name of item you wish to add." +msgstr "追加情報の名称を入力してください。" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "Create Additional Item" +msgstr "追加情報作成" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "Editing Additional Item" +msgstr "追加情報設定" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "Enrolling Learners to Additional Item" +msgstr "追加情報登録" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "· Up to {max_students_number} Learners can be enrolled at a time." +msgstr "・一度に登録できる受講者数は、{max_students_number}人です" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "· {additional_item_name}: {max_length} characters or less." +msgstr "・{additional_item_name}:{max_length}文字以下" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "If the course instructor wishes to Enroll more information to the existing list, add/delete item as follows:" +msgstr "追加情報の設定を行います。" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "Each user enter the additional item will be delete. Is it OK?" +msgstr "受講者がすでに入力した追加情報も削除されます。よろしいですか?" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "Up to {max_number} number of additional item is created." +msgstr "登録できる追加情報は{max_number}件までです。" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "To enroll additional item of the Learners separately from the first registration, enroll each Learner by a single command-line using comma(,), in the order." +msgstr "受講者の追加情報を個別に登録できます。
1受講者につき1行で【メールアドレス】,【追加情報】の順に区切り文字カンマ(,)で区切って記入ください。" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "· E-mail address: The same e-mail address as the Learners already registered." +msgstr "・メールアドレス:受講者一覧に記載されているメールアドレス" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "ex. username1@domain.com,system department,manager" +msgstr "(例)username1@domain.com,システム事業部,部長" + +#: biz/templates/ga_contract_operation/register_students.html +msgid "username1@domain.com, [additional item]*" +msgstr "username1@domain.com, [追加情報]*" #: biz/templates/ga_contract_operation/register_students.html -msgid "To register a list of users in this course, contains the following columns in this exact order: e-mail, username, name, login code, and password. Please include one user per row and do not include any headers, footers, or blank lines." -msgstr "区切り文字カンマ(,)で、次に示す項目の順序に従い値を指定してください。なお、1受講者につき1行で記載し、ヘッダー、フッターや空行を含んではいけません。
" -"・メールアドレス:半角英数字、ハイフン(-)、アンダーバー(_)、ピリオド(.)。
" -"・ユーザー名:半角英数2文字以上、30文字以下。ディスカッションの際に表示されるユーザー名となります。(一度、登録すると変更できません)
" -"・氏名:修了証に表示するために用いる情報です。
" -"・ログインコード:半角英数2文字以上、30文字以下
" -"・パスワード:半角英数8文字以上(半角アルファベットの大文字、小文字、数字を1文字以上含めてください)
" +msgid "You will register additional item for students. Are you sure?" +msgstr "受講者に対して追加情報の登録を行います。よろしいですか?" #: biz/templates/ga_contract_operation/register_students.html -msgid "Once can be registered number is {max_register_number}." -msgstr "一度に登録できる受講者は{max_register_number}件です。" +msgid "Register additional item" +msgstr "追加情報を登録する" #: biz/templates/ga_contract_operation/register_students.html msgid "username1@domain.com,gaccotarou,gacco taro" @@ -620,6 +737,10 @@ msgstr "受講ユーザーが直ちに受講が開始できる状態となりま msgid "No messages." msgstr "メッセージはありません。" +#: biz/templates/ga_contract_operation/register_students.html +msgid "· Additional item use comma(,) after the E-mail address, then enter the additional item." +msgstr "・追加情報は、メールアドレスの後に区切り文字カンマ(,)を入れて、記入ください" + #: biz/djangoapps/ga_contract_operation/student_register.py msgid "Invalid email {email}." msgstr "メールアドレスが不正です:{email}" @@ -756,7 +877,7 @@ msgid "Submission Reminder Mail" msgstr "未提出課題督促メール" #: biz/templates/ga_contract_operation/register_students.html -msgid "Register Students" +msgid "Enrolling New Learners" msgstr "受講者を新規登録" #: biz/templates/ga_contract_operation/register_students.html @@ -857,6 +978,46 @@ msgstr "実行状況はタスク履歴から確認してください。" msgid "Total: {total}, Success: {succeeded}, Skipped: {skipped}, Failed: {failed}" msgstr "対象: {total}件、成功: {succeeded}件、スキップ: {skipped}件、失敗: {failed}件" +#: biz/templates/ga_contract_operation/views.py +msgid "Please enter the name of item within {max_number} characters." +msgstr "追加情報は{max_number}文字以内で入力してください。" + +#: biz/templates/ga_contract_operation/views.py +msgid "The same item has already been registered." +msgstr "同じ追加情報の名称が登録されています。" + +#: biz/templates/ga_contract_operation/views.py +msgid "New item has been registered." +msgstr "追加情報が登録されました。" + +#: biz/templates/ga_contract_operation/views.py +msgid "New item has been updated." +msgstr "追加情報が更新されました。" + +#: biz/templates/ga_contract_operation/views.py +msgid "New item has been deleted." +msgstr "追加情報が削除されました。" + +#: biz/templates/ga_contract_operation/views.py +msgid "Failed to register item." +msgstr "追加情報の登録に失敗しました。" + +#: biz/templates/ga_contract_operation/views.py +msgid "Failed to edit item." +msgstr "追加情報の更新に失敗しました。" + +#: biz/templates/ga_contract_operation/views.py +msgid "Failed to deleted item." +msgstr "追加情報の削除に失敗しました。" + +#: biz/templates/ga_contract_operation/views.py +msgid "Already deleted." +msgstr "既に削除されています。" + +#: biz/templates/ga_contract_operation/views.py +msgid "New item registered. Please reload browser." +msgstr "追加情報が変更されました。ブラウザを更新してください。" + #: biz/templates/ga_contract_operation/students.html msgid "Do the bulk students register. Are you sure?" msgstr "受講者登録を行います。よろしいですか?" @@ -866,8 +1027,8 @@ msgid "Do the bulk students unregister. Are you sure?" msgstr "一括受講解除を行います。本当によろしいですか?" #: biz/templates/ga_contract_operation/students.html -msgid "After execution of the personal information mask processing,, it can not be undone. Are you sure?" -msgstr "個人情報削除処理の実行後、元に戻すことはできません。本当によろしいですか?" +msgid "After execution of the personal information mask processing, it can not be undone. Are you sure?" +msgstr "個人情報削除を実行すると復元することができません。個人情報削除してよろしいですか?" #: biz/templates/ga_contract_operation/students.html msgid "Unregister Student" @@ -881,6 +1042,26 @@ msgstr "受講者新規登録" msgid "Student Unregister" msgstr "受講解除" +#: biz/djangoapps/ga_contract_operation/tasks.py +msgid "Additional Item Update" +msgstr "追加情報登録" + +#: biz/djangoapps/ga_contract_operation/additionalinfo.py +msgid "Number of [emails] and [new items] must be the same." +msgstr "メールアドレス、追加情報設定値は同数でなければなりません。" + +#: biz/djangoapps/ga_contract_operation/additionalinfo.py +msgid "Could not find target user." +msgstr "入力された受講者が見つかりませんでした。" + +#: biz/djangoapps/ga_contract_operation/additionalinfo.py +msgid "No additional item registered." +msgstr "追加情報はこの契約に登録されておりません。" + +#: biz/djangoapps/ga_manager/views.py +msgid "The user does not exist. ({email})" +msgstr "指定されたユーザーは存在しません。({email})" + #: biz/templates/ga_contract_operation/students.html msgid "Personal Information Mask" msgstr "個人情報削除" @@ -894,7 +1075,7 @@ msgid "Show Task History" msgstr "タスク履歴を表示" #: biz/templates/ga_contract_operation/students.html -msgid "To see the status of the tasks that have been run in the background, please click this button." +msgid "To see the status of tasks running in the background, click the button below." msgstr "バックグラウンドで実行されたタスクの状況を見るには、このボタンをクリックしてください。" #: biz/templates/ga_contract_operation/students.html @@ -959,6 +1140,10 @@ msgstr "日前" msgid "(The target courses and the section are listed. This part can not be edited.)" msgstr "(対象の講座名および節が列挙されます。この部分は編集できません。)" +#: biz/templates/ga_contract_operation/_task_history.html +msgid "Task Type" +msgstr "タスク種別" + ##### Login #: biz/djangoapps/ga_login/views.py msgid "Login code or password is incorrect." @@ -972,6 +1157,11 @@ msgstr "ログインコード" msgid "example: loginCode123" msgstr "例:loginCode123" +##### util +#: biz/djangoapps/util/task_util.py +msgid "{task_type_name} is being executed. Please check task history, leave time and try again." +msgstr "{task_type_name}が実行されています。タスク履歴を確認し時間を空けて再度、操作してください。" + ##### Common #: */forms.py msgid "max {0} character" diff --git a/ga/conf/locale/ja_JP/LC_MESSAGES/bizjs.po b/ga/conf/locale/ja_JP/LC_MESSAGES/bizjs.po index dcf698e61758..185898779923 100644 --- a/ga/conf/locale/ja_JP/LC_MESSAGES/bizjs.po +++ b/ga/conf/locale/ja_JP/LC_MESSAGES/bizjs.po @@ -23,6 +23,10 @@ msgstr "" msgid "Please select a target." msgstr "対象を選択してください。" +#: biz/templates/ga_contract_operation/register_students.html +msgid "· {additional_item_name}: {max_length} characters or less." +msgstr "・{additional_item_name}:{max_length}文字以下" + ##### Course Operation ##### Course Selection diff --git a/ga/conf/locale/ja_JP/LC_MESSAGES/gacco.po b/ga/conf/locale/ja_JP/LC_MESSAGES/gacco.po index f86914929573..da0b426cb93f 100644 --- a/ga/conf/locale/ja_JP/LC_MESSAGES/gacco.po +++ b/ga/conf/locale/ja_JP/LC_MESSAGES/gacco.po @@ -324,13 +324,17 @@ msgid "Survey Download" msgstr "アンケート結果ダウンロード" #: lms/templates/instructor/instructor_dashboard_2/survey.html -msgid "The survey result can be downloaded as a CSV file." -msgstr "アンケート結果がCSVファイルでダウンロードできます。
CSVファイルをそのままExcelで開こうとすると、日本語が含まれている場合に文字化けしてしまいますので、一旦ファイルを保存した後、「UTF-8」に対応したテキストエディタで開き直してください。" +msgid "You can download the survey results." +msgstr "アンケート結果がCSVファイルでダウンロードできます。" #: lms/templates/instructor/instructor_dashboard_2/survey.html msgid "Get Survey CSV" msgstr "アンケート結果をCSVとしてダウンロードする" +#: lms/templates/instructor/instructor_dashboard_2/survey.html +msgid "If you want to download the file in UTF-8, please place a check mark and then push the button." +msgstr "アンケート結果をエンコード「UTF-8」でダウンロードしたい場合は、チェックを入れてダウンロードしてください。
チェックを入れていない場合は、エンコード「UTF-16」になります。" + #: lms/templates/dashboard/_dashboard_certificate_information.html msgid "Certificate" msgstr "修了証" @@ -642,6 +646,10 @@ msgstr "開く" msgid "Search..." msgstr "講座名で検索" +#: lms/templates/dashboard.html +msgid "Email Settings for {course_name}" +msgstr "「{course_name}」のメール受信設定" + #: lms/templates/courseware/progress.html msgid "Your certificate is available" msgstr "修了証発行が完了しました" @@ -693,6 +701,10 @@ msgstr "(有料)" msgid "[Free of charge] Enroll in {course_name}" msgstr "{course_name}を受講登録する(無料)" +#: templates/courseware/course_about.html +msgid "[Free of charge] Enroll" +msgstr "受講登録する(無料)" + #: templates/register-sidebar.html msgid "Please check the 'FAQs' if you experience any problems." msgstr "お困りの場合はサポートサイトの「よくある質問」をご確認ください。" @@ -841,6 +853,10 @@ msgstr "対面学習コース 購入済み" msgid "View your MDA" msgstr "MDA診断結果を確認する" +#: templates/dashboard/_dashboard_course_listing.html +msgid "Are you sure you want to unenroll from %(course_name)s?" +msgstr "「%(course_name)s」の受講を取り消します。よろしいですか?" + #: templates/ga_advanced_course/choose.html msgid "Recommended: Online and Face-to-face course" msgstr "<推奨> オンライン学習 + 対面学習" @@ -1177,6 +1193,10 @@ msgstr "機能" msgid "Staff Assessment for Peer Grading" msgstr "相互採点のスタッフ採点" +#: openedx/core/djangoapps/ga_optional/models.py +msgid "Video Upload Option for Peer Grading" +msgstr "相互採点の動画アップロード" + #: openedx/core/djangoapps/ga_optional/models.py msgid "Providing Image Server for Discussion" msgstr "ディスカッションでの画像サーバー提供" @@ -1788,3 +1808,8 @@ msgstr "" #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Add Course Scorer" msgstr "講座スタッフ(採点用権限)を追加する" + +#: lms/templates/seq_module.html +msgctxt "sequence_nav" +msgid "Video" +msgstr "動画" diff --git a/lms/djangoapps/courseware/tests/test_about.py b/lms/djangoapps/courseware/tests/test_about.py index a454966e9ce2..021ee4eee457 100644 --- a/lms/djangoapps/courseware/tests/test_about.py +++ b/lms/djangoapps/courseware/tests/test_about.py @@ -80,7 +80,7 @@ def test_anonymous_user(self): # Check that registration button is present self.assertIn(REG_STR, resp.content) - self.assertIn("[Free of charge] Enroll in {}".format(self.course.display_number_with_default), resp.content) + self.assertIn("[Free of charge] Enroll", resp.content) self.assertNotIn("fee-charging", resp.content) @ddt.data(CourseMode.PROFESSIONAL, CourseMode.NO_ID_PROFESSIONAL_MODE) @@ -109,7 +109,7 @@ def test_logged_in(self): resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertIn("OOGIE BLOOGIE", resp.content) - self.assertIn("[Free of charge] Enroll in {}".format(self.course.display_number_with_default), resp.content) + self.assertIn("[Free of charge] Enroll", resp.content) self.assertNotIn("fee-charging", resp.content) @ddt.data(CourseMode.PROFESSIONAL, CourseMode.NO_ID_PROFESSIONAL_MODE) @@ -347,7 +347,7 @@ def test_invitation_only_but_allowed(self): url = reverse('about_course', args=[self.course.id.to_deprecated_string()]) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) - self.assertIn(u"[Free of charge] Enroll in {}".format(self.course.id.course), resp.content.decode('utf-8')) + self.assertIn(u"[Free of charge] Enroll", resp.content.decode('utf-8')) # Check that registration button is present self.assertIn(REG_STR, resp.content) @@ -377,7 +377,7 @@ def test_logged_in_shib_course(self): resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertIn("OOGIE BLOOGIE", resp.content) - self.assertIn(u"[Free of charge] Enroll in {}".format(self.course.id.course), resp.content.decode('utf-8')) + self.assertIn(u"[Free of charge] Enroll", resp.content.decode('utf-8')) self.assertIn(SHIB_ERROR_STR, resp.content) self.assertIn(REG_STR, resp.content) @@ -389,7 +389,7 @@ def test_anonymous_user_shib_course(self): resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertIn("OOGIE BLOOGIE", resp.content) - self.assertIn(u"[Free of charge] Enroll in {}".format(self.course.id.course), resp.content.decode('utf-8')) + self.assertIn(u"[Free of charge] Enroll", resp.content.decode('utf-8')) self.assertIn(SHIB_ERROR_STR, resp.content) self.assertIn(REG_STR, resp.content) diff --git a/lms/djangoapps/courseware/tests/test_microsites.py b/lms/djangoapps/courseware/tests/test_microsites.py index 849ef224a111..89b02c4028c7 100644 --- a/lms/djangoapps/courseware/tests/test_microsites.py +++ b/lms/djangoapps/courseware/tests/test_microsites.py @@ -222,14 +222,14 @@ def test_paid_course_registration(self): url = reverse('about_course', args=[self.course_with_visibility.id.to_deprecated_string()]) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) - self.assertIn("Enroll in {}".format(self.course_with_visibility.id.course), resp.content) + self.assertIn("[Free of charge] Enroll", resp.content) self.assertNotIn("Add {} to Cart ($10)".format(self.course_with_visibility.id.course), resp.content) # now try on the microsite url = reverse('about_course', args=[self.course_with_visibility.id.to_deprecated_string()]) resp = self.client.get(url, HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME) self.assertEqual(resp.status_code, 200) - self.assertNotIn("Enroll in {}".format(self.course_with_visibility.id.course), resp.content) + self.assertNotIn("[Free of charge] Enroll", resp.content) self.assertIn("Add {} to Cart ($10 USD)".format( self.course_with_visibility.id.course ), resp.content) diff --git a/lms/djangoapps/courseware/tests/test_split_module.py b/lms/djangoapps/courseware/tests/test_split_module.py index 7821a2c0df5a..6cfce89e3bc0 100644 --- a/lms/djangoapps/courseware/tests/test_split_module.py +++ b/lms/djangoapps/courseware/tests/test_split_module.py @@ -22,7 +22,6 @@ class SplitTestBase(ModuleStoreTestCase): """ __test__ = False COURSE_NUMBER = 'split-test-base' - ICON_CLASSES = None TOOLTIPS = None HIDDEN_CONTENT = None VISIBLE_CONTENT = None @@ -119,7 +118,7 @@ def _check_split_test(self, user_tag): content = resp.content # Assert we see the proper icon in the top display - self.assertIn(' - url = @$list_survey_btn.data 'endpoint' + encoding_utf8 = @$encoding_utf8_chkbox.prop 'checked' + if encoding_utf8 + saveInstructorSurveyEncodingUTF8.call @ , 'true' + url = @$list_survey_btn.data 'endpointUtf8' + else + saveInstructorSurveyEncodingUTF8.call @ , 'false' + url = @$list_survey_btn.data 'endpoint' downloadFileUsingPost url # handler for when the section title is clicked. onClickTitle: -> + @$encoding_utf8_chkbox.prop 'checked' , getInstructorSurveyEncodingUTF8.call @ + + saveInstructorSurveyEncodingUTF8 = (value)-> + if window.localStorage + window.localStorage.setItem 'instructor.survey.encodingUTF8', value + + getInstructorSurveyEncodingUTF8 = -> + if window.localStorage + return window.localStorage.getItem('instructor.survey.encodingUTF8') == 'true' # export for use diff --git a/lms/static/css/gacco-course.css b/lms/static/css/gacco-course.css index c1b078f73290..ed6f787f2a57 100644 --- a/lms/static/css/gacco-course.css +++ b/lms/static/css/gacco-course.css @@ -139,10 +139,6 @@ div.course-wrapper > a[href="#course-accordion-modal"] span i { .xmodule_display.xmodule_SequenceModule .sequence-nav .sequence-list-wrapper { z-index: auto; } -.xmodule_display.xmodule_SequenceModule .sequence-nav .sequence-list-wrapper ol li a:hover, -.xmodule_display.xmodule_SequenceModule .sequence-nav .sequence-list-wrapper ol li a.active { - border-bottom: solid 3px #008cba; -} #course-accordion-modal .course-index { background-color: #f6f6f6; @@ -892,6 +888,7 @@ body.discussion .forum-nav-header > .forum-nav-toggle li:hover { z-index: 1001; } #more-menu > ul > li > a { + background-image: initial; background-color: rgba(0, 0, 0, 0.7); border: 1px solid; color: #fff; @@ -952,7 +949,6 @@ nav.wrapper-course-material ol.course-tabs li .tab-icon { .xmodule_display.xmodule_SequenceModule nav.sequence-bottom button:hover:not(:disabled), .xmodule_display.xmodule_SequenceModule nav.sequence-bottom button:active:not(:disabled), .xmodule_display.xmodule_SequenceModule nav.sequence-bottom button:focus:not(:disabled) { - background-color: #e4e4e4; transition: none; box-shadow: none; } @@ -997,6 +993,10 @@ nav.wrapper-course-material ol.course-tabs li .tab-icon { border-left: 1px solid #ccc; right: 0; } + + .xmodule_display.xmodule_SequenceModule .sequence-nav-button:hover { + background: #cc0033; + } } /* ### course about page */ @@ -1075,21 +1075,6 @@ nav.wrapper-course-material { nav.wrapper-course-material ol.course-tabs { padding: 15px 0 10px; } -nav.wrapper-course-material ol.course-tabs li a { - color: #222; - border-radius: 0; -} -nav.wrapper-course-material ol.course-tabs li a.active { - background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0.5)); -/* background-color: #43AC6A; */ - transition: background-color 300ms ease-out; - color: #fff; - transition-delay: 300ms; -} -nav.wrapper-course-material ol.course-tabs li a.active:hover { - background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.3)); -/* background-color: #368a55; */ -} @media screen and (max-width: 800px) { nav.wrapper-course-material { diff --git a/lms/static/js/dashboard/legacy.js b/lms/static/js/dashboard/legacy.js index 81bb1dacfbca..4e05587fd434 100644 --- a/lms/static/js/dashboard/legacy.js +++ b/lms/static/js/dashboard/legacy.js @@ -119,7 +119,7 @@ $(".action-email-settings").click(function(event) { $("#email_settings_course_id").val( $(event.target).data("course-id") ); - $("#email_settings_course_number").text( $(event.target).data("course-number") ); + $("#email_settings_course_name").text( $(event.target).data("course-name") ); if($(event.target).data("optout") === "False") { $("#receive_emails").prop('checked', true); } diff --git a/lms/static/sass/course/layout/_courseware_header.scss b/lms/static/sass/course/layout/_courseware_header.scss index 4b01542e8cd2..c939e14b01ea 100644 --- a/lms/static/sass/course/layout/_courseware_header.scss +++ b/lms/static/sass/course/layout/_courseware_header.scss @@ -35,8 +35,9 @@ } a { - border-radius: 3px; - color: #555; + background: linear-gradient(to bottom, rgb(255, 255, 255), rgb(228, 228, 228)); + border-radius: 0; + color: #222; display: block; text-align: center; padding: ($baseline/2) 13px 12px; @@ -47,16 +48,21 @@ &:hover, &:focus { color: #333; - background: rgba(255, 255, 255, .6); + background: linear-gradient(to bottom, rgb(255, 244, 247), rgb(255, 207, 219)); } &.active { // background: $shadow; - @include linear-gradient(top, rgba(0, 0, 0, .4), rgba(0, 0, 0, .25)); - background-color: transparent; + background: linear-gradient(to bottom, rgb(255, 0, 64), rgb(167, 0, 42)); box-shadow: 0 1px 0 rgba(255, 255, 255, .5), 0 1px 1px rgba(0, 0, 0, .3) inset; color: $white; text-shadow: 0 1px 0 rgba(0, 0, 0, .4); + transition: background-color 300ms ease-out; + transition-delay: 300ms; + + &:hover, &:focus { + background: linear-gradient(to bottom, rgb(204, 0, 51), rgb(138, 0, 35)); + } } } } diff --git a/lms/static/sass/multicourse/_course_about.scss b/lms/static/sass/multicourse/_course_about.scss index 419dadb2d285..ef60f1a1c126 100644 --- a/lms/static/sass/multicourse/_course_about.scss +++ b/lms/static/sass/multicourse/_course_about.scss @@ -276,7 +276,7 @@ a { border-bottom: 3px solid transparent; - color: $lighter-base-font-color; + color: #000000; display: inline-block; letter-spacing: 1px; margin: 0 15px; diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html index e85fc6d849da..179d6597cf43 100644 --- a/lms/templates/courseware/course_about.html +++ b/lms/templates/courseware/course_about.html @@ -15,6 +15,10 @@ ## (https://developers.facebook.com/docs/opengraph/howtos/maximizing-distribution-media-content#tags) + <%block name="js_extra"> @@ -145,7 +149,7 @@

- ${course.display_number_with_default | h}: ${course.display_name_with_default | h} + ${course.display_name_with_default | h}

@@ -200,7 +204,7 @@

% else: - ${_("[Free of charge] Enroll in {course_name}").format(course_name=course.display_number_with_default) | h} + ${_("[Free of charge] Enroll")} % endif
diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 1658af5e323a..cebb4d3b5e33 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -211,7 +211,7 @@

${course_dir | h}

- ${_("Email Settings for {course_number}").format(course_number='')} + ${_("Email Settings for {course_name}").format(course_name='')} , ## Translators: this text gives status on if the modal interface (a menu or piece of UI that takes the full focus of the screen) is open or not ${_("window open")} diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index e02709683b79..d3a68f800f71 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -288,13 +288,13 @@

% elif enrollment.mode != "verified": % if advanced_course_status.is_purchased(): ${_('Unenroll')} % else: ${_('Unenroll')} @@ -332,7 +332,7 @@

% endif
  • % if show_email_settings: - ${_('Email Settings')} + ${_('Email Settings')} % endif
  • diff --git a/lms/templates/instructor/instructor_dashboard_2/survey.html b/lms/templates/instructor/instructor_dashboard_2/survey.html index 9f31b6cebcf1..b6f4daa8183d 100755 --- a/lms/templates/instructor/instructor_dashboard_2/survey.html +++ b/lms/templates/instructor/instructor_dashboard_2/survey.html @@ -3,6 +3,9 @@

    ${_("Survey Download")}

    -

    ${_("The survey result can be downloaded as a CSV file.")}

    -

    +

    ${_("You can download the survey results.")}

    +

    +
    + +

    diff --git a/lms/templates/manage_user_standing.html b/lms/templates/manage_user_standing.html index 7d515d4399a2..9b6273b2c81b 100644 --- a/lms/templates/manage_user_standing.html +++ b/lms/templates/manage_user_standing.html @@ -133,7 +133,7 @@

    ${_("Choose an action:")}

    var username_or_email = $('#username_or_email', form).val(), action = $("input:radio[name=account_action]:checked", form).val(); if (action === 'disable' - && !confirm("${_('After execution of the personal information mask processing,, it can not be undone. Are you sure?')}")) { + && !confirm("${_('After execution of the personal information mask processing, it can not be undone. Are you sure?')}")) { return false; } clearMessages(); diff --git a/lms/templates/seq_module.html b/lms/templates/seq_module.html index 2d186be58610..3df5fff732d7 100644 --- a/lms/templates/seq_module.html +++ b/lms/templates/seq_module.html @@ -1,9 +1,10 @@ -<%! from django.utils.translation import ugettext as _ %> +<%! from django.conf import settings %> +<%! from django.utils.translation import pgettext, ugettext as _ %>
    @@ -47,10 +48,10 @@
    diff --git a/openedx/core/djangoapps/ga_optional/migrations/0008_ora2_video_upload_option.py b/openedx/core/djangoapps/ga_optional/migrations/0008_ora2_video_upload_option.py new file mode 100644 index 000000000000..7970daf95adf --- /dev/null +++ b/openedx/core/djangoapps/ga_optional/migrations/0008_ora2_video_upload_option.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ga_optional', '0007_progress_restriction'), + ] + + operations = [ + migrations.AlterField( + model_name='courseoptionalconfiguration', + name='key', + field=models.CharField(db_index=True, max_length=100, verbose_name='Feature', choices=[(b'ora2-staff-assessment', 'Staff Assessment for Peer Grading'), (b'ora2-video-upload', 'Video Upload Option for Peer Grading'), (b'custom-logo-for-settings', 'Custom Logo for Settings'), (b'disccusion-image-upload-settings', 'Providing Image Server for Discussion'), (b'library-for-settings', 'Library for Settings'), (b'progress-restriction-settings', 'Progress Restriction by Correct Answer Rate')]), + ), + ] diff --git a/openedx/core/djangoapps/ga_optional/models.py b/openedx/core/djangoapps/ga_optional/models.py index 12b8bff21498..d71942a5de80 100644 --- a/openedx/core/djangoapps/ga_optional/models.py +++ b/openedx/core/djangoapps/ga_optional/models.py @@ -6,12 +6,15 @@ from config_models.models import ConfigurationModel, ConfigurationModelManager from xmodule_django.models import CourseKeyField +ORA2_STAFF_ASSESSMENT_OPTION_KEY = 'ora2-staff-assessment' +ORA2_VIDEO_UPLOAD_OPTION_KEY = 'ora2-video-upload' CUSTOM_LOGO_OPTION_KEY = 'custom-logo-for-settings' DISCCUSION_IMAGE_UPLOAD_KEY = 'disccusion-image-upload-settings' LIBRARY_OPTION_KEY = 'library-for-settings' PROGRESS_RESTRICTION_OPTION_KEY = 'progress-restriction-settings' OPTIONAL_FEATURES = [ - ('ora2-staff-assessment', _("Staff Assessment for Peer Grading")), + (ORA2_STAFF_ASSESSMENT_OPTION_KEY, _("Staff Assessment for Peer Grading")), + (ORA2_VIDEO_UPLOAD_OPTION_KEY, _("Video Upload Option for Peer Grading")), (CUSTOM_LOGO_OPTION_KEY, _("Custom Logo for Settings")), (DISCCUSION_IMAGE_UPLOAD_KEY, _("Providing Image Server for Discussion")), (LIBRARY_OPTION_KEY, _("Library for Settings")), diff --git a/openedx/core/lib/ga_csv_utils.py b/openedx/core/lib/ga_csv_utils.py new file mode 100644 index 000000000000..52bb9be9b9d6 --- /dev/null +++ b/openedx/core/lib/ga_csv_utils.py @@ -0,0 +1,39 @@ +""" +CSV utilities +""" +from cStringIO import StringIO + +import codecs +import unicodecsv as csv +from django.http import HttpResponse + + +class TSVWriter(object): + def __init__(self, f, encoding='utf-16', **kwargs): + self.queue = StringIO() + # Not either encoding='utf-16' or encoding='utf-8-sig' works with unicodecsv + self.writer = csv.writer(self.queue, delimiter='\t', quoting=csv.QUOTE_ALL, **kwargs) + self.stream = f + self.encoder = codecs.getincrementalencoder(encoding)() + + def writerow(self, row): + self.writer.writerow([unicode(s).encode('utf-8') for s in row]) + data = self.queue.getvalue() + data = data.decode('utf-8') + data = self.encoder.encode(data) + self.stream.write(data) + self.queue.truncate(0) + + def writerows(self, rows): + for row in rows: + self.writerow(row) + + +def create_tsv_response(filename, header, datarows, encoding): + """Returns a TSV response for the given header and rows.""" + response = HttpResponse(content_type='text/tab-separated-values') + response['Content-Disposition'] = 'attachment; filename={0}'.format(filename) + writer = TSVWriter(response, encoding) + writer.writerow(header) + writer.writerows(datarows) + return response diff --git a/scripts/check_playback_log.sh b/scripts/check_playback_log.sh new file mode 100755 index 000000000000..4520065deb3d --- /dev/null +++ b/scripts/check_playback_log.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +cd /edx/app/edxapp/edx-platform +source ../venvs/edxapp/bin/activate +python manage.py lms --settings=aws check_playback_log