• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DemocracyClub / aggregator-api / ec079e66-92d6-475d-975f-f9a55533869b

13 May 2025 08:11AM UTC coverage: 74.422% (+0.9%) from 73.52%
ec079e66-92d6-475d-975f-f9a55533869b

push

circleci

GeoWill
Command to write csvs for all api keys

1158 of 1556 relevant lines covered (74.42%)

0.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.84
/frontend/apps/api_users/models.py
1
# Create your models here.
2
import binascii
1✔
3
import os
1✔
4

5
from common.conf import settings
1✔
6
from django.contrib.auth import get_user_model
1✔
7
from django.contrib.auth.base_user import AbstractBaseUser
1✔
8
from django.contrib.auth.models import PermissionsMixin
1✔
9
from django.core.mail import send_mail
1✔
10
from django.db import models
1✔
11
from django.urls import reverse
1✔
12
from django.utils import timezone
1✔
13
from django.utils.translation import gettext_lazy as _
1✔
14

15
from api_users.logging_helpers import APIKeyForLogging
1✔
16
from api_users.managers import CustomUserManager
1✔
17
from api_users.mixins import TimestampMixin
1✔
18

19

20
class CustomUser(AbstractBaseUser, PermissionsMixin):
1✔
21
    """
22
    Custom implementation of django User model to use the email for login
23
    """
24

25
    email = models.EmailField(_("email address"), unique=True)
1✔
26
    name = models.CharField(
1✔
27
        max_length=255, help_text=_("Either your name or an organisation name")
28
    )
29
    is_staff = models.BooleanField(
1✔
30
        _("staff status"),
31
        default=False,
32
        help_text=_(
33
            "Designates whether the user can log into this admin site."
34
        ),
35
    )
36
    is_active = models.BooleanField(
1✔
37
        _("active"),
38
        default=True,
39
        help_text=_(
40
            "Designates whether this user should be treated as active. "
41
            "Unselect this instead of deleting accounts."
42
        ),
43
    )
44
    date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
1✔
45
    api_plan = models.CharField(
1✔
46
        choices=[
47
            (name, plan.label) for name, plan in settings.API_PLANS.items()
48
        ],
49
        default="hobbyists",
50
        max_length=100,
51
        verbose_name="API plan",
52
        help_text="The plan that this user has paid for",
53
    )
54

55
    USERNAME_FIELD = "email"
1✔
56
    EMAIL_FIELD = "email"
1✔
57

58
    objects = CustomUserManager()
1✔
59

60
    class Meta:
1✔
61
        verbose_name = _("user")
1✔
62
        verbose_name_plural = _("users")
1✔
63

64
    def clean(self):
1✔
65
        super().clean()
×
66
        self.email = CustomUser.objects.normalize_email(self.email)
×
67

68
    def email_user(self, subject, message, from_email=None, **kwargs):
1✔
69
        """
70
        Email this user.
71
        """
72
        send_mail(subject, message, from_email, [self.email], **kwargs)
×
73

74

75
class APIKey(TimestampMixin, models.Model):
1✔
76
    name = models.CharField(
1✔
77
        max_length=255, help_text="To help identify your key"
78
    )
79
    usage_reason = models.TextField(
1✔
80
        help_text="Short description of the usage reason for this key"
81
    )
82
    key = models.CharField(max_length=255)
1✔
83
    is_active = models.BooleanField(default=True)
1✔
84
    rate_limit_warn = models.BooleanField(default=False)
1✔
85
    key_type = models.CharField(
1✔
86
        choices=[
87
            ("development", "Development"),
88
            ("production", "Production"),
89
        ],
90
        max_length=100,
91
        verbose_name="API plan",
92
        help_text="The plan for this API key. Only one production key allowed.",
93
        default="development",
94
    )
95
    user: CustomUser = models.ForeignKey(
1✔
96
        to=get_user_model(),
97
        on_delete=models.CASCADE,
98
        related_name="api_keys",
99
    )
100

101
    class Meta:
1✔
102
        ordering = ["-created_at"]
1✔
103
        verbose_name = "API key"
1✔
104
        verbose_name_plural = "API keys"
1✔
105

106
    def __str__(self):
1✔
107
        return f"{self.name} ({self.truncated_key})"
×
108

109
    @classmethod
1✔
110
    def _generate_key(self):
1✔
111
        return binascii.hexlify(os.urandom(20)).decode()
1✔
112

113
    def save(self, *args, **kwargs):
1✔
114
        """
115
        On initial save generates a key
116
        """
117
        if not self.key:
1✔
118
            self.key = self._generate_key()
1✔
119

120
        from api_users.dynamodb_helpers import DynamoDBClient
1✔
121

122
        dynamodb_client = DynamoDBClient()
1✔
123

124
        super().save(*args, **kwargs)
1✔
125
        dynamodb_client.update_key(self)
1✔
126

127
        key_for_logging = APIKeyForLogging.from_django_model(self)
1✔
128
        key_for_logging.upload_to_s3()
1✔
129

130
        return self
1✔
131

132
    def delete(self, using=None, keep_parents=False):
1✔
133
        from api_users.dynamodb_helpers import DynamoDBClient
1✔
134

135
        dynamodb_client = DynamoDBClient()
1✔
136
        dynamodb_client.delete_key(self)
1✔
137
        return super().delete(using, keep_parents)
1✔
138

139
    def get_absolute_delete_url(self):
1✔
140
        """
141
        Build URL to delete the key
142
        """
143
        return reverse("api_users:delete-key", kwargs={"pk": self.pk})
×
144

145
    @property
1✔
146
    def label(self):
1✔
147
        if self.user.api_plan == "hobbyists":
×
148
            return "Hobbyist"
×
149
        return self.key_type.title()
×
150

151
    @property
1✔
152
    def truncated_key(self):
1✔
153
        return f"{self.key[:4]}…{self.key[-4:]}"
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc