Commit 0409a481 by Renzo Lucioni

Merge pull request #269 from edx/renzo/remove-slugs

Eliminate use of product slugs as unique selectors
parents 13a121cc a5c0a717
...@@ -2,7 +2,7 @@ from __future__ import unicode_literals ...@@ -2,7 +2,7 @@ from __future__ import unicode_literals
import logging import logging
from django.db import models, transaction from django.db import models, transaction
from django.utils.text import slugify from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from oscar.core.loading import get_model from oscar.core.loading import get_model
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
...@@ -35,15 +35,14 @@ class Course(models.Model): ...@@ -35,15 +35,14 @@ class Course(models.Model):
def _create_parent_seat(self): def _create_parent_seat(self):
""" Create the parent seat product if it does not already exist. """ """ Create the parent seat product if it does not already exist. """
slug = 'parent-cs-{}'.format(slugify(unicode(self.id))) parent, created = self.products.get_or_create(
defaults = { course=self,
'is_discountable': True, structure=Product.PARENT,
'structure': Product.PARENT, product_class=ProductClass.objects.get(slug='seat'),
'product_class': ProductClass.objects.get(slug='seat') )
}
parent, created = self.products.get_or_create(slug=slug, defaults=defaults)
ProductCategory.objects.get_or_create(category=Category.objects.get(name='Seats'), product=parent) ProductCategory.objects.get_or_create(category=Category.objects.get(name='Seats'), product=parent)
parent.title = 'Seat in {}'.format(self.name) parent.title = 'Seat in {}'.format(self.name)
parent.is_discountable = True
parent.attr.course_key = self.id parent.attr.course_key = self.id
parent.save() parent.save()
...@@ -98,8 +97,8 @@ class Course(models.Model): ...@@ -98,8 +97,8 @@ class Course(models.Model):
@property @property
def seat_products(self): def seat_products(self):
""" Returns a list of course seat Products related to this course. """ """ Returns a queryset of course seat Products related to this course. """
return list(self.parent_seat_product.children.all().prefetch_related('stockrecords')) return self.parent_seat_product.children.all().prefetch_related('stockrecords')
def _get_course_seat_name(self, certificate_type, id_verification_required): def _get_course_seat_name(self, certificate_type, id_verification_required):
""" Returns the name for a course seat. """ """ Returns the name for a course seat. """
...@@ -124,22 +123,23 @@ class Course(models.Model): ...@@ -124,22 +123,23 @@ class Course(models.Model):
certificate_type = certificate_type.lower() certificate_type = certificate_type.lower()
course_id = unicode(self.id) course_id = unicode(self.id)
slugs = [] if certificate_type == self.certificate_type_for_mode('audit'):
slug = 'child-cs-{}-{}'.format(certificate_type, slugify(course_id)) # Yields a match if attribute names do not include 'certificate_type'.
certificate_type_query = ~Q(attributes__name='certificate_type')
# Note (CCB): Our previous method of slug generation did not account for ID verification. By using a list else:
# we can update these seats. This should be removed after the courses have been re-migrated. # Yields a match if attribute with name 'certificate_type' matches provided value.
if certificate_type == 'verified': certificate_type_query = Q(
slugs.append(slug) attributes__name='certificate_type',
attribute_values__value_text=certificate_type
)
if id_verification_required: id_verification_required_query = Q(
slug += '-id-verified' attributes__name='id_verification_required',
slugs.append(slug) attribute_values__value_boolean=id_verification_required
slugs = set(slugs) )
try: try:
seat = Product.objects.get(slug__in=slugs) seat = self.seat_products.filter(certificate_type_query).get(id_verification_required_query)
seat.slug = slug
logger.info( logger.info(
'Retrieved course seat child product with certificate type [%s] for [%s] from database.', 'Retrieved course seat child product with certificate type [%s] for [%s] from database.',
...@@ -147,7 +147,7 @@ class Course(models.Model): ...@@ -147,7 +147,7 @@ class Course(models.Model):
course_id course_id
) )
except Product.DoesNotExist: except Product.DoesNotExist:
seat = Product(slug=slug) seat = Product()
logger.info( logger.info(
'Course seat product with certificate type [%s] for [%s] does not exist. Instantiated a new instance.', 'Course seat product with certificate type [%s] for [%s] does not exist. Instantiated a new instance.',
certificate_type, certificate_type,
...@@ -155,16 +155,15 @@ class Course(models.Model): ...@@ -155,16 +155,15 @@ class Course(models.Model):
) )
seat.course = self seat.course = self
seat.structure = Product.CHILD
seat.parent = self.parent_seat_product seat.parent = self.parent_seat_product
seat.is_discountable = True seat.is_discountable = True
seat.structure = Product.CHILD
seat.title = self._get_course_seat_name(certificate_type, id_verification_required) seat.title = self._get_course_seat_name(certificate_type, id_verification_required)
seat.expires = expires seat.expires = expires
# If a ProductAttribute is saved with a value of None or the empty string, the ProductAttribute is deleted. # If a ProductAttribute is saved with a value of None or the empty string, the ProductAttribute is deleted.
# As a consequence, Seats derived from a migrated "audit" mode do not have a certificate_type attribute. # As a consequence, Seats derived from a migrated "audit" mode do not have a certificate_type attribute.
seat.attr.certificate_type = certificate_type seat.attr.certificate_type = certificate_type
seat.attr.course_key = course_id seat.attr.course_key = course_id
seat.attr.id_verification_required = id_verification_required seat.attr.id_verification_required = id_verification_required
......
...@@ -133,6 +133,23 @@ class CourseTests(CourseCatalogTestMixin, TestCase): ...@@ -133,6 +133,23 @@ class CourseTests(CourseCatalogTestMixin, TestCase):
seat, course, certificate_type, id_verification_required, price, credit_provider, credit_hours=credit_hours seat, course, certificate_type, id_verification_required, price, credit_provider, credit_hours=credit_hours
) )
def test_collision_avoidance(self):
"""
Sanity check verifying that course IDs which produced collisions due to a
lossy slug generation process no longer do so.
"""
dotted_course = Course.objects.create(id='a/...course.../id')
regular_course = Course.objects.create(id='a/course/id')
certificate_type = 'honor'
id_verification_required = False
price = 0
dotted_course.create_or_update_seat(certificate_type, id_verification_required, price)
regular_course.create_or_update_seat(certificate_type, id_verification_required, price)
child_products = Product.objects.filter(structure=Product.CHILD).count()
self.assertEqual(child_products, 2)
def test_type(self): def test_type(self):
""" Verify the property returns a type value corresponding to the available products. """ """ Verify the property returns a type value corresponding to the available products. """
course = Course.objects.create(id='a/b/c', name='Test Course') course = Course.objects.create(id='a/b/c', name='Test Course')
......
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