Define on_delete argument for ForeignKey and OneToOneField

Required since Django 2.0
This commit is contained in:
Santiago L 2021-04-22 10:28:00 +02:00
parent eadc06d4c5
commit d863598d81
21 changed files with 355 additions and 348 deletions

View File

@ -29,7 +29,7 @@ class Account(auth.AbstractBaseUser):
validators.RegexValidator(r'^[\w.-]+$', _("Enter a valid username."), 'invalid') validators.RegexValidator(r'^[\w.-]+$', _("Enter a valid username."), 'invalid')
]) ])
main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True, main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True,
related_name='accounts_main', editable=False) related_name='accounts_main', editable=False, on_delete=models.SET_NULL)
short_name = models.CharField(_("short name"), max_length=64, blank=True) short_name = models.CharField(_("short name"), max_length=64, blank=True)
full_name = models.CharField(_("full name"), max_length=256) full_name = models.CharField(_("full name"), max_length=256)
email = models.EmailField(_('email address'), help_text=_("Used for password recovery")) email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))

View File

@ -24,7 +24,7 @@ from . import settings
class BillContact(models.Model): class BillContact(models.Model):
account = models.OneToOneField('accounts.Account', verbose_name=_("account"), account = models.OneToOneField('accounts.Account', verbose_name=_("account"),
related_name='billcontact') related_name='billcontact', on_delete=models.CASCADE)
name = models.CharField(_("name"), max_length=256, blank=True, name = models.CharField(_("name"), max_length=256, blank=True,
help_text=_("Account full name will be used when left blank.")) help_text=_("Account full name will be used when left blank."))
address = models.TextField(_("address")) address = models.TextField(_("address"))
@ -102,9 +102,9 @@ class Bill(models.Model):
number = models.CharField(_("number"), max_length=16, unique=True, blank=True) number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
account = models.ForeignKey('accounts.Account', verbose_name=_("account"), account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='%(class)s') related_name='%(class)s', on_delete=models.CASCADE)
amend_of = models.ForeignKey('self', null=True, blank=True, verbose_name=_("amend of"), amend_of = models.ForeignKey('self', null=True, blank=True, verbose_name=_("amend of"),
related_name='amends') related_name='amends', on_delete=models.SET_NULL)
type = models.CharField(_("type"), max_length=16, choices=TYPES) type = models.CharField(_("type"), max_length=16, choices=TYPES)
created_on = models.DateField(_("created on"), auto_now_add=True) created_on = models.DateField(_("created on"), auto_now_add=True)
closed_on = models.DateField(_("closed on"), blank=True, null=True, db_index=True) closed_on = models.DateField(_("closed on"), blank=True, null=True, db_index=True)
@ -416,7 +416,7 @@ class ProForma(Bill):
class BillLine(models.Model): class BillLine(models.Model):
""" Base model for bill item representation """ """ Base model for bill item representation """
bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines') bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines', on_delete=models.CASCADE)
description = models.CharField(_("description"), max_length=256) description = models.CharField(_("description"), max_length=256)
rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2) rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2)
quantity = models.DecimalField(_("quantity"), blank=True, null=True, max_digits=12, quantity = models.DecimalField(_("quantity"), blank=True, null=True, max_digits=12,
@ -434,7 +434,7 @@ class BillLine(models.Model):
created_on = models.DateField(_("created"), auto_now_add=True) created_on = models.DateField(_("created"), auto_now_add=True)
# Amendment # Amendment
amended_line = models.ForeignKey('self', verbose_name=_("amended line"), amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
related_name='amendment_lines', null=True, blank=True) related_name='amendment_lines', null=True, blank=True, on_delete=models.CASCADE)
class Meta: class Meta:
get_latest_by = 'id' get_latest_by = 'id'
@ -495,7 +495,7 @@ class BillSubline(models.Model):
) )
# TODO: order info for undoing # TODO: order info for undoing
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines') line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines', on_delete=models.CASCADE)
description = models.CharField(_("description"), max_length=256) description = models.CharField(_("description"), max_length=256)
total = models.DecimalField(max_digits=12, decimal_places=2) total = models.DecimalField(max_digits=12, decimal_places=2)
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER) type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)

View File

@ -29,11 +29,11 @@ class Contact(models.Model):
('ADDS', _("Announcements")), ('ADDS', _("Announcements")),
('EMERGENCY', _("Emergency contact")), ('EMERGENCY', _("Emergency contact")),
) )
objects = ContactQuerySet.as_manager() objects = ContactQuerySet.as_manager()
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='contacts', null=True) related_name='contacts', null=True, on_delete=models.SET_NULL)
short_name = models.CharField(_("short name"), max_length=128) short_name = models.CharField(_("short name"), max_length=128)
full_name = models.CharField(_("full name"), max_length=256, blank=True) full_name = models.CharField(_("full name"), max_length=256, blank=True)
email = models.EmailField() email = models.EmailField()
@ -54,10 +54,10 @@ class Contact(models.Model):
country = models.CharField(_("country"), max_length=20, blank=True, country = models.CharField(_("country"), max_length=20, blank=True,
choices=settings.CONTACTS_COUNTRIES, choices=settings.CONTACTS_COUNTRIES,
default=settings.CONTACTS_DEFAULT_COUNTRY) default=settings.CONTACTS_DEFAULT_COUNTRY)
def __str__(self): def __str__(self):
return self.full_name or self.short_name return self.full_name or self.short_name
def clean(self): def clean(self):
self.short_name = self.short_name.strip() self.short_name = self.short_name.strip()
self.full_name = self.full_name.strip() self.full_name = self.full_name.strip()

View File

@ -12,7 +12,7 @@ class Database(models.Model):
""" Represents a basic database for a web application """ """ Represents a basic database for a web application """
MYSQL = 'mysql' MYSQL = 'mysql'
POSTGRESQL = 'postgresql' POSTGRESQL = 'postgresql'
name = models.CharField(_("name"), max_length=64, # MySQL limit name = models.CharField(_("name"), max_length=64, # MySQL limit
validators=[validators.validate_name]) validators=[validators.validate_name])
users = models.ManyToManyField('databases.DatabaseUser', blank=True, users = models.ManyToManyField('databases.DatabaseUser', blank=True,
@ -20,16 +20,16 @@ class Database(models.Model):
type = models.CharField(_("type"), max_length=32, type = models.CharField(_("type"), max_length=32,
choices=settings.DATABASES_TYPE_CHOICES, choices=settings.DATABASES_TYPE_CHOICES,
default=settings.DATABASES_DEFAULT_TYPE) default=settings.DATABASES_DEFAULT_TYPE)
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
related_name='databases') verbose_name=_("Account"), related_name='databases')
comments = models.TextField(default="", blank=True) comments = models.TextField(default="", blank=True)
class Meta: class Meta:
unique_together = ('name', 'type') unique_together = ('name', 'type')
def __str__(self): def __str__(self):
return "%s" % self.name return "%s" % self.name
@property @property
def owner(self): def owner(self):
""" database owner is the first user related to it """ """ database owner is the first user related to it """
@ -39,7 +39,7 @@ class Database(models.Model):
if user is not None: if user is not None:
return user.databaseuser return user.databaseuser
return None return None
@property @property
def active(self): def active(self):
return self.account.is_active return self.account.is_active
@ -53,26 +53,26 @@ Database.users.through._meta.unique_together = (
class DatabaseUser(models.Model): class DatabaseUser(models.Model):
MYSQL = Database.MYSQL MYSQL = Database.MYSQL
POSTGRESQL = Database.POSTGRESQL POSTGRESQL = Database.POSTGRESQL
username = models.CharField(_("username"), max_length=16, # MySQL usernames 16 char long username = models.CharField(_("username"), max_length=16, # MySQL usernames 16 char long
validators=[validators.validate_name]) validators=[validators.validate_name])
password = models.CharField(_("password"), max_length=256) password = models.CharField(_("password"), max_length=256)
type = models.CharField(_("type"), max_length=32, type = models.CharField(_("type"), max_length=32,
choices=settings.DATABASES_TYPE_CHOICES, choices=settings.DATABASES_TYPE_CHOICES,
default=settings.DATABASES_DEFAULT_TYPE) default=settings.DATABASES_DEFAULT_TYPE)
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
related_name='databaseusers') verbose_name=_("Account"), related_name='databaseusers')
class Meta: class Meta:
verbose_name_plural = _("DB users") verbose_name_plural = _("DB users")
unique_together = ('username', 'type') unique_together = ('username', 'type')
def __str__(self): def __str__(self):
return self.username return self.username
def get_username(self): def get_username(self):
return self.username return self.username
def set_password(self, password): def set_password(self, password):
if self.type == self.MYSQL: if self.type == self.MYSQL:
# MySQL stores sha1(sha1(password).binary).hex # MySQL stores sha1(sha1(password).binary).hex

View File

@ -31,9 +31,9 @@ class Domain(models.Model):
validators.validate_allowed_domain validators.validate_allowed_domain
]) ])
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), blank=True, account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), blank=True,
related_name='domains', help_text=_("Automatically selected for subdomains.")) related_name='domains', on_delete=models.CASCADE, help_text=_("Automatically selected for subdomains."))
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set', top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set',
editable=False, verbose_name=_("top domain")) editable=False, verbose_name=_("top domain"), on_delete=models.CASCADE)
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, editable=False, serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, editable=False,
help_text=_("A revision number that changes whenever this domain is updated.")) help_text=_("A revision number that changes whenever this domain is updated."))
refresh = models.CharField(_("refresh"), max_length=16, blank=True, refresh = models.CharField(_("refresh"), max_length=16, blank=True,
@ -69,16 +69,16 @@ class Domain(models.Model):
blank=True, blank=True,
help_text="A bind-9 'address_match_list' that will be granted permission to perform " help_text="A bind-9 'address_match_list' that will be granted permission to perform "
"dns2136 updates. Chiefly used to enable Let's Encrypt self-service validation.") "dns2136 updates. Chiefly used to enable Let's Encrypt self-service validation.")
objects = DomainQuerySet.as_manager() objects = DomainQuerySet.as_manager()
def __str__(self): def __str__(self):
return self.name return self.name
@property @property
def origin(self): def origin(self):
return self.top or self return self.top or self
@property @property
def is_top(self): def is_top(self):
# don't cache, don't replace by top_id # don't cache, don't replace by top_id
@ -86,14 +86,14 @@ class Domain(models.Model):
return not bool(self.top) return not bool(self.top)
except Domain.DoesNotExist: except Domain.DoesNotExist:
return False return False
@property @property
def subdomains(self): def subdomains(self):
return Domain.objects.filter(name__regex='\.%s$' % self.name) return Domain.objects.filter(name__regex='\.%s$' % self.name)
def clean(self): def clean(self):
self.name = self.name.lower() self.name = self.name.lower()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" create top relation """ """ create top relation """
update = False update = False
@ -110,7 +110,7 @@ class Domain(models.Model):
# queryset.update() is not used because we want to trigger backend to delete ex-topdomains # queryset.update() is not used because we want to trigger backend to delete ex-topdomains
domain.top = self domain.top = self
domain.save(update_fields=('top',)) domain.save(update_fields=('top',))
def get_description(self): def get_description(self):
if self.is_top: if self.is_top:
num = self.subdomains.count() num = self.subdomains.count()
@ -119,21 +119,21 @@ class Domain(models.Model):
_("top domain with %d subdomains") % num, _("top domain with %d subdomains") % num,
num) num)
return _("subdomain") return _("subdomain")
def get_absolute_url(self): def get_absolute_url(self):
return 'http://%s' % self.name return 'http://%s' % self.name
def get_declared_records(self): def get_declared_records(self):
""" proxy method, needed for input validation, see helpers.domain_for_validation """ """ proxy method, needed for input validation, see helpers.domain_for_validation """
return self.records.all() return self.records.all()
def get_subdomains(self): def get_subdomains(self):
""" proxy method, needed for input validation, see helpers.domain_for_validation """ """ proxy method, needed for input validation, see helpers.domain_for_validation """
return self.origin.subdomain_set.all().prefetch_related('records') return self.origin.subdomain_set.all().prefetch_related('records')
def get_parent(self, top=False): def get_parent(self, top=False):
return type(self).objects.get_parent(self.name, top=top) return type(self).objects.get_parent(self.name, top=top)
def render_zone(self): def render_zone(self):
origin = self.origin origin = self.origin
zone = origin.render_records() zone = origin.render_records()
@ -147,7 +147,7 @@ class Domain(models.Model):
for subdomain in sorted(tail, key=lambda x: len(x.name), reverse=True): for subdomain in sorted(tail, key=lambda x: len(x.name), reverse=True):
zone += subdomain.render_records() zone += subdomain.render_records()
return zone.strip() return zone.strip()
def refresh_serial(self): def refresh_serial(self):
""" Increases the domain serial number by one """ """ Increases the domain serial number by one """
serial = utils.generate_zone_serial() serial = utils.generate_zone_serial()
@ -159,7 +159,7 @@ class Domain(models.Model):
serial = int(serial) serial = int(serial)
self.serial = serial self.serial = serial
self.save(update_fields=('serial',)) self.save(update_fields=('serial',))
def get_default_soa(self): def get_default_soa(self):
return ' '.join([ return ' '.join([
"%s." % settings.DOMAINS_DEFAULT_NAME_SERVER, "%s." % settings.DOMAINS_DEFAULT_NAME_SERVER,
@ -170,7 +170,7 @@ class Domain(models.Model):
self.expire or settings.DOMAINS_DEFAULT_EXPIRE, self.expire or settings.DOMAINS_DEFAULT_EXPIRE,
self.min_ttl or settings.DOMAINS_DEFAULT_MIN_TTL, self.min_ttl or settings.DOMAINS_DEFAULT_MIN_TTL,
]) ])
def get_default_records(self): def get_default_records(self):
defaults = [] defaults = []
if self.is_top: if self.is_top:
@ -202,7 +202,7 @@ class Domain(models.Model):
value=default_aaaa value=default_aaaa
)) ))
return defaults return defaults
def record_is_implicit(self, record, types): def record_is_implicit(self, record, types):
if record.type not in types: if record.type not in types:
if record.type is Record.NS: if record.type is Record.NS:
@ -221,7 +221,7 @@ class Domain(models.Model):
elif not has_a and not has_aaaa: elif not has_a and not has_aaaa:
return True return True
return False return False
def get_records(self): def get_records(self):
types = set() types = set()
records = utils.RecordStorage() records = utils.RecordStorage()
@ -249,7 +249,7 @@ class Domain(models.Model):
else: else:
records.append(record) records.append(record)
return records return records
def render_records(self): def render_records(self):
result = '' result = ''
for record in self.get_records(): for record in self.get_records():
@ -273,7 +273,7 @@ class Domain(models.Model):
value=record.value value=record.value
) )
return result return result
def has_default_mx(self): def has_default_mx(self):
records = self.get_records() records = self.get_records()
for record in records.by_type('MX'): for record in records.by_type('MX'):
@ -294,7 +294,7 @@ class Record(models.Model):
TXT = 'TXT' TXT = 'TXT'
SPF = 'SPF' SPF = 'SPF'
SOA = 'SOA' SOA = 'SOA'
TYPE_CHOICES = ( TYPE_CHOICES = (
(MX, "MX"), (MX, "MX"),
(NS, "NS"), (NS, "NS"),
@ -305,7 +305,7 @@ class Record(models.Model):
(TXT, "TXT"), (TXT, "TXT"),
(SPF, "SPF"), (SPF, "SPF"),
) )
VALIDATORS = { VALIDATORS = {
MX: (validators.validate_mx_record,), MX: (validators.validate_mx_record,),
NS: (validators.validate_zone_label,), NS: (validators.validate_zone_label,),
@ -317,8 +317,8 @@ class Record(models.Model):
SRV: (validators.validate_srv_record,), SRV: (validators.validate_srv_record,),
SOA: (validators.validate_soa_record,), SOA: (validators.validate_soa_record,),
} }
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records') domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records', on_delete=models.CASCADE)
ttl = models.CharField(_("TTL"), max_length=8, blank=True, ttl = models.CharField(_("TTL"), max_length=8, blank=True,
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL, help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
validators=[validators.validate_zone_interval]) validators=[validators.validate_zone_interval])
@ -326,10 +326,10 @@ class Record(models.Model):
# max_length bumped from 256 to 1024 (arbitrary) on August 2019. # max_length bumped from 256 to 1024 (arbitrary) on August 2019.
value = models.CharField(_("value"), max_length=1024, value = models.CharField(_("value"), max_length=1024,
help_text=_("MX, NS and CNAME records sould end with a dot.")) help_text=_("MX, NS and CNAME records sould end with a dot."))
def __str__(self): def __str__(self):
return "%s %s IN %s %s" % (self.domain, self.get_ttl(), self.type, self.value) return "%s %s IN %s %s" % (self.domain, self.get_ttl(), self.type, self.value)
def clean(self): def clean(self):
""" validates record value based on its type """ """ validates record value based on its type """
# validate value # validate value
@ -343,6 +343,6 @@ class Record(models.Model):
raise ValidationError({ raise ValidationError({
'value': error, 'value': error,
}) })
def get_ttl(self): def get_ttl(self):
return self.ttl or settings.DOMAINS_DEFAULT_TTL return self.ttl or settings.DOMAINS_DEFAULT_TTL

View File

@ -19,10 +19,10 @@ class Queue(models.Model):
choices=Contact.EMAIL_USAGES, choices=Contact.EMAIL_USAGES,
default=contacts_settings.CONTACTS_DEFAULT_EMAIL_USAGES, default=contacts_settings.CONTACTS_DEFAULT_EMAIL_USAGES,
help_text=_("Contacts to notify by email")) help_text=_("Contacts to notify by email"))
def __str__(self): def __str__(self):
return self.verbose_name or self.name return self.verbose_name or self.name
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" mark as default queue if needed """ """ mark as default queue if needed """
existing_default = Queue.objects.filter(default=True) existing_default = Queue.objects.filter(default=True)
@ -48,7 +48,7 @@ class Ticket(models.Model):
(MEDIUM, 'Medium'), (MEDIUM, 'Medium'),
(LOW, 'Low'), (LOW, 'Low'),
) )
NEW = 'NEW' NEW = 'NEW'
IN_PROGRESS = 'IN_PROGRESS' IN_PROGRESS = 'IN_PROGRESS'
RESOLVED = 'RESOLVED' RESOLVED = 'RESOLVED'
@ -63,7 +63,7 @@ class Ticket(models.Model):
(REJECTED, 'Rejected'), (REJECTED, 'Rejected'),
(CLOSED, 'Closed'), (CLOSED, 'Closed'),
) )
creator = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("created by"), creator = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("created by"),
related_name='tickets_created', null=True, on_delete=models.SET_NULL) related_name='tickets_created', null=True, on_delete=models.SET_NULL)
creator_name = models.CharField(_("creator name"), max_length=256, blank=True) creator_name = models.CharField(_("creator name"), max_length=256, blank=True)
@ -79,15 +79,15 @@ class Ticket(models.Model):
created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True) created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(_("modified"), auto_now=True) updated_at = models.DateTimeField(_("modified"), auto_now=True)
cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"), blank=True) cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"), blank=True)
objects = TicketQuerySet.as_manager() objects = TicketQuerySet.as_manager()
class Meta: class Meta:
ordering = ['-updated_at'] ordering = ['-updated_at']
def __str__(self): def __str__(self):
return str(self.pk) return str(self.pk)
def get_notification_emails(self): def get_notification_emails(self):
""" Get emails of the users related to the ticket """ """ Get emails of the users related to the ticket """
emails = list(settings.ISSUES_SUPPORT_EMAILS) emails = list(settings.ISSUES_SUPPORT_EMAILS)
@ -100,7 +100,7 @@ class Ticket(models.Model):
for message in self.messages.distinct('author'): for message in self.messages.distinct('author'):
emails.append(message.author.email) emails.append(message.author.email)
return set(emails + self.get_cc_emails()) return set(emails + self.get_cc_emails())
def notify(self, message=None, content=None): def notify(self, message=None, content=None):
""" Send an email to ticket stakeholders notifying an state update """ """ Send an email to ticket stakeholders notifying an state update """
emails = self.get_notification_emails() emails = self.get_notification_emails()
@ -111,7 +111,7 @@ class Ticket(models.Model):
'ticket_message': message 'ticket_message': message
} }
send_email_template(template, context, emails, html=html_template) send_email_template(template, context, emails, html=html_template)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" notify stakeholders of new ticket """ """ notify stakeholders of new ticket """
new_issue = not self.pk new_issue = not self.pk
@ -121,60 +121,60 @@ class Ticket(models.Model):
if new_issue: if new_issue:
# PK should be available for rendering the template # PK should be available for rendering the template
self.notify() self.notify()
def is_involved_by(self, user): def is_involved_by(self, user):
""" returns whether user has participated or is referenced on the ticket """ returns whether user has participated or is referenced on the ticket
as owner or member of the group as owner or member of the group
""" """
return Ticket.objects.filter(pk=self.pk).involved_by(user).exists() return Ticket.objects.filter(pk=self.pk).involved_by(user).exists()
def get_cc_emails(self): def get_cc_emails(self):
return self.cc.split(',') if self.cc else [] return self.cc.split(',') if self.cc else []
def mark_as_read_by(self, user): def mark_as_read_by(self, user):
self.trackers.get_or_create(user=user) self.trackers.get_or_create(user=user)
def mark_as_unread_by(self, user): def mark_as_unread_by(self, user):
self.trackers.filter(user=user).delete() self.trackers.filter(user=user).delete()
def mark_as_unread(self): def mark_as_unread(self):
self.trackers.all().delete() self.trackers.all().delete()
def is_read_by(self, user): def is_read_by(self, user):
return self.trackers.filter(user=user).exists() return self.trackers.filter(user=user).exists()
def reject(self): def reject(self):
self.state = Ticket.REJECTED self.state = Ticket.REJECTED
self.save(update_fields=('state', 'updated_at')) self.save(update_fields=('state', 'updated_at'))
def resolve(self): def resolve(self):
self.state = Ticket.RESOLVED self.state = Ticket.RESOLVED
self.save(update_fields=('state', 'updated_at')) self.save(update_fields=('state', 'updated_at'))
def close(self): def close(self):
self.state = Ticket.CLOSED self.state = Ticket.CLOSED
self.save(update_fields=('state', 'updated_at')) self.save(update_fields=('state', 'updated_at'))
def take(self, user): def take(self, user):
self.owner = user self.owner = user
self.save(update_fields=('state', 'updated_at')) self.save(update_fields=('state', 'updated_at'))
class Message(models.Model): class Message(models.Model):
ticket = models.ForeignKey('issues.Ticket', verbose_name=_("ticket"), ticket = models.ForeignKey('issues.Ticket', on_delete=models.CASCADE,
related_name='messages') verbose_name=_("ticket"), related_name='messages')
author = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("author"), author = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
related_name='ticket_messages') verbose_name=_("author"), related_name='ticket_messages')
author_name = models.CharField(_("author name"), max_length=256, blank=True) author_name = models.CharField(_("author name"), max_length=256, blank=True)
content = models.TextField(_("content")) content = models.TextField(_("content"))
created_at = models.DateTimeField(_("created at"), auto_now_add=True) created_at = models.DateTimeField(_("created at"), auto_now_add=True)
class Meta: class Meta:
get_latest_by = 'id' get_latest_by = 'id'
def __str__(self): def __str__(self):
return "#%i" % self.id return "#%i" % self.id
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" notify stakeholders of ticket update """ """ notify stakeholders of ticket update """
if not self.pk: if not self.pk:
@ -183,7 +183,7 @@ class Message(models.Model):
self.ticket.notify(message=self) self.ticket.notify(message=self)
self.author_name = self.author.get_full_name() self.author_name = self.author.get_full_name()
super(Message, self).save(*args, **kwargs) super(Message, self).save(*args, **kwargs)
@property @property
def number(self): def number(self):
return self.ticket.messages.filter(id__lte=self.id).count() return self.ticket.messages.filter(id__lte=self.id).count()
@ -191,10 +191,11 @@ class Message(models.Model):
class TicketTracker(models.Model): class TicketTracker(models.Model):
""" Keeps track of user read tickets """ """ Keeps track of user read tickets """
ticket = models.ForeignKey(Ticket, verbose_name=_("ticket"), related_name='trackers') ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE,
user = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("user"), verbose_name=_("ticket"), related_name='trackers')
related_name='ticket_trackers') user = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
verbose_name=_("user"), related_name='ticket_trackers')
class Meta: class Meta:
unique_together = ( unique_together = (
('ticket', 'user'), ('ticket', 'user'),

View File

@ -30,54 +30,54 @@ class List(models.Model):
admin_email = models.EmailField(_("admin email"), admin_email = models.EmailField(_("admin email"),
help_text=_("Administration email address")) help_text=_("Administration email address"))
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='lists') related_name='lists', on_delete=models.CASCADE)
# TODO also admin # TODO also admin
is_active = models.BooleanField(_("active"), default=True, is_active = models.BooleanField(_("active"), default=True,
help_text=_("Designates whether this account should be treated as active. " help_text=_("Designates whether this account should be treated as active. "
"Unselect this instead of deleting accounts.")) "Unselect this instead of deleting accounts."))
password = None password = None
objects = ListQuerySet.as_manager() objects = ListQuerySet.as_manager()
class Meta: class Meta:
unique_together = ('address_name', 'address_domain') unique_together = ('address_name', 'address_domain')
def __str__(self): def __str__(self):
return self.name return self.name
@property @property
def address(self): def address(self):
if self.address_name and self.address_domain: if self.address_name and self.address_domain:
return "%s@%s" % (self.address_name, self.address_domain) return "%s@%s" % (self.address_name, self.address_domain)
return '' return ''
@cached_property @cached_property
def active(self): def active(self):
return self.is_active and self.account.is_active return self.is_active and self.account.is_active
def clean(self): def clean(self):
if self.address_name and not self.address_domain_id: if self.address_name and not self.address_domain_id:
raise ValidationError({ raise ValidationError({
'address_domain': _("Domain should be selected for provided address name."), 'address_domain': _("Domain should be selected for provided address name."),
}) })
def disable(self): def disable(self):
self.is_active = False self.is_active = False
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def enable(self): def enable(self):
self.is_active = False self.is_active = False
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def get_address_name(self): def get_address_name(self):
return self.address_name or self.name return self.address_name or self.name
def get_username(self): def get_username(self):
return self.name return self.name
def set_password(self, password): def set_password(self, password):
self.password = password self.password = password
def get_absolute_url(self): def get_absolute_url(self):
context = { context = {
'name': self.name 'name': self.name

View File

@ -13,7 +13,7 @@ from . import validators, settings
class Mailbox(models.Model): class Mailbox(models.Model):
CUSTOM = 'CUSTOM' CUSTOM = 'CUSTOM'
name = models.CharField(_("name"), unique=True, db_index=True, name = models.CharField(_("name"), unique=True, db_index=True,
max_length=settings.MAILBOXES_NAME_MAX_LENGTH, max_length=settings.MAILBOXES_NAME_MAX_LENGTH,
help_text=_("Required. %s characters or fewer. Letters, digits and ./-/_ only.") % help_text=_("Required. %s characters or fewer. Letters, digits and ./-/_ only.") %
@ -23,7 +23,7 @@ class Mailbox(models.Model):
]) ])
password = models.CharField(_("password"), max_length=128) password = models.CharField(_("password"), max_length=128)
account = models.ForeignKey('accounts.Account', verbose_name=_("account"), account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='mailboxes') related_name='mailboxes', on_delete=models.CASCADE)
filtering = models.CharField(max_length=16, filtering = models.CharField(max_length=16,
default=settings.MAILBOXES_MAILBOX_DEFAULT_FILTERING, default=settings.MAILBOXES_MAILBOX_DEFAULT_FILTERING,
choices=[(k, v[0]) for k,v in sorted(settings.MAILBOXES_MAILBOX_FILTERINGS.items())]) choices=[(k, v[0]) for k,v in sorted(settings.MAILBOXES_MAILBOX_FILTERINGS.items())])
@ -33,59 +33,59 @@ class Mailbox(models.Model):
"<a href='https://tty1.net/blog/2011/sieve-tutorial_en.html'>sieve language</a>. " "<a href='https://tty1.net/blog/2011/sieve-tutorial_en.html'>sieve language</a>. "
"This overrides any automatic junk email filtering")) "This overrides any automatic junk email filtering"))
is_active = models.BooleanField(_("active"), default=True) is_active = models.BooleanField(_("active"), default=True)
class Meta: class Meta:
verbose_name_plural = _("mailboxes") verbose_name_plural = _("mailboxes")
def __str__(self): def __str__(self):
return self.name return self.name
@cached_property @cached_property
def active(self): def active(self):
try: try:
return self.is_active and self.account.is_active return self.is_active and self.account.is_active
except type(self).account.field.rel.to.DoesNotExist: except type(self).account.field.rel.to.DoesNotExist:
return self.is_active return self.is_active
def disable(self): def disable(self):
self.is_active = False self.is_active = False
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def enable(self): def enable(self):
self.is_active = False self.is_active = False
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def set_password(self, raw_password): def set_password(self, raw_password):
self.password = make_password(raw_password) self.password = make_password(raw_password)
def get_home(self): def get_home(self):
context = { context = {
'name': self.name, 'name': self.name,
'username': self.name, 'username': self.name,
} }
return os.path.normpath(settings.MAILBOXES_HOME % context) return os.path.normpath(settings.MAILBOXES_HOME % context)
def clean(self): def clean(self):
if self.filtering == self.CUSTOM and not self.custom_filtering: if self.filtering == self.CUSTOM and not self.custom_filtering:
raise ValidationError({ raise ValidationError({
'custom_filtering': _("Custom filtering is selected but not provided.") 'custom_filtering': _("Custom filtering is selected but not provided.")
}) })
def get_filtering(self): def get_filtering(self):
name, content = settings.MAILBOXES_MAILBOX_FILTERINGS[self.filtering] name, content = settings.MAILBOXES_MAILBOX_FILTERINGS[self.filtering]
if callable(content): if callable(content):
# Custom filtering # Custom filtering
content = content(self) content = content(self)
return (name, content) return (name, content)
def get_local_address(self): def get_local_address(self):
if not settings.MAILBOXES_LOCAL_DOMAIN: if not settings.MAILBOXES_LOCAL_DOMAIN:
raise AttributeError("Mailboxes do not have a defined local address domain.") raise AttributeError("Mailboxes do not have a defined local address domain.")
return '@'.join((self.name, settings.MAILBOXES_LOCAL_DOMAIN)) return '@'.join((self.name, settings.MAILBOXES_LOCAL_DOMAIN))
def get_forwards(self): def get_forwards(self):
return Address.objects.filter(forward__regex=r'(^|.*\s)%s(\s.*|$)' % self.name) return Address.objects.filter(forward__regex=r'(^|.*\s)%s(\s.*|$)' % self.name)
def get_addresses(self): def get_addresses(self):
mboxes = self.addresses.all() mboxes = self.addresses.all()
forwards = self.get_forwards() forwards = self.get_forwards()
@ -97,33 +97,33 @@ class Address(models.Model):
validators=[validators.validate_emailname], validators=[validators.validate_emailname],
help_text=_("Address name, left blank for a <i>catch-all</i> address")) help_text=_("Address name, left blank for a <i>catch-all</i> address"))
domain = models.ForeignKey(settings.MAILBOXES_DOMAIN_MODEL, domain = models.ForeignKey(settings.MAILBOXES_DOMAIN_MODEL,
verbose_name=_("domain"), related_name='addresses') verbose_name=_("domain"), related_name='addresses', on_delete=models.CASCADE)
mailboxes = models.ManyToManyField(Mailbox, verbose_name=_("mailboxes"), mailboxes = models.ManyToManyField(Mailbox, verbose_name=_("mailboxes"),
related_name='addresses', blank=True) related_name='addresses', blank=True)
forward = models.CharField(_("forward"), max_length=256, blank=True, forward = models.CharField(_("forward"), max_length=256, blank=True,
validators=[validators.validate_forward], validators=[validators.validate_forward],
help_text=_("Space separated email addresses or mailboxes")) help_text=_("Space separated email addresses or mailboxes"))
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='addresses') related_name='addresses', on_delete=models.CASCADE)
class Meta: class Meta:
verbose_name_plural = _("addresses") verbose_name_plural = _("addresses")
unique_together = ('name', 'domain') unique_together = ('name', 'domain')
def __str__(self): def __str__(self):
return self.email return self.email
@property @property
def email(self): def email(self):
return "%s@%s" % (self.name, self.domain) return "%s@%s" % (self.name, self.domain)
@cached_property @cached_property
def destination(self): def destination(self):
destinations = list(self.mailboxes.values_list('name', flat=True)) destinations = list(self.mailboxes.values_list('name', flat=True))
if self.forward: if self.forward:
destinations += self.forward.split() destinations += self.forward.split()
return ' '.join(destinations) return ' '.join(destinations)
def clean(self): def clean(self):
errors = defaultdict(list) errors = defaultdict(list)
local_domain = settings.MAILBOXES_LOCAL_DOMAIN local_domain = settings.MAILBOXES_LOCAL_DOMAIN
@ -149,7 +149,7 @@ class Address(models.Model):
) )
if errors: if errors:
raise ValidationError(errors) raise ValidationError(errors)
def get_forward_mailboxes(self): def get_forward_mailboxes(self):
rm_local_domain = re.compile(r'@%s$' % settings.MAILBOXES_LOCAL_DOMAIN) rm_local_domain = re.compile(r'@%s$' % settings.MAILBOXES_LOCAL_DOMAIN)
mailboxes = [] mailboxes = []
@ -158,7 +158,7 @@ class Address(models.Model):
if '@' not in forward: if '@' not in forward:
mailboxes.append(forward) mailboxes.append(forward)
return Mailbox.objects.filter(name__in=mailboxes) return Mailbox.objects.filter(name__in=mailboxes)
def get_mailboxes(self): def get_mailboxes(self):
for mailbox in self.mailboxes.all(): for mailbox in self.mailboxes.all():
yield mailbox yield mailbox
@ -168,11 +168,11 @@ class Address(models.Model):
class Autoresponse(models.Model): class Autoresponse(models.Model):
address = models.OneToOneField(Address, verbose_name=_("address"), address = models.OneToOneField(Address, verbose_name=_("address"),
related_name='autoresponse') related_name='autoresponse', on_delete=models.CASCADE)
# TODO initial_date # TODO initial_date
subject = models.CharField(_("subject"), max_length=256) subject = models.CharField(_("subject"), max_length=256)
message = models.TextField(_("message")) message = models.TextField(_("message"))
enabled = models.BooleanField(_("enabled"), default=False) enabled = models.BooleanField(_("enabled"), default=False)
def __str__(self): def __str__(self):
return self.address return self.address

View File

@ -15,7 +15,7 @@ class Message(models.Model):
(DEFERRED, _("Deferred")), (DEFERRED, _("Deferred")),
(FAILED, _("Failed")), (FAILED, _("Failed")),
) )
CRITICAL = 0 CRITICAL = 0
HIGH = 1 HIGH = 1
NORMAL = 2 NORMAL = 2
@ -26,7 +26,7 @@ class Message(models.Model):
(NORMAL, _("Normal")), (NORMAL, _("Normal")),
(LOW, _("Low")), (LOW, _("Low")),
) )
state = models.CharField(_("State"), max_length=16, choices=STATES, default=QUEUED, state = models.CharField(_("State"), max_length=16, choices=STATES, default=QUEUED,
db_index=True) db_index=True)
priority = models.PositiveIntegerField(_("Priority"), choices=PRIORITIES, default=NORMAL, priority = models.PositiveIntegerField(_("Priority"), choices=PRIORITIES, default=NORMAL,
@ -38,21 +38,21 @@ class Message(models.Model):
created_at = models.DateTimeField(_("created at"), auto_now_add=True) created_at = models.DateTimeField(_("created at"), auto_now_add=True)
retries = models.PositiveIntegerField(_("retries"), default=0, db_index=True) retries = models.PositiveIntegerField(_("retries"), default=0, db_index=True)
last_try = models.DateTimeField(_("last try"), null=True, db_index=True) last_try = models.DateTimeField(_("last try"), null=True, db_index=True)
def __str__(self): def __str__(self):
return '%s to %s' % (self.subject, self.to_address) return '%s to %s' % (self.subject, self.to_address)
def defer(self): def defer(self):
self.state = self.DEFERRED self.state = self.DEFERRED
# Max tries # Max tries
if self.retries >= len(settings.MAILER_DEFERE_SECONDS): if self.retries >= len(settings.MAILER_DEFERE_SECONDS):
self.state = self.FAILED self.state = self.FAILED
self.save(update_fields=('state',)) self.save(update_fields=('state',))
def sent(self): def sent(self):
self.state = self.SENT self.state = self.SENT
self.save(update_fields=('state',)) self.save(update_fields=('state',))
def log(self, error): def log(self, error):
result = SMTPLog.SUCCESS result = SMTPLog.SUCCESS
if error: if error:
@ -67,7 +67,7 @@ class SMTPLog(models.Model):
(SUCCESS, _("Success")), (SUCCESS, _("Success")),
(FAILURE, _("Failure")), (FAILURE, _("Failure")),
) )
message = models.ForeignKey(Message, editable=False, related_name='logs') message = models.ForeignKey(Message, editable=False, related_name='logs', on_delete=models.CASCADE)
result = models.CharField(max_length=16, choices=RESULTS, default=SUCCESS) result = models.CharField(max_length=16, choices=RESULTS, default=SUCCESS)
date = models.DateTimeField(auto_now_add=True) date = models.DateTimeField(auto_now_add=True)
log_message = models.TextField() log_message = models.TextField()

View File

@ -22,30 +22,30 @@ class MiscService(models.Model):
is_active = models.BooleanField(_("active"), default=True, is_active = models.BooleanField(_("active"), default=True,
help_text=_("Whether new instances of this service can be created " help_text=_("Whether new instances of this service can be created "
"or not. Unselect this instead of deleting services.")) "or not. Unselect this instead of deleting services."))
def __str__(self): def __str__(self):
return self.name return self.name
def clean(self): def clean(self):
self.verbose_name = self.verbose_name.strip() self.verbose_name = self.verbose_name.strip()
def get_verbose_name(self): def get_verbose_name(self):
return self.verbose_name or self.name return self.verbose_name or self.name
def disable(self): def disable(self):
self.is_active = False self.is_active = False
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def enable(self): def enable(self):
self.is_active = False self.is_active = False
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
class Miscellaneous(models.Model): class Miscellaneous(models.Model):
service = models.ForeignKey(MiscService, verbose_name=_("service"), service = models.ForeignKey(MiscService, on_delete=models.CASCADE,
related_name='instances') verbose_name=_("service"), related_name='instances')
account = models.ForeignKey('accounts.Account', verbose_name=_("account"), account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
related_name='miscellaneous') verbose_name=_("account"), related_name='miscellaneous')
identifier = NullableCharField(_("identifier"), max_length=256, null=True, unique=True, identifier = NullableCharField(_("identifier"), max_length=256, null=True, unique=True,
db_index=True, help_text=_("A unique identifier for this service.")) db_index=True, help_text=_("A unique identifier for this service."))
description = models.TextField(_("description"), blank=True) description = models.TextField(_("description"), blank=True)
@ -53,32 +53,32 @@ class Miscellaneous(models.Model):
is_active = models.BooleanField(_("active"), default=True, is_active = models.BooleanField(_("active"), default=True,
help_text=_("Designates whether this service should be treated as " help_text=_("Designates whether this service should be treated as "
"active. Unselect this instead of deleting services.")) "active. Unselect this instead of deleting services."))
class Meta: class Meta:
verbose_name_plural = _("miscellaneous") verbose_name_plural = _("miscellaneous")
def __str__(self): def __str__(self):
return self.identifier or self.description[:32] or str(self.service) return self.identifier or self.description[:32] or str(self.service)
@cached_property @cached_property
def active(self): def active(self):
return self.is_active and self.service.is_active and self.account.is_active return self.is_active and self.service.is_active and self.account.is_active
def get_description(self): def get_description(self):
return ' '.join((str(self.amount), self.service.description or self.service.verbose_name)) return ' '.join((str(self.amount), self.service.description or self.service.verbose_name))
def disable(self): def disable(self):
self.is_active = False self.is_active = False
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def enable(self): def enable(self):
self.is_active = False self.is_active = False
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
@cached_property @cached_property
def service_class(self): def service_class(self):
return self.service return self.service
def clean(self): def clean(self):
if self.identifier: if self.identifier:
self.identifier = self.identifier.strip().lower() self.identifier = self.identifier.strip().lower()

View File

@ -90,7 +90,7 @@ class BackendLog(models.Model):
backend = models.CharField(_("backend"), max_length=256) backend = models.CharField(_("backend"), max_length=256)
state = models.CharField(_("state"), max_length=16, choices=STATES, default=RECEIVED) state = models.CharField(_("state"), max_length=16, choices=STATES, default=RECEIVED)
server = models.ForeignKey(Server, verbose_name=_("server"), related_name='execution_logs') server = models.ForeignKey(Server, verbose_name=_("server"), related_name='execution_logs', on_delete=models.CASCADE)
script = models.TextField(_("script")) script = models.TextField(_("script"))
stdout = models.TextField(_("stdout")) stdout = models.TextField(_("stdout"))
stderr = models.TextField(_("stderr")) stderr = models.TextField(_("stderr"))
@ -135,10 +135,10 @@ class BackendOperation(models.Model):
""" """
Encapsulates an operation, storing its related object, the action and the backend. Encapsulates an operation, storing its related object, the action and the backend.
""" """
log = models.ForeignKey('orchestration.BackendLog', related_name='operations') log = models.ForeignKey('orchestration.BackendLog', related_name='operations', on_delete=models.CASCADE)
backend = models.CharField(_("backend"), max_length=256) backend = models.CharField(_("backend"), max_length=256)
action = models.CharField(_("action"), max_length=64) action = models.CharField(_("action"), max_length=64)
content_type = models.ForeignKey(ContentType) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField(null=True) object_id = models.PositiveIntegerField(null=True)
instance_repr = models.CharField(_("instance representation"), max_length=256) instance_repr = models.CharField(_("instance representation"), max_length=256)
@ -199,7 +199,7 @@ class Route(models.Model):
""" """
backend = models.CharField(_("backend"), max_length=256, backend = models.CharField(_("backend"), max_length=256,
choices=ServiceBackend.get_choices()) choices=ServiceBackend.get_choices())
host = models.ForeignKey(Server, verbose_name=_("host"), related_name='routes') host = models.ForeignKey(Server, verbose_name=_("host"), related_name='routes', on_delete=models.CASCADE)
match = models.CharField(_("match"), max_length=256, blank=True, default='True', match = models.CharField(_("match"), max_length=256, blank=True, default='True',
help_text=_("Python expression used for selecting the targe host, " help_text=_("Python expression used for selecting the targe host, "
"<em>instance</em> referes to the current object.")) "<em>instance</em> referes to the current object."))

View File

@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
class OrderQuerySet(models.QuerySet): class OrderQuerySet(models.QuerySet):
group_by = queryset.group_by group_by = queryset.group_by
def bill(self, **options): def bill(self, **options):
bills = [] bills = []
bill_backend = Order.get_bill_backend() bill_backend = Order.get_bill_backend()
@ -46,17 +46,17 @@ class OrderQuerySet(models.QuerySet):
if commit: if commit:
return list(set(bills)) return list(set(bills))
return bills return bills
def givers(self, ini, end): def givers(self, ini, end):
return self.cancelled_and_billed().filter(billed_until__gt=ini, registered_on__lt=end) return self.cancelled_and_billed().filter(billed_until__gt=ini, registered_on__lt=end)
def cancelled_and_billed(self, exclude=False): def cancelled_and_billed(self, exclude=False):
qs = dict(cancelled_on__isnull=False, billed_until__isnull=False, qs = dict(cancelled_on__isnull=False, billed_until__isnull=False,
cancelled_on__lte=F('billed_until')) cancelled_on__lte=F('billed_until'))
if exclude: if exclude:
return self.exclude(**qs) return self.exclude(**qs)
return self.filter(**qs) return self.filter(**qs)
def get_related(self, **options): def get_related(self, **options):
""" returns related orders that could have a pricing effect """ """ returns related orders that could have a pricing effect """
Service = apps.get_model(settings.ORDERS_SERVICE_MODEL) Service = apps.get_model(settings.ORDERS_SERVICE_MODEL)
@ -86,25 +86,25 @@ class OrderQuerySet(models.QuerySet):
return self.model.objects.none() return self.model.objects.none()
ids = self.values_list('id', flat=True) ids = self.values_list('id', flat=True)
return self.model.objects.filter(qs).exclude(id__in=ids) return self.model.objects.filter(qs).exclude(id__in=ids)
def pricing_orders(self, ini, end): def pricing_orders(self, ini, end):
return self.filter(billed_until__isnull=False, billed_until__gt=ini, return self.filter(billed_until__isnull=False, billed_until__gt=ini,
registered_on__lt=end) registered_on__lt=end)
def by_object(self, obj, **kwargs): def by_object(self, obj, **kwargs):
ct = ContentType.objects.get_for_model(obj) ct = ContentType.objects.get_for_model(obj)
return self.filter(object_id=obj.pk, content_type=ct, **kwargs) return self.filter(object_id=obj.pk, content_type=ct, **kwargs)
def active(self, **kwargs): def active(self, **kwargs):
""" return active orders """ """ return active orders """
return self.filter( return self.filter(
Q(cancelled_on__isnull=True) | Q(cancelled_on__gt=timezone.now()) Q(cancelled_on__isnull=True) | Q(cancelled_on__gt=timezone.now())
).filter(**kwargs) ).filter(**kwargs)
def inactive(self, **kwargs): def inactive(self, **kwargs):
""" return inactive orders """ """ return inactive orders """
return self.filter(cancelled_on__lte=timezone.now(), **kwargs) return self.filter(cancelled_on__lte=timezone.now(), **kwargs)
def update_by_instance(self, instance, service=None, commit=True): def update_by_instance(self, instance, service=None, commit=True):
updates = [] updates = []
if service is None: if service is None:
@ -150,12 +150,12 @@ class OrderQuerySet(models.QuerySet):
class Order(models.Model): class Order(models.Model):
account = models.ForeignKey('accounts.Account', verbose_name=_("account"), account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
related_name='orders') verbose_name=_("account"), related_name='orders')
content_type = models.ForeignKey(ContentType) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField(null=True) object_id = models.PositiveIntegerField(null=True)
service = models.ForeignKey(settings.ORDERS_SERVICE_MODEL, verbose_name=_("service"), service = models.ForeignKey(settings.ORDERS_SERVICE_MODEL, on_delete=models.PROTECT,
related_name='orders') verbose_name=_("service"), related_name='orders')
registered_on = models.DateField(_("registered"), default=timezone.now, db_index=True) registered_on = models.DateField(_("registered"), default=timezone.now, db_index=True)
cancelled_on = models.DateField(_("cancelled"), null=True, blank=True) cancelled_on = models.DateField(_("cancelled"), null=True, blank=True)
billed_on = models.DateField(_("billed"), null=True, blank=True) billed_on = models.DateField(_("billed"), null=True, blank=True)
@ -166,29 +166,29 @@ class Order(models.Model):
description = models.TextField(_("description"), blank=True) description = models.TextField(_("description"), blank=True)
content_object_repr = models.CharField(_("content object representation"), max_length=256, content_object_repr = models.CharField(_("content object representation"), max_length=256,
editable=False, help_text=_("Used for searches.")) editable=False, help_text=_("Used for searches."))
content_object = GenericForeignKey() content_object = GenericForeignKey()
objects = OrderQuerySet.as_manager() objects = OrderQuerySet.as_manager()
class Meta: class Meta:
get_latest_by = 'id' get_latest_by = 'id'
index_together = ( index_together = (
('content_type', 'object_id'), ('content_type', 'object_id'),
) )
def __str__(self): def __str__(self):
return str(self.service) return str(self.service)
@classmethod @classmethod
def get_bill_backend(cls): def get_bill_backend(cls):
return import_class(settings.ORDERS_BILLING_BACKEND)() return import_class(settings.ORDERS_BILLING_BACKEND)()
def clean(self): def clean(self):
if self.billed_on and self.billed_on < self.registered_on: if self.billed_on and self.billed_on < self.registered_on:
raise ValidationError(_("Billed date can not be earlier than registered on.")) raise ValidationError(_("Billed date can not be earlier than registered on."))
if self.billed_until and not self.billed_on: if self.billed_until and not self.billed_on:
raise ValidationError(_("Billed on is missing while billed until is being provided.")) raise ValidationError(_("Billed on is missing while billed until is being provided."))
def update(self): def update(self):
instance = self.content_object instance = self.content_object
if instance is None: if instance is None:
@ -214,22 +214,22 @@ class Order(models.Model):
update_fields.append('content_object_repr') update_fields.append('content_object_repr')
if update_fields: if update_fields:
self.save(update_fields=update_fields) self.save(update_fields=update_fields)
def cancel(self, commit=True): def cancel(self, commit=True):
self.cancelled_on = timezone.now() self.cancelled_on = timezone.now()
self.ignore = self.service.handler.get_order_ignore(self) self.ignore = self.service.handler.get_order_ignore(self)
if commit: if commit:
self.save(update_fields=['cancelled_on', 'ignore']) self.save(update_fields=['cancelled_on', 'ignore'])
logger.info("CANCELLED order id: {id}".format(id=self.id)) logger.info("CANCELLED order id: {id}".format(id=self.id))
def mark_as_ignored(self): def mark_as_ignored(self):
self.ignore = True self.ignore = True
self.save(update_fields=['ignore']) self.save(update_fields=['ignore'])
def mark_as_not_ignored(self): def mark_as_not_ignored(self):
self.ignore = False self.ignore = False
self.save(update_fields=['ignore']) self.save(update_fields=['ignore'])
def get_metric(self, *args, **kwargs): def get_metric(self, *args, **kwargs):
if kwargs.pop('changes', False): if kwargs.pop('changes', False):
ini, end = args ini, end = args
@ -294,16 +294,17 @@ class MetricStorageQuerySet(models.QuerySet):
class MetricStorage(models.Model): class MetricStorage(models.Model):
""" Stores metric state for future billing """ """ Stores metric state for future billing """
order = models.ForeignKey(Order, verbose_name=_("order"), related_name='metrics') order = models.ForeignKey(Order, on_delete=models.CASCADE,
verbose_name=_("order"), related_name='metrics')
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2) value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
created_on = models.DateField(_("created"), auto_now_add=True, editable=True) created_on = models.DateField(_("created"), auto_now_add=True, editable=True)
# TODO time field? # TODO time field?
updated_on = models.DateTimeField(_("updated")) updated_on = models.DateTimeField(_("updated"))
objects = MetricStorageQuerySet.as_manager() objects = MetricStorageQuerySet.as_manager()
class Meta: class Meta:
get_latest_by = 'id' get_latest_by = 'id'
def __str__(self): def __str__(self):
return str(self.order) return str(self.order)

View File

@ -18,50 +18,50 @@ class PaymentSourcesQueryset(models.QuerySet):
class PaymentSource(models.Model): class PaymentSource(models.Model):
account = models.ForeignKey('accounts.Account', verbose_name=_("account"), account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='paymentsources') related_name='paymentsources', on_delete=models.CASCADE)
method = models.CharField(_("method"), max_length=32, method = models.CharField(_("method"), max_length=32,
choices=PaymentMethod.get_choices()) choices=PaymentMethod.get_choices())
data = JSONField(_("data"), default={}) data = JSONField(_("data"), default={})
is_active = models.BooleanField(_("active"), default=True) is_active = models.BooleanField(_("active"), default=True)
objects = PaymentSourcesQueryset.as_manager() objects = PaymentSourcesQueryset.as_manager()
def __str__(self): def __str__(self):
return "%s (%s)" % (self.label, self.method_class.verbose_name) return "%s (%s)" % (self.label, self.method_class.verbose_name)
@cached_property @cached_property
def method_class(self): def method_class(self):
return PaymentMethod.get(self.method) return PaymentMethod.get(self.method)
@cached_property @cached_property
def method_instance(self): def method_instance(self):
""" Per request lived method_instance """ """ Per request lived method_instance """
return self.method_class(self) return self.method_class(self)
@cached_property @cached_property
def label(self): def label(self):
return self.method_instance.get_label() return self.method_instance.get_label()
@cached_property @cached_property
def number(self): def number(self):
return self.method_instance.get_number() return self.method_instance.get_number()
def get_bill_context(self): def get_bill_context(self):
method = self.method_instance method = self.method_instance
return { return {
'message': method.get_bill_message(), 'message': method.get_bill_message(),
} }
def get_due_delta(self): def get_due_delta(self):
return self.method_instance.due_delta return self.method_instance.due_delta
def clean(self): def clean(self):
self.data = self.method_instance.clean_data() self.data = self.method_instance.clean_data()
class TransactionQuerySet(models.QuerySet): class TransactionQuerySet(models.QuerySet):
group_by = group_by group_by = group_by
def create(self, **kwargs): def create(self, **kwargs):
source = kwargs.get('source') source = kwargs.get('source')
if source is None or not hasattr(source.method_class, 'process'): if source is None or not hasattr(source.method_class, 'process'):
@ -71,16 +71,16 @@ class TransactionQuerySet(models.QuerySet):
if amount == 0: if amount == 0:
kwargs['state'] = self.model.SECURED kwargs['state'] = self.model.SECURED
return super(TransactionQuerySet, self).create(**kwargs) return super(TransactionQuerySet, self).create(**kwargs)
def secured(self): def secured(self):
return self.filter(state=Transaction.SECURED) return self.filter(state=Transaction.SECURED)
def exclude_rejected(self): def exclude_rejected(self):
return self.exclude(state=Transaction.REJECTED) return self.exclude(state=Transaction.REJECTED)
def amount(self): def amount(self):
return next(iter(self.aggregate(models.Sum('amount')).values())) or 0 return next(iter(self.aggregate(models.Sum('amount')).values())) or 0
def processing(self): def processing(self):
return self.filter(state__in=[Transaction.EXECUTED, Transaction.WAITTING_EXECUTION]) return self.filter(state__in=[Transaction.EXECUTED, Transaction.WAITTING_EXECUTION])
@ -108,8 +108,8 @@ class Transaction(models.Model):
REJECTED: _("The transaction has failed and the ammount is lost, a new transaction " REJECTED: _("The transaction has failed and the ammount is lost, a new transaction "
"should be created for recharging."), "should be created for recharging."),
} }
bill = models.ForeignKey('bills.bill', verbose_name=_("bill"), bill = models.ForeignKey('bills.bill', on_delete=models.CASCADE, verbose_name=_("bill"),
related_name='transactions') related_name='transactions')
source = models.ForeignKey(PaymentSource, null=True, blank=True, on_delete=models.SET_NULL, source = models.ForeignKey(PaymentSource, null=True, blank=True, on_delete=models.SET_NULL,
verbose_name=_("source"), related_name='transactions') verbose_name=_("source"), related_name='transactions')
@ -121,16 +121,16 @@ class Transaction(models.Model):
currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY) currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY)
created_at = models.DateTimeField(_("created"), auto_now_add=True) created_at = models.DateTimeField(_("created"), auto_now_add=True)
modified_at = models.DateTimeField(_("modified"), auto_now=True) modified_at = models.DateTimeField(_("modified"), auto_now=True)
objects = TransactionQuerySet.as_manager() objects = TransactionQuerySet.as_manager()
def __str__(self): def __str__(self):
return "#%i" % self.id return "#%i" % self.id
@property @property
def account(self): def account(self):
return self.bill.account return self.bill.account
def clean(self): def clean(self):
if not self.pk: if not self.pk:
amount = self.bill.transactions.exclude(state=self.REJECTED).amount() amount = self.bill.transactions.exclude(state=self.REJECTED).amount()
@ -141,24 +141,24 @@ class Transaction(models.Model):
'amount': amount, 'amount': amount,
} }
) )
def get_state_help(self): def get_state_help(self):
if self.source: if self.source:
return self.source.method_instance.state_help.get(self.state) or self.STATE_HELP.get(self.state) return self.source.method_instance.state_help.get(self.state) or self.STATE_HELP.get(self.state)
return self.STATE_HELP.get(self.state) return self.STATE_HELP.get(self.state)
def mark_as_processed(self): def mark_as_processed(self):
self.state = self.WAITTING_EXECUTION self.state = self.WAITTING_EXECUTION
self.save(update_fields=('state', 'modified_at')) self.save(update_fields=('state', 'modified_at'))
def mark_as_executed(self): def mark_as_executed(self):
self.state = self.EXECUTED self.state = self.EXECUTED
self.save(update_fields=('state', 'modified_at')) self.save(update_fields=('state', 'modified_at'))
def mark_as_secured(self): def mark_as_secured(self):
self.state = self.SECURED self.state = self.SECURED
self.save(update_fields=('state', 'modified_at')) self.save(update_fields=('state', 'modified_at'))
def mark_as_rejected(self): def mark_as_rejected(self):
self.state = self.REJECTED self.state = self.REJECTED
self.save(update_fields=('state', 'modified_at')) self.save(update_fields=('state', 'modified_at'))
@ -178,31 +178,31 @@ class TransactionProcess(models.Model):
(ABORTED, _("Aborted")), (ABORTED, _("Aborted")),
(COMMITED, _("Commited")), (COMMITED, _("Commited")),
) )
data = JSONField(_("data"), blank=True) data = JSONField(_("data"), blank=True)
file = PrivateFileField(_("file"), blank=True) file = PrivateFileField(_("file"), blank=True)
state = models.CharField(_("state"), max_length=16, choices=STATES, default=CREATED) state = models.CharField(_("state"), max_length=16, choices=STATES, default=CREATED)
created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True) created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(_("updated"), auto_now=True) updated_at = models.DateTimeField(_("updated"), auto_now=True)
class Meta: class Meta:
verbose_name_plural = _("Transaction processes") verbose_name_plural = _("Transaction processes")
def __str__(self): def __str__(self):
return '#%i' % self.id return '#%i' % self.id
def mark_as_executed(self): def mark_as_executed(self):
self.state = self.EXECUTED self.state = self.EXECUTED
for transaction in self.transactions.all(): for transaction in self.transactions.all():
transaction.mark_as_executed() transaction.mark_as_executed()
self.save(update_fields=('state', 'updated_at')) self.save(update_fields=('state', 'updated_at'))
def abort(self): def abort(self):
self.state = self.ABORTED self.state = self.ABORTED
for transaction in self.transactions.all(): for transaction in self.transactions.all():
transaction.mark_as_rejected() transaction.mark_as_rejected()
self.save(update_fields=('state', 'updated_at')) self.save(update_fields=('state', 'updated_at'))
def commit(self): def commit(self):
self.state = self.COMMITED self.state = self.COMMITED
for transaction in self.transactions.processing(): for transaction in self.transactions.processing():

View File

@ -25,32 +25,33 @@ class Plan(models.Model):
help_text=_("Designates whether this plan can be combined with other plans or not.")) help_text=_("Designates whether this plan can be combined with other plans or not."))
allow_multiple = models.BooleanField(_("allow multiple"), default=False, allow_multiple = models.BooleanField(_("allow multiple"), default=False,
help_text=_("Designates whether this plan allow for multiple contractions.")) help_text=_("Designates whether this plan allow for multiple contractions."))
def __str__(self): def __str__(self):
return self.get_verbose_name() return self.get_verbose_name()
def clean(self): def clean(self):
self.verbose_name = self.verbose_name.strip() self.verbose_name = self.verbose_name.strip()
def get_verbose_name(self): def get_verbose_name(self):
return self.verbose_name or self.name return self.verbose_name or self.name
class ContractedPlan(models.Model): class ContractedPlan(models.Model):
plan = models.ForeignKey(Plan, verbose_name=_("plan"), related_name='contracts') plan = models.ForeignKey(Plan, on_delete=models.CASCADE,
account = models.ForeignKey('accounts.Account', verbose_name=_("account"), verbose_name=_("plan"), related_name='contracts')
related_name='plans') account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
verbose_name=_("account"), related_name='plans')
class Meta: class Meta:
verbose_name_plural = _("plans") verbose_name_plural = _("plans")
def __str__(self): def __str__(self):
return str(self.plan) return str(self.plan)
@cached_property @cached_property
def active(self): def active(self):
return self.plan.is_active and self.account.is_active return self.plan.is_active and self.account.is_active
def clean(self): def clean(self):
if not self.pk and not self.plan.allow_multiple: if not self.pk and not self.plan.allow_multiple:
if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists(): if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists():
@ -59,7 +60,7 @@ class ContractedPlan(models.Model):
class RateQuerySet(models.QuerySet): class RateQuerySet(models.QuerySet):
group_by = queryset.group_by group_by = queryset.group_by
def by_account(self, account): def by_account(self, account):
# Default allways selected # Default allways selected
return self.filter( return self.filter(
@ -69,27 +70,27 @@ class RateQuerySet(models.QuerySet):
class Rate(models.Model): class Rate(models.Model):
service = models.ForeignKey('services.Service', verbose_name=_("service"), service = models.ForeignKey('services.Service', on_delete=models.CASCADE,
related_name='rates') verbose_name=_("service"), related_name='rates')
plan = models.ForeignKey(Plan, verbose_name=_("plan"), related_name='rates', null=True, plan = models.ForeignKey(Plan, on_delete=models.SET_NULL, null=True, blank=True,
blank=True) verbose_name=_("plan"), related_name='rates')
quantity = models.PositiveIntegerField(_("quantity"), null=True, blank=True, quantity = models.PositiveIntegerField(_("quantity"), null=True, blank=True,
help_text=_("See rate algorihm help text.")) help_text=_("See rate algorihm help text."))
price = models.DecimalField(_("price"), max_digits=12, decimal_places=2) price = models.DecimalField(_("price"), max_digits=12, decimal_places=2)
objects = RateQuerySet.as_manager() objects = RateQuerySet.as_manager()
class Meta: class Meta:
unique_together = ('service', 'plan', 'quantity') unique_together = ('service', 'plan', 'quantity')
def __str__(self): def __str__(self):
return "{}-{}".format(str(self.price), self.quantity) return "{}-{}".format(str(self.price), self.quantity)
@classmethod @classmethod
@lru_cache() @lru_cache()
def get_methods(cls): def get_methods(cls):
return dict((method, import_class(method)) for method in settings.PLANS_RATE_METHODS) return dict((method, import_class(method)) for method in settings.PLANS_RATE_METHODS)
@classmethod @classmethod
@lru_cache() @lru_cache()
def get_choices(cls): def get_choices(cls):
@ -97,7 +98,7 @@ class Rate(models.Model):
for name, method in cls.get_methods().items(): for name, method in cls.get_methods().items():
choices.append((name, method.verbose_name)) choices.append((name, method.verbose_name))
return choices return choices
@classmethod @classmethod
def get_default(cls): def get_default(cls):
return settings.PLANS_DEFAULT_RATE_METHOD return settings.PLANS_DEFAULT_RATE_METHOD

View File

@ -42,7 +42,7 @@ class Resource(models.Model):
"digits and hyphen only."), "digits and hyphen only."),
validators=[validators.validate_name]) validators=[validators.validate_name])
verbose_name = models.CharField(_("verbose name"), max_length=256) verbose_name = models.CharField(_("verbose name"), max_length=256)
content_type = models.ForeignKey(ContentType, content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE,
help_text=_("Model where this resource will be hooked.")) help_text=_("Model where this resource will be hooked."))
aggregation = models.CharField(_("aggregation"), max_length=16, aggregation = models.CharField(_("aggregation"), max_length=16,
choices=Aggregation.get_choices(), default=Aggregation.get_choices()[0][0], choices=Aggregation.get_choices(), default=Aggregation.get_choices()[0][0],
@ -178,8 +178,8 @@ class ResourceDataQuerySet(models.QuerySet):
class ResourceData(models.Model): class ResourceData(models.Model):
""" Stores computed resource usage and allocation """ """ Stores computed resource usage and allocation """
resource = models.ForeignKey(Resource, related_name='dataset', verbose_name=_("resource")) resource = models.ForeignKey(Resource, on_delete=models.CASCADE, related_name='dataset', verbose_name=_("resource"))
content_type = models.ForeignKey(ContentType, verbose_name=_("content type")) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, verbose_name=_("content type"))
object_id = models.PositiveIntegerField(_("object id")) object_id = models.PositiveIntegerField(_("object id"))
used = models.DecimalField(_("used"), max_digits=16, decimal_places=3, null=True, used = models.DecimalField(_("used"), max_digits=16, decimal_places=3, null=True,
editable=False) editable=False)
@ -267,7 +267,7 @@ class MonitorData(models.Model):
""" Stores monitored data """ """ Stores monitored data """
monitor = models.CharField(_("monitor"), max_length=256, db_index=True, monitor = models.CharField(_("monitor"), max_length=256, db_index=True,
choices=ServiceMonitor.get_choices()) choices=ServiceMonitor.get_choices())
content_type = models.ForeignKey(ContentType, verbose_name=_("content type")) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, verbose_name=_("content type"))
object_id = models.PositiveIntegerField(_("object id")) object_id = models.PositiveIntegerField(_("object id"))
created_at = models.DateTimeField(_("created"), default=timezone.now, db_index=True) created_at = models.DateTimeField(_("created"), default=timezone.now, db_index=True)
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2) value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)

View File

@ -26,8 +26,8 @@ class SaaS(models.Model):
name = models.CharField(_("Name"), max_length=64, name = models.CharField(_("Name"), max_length=64,
help_text=_("Required. 64 characters or fewer. Letters, digits and ./- only."), help_text=_("Required. 64 characters or fewer. Letters, digits and ./- only."),
validators=[validators.validate_hostname]) validators=[validators.validate_hostname])
account = models.ForeignKey('accounts.Account', verbose_name=_("account"), account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
related_name='saas') verbose_name=_("account"), related_name='saas')
is_active = models.BooleanField(_("active"), default=True, is_active = models.BooleanField(_("active"), default=True,
help_text=_("Designates whether this service should be treated as active. ")) help_text=_("Designates whether this service should be treated as active. "))
data = JSONField(_("data"), default={}, data = JSONField(_("data"), default={},
@ -36,51 +36,52 @@ class SaaS(models.Model):
help_text=_("Optional and alternative URL for accessing this service instance. " help_text=_("Optional and alternative URL for accessing this service instance. "
"i.e. <tt>https://wiki.mydomain/doku/</tt><br>" "i.e. <tt>https://wiki.mydomain/doku/</tt><br>"
"A related website will be automatically configured if needed.")) "A related website will be automatically configured if needed."))
database = models.ForeignKey('databases.Database', null=True, blank=True) database = models.ForeignKey('databases.Database',
on_delete=models.SET_NULL, null=True, blank=True)
# Some SaaS sites may need a database, with this virtual field we tell the ORM to delete them # Some SaaS sites may need a database, with this virtual field we tell the ORM to delete them
databases = VirtualDatabaseRelation('databases.Database') databases = VirtualDatabaseRelation('databases.Database')
objects = SaaSQuerySet.as_manager() objects = SaaSQuerySet.as_manager()
class Meta: class Meta:
verbose_name = "SaaS" verbose_name = "SaaS"
verbose_name_plural = "SaaS" verbose_name_plural = "SaaS"
unique_together = ( unique_together = (
('name', 'service'), ('name', 'service'),
) )
def __str__(self): def __str__(self):
return "%s@%s" % (self.name, self.service) return "%s@%s" % (self.name, self.service)
@cached_property @cached_property
def service_class(self): def service_class(self):
return SoftwareService.get(self.service) return SoftwareService.get(self.service)
@cached_property @cached_property
def service_instance(self): def service_instance(self):
""" Per request lived service_instance """ """ Per request lived service_instance """
return self.service_class(self) return self.service_class(self)
@cached_property @cached_property
def active(self): def active(self):
return self.is_active and self.account.is_active return self.is_active and self.account.is_active
def disable(self): def disable(self):
self.is_active = False self.is_active = False
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def enable(self): def enable(self):
self.is_active = True self.is_active = True
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def clean(self): def clean(self):
if not self.pk: if not self.pk:
self.name = self.name.lower() self.name = self.name.lower()
self.service_instance.clean() self.service_instance.clean()
self.data = self.service_instance.clean_data() self.data = self.service_instance.clean_data()
def get_site_domain(self): def get_site_domain(self):
return self.service_instance.get_site_domain() return self.service_instance.get_site_domain()
def set_password(self, password): def set_password(self, password):
self.password = password self.password = password

View File

@ -53,12 +53,13 @@ class Service(models.Model):
REFUND = 'REFUND' REFUND = 'REFUND'
PREPAY = 'PREPAY' PREPAY = 'PREPAY'
POSTPAY = 'POSTPAY' POSTPAY = 'POSTPAY'
_ignore_types = ' and '.join( _ignore_types = ' and '.join(
', '.join(settings.SERVICES_IGNORE_ACCOUNT_TYPE).rsplit(', ', 1)).lower() ', '.join(settings.SERVICES_IGNORE_ACCOUNT_TYPE).rsplit(', ', 1)).lower()
description = models.CharField(_("description"), max_length=256, unique=True) description = models.CharField(_("description"), max_length=256, unique=True)
content_type = models.ForeignKey(ContentType, verbose_name=_("content type"), content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE,
verbose_name=_("content type"),
help_text=_("Content type of the related service objects.")) help_text=_("Content type of the related service objects."))
match = models.CharField(_("match"), max_length=256, blank=True, match = models.CharField(_("match"), max_length=256, blank=True,
help_text=_( help_text=_(
@ -168,19 +169,19 @@ class Service(models.Model):
(POSTPAY, _("Postpay (on demand)")), (POSTPAY, _("Postpay (on demand)")),
), ),
default=PREPAY) default=PREPAY)
objects = ServiceQuerySet.as_manager() objects = ServiceQuerySet.as_manager()
def __str__(self): def __str__(self):
return self.description return self.description
@cached_property @cached_property
def handler(self): def handler(self):
""" Accessor of this service handler instance """ """ Accessor of this service handler instance """
if self.handler_type: if self.handler_type:
return ServiceHandler.get(self.handler_type)(self) return ServiceHandler.get(self.handler_type)(self)
return ServiceHandler(self) return ServiceHandler(self)
def clean(self): def clean(self):
self.description = self.description.strip() self.description = self.description.strip()
if hasattr(self, 'content_type'): if hasattr(self, 'content_type'):
@ -190,12 +191,12 @@ class Service(models.Model):
'metric': (self.handler.validate_metric, self), 'metric': (self.handler.validate_metric, self),
'order_description': (self.handler.validate_order_description, self), 'order_description': (self.handler.validate_order_description, self),
}) })
def get_pricing_period(self): def get_pricing_period(self):
if self.pricing_period == self.BILLING_PERIOD: if self.pricing_period == self.BILLING_PERIOD:
return self.billing_period return self.billing_period
return self.pricing_period return self.pricing_period
def get_price(self, account, metric, rates=None, position=None): def get_price(self, account, metric, rates=None, position=None):
""" """
if position is provided an specific price for that position is returned, if position is provided an specific price for that position is returned,
@ -233,7 +234,7 @@ class Service(models.Model):
price = round(rate['price'], 2) price = round(rate['price'], 2)
return decimal.Decimal(str(rate['price'])) return decimal.Decimal(str(rate['price']))
raise RuntimeError("Rating algorithm bad result") raise RuntimeError("Rating algorithm bad result")
def get_rates(self, account, cache=True): def get_rates(self, account, cache=True):
# rates are cached per account # rates are cached per account
if not cache: if not cache:
@ -246,11 +247,11 @@ class Service(models.Model):
rates = self.rates.by_account(account) rates = self.rates.by_account(account)
self.__cached_rates[account.id] = rates self.__cached_rates[account.id] = rates
return rates return rates
@property @property
def rate_method(self): def rate_method(self):
return rate_class.get_methods()[self.rate_algorithm] return rate_class.get_methods()[self.rate_algorithm]
def update_orders(self, commit=True): def update_orders(self, commit=True):
order_model = apps.get_model(settings.SERVICES_ORDER_MODEL) order_model = apps.get_model(settings.SERVICES_ORDER_MODEL)
manager = order_model.objects manager = order_model.objects

View File

@ -19,7 +19,7 @@ class SystemUserQuerySet(models.QuerySet):
user.set_password(password) user.set_password(password)
user.save(update_fields=['password']) user.save(update_fields=['password'])
return user return user
def by_is_main(self, is_main=True, **kwargs): def by_is_main(self, is_main=True, **kwargs):
if is_main: if is_main:
return self.filter(account__main_systemuser_id=F('id')) return self.filter(account__main_systemuser_id=F('id'))
@ -30,7 +30,7 @@ class SystemUserQuerySet(models.QuerySet):
class SystemUser(models.Model): class SystemUser(models.Model):
""" """
System users System users
Username max_length determined by LINUX system user/group lentgh: 32 Username max_length determined by LINUX system user/group lentgh: 32
""" """
username = models.CharField(_("username"), max_length=32, unique=True, username = models.CharField(_("username"), max_length=32, unique=True,
@ -38,7 +38,7 @@ class SystemUser(models.Model):
validators=[validators.validate_username]) validators=[validators.validate_username])
password = models.CharField(_("password"), max_length=128) password = models.CharField(_("password"), max_length=128)
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='systemusers') related_name='systemusers', on_delete=models.CASCADE)
home = models.CharField(_("home"), max_length=256, blank=True, home = models.CharField(_("home"), max_length=256, blank=True,
help_text=_("Starting location when login with this no-shell user.")) help_text=_("Starting location when login with this no-shell user."))
directory = models.CharField(_("directory"), max_length=256, blank=True, directory = models.CharField(_("directory"), max_length=256, blank=True,
@ -51,19 +51,19 @@ class SystemUser(models.Model):
is_active = models.BooleanField(_("active"), default=True, is_active = models.BooleanField(_("active"), default=True,
help_text=_("Designates whether this account should be treated as active. " help_text=_("Designates whether this account should be treated as active. "
"Unselect this instead of deleting accounts.")) "Unselect this instead of deleting accounts."))
objects = SystemUserQuerySet.as_manager() objects = SystemUserQuerySet.as_manager()
def __str__(self): def __str__(self):
return self.username return self.username
@cached_property @cached_property
def active(self): def active(self):
try: try:
return self.is_active and self.account.is_active return self.is_active and self.account.is_active
except type(self).account.field.rel.to.DoesNotExist: except type(self).account.field.rel.to.DoesNotExist:
return self.is_active return self.is_active
@cached_property @cached_property
def is_main(self): def is_main(self):
# TODO on account delete # TODO on account delete
@ -71,34 +71,34 @@ class SystemUser(models.Model):
if self.account.main_systemuser_id: if self.account.main_systemuser_id:
return self.account.main_systemuser_id == self.pk return self.account.main_systemuser_id == self.pk
return self.account.username == self.username return self.account.username == self.username
@cached_property @cached_property
def main(self): def main(self):
# On account creation main_systemuser_id is still None # On account creation main_systemuser_id is still None
if self.account.main_systemuser_id: if self.account.main_systemuser_id:
return self.account.main_systemuser return self.account.main_systemuser
return type(self).objects.get(username=self.account.username) return type(self).objects.get(username=self.account.username)
@property @property
def has_shell(self): def has_shell(self):
return self.shell not in settings.SYSTEMUSERS_DISABLED_SHELLS return self.shell not in settings.SYSTEMUSERS_DISABLED_SHELLS
def disable(self): def disable(self):
self.is_active = False self.is_active = False
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def enable(self): def enable(self):
self.is_active = True self.is_active = True
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def get_description(self): def get_description(self):
return self.get_shell_display() return self.get_shell_display()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.home: if not self.home:
self.home = self.get_base_home() self.home = self.get_base_home()
super(SystemUser, self).save(*args, **kwargs) super(SystemUser, self).save(*args, **kwargs)
def clean(self): def clean(self):
self.directory = self.directory.lstrip('/') self.directory = self.directory.lstrip('/')
if self.home: if self.home:
@ -123,16 +123,16 @@ class SystemUser(models.Model):
raise ValidationError({ raise ValidationError({
'home': _("Shell users should use their own home."), 'home': _("Shell users should use their own home."),
}) })
def set_password(self, raw_password): def set_password(self, raw_password):
self.password = make_password(raw_password) self.password = make_password(raw_password)
def get_base_home(self): def get_base_home(self):
context = { context = {
'user': self.username, 'user': self.username,
'username': self.username, 'username': self.username,
} }
return os.path.normpath(settings.SYSTEMUSERS_HOME % context) return os.path.normpath(settings.SYSTEMUSERS_HOME % context)
def get_home(self): def get_home(self):
return os.path.normpath(os.path.join(self.home, self.directory)) return os.path.normpath(os.path.join(self.home, self.directory))

View File

@ -15,31 +15,31 @@ class VPS(models.Model):
template = models.CharField(_("template"), max_length=64, template = models.CharField(_("template"), max_length=64,
choices=settings.VPS_TEMPLATES, default=settings.VPS_DEFAULT_TEMPLATE, choices=settings.VPS_TEMPLATES, default=settings.VPS_DEFAULT_TEMPLATE,
help_text=_("Initial template.")) help_text=_("Initial template."))
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
related_name='vpss') verbose_name=_("Account"), related_name='vpss')
is_active = models.BooleanField(_("active"), default=True) is_active = models.BooleanField(_("active"), default=True)
class Meta: class Meta:
verbose_name = "VPS" verbose_name = "VPS"
verbose_name_plural = "VPSs" verbose_name_plural = "VPSs"
def __str__(self): def __str__(self):
return self.hostname return self.hostname
def set_password(self, raw_password): def set_password(self, raw_password):
self.password = make_password(raw_password) self.password = make_password(raw_password)
def get_username(self): def get_username(self):
return self.hostname return self.hostname
def disable(self): def disable(self):
self.is_active = False self.is_active = False
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def enable(self): def enable(self):
self.is_active = False self.is_active = False
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
@property @property
def active(self): def active(self):
return self.is_active and self.account.is_active return self.is_active and self.account.is_active

View File

@ -19,44 +19,44 @@ class WebApp(models.Model):
name = models.CharField(_("name"), max_length=128, validators=[validators.validate_name], name = models.CharField(_("name"), max_length=128, validators=[validators.validate_name],
help_text=_("The app will be installed in %s") % settings.WEBAPPS_BASE_DIR) help_text=_("The app will be installed in %s") % settings.WEBAPPS_BASE_DIR)
type = models.CharField(_("type"), max_length=32, choices=AppType.get_choices()) type = models.CharField(_("type"), max_length=32, choices=AppType.get_choices())
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
related_name='webapps') verbose_name=_("Account"), related_name='webapps')
data = JSONField(_("data"), blank=True, default={}, data = JSONField(_("data"), blank=True, default={},
help_text=_("Extra information dependent of each service.")) help_text=_("Extra information dependent of each service."))
target_server = models.ForeignKey('orchestration.Server', verbose_name=_("Target Server"), target_server = models.ForeignKey('orchestration.Server', on_delete=models.CASCADE,
related_name='webapps') verbose_name=_("Target Server"), related_name='webapps')
comments = models.TextField(default="", blank=True) comments = models.TextField(default="", blank=True)
# CMS webapps usually need a database and dbuser, with these virtual fields we tell the ORM to delete them # CMS webapps usually need a database and dbuser, with these virtual fields we tell the ORM to delete them
databases = VirtualDatabaseRelation('databases.Database') databases = VirtualDatabaseRelation('databases.Database')
databaseusers = VirtualDatabaseUserRelation('databases.DatabaseUser') databaseusers = VirtualDatabaseUserRelation('databases.DatabaseUser')
class Meta: class Meta:
unique_together = ('name', 'account') unique_together = ('name', 'account')
verbose_name = _("Web App") verbose_name = _("Web App")
verbose_name_plural = _("Web Apps") verbose_name_plural = _("Web Apps")
def __str__(self): def __str__(self):
return self.name return self.name
def get_description(self): def get_description(self):
return self.get_type_display() return self.get_type_display()
@cached_property @cached_property
def type_class(self): def type_class(self):
return AppType.get(self.type) return AppType.get(self.type)
@cached_property @cached_property
def type_instance(self): def type_instance(self):
""" Per request lived type_instance """ """ Per request lived type_instance """
return self.type_class(self) return self.type_class(self)
def clean(self): def clean(self):
apptype = self.type_instance apptype = self.type_instance
apptype.validate() apptype.validate()
a = apptype.clean_data() a = apptype.clean_data()
self.data = apptype.clean_data() self.data = apptype.clean_data()
def get_options(self, **kwargs): def get_options(self, **kwargs):
options = OrderedDict() options = OrderedDict()
qs = WebAppOption.objects.filter(**kwargs) qs = WebAppOption.objects.filter(**kwargs)
@ -69,57 +69,57 @@ class WebApp(models.Model):
else: else:
options[name] = value options[name] = value
return options return options
def get_directive(self): def get_directive(self):
return self.type_instance.get_directive() return self.type_instance.get_directive()
def get_base_path(self): def get_base_path(self):
context = { context = {
'home': self.get_user().get_home(), 'home': self.get_user().get_home(),
'app_name': self.name, 'app_name': self.name,
} }
return settings.WEBAPPS_BASE_DIR % context return settings.WEBAPPS_BASE_DIR % context
def get_path(self): def get_path(self):
path = self.get_base_path() path = self.get_base_path()
public_root = self.options.filter(name='public-root').first() public_root = self.options.filter(name='public-root').first()
if public_root: if public_root:
path = os.path.join(path, public_root.value) path = os.path.join(path, public_root.value)
return os.path.normpath(path.replace('//', '/')) return os.path.normpath(path.replace('//', '/'))
def get_user(self): def get_user(self):
return self.account.main_systemuser return self.account.main_systemuser
def get_username(self): def get_username(self):
return self.get_user().username return self.get_user().username
def get_groupname(self): def get_groupname(self):
return self.get_username() return self.get_username()
class WebAppOption(models.Model): class WebAppOption(models.Model):
webapp = models.ForeignKey(WebApp, verbose_name=_("Web application"), webapp = models.ForeignKey(WebApp, on_delete=models.CASCADE,
related_name='options') verbose_name=_("Web application"), related_name='options')
name = models.CharField(_("name"), max_length=128, name = models.CharField(_("name"), max_length=128,
choices=AppType.get_group_options_choices()) choices=AppType.get_group_options_choices())
value = models.CharField(_("value"), max_length=256) value = models.CharField(_("value"), max_length=256)
class Meta: class Meta:
unique_together = ('webapp', 'name') unique_together = ('webapp', 'name')
verbose_name = _("option") verbose_name = _("option")
verbose_name_plural = _("options") verbose_name_plural = _("options")
def __str__(self): def __str__(self):
return self.name return self.name
@cached_property @cached_property
def option_class(self): def option_class(self):
return AppOption.get(self.name) return AppOption.get(self.name)
@cached_property @cached_property
def option_instance(self): def option_instance(self):
""" Per request lived option instance """ """ Per request lived option instance """
return self.option_class(self) return self.option_class(self)
def clean(self): def clean(self):
self.option_instance.validate() self.option_instance.validate()

View File

@ -18,11 +18,11 @@ class Website(models.Model):
HTTPS = 'https' HTTPS = 'https'
HTTP_AND_HTTPS = 'http/https' HTTP_AND_HTTPS = 'http/https'
HTTPS_ONLY = 'https-only' HTTPS_ONLY = 'https-only'
name = models.CharField(_("name"), max_length=128, name = models.CharField(_("name"), max_length=128,
validators=[validators.validate_name]) validators=[validators.validate_name])
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
related_name='websites') verbose_name=_("Account"), related_name='websites')
protocol = models.CharField(_("protocol"), max_length=16, protocol = models.CharField(_("protocol"), max_length=16,
choices=settings.WEBSITES_PROTOCOL_CHOICES, choices=settings.WEBSITES_PROTOCOL_CHOICES,
default=settings.WEBSITES_DEFAULT_PROTOCOL, default=settings.WEBSITES_DEFAULT_PROTOCOL,
@ -34,34 +34,34 @@ class Website(models.Model):
domains = models.ManyToManyField(settings.WEBSITES_DOMAIN_MODEL, blank=True, domains = models.ManyToManyField(settings.WEBSITES_DOMAIN_MODEL, blank=True,
related_name='websites', verbose_name=_("domains")) related_name='websites', verbose_name=_("domains"))
contents = models.ManyToManyField('webapps.WebApp', through='websites.Content') contents = models.ManyToManyField('webapps.WebApp', through='websites.Content')
target_server = models.ForeignKey('orchestration.Server', verbose_name=_("Target Server"), target_server = models.ForeignKey('orchestration.Server', on_delete=models.CASCADE,
related_name='websites') verbose_name=_("Target Server"), related_name='websites')
is_active = models.BooleanField(_("active"), default=True) is_active = models.BooleanField(_("active"), default=True)
comments = models.TextField(default="", blank=True) comments = models.TextField(default="", blank=True)
class Meta: class Meta:
unique_together = ('name', 'account') unique_together = ('name', 'account')
def __str__(self): def __str__(self):
return self.name return self.name
@property @property
def unique_name(self): def unique_name(self):
context = self.get_settings_context() context = self.get_settings_context()
return settings.WEBSITES_UNIQUE_NAME_FORMAT % context return settings.WEBSITES_UNIQUE_NAME_FORMAT % context
@cached_property @cached_property
def active(self): def active(self):
return self.is_active and self.account.is_active return self.is_active and self.account.is_active
def disable(self): def disable(self):
self.is_active = False self.is_active = False
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def enable(self): def enable(self):
self.is_active = False self.is_active = False
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))
def get_settings_context(self): def get_settings_context(self):
""" format settings strings """ """ format settings strings """
return { return {
@ -73,12 +73,12 @@ class Website(models.Model):
'site_name': self.name, 'site_name': self.name,
'protocol': self.protocol, 'protocol': self.protocol,
} }
def get_protocol(self): def get_protocol(self):
if self.protocol in (self.HTTP, self.HTTP_AND_HTTPS): if self.protocol in (self.HTTP, self.HTTP_AND_HTTPS):
return self.HTTP return self.HTTP
return self.HTTPS return self.HTTPS
@cached @cached
def get_directives(self): def get_directives(self):
directives = OrderedDict() directives = OrderedDict()
@ -88,7 +88,7 @@ class Website(models.Model):
except KeyError: except KeyError:
directives[opt.name] = [opt.value] directives[opt.name] = [opt.value]
return directives return directives
def get_absolute_url(self): def get_absolute_url(self):
try: try:
domain = self.domains.all()[0] domain = self.domains.all()[0]
@ -96,22 +96,22 @@ class Website(models.Model):
return return
else: else:
return '%s://%s' % (self.get_protocol(), domain) return '%s://%s' % (self.get_protocol(), domain)
def get_user(self): def get_user(self):
return self.account.main_systemuser return self.account.main_systemuser
def get_username(self): def get_username(self):
return self.get_user().username return self.get_user().username
def get_groupname(self): def get_groupname(self):
return self.get_username() return self.get_username()
def get_www_access_log_path(self): def get_www_access_log_path(self):
context = self.get_settings_context() context = self.get_settings_context()
context['unique_name'] = self.unique_name context['unique_name'] = self.unique_name
path = settings.WEBSITES_WEBSITE_WWW_ACCESS_LOG_PATH % context path = settings.WEBSITES_WEBSITE_WWW_ACCESS_LOG_PATH % context
return os.path.normpath(path) return os.path.normpath(path)
def get_www_error_log_path(self): def get_www_error_log_path(self):
context = self.get_settings_context() context = self.get_settings_context()
context['unique_name'] = self.unique_name context['unique_name'] = self.unique_name
@ -120,52 +120,54 @@ class Website(models.Model):
class WebsiteDirective(models.Model): class WebsiteDirective(models.Model):
website = models.ForeignKey(Website, verbose_name=_("web site"), website = models.ForeignKey(Website, on_delete=models.CASCADE,
related_name='directives') verbose_name=_("web site"), related_name='directives')
name = models.CharField(_("name"), max_length=128, db_index=True, name = models.CharField(_("name"), max_length=128, db_index=True,
choices=SiteDirective.get_choices()) choices=SiteDirective.get_choices())
value = models.CharField(_("value"), max_length=256, blank=True) value = models.CharField(_("value"), max_length=256, blank=True)
def __str__(self): def __str__(self):
return self.name return self.name
@cached_property @cached_property
def directive_class(self): def directive_class(self):
return SiteDirective.get(self.name) return SiteDirective.get(self.name)
@cached_property @cached_property
def directive_instance(self): def directive_instance(self):
""" Per request lived directive instance """ """ Per request lived directive instance """
return self.directive_class() return self.directive_class()
def clean(self): def clean(self):
self.directive_instance.validate(self) self.directive_instance.validate(self)
class Content(models.Model): class Content(models.Model):
# related_name is content_set to differentiate between website.content -> webapp # related_name is content_set to differentiate between website.content -> webapp
webapp = models.ForeignKey('webapps.WebApp', verbose_name=_("web application")) webapp = models.ForeignKey('webapps.WebApp', on_delete=models.CASCADE,
website = models.ForeignKey('websites.Website', verbose_name=_("web site")) verbose_name=_("web application"))
website = models.ForeignKey('websites.Website', on_delete=models.CASCADE,
verbose_name=_("web site"))
path = models.CharField(_("path"), max_length=256, blank=True, path = models.CharField(_("path"), max_length=256, blank=True,
validators=[validators.validate_url_path]) validators=[validators.validate_url_path])
class Meta: class Meta:
unique_together = ('website', 'path') unique_together = ('website', 'path')
def __str__(self): def __str__(self):
try: try:
return self.website.name + self.path return self.website.name + self.path
except Website.DoesNotExist: except Website.DoesNotExist:
return self.path return self.path
def clean_fields(self, *args, **kwargs): def clean_fields(self, *args, **kwargs):
self.path = self.path.strip() self.path = self.path.strip()
return super(Content, self).clean_fields(*args, **kwargs) return super(Content, self).clean_fields(*args, **kwargs)
def clean(self): def clean(self):
if not self.path: if not self.path:
self.path = '/' self.path = '/'
def get_absolute_url(self): def get_absolute_url(self):
try: try:
domain = self.website.domains.all()[0] domain = self.website.domains.all()[0]