diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37c4ce7c..49e906d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ jobs: strategy: matrix: python-version: ["3.8"] + django-version: ["pinned", "4.2"] steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 @@ -21,7 +22,12 @@ jobs: - name: Install requirements run: make requirements - name: Upgrade packages - run: pip install -U pip wheel codecov + run: | + pip install -U pip wheel codecov + if [[ "${{ matrix.django-version }}" != "pinned" ]]; then + pip install "django~=${{ matrix.django-version }}.0" + pip check # fail if this test-reqs/Django combination is broken + fi - name: Validate translations run: make validate_translations - name: Run tests diff --git a/license_manager/apps/api/urls.py b/license_manager/apps/api/urls.py index 3ba4f7b8..e8f6ce38 100644 --- a/license_manager/apps/api/urls.py +++ b/license_manager/apps/api/urls.py @@ -4,12 +4,12 @@ All API URLs should be versioned, so urlpatterns should only contain namespaces for the active versions of the API. """ -from django.urls import include, re_path +from django.urls import include, path from license_manager.apps.api.v1 import urls as v1_urls app_name = 'api' urlpatterns = [ - re_path(r'^v1/', include(v1_urls)), + path('v1/', include(v1_urls)), ] diff --git a/license_manager/apps/core/admin.py b/license_manager/apps/core/admin.py index c923bcd1..4ddfe9eb 100644 --- a/license_manager/apps/core/admin.py +++ b/license_manager/apps/core/admin.py @@ -7,6 +7,7 @@ from license_manager.apps.core.models import User +@admin.register(User) class CustomUserAdmin(UserAdmin): """ Admin configuration for the custom User model. """ list_display = ('username', 'email', 'full_name', 'first_name', 'last_name', 'is_staff') @@ -17,6 +18,3 @@ class CustomUserAdmin(UserAdmin): 'groups', 'user_permissions')}), (_('Important dates'), {'fields': ('last_login', 'date_joined')}), ) - - -admin.site.register(User, CustomUserAdmin) diff --git a/license_manager/apps/subscriptions/admin.py b/license_manager/apps/subscriptions/admin.py index 7d62826d..dac23505 100644 --- a/license_manager/apps/subscriptions/admin.py +++ b/license_manager/apps/subscriptions/admin.py @@ -81,15 +81,23 @@ def get_queryset(self, request): 'subscription_plan', ) + @admin.display( + description='Subscription Plan' + ) def get_subscription_plan_title(self, obj): return get_related_object_link( 'admin:subscriptions_subscriptionplan_change', obj.subscription_plan.uuid, obj.subscription_plan.title, ) - get_subscription_plan_title.short_description = 'Subscription Plan' + @admin.display( + description='License renewed to' + ) def get_renewed_to(self, obj): + """ + Returns License renewed to + """ if not obj.renewed_to: return '' return get_related_object_link( @@ -97,9 +105,14 @@ def get_renewed_to(self, obj): obj.renewed_to.uuid, obj.renewed_to.uuid, ) - get_renewed_to.short_description = 'License renewed to' + @admin.display( + description='License renewed from' + ) def get_renewed_from(self, obj): + """ + Returns License renewed from + """ if not obj.renewed_from: return '' return get_related_object_link( @@ -107,7 +120,6 @@ def get_renewed_from(self, obj): obj.renewed_from.uuid, obj.renewed_from.uuid, ) - get_renewed_from.short_description = 'License renewed from' def _parse_snapshot_timestamp(self): """ @@ -120,6 +132,9 @@ def _parse_snapshot_timestamp(self): # pylint: disable=no-value-for-parameter return UTC.localize(snapshot_datetime) + @admin.action( + description='Revert licenses to snapshot' + ) def revert_licenses_to_snapshot_time(self, request, queryset): """ Sets a license back to whatever it was at some timestamp defined in config. @@ -140,7 +155,6 @@ def revert_licenses_to_snapshot_time(self, request, queryset): ) except Exception as exc: # pylint: disable=broad-except messages.add_message(request, messages.ERROR, exc) - revert_licenses_to_snapshot_time.short_description = 'Revert licenses to snapshot' @admin.register(SubscriptionPlan) @@ -253,6 +267,9 @@ def get_readonly_fields(self, request, obj=None): return self.read_only_fields return () + @admin.display( + description='Customer Agreement' + ) def get_customer_agreement_link(self, obj): """ Returns a link to the customer agreement for this plan. @@ -264,7 +281,6 @@ def get_customer_agreement_link(self, obj): obj.customer_agreement.enterprise_customer_slug, ) return '' - get_customer_agreement_link.short_description = 'Customer Agreement' def formfield_for_foreignkey(self, db_field, request, **kwargs): """ @@ -274,6 +290,9 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): kwargs['queryset'] = CustomerAgreement.objects.filter().order_by('enterprise_customer_slug') return super().formfield_for_foreignkey(db_field, request, **kwargs) + @admin.action( + description='Freeze selected Subscription Plans (deletes unused licenses)' + ) def process_unused_licenses_post_freeze(self, request, queryset): """ Used as an action; this function deletes unused licenses after a plan is frozen. @@ -285,9 +304,6 @@ def process_unused_licenses_post_freeze(self, request, queryset): messages.add_message(request, messages.SUCCESS, 'Successfully froze selected Subscription Plans.') except UnprocessableSubscriptionPlanFreezeError as exc: messages.add_message(request, messages.ERROR, exc) - process_unused_licenses_post_freeze.short_description = ( - 'Freeze selected Subscription Plans (deletes unused licenses)' - ) def save_model(self, request, obj, form, change): # Record change reason for simple history @@ -344,6 +360,9 @@ class CustomerAgreementAdmin(admin.ModelAdmin): ) actions = ['sync_agreement_with_enterprise_customer'] + @admin.action( + description='Sync enterprise customer fields for selected records' + ) def sync_agreement_with_enterprise_customer(self, request, queryset): """ Django action handler to sync any updates made to the enterprise customer @@ -364,8 +383,6 @@ def sync_agreement_with_enterprise_customer(self, request, queryset): except CustomerAgreementError as exc: messages.add_message(request, messages.ERROR, exc) - sync_agreement_with_enterprise_customer.short_description = 'Sync enterprise customer fields for selected records' - def save_model(self, request, obj, form, change): """ Saves the CustomerAgreement instance. @@ -399,6 +416,9 @@ def get_readonly_fields(self, request, obj=None): 'get_subscription_plan_links', ) + @admin.display( + description='Subscription Plans' + ) def get_subscription_plan_links(self, obj): """ Gets links to all active subscription plans for this customer agreement. @@ -414,7 +434,6 @@ def get_subscription_plan_links(self, obj): ) ) return mark_safe(' '.join(links)) - get_subscription_plan_links.short_description = 'Subscription Plans' @admin.register(SubscriptionPlanRenewal) @@ -444,33 +463,59 @@ class SubscriptionPlanRenewalAdmin(DjangoQLSearchMixin, admin.ModelAdmin): ) actions = ['process_renewal'] + @admin.action( + description='Process selected renewal records' + ) def process_renewal(self, request, queryset): + """ + Process selected renewal records + """ for renewal in queryset: renew_subscription(renewal) - process_renewal.short_description = 'Process selected renewal records' + @admin.display( + description='Subscription Title', + ordering='prior_subscription_plan__title', + ) def get_prior_subscription_plan_title(self, obj): + """ + Returns Subscription Title + """ return obj.prior_subscription_plan.title - get_prior_subscription_plan_title.short_description = 'Subscription Title' - get_prior_subscription_plan_title.admin_order_field = 'prior_subscription_plan__title' + @admin.display( + description='Subscription UUID', + ordering='prior_subscription_plan__uuid', + ) def get_prior_subscription_plan_uuid(self, obj): + """ + Returns Subscription UUID + """ return obj.prior_subscription_plan.uuid - get_prior_subscription_plan_uuid.short_description = 'Subscription UUID' - get_prior_subscription_plan_uuid.admin_order_field = 'prior_subscription_plan__uuid' + @admin.display( + description='Enterprise Customer UUID', + ordering='prior_subscription_plan__enterprise_customer_uuid', + ) def get_prior_subscription_plan_enterprise_customer(self, obj): + """ + Returns Enterprise Customer UUID + """ return obj.prior_subscription_plan.enterprise_customer_uuid - get_prior_subscription_plan_enterprise_customer.short_description = 'Enterprise Customer UUID' - get_prior_subscription_plan_enterprise_customer.admin_order_field = \ - 'prior_subscription_plan__enterprise_customer_uuid' + @admin.display( + description='Enterprise Catalog UUID', + ordering='prior_subscription_plan__enterprise_catalog_uuid', + ) def get_prior_subscription_plan_enterprise_catalog(self, obj): + """ + Returns Enterprise Catalog UUID + """ return obj.prior_subscription_plan.enterprise_catalog_uuid - get_prior_subscription_plan_enterprise_catalog.short_description = 'Enterprise Catalog UUID' - get_prior_subscription_plan_enterprise_catalog.admin_order_field = \ - 'prior_subscription_plan__enterprise_catalog_uuid' + @admin.display( + description='Renewed Subscription Plan' + ) def get_renewed_plan_link(self, obj): """ Returns a link to the renewed subscription plan. @@ -482,7 +527,6 @@ def get_renewed_plan_link(self, obj): '{}: {}'.format(obj.renewed_subscription_plan.title, obj.renewed_subscription_plan.uuid), ) return '' - get_renewed_plan_link.short_description = 'Renewed Subscription Plan' def has_change_permission(self, request, obj=None): """ diff --git a/license_manager/settings/base.py b/license_manager/settings/base.py index e042a572..aa05d2f4 100644 --- a/license_manager/settings/base.py +++ b/license_manager/settings/base.py @@ -405,7 +405,6 @@ DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' -DEFAULT_HASHING_ALGORITHM = "sha1" DEFAULT_DAYS_BEFORE_LICENSE_PURGE = 90 diff --git a/license_manager/urls.py b/license_manager/urls.py index 38467798..a30c55dc 100644 --- a/license_manager/urls.py +++ b/license_manager/urls.py @@ -18,7 +18,7 @@ from auth_backends.urls import oauth2_urlpatterns from django.conf import settings from django.contrib import admin -from django.urls import include, re_path +from django.urls import include, path from drf_yasg.views import get_schema_view from edx_api_doc_tools import make_api_info from rest_framework import permissions @@ -38,13 +38,13 @@ ) urlpatterns = [ - re_path(r'', include(oauth2_urlpatterns)), - re_path(r'', include('csrf.urls')), # Include csrf urls from edx-drf-extensions - re_path(r'^admin/', admin.site.urls), - re_path(r'^api/', include(api_urls)), - re_path(r'^api-docs/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), - re_path(r'^auto_auth/$', core_views.AutoAuth.as_view(), name='auto_auth'), - re_path(r'^health/$', core_views.health, name='health'), + path('', include(oauth2_urlpatterns)), + path('', include('csrf.urls')), # Include csrf urls from edx-drf-extensions + path('admin/', admin.site.urls), + path('api/', include(api_urls)), + path('api-docs/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + path('auto_auth/', core_views.AutoAuth.as_view(), name='auto_auth'), + path('health/', core_views.health, name='health'), ] @@ -52,4 +52,4 @@ # Disable pylint import error because we don't install django-debug-toolbar # for CI build import debug_toolbar # pylint: disable=import-error,useless-suppression - urlpatterns.append(re_path(r'^__debug__/', include(debug_toolbar.urls))) + urlpatterns.append(path('__debug__/', include(debug_toolbar.urls)))