Merge pull request #922 from MITx/feature/rocha/dashboard-software-licenses
Retrieve and assign course software licenses
This commit is contained in:
0
lms/djangoapps/licenses/__init__.py
Normal file
0
lms/djangoapps/licenses/__init__.py
Normal file
0
lms/djangoapps/licenses/management/__init__.py
Normal file
0
lms/djangoapps/licenses/management/__init__.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import os.path
|
||||
from uuid import uuid4
|
||||
from optparse import make_option
|
||||
|
||||
from django.utils.html import escape
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from licenses.models import CourseSoftware, UserLicense
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = """Generate random serial numbers for software used in a course.
|
||||
|
||||
Usage: generate_serial_numbers <course_id> <software_name> <count>
|
||||
|
||||
<count> is the number of numbers to generate.
|
||||
|
||||
Example:
|
||||
|
||||
import_serial_numbers MITx/6.002x/2012_Fall matlab 100
|
||||
|
||||
"""
|
||||
args = "course_id software_id count"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
"""
|
||||
course_id, software_name, count = self._parse_arguments(args)
|
||||
|
||||
software, _ = CourseSoftware.objects.get_or_create(course_id=course_id,
|
||||
name=software_name)
|
||||
self._generate_serials(software, count)
|
||||
|
||||
def _parse_arguments(self, args):
|
||||
if len(args) != 3:
|
||||
raise CommandError("Incorrect number of arguments")
|
||||
|
||||
course_id = args[0]
|
||||
courses = modulestore().get_courses()
|
||||
known_course_ids = set(c.id for c in courses)
|
||||
|
||||
if course_id not in known_course_ids:
|
||||
raise CommandError("Unknown course_id")
|
||||
|
||||
software_name = escape(args[1].lower())
|
||||
|
||||
try:
|
||||
count = int(args[2])
|
||||
except ValueError:
|
||||
raise CommandError("Invalid <count> argument.")
|
||||
|
||||
return course_id, software_name, count
|
||||
|
||||
def _generate_serials(self, software, count):
|
||||
print "Generating {0} serials".format(count)
|
||||
|
||||
# add serial numbers them to the database
|
||||
for _ in xrange(count):
|
||||
serial = str(uuid4())
|
||||
license = UserLicense(software=software, serial=serial)
|
||||
license.save()
|
||||
|
||||
print "{0} new serial numbers generated.".format(count)
|
||||
@@ -0,0 +1,70 @@
|
||||
import os.path
|
||||
from optparse import make_option
|
||||
|
||||
from django.utils.html import escape
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from licenses.models import CourseSoftware, UserLicense
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = """Imports serial numbers for software used in a course.
|
||||
|
||||
Usage: import_serial_numbers <course_id> <software_name> <file>
|
||||
|
||||
<file> is a text file that list one available serial number per line.
|
||||
|
||||
Example:
|
||||
|
||||
import_serial_numbers MITx/6.002x/2012_Fall matlab serials.txt
|
||||
|
||||
"""
|
||||
args = "course_id software_id serial_file"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
"""
|
||||
course_id, software_name, filename = self._parse_arguments(args)
|
||||
|
||||
software, _ = CourseSoftware.objects.get_or_create(course_id=course_id,
|
||||
name=software_name)
|
||||
self._import_serials(software, filename)
|
||||
|
||||
def _parse_arguments(self, args):
|
||||
if len(args) != 3:
|
||||
raise CommandError("Incorrect number of arguments")
|
||||
|
||||
course_id = args[0]
|
||||
courses = modulestore().get_courses()
|
||||
known_course_ids = set(c.id for c in courses)
|
||||
|
||||
if course_id not in known_course_ids:
|
||||
raise CommandError("Unknown course_id")
|
||||
|
||||
software_name = escape(args[1].lower())
|
||||
|
||||
filename = os.path.abspath(args[2])
|
||||
if not os.path.exists(filename):
|
||||
raise CommandError("Cannot find filename {0}".format(filename))
|
||||
|
||||
return course_id, software_name, filename
|
||||
|
||||
def _import_serials(self, software, filename):
|
||||
print "Importing serial numbers for {0}.".format(software)
|
||||
|
||||
serials = set(unicode(l.strip()) for l in open(filename))
|
||||
|
||||
# remove serial numbers we already have
|
||||
licenses = UserLicense.objects.filter(software=software)
|
||||
known_serials = set(l.serial for l in licenses)
|
||||
if known_serials:
|
||||
serials = serials.difference(known_serials)
|
||||
|
||||
# add serial numbers them to the database
|
||||
for serial in serials:
|
||||
license = UserLicense(software=software, serial=serial)
|
||||
license.save()
|
||||
|
||||
print "{0} new serial numbers imported.".format(len(serials))
|
||||
78
lms/djangoapps/licenses/models.py
Normal file
78
lms/djangoapps/licenses/models.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import logging
|
||||
|
||||
from django.db import models, transaction
|
||||
|
||||
from student.models import User
|
||||
|
||||
log = logging.getLogger("mitx.licenses")
|
||||
|
||||
|
||||
class CourseSoftware(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
full_name = models.CharField(max_length=255)
|
||||
url = models.CharField(max_length=255)
|
||||
course_id = models.CharField(max_length=255)
|
||||
|
||||
def __unicode__(self):
|
||||
return u'{0} for {1}'.format(self.name, self.course_id)
|
||||
|
||||
|
||||
class UserLicense(models.Model):
|
||||
software = models.ForeignKey(CourseSoftware, db_index=True)
|
||||
user = models.ForeignKey(User, null=True)
|
||||
serial = models.CharField(max_length=255)
|
||||
|
||||
|
||||
def get_courses_licenses(user, courses):
|
||||
course_ids = set(course.id for course in courses)
|
||||
all_software = CourseSoftware.objects.filter(course_id__in=course_ids)
|
||||
|
||||
assigned_licenses = UserLicense.objects.filter(software__in=all_software,
|
||||
user=user)
|
||||
|
||||
licenses = dict.fromkeys(all_software, None)
|
||||
for license in assigned_licenses:
|
||||
licenses[license.software] = license
|
||||
|
||||
log.info(assigned_licenses)
|
||||
log.info(licenses)
|
||||
|
||||
return licenses
|
||||
|
||||
|
||||
def get_license(user, software):
|
||||
try:
|
||||
license = UserLicense.objects.get(user=user, software=software)
|
||||
except UserLicense.DoesNotExist:
|
||||
license = None
|
||||
|
||||
return license
|
||||
|
||||
|
||||
def get_or_create_license(user, software):
|
||||
license = get_license(user, software)
|
||||
if license is None:
|
||||
license = _create_license(user, software)
|
||||
|
||||
return license
|
||||
|
||||
|
||||
def _create_license(user, software):
|
||||
license = None
|
||||
|
||||
try:
|
||||
# find one license that has not been assigned, locking the
|
||||
# table/rows with select_for_update to prevent race conditions
|
||||
with transaction.commit_on_success():
|
||||
selected = UserLicense.objects.select_for_update()
|
||||
license = selected.filter(user__isnull=True, software=software)[0]
|
||||
license.user = user
|
||||
license.save()
|
||||
except IndexError:
|
||||
# there are no free licenses
|
||||
log.error('No serial numbers available for {0}', software)
|
||||
license = None
|
||||
# TODO [rocha]look if someone has unenrolled from the class
|
||||
# and already has a serial number
|
||||
|
||||
return license
|
||||
85
lms/djangoapps/licenses/tests.py
Normal file
85
lms/djangoapps/licenses/tests.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
from random import shuffle
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core.management import call_command
|
||||
|
||||
from models import CourseSoftware, UserLicense
|
||||
|
||||
COURSE_1 = 'MITx/6.002x/2012_Fall'
|
||||
|
||||
SOFTWARE_1 = 'matlab'
|
||||
SOFTWARE_2 = 'stata'
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CommandTest(TestCase):
|
||||
def test_import_serial_numbers(self):
|
||||
size = 20
|
||||
|
||||
log.debug('Adding one set of serials for {0}'.format(SOFTWARE_1))
|
||||
with generate_serials_file(size) as temp_file:
|
||||
args = [COURSE_1, SOFTWARE_1, temp_file.name]
|
||||
call_command('import_serial_numbers', *args)
|
||||
|
||||
log.debug('Adding one set of serials for {0}'.format(SOFTWARE_2))
|
||||
with generate_serials_file(size) as temp_file:
|
||||
args = [COURSE_1, SOFTWARE_2, temp_file.name]
|
||||
call_command('import_serial_numbers', *args)
|
||||
|
||||
log.debug('There should be only 2 course-software entries')
|
||||
software_count = CourseSoftware.objects.all().count()
|
||||
self.assertEqual(2, software_count)
|
||||
|
||||
log.debug('We added two sets of {0} serials'.format(size))
|
||||
licenses_count = UserLicense.objects.all().count()
|
||||
self.assertEqual(2 * size, licenses_count)
|
||||
|
||||
log.debug('Adding more serial numbers to {0}'.format(SOFTWARE_1))
|
||||
with generate_serials_file(size) as temp_file:
|
||||
args = [COURSE_1, SOFTWARE_1, temp_file.name]
|
||||
call_command('import_serial_numbers', *args)
|
||||
|
||||
log.debug('There should be still only 2 course-software entries')
|
||||
software_count = CourseSoftware.objects.all().count()
|
||||
self.assertEqual(2, software_count)
|
||||
|
||||
log.debug('Now we should have 3 sets of 20 serials'.format(size))
|
||||
licenses_count = UserLicense.objects.all().count()
|
||||
self.assertEqual(3 * size, licenses_count)
|
||||
|
||||
cs = CourseSoftware.objects.get(pk=1)
|
||||
|
||||
lics = UserLicense.objects.filter(software=cs)[:size]
|
||||
known_serials = list(l.serial for l in lics)
|
||||
known_serials.extend(generate_serials(10))
|
||||
|
||||
shuffle(known_serials)
|
||||
|
||||
log.debug('Adding some new and old serials to {0}'.format(SOFTWARE_1))
|
||||
with NamedTemporaryFile() as f:
|
||||
f.write('\n'.join(known_serials))
|
||||
f.flush()
|
||||
args = [COURSE_1, SOFTWARE_1, f.name]
|
||||
call_command('import_serial_numbers', *args)
|
||||
|
||||
log.debug('Check if we added only the new ones')
|
||||
licenses_count = UserLicense.objects.filter(software=cs).count()
|
||||
self.assertEqual((2 * size) + 10, licenses_count)
|
||||
|
||||
|
||||
def generate_serials(size=20):
|
||||
return [str(uuid4()) for _ in range(size)]
|
||||
|
||||
|
||||
def generate_serials_file(size=20):
|
||||
serials = generate_serials(size)
|
||||
|
||||
temp_file = NamedTemporaryFile()
|
||||
temp_file.write('\n'.join(serials))
|
||||
temp_file.flush()
|
||||
|
||||
return temp_file
|
||||
84
lms/djangoapps/licenses/views.py
Normal file
84
lms/djangoapps/licenses/views.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
from urlparse import urlparse
|
||||
from collections import namedtuple, defaultdict
|
||||
|
||||
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.views.decorators.csrf import requires_csrf_token, csrf_protect
|
||||
|
||||
from models import CourseSoftware
|
||||
from models import get_courses_licenses, get_or_create_license, get_license
|
||||
|
||||
|
||||
log = logging.getLogger("mitx.licenses")
|
||||
|
||||
|
||||
License = namedtuple('License', 'software serial')
|
||||
|
||||
|
||||
def get_licenses_by_course(user, courses):
|
||||
licenses = get_courses_licenses(user, courses)
|
||||
licenses_by_course = defaultdict(list)
|
||||
|
||||
# create missing licenses and group by course_id
|
||||
for software, license in licenses.iteritems():
|
||||
if license is None:
|
||||
licenses[software] = get_or_create_license(user, software)
|
||||
|
||||
course_id = software.course_id
|
||||
serial = license.serial if license else None
|
||||
licenses_by_course[course_id].append(License(software, serial))
|
||||
|
||||
# render elements
|
||||
data_by_course = {}
|
||||
for course_id, licenses in licenses_by_course.iteritems():
|
||||
context = {'licenses': licenses}
|
||||
template = 'licenses/serial_numbers.html'
|
||||
data_by_course[course_id] = render_to_string(template, context)
|
||||
|
||||
return data_by_course
|
||||
|
||||
|
||||
@requires_csrf_token
|
||||
def user_software_license(request):
|
||||
if request.method != 'POST' or not request.is_ajax():
|
||||
raise Http404
|
||||
|
||||
# get the course id from the referer
|
||||
url_path = urlparse(request.META.get('HTTP_REFERER', '')).path
|
||||
pattern = re.compile('^/courses/(?P<id>[^/]+/[^/]+/[^/]+)/.*/?$')
|
||||
match = re.match(pattern, url_path)
|
||||
|
||||
if not match:
|
||||
raise Http404
|
||||
course_id = match.groupdict().get('id', '')
|
||||
|
||||
user_id = request.session.get('_auth_user_id')
|
||||
software_name = request.POST.get('software')
|
||||
generate = request.POST.get('generate', False) == 'true'
|
||||
|
||||
try:
|
||||
software = CourseSoftware.objects.get(name=software_name,
|
||||
course_id=course_id)
|
||||
print software
|
||||
except CourseSoftware.DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
user = User.objects.get(id=user_id)
|
||||
|
||||
if generate:
|
||||
license = get_or_create_license(user, software)
|
||||
else:
|
||||
license = get_license(user, software)
|
||||
|
||||
if license:
|
||||
response = {'serial': license.serial}
|
||||
else:
|
||||
response = {'error': 'No serial number found'}
|
||||
|
||||
return HttpResponse(json.dumps(response), mimetype='application/json')
|
||||
@@ -626,6 +626,7 @@ INSTALLED_APPS = (
|
||||
'certificates',
|
||||
'instructor',
|
||||
'psychometrics',
|
||||
'licenses',
|
||||
|
||||
#For the wiki
|
||||
'wiki', # The new django-wiki from benjaoming
|
||||
|
||||
10
lms/templates/licenses/serial_numbers.html
Normal file
10
lms/templates/licenses/serial_numbers.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<dl>
|
||||
% for license in licenses:
|
||||
<dt> ${license.software.name}: </dt>
|
||||
% if license.serial:
|
||||
<dd> ${license.serial} </dd>
|
||||
% else:
|
||||
<dd> None Available </dd>
|
||||
% endif
|
||||
% endfor
|
||||
</dl>
|
||||
@@ -154,6 +154,14 @@ if settings.COURSEWARE_ENABLED:
|
||||
url(r'^preview/chemcalc', 'courseware.module_render.preview_chemcalc',
|
||||
name='preview_chemcalc'),
|
||||
|
||||
# Software Licenses
|
||||
|
||||
# TODO: for now, this is the endpoint of an ajax replay
|
||||
# service that retrieve and assigns license numbers for
|
||||
# software assigned to a course. The numbers have to be loaded
|
||||
# into the database.
|
||||
url(r'^software-licenses$', 'licenses.views.user_software_license', name="user_software_license"),
|
||||
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/xqueue/(?P<userid>[^/]*)/(?P<id>.*?)/(?P<dispatch>[^/]*)$',
|
||||
'courseware.module_render.xqueue_callback',
|
||||
name='xqueue_callback'),
|
||||
|
||||
Reference in New Issue
Block a user