diff --git a/lms/djangoapps/onboarding/constants.py b/lms/djangoapps/onboarding/constants.py index 1c826e81fdf..891f2879a2d 100644 --- a/lms/djangoapps/onboarding/constants.py +++ b/lms/djangoapps/onboarding/constants.py @@ -9,3 +9,4 @@ NOT_INTERESTED_KEY = 'NT' NOT_INTERESTED_VAL = "No Thanks, I'm Not Interested" +ORG_SEARCH_TERM_LENGTH = 2 diff --git a/lms/djangoapps/onboarding/helpers.py b/lms/djangoapps/onboarding/helpers.py index 84ce8686cbd..a856a10571f 100644 --- a/lms/djangoapps/onboarding/helpers.py +++ b/lms/djangoapps/onboarding/helpers.py @@ -13,6 +13,7 @@ from mailchimp_pipeline.signals.handlers import update_user_email_in_mailchimp from nodebb.tasks import task_update_user_profile_on_nodebb from oef.models import OrganizationOefUpdatePrompt +from lms.djangoapps.onboarding.constants import ORG_SEARCH_TERM_LENGTH from lms.djangoapps.onboarding.models import ( Organization, OrganizationMetricUpdatePrompt, PartnerNetwork, OrganizationAdminHashKeys ) @@ -7864,6 +7865,8 @@ def get_close_matching_orgs_with_suggestions(request, query): data = {} organizations = Organization.objects.filter(label__istartswith=query) + if len(query) == ORG_SEARCH_TERM_LENGTH: + organizations = organizations.filter(label__length=ORG_SEARCH_TERM_LENGTH) for organization in organizations: match_ratio = get_str_match_ratio(query.lower(), organization.label.lower()) is_suggestion = True if re.match(query, organization.label, re.I) else False @@ -8022,6 +8025,7 @@ def serialize_partner_networks(): return data + def get_user_on_demand_courses(user): """ Return user on demand courses diff --git a/lms/envs/common.py b/lms/envs/common.py index 4f097f1d3d3..a45bf8dabca 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3564,3 +3564,4 @@ def _make_locale_paths(settings): # CDN LINK CDN_LINK = "https://static.philanthropyu.org/" + diff --git a/lms/urls.py b/lms/urls.py index 11a52f39cfc..d47535bcad1 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -98,6 +98,16 @@ url(r'^marketplace/', include('openedx.features.marketplace.urls')), url(r'^idea/', include('openedx.features.idea.urls')), + # temporary experiment urls block start. + url(r'^organizations/', TemplateView.as_view(template_name='main_django.html')), + url(r'^learners/', TemplateView.as_view(template_name='main_django.html')), + url(r'^organizational-capacity-assessment/', + TemplateView.as_view(template_name='main_django.html')), + url(r'^course-development/', TemplateView.as_view(template_name='main_django.html')), + url(r'^organization-onboarding/', + TemplateView.as_view(template_name='main_django.html')), + # temporary experiment urls block End. + # CloudSponge proxy url to lets the OAuth providers know, we are asking CloudSponge for address book url(r'^auth/proxy/cloudsponge/$', TemplateView.as_view(template_name='features/smart_referral/auth_proxy.html'), name='cloud_sponge_proxy'), diff --git a/openedx/features/__init__.py b/openedx/features/__init__.py index e69de29bb2d..b8f83f09c1d 100644 --- a/openedx/features/__init__.py +++ b/openedx/features/__init__.py @@ -0,0 +1 @@ +from openedx.features.lookups import * diff --git a/openedx/features/course_card/helpers.py b/openedx/features/course_card/helpers.py index 2f729a10e0c..136f3e83692 100644 --- a/openedx/features/course_card/helpers.py +++ b/openedx/features/course_card/helpers.py @@ -1,13 +1,16 @@ from datetime import datetime - -import pytz from logging import getLogger +import pytz from crum import get_current_request -from custom_settings.models import CustomSettings + from course_action_state.models import CourseRerunState +from custom_settings.models import CustomSettings +from openedx.core.djangoapps.catalog.utils import get_programs from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from openedx.core.djangoapps.theming.helpers import get_current_request from openedx.features.course_card.models import CourseCard + log = getLogger(__name__) @@ -96,3 +99,20 @@ def get_course_cards_list(): course_card_ids = [cc.course_id for cc in cards_query_set] courses_list = CourseOverview.objects.select_related('image_set').filter(id__in=course_card_ids) return courses_list + + +def is_course_in_programs(course_id): + """ + Helper function to check if course is part of program + @param course_id: course key + @return: true if course is part of program otherwise false + """ + programs = get_programs(get_current_request().site) + + for program in programs: + for program_course in program['courses']: + if program_course['course_runs']: + for course_rerun in program_course['course_runs']: + if course_rerun['key'] == str(course_id): + return True + return False diff --git a/openedx/features/job_board/constants.py b/openedx/features/job_board/constants.py index 51f023ac975..d3f653250f1 100644 --- a/openedx/features/job_board/constants.py +++ b/openedx/features/job_board/constants.py @@ -1,6 +1,7 @@ JOB_PARAM_QUERY_KEY = 'query' JOB_PARAM_COUNTRY_KEY = 'country' JOB_PARAM_CITY_KEY = 'city' +JOB_PARAM_TRUE_VALUE = '1' JOB_TYPE_REMOTE_KEY = 'remote' JOB_TYPE_ONSITE_KEY = 'onsite' diff --git a/openedx/features/job_board/tests/views/test_job_board.py b/openedx/features/job_board/tests/views/test_job_board.py index 9ac3cfc868c..12cb855e6e1 100644 --- a/openedx/features/job_board/tests/views/test_job_board.py +++ b/openedx/features/job_board/tests/views/test_job_board.py @@ -1,13 +1,31 @@ +from ddt import data, ddt, unpack +from rest_framework import status +from w3lib.url import add_or_replace_parameters + from django.test import TestCase from django.urls import reverse -from rest_framework import status from openedx.core.djangolib.testing.philu_utils import configure_philu_theme +from openedx.features.job_board.constants import ( + JOB_COMP_HOURLY_KEY, + JOB_COMP_SALARIED_KEY, + JOB_COMP_VOLUNTEER_KEY, + JOB_HOURS_FREELANCE_KEY, + JOB_HOURS_FULLTIME_KEY, + JOB_HOURS_PARTTIME_KEY, + JOB_PARAM_CITY_KEY, + JOB_PARAM_COUNTRY_KEY, + JOB_PARAM_QUERY_KEY, + JOB_PARAM_TRUE_VALUE, + JOB_TYPE_ONSITE_KEY, + JOB_TYPE_REMOTE_KEY +) from openedx.features.job_board.models import Job from openedx.features.job_board.tests.factories import JobFactory from openedx.features.job_board.views import JobCreateView, JobListView +@ddt class JobBoardViewTest(TestCase): def setUp(self): @@ -21,7 +39,7 @@ def setUpClass(cls): def test_job_create_view(self): response = self.client.get(reverse('job_create')) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(JobCreateView.fields, '__all__') + self.assertEqual(JobCreateView.form_class.Meta.fields, '__all__') def test_create_view_post_successful(self): self.client.post('/jobs/create/', {"function": "dummy function", @@ -69,3 +87,116 @@ def test_job_list_view(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(JobListView.paginate_by, 10) self.assertEqual(JobListView.ordering, ['-created']) + + @unpack + @data(('', 10), ('?page=2', 4)) + def test_job_list_view_pagination(self, page_query_param, job_list_size): + # Create multiple new jobs to test pagination. + JobFactory.create_batch(14) + response_second_page = self.client.get(reverse('job_list') + page_query_param) + self.assertEqual(response_second_page.status_code, status.HTTP_200_OK) + self.assertTrue('is_paginated' in response_second_page.context_data) + self.assertTrue(response_second_page.context_data['is_paginated'] is True) + self.assertTrue(len(response_second_page.context_data['job_list']) == job_list_size) + + @data(JOB_TYPE_REMOTE_KEY, JOB_TYPE_ONSITE_KEY) + def test_job_list_view_filters_job_type(self, job_type): + # Create a new job with `type=job_type` to search for. + # And another job with `type="test"` to see it's not fetched. + JobFactory(type=job_type) + JobFactory(type="test") + + query_params = {job_type: JOB_PARAM_TRUE_VALUE} + response = self.client.get(add_or_replace_parameters(reverse('job_list'), query_params)) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(response.context_data['search_fields'][job_type], JOB_PARAM_TRUE_VALUE) + + # ensure that no other filter was applied + del [response.context_data['search_fields'][job_type]] + self.assertTrue(not any(response.context_data['search_fields'].values())) + + self.assertTrue(response.context_data['filtered'], True) + self.assertTrue(len(response.context_data['job_list']) == 1) + self.assertEqual(response.context_data['job_list'].first().type, job_type) + + @data(JOB_COMP_VOLUNTEER_KEY, JOB_COMP_HOURLY_KEY, JOB_COMP_SALARIED_KEY) + def test_job_list_view_filters_job_compensation(self, job_comp): + # Create a new job with `compensation=job_comp` to search for. + # And another job with `compensation="test"` to see it's not fetched. + JobFactory(compensation=job_comp) + JobFactory(compensation="test") + + query_params = {job_comp: JOB_PARAM_TRUE_VALUE} + response = self.client.get(add_or_replace_parameters(reverse('job_list'), query_params)) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(response.context_data['search_fields'][job_comp], JOB_PARAM_TRUE_VALUE) + + # ensure that no other filter was applied + del [response.context_data['search_fields'][job_comp]] + self.assertTrue(not any(response.context_data['search_fields'].values())) + + self.assertTrue(response.context_data['filtered'], True) + self.assertTrue(len(response.context_data['job_list']) == 1) + self.assertEqual(response.context_data['job_list'].first().compensation, job_comp) + + @data(JOB_HOURS_FULLTIME_KEY, JOB_HOURS_PARTTIME_KEY, JOB_HOURS_FREELANCE_KEY) + def test_job_list_view_filters_job_hours(self, job_hours): + # Create a new job with `hours=job_hours` to search for. + # And another job with `hours="test"` to see it's not fetched. + JobFactory(hours=job_hours) + JobFactory(hours="test") + + query_params = {job_hours: JOB_PARAM_TRUE_VALUE} + response = self.client.get(add_or_replace_parameters(reverse('job_list'), query_params)) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(response.context_data['search_fields'][job_hours], JOB_PARAM_TRUE_VALUE) + + # ensure that no other filter was applied + del [response.context_data['search_fields'][job_hours]] + self.assertTrue(not any(response.context_data['search_fields'].values())) + + self.assertTrue(response.context_data['filtered'], True) + self.assertTrue(len(response.context_data['job_list']) == 1) + self.assertEqual(response.context_data['job_list'].first().hours, job_hours) + + def test_job_list_view_filters_job_location(self): + # Create a new job with custom location to search for. + job = JobFactory(country='PK', city='Karachi') + + query_params = {JOB_PARAM_CITY_KEY: job.city, JOB_PARAM_COUNTRY_KEY: job.country.name} + response = self.client.get(add_or_replace_parameters(reverse('job_list'), query_params)) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(response.context_data['search_fields']['country'], job.country.name) + self.assertTrue(response.context_data['search_fields']['city'], job.city) + + # ensure that no other filter was applied + del [response.context_data['search_fields']['country']] + del [response.context_data['search_fields']['city']] + self.assertTrue(not any(response.context_data['search_fields'].values())) + + self.assertTrue(response.context_data['filtered'], True) + self.assertTrue(len(response.context_data['job_list']) == 1) + self.assertTrue(response.context_data['job_list'][0].country.name == job.country.name) + self.assertTrue(response.context_data['job_list'][0].city == job.city) + + def test_job_list_view_filters_job_query(self): + # Create a new job with custom title to search for. + job = JobFactory(title='custom_job') + + query_params = {JOB_PARAM_QUERY_KEY: job.title} + response = self.client.get(add_or_replace_parameters(reverse('job_list'), query_params)) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(response.context_data['search_fields']['query'], job.title) + + # ensure that no other filter was applied + del [response.context_data['search_fields']['query']] + self.assertTrue(not any(response.context_data['search_fields'].values())) + + self.assertTrue(response.context_data['filtered'], True) + self.assertTrue(len(response.context_data['job_list']) == 1) + self.assertTrue(job.title in response.context_data['job_list'][0].title) diff --git a/openedx/features/lookups.py b/openedx/features/lookups.py new file mode 100644 index 00000000000..7e21bf330cf --- /dev/null +++ b/openedx/features/lookups.py @@ -0,0 +1,3 @@ +from django.db import models + +models.CharField.register_lookup(models.functions.Length, 'length') diff --git a/openedx/features/partners/forms.py b/openedx/features/partners/forms.py index c73871ef166..e78c0ac42b3 100644 --- a/openedx/features/partners/forms.py +++ b/openedx/features/partners/forms.py @@ -114,11 +114,11 @@ def clean_registration_data(self, registration_data): registration_data.update(self.cleaned_data) def clean_country(self): - country = self.cleaned_data['country'] - cleaned_country = next((code for code, label in COUNTRIES.items() if label.lower() == country.lower()), None) - if cleaned_country: - return cleaned_country - raise ValidationError(_('Please select country.')) + country_code = self.cleaned_data['country'] + if country_code in COUNTRIES.keys(): + return country_code + else: + raise ValidationError(_('The given country is invalid.') if country_code else _('Please select country.')) username = UsernameField() password = forms.CharField() diff --git a/openedx/features/partners/views.py b/openedx/features/partners/views.py index f62c2ccde99..206a5b0f8ec 100644 --- a/openedx/features/partners/views.py +++ b/openedx/features/partners/views.py @@ -3,7 +3,6 @@ from datetime import datetime from logging import getLogger -import analytics from django.conf import settings from django.contrib.auth import authenticate, login from django.contrib.auth.models import User @@ -14,29 +13,29 @@ from django.utils.translation import get_language from django.views.decorators.debug import sensitive_post_parameters from django.views.decorators.http import require_http_methods -from edxmako.shortcuts import render_to_response -from eventtracking import tracker -from notification_prefs.views import enable_notifications from pytz import UTC -from rest_framework import status -from openedx.core.djangoapps.user_api.views import RegistrationView +import analytics +from edxmako.shortcuts import render_to_response +from eventtracking import tracker from lms.djangoapps.onboarding.models import EmailPreference, Organization, PartnerNetwork, UserExtendedProfile -from nodebb.helpers import update_nodebb_for_user_status, set_user_activation_status_on_nodebb +from nodebb.helpers import set_user_activation_status_on_nodebb, update_nodebb_for_user_status +from notification_prefs.views import enable_notifications from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY from openedx.core.djangoapps.user_api.preferences import api as preferences_api +from openedx.core.djangoapps.user_api.views import RegistrationView from openedx.core.djangoapps.user_authn.cookies import set_logged_in_cookies from openedx.core.djangoapps.user_authn.views.register import REGISTER_USER, record_registration_attributions from openedx.features.partners.helpers import auto_join_partner_community, get_partner_recommended_courses from openedx.features.partners.models import PartnerUser -from openedx.features.student_account.helpers import save_user_utm_info +from openedx.features.student_account.helpers import save_user_utm_info, get_registration_countries from philu_overrides.user_api.views import LoginSessionViewCustom +from rest_framework import status from student.models import Registration, UserProfile from student.views import password_change_request_handler from . import constants as partner_constants -from .forms import PartnerAccountCreationForm -from .forms import PartnerResetPasswordForm +from .forms import PartnerAccountCreationForm, PartnerResetPasswordForm from .helpers import import_form_using_slug, user_has_performance_access from .models import Partner @@ -47,8 +46,15 @@ def dashboard(request, slug): partner = get_object_or_404(Partner, slug=slug) courses = get_partner_recommended_courses(partner.slug, request.user) - return render_to_response('features/partners/dashboard.html', {'recommended_courses': courses, - 'slug': partner.slug, 'partner': partner}) + registration_countries = get_registration_countries() + + context = { + 'recommended_courses': courses, + 'slug': partner.slug, 'partner': partner, + 'registration_countries': registration_countries, + } + + return render_to_response('features/partners/dashboard.html', context) def performance_dashboard(request, slug): @@ -359,4 +365,3 @@ def post(self, request, partner): user=user.username, partner=partner.slug, exp=str(ex)) ) return response - diff --git a/openedx/features/student_account/constants.py b/openedx/features/student_account/constants.py index b44cf1245ee..a95d0933080 100644 --- a/openedx/features/student_account/constants.py +++ b/openedx/features/student_account/constants.py @@ -1,2 +1,10 @@ NON_ACTIVE_COURSE_NOTIFICATION = 'The course you enrolled in, %s opened last week but you can start it right now. Click here to get started.' + +TOP_REGISTRATION_COUNTRIES = ( + ('NG', 'Nigeria'), + ('IN', 'India'), + ('PK', 'Pakistan'), + ('KE', 'Kenya'), + ('GH', 'Ghana'), +) diff --git a/openedx/features/student_account/helpers.py b/openedx/features/student_account/helpers.py index 66799bc15ce..16b6a45248d 100644 --- a/openedx/features/student_account/helpers.py +++ b/openedx/features/student_account/helpers.py @@ -1,4 +1,5 @@ import logging +import operator from datetime import datetime from pytz import utc @@ -7,7 +8,7 @@ from mailchimp_pipeline.signals.handlers import task_send_account_activation_email -from constants import NON_ACTIVE_COURSE_NOTIFICATION +from constants import NON_ACTIVE_COURSE_NOTIFICATION, TOP_REGISTRATION_COUNTRIES from student.models import CourseEnrollment from courseware.models import StudentModule @@ -16,6 +17,7 @@ from openedx.core.djangoapps.timed_notification.core import get_course_first_chapter_link from openedx.core.lib.request_utils import safe_get_host +from lms.djangoapps.onboarding.helpers import COUNTRIES from lms.djangoapps.onboarding.models import EmailPreference, Organization, UserExtendedProfile from lms.djangoapps.philu_overrides.constants import ACTIVATION_ALERT_TYPE @@ -163,3 +165,10 @@ def check_and_add_third_party_params(request, params): params['provider'] = running_pipeline.get('backend') params['access_token'] = running_pipeline['kwargs']['response']['access_token'] + + +def get_registration_countries(): + return { + 'all_countries': sorted(COUNTRIES.items(), key=operator.itemgetter(1)), + 'top_countries': TOP_REGISTRATION_COUNTRIES or [] + }