Skip to content

Commit

Permalink
Refactored meta.py -- created a django.core.meta package, with init.p…
Browse files Browse the repository at this point in the history
…y and fields.py

git-svn-id: http://code.djangoproject.com/svn/django/trunk@378 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
adrianholovaty committed Aug 1, 2005
1 parent 006e9e7 commit e0c3dd3
Show file tree
Hide file tree
Showing 8 changed files with 712 additions and 710 deletions.
4 changes: 2 additions & 2 deletions django/core/management.py
Expand Up @@ -264,7 +264,7 @@ def database_check(mod):


def get_admin_index(mod): def get_admin_index(mod):
"Returns admin-index template snippet (in list form) for the given module." "Returns admin-index template snippet (in list form) for the given module."
from django.core import meta from django.utils.text import capfirst
output = [] output = []
app_label = mod._MODELS[0]._meta.app_label app_label = mod._MODELS[0]._meta.app_label
output.append('{%% if perms.%s %%}' % app_label) output.append('{%% if perms.%s %%}' % app_label)
Expand All @@ -274,7 +274,7 @@ def get_admin_index(mod):
output.append(MODULE_TEMPLATE % { output.append(MODULE_TEMPLATE % {
'app': app_label, 'app': app_label,
'mod': klass._meta.module_name, 'mod': klass._meta.module_name,
'name': meta.capfirst(klass._meta.verbose_name_plural), 'name': capfirst(klass._meta.verbose_name_plural),
'addperm': klass._meta.get_add_permission(), 'addperm': klass._meta.get_add_permission(),
'changeperm': klass._meta.get_change_permission(), 'changeperm': klass._meta.get_change_permission(),
}) })
Expand Down
684 changes: 5 additions & 679 deletions django/core/meta.py → django/core/meta/__init__.py

Large diffs are not rendered by default.

667 changes: 667 additions & 0 deletions django/core/meta/fields.py

Large diffs are not rendered by default.

23 changes: 12 additions & 11 deletions django/models/__init__.py
@@ -1,4 +1,5 @@
from django.core import meta from django.core import meta
from django.utils.functional import curry


__all__ = ['auth', 'core'] __all__ = ['auth', 'core']


Expand Down Expand Up @@ -28,30 +29,30 @@
if isinstance(rel_field.rel, meta.OneToOne): if isinstance(rel_field.rel, meta.OneToOne):
# Add "get_thingie" methods for one-to-one related objects. # Add "get_thingie" methods for one-to-one related objects.
# EXAMPLE: Place.get_restaurants_restaurant() # EXAMPLE: Place.get_restaurants_restaurant()
func = meta.curry(meta.method_get_related, 'get_object', rel_mod, rel_field) func = curry(meta.method_get_related, 'get_object', rel_mod, rel_field)
func.__doc__ = "Returns the associated `%s.%s` object." % (rel_obj.app_label, rel_obj.module_name) func.__doc__ = "Returns the associated `%s.%s` object." % (rel_obj.app_label, rel_obj.module_name)
setattr(klass, 'get_%s' % rel_obj_name, func) setattr(klass, 'get_%s' % rel_obj_name, func)
elif isinstance(rel_field.rel, meta.ManyToOne): elif isinstance(rel_field.rel, meta.ManyToOne):
# Add "get_thingie" methods for many-to-one related objects. # Add "get_thingie" methods for many-to-one related objects.
# EXAMPLE: Poll.get_choice() # EXAMPLE: Poll.get_choice()
func = meta.curry(meta.method_get_related, 'get_object', rel_mod, rel_field) func = curry(meta.method_get_related, 'get_object', rel_mod, rel_field)
func.__doc__ = "Returns the associated `%s.%s` object matching the given criteria." % (rel_obj.app_label, rel_obj.module_name) func.__doc__ = "Returns the associated `%s.%s` object matching the given criteria." % (rel_obj.app_label, rel_obj.module_name)
setattr(klass, 'get_%s' % rel_obj_name, func) setattr(klass, 'get_%s' % rel_obj_name, func)
# Add "get_thingie_count" methods for many-to-one related objects. # Add "get_thingie_count" methods for many-to-one related objects.
# EXAMPLE: Poll.get_choice_count() # EXAMPLE: Poll.get_choice_count()
func = meta.curry(meta.method_get_related, 'get_count', rel_mod, rel_field) func = curry(meta.method_get_related, 'get_count', rel_mod, rel_field)
func.__doc__ = "Returns the number of associated `%s.%s` objects." % (rel_obj.app_label, rel_obj.module_name) func.__doc__ = "Returns the number of associated `%s.%s` objects." % (rel_obj.app_label, rel_obj.module_name)
setattr(klass, 'get_%s_count' % rel_obj_name, func) setattr(klass, 'get_%s_count' % rel_obj_name, func)
# Add "get_thingie_list" methods for many-to-one related objects. # Add "get_thingie_list" methods for many-to-one related objects.
# EXAMPLE: Poll.get_choice_list() # EXAMPLE: Poll.get_choice_list()
func = meta.curry(meta.method_get_related, 'get_list', rel_mod, rel_field) func = curry(meta.method_get_related, 'get_list', rel_mod, rel_field)
func.__doc__ = "Returns a list of associated `%s.%s` objects." % (rel_obj.app_label, rel_obj.module_name) func.__doc__ = "Returns a list of associated `%s.%s` objects." % (rel_obj.app_label, rel_obj.module_name)
setattr(klass, 'get_%s_list' % rel_obj_name, func) setattr(klass, 'get_%s_list' % rel_obj_name, func)
# Add "add_thingie" methods for many-to-one related objects, # Add "add_thingie" methods for many-to-one related objects,
# but only for related objects that are in the same app. # but only for related objects that are in the same app.
# EXAMPLE: Poll.add_choice() # EXAMPLE: Poll.add_choice()
if rel_obj.app_label == klass._meta.app_label: if rel_obj.app_label == klass._meta.app_label:
func = meta.curry(meta.method_add_related, rel_obj, rel_mod, rel_field) func = curry(meta.method_add_related, rel_obj, rel_mod, rel_field)
func.alters_data = True func.alters_data = True
setattr(klass, 'add_%s' % rel_obj_name, func) setattr(klass, 'add_%s' % rel_obj_name, func)
del func del func
Expand All @@ -61,11 +62,11 @@
for rel_opts, rel_field in klass._meta.get_all_related_many_to_many_objects(): for rel_opts, rel_field in klass._meta.get_all_related_many_to_many_objects():
rel_mod = rel_opts.get_model_module() rel_mod = rel_opts.get_model_module()
rel_obj_name = klass._meta.get_rel_object_method_name(rel_opts, rel_field) rel_obj_name = klass._meta.get_rel_object_method_name(rel_opts, rel_field)
setattr(klass, 'get_%s' % rel_obj_name, meta.curry(meta.method_get_related_many_to_many, 'get_object', rel_mod, rel_field)) setattr(klass, 'get_%s' % rel_obj_name, curry(meta.method_get_related_many_to_many, 'get_object', rel_mod, rel_field))
setattr(klass, 'get_%s_count' % rel_obj_name, meta.curry(meta.method_get_related_many_to_many, 'get_count', rel_mod, rel_field)) setattr(klass, 'get_%s_count' % rel_obj_name, curry(meta.method_get_related_many_to_many, 'get_count', rel_mod, rel_field))
setattr(klass, 'get_%s_list' % rel_obj_name, meta.curry(meta.method_get_related_many_to_many, 'get_list', rel_mod, rel_field)) setattr(klass, 'get_%s_list' % rel_obj_name, curry(meta.method_get_related_many_to_many, 'get_list', rel_mod, rel_field))
if rel_opts.app_label == klass._meta.app_label: if rel_opts.app_label == klass._meta.app_label:
func = meta.curry(meta.method_set_related_many_to_many, rel_opts, rel_field) func = curry(meta.method_set_related_many_to_many, rel_opts, rel_field)
func.alters_data = True func.alters_data = True
setattr(klass, 'set_%s' % rel_opts.module_name, func) setattr(klass, 'set_%s' % rel_opts.module_name, func)
del func del func
Expand All @@ -74,12 +75,12 @@
# Add "set_thingie_order" and "get_thingie_order" methods for objects # Add "set_thingie_order" and "get_thingie_order" methods for objects
# that are ordered with respect to this. # that are ordered with respect to this.
for obj in klass._meta.get_ordered_objects(): for obj in klass._meta.get_ordered_objects():
func = meta.curry(meta.method_set_order, obj) func = curry(meta.method_set_order, obj)
func.__doc__ = "Sets the order of associated `%s.%s` objects to the given ID list." % (obj.app_label, obj.module_name) func.__doc__ = "Sets the order of associated `%s.%s` objects to the given ID list." % (obj.app_label, obj.module_name)
func.alters_data = True func.alters_data = True
setattr(klass, 'set_%s_order' % obj.object_name.lower(), func) setattr(klass, 'set_%s_order' % obj.object_name.lower(), func)


func = meta.curry(meta.method_get_order, obj) func = curry(meta.method_get_order, obj)
func.__doc__ = "Returns the order of associated `%s.%s` objects as a list of IDs." % (obj.app_label, obj.module_name) func.__doc__ = "Returns the order of associated `%s.%s` objects as a list of IDs." % (obj.app_label, obj.module_name)
setattr(klass, 'get_%s_order' % obj.object_name.lower(), func) setattr(klass, 'get_%s_order' % obj.object_name.lower(), func)
del func, obj # clean up del func, obj # clean up
Expand Down
3 changes: 2 additions & 1 deletion django/templatetags/adminapplist.py
Expand Up @@ -6,10 +6,11 @@ def __init__(self, varname):


def render(self, context): def render(self, context):
from django.core import meta from django.core import meta
from django.utils.text import capfirst
app_list = [] app_list = []
for app in meta.get_installed_model_modules(): for app in meta.get_installed_model_modules():
app_label = app.__name__[app.__name__.rindex('.')+1:] app_label = app.__name__[app.__name__.rindex('.')+1:]
model_list = [{'name': meta.capfirst(m._meta.verbose_name_plural), model_list = [{'name': capfirst(m._meta.verbose_name_plural),
'admin_url': '%s/%s/' % (app_label, m._meta.module_name)} \ 'admin_url': '%s/%s/' % (app_label, m._meta.module_name)} \
for m in app._MODELS if m._meta.admin] for m in app._MODELS if m._meta.admin]
if model_list: if model_list:
Expand Down
4 changes: 4 additions & 0 deletions django/utils/functional.py
@@ -0,0 +1,4 @@
def curry(*args, **kwargs):
def _curried(*moreargs, **morekwargs):
return args[0](*(args[1:]+moreargs), **dict(kwargs.items() + morekwargs.items()))
return _curried
3 changes: 3 additions & 0 deletions django/utils/text.py
@@ -1,5 +1,8 @@
import re import re


# Capitalizes the first letter of a string.
capfirst = lambda x: x and x[0].upper() + x[1:]

def wrap(text, width): def wrap(text, width):
""" """
A word-wrap function that preserves existing line breaks and most spaces in A word-wrap function that preserves existing line breaks and most spaces in
Expand Down
34 changes: 17 additions & 17 deletions django/views/admin/main.py
Expand Up @@ -6,7 +6,7 @@
from django.models.auth import log from django.models.auth import log
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect
from django.utils.text import get_text_list from django.utils.text import capfirst, get_text_list
from django.conf.settings import ADMIN_MEDIA_PREFIX from django.conf.settings import ADMIN_MEDIA_PREFIX
import operator import operator


Expand Down Expand Up @@ -266,7 +266,7 @@ def change_list(request, app_label, module_name):
raw_template = ['{% extends "base_site" %}\n'] raw_template = ['{% extends "base_site" %}\n']
raw_template.append('{% block bodyclass %}change-list{% endblock %}\n') raw_template.append('{% block bodyclass %}change-list{% endblock %}\n')
if not is_popup: if not is_popup:
raw_template.append('{%% block breadcrumbs %%}<div class="breadcrumbs"><a href="../../">Home</a> &rsaquo; %s</div>{%% endblock %%}\n' % meta.capfirst(opts.verbose_name_plural)) raw_template.append('{%% block breadcrumbs %%}<div class="breadcrumbs"><a href="../../">Home</a> &rsaquo; %s</div>{%% endblock %%}\n' % capfirst(opts.verbose_name_plural))
raw_template.append('{% block coltype %}flex{% endblock %}') raw_template.append('{% block coltype %}flex{% endblock %}')
raw_template.append('{% block content %}\n') raw_template.append('{% block content %}\n')
raw_template.append('<div id="content-main">\n') raw_template.append('<div id="content-main">\n')
Expand Down Expand Up @@ -356,10 +356,10 @@ def change_list(request, app_label, module_name):
except AttributeError: except AttributeError:
header = func.__name__ header = func.__name__
# Non-field list_display values don't get ordering capability. # Non-field list_display values don't get ordering capability.
raw_template.append('<th>%s</th>' % meta.capfirst(header)) raw_template.append('<th>%s</th>' % capfirst(header))
else: else:
if isinstance(f.rel, meta.ManyToOne) and f.null: if isinstance(f.rel, meta.ManyToOne) and f.null:
raw_template.append('<th>%s</th>' % meta.capfirst(f.verbose_name)) raw_template.append('<th>%s</th>' % capfirst(f.verbose_name))
else: else:
th_classes = [] th_classes = []
new_order_type = 'asc' new_order_type = 'asc'
Expand All @@ -369,7 +369,7 @@ def change_list(request, app_label, module_name):
raw_template.append('<th%s><a href="%s">%s</a></th>' % \ raw_template.append('<th%s><a href="%s">%s</a></th>' % \
((th_classes and ' class="%s"' % ' '.join(th_classes) or ''), ((th_classes and ' class="%s"' % ' '.join(th_classes) or ''),
get_query_string(params, {ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}), get_query_string(params, {ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
meta.capfirst(f.verbose_name))) capfirst(f.verbose_name)))
raw_template.append('</tr>\n</thead>\n') raw_template.append('</tr>\n</thead>\n')
# Result rows. # Result rows.
pk = lookup_opts.pk.name pk = lookup_opts.pk.name
Expand Down Expand Up @@ -572,7 +572,7 @@ def _get_template(opts, app_label, add=False, change=False, show_delete=False, f
t.append('{%% block bodyclass %%}%s-%s change-form{%% endblock %%}\n' % (app_label, opts.object_name.lower())) t.append('{%% block bodyclass %%}%s-%s change-form{%% endblock %%}\n' % (app_label, opts.object_name.lower()))
breadcrumb_title = add and "Add %s" % opts.verbose_name or '{{ original|striptags|truncatewords:"18" }}' breadcrumb_title = add and "Add %s" % opts.verbose_name or '{{ original|striptags|truncatewords:"18" }}'
t.append('{%% block breadcrumbs %%}{%% if not is_popup %%}<div class="breadcrumbs"><a href="../../../">Home</a> &rsaquo; <a href="../">%s</a> &rsaquo; %s</div>{%% endif %%}{%% endblock %%}\n' % \ t.append('{%% block breadcrumbs %%}{%% if not is_popup %%}<div class="breadcrumbs"><a href="../../../">Home</a> &rsaquo; <a href="../">%s</a> &rsaquo; %s</div>{%% endif %%}{%% endblock %%}\n' % \
(meta.capfirst(opts.verbose_name_plural), breadcrumb_title)) (capfirst(opts.verbose_name_plural), breadcrumb_title))
t.append('{% block content %}<div id="content-main">\n') t.append('{% block content %}<div id="content-main">\n')
if change: if change:
t.append('{% if not is_popup %}') t.append('{% if not is_popup %}')
Expand Down Expand Up @@ -613,12 +613,12 @@ def _get_template(opts, app_label, add=False, change=False, show_delete=False, f
if change and hasattr(rel_obj, 'get_absolute_url'): if change and hasattr(rel_obj, 'get_absolute_url'):
view_on_site = '{%% if %s.original %%}<a href="/r/{{ %s.content_type_id }}/{{ %s.original.id }}/">View on site</a>{%% endif %%}' % (var_name, var_name, var_name) view_on_site = '{%% if %s.original %%}<a href="/r/{{ %s.content_type_id }}/{{ %s.original.id }}/">View on site</a>{%% endif %%}' % (var_name, var_name, var_name)
if rel_field.rel.edit_inline_type == meta.TABULAR: if rel_field.rel.edit_inline_type == meta.TABULAR:
t.append('<h2>%s</h2>\n<table>\n' % meta.capfirst(rel_obj.verbose_name_plural)) t.append('<h2>%s</h2>\n<table>\n' % capfirst(rel_obj.verbose_name_plural))
t.append('<thead><tr>') t.append('<thead><tr>')
for f in field_list: for f in field_list:
if isinstance(f, meta.AutoField): if isinstance(f, meta.AutoField):
continue continue
t.append('<th%s>%s</th>' % (f.blank and ' class="optional"' or '', meta.capfirst(f.verbose_name))) t.append('<th%s>%s</th>' % (f.blank and ' class="optional"' or '', capfirst(f.verbose_name)))
t.append('</tr></thead>\n') t.append('</tr></thead>\n')
t.append('{%% for %s in form.%s %%}\n' % (var_name, rel_obj.module_name)) t.append('{%% for %s in form.%s %%}\n' % (var_name, rel_obj.module_name))
if change: if change:
Expand Down Expand Up @@ -656,7 +656,7 @@ def _get_template(opts, app_label, add=False, change=False, show_delete=False, f
t.append('{% endfor %}\n') t.append('{% endfor %}\n')
else: # edit_inline_type == STACKED else: # edit_inline_type == STACKED
t.append('{%% for %s in form.%s %%}' % (var_name, rel_obj.module_name)) t.append('{%% for %s in form.%s %%}' % (var_name, rel_obj.module_name))
t.append('<h2>%s #{{ forloop.counter }}</h2>' % meta.capfirst(rel_obj.verbose_name)) t.append('<h2>%s #{{ forloop.counter }}</h2>' % capfirst(rel_obj.verbose_name))
if view_on_site: if view_on_site:
t.append('<p>%s</p>' % view_on_site) t.append('<p>%s</p>' % view_on_site)
for f in field_list: for f in field_list:
Expand Down Expand Up @@ -710,14 +710,14 @@ def _get_admin_field(field_list, name_prefix, rel, add, change):
# the *left* of the label. # the *left* of the label.
if isinstance(field, meta.BooleanField): if isinstance(field, meta.BooleanField):
t.append(_get_admin_field_form_widget(field, name_prefix, rel, add, change)) t.append(_get_admin_field_form_widget(field, name_prefix, rel, add, change))
t.append(' <label for="%s" class="vCheckboxLabel">%s</label>' % (label_name, meta.capfirst(field.verbose_name))) t.append(' <label for="%s" class="vCheckboxLabel">%s</label>' % (label_name, capfirst(field.verbose_name)))
else: else:
class_names = [] class_names = []
if not field.blank: if not field.blank:
class_names.append('required') class_names.append('required')
if i > 0: if i > 0:
class_names.append('inline') class_names.append('inline')
t.append('<label for="%s"%s>%s:</label> ' % (label_name, class_names and ' class="%s"' % ' '.join(class_names) or '', meta.capfirst(field.verbose_name))) t.append('<label for="%s"%s>%s:</label> ' % (label_name, class_names and ' class="%s"' % ' '.join(class_names) or '', capfirst(field.verbose_name)))
t.append(_get_admin_field_form_widget(field, name_prefix, rel, add, change)) t.append(_get_admin_field_form_widget(field, name_prefix, rel, add, change))
if change and use_raw_id_admin(field): if change and use_raw_id_admin(field):
obj_repr = '%soriginal.get_%s|truncatewords:"14"' % (rel and name_prefix or '', field.rel.name) obj_repr = '%soriginal.get_%s|truncatewords:"14"' % (rel and name_prefix or '', field.rel.name)
Expand Down Expand Up @@ -991,11 +991,11 @@ def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current
if rel_field.rel.edit_inline or not rel_opts.admin: if rel_field.rel.edit_inline or not rel_opts.admin:
# Don't display link to edit, because it either has no # Don't display link to edit, because it either has no
# admin or is edited inline. # admin or is edited inline.
nh(deleted_objects, current_depth, ['%s: %r' % (meta.capfirst(rel_opts.verbose_name), sub_obj), []]) nh(deleted_objects, current_depth, ['%s: %r' % (capfirst(rel_opts.verbose_name), sub_obj), []])
else: else:
# Display a link to the admin page. # Display a link to the admin page.
nh(deleted_objects, current_depth, ['%s: <a href="../../../../%s/%s/%s/">%r</a>' % \ nh(deleted_objects, current_depth, ['%s: <a href="../../../../%s/%s/%s/">%r</a>' % \
(meta.capfirst(rel_opts.verbose_name), rel_opts.app_label, rel_opts.module_name, (capfirst(rel_opts.verbose_name), rel_opts.app_label, rel_opts.module_name,
getattr(sub_obj, rel_opts.pk.name), sub_obj), []]) getattr(sub_obj, rel_opts.pk.name), sub_obj), []])
_get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, rel_opts, current_depth+2) _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, rel_opts, current_depth+2)
else: else:
Expand All @@ -1005,11 +1005,11 @@ def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current
if rel_field.rel.edit_inline or not rel_opts.admin: if rel_field.rel.edit_inline or not rel_opts.admin:
# Don't display link to edit, because it either has no # Don't display link to edit, because it either has no
# admin or is edited inline. # admin or is edited inline.
nh(deleted_objects, current_depth, ['%s: %s' % (meta.capfirst(rel_opts.verbose_name), strip_tags(repr(sub_obj))), []]) nh(deleted_objects, current_depth, ['%s: %s' % (capfirst(rel_opts.verbose_name), strip_tags(repr(sub_obj))), []])
else: else:
# Display a link to the admin page. # Display a link to the admin page.
nh(deleted_objects, current_depth, ['%s: <a href="../../../../%s/%s/%s/">%s</a>' % \ nh(deleted_objects, current_depth, ['%s: <a href="../../../../%s/%s/%s/">%s</a>' % \
(meta.capfirst(rel_opts.verbose_name), rel_opts.app_label, rel_opts.module_name, sub_obj.id, strip_tags(repr(sub_obj))), []]) (capfirst(rel_opts.verbose_name), rel_opts.app_label, rel_opts.module_name, sub_obj.id, strip_tags(repr(sub_obj))), []])
_get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, rel_opts, current_depth+2) _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, rel_opts, current_depth+2)
# If there were related objects, and the user doesn't have # If there were related objects, and the user doesn't have
# permission to delete them, add the missing perm to perms_needed. # permission to delete them, add the missing perm to perms_needed.
Expand Down Expand Up @@ -1053,7 +1053,7 @@ def delete_stage(request, app_label, module_name, object_id):


# Populate deleted_objects, a data structure of all related objects that # Populate deleted_objects, a data structure of all related objects that
# will also be deleted. # will also be deleted.
deleted_objects = ['%s: <a href="../../%s/">%s</a>' % (meta.capfirst(opts.verbose_name), object_id, strip_tags(repr(obj))), []] deleted_objects = ['%s: <a href="../../%s/">%s</a>' % (capfirst(opts.verbose_name), object_id, strip_tags(repr(obj))), []]
perms_needed = sets.Set() perms_needed = sets.Set()
_get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1) _get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1)


Expand Down Expand Up @@ -1088,7 +1088,7 @@ def history(request, app_label, module_name, object_id):
c = Context(request, { c = Context(request, {
'title': 'Change history: %r' % obj, 'title': 'Change history: %r' % obj,
'action_list': action_list, 'action_list': action_list,
'module_name': meta.capfirst(opts.verbose_name_plural), 'module_name': capfirst(opts.verbose_name_plural),
'object': obj, 'object': obj,
}) })
return HttpResponse(t.render(c), mimetype='text/html; charset=utf-8') return HttpResponse(t.render(c), mimetype='text/html; charset=utf-8')

0 comments on commit e0c3dd3

Please sign in to comment.