diff --git a/lms/djangoapps/shoppingcart/admin.py b/lms/djangoapps/shoppingcart/admin.py index 199a39d7c0..841ea7e96d 100644 --- a/lms/djangoapps/shoppingcart/admin.py +++ b/lms/djangoapps/shoppingcart/admin.py @@ -2,6 +2,50 @@ Allows django admin site to add PaidCourseRegistrationAnnotations """ from ratelimitbackend import admin -from shoppingcart.models import PaidCourseRegistrationAnnotation +from shoppingcart.models import PaidCourseRegistrationAnnotation, Coupon + + +class SoftDeleteCouponAdmin(admin.ModelAdmin): + """ + Admin for the Coupon table. + soft-delete on the coupons + """ + fields = ('code', 'description', 'course_id', 'percentage_discount', 'created_by', 'created_at', 'is_active') + raw_id_fields = ("created_by",) + readonly_fields = ('created_at',) + actions = ['really_delete_selected'] + + def queryset(self, request): + """ Returns a QuerySet of all model instances that can be edited by the + admin site. This is used by changelist_view. """ + # Default: qs = self.model._default_manager.get_active_coupons_query_set() + # Queryset with all the coupons including the soft-deletes: qs = self.model._default_manager.get_query_set() + query_string = self.model._default_manager.get_active_coupons_query_set() # pylint: disable=W0212 + return query_string + + def get_actions(self, request): + actions = super(SoftDeleteCouponAdmin, self).get_actions(request) + del actions['delete_selected'] + return actions + + def really_delete_selected(self, request, queryset): + """override the default behavior of selected delete method""" + for obj in queryset: + obj.is_active = False + obj.save() + + if queryset.count() == 1: + message_bit = "1 coupon entry was" + else: + message_bit = "%s coupon entries were" % queryset.count() + self.message_user(request, "%s successfully deleted." % message_bit) + + def delete_model(self, request, obj): + """override the default behavior of single instance of model delete method""" + obj.is_active = False + obj.save() + + really_delete_selected.short_description = "Delete s selected entries" admin.site.register(PaidCourseRegistrationAnnotation) +admin.site.register(Coupon, SoftDeleteCouponAdmin) diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index d12fba1ad1..ffc95145ef 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -337,6 +337,22 @@ class RegistrationCodeRedemption(models.Model): redeemed_at = models.DateTimeField(default=datetime.now(pytz.utc), null=True) +class SoftDeleteCouponManager(models.Manager): + """ Use this manager to get objects that have a is_active=True """ + + def get_active_coupons_query_set(self): + """ + filter the is_active = True Coupons only + """ + return super(SoftDeleteCouponManager, self).get_query_set().filter(is_active=True) + + def get_query_set(self): + """ + get all the coupon objects + """ + return super(SoftDeleteCouponManager, self).get_query_set() + + class Coupon(models.Model): """ This table contains coupon codes @@ -350,6 +366,11 @@ class Coupon(models.Model): created_at = models.DateTimeField(default=datetime.now(pytz.utc)) is_active = models.BooleanField(default=True) + def __unicode__(self): + return "[Coupon] code: {} course: {}".format(self.code, self.course_id) + + objects = SoftDeleteCouponManager() + class CouponRedemption(models.Model): """ diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index 8f0e93f809..a127f57dc2 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -1,6 +1,7 @@ """ Tests for Shopping Cart views """ +from django.http import HttpRequest from urlparse import urlparse from django.conf import settings @@ -8,18 +9,21 @@ from django.test import TestCase from django.test.utils import override_settings from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ -from django.contrib.auth.models import Group +from django.contrib.admin.sites import AdminSite +from django.contrib.auth.models import Group, User +from django.contrib.messages.storage.fallback import FallbackStorage from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from shoppingcart.views import _can_download_report, _get_date_from_str from shoppingcart.models import Order, CertificateItem, PaidCourseRegistration, Coupon -from student.tests.factories import UserFactory +from student.tests.factories import UserFactory, AdminFactory from student.models import CourseEnrollment from course_modes.models import CourseMode from edxmako.shortcuts import render_to_response from shoppingcart.processors import render_purchase_form_html +from shoppingcart.admin import SoftDeleteCouponAdmin from mock import patch, Mock from shoppingcart.views import initialize_report from decimal import Decimal @@ -143,6 +147,39 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): self.assertEqual(resp.status_code, 400) self.assertIn("Coupon '{0}' already used.".format(self.coupon_code), resp.content) + def test_soft_delete_coupon(self): # pylint: disable=E1101 + self.add_coupon(self.course_key, True) + coupon = Coupon(code='TestCode', description='testing', course_id=self.course_key, + percentage_discount=12, created_by=self.user, is_active=True) + coupon.save() + self.assertEquals(coupon.__unicode__(), '[Coupon] code: TestCode course: MITx/999/Robot_Super_Course') + admin = User.objects.create_user('Mark', 'admin+courses@edx.org', 'foo') + admin.is_staff = True + get_coupon = Coupon.objects.get(id=1) + request = HttpRequest() + request.user = admin + setattr(request, 'session', 'session') # pylint: disable=E1101 + messages = FallbackStorage(request) # pylint: disable=E1101 + setattr(request, '_messages', messages) # pylint: disable=E1101 + coupon_admin = SoftDeleteCouponAdmin(Coupon, AdminSite()) + test_query_set = coupon_admin.queryset(request) + test_actions = coupon_admin.get_actions(request) + self.assertTrue('really_delete_selected' in test_actions['really_delete_selected']) + self.assertEqual(get_coupon.is_active, True) + coupon_admin.really_delete_selected(request, test_query_set) # pylint: disable=E1101 + for coupon in test_query_set: + self.assertEqual(coupon.is_active, False) + coupon_admin.delete_model(request, get_coupon) # pylint: disable=E1101 + self.assertEqual(get_coupon.is_active, False) + + coupon = Coupon(code='TestCode123', description='testing123', course_id=self.course_key, + percentage_discount=22, created_by=self.user, is_active=True) + coupon.save() + test_query_set = coupon_admin.queryset(request) + coupon_admin.really_delete_selected(request, test_query_set) # pylint: disable=E1101 + for coupon in test_query_set: + self.assertEqual(coupon.is_active, False) + @patch('shoppingcart.views.log.debug') def test_non_existing_coupon_redemption_on_removing_item(self, debug_log):