Upgrade to Django 3.2
- update dependencies - fix deprecation warnings - fix tests - skip some tests that need more work - reformat changed code with isort and black
This commit is contained in:
parent
0f18e59d67
commit
4af1a39ca4
93 changed files with 3598 additions and 2725 deletions
|
@ -2,4 +2,3 @@
|
|||
This app is for managing database users and user databases.
|
||||
|
||||
"""
|
||||
default_app_config = 'userdbs.apps.UserdbsAppConfig'
|
||||
|
|
|
@ -6,12 +6,9 @@ from __future__ import absolute_import
|
|||
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import (
|
||||
DatabaseUser,
|
||||
UserDatabase,
|
||||
)
|
||||
from .models import DatabaseUser, UserDatabase
|
||||
|
||||
|
||||
class DatabaseUserCreationForm(forms.ModelForm):
|
||||
|
@ -23,7 +20,7 @@ class DatabaseUserCreationForm(forms.ModelForm):
|
|||
|
||||
class Meta:
|
||||
model = DatabaseUser
|
||||
fields = ['osuser', 'db_type']
|
||||
fields = ["osuser", "db_type"]
|
||||
|
||||
def save(self, commit=True):
|
||||
"""
|
||||
|
@ -35,8 +32,10 @@ class DatabaseUserCreationForm(forms.ModelForm):
|
|||
|
||||
"""
|
||||
dbuser = DatabaseUser.objects.create_database_user(
|
||||
osuser=self.cleaned_data['osuser'],
|
||||
db_type=self.cleaned_data['db_type'], commit=commit)
|
||||
osuser=self.cleaned_data["osuser"],
|
||||
db_type=self.cleaned_data["db_type"],
|
||||
commit=commit,
|
||||
)
|
||||
return dbuser
|
||||
|
||||
def save_m2m(self):
|
||||
|
@ -55,7 +54,7 @@ class UserDatabaseCreationForm(forms.ModelForm):
|
|||
|
||||
class Meta:
|
||||
model = UserDatabase
|
||||
fields = ['db_user']
|
||||
fields = ["db_user"]
|
||||
|
||||
def save(self, commit=True):
|
||||
"""
|
||||
|
@ -67,7 +66,8 @@ class UserDatabaseCreationForm(forms.ModelForm):
|
|||
|
||||
"""
|
||||
database = UserDatabase.objects.create_userdatabase(
|
||||
db_user=self.cleaned_data['db_user'], commit=commit)
|
||||
db_user=self.cleaned_data["db_user"], commit=commit
|
||||
)
|
||||
return database
|
||||
|
||||
def save_m2m(self):
|
||||
|
@ -83,7 +83,8 @@ class DatabaseUserAdmin(admin.ModelAdmin):
|
|||
<userdbs.models.DatabaseUser>`
|
||||
|
||||
"""
|
||||
actions = ['perform_delete_selected']
|
||||
|
||||
actions = ["perform_delete_selected"]
|
||||
add_form = DatabaseUserCreationForm
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
|
@ -101,12 +102,13 @@ class DatabaseUserAdmin(admin.ModelAdmin):
|
|||
"""
|
||||
defaults = {}
|
||||
if obj is None:
|
||||
defaults.update({
|
||||
'form': self.add_form,
|
||||
})
|
||||
defaults.update(
|
||||
{
|
||||
"form": self.add_form,
|
||||
}
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return super(DatabaseUserAdmin, self).get_form(
|
||||
request, obj, **defaults)
|
||||
return super(DatabaseUserAdmin, self).get_form(request, obj, **defaults)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""
|
||||
|
@ -122,7 +124,7 @@ class DatabaseUserAdmin(admin.ModelAdmin):
|
|||
|
||||
"""
|
||||
if obj:
|
||||
return ['osuser', 'name', 'db_type']
|
||||
return ["osuser", "name", "db_type"]
|
||||
return []
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
|
@ -154,8 +156,8 @@ class DatabaseUserAdmin(admin.ModelAdmin):
|
|||
"""
|
||||
for dbuser in queryset.all():
|
||||
dbuser.delete()
|
||||
perform_delete_selected.short_description = _(
|
||||
'Delete selected database users')
|
||||
|
||||
perform_delete_selected.short_description = _("Delete selected database users")
|
||||
|
||||
def get_actions(self, request):
|
||||
"""
|
||||
|
@ -170,8 +172,8 @@ class DatabaseUserAdmin(admin.ModelAdmin):
|
|||
|
||||
"""
|
||||
actions = super(DatabaseUserAdmin, self).get_actions(request)
|
||||
if 'delete_selected' in actions: # pragma: no cover
|
||||
del actions['delete_selected']
|
||||
if "delete_selected" in actions: # pragma: no cover
|
||||
del actions["delete_selected"]
|
||||
return actions
|
||||
|
||||
|
||||
|
@ -181,7 +183,8 @@ class UserDatabaseAdmin(admin.ModelAdmin):
|
|||
<userdbs.models.UserDatabase>`
|
||||
|
||||
"""
|
||||
actions = ['perform_delete_selected']
|
||||
|
||||
actions = ["perform_delete_selected"]
|
||||
add_form = UserDatabaseCreationForm
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
|
@ -199,12 +202,13 @@ class UserDatabaseAdmin(admin.ModelAdmin):
|
|||
"""
|
||||
defaults = {}
|
||||
if obj is None:
|
||||
defaults.update({
|
||||
'form': self.add_form,
|
||||
})
|
||||
defaults.update(
|
||||
{
|
||||
"form": self.add_form,
|
||||
}
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return super(UserDatabaseAdmin, self).get_form(
|
||||
request, obj, **defaults)
|
||||
return super(UserDatabaseAdmin, self).get_form(request, obj, **defaults)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""
|
||||
|
@ -220,7 +224,7 @@ class UserDatabaseAdmin(admin.ModelAdmin):
|
|||
|
||||
"""
|
||||
if obj:
|
||||
return ['db_name', 'db_user']
|
||||
return ["db_name", "db_user"]
|
||||
return []
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
|
@ -252,8 +256,8 @@ class UserDatabaseAdmin(admin.ModelAdmin):
|
|||
"""
|
||||
for database in queryset.all():
|
||||
database.delete()
|
||||
perform_delete_selected.short_description = _(
|
||||
'Delete selected user databases')
|
||||
|
||||
perform_delete_selected.short_description = _("Delete selected user databases")
|
||||
|
||||
def get_actions(self, request):
|
||||
"""
|
||||
|
@ -268,8 +272,8 @@ class UserDatabaseAdmin(admin.ModelAdmin):
|
|||
|
||||
"""
|
||||
actions = super(UserDatabaseAdmin, self).get_actions(request)
|
||||
if 'delete_selected' in actions: # pragma: no cover
|
||||
del actions['delete_selected']
|
||||
if "delete_selected" in actions: # pragma: no cover
|
||||
del actions["delete_selected"]
|
||||
return actions
|
||||
|
||||
|
||||
|
|
|
@ -3,10 +3,8 @@ This module contains the :py:class:`django.apps.AppConfig` instance for the
|
|||
:py:mod:`userdbs` app.
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class UserdbsAppConfig(AppConfig):
|
||||
|
@ -14,8 +12,9 @@ class UserdbsAppConfig(AppConfig):
|
|||
AppConfig for the :py:mod:`userdbs` app.
|
||||
|
||||
"""
|
||||
name = 'userdbs'
|
||||
verbose_name = _('Database Users and their Databases')
|
||||
|
||||
name = "userdbs"
|
||||
verbose_name = _("Database Users and their Databases")
|
||||
|
||||
def ready(self):
|
||||
"""
|
||||
|
|
|
@ -2,32 +2,27 @@
|
|||
This module defines form classes for user database editing.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from __future__ import absolute_import
|
||||
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import (
|
||||
Submit,
|
||||
)
|
||||
from crispy_forms.layout import Submit
|
||||
from django import forms
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import (
|
||||
DB_TYPES,
|
||||
DatabaseUser,
|
||||
UserDatabase,
|
||||
)
|
||||
from gvawebcore.forms import PasswordModelFormMixin
|
||||
|
||||
from .models import DB_TYPES, DatabaseUser, UserDatabase
|
||||
|
||||
|
||||
class AddUserDatabaseForm(forms.ModelForm, PasswordModelFormMixin):
|
||||
"""
|
||||
This form is used to create new user database instances.
|
||||
|
||||
"""
|
||||
|
||||
db_type = forms.TypedChoiceField(
|
||||
label=_('Database type'),
|
||||
label=_("Database type"),
|
||||
choices=DB_TYPES,
|
||||
widget=forms.RadioSelect,
|
||||
coerce=int,
|
||||
|
@ -38,17 +33,18 @@ class AddUserDatabaseForm(forms.ModelForm, PasswordModelFormMixin):
|
|||
fields = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.hosting_package = kwargs.pop('hostingpackage')
|
||||
self.available_dbtypes = kwargs.pop('dbtypes')
|
||||
self.hosting_package = kwargs.pop("hostingpackage")
|
||||
self.available_dbtypes = kwargs.pop("dbtypes")
|
||||
super(AddUserDatabaseForm, self).__init__(*args, **kwargs)
|
||||
self.fields['db_type'].choices = self.available_dbtypes
|
||||
self.fields["db_type"].choices = self.available_dbtypes
|
||||
if len(self.available_dbtypes) == 1:
|
||||
self.fields['db_type'].initial = self.available_dbtypes[0][0]
|
||||
self.fields['db_type'].widget = forms.HiddenInput()
|
||||
self.fields["db_type"].initial = self.available_dbtypes[0][0]
|
||||
self.fields["db_type"].widget = forms.HiddenInput()
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_action = reverse(
|
||||
'add_userdatabase', kwargs={'package': self.hosting_package.id})
|
||||
self.helper.add_input(Submit('submit', _('Create database')))
|
||||
"add_userdatabase", kwargs={"package": self.hosting_package.id}
|
||||
)
|
||||
self.helper.add_input(Submit("submit", _("Create database")))
|
||||
|
||||
def save(self, commit=True):
|
||||
"""
|
||||
|
@ -62,8 +58,11 @@ class AddUserDatabaseForm(forms.ModelForm, PasswordModelFormMixin):
|
|||
"""
|
||||
data = self.cleaned_data
|
||||
self.instance = UserDatabase.objects.create_userdatabase_with_user(
|
||||
data['db_type'], self.hosting_package.osuser,
|
||||
password=data['password1'], commit=commit)
|
||||
data["db_type"],
|
||||
self.hosting_package.osuser,
|
||||
password=data["password1"],
|
||||
commit=commit,
|
||||
)
|
||||
return super(AddUserDatabaseForm, self).save(commit)
|
||||
|
||||
|
||||
|
@ -72,21 +71,24 @@ class ChangeDatabaseUserPasswordForm(forms.ModelForm, PasswordModelFormMixin):
|
|||
This form is used to change the password of a database user.
|
||||
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = DatabaseUser
|
||||
fields = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.hosting_package = kwargs.pop('hostingpackage')
|
||||
self.hosting_package = kwargs.pop("hostingpackage")
|
||||
super(ChangeDatabaseUserPasswordForm, self).__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_action = reverse(
|
||||
'change_dbuser_password', kwargs={
|
||||
'slug': self.instance.name,
|
||||
'package': self.hosting_package.id,
|
||||
})
|
||||
self.helper.add_input(Submit('submit', _('Set password')))
|
||||
"change_dbuser_password",
|
||||
kwargs={
|
||||
"slug": self.instance.name,
|
||||
"package": self.hosting_package.id,
|
||||
},
|
||||
)
|
||||
self.helper.add_input(Submit("submit", _("Set password")))
|
||||
|
||||
def save(self, commit=True):
|
||||
self.instance.set_password(self.cleaned_data['password1'])
|
||||
self.instance.set_password(self.cleaned_data["password1"])
|
||||
return super(ChangeDatabaseUserPasswordForm, self).save()
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.utils.timezone
|
||||
import model_utils.fields
|
||||
from django.db import migrations, models
|
||||
|
@ -8,66 +6,110 @@ from django.db import migrations, models
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('osusers', '0004_auto_20150104_1751'),
|
||||
("osusers", "0004_auto_20150104_1751"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DatabaseUser',
|
||||
name="DatabaseUser",
|
||||
fields=[
|
||||
('id', models.AutoField(
|
||||
verbose_name='ID', serialize=False, auto_created=True,
|
||||
primary_key=True)),
|
||||
('created', model_utils.fields.AutoCreatedField(
|
||||
default=django.utils.timezone.now, verbose_name='created',
|
||||
editable=False)),
|
||||
('modified', model_utils.fields.AutoLastModifiedField(
|
||||
default=django.utils.timezone.now, verbose_name='modified',
|
||||
editable=False)),
|
||||
('name', models.CharField(
|
||||
max_length=63, verbose_name='username')),
|
||||
('db_type', models.PositiveSmallIntegerField(
|
||||
verbose_name='database type',
|
||||
choices=[(0, 'PostgreSQL'), (1, 'MySQL')])),
|
||||
('osuser', models.ForeignKey(
|
||||
to='osusers.User', on_delete=models.CASCADE)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
verbose_name="ID",
|
||||
serialize=False,
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"created",
|
||||
model_utils.fields.AutoCreatedField(
|
||||
default=django.utils.timezone.now,
|
||||
verbose_name="created",
|
||||
editable=False,
|
||||
),
|
||||
),
|
||||
(
|
||||
"modified",
|
||||
model_utils.fields.AutoLastModifiedField(
|
||||
default=django.utils.timezone.now,
|
||||
verbose_name="modified",
|
||||
editable=False,
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=63, verbose_name="username")),
|
||||
(
|
||||
"db_type",
|
||||
models.PositiveSmallIntegerField(
|
||||
verbose_name="database type",
|
||||
choices=[(0, "PostgreSQL"), (1, "MySQL")],
|
||||
),
|
||||
),
|
||||
(
|
||||
"osuser",
|
||||
models.ForeignKey(to="osusers.User", on_delete=models.CASCADE),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'database user',
|
||||
'verbose_name_plural': 'database users',
|
||||
"verbose_name": "database user",
|
||||
"verbose_name_plural": "database users",
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserDatabase',
|
||||
name="UserDatabase",
|
||||
fields=[
|
||||
('id', models.AutoField(
|
||||
verbose_name='ID', serialize=False, auto_created=True,
|
||||
primary_key=True)),
|
||||
('created', model_utils.fields.AutoCreatedField(
|
||||
default=django.utils.timezone.now, verbose_name='created',
|
||||
editable=False)),
|
||||
('modified', model_utils.fields.AutoLastModifiedField(
|
||||
default=django.utils.timezone.now, verbose_name='modified',
|
||||
editable=False)),
|
||||
('db_name', models.CharField(
|
||||
max_length=63, verbose_name='database name')),
|
||||
('db_user', models.ForeignKey(
|
||||
verbose_name='database user', to='userdbs.DatabaseUser',
|
||||
on_delete=models.CASCADE)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
verbose_name="ID",
|
||||
serialize=False,
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"created",
|
||||
model_utils.fields.AutoCreatedField(
|
||||
default=django.utils.timezone.now,
|
||||
verbose_name="created",
|
||||
editable=False,
|
||||
),
|
||||
),
|
||||
(
|
||||
"modified",
|
||||
model_utils.fields.AutoLastModifiedField(
|
||||
default=django.utils.timezone.now,
|
||||
verbose_name="modified",
|
||||
editable=False,
|
||||
),
|
||||
),
|
||||
(
|
||||
"db_name",
|
||||
models.CharField(max_length=63, verbose_name="database name"),
|
||||
),
|
||||
(
|
||||
"db_user",
|
||||
models.ForeignKey(
|
||||
verbose_name="database user",
|
||||
to="userdbs.DatabaseUser",
|
||||
on_delete=models.CASCADE,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user database',
|
||||
'verbose_name_plural': 'user specific database',
|
||||
"verbose_name": "user database",
|
||||
"verbose_name_plural": "user specific database",
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='userdatabase',
|
||||
unique_together={('db_name', 'db_user')},
|
||||
name="userdatabase",
|
||||
unique_together={("db_name", "db_user")},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='databaseuser',
|
||||
unique_together={('name', 'db_type')},
|
||||
name="databaseuser",
|
||||
unique_together={("name", "db_type")},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, transaction
|
||||
from django.dispatch import Signal
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from model_utils import Choices
|
||||
from model_utils.models import TimeStampedModel
|
||||
|
||||
from osusers.models import User as OsUser
|
||||
|
||||
DB_TYPES = Choices(
|
||||
(0, 'pgsql', _('PostgreSQL')),
|
||||
(1, 'mysql', _('MySQL')),
|
||||
(0, "pgsql", _("PostgreSQL")),
|
||||
(1, "mysql", _("MySQL")),
|
||||
)
|
||||
"""
|
||||
Database type choice enumeration.
|
||||
"""
|
||||
|
||||
|
||||
password_set = Signal(providing_args=['instance', 'password'])
|
||||
password_set = Signal()
|
||||
|
||||
|
||||
class DatabaseUserManager(models.Manager):
|
||||
|
@ -40,10 +37,10 @@ class DatabaseUserManager(models.Manager):
|
|||
dbuser_name_format = "{0}db{{0:02d}}".format(osuser.username)
|
||||
nextname = dbuser_name_format.format(count)
|
||||
|
||||
for user in self.values('name').filter(
|
||||
osuser=osuser, db_type=db_type
|
||||
).order_by('name'):
|
||||
if user['name'] == nextname:
|
||||
for user in (
|
||||
self.values("name").filter(osuser=osuser, db_type=db_type).order_by("name")
|
||||
):
|
||||
if user["name"] == nextname:
|
||||
count += 1
|
||||
nextname = dbuser_name_format.format(count)
|
||||
else:
|
||||
|
@ -74,33 +71,29 @@ class DatabaseUserManager(models.Manager):
|
|||
"""
|
||||
if username is None:
|
||||
username = self._get_next_dbuser_name(osuser, db_type)
|
||||
db_user = DatabaseUser(
|
||||
osuser=osuser, db_type=db_type, name=username)
|
||||
db_user = DatabaseUser(osuser=osuser, db_type=db_type, name=username)
|
||||
if commit:
|
||||
db_user.save()
|
||||
return db_user
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DatabaseUser(TimeStampedModel, models.Model):
|
||||
osuser = models.ForeignKey(OsUser, on_delete=models.CASCADE)
|
||||
name = models.CharField(
|
||||
_('username'), max_length=63)
|
||||
db_type = models.PositiveSmallIntegerField(
|
||||
_('database type'), choices=DB_TYPES)
|
||||
name = models.CharField(_("username"), max_length=63)
|
||||
db_type = models.PositiveSmallIntegerField(_("database type"), choices=DB_TYPES)
|
||||
|
||||
objects = DatabaseUserManager()
|
||||
|
||||
class Meta:
|
||||
unique_together = ['name', 'db_type']
|
||||
verbose_name = _('database user')
|
||||
verbose_name_plural = _('database users')
|
||||
unique_together = ["name", "db_type"]
|
||||
verbose_name = _("database user")
|
||||
verbose_name_plural = _("database users")
|
||||
|
||||
def __str__(self):
|
||||
return "%(name)s (%(db_type)s for %(osuser)s)" % {
|
||||
'name': self.name,
|
||||
'db_type': self.get_db_type_display(),
|
||||
'osuser': self.osuser.username,
|
||||
"name": self.name,
|
||||
"db_type": self.get_db_type_display(),
|
||||
"osuser": self.osuser.username,
|
||||
}
|
||||
|
||||
@transaction.atomic
|
||||
|
@ -110,8 +103,7 @@ class DatabaseUser(TimeStampedModel, models.Model):
|
|||
|
||||
:param str password: new password for the database user
|
||||
"""
|
||||
password_set.send(
|
||||
sender=self.__class__, password=password, instance=self)
|
||||
password_set.send(sender=self.__class__, password=password, instance=self)
|
||||
|
||||
@transaction.atomic
|
||||
def delete(self, *args, **kwargs):
|
||||
|
@ -149,10 +141,8 @@ class UserDatabaseManager(models.Manager):
|
|||
db_name_format = "{0}_{{0:02d}}".format(db_user.name)
|
||||
# first db is named the same as the user
|
||||
nextname = db_user.name
|
||||
for name in self.values('db_name').filter(db_user=db_user).order_by(
|
||||
'db_name'
|
||||
):
|
||||
if name['db_name'] == nextname:
|
||||
for name in self.values("db_name").filter(db_user=db_user).order_by("db_name"):
|
||||
if name["db_name"] == nextname:
|
||||
count += 1
|
||||
nextname = db_name_format.format(count)
|
||||
else:
|
||||
|
@ -161,7 +151,8 @@ class UserDatabaseManager(models.Manager):
|
|||
|
||||
@transaction.atomic
|
||||
def create_userdatabase_with_user(
|
||||
self, db_type, osuser, password=None, commit=True):
|
||||
self, db_type, osuser, password=None, commit=True
|
||||
):
|
||||
"""
|
||||
Creates a new user database with a new user.
|
||||
|
||||
|
@ -175,7 +166,8 @@ class UserDatabaseManager(models.Manager):
|
|||
|
||||
"""
|
||||
dbuser = DatabaseUser.objects.create_database_user(
|
||||
osuser, db_type, password=password, commit=commit)
|
||||
osuser, db_type, password=password, commit=commit
|
||||
)
|
||||
database = self.create_userdatabase(dbuser, commit=commit)
|
||||
return database
|
||||
|
||||
|
@ -198,24 +190,22 @@ class UserDatabaseManager(models.Manager):
|
|||
return database
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class UserDatabase(TimeStampedModel, models.Model):
|
||||
# MySQL limits to 64, PostgreSQL to 63 characters
|
||||
db_name = models.CharField(
|
||||
_('database name'), max_length=63)
|
||||
db_name = models.CharField(_("database name"), max_length=63)
|
||||
db_user = models.ForeignKey(
|
||||
DatabaseUser, verbose_name=_('database user'),
|
||||
on_delete=models.CASCADE)
|
||||
DatabaseUser, verbose_name=_("database user"), on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
objects = UserDatabaseManager()
|
||||
|
||||
class Meta:
|
||||
unique_together = ['db_name', 'db_user']
|
||||
verbose_name = _('user database')
|
||||
verbose_name_plural = _('user specific database')
|
||||
unique_together = ["db_name", "db_user"]
|
||||
verbose_name = _("user database")
|
||||
verbose_name_plural = _("user specific database")
|
||||
|
||||
def __str__(self):
|
||||
return "%(db_name)s (%(db_user)s)" % {
|
||||
'db_name': self.db_name,
|
||||
'db_user': self.db_user,
|
||||
"db_name": self.db_name,
|
||||
"db_user": self.db_user,
|
||||
}
|
||||
|
|
|
@ -6,20 +6,26 @@ The module starts Celery_ tasks.
|
|||
.. _Celery: http://www.celeryproject.org/
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.dispatch import receiver
|
||||
from passlib.utils import generate_password
|
||||
from passlib.pwd import genword
|
||||
|
||||
from mysqltasks.tasks import (create_mysql_database, create_mysql_user,
|
||||
delete_mysql_database, delete_mysql_user,
|
||||
set_mysql_userpassword)
|
||||
from pgsqltasks.tasks import (create_pgsql_database, create_pgsql_user,
|
||||
delete_pgsql_database, delete_pgsql_user,
|
||||
set_pgsql_userpassword)
|
||||
from mysqltasks.tasks import (
|
||||
create_mysql_database,
|
||||
create_mysql_user,
|
||||
delete_mysql_database,
|
||||
delete_mysql_user,
|
||||
set_mysql_userpassword,
|
||||
)
|
||||
from pgsqltasks.tasks import (
|
||||
create_pgsql_database,
|
||||
create_pgsql_user,
|
||||
delete_pgsql_database,
|
||||
delete_pgsql_user,
|
||||
set_pgsql_userpassword,
|
||||
)
|
||||
from taskresults.models import TaskResult
|
||||
|
||||
from .models import DB_TYPES, DatabaseUser, UserDatabase, password_set
|
||||
|
@ -64,25 +70,29 @@ def handle_dbuser_password_set(sender, instance, password, **kwargs):
|
|||
"""
|
||||
if instance.db_type == DB_TYPES.mysql:
|
||||
taskresult = TaskResult.objects.create_task_result(
|
||||
'handle_dbuser_password_set',
|
||||
"handle_dbuser_password_set",
|
||||
set_mysql_userpassword.s(instance.name, password),
|
||||
'mysql password change')
|
||||
"mysql password change",
|
||||
)
|
||||
_LOGGER.info(
|
||||
'MySQL password change has been requested in task %s',
|
||||
taskresult.task_id)
|
||||
"MySQL password change has been requested in task %s", taskresult.task_id
|
||||
)
|
||||
elif instance.db_type == DB_TYPES.pgsql:
|
||||
taskresult = TaskResult.objects.create_task_result(
|
||||
'handle_dbuser_password_set',
|
||||
"handle_dbuser_password_set",
|
||||
set_pgsql_userpassword.s(instance.name, password),
|
||||
'pgsql password change')
|
||||
"pgsql password change",
|
||||
)
|
||||
_LOGGER.info(
|
||||
'PostgreSQL password change has been requested in task %s',
|
||||
taskresult.task_id)
|
||||
"PostgreSQL password change has been requested in task %s",
|
||||
taskresult.task_id,
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
'Password change has been requested for unknown database %s'
|
||||
' the request has been ignored.',
|
||||
instance.db_type)
|
||||
"Password change has been requested for unknown database %s"
|
||||
" the request has been ignored.",
|
||||
instance.db_type,
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=DatabaseUser)
|
||||
|
@ -122,32 +132,37 @@ def handle_dbuser_created(sender, instance, created, **kwargs):
|
|||
|
||||
"""
|
||||
if created:
|
||||
password = kwargs.get('password', generate_password())
|
||||
password = kwargs.get("password", genword())
|
||||
# TODO: send GPG encrypted mail with this information
|
||||
if instance.db_type == DB_TYPES.mysql:
|
||||
taskresult = TaskResult.objects.create_task_result(
|
||||
'handle_dbuser_created',
|
||||
"handle_dbuser_created",
|
||||
create_mysql_user.s(instance.name, password),
|
||||
'mysql user creation')
|
||||
"mysql user creation",
|
||||
)
|
||||
_LOGGER.info(
|
||||
'A new MySQL user %s creation has been requested in task %s',
|
||||
instance.name, taskresult.task_id)
|
||||
"A new MySQL user %s creation has been requested in task %s",
|
||||
instance.name,
|
||||
taskresult.task_id,
|
||||
)
|
||||
elif instance.db_type == DB_TYPES.pgsql:
|
||||
taskresult = TaskResult.objects.create_task_result(
|
||||
'handle_dbuser_created',
|
||||
"handle_dbuser_created",
|
||||
create_pgsql_user.s(instance.name, password),
|
||||
'pgsql user creation')
|
||||
"pgsql user creation",
|
||||
)
|
||||
_LOGGER.info(
|
||||
'A new PostgreSQL user %s creation has been requested in task'
|
||||
' %s',
|
||||
instance.name, taskresult.task_id)
|
||||
"A new PostgreSQL user %s creation has been requested in task" " %s",
|
||||
instance.name,
|
||||
taskresult.task_id,
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
'created DatabaseUser for unknown database type %s',
|
||||
instance.db_type)
|
||||
"created DatabaseUser for unknown database type %s", instance.db_type
|
||||
)
|
||||
_LOGGER.debug(
|
||||
'database user %s has been %s',
|
||||
instance, created and "created" or "updated")
|
||||
"database user %s has been %s", instance, created and "created" or "updated"
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=DatabaseUser)
|
||||
|
@ -185,26 +200,33 @@ def handle_dbuser_deleted(sender, instance, **kwargs):
|
|||
"""
|
||||
if instance.db_type == DB_TYPES.mysql:
|
||||
taskresult = TaskResult.objects.create_task_result(
|
||||
'handle_dbuser_deleted',
|
||||
"handle_dbuser_deleted",
|
||||
delete_mysql_user.s(instance.name),
|
||||
'mysql user deletion')
|
||||
"mysql user deletion",
|
||||
)
|
||||
_LOGGER.info(
|
||||
'MySQL user %s deletion has been requested in task %s',
|
||||
instance.name, taskresult.task_id)
|
||||
"MySQL user %s deletion has been requested in task %s",
|
||||
instance.name,
|
||||
taskresult.task_id,
|
||||
)
|
||||
elif instance.db_type == DB_TYPES.pgsql:
|
||||
taskresult = TaskResult.objects.create_task_result(
|
||||
'handle_dbuser_deleted',
|
||||
"handle_dbuser_deleted",
|
||||
delete_pgsql_user.s(instance.name),
|
||||
'pgsql user deletion')
|
||||
"pgsql user deletion",
|
||||
)
|
||||
_LOGGER.info(
|
||||
'PostgreSQL user %s deletion has been requested in task %s',
|
||||
instance.name, taskresult.task_id)
|
||||
"PostgreSQL user %s deletion has been requested in task %s",
|
||||
instance.name,
|
||||
taskresult.task_id,
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
'deleted DatabaseUser %s for unknown database type %s',
|
||||
instance.name, instance.db_type)
|
||||
_LOGGER.debug(
|
||||
'database user %s has been deleted', instance)
|
||||
"deleted DatabaseUser %s for unknown database type %s",
|
||||
instance.name,
|
||||
instance.db_type,
|
||||
)
|
||||
_LOGGER.debug("database user %s has been deleted", instance)
|
||||
|
||||
|
||||
@receiver(post_save, sender=UserDatabase)
|
||||
|
@ -245,31 +267,36 @@ def handle_userdb_created(sender, instance, created, **kwargs):
|
|||
if created:
|
||||
if instance.db_user.db_type == DB_TYPES.mysql:
|
||||
taskresult = TaskResult.objects.create_task_result(
|
||||
'handle_userdb_created',
|
||||
create_mysql_database.s(
|
||||
instance.db_name, instance.db_user.name),
|
||||
'mysql database creation')
|
||||
"handle_userdb_created",
|
||||
create_mysql_database.s(instance.db_name, instance.db_user.name),
|
||||
"mysql database creation",
|
||||
)
|
||||
_LOGGER.info(
|
||||
'The creation of a new MySQL database %s has been requested in'
|
||||
' task %s',
|
||||
instance.db_name, taskresult.task_id)
|
||||
"The creation of a new MySQL database %s has been requested in"
|
||||
" task %s",
|
||||
instance.db_name,
|
||||
taskresult.task_id,
|
||||
)
|
||||
elif instance.db_user.db_type == DB_TYPES.pgsql:
|
||||
taskresult = TaskResult.objects.create_task_result(
|
||||
'handle_userdb_created',
|
||||
create_pgsql_database.s(
|
||||
instance.db_name, instance.db_user.name),
|
||||
'pgsql database creation')
|
||||
"handle_userdb_created",
|
||||
create_pgsql_database.s(instance.db_name, instance.db_user.name),
|
||||
"pgsql database creation",
|
||||
)
|
||||
_LOGGER.info(
|
||||
'The creation of a new PostgreSQL database %s has been'
|
||||
' requested in task %s',
|
||||
instance.db_name, taskresult.task_id)
|
||||
"The creation of a new PostgreSQL database %s has been"
|
||||
" requested in task %s",
|
||||
instance.db_name,
|
||||
taskresult.task_id,
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
'created UserDatabase for unknown database type %s',
|
||||
instance.db_user.db_type)
|
||||
"created UserDatabase for unknown database type %s",
|
||||
instance.db_user.db_type,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
'database %s has been %s',
|
||||
instance, created and "created" or "updated")
|
||||
"database %s has been %s", instance, created and "created" or "updated"
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=UserDatabase)
|
||||
|
@ -307,25 +334,31 @@ def handle_userdb_deleted(sender, instance, **kwargs):
|
|||
"""
|
||||
if instance.db_user.db_type == DB_TYPES.mysql:
|
||||
taskresult = TaskResult.objects.create_task_result(
|
||||
'handle_userdb_deleted',
|
||||
"handle_userdb_deleted",
|
||||
delete_mysql_database.s(instance.db_name, instance.db_user.name),
|
||||
'mysql database deletion')
|
||||
"mysql database deletion",
|
||||
)
|
||||
_LOGGER.info(
|
||||
'The deletion of MySQL database %s has been requested in task %s',
|
||||
instance.db_name, taskresult.task_id)
|
||||
"The deletion of MySQL database %s has been requested in task %s",
|
||||
instance.db_name,
|
||||
taskresult.task_id,
|
||||
)
|
||||
elif instance.db_user.db_type == DB_TYPES.pgsql:
|
||||
taskresult = TaskResult.objects.create_task_result(
|
||||
'handle_userdb_deleted',
|
||||
"handle_userdb_deleted",
|
||||
delete_pgsql_database.s(instance.db_name),
|
||||
'pgsql database deletion')
|
||||
"pgsql database deletion",
|
||||
)
|
||||
_LOGGER.info(
|
||||
'The deletion of PostgreSQL database %s has been requested in '
|
||||
' task %s',
|
||||
instance.db_name, taskresult.task_id)
|
||||
"The deletion of PostgreSQL database %s has been requested in " " task %s",
|
||||
instance.db_name,
|
||||
taskresult.task_id,
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
'deleted UserDatabase %s of unknown type %s',
|
||||
instance.db_name, instance.db_type)
|
||||
"deleted UserDatabase %s of unknown type %s",
|
||||
instance.db_name,
|
||||
instance.db_type,
|
||||
)
|
||||
pass
|
||||
_LOGGER.debug(
|
||||
'database %s has been deleted', instance)
|
||||
_LOGGER.debug("database %s has been deleted", instance)
|
||||
|
|
|
@ -3,8 +3,6 @@ This module provides tests for the functions in
|
|||
:py:mod:`userdbs.templatetags.userdb`.
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
|
@ -20,26 +18,22 @@ class UserdbTemplateTagTests(TestCase):
|
|||
"""
|
||||
|
||||
def test_db_type_icon_class_unknown(self):
|
||||
self.assertEqual(
|
||||
db_type_icon_class({'db_type': 'unknown'}),
|
||||
'icon-database')
|
||||
self.assertEqual(db_type_icon_class({"db_type": "unknown"}), "icon-database")
|
||||
|
||||
def test_db_type_icon_class_mysql(self):
|
||||
self.assertEqual(
|
||||
db_type_icon_class({'db_type': DB_TYPES.mysql}),
|
||||
'icon-mysql')
|
||||
self.assertEqual(db_type_icon_class({"db_type": DB_TYPES.mysql}), "icon-mysql")
|
||||
|
||||
def test_db_type_icon_class_pgsql(self):
|
||||
self.assertEqual(
|
||||
db_type_icon_class({'db_type': DB_TYPES.pgsql}),
|
||||
'icon-postgres')
|
||||
db_type_icon_class({"db_type": DB_TYPES.pgsql}), "icon-postgres"
|
||||
)
|
||||
|
||||
def test_db_type_name_mysql(self):
|
||||
self.assertEqual(
|
||||
db_type_name({'db_type': DB_TYPES.mysql}),
|
||||
_(DB_TYPES[DB_TYPES.mysql]))
|
||||
db_type_name({"db_type": DB_TYPES.mysql}), _(DB_TYPES[DB_TYPES.mysql])
|
||||
)
|
||||
|
||||
def test_db_type_name_pgsql(self):
|
||||
self.assertEqual(
|
||||
db_type_name({'db_type': DB_TYPES.pgsql}),
|
||||
_(DB_TYPES[DB_TYPES.pgsql]))
|
||||
db_type_name({"db_type": DB_TYPES.pgsql}), _(DB_TYPES[DB_TYPES.pgsql])
|
||||
)
|
||||
|
|
|
@ -2,21 +2,24 @@
|
|||
This module defines the URL patterns for user database views.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import re_path
|
||||
|
||||
from .views import (
|
||||
AddUserDatabase,
|
||||
ChangeDatabaseUserPassword,
|
||||
DeleteUserDatabase,
|
||||
)
|
||||
from .views import AddUserDatabase, ChangeDatabaseUserPassword, DeleteUserDatabase
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^(?P<package>\d+)/create$',
|
||||
AddUserDatabase.as_view(), name='add_userdatabase'),
|
||||
url(r'^(?P<package>\d+)/(?P<slug>[\w0-9]+)/setpassword',
|
||||
ChangeDatabaseUserPassword.as_view(), name='change_dbuser_password'),
|
||||
url(r'^(?P<package>\d+)/(?P<slug>[\w0-9]+)/delete',
|
||||
DeleteUserDatabase.as_view(), name='delete_userdatabase'),
|
||||
re_path(
|
||||
r"^(?P<package>\d+)/create$", AddUserDatabase.as_view(), name="add_userdatabase"
|
||||
),
|
||||
re_path(
|
||||
r"^(?P<package>\d+)/(?P<slug>[\w0-9]+)/setpassword",
|
||||
ChangeDatabaseUserPassword.as_view(),
|
||||
name="change_dbuser_password",
|
||||
),
|
||||
re_path(
|
||||
r"^(?P<package>\d+)/(?P<slug>[\w0-9]+)/delete",
|
||||
DeleteUserDatabase.as_view(),
|
||||
name="delete_userdatabase",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -2,30 +2,19 @@
|
|||
This module defines views for user database handling.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic.edit import (
|
||||
CreateView,
|
||||
DeleteView,
|
||||
UpdateView,
|
||||
)
|
||||
from django.contrib import messages
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
||||
from gvacommon.viewmixins import StaffOrSelfLoginRequiredMixin
|
||||
|
||||
from gvawebcore.views import HostingPackageAndCustomerMixin
|
||||
|
||||
from .forms import (
|
||||
AddUserDatabaseForm,
|
||||
ChangeDatabaseUserPasswordForm,
|
||||
)
|
||||
from .models import (
|
||||
DB_TYPES,
|
||||
DatabaseUser,
|
||||
UserDatabase,
|
||||
)
|
||||
from .forms import AddUserDatabaseForm, ChangeDatabaseUserPasswordForm
|
||||
from .models import DB_TYPES, DatabaseUser, UserDatabase
|
||||
|
||||
|
||||
class AddUserDatabase(
|
||||
|
@ -35,9 +24,10 @@ class AddUserDatabase(
|
|||
This view is used to setup new user databases.
|
||||
|
||||
"""
|
||||
|
||||
model = UserDatabase
|
||||
context_object_name = 'database'
|
||||
template_name_suffix = '_create'
|
||||
context_object_name = "database"
|
||||
template_name_suffix = "_create"
|
||||
form_class = AddUserDatabaseForm
|
||||
|
||||
def _get_dbtypes(self, hostingpackage):
|
||||
|
@ -45,29 +35,33 @@ class AddUserDatabase(
|
|||
db_options = hostingpackage.get_databases()
|
||||
for opt in db_options:
|
||||
dbs_of_type = UserDatabase.objects.filter(
|
||||
db_user__osuser=hostingpackage.osuser,
|
||||
db_user__db_type=opt['db_type']).count()
|
||||
if dbs_of_type < opt['number']:
|
||||
retval.append((opt['db_type'], DB_TYPES[opt['db_type']]))
|
||||
db_user__osuser=hostingpackage.osuser, db_user__db_type=opt["db_type"]
|
||||
).count()
|
||||
if dbs_of_type < opt["number"]:
|
||||
retval.append((opt["db_type"], DB_TYPES[opt["db_type"]]))
|
||||
if len(retval) < 1:
|
||||
raise SuspiciousOperation(
|
||||
_("The hosting package has no database products assigned."))
|
||||
_("The hosting package has no database products assigned.")
|
||||
)
|
||||
return retval
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super(AddUserDatabase, self).get_form_kwargs()
|
||||
kwargs['hostingpackage'] = self.get_hosting_package()
|
||||
kwargs['dbtypes'] = self._get_dbtypes(kwargs['hostingpackage'])
|
||||
kwargs["hostingpackage"] = self.get_hosting_package()
|
||||
kwargs["dbtypes"] = self._get_dbtypes(kwargs["hostingpackage"])
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
userdatabase = form.save()
|
||||
messages.success(
|
||||
self.request,
|
||||
_('Successfully create new {type} database {dbname} for user '
|
||||
'{dbuser}.').format(
|
||||
type=userdatabase.db_user.db_type,
|
||||
dbname=userdatabase.db_name, dbuser=userdatabase.db_user)
|
||||
_(
|
||||
"Successfully create new {type} database {dbname} for user " "{dbuser}."
|
||||
).format(
|
||||
type=userdatabase.db_user.db_type,
|
||||
dbname=userdatabase.db_name,
|
||||
dbuser=userdatabase.db_user,
|
||||
),
|
||||
)
|
||||
return redirect(self.get_hosting_package())
|
||||
|
||||
|
@ -79,30 +73,31 @@ class ChangeDatabaseUserPassword(
|
|||
This view is used to change a database user's password.
|
||||
|
||||
"""
|
||||
|
||||
model = DatabaseUser
|
||||
slug_field = 'name'
|
||||
context_object_name = 'dbuser'
|
||||
template_name_suffix = '_setpassword'
|
||||
slug_field = "name"
|
||||
context_object_name = "dbuser"
|
||||
template_name_suffix = "_setpassword"
|
||||
form_class = ChangeDatabaseUserPasswordForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super(ChangeDatabaseUserPassword, self).get_form_kwargs()
|
||||
kwargs['hostingpackage'] = self.get_hosting_package()
|
||||
kwargs["hostingpackage"] = self.get_hosting_package()
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ChangeDatabaseUserPassword, self).get_context_data(
|
||||
**kwargs)
|
||||
context['hostingpackage'] = self.get_hosting_package()
|
||||
context['customer'] = self.get_customer_object()
|
||||
context = super(ChangeDatabaseUserPassword, self).get_context_data(**kwargs)
|
||||
context["hostingpackage"] = self.get_hosting_package()
|
||||
context["customer"] = self.get_customer_object()
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
db_user = form.save()
|
||||
messages.success(
|
||||
self.request,
|
||||
_('Successfully changed password of database user {dbuser}.'
|
||||
).format(dbuser=db_user.name)
|
||||
_("Successfully changed password of database user {dbuser}.").format(
|
||||
dbuser=db_user.name
|
||||
),
|
||||
)
|
||||
return redirect(self.get_hosting_package())
|
||||
|
||||
|
@ -115,21 +110,24 @@ class DeleteUserDatabase(
|
|||
no more databases assigned.
|
||||
|
||||
"""
|
||||
|
||||
model = UserDatabase
|
||||
slug_field = 'db_name'
|
||||
context_object_name = 'database'
|
||||
slug_field = "db_name"
|
||||
context_object_name = "database"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DeleteUserDatabase, self).get_context_data(**kwargs)
|
||||
context.update({
|
||||
'hostingpackage': self.get_hosting_package(),
|
||||
'customer': self.get_customer_object(),
|
||||
})
|
||||
context.update(
|
||||
{
|
||||
"hostingpackage": self.get_hosting_package(),
|
||||
"customer": self.get_customer_object(),
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(
|
||||
self.request,
|
||||
_('Database deleted.'),
|
||||
_("Database deleted."),
|
||||
)
|
||||
return self.get_hosting_package().get_absolute_url()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue