Skip to content
Snippets Groups Projects
Commit f8f2e63e authored by Victor Shnayder's avatar Victor Shnayder
Browse files

Merge pull request #922 from MITx/feature/rocha/dashboard-software-licenses

Retrieve and assign course software licenses
parents ca747622 a8cedf8a
No related branches found
No related tags found
No related merge requests found
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)
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))
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
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
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
......
<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'),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment