Refactor admin_fields
This commit is contained in:
parent
8c13e75d5d
commit
06db4cd346
2
TODO.md
2
TODO.md
|
@ -65,3 +65,5 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
|
|||
|
||||
|
||||
* Be consistent with dates: name_on, created ?
|
||||
|
||||
* backend logs with hal logo
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from functools import wraps
|
||||
from functools import wraps, partial
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.admin import helpers
|
||||
|
@ -7,6 +7,22 @@ from django.utils.decorators import available_attrs
|
|||
from django.utils.encoding import force_text
|
||||
|
||||
|
||||
def admin_field(method):
|
||||
def admin_field_wrapper(*args, **kwargs):
|
||||
""" utility function for creating admin links """
|
||||
kwargs['field'] = args[0] if args else ''
|
||||
kwargs['order'] = kwargs.get('order', kwargs['field'])
|
||||
kwargs['popup'] = kwargs.get('popup', False)
|
||||
kwargs['description'] = kwargs.get('description',
|
||||
kwargs['field'].split('__')[-1].replace('_', ' ').capitalize())
|
||||
admin_method = partial(method, **kwargs)
|
||||
admin_method.short_description = kwargs['description']
|
||||
admin_method.allow_tags = True
|
||||
admin_method.admin_order_field = kwargs['order']
|
||||
return admin_method
|
||||
return admin_field_wrapper
|
||||
|
||||
|
||||
def action_with_confirmation(action_name, extra_context={},
|
||||
template='admin/orchestra/generic_confirmation.html'):
|
||||
"""
|
||||
|
@ -14,7 +30,6 @@ def action_with_confirmation(action_name, extra_context={},
|
|||
If custom template is provided the form must contain:
|
||||
<input type="hidden" name="post" value="generic_confirmation" />
|
||||
"""
|
||||
|
||||
def decorator(func, extra_context=extra_context, template=template):
|
||||
@wraps(func, assigned=available_attrs(func))
|
||||
def inner(modeladmin, request, queryset):
|
||||
|
@ -23,16 +38,16 @@ def action_with_confirmation(action_name, extra_context={},
|
|||
stay = func(modeladmin, request, queryset)
|
||||
if not stay:
|
||||
return
|
||||
|
||||
|
||||
opts = modeladmin.model._meta
|
||||
app_label = opts.app_label
|
||||
action_value = func.__name__
|
||||
|
||||
|
||||
if len(queryset) == 1:
|
||||
objects_name = force_text(opts.verbose_name)
|
||||
else:
|
||||
objects_name = force_text(opts.verbose_name_plural)
|
||||
|
||||
|
||||
context = {
|
||||
"title": "Are you sure?",
|
||||
"content_message": "Are you sure you want to %s the selected %s?" %
|
||||
|
@ -45,12 +60,11 @@ def action_with_confirmation(action_name, extra_context={},
|
|||
"app_label": app_label,
|
||||
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
||||
}
|
||||
|
||||
|
||||
context.update(extra_context)
|
||||
|
||||
|
||||
# Display the confirmation page
|
||||
return TemplateResponse(request, template,
|
||||
context, current_app=modeladmin.admin_site.name)
|
||||
return inner
|
||||
return decorator
|
||||
|
||||
|
|
|
@ -32,7 +32,8 @@ def get_services():
|
|||
for model, options in services.get().iteritems():
|
||||
if options.get('menu', True):
|
||||
opts = model._meta
|
||||
url = reverse('admin:%s_%s_changelist' % (opts.app_label, opts.model_name))
|
||||
url = reverse('admin:{}_{}_changelist'.format(
|
||||
opts.app_label, opts.model_name))
|
||||
name = capfirst(options.get('verbose_name_plural'))
|
||||
result.append(items.MenuItem(name, url))
|
||||
return sorted(result, key=lambda i: i.title)
|
||||
|
@ -40,24 +41,27 @@ def get_services():
|
|||
|
||||
def get_account_items():
|
||||
childrens = [
|
||||
items.MenuItem(_("Accounts"), reverse('admin:accounts_account_changelist'))
|
||||
items.MenuItem(_("Accounts"),
|
||||
reverse('admin:accounts_account_changelist'))
|
||||
]
|
||||
if isinstalled('orchestra.apps.contacts'):
|
||||
url = reverse('admin:contacts_contact_changelist')
|
||||
childrens.append(items.MenuItem(_("Contacts"), url))
|
||||
if isinstalled('orchestra.apps.users'):
|
||||
url = reverse('admin:users_user_changelist')
|
||||
users = [items.MenuItem(_("Users"), url)]
|
||||
if isinstalled('rest_framework.authtoken'):
|
||||
tokens = reverse('admin:authtoken_token_changelist')
|
||||
users.append(items.MenuItem(_("Tokens"), tokens))
|
||||
childrens.append(items.MenuItem(_("Users"), url, children=users))
|
||||
childrens.append(items.MenuItem(_("Users"), url))
|
||||
if isinstalled('orchestra.apps.prices'):
|
||||
url = reverse('admin:prices_pack_changelist')
|
||||
childrens.append(items.MenuItem(_("Packs"), url))
|
||||
if isinstalled('orchestra.apps.orders'):
|
||||
url = reverse('admin:orders_order_changelist')
|
||||
childrens.append(items.MenuItem(_("Orders"), url))
|
||||
if isinstalled('orchestra.apps.bills'):
|
||||
url = reverse('admin:bills_bill_changelist')
|
||||
childrens.append(items.MenuItem(_("Bills"), url))
|
||||
if isinstalled('orchestra.apps.payments'):
|
||||
url = reverse('admin:payments_transaction_changelist')
|
||||
childrens.append(items.MenuItem(_("Transactions"), url))
|
||||
if isinstalled('orchestra.apps.issues'):
|
||||
url = reverse('admin:issues_ticket_changelist')
|
||||
childrens.append(items.MenuItem(_("Tickets"), url))
|
||||
|
@ -92,7 +96,7 @@ def get_administration_items():
|
|||
childrens.append(items.MenuItem(_("Miscellaneous"), url))
|
||||
if isinstalled('orchestra.apps.issues'):
|
||||
url = reverse('admin:issues_queue_changelist')
|
||||
childrens.append(items.MenuItem(_("Issue queues"), url))
|
||||
childrens.append(items.MenuItem(_("Ticket queues"), url))
|
||||
if isinstalled('djcelery'):
|
||||
task = reverse('admin:djcelery_taskstate_changelist')
|
||||
periodic = reverse('admin:djcelery_periodictask_changelist')
|
||||
|
|
|
@ -12,6 +12,8 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from orchestra.models.utils import get_field_value
|
||||
from orchestra.utils.humanize import naturaldate
|
||||
|
||||
from .decorators import admin_field
|
||||
|
||||
|
||||
def get_modeladmin(model, import_module=True):
|
||||
""" returns the modeladmin registred for model """
|
||||
|
@ -44,7 +46,9 @@ def insertattr(model, name, value, weight=0):
|
|||
weights = {}
|
||||
if hasattr(modeladmin, 'weights') and name in modeladmin.weights:
|
||||
weights = modeladmin.weights.get(name)
|
||||
inserted_attrs[name] = [ (attr, weights.get(attr, 0)) for attr in getattr(modeladmin, name) ]
|
||||
inserted_attrs[name] = [
|
||||
(attr, weights.get(attr, 0)) for attr in getattr(modeladmin, name)
|
||||
]
|
||||
|
||||
inserted_attrs[name].append((value, weight))
|
||||
inserted_attrs[name].sort(key=lambda a: a[1])
|
||||
|
@ -70,85 +74,40 @@ def set_default_filter(queryarg, request, value):
|
|||
request.META['QUERY_STRING'] = request.GET.urlencode()
|
||||
|
||||
|
||||
@admin_field
|
||||
def admin_link(*args, **kwargs):
|
||||
""" utility function for creating admin links """
|
||||
field = args[0] if args else ''
|
||||
order = kwargs.pop('order', field)
|
||||
popup = kwargs.pop('popup', False)
|
||||
|
||||
def display_link(*args):
|
||||
instance = args[-1]
|
||||
obj = getattr(instance, field, instance)
|
||||
if not getattr(obj, 'pk', None):
|
||||
return '---'
|
||||
opts = obj._meta
|
||||
view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name)
|
||||
url = reverse(view_name, args=(obj.pk,))
|
||||
extra = ''
|
||||
if popup:
|
||||
extra = 'onclick="return showAddAnotherPopup(this);"'
|
||||
return '<a href="%s" %s>%s</a>' % (url, extra, obj)
|
||||
display_link.allow_tags = True
|
||||
display_link.short_description = _(field.replace('_', ' '))
|
||||
display_link.admin_order_field = order
|
||||
return display_link
|
||||
instance = args[-1]
|
||||
obj = get_field_value(instance, kwargs['field'])
|
||||
if not getattr(obj, 'pk', None):
|
||||
return '---'
|
||||
opts = obj._meta
|
||||
view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name)
|
||||
url = reverse(view_name, args=(obj.pk,))
|
||||
extra = ''
|
||||
if kwargs['popup']:
|
||||
extra = 'onclick="return showAddAnotherPopup(this);"'
|
||||
return '<a href="%s" %s>%s</a>' % (url, extra, obj)
|
||||
|
||||
|
||||
def colored(field_name, colours, description='', verbose=False, bold=True):
|
||||
""" returns a method that will render obj with colored html """
|
||||
def colored_field(obj, field=field_name, colors=colours, verbose=verbose):
|
||||
value = escape(get_field_value(obj, field))
|
||||
color = colors.get(value, "black")
|
||||
if verbose:
|
||||
# Get the human-readable value of a choice field
|
||||
value = getattr(obj, 'get_%s_display' % field)()
|
||||
colored_value = '<span style="color: %s;">%s</span>' % (color, value)
|
||||
if bold:
|
||||
colored_value = '<b>%s</b>' % colored_value
|
||||
return mark_safe(colored_value)
|
||||
if not description:
|
||||
description = field_name.split('__').pop().replace('_', ' ').capitalize()
|
||||
colored_field.short_description = description
|
||||
colored_field.allow_tags = True
|
||||
colored_field.admin_order_field = field_name
|
||||
return colored_field
|
||||
@admin_field
|
||||
def admin_colored(*args, **kwargs):
|
||||
instance = args[-1]
|
||||
field = kwargs['field']
|
||||
value = escape(get_field_value(instance, field))
|
||||
color = kwargs.get('colors', {}).get(value, 'black')
|
||||
value = getattr(instance, 'get_%s_display' % field)().upper()
|
||||
colored_value = '<span style="color: %s;">%s</span>' % (color, value)
|
||||
if kwargs.get('bold', True):
|
||||
colored_value = '<b>%s</b>' % colored_value
|
||||
return mark_safe(colored_value)
|
||||
|
||||
|
||||
#def display_timesince(date, double=False):
|
||||
# """
|
||||
# Format date for messages create_on: show a relative time
|
||||
# with contextual helper to show fulltime format.
|
||||
# """
|
||||
# if not date:
|
||||
# return 'Never'
|
||||
# date_rel = timesince(date)
|
||||
# if not double:
|
||||
# date_rel = date_rel.split(',')[0]
|
||||
# date_rel += ' ago'
|
||||
# date_abs = date.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||
# return mark_safe("<span title='%s'>%s</span>" % (date_abs, date_rel))
|
||||
|
||||
|
||||
def admin_date(field, **kwargs):
|
||||
""" utility function for creating admin dates """
|
||||
default = kwargs.pop('default', '')
|
||||
order = kwargs.pop('order', field)
|
||||
|
||||
def display_date(*args):
|
||||
instance = args[-1]
|
||||
value = get_field_value(instance, field)
|
||||
if not value:
|
||||
return default
|
||||
return '<span title="{0}">{1}</span>'.format(
|
||||
escape(str(value)), escape(naturaldate(value)),
|
||||
)
|
||||
display_date.short_description = _(field.replace('_', ' '))
|
||||
display_date.admin_order_field = order
|
||||
display_date.allow_tags = True
|
||||
return display_date
|
||||
|
||||
|
||||
#def display_timeuntil(date):
|
||||
# date_rel = timeuntil(date) + ' left'
|
||||
# date_abs = date.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||
# return mark_safe("<span title='%s'>%s</span>" % (date_abs, date_rel))
|
||||
@admin_field
|
||||
def admin_date(*args, **kwargs):
|
||||
instance = args[-1]
|
||||
value = get_field_value(instance, kwargs['field'])
|
||||
if not value:
|
||||
return kwargs.get('default', '')
|
||||
return '<span title="{0}">{1}</span>'.format(
|
||||
escape(str(value)), escape(naturaldate(value)),
|
||||
)
|
||||
|
|
|
@ -12,7 +12,8 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from markdown import markdown
|
||||
|
||||
from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin#, ChangeViewActions
|
||||
from orchestra.admin.utils import admin_link, colored, wrap_admin_view, admin_date
|
||||
from orchestra.admin.utils import (admin_link, admin_colored, wrap_admin_view,
|
||||
admin_date)
|
||||
from orchestra.apps.contacts import settings as contacts_settings
|
||||
|
||||
from .actions import (reject_tickets, resolve_tickets, take_tickets, close_tickets,
|
||||
|
@ -111,19 +112,13 @@ class TicketInline(admin.TabularInline):
|
|||
owner_link = admin_link('owner')
|
||||
created = admin_link('created_on')
|
||||
last_modified = admin_link('last_modified_on')
|
||||
colored_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||
colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||
|
||||
def ticket_id(self, instance):
|
||||
return '<b>%s</b>' % admin_link()(instance)
|
||||
ticket_id.short_description = '#'
|
||||
ticket_id.allow_tags = True
|
||||
|
||||
def colored_state(self, instance):
|
||||
return colored('state', STATE_COLORS, bold=False)(instance)
|
||||
colored_state.short_description = _("State")
|
||||
|
||||
def colored_priority(self, instance):
|
||||
return colored('priority', PRIORITY_COLORS, bold=False)(instance)
|
||||
colored_priority.short_description = _("Priority")
|
||||
|
||||
|
||||
class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeViewActions,
|
||||
|
@ -198,6 +193,8 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView
|
|||
display_queue = admin_link('queue')
|
||||
display_owner = admin_link('owner')
|
||||
last_modified = admin_date('last_modified_on')
|
||||
display_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||
display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||
|
||||
def display_summary(self, ticket):
|
||||
context = {
|
||||
|
@ -216,18 +213,6 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView
|
|||
display_summary.short_description = 'Summary'
|
||||
display_summary.allow_tags = True
|
||||
|
||||
def display_priority(self, ticket):
|
||||
""" State colored for change_form """
|
||||
return colored('priority', PRIORITY_COLORS, bold=False, verbose=True)(ticket)
|
||||
display_priority.short_description = _("Priority")
|
||||
display_priority.admin_order_field = 'priority'
|
||||
|
||||
def display_state(self, ticket):
|
||||
""" State colored for change_form """
|
||||
return colored('state', STATE_COLORS, bold=False, verbose=True)(ticket)
|
||||
display_state.short_description = _("State")
|
||||
display_state.admin_order_field = 'state'
|
||||
|
||||
def unbold_id(self, ticket):
|
||||
""" Unbold id if ticket is read """
|
||||
if ticket.is_read_by(self.user):
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.utils.html import escape
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin.html import monospace_format
|
||||
from orchestra.admin.utils import admin_link, admin_date, colored
|
||||
from orchestra.admin.utils import admin_link, admin_date, admin_colored
|
||||
|
||||
from .models import Server, Route, BackendLog, BackendOperation
|
||||
|
||||
|
@ -90,7 +90,7 @@ class BackendLogAdmin(admin.ModelAdmin):
|
|||
server_link = admin_link('server')
|
||||
display_last_update = admin_date('last_update')
|
||||
display_created = admin_date('created')
|
||||
display_state = colored('state', STATE_COLORS)
|
||||
display_state = admin_colored('state', colors=STATE_COLORS)
|
||||
|
||||
def mono_script(self, log):
|
||||
return monospace_format(escape(log.script))
|
||||
|
|
|
@ -1,7 +1,30 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from orchestra.admin.utils import admin_colored, admin_link
|
||||
|
||||
from .models import PaymentSource, Transaction
|
||||
|
||||
|
||||
STATE_COLORS = {
|
||||
Transaction.WAITTING_PROCESSING: 'darkorange',
|
||||
Transaction.WAITTING_CONFIRMATION: 'orange',
|
||||
Transaction.CONFIRMED: 'green',
|
||||
Transaction.REJECTED: 'red',
|
||||
Transaction.LOCKED: 'magenta',
|
||||
Transaction.DISCARTED: 'blue',
|
||||
}
|
||||
|
||||
|
||||
class TransactionAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'id', 'bill_link', 'account_link', 'method', 'display_state', 'amount'
|
||||
)
|
||||
list_filter = ('method', 'state')
|
||||
|
||||
bill_link = admin_link('bill')
|
||||
account_link = admin_link('bill__account')
|
||||
display_state = admin_colored('state', colors=STATE_COLORS)
|
||||
|
||||
|
||||
admin.site.register(PaymentSource)
|
||||
admin.site.register(Transaction)
|
||||
admin.site.register(Transaction, TransactionAdmin)
|
||||
|
|
|
@ -12,6 +12,7 @@ class PaymentSource(models.Model):
|
|||
method = models.CharField(_("method"), max_length=32,
|
||||
choices=PaymentMethod.get_plugin_choices())
|
||||
data = JSONField(_("data"))
|
||||
is_active = models.BooleanField(_("is active"), default=True)
|
||||
|
||||
|
||||
class Transaction(models.Model):
|
||||
|
@ -22,14 +23,15 @@ class Transaction(models.Model):
|
|||
LOCKED = 'LOCKED'
|
||||
DISCARTED = 'DISCARTED'
|
||||
STATES = (
|
||||
(WAITTING_PROCESSING, _("Waitting for processing")),
|
||||
(WAITTING_CONFIRMATION, _("Waitting for confirmation")),
|
||||
(WAITTING_PROCESSING, _("Waitting processing")),
|
||||
(WAITTING_CONFIRMATION, _("Waitting confirmation")),
|
||||
(CONFIRMED, _("Confirmed")),
|
||||
(REJECTED, _("Rejected")),
|
||||
(LOCKED, _("Locked")),
|
||||
(DISCARTED, _("Discarted")),
|
||||
)
|
||||
|
||||
# TODO account fk?
|
||||
bill = models.ForeignKey('bills.bill', verbose_name=_("bill"),
|
||||
related_name='transactions')
|
||||
method = models.CharField(_("payment method"), max_length=32,
|
||||
|
@ -42,3 +44,6 @@ class Transaction(models.Model):
|
|||
created_on = models.DateTimeField(auto_now_add=True)
|
||||
modified_on = models.DateTimeField(auto_now=True)
|
||||
related = models.ForeignKey('self', null=True, blank=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return "Transaction {}".format(self.id)
|
||||
|
|
|
@ -15,8 +15,8 @@ from .models import Resource, ResourceData, MonitorData
|
|||
|
||||
class ResourceAdmin(ExtendedModelAdmin):
|
||||
list_display = (
|
||||
'id', 'name', 'verbose_name', 'content_type', 'period', 'ondemand',
|
||||
'default_allocation', 'disable_trigger', 'crontab',
|
||||
'id', 'verbose_name', 'content_type', 'period', 'ondemand',
|
||||
'default_allocation', 'unit', 'disable_trigger', 'crontab',
|
||||
)
|
||||
list_filter = (UsedContentTypeFilter, 'period', 'ondemand', 'disable_trigger')
|
||||
fieldsets = (
|
||||
|
|
|
@ -179,7 +179,7 @@ FLUENT_DASHBOARD_APP_ICONS = {
|
|||
'miscellaneous/miscellaneous': 'applications-other.png',
|
||||
# Accounts
|
||||
'accounts/account': 'Face-monkey.png',
|
||||
'contacts/contact': 'contact.png',
|
||||
'contacts/contact': 'contact_book.png',
|
||||
'orders/order': 'basket.png',
|
||||
'orders/service': 'price.png',
|
||||
'prices/pack': 'Pack.png',
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.6 KiB |
|
@ -15,8 +15,8 @@
|
|||
width="48"
|
||||
version="1.0"
|
||||
inkscape:version="0.48.3.1 r9886"
|
||||
sodipodi:docname="TuxBox.svg"
|
||||
inkscape:export-filename="/home/glic3rinu/orchestra/django-orchestra/orchestra/static/orchestra/icons/Pack.png"
|
||||
sodipodi:docname="Pack.svg"
|
||||
inkscape:export-filename="/home/glic3/orchestra/django-orchestra/orchestra/static/orchestra/icons/Pack.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<svg:metadata
|
||||
|
@ -45,7 +45,7 @@
|
|||
id="namedview50"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.9166667"
|
||||
inkscape:cx="75.979939"
|
||||
inkscape:cx="75.97994"
|
||||
inkscape:cy="-22.905943"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
|
@ -3780,19 +3780,6 @@
|
|||
y1="7.9757"
|
||||
x2="15.464"
|
||||
y2="45.042" />
|
||||
<svg:filter
|
||||
color-interpolation-filters="sRGB"
|
||||
id="filter3974-4"
|
||||
height="2.5622675"
|
||||
y="-0.78113377"
|
||||
width="1.1420243"
|
||||
x="-0.071012162"
|
||||
inkscape:collect="always">
|
||||
<svg:feGaussianBlur
|
||||
id="feGaussianBlur3976-3"
|
||||
stdDeviation="0.97641723"
|
||||
inkscape:collect="always" />
|
||||
</svg:filter>
|
||||
<svg:linearGradient
|
||||
gradientTransform="translate(0.01860128,-4.0163098)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
|
@ -4294,7 +4281,7 @@
|
|||
id="path3273" />
|
||||
<svg:g
|
||||
id="g3888"
|
||||
transform="matrix(0.7674386,0,0,0.7674386,-7.6813433,9.7817236)">
|
||||
transform="matrix(0.67441404,0,0,0.67441404,-1.7640493,12.799498)">
|
||||
<svg:g
|
||||
transform="matrix(0.91489252,0,0,0.91489252,30.720532,5.6526667)"
|
||||
id="layer2">
|
||||
|
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 127 KiB |
BIN
orchestra/static/orchestra/icons/contact_alt.png
Normal file
BIN
orchestra/static/orchestra/icons/contact_alt.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
orchestra/static/orchestra/icons/contact_book.png
Normal file
BIN
orchestra/static/orchestra/icons/contact_book.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.6 KiB |
Loading…
Reference in a new issue