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

DemocracyClub / aggregator-api / 93f28176-d12b-44aa-8582-a03ba3be77a2

05 Dec 2023 03:31PM UTC coverage: 77.419% (-0.8%) from 78.236%
93f28176-d12b-44aa-8582-a03ba3be77a2

push

circleci

web-flow
Merge pull request #441 from DemocracyClub/self-serve-api-keys

Self-serve API keys

315 of 422 new or added lines in 17 files covered. (74.64%)

1 existing line in 1 file now uncovered.

864 of 1116 relevant lines covered (77.42%)

0.77 hits per line

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

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

5
from common.settings import API_PLANS
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.managers import CustomUserManager
1✔
16
from api_users.mixins import TimestampMixin
1✔
17

18

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

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

52
    USERNAME_FIELD = "email"
1✔
53
    EMAIL_FIELD = "email"
1✔
54

55
    objects = CustomUserManager()
1✔
56

57
    class Meta:
1✔
58
        verbose_name = _("user")
1✔
59
        verbose_name_plural = _("users")
1✔
60

61
    def clean(self):
1✔
NEW
62
        super().clean()
×
NEW
63
        self.email = CustomUser.objects.normalize_email(self.email)
×
64

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

71

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

98
    class Meta:
1✔
99
        ordering = ["-created_at"]
1✔
100
        verbose_name = "API key"
1✔
101
        verbose_name_plural = "API keys"
1✔
102

103
    def __str__(self):
1✔
NEW
104
        return f"{self.name} ({self.truncated_key})"
×
105

106
    @classmethod
1✔
107
    def _generate_key(self):
1✔
108
        return binascii.hexlify(os.urandom(20)).decode()
1✔
109

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

117
        from api_users.dynamodb_helpers import DynamoDBClient
1✔
118

119
        dynamodb_client = DynamoDBClient()
1✔
120

121
        super().save(*args, **kwargs)
1✔
122
        dynamodb_client.update_key(self)
1✔
123
        return self
1✔
124

125
    def delete(self, using=None, keep_parents=False):
1✔
126
        from api_users.dynamodb_helpers import DynamoDBClient
1✔
127

128
        dynamodb_client = DynamoDBClient()
1✔
129
        dynamodb_client.delete_key(self)
1✔
130
        return super().delete(using, keep_parents)
1✔
131

132
    def get_absolute_delete_url(self):
1✔
133
        """
134
        Build URL to delete the key
135
        """
NEW
136
        return reverse("api_users:delete-key", kwargs={"pk": self.pk})
×
137

138
    @property
1✔
139
    def label(self):
1✔
NEW
140
        if self.user.api_plan == "hobbyists":
×
NEW
141
            return "Hobbyist"
×
NEW
142
        return self.key_type.title()
×
143

144
    @property
1✔
145
    def truncated_key(self):
1✔
NEW
146
        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