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')
])
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)
full_name = models.CharField(_("full name"), max_length=256)
email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))

View file

@ -24,7 +24,7 @@ from . import settings
class BillContact(models.Model):
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,
help_text=_("Account full name will be used when left blank."))
address = models.TextField(_("address"))
@ -102,9 +102,9 @@ class Bill(models.Model):
number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
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"),
related_name='amends')
related_name='amends', on_delete=models.SET_NULL)
type = models.CharField(_("type"), max_length=16, choices=TYPES)
created_on = models.DateField(_("created on"), auto_now_add=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):
""" 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)
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,
@ -434,7 +434,7 @@ class BillLine(models.Model):
created_on = models.DateField(_("created"), auto_now_add=True)
# Amendment
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:
get_latest_by = 'id'
@ -495,7 +495,7 @@ class BillSubline(models.Model):
)
# 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)
total = models.DecimalField(max_digits=12, decimal_places=2)
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)

View file

@ -29,11 +29,11 @@ class Contact(models.Model):
('ADDS', _("Announcements")),
('EMERGENCY', _("Emergency contact")),
)
objects = ContactQuerySet.as_manager()
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)
full_name = models.CharField(_("full name"), max_length=256, blank=True)
email = models.EmailField()
@ -54,10 +54,10 @@ class Contact(models.Model):
country = models.CharField(_("country"), max_length=20, blank=True,
choices=settings.CONTACTS_COUNTRIES,
default=settings.CONTACTS_DEFAULT_COUNTRY)
def __str__(self):
return self.full_name or self.short_name
def clean(self):
self.short_name = self.short_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 """
MYSQL = 'mysql'
POSTGRESQL = 'postgresql'
name = models.CharField(_("name"), max_length=64, # MySQL limit
validators=[validators.validate_name])
users = models.ManyToManyField('databases.DatabaseUser', blank=True,
@ -20,16 +20,16 @@ class Database(models.Model):
type = models.CharField(_("type"), max_length=32,
choices=settings.DATABASES_TYPE_CHOICES,
default=settings.DATABASES_DEFAULT_TYPE)
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='databases')
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
verbose_name=_("Account"), related_name='databases')
comments = models.TextField(default="", blank=True)
class Meta:
unique_together = ('name', 'type')
def __str__(self):
return "%s" % self.name
@property
def owner(self):
""" database owner is the first user related to it """
@ -39,7 +39,7 @@ class Database(models.Model):
if user is not None:
return user.databaseuser
return None
@property
def active(self):
return self.account.is_active
@ -53,26 +53,26 @@ Database.users.through._meta.unique_together = (
class DatabaseUser(models.Model):
MYSQL = Database.MYSQL
POSTGRESQL = Database.POSTGRESQL
username = models.CharField(_("username"), max_length=16, # MySQL usernames 16 char long
validators=[validators.validate_name])
password = models.CharField(_("password"), max_length=256)
type = models.CharField(_("type"), max_length=32,
choices=settings.DATABASES_TYPE_CHOICES,
default=settings.DATABASES_DEFAULT_TYPE)
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='databaseusers')
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
verbose_name=_("Account"), related_name='databaseusers')
class Meta:
verbose_name_plural = _("DB users")
unique_together = ('username', 'type')
def __str__(self):
return self.username
def get_username(self):
return self.username
def set_password(self, password):
if self.type == self.MYSQL:
# MySQL stores sha1(sha1(password).binary).hex

View file

@ -31,9 +31,9 @@ class Domain(models.Model):
validators.validate_allowed_domain
])
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',
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,
help_text=_("A revision number that changes whenever this domain is updated."))
refresh = models.CharField(_("refresh"), max_length=16, blank=True,
@ -69,16 +69,16 @@ class Domain(models.Model):
blank=True,
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.")
objects = DomainQuerySet.as_manager()
def __str__(self):
return self.name
@property
def origin(self):
return self.top or self
@property
def is_top(self):
# don't cache, don't replace by top_id
@ -86,14 +86,14 @@ class Domain(models.Model):
return not bool(self.top)
except Domain.DoesNotExist:
return False
@property
def subdomains(self):
return Domain.objects.filter(name__regex='\.%s$' % self.name)
def clean(self):
self.name = self.name.lower()
def save(self, *args, **kwargs):
""" create top relation """
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
domain.top = self
domain.save(update_fields=('top',))
def get_description(self):
if self.is_top:
num = self.subdomains.count()
@ -119,21 +119,21 @@ class Domain(models.Model):
_("top domain with %d subdomains") % num,
num)
return _("subdomain")
def get_absolute_url(self):
return 'http://%s' % self.name
def get_declared_records(self):
""" proxy method, needed for input validation, see helpers.domain_for_validation """
return self.records.all()
def get_subdomains(self):
""" proxy method, needed for input validation, see helpers.domain_for_validation """
return self.origin.subdomain_set.all().prefetch_related('records')
def get_parent(self, top=False):
return type(self).objects.get_parent(self.name, top=top)
def render_zone(self):
origin = self.origin
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):
zone += subdomain.render_records()
return zone.strip()
def refresh_serial(self):
""" Increases the domain serial number by one """
serial = utils.generate_zone_serial()
@ -159,7 +159,7 @@ class Domain(models.Model):
serial = int(serial)
self.serial = serial
self.save(update_fields=('serial',))
def get_default_soa(self):
return ' '.join([
"%s." % settings.DOMAINS_DEFAULT_NAME_SERVER,
@ -170,7 +170,7 @@ class Domain(models.Model):
self.expire or settings.DOMAINS_DEFAULT_EXPIRE,
self.min_ttl or settings.DOMAINS_DEFAULT_MIN_TTL,
])
def get_default_records(self):
defaults = []
if self.is_top:
@ -202,7 +202,7 @@ class Domain(models.Model):
value=default_aaaa
))
return defaults
def record_is_implicit(self, record, types):
if record.type not in types:
if record.type is Record.NS:
@ -221,7 +221,7 @@ class Domain(models.Model):
elif not has_a and not has_aaaa:
return True
return False
def get_records(self):
types = set()
records = utils.RecordStorage()
@ -249,7 +249,7 @@ class Domain(models.Model):
else:
records.append(record)
return records
def render_records(self):
result = ''
for record in self.get_records():
@ -273,7 +273,7 @@ class Domain(models.Model):
value=record.value
)
return result
def has_default_mx(self):
records = self.get_records()
for record in records.by_type('MX'):
@ -294,7 +294,7 @@ class Record(models.Model):
TXT = 'TXT'
SPF = 'SPF'
SOA = 'SOA'
TYPE_CHOICES = (
(MX, "MX"),
(NS, "NS"),
@ -305,7 +305,7 @@ class Record(models.Model):
(TXT, "TXT"),
(SPF, "SPF"),
)
VALIDATORS = {
MX: (validators.validate_mx_record,),
NS: (validators.validate_zone_label,),
@ -317,8 +317,8 @@ class Record(models.Model):
SRV: (validators.validate_srv_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,
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
validators=[validators.validate_zone_interval])
@ -326,10 +326,10 @@ class Record(models.Model):
# max_length bumped from 256 to 1024 (arbitrary) on August 2019.
value = models.CharField(_("value"), max_length=1024,
help_text=_("MX, NS and CNAME records sould end with a dot."))
def __str__(self):
return "%s %s IN %s %s" % (self.domain, self.get_ttl(), self.type, self.value)
def clean(self):
""" validates record value based on its type """
# validate value
@ -343,6 +343,6 @@ class Record(models.Model):
raise ValidationError({
'value': error,
})
def get_ttl(self):
return self.ttl or settings.DOMAINS_DEFAULT_TTL

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@ class Message(models.Model):
(DEFERRED, _("Deferred")),
(FAILED, _("Failed")),
)
CRITICAL = 0
HIGH = 1
NORMAL = 2
@ -26,7 +26,7 @@ class Message(models.Model):
(NORMAL, _("Normal")),
(LOW, _("Low")),
)
state = models.CharField(_("State"), max_length=16, choices=STATES, default=QUEUED,
db_index=True)
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)
retries = models.PositiveIntegerField(_("retries"), default=0, db_index=True)
last_try = models.DateTimeField(_("last try"), null=True, db_index=True)
def __str__(self):
return '%s to %s' % (self.subject, self.to_address)
def defer(self):
self.state = self.DEFERRED
# Max tries
if self.retries >= len(settings.MAILER_DEFERE_SECONDS):
self.state = self.FAILED
self.save(update_fields=('state',))
def sent(self):
self.state = self.SENT
self.save(update_fields=('state',))
def log(self, error):
result = SMTPLog.SUCCESS
if error:
@ -67,7 +67,7 @@ class SMTPLog(models.Model):
(SUCCESS, _("Success")),
(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)
date = models.DateTimeField(auto_now_add=True)
log_message = models.TextField()

View file

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

View file

@ -90,7 +90,7 @@ class BackendLog(models.Model):
backend = models.CharField(_("backend"), max_length=256)
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"))
stdout = models.TextField(_("stdout"))
stderr = models.TextField(_("stderr"))
@ -135,10 +135,10 @@ class BackendOperation(models.Model):
"""
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)
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)
instance_repr = models.CharField(_("instance representation"), max_length=256)
@ -199,7 +199,7 @@ class Route(models.Model):
"""
backend = models.CharField(_("backend"), max_length=256,
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',
help_text=_("Python expression used for selecting the targe host, "
"<em>instance</em> referes to the current object."))

View file

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

View file

@ -18,50 +18,50 @@ class PaymentSourcesQueryset(models.QuerySet):
class PaymentSource(models.Model):
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,
choices=PaymentMethod.get_choices())
data = JSONField(_("data"), default={})
is_active = models.BooleanField(_("active"), default=True)
objects = PaymentSourcesQueryset.as_manager()
def __str__(self):
return "%s (%s)" % (self.label, self.method_class.verbose_name)
@cached_property
def method_class(self):
return PaymentMethod.get(self.method)
@cached_property
def method_instance(self):
""" Per request lived method_instance """
return self.method_class(self)
@cached_property
def label(self):
return self.method_instance.get_label()
@cached_property
def number(self):
return self.method_instance.get_number()
def get_bill_context(self):
method = self.method_instance
return {
'message': method.get_bill_message(),
}
def get_due_delta(self):
return self.method_instance.due_delta
def clean(self):
self.data = self.method_instance.clean_data()
class TransactionQuerySet(models.QuerySet):
group_by = group_by
def create(self, **kwargs):
source = kwargs.get('source')
if source is None or not hasattr(source.method_class, 'process'):
@ -71,16 +71,16 @@ class TransactionQuerySet(models.QuerySet):
if amount == 0:
kwargs['state'] = self.model.SECURED
return super(TransactionQuerySet, self).create(**kwargs)
def secured(self):
return self.filter(state=Transaction.SECURED)
def exclude_rejected(self):
return self.exclude(state=Transaction.REJECTED)
def amount(self):
return next(iter(self.aggregate(models.Sum('amount')).values())) or 0
def processing(self):
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 "
"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')
source = models.ForeignKey(PaymentSource, null=True, blank=True, on_delete=models.SET_NULL,
verbose_name=_("source"), related_name='transactions')
@ -121,16 +121,16 @@ class Transaction(models.Model):
currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY)
created_at = models.DateTimeField(_("created"), auto_now_add=True)
modified_at = models.DateTimeField(_("modified"), auto_now=True)
objects = TransactionQuerySet.as_manager()
def __str__(self):
return "#%i" % self.id
@property
def account(self):
return self.bill.account
def clean(self):
if not self.pk:
amount = self.bill.transactions.exclude(state=self.REJECTED).amount()
@ -141,24 +141,24 @@ class Transaction(models.Model):
'amount': amount,
}
)
def get_state_help(self):
if self.source:
return self.source.method_instance.state_help.get(self.state) or self.STATE_HELP.get(self.state)
return self.STATE_HELP.get(self.state)
def mark_as_processed(self):
self.state = self.WAITTING_EXECUTION
self.save(update_fields=('state', 'modified_at'))
def mark_as_executed(self):
self.state = self.EXECUTED
self.save(update_fields=('state', 'modified_at'))
def mark_as_secured(self):
self.state = self.SECURED
self.save(update_fields=('state', 'modified_at'))
def mark_as_rejected(self):
self.state = self.REJECTED
self.save(update_fields=('state', 'modified_at'))
@ -178,31 +178,31 @@ class TransactionProcess(models.Model):
(ABORTED, _("Aborted")),
(COMMITED, _("Commited")),
)
data = JSONField(_("data"), blank=True)
file = PrivateFileField(_("file"), blank=True)
state = models.CharField(_("state"), max_length=16, choices=STATES, default=CREATED)
created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(_("updated"), auto_now=True)
class Meta:
verbose_name_plural = _("Transaction processes")
def __str__(self):
return '#%i' % self.id
def mark_as_executed(self):
self.state = self.EXECUTED
for transaction in self.transactions.all():
transaction.mark_as_executed()
self.save(update_fields=('state', 'updated_at'))
def abort(self):
self.state = self.ABORTED
for transaction in self.transactions.all():
transaction.mark_as_rejected()
self.save(update_fields=('state', 'updated_at'))
def commit(self):
self.state = self.COMMITED
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."))
allow_multiple = models.BooleanField(_("allow multiple"), default=False,
help_text=_("Designates whether this plan allow for multiple contractions."))
def __str__(self):
return self.get_verbose_name()
def clean(self):
self.verbose_name = self.verbose_name.strip()
def get_verbose_name(self):
return self.verbose_name or self.name
class ContractedPlan(models.Model):
plan = models.ForeignKey(Plan, verbose_name=_("plan"), related_name='contracts')
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='plans')
plan = models.ForeignKey(Plan, on_delete=models.CASCADE,
verbose_name=_("plan"), related_name='contracts')
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
verbose_name=_("account"), related_name='plans')
class Meta:
verbose_name_plural = _("plans")
def __str__(self):
return str(self.plan)
@cached_property
def active(self):
return self.plan.is_active and self.account.is_active
def clean(self):
if not self.pk and not self.plan.allow_multiple:
if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists():
@ -59,7 +60,7 @@ class ContractedPlan(models.Model):
class RateQuerySet(models.QuerySet):
group_by = queryset.group_by
def by_account(self, account):
# Default allways selected
return self.filter(
@ -69,27 +70,27 @@ class RateQuerySet(models.QuerySet):
class Rate(models.Model):
service = models.ForeignKey('services.Service', verbose_name=_("service"),
related_name='rates')
plan = models.ForeignKey(Plan, verbose_name=_("plan"), related_name='rates', null=True,
blank=True)
service = models.ForeignKey('services.Service', on_delete=models.CASCADE,
verbose_name=_("service"), related_name='rates')
plan = models.ForeignKey(Plan, on_delete=models.SET_NULL, null=True, blank=True,
verbose_name=_("plan"), related_name='rates')
quantity = models.PositiveIntegerField(_("quantity"), null=True, blank=True,
help_text=_("See rate algorihm help text."))
price = models.DecimalField(_("price"), max_digits=12, decimal_places=2)
objects = RateQuerySet.as_manager()
class Meta:
unique_together = ('service', 'plan', 'quantity')
def __str__(self):
return "{}-{}".format(str(self.price), self.quantity)
@classmethod
@lru_cache()
def get_methods(cls):
return dict((method, import_class(method)) for method in settings.PLANS_RATE_METHODS)
@classmethod
@lru_cache()
def get_choices(cls):
@ -97,7 +98,7 @@ class Rate(models.Model):
for name, method in cls.get_methods().items():
choices.append((name, method.verbose_name))
return choices
@classmethod
def get_default(cls):
return settings.PLANS_DEFAULT_RATE_METHOD

View file

@ -42,7 +42,7 @@ class Resource(models.Model):
"digits and hyphen only."),
validators=[validators.validate_name])
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."))
aggregation = models.CharField(_("aggregation"), max_length=16,
choices=Aggregation.get_choices(), default=Aggregation.get_choices()[0][0],
@ -178,8 +178,8 @@ class ResourceDataQuerySet(models.QuerySet):
class ResourceData(models.Model):
""" Stores computed resource usage and allocation """
resource = models.ForeignKey(Resource, related_name='dataset', verbose_name=_("resource"))
content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
resource = models.ForeignKey(Resource, on_delete=models.CASCADE, related_name='dataset', verbose_name=_("resource"))
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, verbose_name=_("content type"))
object_id = models.PositiveIntegerField(_("object id"))
used = models.DecimalField(_("used"), max_digits=16, decimal_places=3, null=True,
editable=False)
@ -267,7 +267,7 @@ class MonitorData(models.Model):
""" Stores monitored data """
monitor = models.CharField(_("monitor"), max_length=256, db_index=True,
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"))
created_at = models.DateTimeField(_("created"), default=timezone.now, db_index=True)
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,
help_text=_("Required. 64 characters or fewer. Letters, digits and ./- only."),
validators=[validators.validate_hostname])
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='saas')
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
verbose_name=_("account"), related_name='saas')
is_active = models.BooleanField(_("active"), default=True,
help_text=_("Designates whether this service should be treated as active. "))
data = JSONField(_("data"), default={},
@ -36,51 +36,52 @@ class SaaS(models.Model):
help_text=_("Optional and alternative URL for accessing this service instance. "
"i.e. <tt>https://wiki.mydomain/doku/</tt><br>"
"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
databases = VirtualDatabaseRelation('databases.Database')
objects = SaaSQuerySet.as_manager()
class Meta:
verbose_name = "SaaS"
verbose_name_plural = "SaaS"
unique_together = (
('name', 'service'),
)
def __str__(self):
return "%s@%s" % (self.name, self.service)
@cached_property
def service_class(self):
return SoftwareService.get(self.service)
@cached_property
def service_instance(self):
""" Per request lived service_instance """
return self.service_class(self)
@cached_property
def active(self):
return self.is_active and self.account.is_active
def disable(self):
self.is_active = False
self.save(update_fields=('is_active',))
def enable(self):
self.is_active = True
self.save(update_fields=('is_active',))
def clean(self):
if not self.pk:
self.name = self.name.lower()
self.service_instance.clean()
self.data = self.service_instance.clean_data()
def get_site_domain(self):
return self.service_instance.get_site_domain()
def set_password(self, password):
self.password = password

View file

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

View file

@ -19,7 +19,7 @@ class SystemUserQuerySet(models.QuerySet):
user.set_password(password)
user.save(update_fields=['password'])
return user
def by_is_main(self, is_main=True, **kwargs):
if is_main:
return self.filter(account__main_systemuser_id=F('id'))
@ -30,7 +30,7 @@ class SystemUserQuerySet(models.QuerySet):
class SystemUser(models.Model):
"""
System users
Username max_length determined by LINUX system user/group lentgh: 32
"""
username = models.CharField(_("username"), max_length=32, unique=True,
@ -38,7 +38,7 @@ class SystemUser(models.Model):
validators=[validators.validate_username])
password = models.CharField(_("password"), max_length=128)
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,
help_text=_("Starting location when login with this no-shell user."))
directory = models.CharField(_("directory"), max_length=256, blank=True,
@ -51,19 +51,19 @@ class SystemUser(models.Model):
is_active = models.BooleanField(_("active"), default=True,
help_text=_("Designates whether this account should be treated as active. "
"Unselect this instead of deleting accounts."))
objects = SystemUserQuerySet.as_manager()
def __str__(self):
return self.username
@cached_property
def active(self):
try:
return self.is_active and self.account.is_active
except type(self).account.field.rel.to.DoesNotExist:
return self.is_active
@cached_property
def is_main(self):
# TODO on account delete
@ -71,34 +71,34 @@ class SystemUser(models.Model):
if self.account.main_systemuser_id:
return self.account.main_systemuser_id == self.pk
return self.account.username == self.username
@cached_property
def main(self):
# On account creation main_systemuser_id is still None
if self.account.main_systemuser_id:
return self.account.main_systemuser
return type(self).objects.get(username=self.account.username)
@property
def has_shell(self):
return self.shell not in settings.SYSTEMUSERS_DISABLED_SHELLS
def disable(self):
self.is_active = False
self.save(update_fields=('is_active',))
def enable(self):
self.is_active = True
self.save(update_fields=('is_active',))
def get_description(self):
return self.get_shell_display()
def save(self, *args, **kwargs):
if not self.home:
self.home = self.get_base_home()
super(SystemUser, self).save(*args, **kwargs)
def clean(self):
self.directory = self.directory.lstrip('/')
if self.home:
@ -123,16 +123,16 @@ class SystemUser(models.Model):
raise ValidationError({
'home': _("Shell users should use their own home."),
})
def set_password(self, raw_password):
self.password = make_password(raw_password)
def get_base_home(self):
context = {
'user': self.username,
'username': self.username,
}
return os.path.normpath(settings.SYSTEMUSERS_HOME % context)
def get_home(self):
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,
choices=settings.VPS_TEMPLATES, default=settings.VPS_DEFAULT_TEMPLATE,
help_text=_("Initial template."))
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='vpss')
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
verbose_name=_("Account"), related_name='vpss')
is_active = models.BooleanField(_("active"), default=True)
class Meta:
verbose_name = "VPS"
verbose_name_plural = "VPSs"
def __str__(self):
return self.hostname
def set_password(self, raw_password):
self.password = make_password(raw_password)
def get_username(self):
return self.hostname
def disable(self):
self.is_active = False
self.save(update_fields=('is_active',))
def enable(self):
self.is_active = False
self.save(update_fields=('is_active',))
@property
def active(self):
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],
help_text=_("The app will be installed in %s") % settings.WEBAPPS_BASE_DIR)
type = models.CharField(_("type"), max_length=32, choices=AppType.get_choices())
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='webapps')
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE,
verbose_name=_("Account"), related_name='webapps')
data = JSONField(_("data"), blank=True, default={},
help_text=_("Extra information dependent of each service."))
target_server = models.ForeignKey('orchestration.Server', verbose_name=_("Target Server"),
related_name='webapps')
target_server = models.ForeignKey('orchestration.Server', on_delete=models.CASCADE,
verbose_name=_("Target Server"), related_name='webapps')
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
databases = VirtualDatabaseRelation('databases.Database')
databaseusers = VirtualDatabaseUserRelation('databases.DatabaseUser')
class Meta:
unique_together = ('name', 'account')
verbose_name = _("Web App")
verbose_name_plural = _("Web Apps")
def __str__(self):
return self.name
def get_description(self):
return self.get_type_display()
@cached_property
def type_class(self):
return AppType.get(self.type)
@cached_property
def type_instance(self):
""" Per request lived type_instance """
return self.type_class(self)
def clean(self):
apptype = self.type_instance
apptype.validate()
a = apptype.clean_data()
self.data = apptype.clean_data()
def get_options(self, **kwargs):
options = OrderedDict()
qs = WebAppOption.objects.filter(**kwargs)
@ -69,57 +69,57 @@ class WebApp(models.Model):
else:
options[name] = value
return options
def get_directive(self):
return self.type_instance.get_directive()
def get_base_path(self):
context = {
'home': self.get_user().get_home(),
'app_name': self.name,
}
return settings.WEBAPPS_BASE_DIR % context
def get_path(self):
path = self.get_base_path()
public_root = self.options.filter(name='public-root').first()
if public_root:
path = os.path.join(path, public_root.value)
return os.path.normpath(path.replace('//', '/'))
def get_user(self):
return self.account.main_systemuser
def get_username(self):
return self.get_user().username
def get_groupname(self):
return self.get_username()
class WebAppOption(models.Model):
webapp = models.ForeignKey(WebApp, verbose_name=_("Web application"),
related_name='options')
webapp = models.ForeignKey(WebApp, on_delete=models.CASCADE,
verbose_name=_("Web application"), related_name='options')
name = models.CharField(_("name"), max_length=128,
choices=AppType.get_group_options_choices())
value = models.CharField(_("value"), max_length=256)
class Meta:
unique_together = ('webapp', 'name')
verbose_name = _("option")
verbose_name_plural = _("options")
def __str__(self):
return self.name
@cached_property
def option_class(self):
return AppOption.get(self.name)
@cached_property
def option_instance(self):
""" Per request lived option instance """
return self.option_class(self)
def clean(self):
self.option_instance.validate()

View file

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