From d863598d81950dcd34675fe0e36baf79b08b82e6 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 22 Apr 2021 10:28:00 +0200 Subject: [PATCH] Define on_delete argument for ForeignKey and OneToOneField Required since Django 2.0 --- orchestra/contrib/accounts/models.py | 2 +- orchestra/contrib/bills/models.py | 12 ++-- orchestra/contrib/contacts/models.py | 10 ++-- orchestra/contrib/databases/models.py | 28 +++++----- orchestra/contrib/domains/models.py | 58 +++++++++---------- orchestra/contrib/issues/models.py | 65 +++++++++++----------- orchestra/contrib/lists/models.py | 26 ++++----- orchestra/contrib/mailboxes/models.py | 50 ++++++++--------- orchestra/contrib/mailer/models.py | 14 ++--- orchestra/contrib/miscellaneous/models.py | 34 ++++++------ orchestra/contrib/orchestration/models.py | 8 +-- orchestra/contrib/orders/models.py | 57 +++++++++---------- orchestra/contrib/payments/models.py | 64 ++++++++++----------- orchestra/contrib/plans/models.py | 43 +++++++------- orchestra/contrib/resources/models.py | 8 +-- orchestra/contrib/saas/models.py | 29 +++++----- orchestra/contrib/services/models.py | 25 +++++---- orchestra/contrib/systemusers/models.py | 34 ++++++------ orchestra/contrib/vps/models.py | 18 +++--- orchestra/contrib/webapps/models.py | 50 ++++++++--------- orchestra/contrib/websites/models.py | 68 ++++++++++++----------- 21 files changed, 355 insertions(+), 348 deletions(-) diff --git a/orchestra/contrib/accounts/models.py b/orchestra/contrib/accounts/models.py index 8ef8ec95..94cd29c6 100644 --- a/orchestra/contrib/accounts/models.py +++ b/orchestra/contrib/accounts/models.py @@ -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")) diff --git a/orchestra/contrib/bills/models.py b/orchestra/contrib/bills/models.py index da79ec08..b39661a9 100644 --- a/orchestra/contrib/bills/models.py +++ b/orchestra/contrib/bills/models.py @@ -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) diff --git a/orchestra/contrib/contacts/models.py b/orchestra/contrib/contacts/models.py index 42fee3fe..a8de1580 100644 --- a/orchestra/contrib/contacts/models.py +++ b/orchestra/contrib/contacts/models.py @@ -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() diff --git a/orchestra/contrib/databases/models.py b/orchestra/contrib/databases/models.py index ab452358..207fe63d 100644 --- a/orchestra/contrib/databases/models.py +++ b/orchestra/contrib/databases/models.py @@ -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 diff --git a/orchestra/contrib/domains/models.py b/orchestra/contrib/domains/models.py index 4cbcfc93..05434559 100644 --- a/orchestra/contrib/domains/models.py +++ b/orchestra/contrib/domains/models.py @@ -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 diff --git a/orchestra/contrib/issues/models.py b/orchestra/contrib/issues/models.py index f2a75f27..aa1e51b0 100644 --- a/orchestra/contrib/issues/models.py +++ b/orchestra/contrib/issues/models.py @@ -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'), diff --git a/orchestra/contrib/lists/models.py b/orchestra/contrib/lists/models.py index 8dc50912..a78580d9 100644 --- a/orchestra/contrib/lists/models.py +++ b/orchestra/contrib/lists/models.py @@ -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 diff --git a/orchestra/contrib/mailboxes/models.py b/orchestra/contrib/mailboxes/models.py index 265519c6..ee4c3594 100644 --- a/orchestra/contrib/mailboxes/models.py +++ b/orchestra/contrib/mailboxes/models.py @@ -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): "sieve language. " "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 catch-all 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 diff --git a/orchestra/contrib/mailer/models.py b/orchestra/contrib/mailer/models.py index 5b43e756..f7c61aff 100644 --- a/orchestra/contrib/mailer/models.py +++ b/orchestra/contrib/mailer/models.py @@ -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() diff --git a/orchestra/contrib/miscellaneous/models.py b/orchestra/contrib/miscellaneous/models.py index c5ab30f2..e5fe5f0f 100644 --- a/orchestra/contrib/miscellaneous/models.py +++ b/orchestra/contrib/miscellaneous/models.py @@ -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() diff --git a/orchestra/contrib/orchestration/models.py b/orchestra/contrib/orchestration/models.py index 37d74702..95e0e4d4 100644 --- a/orchestra/contrib/orchestration/models.py +++ b/orchestra/contrib/orchestration/models.py @@ -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, " "instance referes to the current object.")) diff --git a/orchestra/contrib/orders/models.py b/orchestra/contrib/orders/models.py index 28ceb4fd..69e5fa3b 100644 --- a/orchestra/contrib/orders/models.py +++ b/orchestra/contrib/orders/models.py @@ -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) diff --git a/orchestra/contrib/payments/models.py b/orchestra/contrib/payments/models.py index 82f2ce22..4d7770c8 100644 --- a/orchestra/contrib/payments/models.py +++ b/orchestra/contrib/payments/models.py @@ -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(): diff --git a/orchestra/contrib/plans/models.py b/orchestra/contrib/plans/models.py index 3c87da00..22b698f3 100644 --- a/orchestra/contrib/plans/models.py +++ b/orchestra/contrib/plans/models.py @@ -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 diff --git a/orchestra/contrib/resources/models.py b/orchestra/contrib/resources/models.py index 3da69d81..b886c41a 100644 --- a/orchestra/contrib/resources/models.py +++ b/orchestra/contrib/resources/models.py @@ -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) diff --git a/orchestra/contrib/saas/models.py b/orchestra/contrib/saas/models.py index ee1423b1..c8a31f60 100644 --- a/orchestra/contrib/saas/models.py +++ b/orchestra/contrib/saas/models.py @@ -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. https://wiki.mydomain/doku/
" "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 diff --git a/orchestra/contrib/services/models.py b/orchestra/contrib/services/models.py index 1d3377e7..961c5c6b 100644 --- a/orchestra/contrib/services/models.py +++ b/orchestra/contrib/services/models.py @@ -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 diff --git a/orchestra/contrib/systemusers/models.py b/orchestra/contrib/systemusers/models.py index de60a698..c7e9abd0 100644 --- a/orchestra/contrib/systemusers/models.py +++ b/orchestra/contrib/systemusers/models.py @@ -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)) diff --git a/orchestra/contrib/vps/models.py b/orchestra/contrib/vps/models.py index 4c7cc155..fe3c62c9 100644 --- a/orchestra/contrib/vps/models.py +++ b/orchestra/contrib/vps/models.py @@ -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 diff --git a/orchestra/contrib/webapps/models.py b/orchestra/contrib/webapps/models.py index 2180e01b..42b3a227 100644 --- a/orchestra/contrib/webapps/models.py +++ b/orchestra/contrib/webapps/models.py @@ -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() diff --git a/orchestra/contrib/websites/models.py b/orchestra/contrib/websites/models.py index 80e1bf1f..f71d1a1f 100644 --- a/orchestra/contrib/websites/models.py +++ b/orchestra/contrib/websites/models.py @@ -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]