Commit af9cff47 by Sarina Canelake

Merge pull request #10837 from edx/kill-licenses

Remove "Licenses" djangoapp
parents 2cfeb34f af8a0bb6
......@@ -1875,33 +1875,6 @@ CREATE TABLE `instructor_task_instructortask` (
CONSTRAINT `instructor_task_in_requester_id_3383acfe2fe42391_fk_auth_user_id` FOREIGN KEY (`requester_id`) REFERENCES `auth_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `licenses_coursesoftware`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `licenses_coursesoftware` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`full_name` varchar(255) NOT NULL,
`url` varchar(255) NOT NULL,
`course_id` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `licenses_userlicense`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `licenses_userlicense` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`serial` varchar(255) NOT NULL,
`software_id` int(11) NOT NULL,
`user_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `licen_software_id_209fd400b7c7a3ca_fk_licenses_coursesoftware_id` (`software_id`),
KEY `licenses_userlicense_user_id_7d98c37aa4438a34_fk_auth_user_id` (`user_id`),
CONSTRAINT `licen_software_id_209fd400b7c7a3ca_fk_licenses_coursesoftware_id` FOREIGN KEY (`software_id`) REFERENCES `licenses_coursesoftware` (`id`),
CONSTRAINT `licenses_userlicense_user_id_7d98c37aa4438a34_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `lms_xblock_xblockasidesconfig`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
......
from uuid import uuid4
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
from opaque_keys.edx.locations import SlashSeparatedCourseKey
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]
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
if not modulestore().has_course(course_key):
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_key, 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 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
from opaque_keys.edx.locations import SlashSeparatedCourseKey
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]
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
if not modulestore().has_course(course_key):
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_key, 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))
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import xmodule_django.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CourseSoftware',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=255)),
('full_name', models.CharField(max_length=255)),
('url', models.CharField(max_length=255)),
('course_id', xmodule_django.models.CourseKeyField(max_length=255)),
],
),
migrations.CreateModel(
name='UserLicense',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('serial', models.CharField(max_length=255)),
('software', models.ForeignKey(to='licenses.CourseSoftware')),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True)),
],
),
]
import logging
from django.db import models, transaction
from student.models import User
from xmodule_django.models import CourseKeyField
log = logging.getLogger("edx.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 = CourseKeyField(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:
# TODO: temporary fix for when somehow a user got more that one license.
# The proper fix should use Meta.unique_together in the UserLicense model.
licenses = UserLicense.objects.filter(user=user, software=software)
license = licenses[0] if licenses else None
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.atomic():
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 %s', software)
license = None
# TODO [rocha]look if someone has unenrolled from the class
# and already has a serial number
return license
"""Tests for License package"""
import logging
import json
from uuid import uuid4
from random import shuffle
from tempfile import NamedTemporaryFile
import factory
from factory.django import DjangoModelFactory
from django.test import TestCase
from django.test.client import Client
from django.core.management import call_command
from django.core.urlresolvers import reverse
from nose.tools import assert_true
from licenses.models import CourseSoftware, UserLicense
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
COURSE_1 = 'edX/toy/2012_Fall'
SOFTWARE_1 = 'matlab'
SOFTWARE_2 = 'stata'
SERIAL_1 = '123456abcde'
log = logging.getLogger(__name__)
class CourseSoftwareFactory(DjangoModelFactory):
'''Factory for generating CourseSoftware objects in database'''
class Meta(object):
model = CourseSoftware
name = SOFTWARE_1
full_name = SOFTWARE_1
url = SOFTWARE_1
course_id = COURSE_1
class UserLicenseFactory(DjangoModelFactory):
'''
Factory for generating UserLicense objects in database
By default, the user assigned is null, indicating that the
serial number has not yet been assigned.
'''
class Meta(object):
model = UserLicense
user = None
software = factory.SubFactory(CourseSoftwareFactory)
serial = SERIAL_1
class LicenseTestCase(TestCase):
'''Tests for licenses.views'''
def setUp(self):
'''creates a user and logs in'''
super(LicenseTestCase, self).setUp()
# self.setup_viewtest_user()
self.user = UserFactory(username='test',
email='test@edx.org', password='test_password')
self.client = Client()
assert_true(self.client.login(username='test', password='test_password'))
self.software = CourseSoftwareFactory()
def test_get_license(self):
UserLicenseFactory(user=self.user, software=self.software)
response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'false'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
self.assertEqual(200, response.status_code)
json_returned = json.loads(response.content)
self.assertFalse('error' in json_returned)
self.assertTrue('serial' in json_returned)
self.assertEquals(json_returned['serial'], SERIAL_1)
def test_get_nonexistent_license(self):
response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'false'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
self.assertEqual(200, response.status_code)
json_returned = json.loads(response.content)
self.assertFalse('serial' in json_returned)
self.assertTrue('error' in json_returned)
def test_create_nonexistent_license(self):
'''Should not assign a license to an unlicensed user when none are available'''
response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'true'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
self.assertEqual(200, response.status_code)
json_returned = json.loads(response.content)
self.assertFalse('serial' in json_returned)
self.assertTrue('error' in json_returned)
def test_create_license(self):
'''Should assign a license to an unlicensed user if one is unassigned'''
# create an unassigned license
UserLicenseFactory(software=self.software)
response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'true'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
self.assertEqual(200, response.status_code)
json_returned = json.loads(response.content)
self.assertFalse('error' in json_returned)
self.assertTrue('serial' in json_returned)
self.assertEquals(json_returned['serial'], SERIAL_1)
def test_get_license_from_wrong_course(self):
response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'false'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
HTTP_REFERER='/courses/{0}/some_page'.format('some/other/course'))
self.assertEqual(404, response.status_code)
def test_get_license_from_non_ajax(self):
response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'false'},
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
self.assertEqual(404, response.status_code)
def test_get_license_without_software(self):
response = self.client.post(reverse('user_software_license'),
{'generate': 'false'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
self.assertEqual(404, response.status_code)
def test_get_license_without_login(self):
self.client.logout()
response = self.client.post(reverse('user_software_license'),
{'software': SOFTWARE_1, 'generate': 'false'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
# if we're not logged in, we should be referred to the login page
self.assertEqual(302, response.status_code)
class CommandTest(ModuleStoreTestCase):
'''Test management command for importing serial numbers'''
def setUp(self):
super(CommandTest, self).setUp()
course = CourseFactory.create()
self.course_id = course.id
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 = [self.course_id.to_deprecated_string(), 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 = [self.course_id.to_deprecated_string(), 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 = [self.course_id.to_deprecated_string(), 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 %s serials",
size,
)
licenses_count = UserLicense.objects.all().count()
self.assertEqual(3 * size, licenses_count)
software = CourseSoftware.objects.get(pk=1)
lics = UserLicense.objects.filter(software=software)[: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 tmpfile:
tmpfile.write('\n'.join(known_serials))
tmpfile.flush()
args = [self.course_id.to_deprecated_string(), SOFTWARE_1, tmpfile.name]
call_command('import_serial_numbers', *args)
log.debug('Check if we added only the new ones')
licenses_count = UserLicense.objects.filter(software=software).count()
self.assertEqual((2 * size) + 10, licenses_count)
def generate_serials(size=20):
'''generate a list of serial numbers'''
return [str(uuid4()) for _ in range(size)]
def generate_serials_file(size=20):
'''output list of generated serial numbers to a temp file'''
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 edxmako.shortcuts import render_to_string
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.http import HttpResponse, Http404
from django.views.decorators.csrf import requires_csrf_token
from licenses.models import CourseSoftware
from licenses.models import get_courses_licenses, get_or_create_license, get_license
log = logging.getLogger("edx.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
@login_required
@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', '')
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_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_key)
except CourseSoftware.DoesNotExist:
raise Http404
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
raise Http404
if generate:
software_license = get_or_create_license(user, software)
else:
software_license = get_license(user, software)
if software_license:
response = {'serial': software_license.serial}
else:
response = {'error': 'No serial number found'}
return HttpResponse(json.dumps(response), content_type='application/json')
......@@ -1831,7 +1831,6 @@ INSTALLED_APPS = (
'instructor',
'instructor_task',
'open_ended_grading',
'licenses',
'openedx.core.djangoapps.course_groups',
'bulk_email',
'branding',
......
<%! from django.utils.translation import ugettext as _ %>
<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>
......@@ -310,14 +310,6 @@ if settings.COURSEWARE_ENABLED:
name='xblock_resource_url',
),
# 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/{}/xqueue/(?P<userid>[^/]*)/(?P<mod_id>.*?)/(?P<dispatch>[^/]*)$'.format(
settings.COURSE_ID_PATTERN
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment