Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit e2a2bdd

Browse files
committed
final touch, make sure validation works + add tests
1 parent dc6fdce commit e2a2bdd

File tree

4 files changed

+214
-27
lines changed

4 files changed

+214
-27
lines changed

codecov_auth/admin.py

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
Account,
1818
AccountsUsers,
1919
InvoiceBilling,
20+
Plan,
2021
StripeBilling,
21-
Plans,
22-
Tiers,
22+
Tier,
2323
)
2424
from shared.plan.constants import USER_PLAN_REPRESENTATIONS
2525
from shared.plan.service import PlanService
@@ -711,12 +711,12 @@ class AccountsUsersAdmin(AdminMixin, admin.ModelAdmin):
711711

712712
fields = readonly_fields + ["account", "user"]
713713

714+
714715
class PlansInline(admin.TabularInline):
715-
model = Plans
716+
model = Plan
716717
extra = 1
717718
verbose_name_plural = "Plans (click save to commit changes)"
718719
verbose_name = "Plan"
719-
readonly_fields = ["name"]
720720
fields = [
721721
"name",
722722
"marketing_name",
@@ -728,12 +728,12 @@ class PlansInline(admin.TabularInline):
728728
"is_active",
729729
]
730730
formfield_overrides = {
731-
Plans._meta.get_field("benefits"): {"widget": Textarea(attrs={"rows": 3})},
731+
Plan._meta.get_field("benefits"): {"widget": Textarea(attrs={"rows": 3})},
732732
}
733733

734734

735-
@admin.register(Tiers)
736-
class TiersAdmin(admin.ModelAdmin):
735+
@admin.register(Tier)
736+
class TierAdmin(admin.ModelAdmin):
737737
list_display = (
738738
"tier_name",
739739
"bundle_analysis",
@@ -761,9 +761,40 @@ class TiersAdmin(admin.ModelAdmin):
761761
]
762762

763763

764-
@admin.register(Plans)
765-
class PlansAdmin(admin.ModelAdmin):
766-
list_display = ("name", "marketing_name", "base_unit_price", "is_active", "paid_plan")
764+
class PlanAdminForm(forms.ModelForm):
765+
class Meta:
766+
model = Plan
767+
fields = "__all__"
768+
769+
def clean_base_unit_price(self):
770+
base_unit_price = self.cleaned_data.get("base_unit_price")
771+
if base_unit_price is not None and base_unit_price < 0:
772+
raise forms.ValidationError("Base unit price cannot be negative.")
773+
return base_unit_price
774+
775+
def clean_max_seats(self):
776+
max_seats = self.cleaned_data.get("max_seats")
777+
if max_seats is not None and max_seats < 0:
778+
raise forms.ValidationError("Max seats cannot be negative.")
779+
return max_seats
780+
781+
def clean_monthly_uploads_limit(self):
782+
monthly_uploads_limit = self.cleaned_data.get("monthly_uploads_limit")
783+
if monthly_uploads_limit is not None and monthly_uploads_limit < 0:
784+
raise forms.ValidationError("Monthly uploads limit cannot be negative.")
785+
return monthly_uploads_limit
786+
787+
788+
@admin.register(Plan)
789+
class PlanAdmin(admin.ModelAdmin):
790+
form = PlanAdminForm
791+
list_display = (
792+
"name",
793+
"marketing_name",
794+
"base_unit_price",
795+
"is_active",
796+
"paid_plan",
797+
)
767798
list_filter = ("is_active", "paid_plan", "billing_rate")
768799
search_fields = ("name__iregex", "marketing_name__iregex")
769800
fields = [
@@ -779,13 +810,6 @@ class PlansAdmin(admin.ModelAdmin):
779810
"paid_plan",
780811
]
781812
formfield_overrides = {
782-
Plans._meta.get_field("benefits"): {"widget": Textarea(attrs={"rows": 3})},
813+
Plan._meta.get_field("benefits"): {"widget": Textarea(attrs={"rows": 3})},
783814
}
784815
autocomplete_fields = ["tier"] # a dropdown for selecting related Tiers
785-
786-
def save_model(self, request, obj, form, change):
787-
if obj.base_unit_price < 0:
788-
raise forms.ValidationError("Base unit price cannot be negative.")
789-
if obj.max_seats is not None and obj.max_seats < 0:
790-
raise forms.ValidationError("Max seats cannot be negative.")
791-
super().save_model(request, obj, form, change)

codecov_auth/tests/test_admin.py

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
InvoiceBillingFactory,
1919
OrganizationLevelTokenFactory,
2020
OwnerFactory,
21+
PlanFactory,
2122
SentryUserFactory,
2223
SessionFactory,
2324
StripeBillingFactory,
25+
TierFactory,
2426
UserFactory,
2527
)
2628
from shared.django_apps.core.tests.factories import PullFactory, RepositoryFactory
@@ -39,7 +41,14 @@
3941
UserAdmin,
4042
find_and_remove_stale_users,
4143
)
42-
from codecov_auth.models import OrganizationLevelToken, Owner, SentryUser, User
44+
from codecov_auth.models import (
45+
OrganizationLevelToken,
46+
Owner,
47+
Plan,
48+
SentryUser,
49+
Tier,
50+
User,
51+
)
4352
from core.models import Pull
4453

4554

@@ -881,3 +890,157 @@ def test_account_widget(self):
881890
self.assertFalse(form.base_fields["account"].widget.can_add_related)
882891
self.assertFalse(form.base_fields["account"].widget.can_change_related)
883892
self.assertFalse(form.base_fields["account"].widget.can_delete_related)
893+
894+
895+
class PlanAdminTest(TestCase):
896+
def setUp(self):
897+
self.staff_user = UserFactory(is_staff=True)
898+
self.client.force_login(user=self.staff_user)
899+
admin_site = AdminSite()
900+
admin_site.register(Plan)
901+
902+
def test_plan_admin_modal_display(self):
903+
plan = PlanFactory()
904+
response = self.client.get(
905+
reverse("admin:codecov_auth_plan_change", args=[plan.pk])
906+
)
907+
self.assertEqual(response.status_code, 200)
908+
self.assertContains(response, plan.name)
909+
910+
def test_plan_modal_tiers_display(self):
911+
tier = TierFactory()
912+
plan = PlanFactory(tier=tier)
913+
response = self.client.get(
914+
reverse("admin:codecov_auth_plan_change", args=[plan.pk])
915+
)
916+
self.assertEqual(response.status_code, 200)
917+
self.assertContains(response, tier.tier_name)
918+
919+
def test_add_plans_modal_action(self):
920+
plan = PlanFactory(base_unit_price=10, max_seats=5)
921+
tier = TierFactory()
922+
data = {
923+
"action": "add_plans",
924+
ACTION_CHECKBOX_NAME: [plan.pk],
925+
"tier_id": tier.pk,
926+
}
927+
response = self.client.post(
928+
reverse("admin:codecov_auth_plan_changelist"), data=data
929+
)
930+
self.assertEqual(response.status_code, 302)
931+
self.assertEqual(response.url, "/admin/codecov_auth/plan/")
932+
933+
def test_plan_change_form(self):
934+
plan = PlanFactory()
935+
response = self.client.get(
936+
reverse("admin:codecov_auth_plan_change", args=[plan.pk])
937+
)
938+
self.assertEqual(response.status_code, 200)
939+
for field in [
940+
"tier",
941+
"name",
942+
"marketing_name",
943+
"base_unit_price",
944+
"benefits",
945+
"billing_rate",
946+
"is_active",
947+
"max_seats",
948+
"monthly_uploads_limit",
949+
"paid_plan",
950+
]:
951+
self.assertContains(response, f"id_{field}")
952+
953+
def test_plan_change_form_validation(self):
954+
plan = PlanFactory(base_unit_price=-10)
955+
956+
response = self.client.post(
957+
reverse("admin:codecov_auth_plan_change", args=[plan.pk]),
958+
{
959+
"tier": plan.tier_id,
960+
"name": plan.name,
961+
"marketing_name": plan.marketing_name,
962+
"base_unit_price": -10,
963+
"benefits": plan.benefits,
964+
"is_active": plan.is_active,
965+
"paid_plan": plan.paid_plan,
966+
},
967+
)
968+
self.assertEqual(response.status_code, 200)
969+
self.assertContains(response, "Base unit price cannot be negative.")
970+
971+
response = self.client.post(
972+
reverse("admin:codecov_auth_plan_change", args=[plan.pk]),
973+
{
974+
"tier": plan.tier_id,
975+
"name": plan.name,
976+
"marketing_name": plan.marketing_name,
977+
"base_unit_price": plan.base_unit_price,
978+
"benefits": plan.benefits,
979+
"is_active": plan.is_active,
980+
"max_seats": -5,
981+
"paid_plan": plan.paid_plan,
982+
},
983+
)
984+
self.assertEqual(response.status_code, 200)
985+
self.assertContains(response, "Max seats cannot be negative.")
986+
987+
response = self.client.post(
988+
reverse("admin:codecov_auth_plan_change", args=[plan.pk]),
989+
{
990+
"tier": plan.tier_id,
991+
"name": plan.name,
992+
"marketing_name": plan.marketing_name,
993+
"benefits": plan.benefits,
994+
"is_active": plan.is_active,
995+
"monthly_uploads_limit": -5,
996+
"paid_plan": plan.paid_plan,
997+
},
998+
)
999+
self.assertEqual(response.status_code, 200)
1000+
self.assertContains(response, "Monthly uploads limit cannot be negative.")
1001+
1002+
1003+
class TierAdminTest(TestCase):
1004+
def setUp(self):
1005+
self.staff_user = UserFactory(is_staff=True)
1006+
self.client.force_login(user=self.staff_user)
1007+
admin_site = AdminSite()
1008+
admin_site.register(Tier)
1009+
1010+
def test_tier_modal_plans_display(self):
1011+
tier = TierFactory()
1012+
response = self.client.get(
1013+
reverse("admin:codecov_auth_tier_change", args=[tier.pk])
1014+
)
1015+
self.assertEqual(response.status_code, 200)
1016+
self.assertContains(response, tier.tier_name)
1017+
1018+
def test_add_plans_modal_action(self):
1019+
tier = TierFactory()
1020+
plan = PlanFactory()
1021+
data = {
1022+
"action": "add_plans",
1023+
ACTION_CHECKBOX_NAME: [plan.pk],
1024+
"tier_id": tier.pk,
1025+
}
1026+
response = self.client.post(
1027+
reverse("admin:codecov_auth_tier_changelist"), data=data
1028+
)
1029+
self.assertEqual(response.status_code, 302)
1030+
self.assertEqual(response.url, "/admin/codecov_auth/tier/")
1031+
1032+
def test_tier_change_form(self):
1033+
tier = TierFactory()
1034+
response = self.client.get(
1035+
reverse("admin:codecov_auth_tier_change", args=[tier.pk])
1036+
)
1037+
self.assertEqual(response.status_code, 200)
1038+
for field in [
1039+
"tier_name",
1040+
"bundle_analysis",
1041+
"test_analytics",
1042+
"flaky_test_detection",
1043+
"project_coverage",
1044+
"private_repo_support",
1045+
]:
1046+
self.assertContains(response, f"id_{field}")

requirements.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ freezegun
2626
google-cloud-pubsub
2727
gunicorn>=22.0.0
2828
https://github.com/codecov/opentelem-python/archive/refs/tags/v0.0.4a1.tar.gz#egg=codecovopentelem
29-
https://github.com/codecov/shared/archive/c3288a017d49d72a85d6f42a2056d3063cfd8dda.tar.gz#egg=shared
29+
https://github.com/codecov/shared/archive/d5d7c208f9716ac0f48543f84b635aa28d15f0f2.tar.gz#egg=shared
3030
https://github.com/photocrowd/django-cursor-pagination/archive/f560902696b0c8509e4d95c10ba0d62700181d84.tar.gz
3131
idna>=3.7
3232
minio

requirements.txt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,6 @@ googleapis-common-protos[grpc]==1.59.1
202202
# grpcio-status
203203
graphql-core==3.2.3
204204
# via ariadne
205-
greenlet==3.1.1
206-
# via sqlalchemy
207205
grpc-google-iam-v1==0.12.6
208206
# via google-cloud-pubsub
209207
grpcio==1.62.1
@@ -337,9 +335,11 @@ pyasn1-modules==0.2.8
337335
# via google-auth
338336
pycparser==2.20
339337
# via cffi
340-
pydantic==2.8.2
341-
# via -r requirements.in
342-
pydantic-core==2.20.1
338+
pydantic==2.10.5
339+
# via
340+
# -r requirements.in
341+
# shared
342+
pydantic-core==2.27.2
343343
# via pydantic
344344
pyjwt==2.8.0
345345
# via
@@ -416,7 +416,7 @@ sentry-sdk[celery]==2.13.0
416416
# shared
417417
setproctitle==1.1.10
418418
# via -r requirements.in
419-
shared @ https://github.com/codecov/shared/archive/c3288a017d49d72a85d6f42a2056d3063cfd8dda.tar.gz
419+
shared @ https://github.com/codecov/shared/archive/d5d7c208f9716ac0f48543f84b635aa28d15f0f2.tar.gz
420420
# via -r requirements.in
421421
simplejson==3.17.2
422422
# via -r requirements.in
@@ -451,7 +451,7 @@ text-unidecode==1.3
451451
# via faker
452452
toml==0.10.2
453453
# via pre-commit
454-
typing-extensions==4.6.2
454+
typing-extensions==4.12.2
455455
# via
456456
# aiodataloader
457457
# ariadne

0 commit comments

Comments
 (0)