diff --git a/lms/templates/admin/external_user_ids/generate_external_ids_form.html b/lms/templates/admin/external_user_ids/generate_external_ids_form.html
new file mode 100644
index 0000000000..23533bef52
--- /dev/null
+++ b/lms/templates/admin/external_user_ids/generate_external_ids_form.html
@@ -0,0 +1,21 @@
+{% extends 'admin/base.html' %}
+
+{% block content %}
+
+
Generate External IDs in bulk
+
+ This form will generate external IDs for a set of Users based on the
+ provided CSV file. The file should contain a header labeled "ID" and then
+ a list of numeric IDs, one row per ID.
+
+
+
+
+
+{% endblock %}
diff --git a/lms/templates/admin/external_user_ids/generate_external_user_ids.html b/lms/templates/admin/external_user_ids/generate_external_user_ids.html
new file mode 100644
index 0000000000..25fef566ae
--- /dev/null
+++ b/lms/templates/admin/external_user_ids/generate_external_user_ids.html
@@ -0,0 +1,6 @@
+{% extends 'admin/change_list.html' %}
+
+{% block object-tools-items %}
+ Bulk Generate External IDs
+ {{ block.super }}
+{% endblock %}
diff --git a/openedx/core/djangoapps/external_user_ids/admin.py b/openedx/core/djangoapps/external_user_ids/admin.py
new file mode 100644
index 0000000000..749379a213
--- /dev/null
+++ b/openedx/core/djangoapps/external_user_ids/admin.py
@@ -0,0 +1,129 @@
+import csv
+from logging import getLogger
+
+from django import forms
+from django.conf.urls import url
+from django.contrib import admin, messages
+from django.contrib.auth import get_user_model
+from django.http import HttpResponseRedirect
+from django.shortcuts import render
+from django.urls import reverse
+
+from .models import ExternalId, ExternalIdType
+
+User = get_user_model()
+
+logger = getLogger(__name__)
+
+
+class CsvImportForm(forms.Form):
+ csv_file = forms.FileField(label='CSV File')
+ id_type = forms.ModelChoiceField(
+ label='External ID Type',
+ queryset=ExternalIdType.objects.all()
+ )
+
+
+@admin.register(ExternalId)
+class ExternalIdAdmin(admin.ModelAdmin):
+ change_list_template = 'admin/external_user_ids/generate_external_user_ids.html'
+ list_display = ('user', 'external_user_id', 'external_id_type')
+ template = 'openedx/core/djangoapps/external_user_ids/templates/admin/generate_external_ids_template.html'
+
+ def get_urls(self):
+ urls = super(ExternalIdAdmin, self).get_urls()
+ custom_urls = [
+ url(
+ r'^bulk_generate_external_ids/$',
+ self.admin_site.admin_view(self.generate_ids_form),
+ name='bulk_generate_external_ids'
+ ),
+ ]
+ return custom_urls + urls
+
+ def _generate_results_msg(self, user_id_list, unknown_users, created_id_list, existing_id):
+ return (
+ 'Attempted to create for: {}\n'.format(user_id_list) +
+ 'Could not find: {}\n'.format(unknown_users) +
+ 'Created External IDs for: {}\n'.format(created_id_list) +
+ 'External IDs already exist for: {}\n'.format(existing_id)
+ )
+
+ def process_generate_ids_request(self, user_id_list, id_type, request, redirect_url):
+ created_id_list = []
+ existing_id = []
+
+ user_list = User.objects.filter(
+ id__in=user_id_list
+ )
+ for user in user_list:
+ new_external_id, created = ExternalId.objects.get_or_create(
+ user=user,
+ external_id_type=id_type,
+ )
+ if created:
+ created_id_list.append(user.id)
+ else:
+ existing_id.append(user.id)
+ found_user_ids = created_id_list + existing_id
+ unknown_users = list(set(user_id_list) - set(found_user_ids))
+ result_msg = self._generate_results_msg(user_id_list, unknown_users, created_id_list, existing_id)
+ logger.info(result_msg)
+ self.message_user(
+ request,
+ result_msg,
+ level=messages.SUCCESS)
+ return HttpResponseRedirect(redirect_url)
+
+ def _render_form(self, request, form):
+ context = {
+ 'form': form
+ }
+ return render(
+ request,
+ 'admin/external_user_ids/generate_external_ids_form.html',
+ context
+ )
+
+ def generate_ids_form(self, request):
+ if request.method == 'POST':
+ redirect_url = reverse(
+ 'admin:external_user_ids_externalid_changelist',
+ current_app=self.admin_site.name,
+ )
+ upload_file = request.FILES.get('csv_file')
+ id_type = request.POST.get('id_type')
+
+ if not upload_file or not id_type:
+ self.message_user(request, 'CSV file and type are required.', level=messages.ERROR)
+ return HttpResponseRedirect(redirect_url)
+
+ try:
+ id_type = ExternalIdType.objects.get(id=id_type)
+ except ExternalIdType.DoesNotExist:
+ self.message_user(request, 'ID Type selected does not exist', level=messages.ERROR)
+ return HttpResponseRedirect(redirect_url)
+
+ reader = csv.reader(upload_file.read().decode('utf-8').splitlines())
+ headers = next(reader, None)
+ if len(headers) != 1 or 'ID' not in headers:
+ self.message_user(
+ request,
+ 'File is incorrectly formatted. To many columns or incorrectly named ID column',
+ level=messages.ERROR
+ )
+ return HttpResponseRedirect(redirect_url)
+ try:
+ user_ids = [int(row[0]) for row in reader]
+ except ValueError:
+ self.message_user(
+ request,
+ 'Data is incorrectly formatted. All ids must be integers',
+ level=messages.ERROR
+ )
+ return HttpResponseRedirect(redirect_url)
+
+ return self.process_generate_ids_request(user_ids, id_type, request, redirect_url)
+
+ form = CsvImportForm()
+ return self._render_form(request, form)
diff --git a/openedx/core/djangoapps/external_user_ids/models.py b/openedx/core/djangoapps/external_user_ids/models.py
index 3bf0980c85..708385394d 100644
--- a/openedx/core/djangoapps/external_user_ids/models.py
+++ b/openedx/core/djangoapps/external_user_ids/models.py
@@ -11,7 +11,6 @@ from django.db import models
from model_utils.models import TimeStampedModel
from simple_history.models import HistoricalRecords
-
LOGGER = getLogger(__name__)
@@ -28,6 +27,9 @@ class ExternalIdType(TimeStampedModel):
description = models.TextField()
history = HistoricalRecords()
+ def __str__(self):
+ return self.name
+
class ExternalId(TimeStampedModel):
"""
diff --git a/openedx/core/djangoapps/external_user_ids/tests/factories.py b/openedx/core/djangoapps/external_user_ids/tests/factories.py
new file mode 100644
index 0000000000..1cd9cc93fc
--- /dev/null
+++ b/openedx/core/djangoapps/external_user_ids/tests/factories.py
@@ -0,0 +1,26 @@
+"""
+Test Factory classes for ExternalUserIds
+"""
+
+from uuid import uuid4
+
+import factory
+from factory.fuzzy import FuzzyChoice, FuzzyText
+
+from openedx.core.djangoapps.external_user_ids.models import ExternalId, ExternalIdType
+
+
+class ExternalIDTypeFactory(factory.django.DjangoModelFactory):
+ class Meta(object):
+ model = ExternalIdType
+
+ name = FuzzyChoice([ExternalIdType.MICROBACHELORS_COACHING])
+ description = FuzzyText()
+
+
+class ExternalIdFactory(factory.django.DjangoModelFactory):
+ class Meta(object):
+ model = ExternalId
+
+ external_user_id = factory.LazyFunction(uuid4)
+ external_id_type = factory.SubFactory(ExternalIDTypeFactory)
diff --git a/openedx/core/djangoapps/external_user_ids/tests/test_admin_generate_id.py b/openedx/core/djangoapps/external_user_ids/tests/test_admin_generate_id.py
new file mode 100644
index 0000000000..fd48a8292b
--- /dev/null
+++ b/openedx/core/djangoapps/external_user_ids/tests/test_admin_generate_id.py
@@ -0,0 +1,62 @@
+# """
+# Test the logic behind the Generate External IDs tools in Admin
+# """
+import mock
+from django.conf import settings
+from django.contrib.admin.sites import AdminSite
+from django.test import TestCase
+from student.tests.factories import UserFactory
+
+from openedx.core.djangolib.testing.utils import skip_unless_lms
+
+# external_ids is not in CMS' INSTALLED_APPS so these imports will error during test collection
+if settings.ROOT_URLCONF == 'lms.urls':
+ from openedx.core.djangoapps.external_user_ids.models import (
+ ExternalId,
+ )
+ from openedx.core.djangoapps.external_user_ids.tests.factories import ExternalIDTypeFactory
+ from openedx.core.djangoapps.external_user_ids.admin import ExternalIdAdmin
+
+
+@skip_unless_lms
+class TestGenerateExternalIds(TestCase):
+ """
+ Test generating ExternalIDs for Users.
+ """
+ def setUp(self):
+ super(TestGenerateExternalIds, self).setUp()
+ self.users = UserFactory.create_batch(10)
+ self.user_id_list = [user.id for user in self.users]
+ self.external_id_admin = ExternalIdAdmin(ExternalId, AdminSite())
+
+ def test_generate_ids_for_all_users(self):
+ id_type = ExternalIDTypeFactory.create()
+ request = mock.Mock()
+
+ assert ExternalId.objects.count() == 0
+ self.external_id_admin.process_generate_ids_request(
+ user_id_list=self.user_id_list,
+ id_type=id_type,
+ request=request,
+ redirect_url=''
+ )
+ assert ExternalId.objects.count() == 10
+
+ def test_no_new_for_existing_users(self):
+ id_type = ExternalIDTypeFactory.create()
+ request = mock.Mock()
+
+ for user in self.users:
+ ExternalId.objects.create(
+ user=user,
+ external_id_type=id_type
+ )
+
+ assert ExternalId.objects.count() == 10
+ self.external_id_admin.process_generate_ids_request(
+ user_id_list=self.user_id_list,
+ id_type=id_type,
+ request=request,
+ redirect_url=''
+ )
+ assert ExternalId.objects.count() == 10