Coverage for /Users/davegaeddert/Development/dropseed/plain/plain-flags/plain/flags/models.py: 63%
51 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:03 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:03 -0500
1import re
2import uuid
4from plain import models
5from plain.exceptions import ValidationError
6from plain.models import ProgrammingError
7from plain.preflight import Info
8from plain.runtime import settings
10from .bridge import get_flag_class
11from .exceptions import FlagImportError
14def validate_flag_name(value):
15 if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value):
16 raise ValidationError(f"{value} is not a valid Python identifier name")
19class FlagResult(models.Model):
20 uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
21 created_at = models.DateTimeField(auto_now_add=True)
22 updated_at = models.DateTimeField(auto_now=True)
23 flag = models.ForeignKey("Flag", on_delete=models.CASCADE)
24 key = models.CharField(max_length=255)
25 value = models.JSONField()
27 class Meta:
28 constraints = [
29 models.UniqueConstraint(
30 fields=["flag", "key"], name="unique_flag_result_key"
31 )
32 ]
34 def __str__(self):
35 return self.key
38class Flag(models.Model):
39 uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
40 created_at = models.DateTimeField(auto_now_add=True)
41 updated_at = models.DateTimeField(auto_now=True)
42 name = models.CharField(
43 max_length=255, unique=True, validators=[validate_flag_name]
44 )
46 # Optional description that can be filled in after the flag is used/created
47 description = models.TextField(blank=True)
49 # To manually disable a flag before completing deleting
50 # (good to disable first to make sure the code doesn't use the flag anymore)
51 enabled = models.BooleanField(default=True)
53 # To provide an easier way to see if a flag is still being used
54 used_at = models.DateTimeField(blank=True, null=True)
56 def __str__(self):
57 return self.name
59 @classmethod
60 def check(cls, **kwargs):
61 """
62 Check for flags that are in the database, but no longer defined in code.
64 Only returns Info errors because it is valid to leave them if you're worried about
65 putting the flag back, but they should probably be deleted eventually.
66 """
67 errors = super().check(**kwargs)
69 databases = kwargs["databases"]
70 if not databases:
71 return errors
73 for database in databases:
74 flag_names = (
75 cls.objects.using(database).all().values_list("name", flat=True)
76 )
78 try:
79 flag_names = set(flag_names)
80 except ProgrammingError:
81 # The table doesn't exist yet
82 # (migrations probably haven't run yet),
83 # so we can't check it.
84 continue
86 for flag_name in flag_names:
87 try:
88 get_flag_class(flag_name)
89 except FlagImportError:
90 errors.append(
91 Info(
92 f"Flag {flag_name} is not used.",
93 hint=f"Remove the flag from the database or define it in the {settings.FLAGS_MODULE} module.",
94 id="plain.flags.I001",
95 )
96 )
98 return errors