Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain-flags/plain/flags/models.py: 63%

51 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-23 11:16 -0600

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 

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() 

26 

27 class Meta: 

28 constraints = [ 

29 models.UniqueConstraint( 

30 fields=["flag", "key"], name="unique_flag_result_key" 

31 ) 

32 ] 

33 

34 def __str__(self): 

35 return self.key 

36 

37 

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 ) 

45 

46 # Optional description that can be filled in after the flag is used/created 

47 description = models.TextField(blank=True) 

48 

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) 

52 

53 # To provide an easier way to see if a flag is still being used 

54 used_at = models.DateTimeField(blank=True, null=True) 

55 

56 def __str__(self): 

57 return self.name 

58 

59 @classmethod 

60 def check(cls, **kwargs): 

61 """ 

62 Check for flags that are in the database, but no longer defined in code. 

63 

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) 

68 

69 databases = kwargs["databases"] 

70 if not databases: 

71 return errors 

72 

73 for database in databases: 

74 flag_names = ( 

75 cls.objects.using(database).all().values_list("name", flat=True) 

76 ) 

77 

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 

85 

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 ) 

97 

98 return errors