1import re
2import uuid
3
4from plain import models
5from plain.exceptions import ValidationError
6from plain.models import ProgrammingError
7from plain.preflight import Info
8from plain.runtime import settings
9
10from .bridge import get_flag_class
11from .exceptions import FlagImportError
12
13
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")
17
18
19@models.register_model
20class FlagResult(models.Model):
21 uuid = models.UUIDField(default=uuid.uuid4)
22 created_at = models.DateTimeField(auto_now_add=True)
23 updated_at = models.DateTimeField(auto_now=True)
24 flag = models.ForeignKey("Flag", on_delete=models.CASCADE)
25 key = models.CharField(max_length=255)
26 value = models.JSONField()
27
28 class Meta:
29 constraints = [
30 models.UniqueConstraint(
31 fields=["flag", "key"], name="plainflags_flagresult_unique_key"
32 ),
33 models.UniqueConstraint(
34 fields=["uuid"], name="plainflags_flagresult_unique_uuid"
35 ),
36 ]
37
38 def __str__(self):
39 return self.key
40
41
42@models.register_model
43class Flag(models.Model):
44 uuid = models.UUIDField(default=uuid.uuid4)
45 created_at = models.DateTimeField(auto_now_add=True)
46 updated_at = models.DateTimeField(auto_now=True)
47 name = models.CharField(max_length=255, validators=[validate_flag_name])
48
49 # Optional description that can be filled in after the flag is used/created
50 description = models.TextField(required=False)
51
52 # To manually disable a flag before completing deleting
53 # (good to disable first to make sure the code doesn't use the flag anymore)
54 enabled = models.BooleanField(default=True)
55
56 # To provide an easier way to see if a flag is still being used
57 used_at = models.DateTimeField(required=False, allow_null=True)
58
59 class Meta:
60 constraints = [
61 models.UniqueConstraint(
62 fields=["name"], name="plainflags_flag_unique_name"
63 ),
64 models.UniqueConstraint(
65 fields=["uuid"], name="plainflags_flag_unique_uuid"
66 ),
67 ]
68
69 def __str__(self):
70 return self.name
71
72 @classmethod
73 def check(cls, **kwargs):
74 """
75 Check for flags that are in the database, but no longer defined in code.
76
77 Only returns Info errors because it is valid to leave them if you're worried about
78 putting the flag back, but they should probably be deleted eventually.
79 """
80 errors = super().check(**kwargs)
81
82 database = kwargs.get("database", False)
83 if not database:
84 return errors
85
86 flag_names = cls.objects.all().values_list("name", flat=True)
87
88 try:
89 flag_names = set(flag_names)
90 except ProgrammingError:
91 # The table doesn't exist yet
92 # (migrations probably haven't run yet),
93 # so we can't check it.
94 return errors
95
96 for flag_name in flag_names:
97 try:
98 get_flag_class(flag_name)
99 except FlagImportError:
100 errors.append(
101 Info(
102 f"Flag {flag_name} is not used.",
103 hint=f"Remove the flag from the database or define it in the {settings.FLAGS_MODULE} module.",
104 id="plain.flags.I001",
105 )
106 )
107
108 return errors