mirror of
https://github.com/GSA/notifications-api.git
synced 2026-07-28 11:49:42 -04:00
remove broadcast-related code, except migrations
This commit is contained in:
423
app/models.py
423
app/models.py
@@ -16,7 +16,6 @@ from notifications_utils.recipients import (
|
||||
validate_phone_number,
|
||||
)
|
||||
from notifications_utils.template import (
|
||||
BroadcastMessageTemplate,
|
||||
LetterPrintTemplate,
|
||||
PlainTextEmailTemplate,
|
||||
SMSMessageTemplate,
|
||||
@@ -50,10 +49,9 @@ from app.utils import (
|
||||
SMS_TYPE = 'sms'
|
||||
EMAIL_TYPE = 'email'
|
||||
LETTER_TYPE = 'letter'
|
||||
BROADCAST_TYPE = 'broadcast'
|
||||
|
||||
TEMPLATE_TYPES = [SMS_TYPE, EMAIL_TYPE, LETTER_TYPE, BROADCAST_TYPE]
|
||||
NOTIFICATION_TYPES = [SMS_TYPE, EMAIL_TYPE, LETTER_TYPE] # not broadcast
|
||||
TEMPLATE_TYPES = [SMS_TYPE, EMAIL_TYPE, LETTER_TYPE]
|
||||
NOTIFICATION_TYPES = [SMS_TYPE, EMAIL_TYPE, LETTER_TYPE]
|
||||
|
||||
template_types = db.Enum(*TEMPLATE_TYPES, name='template_type')
|
||||
|
||||
@@ -152,7 +150,6 @@ class User(db.Model):
|
||||
return True
|
||||
|
||||
return any(
|
||||
str(service.organisation_id) == current_app.config['BROADCAST_ORGANISATION_ID'] or
|
||||
str(service.id) == current_app.config['NOTIFY_SERVICE_ID']
|
||||
for service in self.services
|
||||
)
|
||||
@@ -326,7 +323,6 @@ SERVICE_PERMISSION_TYPES = [
|
||||
EMAIL_TYPE,
|
||||
SMS_TYPE,
|
||||
LETTER_TYPE,
|
||||
BROADCAST_TYPE,
|
||||
INTERNATIONAL_SMS_TYPE,
|
||||
INBOUND_SMS_TYPE,
|
||||
SCHEDULE_NOTIFICATIONS,
|
||||
@@ -531,9 +527,6 @@ class Service(db.Model, Versioned):
|
||||
uselist=False,
|
||||
backref=db.backref('services', lazy='dynamic'))
|
||||
|
||||
allowed_broadcast_provider = association_proxy('service_broadcast_settings', 'provider')
|
||||
broadcast_channel = association_proxy('service_broadcast_settings', 'channel')
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data):
|
||||
"""
|
||||
@@ -577,13 +570,6 @@ class Service(db.Model, Versioned):
|
||||
'research_mode': self.research_mode
|
||||
}
|
||||
|
||||
def get_available_broadcast_providers(self):
|
||||
# There may be future checks here if we add, for example, platform admin level provider killswitches.
|
||||
if self.allowed_broadcast_provider != ALL_BROADCAST_PROVIDERS:
|
||||
return [x for x in current_app.config['ENABLED_CBCS'] if x == self.allowed_broadcast_provider]
|
||||
else:
|
||||
return current_app.config['ENABLED_CBCS']
|
||||
|
||||
|
||||
class AnnualBilling(db.Model):
|
||||
__tablename__ = "annual_billing"
|
||||
@@ -953,7 +939,6 @@ class TemplateBase(db.Model):
|
||||
hidden = db.Column(db.Boolean, nullable=False, default=False)
|
||||
subject = db.Column(db.Text)
|
||||
postage = db.Column(db.String, nullable=True)
|
||||
broadcast_data = db.Column(JSONB(none_as_null=True), nullable=True)
|
||||
|
||||
@declared_attr
|
||||
def service_id(cls):
|
||||
@@ -1026,8 +1011,6 @@ class TemplateBase(db.Model):
|
||||
return PlainTextEmailTemplate(self.__dict__)
|
||||
if self.template_type == SMS_TYPE:
|
||||
return SMSMessageTemplate(self.__dict__)
|
||||
if self.template_type == BROADCAST_TYPE:
|
||||
return BroadcastMessageTemplate(self.__dict__)
|
||||
if self.template_type == LETTER_TYPE:
|
||||
return LetterPrintTemplate(
|
||||
self.__dict__,
|
||||
@@ -1891,10 +1874,6 @@ SEND_LETTERS = 'send_letters'
|
||||
MANAGE_API_KEYS = 'manage_api_keys'
|
||||
PLATFORM_ADMIN = 'platform_admin'
|
||||
VIEW_ACTIVITY = 'view_activity'
|
||||
CREATE_BROADCASTS = 'create_broadcasts'
|
||||
APPROVE_BROADCASTS = 'approve_broadcasts'
|
||||
CANCEL_BROADCASTS = 'cancel_broadcasts'
|
||||
REJECT_BROADCASTS = 'reject_broadcasts'
|
||||
|
||||
# List of permissions
|
||||
PERMISSION_LIST = [
|
||||
@@ -1907,10 +1886,6 @@ PERMISSION_LIST = [
|
||||
MANAGE_API_KEYS,
|
||||
PLATFORM_ADMIN,
|
||||
VIEW_ACTIVITY,
|
||||
CREATE_BROADCASTS,
|
||||
APPROVE_BROADCASTS,
|
||||
CANCEL_BROADCASTS,
|
||||
REJECT_BROADCASTS,
|
||||
]
|
||||
|
||||
|
||||
@@ -2263,400 +2238,6 @@ class ServiceContactList(db.Model):
|
||||
return contact_list
|
||||
|
||||
|
||||
class BroadcastStatusType(db.Model):
|
||||
__tablename__ = 'broadcast_status_type'
|
||||
DRAFT = 'draft'
|
||||
PENDING_APPROVAL = 'pending-approval'
|
||||
REJECTED = 'rejected'
|
||||
BROADCASTING = 'broadcasting'
|
||||
COMPLETED = 'completed'
|
||||
CANCELLED = 'cancelled'
|
||||
TECHNICAL_FAILURE = 'technical-failure'
|
||||
|
||||
STATUSES = [DRAFT, PENDING_APPROVAL, REJECTED, BROADCASTING, COMPLETED, CANCELLED, TECHNICAL_FAILURE]
|
||||
|
||||
# a broadcast message can be edited while in one of these states
|
||||
PRE_BROADCAST_STATUSES = [DRAFT, PENDING_APPROVAL, REJECTED]
|
||||
LIVE_STATUSES = [BROADCASTING, COMPLETED, CANCELLED]
|
||||
|
||||
# these are only the transitions we expect to administer via the API code.
|
||||
ALLOWED_STATUS_TRANSITIONS = {
|
||||
DRAFT: {PENDING_APPROVAL},
|
||||
PENDING_APPROVAL: {REJECTED, DRAFT, BROADCASTING},
|
||||
REJECTED: {DRAFT, PENDING_APPROVAL},
|
||||
BROADCASTING: {COMPLETED, CANCELLED},
|
||||
COMPLETED: {},
|
||||
CANCELLED: {},
|
||||
TECHNICAL_FAILURE: {},
|
||||
}
|
||||
|
||||
name = db.Column(db.String, primary_key=True)
|
||||
|
||||
|
||||
class BroadcastMessage(db.Model):
|
||||
"""
|
||||
This is for creating a message, viewing it in notify, adding areas, approvals, drafts, etc. Notify logic before
|
||||
hitting send.
|
||||
"""
|
||||
__tablename__ = 'broadcast_message'
|
||||
__table_args__ = (
|
||||
db.ForeignKeyConstraint(
|
||||
['template_id', 'template_version'],
|
||||
['templates_history.id', 'templates_history.version'],
|
||||
),
|
||||
{}
|
||||
)
|
||||
|
||||
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
|
||||
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'))
|
||||
service = db.relationship('Service', backref='broadcast_messages')
|
||||
|
||||
template_id = db.Column(UUID(as_uuid=True), nullable=True)
|
||||
template_version = db.Column(db.Integer, nullable=True)
|
||||
template = db.relationship('TemplateHistory', backref='broadcast_messages')
|
||||
|
||||
_personalisation = db.Column(db.String, nullable=True)
|
||||
content = db.Column(db.String, nullable=False)
|
||||
# defaults to empty list
|
||||
areas = db.Column(JSONB(none_as_null=True), nullable=False, default=list)
|
||||
|
||||
status = db.Column(
|
||||
db.String,
|
||||
db.ForeignKey('broadcast_status_type.name'),
|
||||
nullable=False,
|
||||
default=BroadcastStatusType.DRAFT
|
||||
)
|
||||
|
||||
# these times are related to the actual broadcast, rather than auditing purposes
|
||||
starts_at = db.Column(db.DateTime, nullable=True)
|
||||
finishes_at = db.Column(db.DateTime, nullable=True) # isn't updated if user cancels
|
||||
|
||||
# these times correspond to when
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
approved_at = db.Column(db.DateTime, nullable=True)
|
||||
cancelled_at = db.Column(db.DateTime, nullable=True)
|
||||
updated_at = db.Column(db.DateTime, nullable=True, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
created_by_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=True)
|
||||
approved_by_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=True)
|
||||
cancelled_by_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=True)
|
||||
|
||||
created_by = db.relationship('User', foreign_keys=[created_by_id])
|
||||
approved_by = db.relationship('User', foreign_keys=[approved_by_id])
|
||||
cancelled_by = db.relationship('User', foreign_keys=[cancelled_by_id])
|
||||
|
||||
created_by_api_key_id = db.Column(UUID(as_uuid=True), db.ForeignKey('api_keys.id'), nullable=True)
|
||||
cancelled_by_api_key_id = db.Column(UUID(as_uuid=True), db.ForeignKey('api_keys.id'), nullable=True)
|
||||
created_by_api_key = db.relationship('ApiKey', foreign_keys=[created_by_api_key_id])
|
||||
cancelled_by_api_key = db.relationship('ApiKey', foreign_keys=[cancelled_by_api_key_id])
|
||||
|
||||
reference = db.Column(db.String(255), nullable=True)
|
||||
cap_event = db.Column(db.String(255), nullable=True)
|
||||
|
||||
stubbed = db.Column(db.Boolean, nullable=False)
|
||||
|
||||
CheckConstraint("created_by_id is not null or created_by_api_key_id is not null")
|
||||
|
||||
@property
|
||||
def personalisation(self):
|
||||
if self._personalisation:
|
||||
return encryption.decrypt(self._personalisation)
|
||||
return {}
|
||||
|
||||
@personalisation.setter
|
||||
def personalisation(self, personalisation):
|
||||
self._personalisation = encryption.encrypt(personalisation or {})
|
||||
|
||||
def serialize(self):
|
||||
return {
|
||||
'id': str(self.id),
|
||||
'reference': self.reference,
|
||||
'cap_event': self.cap_event,
|
||||
|
||||
'service_id': str(self.service_id),
|
||||
|
||||
'template_id': str(self.template_id) if self.template else None,
|
||||
'template_version': self.template_version,
|
||||
'template_name': self.template.name if self.template else None,
|
||||
'personalisation': self.personalisation if self.template else None,
|
||||
'content': self.content,
|
||||
|
||||
'areas': self.areas,
|
||||
'status': self.status,
|
||||
|
||||
'starts_at': get_dt_string_or_none(self.starts_at),
|
||||
'finishes_at': get_dt_string_or_none(self.finishes_at),
|
||||
|
||||
'created_at': get_dt_string_or_none(self.created_at),
|
||||
'approved_at': get_dt_string_or_none(self.approved_at),
|
||||
'cancelled_at': get_dt_string_or_none(self.cancelled_at),
|
||||
'updated_at': get_dt_string_or_none(self.updated_at),
|
||||
|
||||
'created_by_id': get_uuid_string_or_none(self.created_by_id),
|
||||
'approved_by_id': get_uuid_string_or_none(self.approved_by_id),
|
||||
'cancelled_by_id': get_uuid_string_or_none(self.cancelled_by_id),
|
||||
}
|
||||
|
||||
|
||||
class BroadcastEventMessageType:
|
||||
ALERT = 'alert'
|
||||
UPDATE = 'update'
|
||||
CANCEL = 'cancel'
|
||||
|
||||
MESSAGE_TYPES = [ALERT, UPDATE, CANCEL]
|
||||
|
||||
|
||||
class BroadcastEvent(db.Model):
|
||||
"""
|
||||
This table represents an instruction that we will send to the broadcast providers. It directly correlates with an
|
||||
instruction from the admin - to broadcast a message, to cancel an existing message, or to update an existing one.
|
||||
|
||||
We should be able to create the complete CAP message without joining from this to any other tables, eg
|
||||
template, service, or broadcast_message.
|
||||
|
||||
The only exception to this is that we will have to join to itself to find other broadcast_events with the
|
||||
same broadcast_message_id when building up the `<references>` xml field for updating/cancelling an existing message.
|
||||
|
||||
As such, this shouldn't have foreign keys to things that can change or be deleted.
|
||||
"""
|
||||
__tablename__ = 'broadcast_event'
|
||||
|
||||
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
|
||||
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'))
|
||||
service = db.relationship('Service')
|
||||
|
||||
broadcast_message_id = db.Column(UUID(as_uuid=True), db.ForeignKey('broadcast_message.id'), nullable=False)
|
||||
broadcast_message = db.relationship('BroadcastMessage', backref='events')
|
||||
|
||||
# this is used for <sent> in the cap xml
|
||||
sent_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
# msgType. alert, cancel, or update. (other options in the spec are "ack" and "error")
|
||||
message_type = db.Column(db.String, nullable=False)
|
||||
|
||||
# this will be json containing anything that isnt hardcoded in utils/cbc proxy. for now just body but may grow to
|
||||
# include, eg, title, headline, instructions.
|
||||
transmitted_content = db.Column(
|
||||
JSONB(none_as_null=True),
|
||||
nullable=True
|
||||
)
|
||||
# unsubstantiated reckon: even if we're sending a cancel, we'll still need to provide areas
|
||||
transmitted_areas = db.Column(JSONB(none_as_null=True), nullable=False, default=list)
|
||||
transmitted_sender = db.Column(db.String(), nullable=False)
|
||||
|
||||
# we may only need this starts_at if this is scheduled for the future. Interested to see how this affects
|
||||
# updates/cancels (ie: can you schedule an update for the future?)
|
||||
transmitted_starts_at = db.Column(db.DateTime, nullable=True)
|
||||
transmitted_finishes_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
@property
|
||||
def reference(self):
|
||||
notify_email_domain = current_app.config['NOTIFY_EMAIL_DOMAIN']
|
||||
return (
|
||||
f'https://www.{notify_email_domain}/,'
|
||||
f'{self.id},'
|
||||
f'{self.sent_at_as_cap_datetime_string}'
|
||||
)
|
||||
|
||||
@property
|
||||
def sent_at_as_cap_datetime_string(self):
|
||||
return self.formatted_datetime_for('sent_at')
|
||||
|
||||
@property
|
||||
def transmitted_finishes_at_as_cap_datetime_string(self):
|
||||
return self.formatted_datetime_for('transmitted_finishes_at')
|
||||
|
||||
def formatted_datetime_for(self, property_name):
|
||||
return self.convert_naive_utc_datetime_to_cap_standard_string(
|
||||
getattr(self, property_name)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def convert_naive_utc_datetime_to_cap_standard_string(dt):
|
||||
"""
|
||||
As defined in section 3.3.2 of
|
||||
http://docs.oasis-open.org/emergency/cap/v1.2/CAP-v1.2-os.html
|
||||
They define the standard "YYYY-MM-DDThh:mm:ssXzh:zm", where X is
|
||||
`+` if the timezone is > UTC, otherwise `-`
|
||||
"""
|
||||
return f"{dt.strftime('%Y-%m-%dT%H:%M:%S')}-00:00"
|
||||
|
||||
def get_provider_message(self, provider):
|
||||
return next(
|
||||
(
|
||||
provider_message
|
||||
for provider_message in self.provider_messages
|
||||
if provider_message.provider == provider
|
||||
),
|
||||
None
|
||||
)
|
||||
|
||||
def get_earlier_provider_messages(self, provider):
|
||||
"""
|
||||
Get the previous message for a provider. These are different per provider, as the identifiers are different.
|
||||
Return the full provider_message object rather than just an identifier, since the different providers expect
|
||||
reference to contain different things - let the cbc_proxy work out what information is relevant.
|
||||
"""
|
||||
from app.dao.broadcast_message_dao import (
|
||||
get_earlier_events_for_broadcast_event,
|
||||
)
|
||||
earlier_events = [
|
||||
event for event in get_earlier_events_for_broadcast_event(self.id)
|
||||
]
|
||||
ret = []
|
||||
for event in earlier_events:
|
||||
provider_message = event.get_provider_message(provider)
|
||||
if provider_message is None:
|
||||
# TODO: We should figure out what to do if a previous message hasn't been sent out yet.
|
||||
# We don't want to not cancel a message just because it's stuck in a queue somewhere.
|
||||
# This exception should probably be named, and then should be caught further up and handled
|
||||
# appropriately.
|
||||
raise Exception(
|
||||
f'Cannot get earlier message references for event {self.id}, previous event {event.id} has not ' +
|
||||
f' been sent to provider "{provider}" yet'
|
||||
)
|
||||
ret.append(provider_message)
|
||||
return ret
|
||||
|
||||
def serialize(self):
|
||||
return {
|
||||
'id': str(self.id),
|
||||
|
||||
'service_id': str(self.service_id),
|
||||
|
||||
'broadcast_message_id': str(self.broadcast_message_id),
|
||||
# sent_at is required by BroadcastMessageTemplate.from_broadcast_event
|
||||
'sent_at': self.sent_at.strftime(DATETIME_FORMAT),
|
||||
'message_type': self.message_type,
|
||||
|
||||
'transmitted_content': self.transmitted_content,
|
||||
'transmitted_areas': self.transmitted_areas,
|
||||
'transmitted_sender': self.transmitted_sender,
|
||||
|
||||
'transmitted_starts_at': get_dt_string_or_none(self.transmitted_starts_at),
|
||||
# transmitted_finishes_at is required by BroadcastMessageTemplate.from_broadcast_event
|
||||
'transmitted_finishes_at': self.transmitted_finishes_at.strftime(DATETIME_FORMAT),
|
||||
|
||||
}
|
||||
|
||||
|
||||
class BroadcastProvider:
|
||||
EE = 'ee'
|
||||
VODAFONE = 'vodafone'
|
||||
THREE = 'three'
|
||||
O2 = 'o2'
|
||||
|
||||
PROVIDERS = [EE, VODAFONE, THREE, O2]
|
||||
|
||||
|
||||
ALL_BROADCAST_PROVIDERS = 'all'
|
||||
|
||||
|
||||
class BroadcastProviderMessageStatus:
|
||||
TECHNICAL_FAILURE = 'technical-failure' # Couldn’t send (cbc proxy 5xx/4xx)
|
||||
SENDING = 'sending' # Sent to cbc, awaiting response
|
||||
ACK = 'returned-ack' # Received ack response
|
||||
ERR = 'returned-error' # Received error response
|
||||
|
||||
STATES = [TECHNICAL_FAILURE, SENDING, ACK, ERR]
|
||||
|
||||
|
||||
class BroadcastProviderMessage(db.Model):
|
||||
"""
|
||||
A row in this table represents the XML blob sent to a single provider.
|
||||
"""
|
||||
__tablename__ = 'broadcast_provider_message'
|
||||
|
||||
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
|
||||
broadcast_event_id = db.Column(UUID(as_uuid=True), db.ForeignKey('broadcast_event.id'))
|
||||
broadcast_event = db.relationship('BroadcastEvent', backref='provider_messages')
|
||||
|
||||
# 'ee', 'three', 'vodafone', etc
|
||||
provider = db.Column(db.String)
|
||||
|
||||
status = db.Column(db.String)
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, nullable=True, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
UniqueConstraint(broadcast_event_id, provider)
|
||||
|
||||
message_number = association_proxy('broadcast_provider_message_number', 'broadcast_provider_message_number')
|
||||
|
||||
|
||||
class BroadcastProviderMessageNumber(db.Model):
|
||||
"""
|
||||
To send IBAG messages via the CBC proxy to Nokia CBC appliances, Notify must generate and store a numeric
|
||||
message_number alongside the message ID (GUID).
|
||||
Subsequent messages (Update, Cancel) in IBAG format must reference the original message_number & message_id.
|
||||
This model relates broadcast_provider_message_id to that numeric message_number.
|
||||
"""
|
||||
__tablename__ = 'broadcast_provider_message_number'
|
||||
|
||||
sequence = Sequence('broadcast_provider_message_number_seq')
|
||||
broadcast_provider_message_number = db.Column(
|
||||
db.Integer, sequence, server_default=sequence.next_value(), primary_key=True
|
||||
)
|
||||
broadcast_provider_message_id = db.Column(
|
||||
UUID(as_uuid=True), db.ForeignKey('broadcast_provider_message.id'), nullable=False
|
||||
)
|
||||
broadcast_provider_message = db.relationship(
|
||||
'BroadcastProviderMessage', backref=db.backref("broadcast_provider_message_number", uselist=False)
|
||||
)
|
||||
|
||||
|
||||
class ServiceBroadcastSettings(db.Model):
|
||||
"""
|
||||
Every broadcast service should have one and only one row in this table.
|
||||
"""
|
||||
__tablename__ = "service_broadcast_settings"
|
||||
|
||||
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), primary_key=True, nullable=False)
|
||||
service = db.relationship(Service, backref=db.backref("service_broadcast_settings", uselist=False))
|
||||
channel = db.Column(
|
||||
db.String(255), db.ForeignKey('broadcast_channel_types.name'), nullable=False
|
||||
)
|
||||
provider = db.Column(db.String, db.ForeignKey('broadcast_provider_types.name'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, nullable=True, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
|
||||
class BroadcastChannelTypes(db.Model):
|
||||
__tablename__ = 'broadcast_channel_types'
|
||||
|
||||
name = db.Column(db.String(255), primary_key=True)
|
||||
|
||||
|
||||
class BroadcastProviderTypes(db.Model):
|
||||
__tablename__ = 'broadcast_provider_types'
|
||||
|
||||
name = db.Column(db.String(255), primary_key=True)
|
||||
|
||||
|
||||
class ServiceBroadcastProviderRestriction(db.Model):
|
||||
"""
|
||||
TODO: Drop this table as no longer used
|
||||
|
||||
Most services don't send broadcasts. Of those that do, most send to all broadcast providers.
|
||||
However, some services don't send to all providers. These services are test services that we or the providers
|
||||
themselves use.
|
||||
|
||||
This table links those services. There should only be one row per service in this table, and this is enforced by
|
||||
the service_id being a primary key.
|
||||
"""
|
||||
__tablename__ = "service_broadcast_provider_restriction"
|
||||
|
||||
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), primary_key=True, nullable=False)
|
||||
service = db.relationship(Service, backref=db.backref("service_broadcast_provider_restriction", uselist=False))
|
||||
|
||||
provider = db.Column(db.String, nullable=False)
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
|
||||
class WebauthnCredential(db.Model):
|
||||
"""
|
||||
A table that stores data for registered webauthn credentials.
|
||||
|
||||
Reference in New Issue
Block a user