mirror of
https://github.com/jazzband/django-admin2.git
synced 2026-03-16 22:20:24 +00:00
Fix for recent Django.
This commit is contained in:
parent
245b5911e6
commit
f5ee0d12c3
8 changed files with 171 additions and 140 deletions
108
djadmin2/core.py
108
djadmin2/core.py
|
|
@ -5,7 +5,7 @@ Issue #99.
|
|||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
from django.urls import re_path
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from . import apiviews
|
||||
|
|
@ -23,12 +23,13 @@ class Admin2(object):
|
|||
It also provides an index view that serves as an entry point to the
|
||||
admin site.
|
||||
"""
|
||||
|
||||
index_view = views.IndexView
|
||||
login_view = views.LoginView
|
||||
app_index_view = views.AppIndexView
|
||||
api_index_view = apiviews.IndexAPIView
|
||||
|
||||
def __init__(self, name='admin2'):
|
||||
def __init__(self, name="admin2"):
|
||||
self.registry = {}
|
||||
self.apps = {}
|
||||
self.app_verbose_names = {}
|
||||
|
|
@ -48,7 +49,8 @@ class Admin2(object):
|
|||
"""
|
||||
if model in self.registry:
|
||||
raise ImproperlyConfigured(
|
||||
'%s is already registered in django-admin2' % model)
|
||||
"%s is already registered in django-admin2" % model
|
||||
)
|
||||
if not model_admin:
|
||||
model_admin = types.ModelAdmin2
|
||||
self.registry[model] = model_admin(model, admin=self, **kwargs)
|
||||
|
|
@ -71,7 +73,8 @@ class Admin2(object):
|
|||
del self.registry[model]
|
||||
except KeyError:
|
||||
raise ImproperlyConfigured(
|
||||
'%s was never registered in django-admin2' % model)
|
||||
"%s was never registered in django-admin2" % model
|
||||
)
|
||||
|
||||
# Remove the model from the apps registry
|
||||
# Get the app label
|
||||
|
|
@ -93,7 +96,8 @@ class Admin2(object):
|
|||
"""
|
||||
if app_label in self.app_verbose_names:
|
||||
raise ImproperlyConfigured(
|
||||
'%s is already registered in django-admin2' % app_label)
|
||||
"%s is already registered in django-admin2" % app_label
|
||||
)
|
||||
|
||||
self.app_verbose_names[app_label] = app_verbose_name
|
||||
|
||||
|
|
@ -109,7 +113,8 @@ class Admin2(object):
|
|||
del self.app_verbose_names[app_label]
|
||||
except KeyError:
|
||||
raise ImproperlyConfigured(
|
||||
'%s app label was never registered in django-admin2' % app_label)
|
||||
"%s app label was never registered in django-admin2" % app_label
|
||||
)
|
||||
|
||||
def autodiscover(self):
|
||||
"""
|
||||
|
|
@ -120,7 +125,7 @@ class Admin2(object):
|
|||
try:
|
||||
import_module("%s.admin2" % app_name)
|
||||
except ImportError as e:
|
||||
if str(e).startswith("No module named") and 'admin2' in str(e):
|
||||
if str(e).startswith("No module named") and "admin2" in str(e):
|
||||
continue
|
||||
raise e
|
||||
|
||||
|
|
@ -132,71 +137,74 @@ class Admin2(object):
|
|||
for object_admin in self.registry.values():
|
||||
if object_admin.name == name:
|
||||
return object_admin
|
||||
raise ValueError(
|
||||
u'No object admin found with name {}'.format(repr(name)))
|
||||
raise ValueError(u"No object admin found with name {}".format(repr(name)))
|
||||
|
||||
def get_index_kwargs(self):
|
||||
return {
|
||||
'registry': self.registry,
|
||||
'app_verbose_names': self.app_verbose_names,
|
||||
'apps': self.apps,
|
||||
'login_view': self.login_view,
|
||||
"registry": self.registry,
|
||||
"app_verbose_names": self.app_verbose_names,
|
||||
"apps": self.apps,
|
||||
"login_view": self.login_view,
|
||||
}
|
||||
|
||||
def get_app_index_kwargs(self):
|
||||
return {
|
||||
'registry': self.registry,
|
||||
'app_verbose_names': self.app_verbose_names,
|
||||
'apps': self.apps,
|
||||
"registry": self.registry,
|
||||
"app_verbose_names": self.app_verbose_names,
|
||||
"apps": self.apps,
|
||||
}
|
||||
|
||||
def get_api_index_kwargs(self):
|
||||
return {
|
||||
'registry': self.registry,
|
||||
'app_verbose_names': self.app_verbose_names,
|
||||
'apps': self.apps,
|
||||
"registry": self.registry,
|
||||
"app_verbose_names": self.app_verbose_names,
|
||||
"apps": self.apps,
|
||||
}
|
||||
|
||||
def get_urls(self):
|
||||
urlpatterns = [
|
||||
url(regex=r'^$',
|
||||
re_path(
|
||||
r"^$",
|
||||
view=self.index_view.as_view(**self.get_index_kwargs()),
|
||||
name='dashboard'
|
||||
),
|
||||
url(regex=r'^auth/user/(?P<pk>\d+)/update/password/$',
|
||||
name="dashboard",
|
||||
),
|
||||
re_path(
|
||||
r"^auth/user/(?P<pk>\d+)/update/password/$",
|
||||
view=views.PasswordChangeView.as_view(),
|
||||
name='password_change'
|
||||
),
|
||||
url(regex='^password_change_done/$',
|
||||
name="password_change",
|
||||
),
|
||||
re_path(
|
||||
"^password_change_done/$",
|
||||
view=views.PasswordChangeDoneView.as_view(),
|
||||
name='password_change_done'
|
||||
),
|
||||
url(regex='^logout/$',
|
||||
view=views.LogoutView.as_view(),
|
||||
name='logout'
|
||||
),
|
||||
url(regex=r'^(?P<app_label>\w+)/$',
|
||||
view=self.app_index_view.as_view(
|
||||
**self.get_app_index_kwargs()),
|
||||
name='app_index'
|
||||
),
|
||||
url(regex=r'^api/v0/$',
|
||||
view=self.api_index_view.as_view(
|
||||
**self.get_api_index_kwargs()),
|
||||
name='api_index'
|
||||
),
|
||||
name="password_change_done",
|
||||
),
|
||||
re_path("^logout/$", view=views.LogoutView.as_view(), name="logout"),
|
||||
re_path(
|
||||
r"^(?P<app_label>\w+)/$",
|
||||
view=self.app_index_view.as_view(**self.get_app_index_kwargs()),
|
||||
name="app_index",
|
||||
),
|
||||
re_path(
|
||||
r"^api/v0/$",
|
||||
view=self.api_index_view.as_view(**self.get_api_index_kwargs()),
|
||||
name="api_index",
|
||||
),
|
||||
]
|
||||
for model, model_admin in self.registry.items():
|
||||
model_options = utils.model_options(model)
|
||||
urlpatterns += [
|
||||
url('^{}/{}/'.format(
|
||||
model_options.app_label,
|
||||
model_options.object_name.lower()),
|
||||
model_admin.urls),
|
||||
url('^api/v0/{}/{}/'.format(
|
||||
model_options.app_label,
|
||||
model_options.object_name.lower()),
|
||||
model_admin.api_urls),
|
||||
re_path(
|
||||
"^{}/{}/".format(
|
||||
model_options.app_label, model_options.object_name.lower()
|
||||
),
|
||||
model_admin.urls,
|
||||
),
|
||||
re_path(
|
||||
"^api/v0/{}/{}/".format(
|
||||
model_options.app_label, model_options.object_name.lower()
|
||||
),
|
||||
model_admin.api_urls,
|
||||
),
|
||||
]
|
||||
return urlpatterns
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import re_path
|
||||
|
||||
from djadmin2.site import djadmin2_site
|
||||
|
||||
|
|
@ -15,5 +15,5 @@ djadmin2_site.login_view = CustomLoginView
|
|||
djadmin2_site.autodiscover()
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin2/', djadmin2_site.urls),
|
||||
re_path(r'^admin2/', djadmin2_site.urls),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
</li>
|
||||
{% endif %}
|
||||
{% for link, date in dates %}
|
||||
<li class="{% ifequal active_day date %}active{% endifequal %}">
|
||||
<li class="{% if active_day == date %}active{% endif %}">
|
||||
<a href="{{ link|safe }}">{{ date }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ import sys
|
|||
from collections import namedtuple
|
||||
|
||||
import extra_views
|
||||
from django.conf.urls import url
|
||||
from django.forms import modelform_factory
|
||||
from django.urls import reverse
|
||||
from django.urls import re_path, reverse
|
||||
|
||||
from . import actions
|
||||
from . import apiviews
|
||||
|
|
@ -15,14 +14,12 @@ from . import utils
|
|||
from . import views
|
||||
|
||||
|
||||
logger = logging.getLogger('djadmin2')
|
||||
logger = logging.getLogger("djadmin2")
|
||||
|
||||
|
||||
class ModelAdminBase2(type):
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
new_class = super(ModelAdminBase2, cls).__new__(cls, name,
|
||||
bases, attrs)
|
||||
new_class = super(ModelAdminBase2, cls).__new__(cls, name, bases, attrs)
|
||||
view_list = []
|
||||
for key, value in attrs.items():
|
||||
if isinstance(value, views.AdminView):
|
||||
|
|
@ -30,7 +27,7 @@ class ModelAdminBase2(type):
|
|||
value.name = key
|
||||
view_list.append(value)
|
||||
|
||||
view_list.extend(getattr(new_class, 'views', []))
|
||||
view_list.extend(getattr(new_class, "views", []))
|
||||
new_class.views = view_list
|
||||
return new_class
|
||||
|
||||
|
|
@ -54,9 +51,10 @@ class ModelAdmin2(metaclass=ModelAdminBase2):
|
|||
This prevents us from easily implementing methods/setters which
|
||||
bypass the blocking features of the ImmutableAdmin.
|
||||
"""
|
||||
|
||||
actions_selection_counter = True
|
||||
date_hierarchy = False
|
||||
list_display = ('__str__',)
|
||||
list_display = ("__str__",)
|
||||
list_display_links = ()
|
||||
list_filter = ()
|
||||
list_select_related = False
|
||||
|
|
@ -113,12 +111,20 @@ class ModelAdmin2(metaclass=ModelAdminBase2):
|
|||
inlines = []
|
||||
|
||||
# Views
|
||||
index_view = views.AdminView(r'^$', views.ModelListView, name='index')
|
||||
create_view = views.AdminView(r'^create/$', views.ModelAddFormView, name='create')
|
||||
update_view = views.AdminView(r'^(?P<pk>[0-9]+)/$', views.ModelEditFormView, name='update')
|
||||
detail_view = views.AdminView(r'^(?P<pk>[0-9]+)/update/$', views.ModelDetailView, name='detail')
|
||||
delete_view = views.AdminView(r'^(?P<pk>[0-9]+)/delete/$', views.ModelDeleteView, name='delete')
|
||||
history_view = views.AdminView(r'^(?P<pk>[0-9]+)/history/$', views.ModelHistoryView, name='history')
|
||||
index_view = views.AdminView(r"^$", views.ModelListView, name="index")
|
||||
create_view = views.AdminView(r"^create/$", views.ModelAddFormView, name="create")
|
||||
update_view = views.AdminView(
|
||||
r"^(?P<pk>[0-9]+)/$", views.ModelEditFormView, name="update"
|
||||
)
|
||||
detail_view = views.AdminView(
|
||||
r"^(?P<pk>[0-9]+)/update/$", views.ModelDetailView, name="detail"
|
||||
)
|
||||
delete_view = views.AdminView(
|
||||
r"^(?P<pk>[0-9]+)/delete/$", views.ModelDeleteView, name="delete"
|
||||
)
|
||||
history_view = views.AdminView(
|
||||
r"^(?P<pk>[0-9]+)/history/$", views.ModelHistoryView, name="history"
|
||||
)
|
||||
views = []
|
||||
|
||||
# API configuration
|
||||
|
|
@ -137,7 +143,7 @@ class ModelAdmin2(metaclass=ModelAdminBase2):
|
|||
self.model_name = model_options.object_name.lower()
|
||||
|
||||
if self.name is None:
|
||||
self.name = '{}_{}'.format(self.app_label, self.model_name)
|
||||
self.name = "{}_{}".format(self.app_label, self.model_name)
|
||||
|
||||
if self.verbose_name is None:
|
||||
self.verbose_name = model_options.verbose_name
|
||||
|
|
@ -146,60 +152,73 @@ class ModelAdmin2(metaclass=ModelAdminBase2):
|
|||
|
||||
def get_default_view_kwargs(self):
|
||||
return {
|
||||
'app_label': self.app_label,
|
||||
'model': self.model,
|
||||
'model_name': self.model_name,
|
||||
'model_admin': immutable_admin_factory(self),
|
||||
"app_label": self.app_label,
|
||||
"model": self.model,
|
||||
"model_name": self.model_name,
|
||||
"model_admin": immutable_admin_factory(self),
|
||||
}
|
||||
|
||||
def get_index_kwargs(self):
|
||||
kwargs = self.get_default_view_kwargs()
|
||||
kwargs.update({
|
||||
'paginate_by': self.list_per_page,
|
||||
})
|
||||
kwargs.update(
|
||||
{
|
||||
"paginate_by": self.list_per_page,
|
||||
}
|
||||
)
|
||||
return kwargs
|
||||
|
||||
def get_default_api_view_kwargs(self):
|
||||
kwargs = self.get_default_view_kwargs()
|
||||
kwargs.update({
|
||||
'serializer_class': self.api_serializer_class,
|
||||
})
|
||||
kwargs.update(
|
||||
{
|
||||
"serializer_class": self.api_serializer_class,
|
||||
}
|
||||
)
|
||||
return kwargs
|
||||
|
||||
def get_prefixed_view_name(self, view_name):
|
||||
return '{}_{}'.format(self.name, view_name)
|
||||
return "{}_{}".format(self.name, view_name)
|
||||
|
||||
def get_create_kwargs(self):
|
||||
kwargs = self.get_default_view_kwargs()
|
||||
kwargs.update({
|
||||
'inlines': self.inlines,
|
||||
'form_class': (self.create_form_class if
|
||||
self.create_form_class else self.form_class),
|
||||
})
|
||||
kwargs.update(
|
||||
{
|
||||
"inlines": self.inlines,
|
||||
"form_class": (
|
||||
self.create_form_class
|
||||
if self.create_form_class
|
||||
else self.form_class
|
||||
),
|
||||
}
|
||||
)
|
||||
return kwargs
|
||||
|
||||
def get_update_kwargs(self):
|
||||
kwargs = self.get_default_view_kwargs()
|
||||
form_class = (self.update_form_class if
|
||||
self.update_form_class else self.form_class)
|
||||
form_class = (
|
||||
self.update_form_class if self.update_form_class else self.form_class
|
||||
)
|
||||
if form_class is None:
|
||||
form_class = modelform_factory(self.model, fields='__all__')
|
||||
kwargs.update({
|
||||
'inlines': self.inlines,
|
||||
'form_class': form_class,
|
||||
})
|
||||
form_class = modelform_factory(self.model, fields="__all__")
|
||||
kwargs.update(
|
||||
{
|
||||
"inlines": self.inlines,
|
||||
"form_class": form_class,
|
||||
}
|
||||
)
|
||||
return kwargs
|
||||
|
||||
def get_index_url(self):
|
||||
return reverse('admin2:{}'.format(
|
||||
self.get_prefixed_view_name('index')))
|
||||
return reverse("admin2:{}".format(self.get_prefixed_view_name("index")))
|
||||
|
||||
def get_api_list_kwargs(self):
|
||||
kwargs = self.get_default_api_view_kwargs()
|
||||
kwargs.update({
|
||||
'queryset': self.model.objects.all(),
|
||||
# 'paginate_by': self.list_per_page,
|
||||
})
|
||||
kwargs.update(
|
||||
{
|
||||
"queryset": self.model.objects.all(),
|
||||
# 'paginate_by': self.list_per_page,
|
||||
}
|
||||
)
|
||||
return kwargs
|
||||
|
||||
def get_api_detail_kwargs(self):
|
||||
|
|
@ -218,34 +237,35 @@ class ModelAdmin2(metaclass=ModelAdminBase2):
|
|||
trace = sys.exc_info()[2]
|
||||
new_exception = TypeError(
|
||||
'Cannot instantiate admin view "{}.{}". '
|
||||
'The error that got raised was: {}'.format(
|
||||
self.__class__.__name__, admin_view.name, e))
|
||||
"The error that got raised was: {}".format(
|
||||
self.__class__.__name__, admin_view.name, e
|
||||
)
|
||||
)
|
||||
try:
|
||||
raise new_exception.with_traceback(trace)
|
||||
except AttributeError:
|
||||
raise (new_exception, None, trace)
|
||||
|
||||
pattern_list.append(
|
||||
url(
|
||||
regex=admin_view.url,
|
||||
re_path(
|
||||
admin_view.url,
|
||||
view=view_instance,
|
||||
name=self.get_prefixed_view_name(admin_view.name)
|
||||
name=self.get_prefixed_view_name(admin_view.name),
|
||||
)
|
||||
)
|
||||
return pattern_list
|
||||
|
||||
def get_api_urls(self):
|
||||
return [
|
||||
url(
|
||||
regex=r'^$',
|
||||
re_path(
|
||||
r"^$",
|
||||
view=self.api_list_view.as_view(**self.get_api_list_kwargs()),
|
||||
name=self.get_prefixed_view_name('api_list'),
|
||||
name=self.get_prefixed_view_name("api_list"),
|
||||
),
|
||||
url(
|
||||
regex=r'^(?P<pk>[0-9]+)/$',
|
||||
view=self.api_detail_view.as_view(
|
||||
**self.get_api_detail_kwargs()),
|
||||
name=self.get_prefixed_view_name('api_detail'),
|
||||
re_path(
|
||||
r"^(?P<pk>[0-9]+)/$",
|
||||
view=self.api_detail_view.as_view(**self.get_api_detail_kwargs()),
|
||||
name=self.get_prefixed_view_name("api_detail"),
|
||||
),
|
||||
]
|
||||
|
||||
|
|
@ -262,12 +282,12 @@ class ModelAdmin2(metaclass=ModelAdminBase2):
|
|||
actions_dict = {}
|
||||
|
||||
for cls in type(self).mro()[::-1]:
|
||||
class_actions = getattr(cls, 'list_actions', [])
|
||||
class_actions = getattr(cls, "list_actions", [])
|
||||
for action in class_actions:
|
||||
actions_dict[action.__name__] = {
|
||||
'name': action.__name__,
|
||||
'description': actions.get_description(action),
|
||||
'action_callable': action
|
||||
"name": action.__name__,
|
||||
"description": actions.get_description(action),
|
||||
"action_callable": action,
|
||||
}
|
||||
return actions_dict
|
||||
|
||||
|
|
@ -280,8 +300,9 @@ class Admin2Inline(extra_views.InlineFormSetFactory):
|
|||
A simple extension of django-extra-view's InlineFormSet that
|
||||
adds some useful functionality.
|
||||
"""
|
||||
|
||||
template = None
|
||||
fields = '__all__'
|
||||
fields = "__all__"
|
||||
|
||||
def construct_formset(self):
|
||||
"""
|
||||
|
|
@ -296,12 +317,14 @@ class Admin2Inline(extra_views.InlineFormSetFactory):
|
|||
|
||||
class Admin2TabularInline(Admin2Inline):
|
||||
template = os.path.join(
|
||||
settings.ADMIN2_THEME_DIRECTORY, 'edit_inlines/tabular.html')
|
||||
settings.ADMIN2_THEME_DIRECTORY, "edit_inlines/tabular.html"
|
||||
)
|
||||
|
||||
|
||||
class Admin2StackedInline(Admin2Inline):
|
||||
template = os.path.join(
|
||||
settings.ADMIN2_THEME_DIRECTORY, 'edit_inlines/stacked.html')
|
||||
settings.ADMIN2_THEME_DIRECTORY, "edit_inlines/stacked.html"
|
||||
)
|
||||
|
||||
|
||||
def immutable_admin_factory(model_admin):
|
||||
|
|
@ -315,7 +338,7 @@ def immutable_admin_factory(model_admin):
|
|||
the result, but hopefully developers attempting that
|
||||
'workaround/hack' will read our documentation.
|
||||
"""
|
||||
ImmutableAdmin = namedtuple('ImmutableAdmin',
|
||||
model_admin.model_admin_attributes)
|
||||
return ImmutableAdmin(*[getattr(
|
||||
model_admin, x) for x in model_admin.model_admin_attributes])
|
||||
ImmutableAdmin = namedtuple("ImmutableAdmin", model_admin.model_admin_attributes)
|
||||
return ImmutableAdmin(
|
||||
*[getattr(model_admin, x) for x in model_admin.model_admin_attributes]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
from blog.views import BlogListView, BlogDetailView
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import re_path
|
||||
|
||||
from djadmin2.site import djadmin2_site
|
||||
|
||||
|
|
@ -10,13 +10,17 @@ from djadmin2.site import djadmin2_site
|
|||
djadmin2_site.autodiscover()
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin2/', djadmin2_site.urls),
|
||||
url(r'^admin/', admin.site.urls),
|
||||
url(r'^blog/', BlogListView.as_view(template_name="blog/blog_list.html"),
|
||||
name='blog_list'),
|
||||
url(r'^blog/detail(?P<pk>\d+)/$',
|
||||
re_path(r"^admin2/", djadmin2_site.urls),
|
||||
re_path(r"^admin/", admin.site.urls),
|
||||
re_path(
|
||||
r"^blog/",
|
||||
BlogListView.as_view(template_name="blog/blog_list.html"),
|
||||
name="blog_list",
|
||||
),
|
||||
re_path(
|
||||
r"^blog/detail(?P<pk>\d+)/$",
|
||||
BlogDetailView.as_view(template_name="blog/blog_detail.html"),
|
||||
name='blog_detail'),
|
||||
url(r'^$', BlogListView.as_view(template_name="blog/home.html"),
|
||||
name='home'),
|
||||
name="blog_detail",
|
||||
),
|
||||
re_path(r"^$", BlogListView.as_view(template_name="blog/home.html"), name="home"),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import division, absolute_import, unicode_literals
|
||||
|
||||
from djadmin2.site import djadmin2_site
|
||||
from .models import CaptionedFile, UncaptionedFile
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
django-extra-views==0.12.0
|
||||
django-braces==1.14.0
|
||||
djangorestframework==3.11.1
|
||||
django-filter==2.3.0
|
||||
django-debug-toolbar>=1.10.1
|
||||
future>=0.15.2
|
||||
pytz>=2016.4
|
||||
django-extra-views
|
||||
django-braces
|
||||
djangorestframework
|
||||
django-filter
|
||||
django-debug-toolbar
|
||||
pytz
|
||||
|
|
|
|||
1
setup.py
1
setup.py
|
|
@ -136,7 +136,6 @@ setup(
|
|||
'djangorestframework>=3.11.1',
|
||||
'django-filter==2.3.0',
|
||||
'pytz>=2016.4',
|
||||
'future>=0.15.2',
|
||||
],
|
||||
extras_require={
|
||||
'testing': ['pytest', 'pytest-django', 'pytest-ipdb'],
|
||||
|
|
|
|||
Loading…
Reference in a new issue