Як у формі Джанго я можу зробити поле читання лише (або відключене), щоб воно не могло редагуватись?


430

Як зробити форму "Джанго", як зробити поле лише для читання (або вимкнено)?

Коли форма використовується для створення нового запису, усі поля повинні бути включені - але коли запис знаходиться в режимі оновлення, деякі поля потрібно лише для читання.

Наприклад, при створенні нової Itemмоделі всі поля повинні бути редаговані, але, оновлюючи запис, чи існує спосіб відключити skuполе таким чином, щоб воно було видимим, але його не можна редагувати?

class Item(models.Model):
    sku = models.CharField(max_length=50)
    description = models.CharField(max_length=200)
    added_by = models.ForeignKey(User)


class ItemForm(ModelForm):
    class Meta:
        model = Item
        exclude = ('added_by')

def new_item_view(request):
    if request.method == 'POST':
        form = ItemForm(request.POST)
        # Validate and save
    else:
            form = ItemForm()
    # Render the view

Чи ItemFormможна повторно використовувати клас ? Які зміни вимагатимуть у класі ItemFormчи Itemмоделі? Чи потрібно мені написати інший клас " ItemUpdateForm" для оновлення елемента?

def update_item_view(request):
    if request.method == 'POST':
        form = ItemUpdateForm(request.POST)
        # Validate and save
    else:
        form = ItemUpdateForm()

Дивіться також ТАКЕ запитання: Чому поля форми лише для читання у Django - це погана ідея? @ stackoverflow.com/questions/2902024 , Прийнята відповідь (автор Daniel Naab) піклується про шкідливі хаки POST.
X10

Відповіді:


421

Як зазначено у цій відповіді , Django 1.9 додав атрибут Field.disabled :

Аргумент відключеного булевого аргументу, коли встановлено значення True, вимикає поле форми за допомогою відключеного атрибута HTML, щоб користувачі не могли його редагувати. Навіть якщо користувач підробляє значення поля, подане на сервер, воно буде ігноруватися на користь значення з початкових даних форми.

З Django 1.8 і новіших версій, щоб відключити вхід у віджет і запобігти зловмисним хакам POST, вам слід скрупувати дані на додаток до встановлення readonlyатрибута в полі форми:

class ItemForm(ModelForm):
    def __init__(self, *args, **kwargs):
        super(ItemForm, self).__init__(*args, **kwargs)
        instance = getattr(self, 'instance', None)
        if instance and instance.pk:
            self.fields['sku'].widget.attrs['readonly'] = True

    def clean_sku(self):
        instance = getattr(self, 'instance', None)
        if instance and instance.pk:
            return instance.sku
        else:
            return self.cleaned_data['sku']

Або замініть if instance and instance.pkіншою умовою, яка вказує на те, що ви редагуєте. Ви також можете встановити атрибут disabledу полі введення замість readonly.

clean_skuФункція гарантує , що readonlyзначення НЕ буде перевизначений POST.

В іншому випадку не існує вбудованого поля форми Django, яке б відображало значення під час відхилення пов'язаних вхідних даних. Якщо цього ви хочете, вам слід створити окреме, ModelFormщо виключає не редагувані поля (поля), а просто надрукувати їх всередині вашого шаблону.


3
Даніель, Дякуємо за публікацію відповіді. Мені незрозуміло, як користуватися цим кодом? Чи не працює цей код так само для нового, а також оновлення? Чи можете ви відредагувати свою відповідь, щоб навести приклади, як її використовувати для нових та оновлення форм? Дякую.
X10

8
Ключовим прикладом Даніеля є тестування поля .id. Новостворені об’єкти матимуть id == None. До речі, один із найдавніших відкритих квитків на Джанго - це саме питання. Дивіться code.djangoproject.com/ticket/342 .
Пітер Роуелл

2
@moadeep додайте clean_descriptionметод до класу форм.
Даніель Нааб

3
на linux (ubuntu 15) / chrome v45, читати лише зміни вказівника на "відключена рука", але поле потім можна натискати. з інвалідом працює як очікується
simone cittadini

7
Цю відповідь потрібно оновити. У disabledDjango 1.9 доданий новий аргумент поля . Якщо Field.disabledвстановлено значення True, значення POST для цього Fieldігнорується. Отже, якщо ви використовуєте 1.9, не потрібно переоцінювати clean, просто встановлюйте disabled = True. Перевірте цю відповідь.
нарендра-чодхарі

174

Django 1.9 додав атрибут Field.disabled: https://docs.djangoproject.com/en/stable/ref/forms/fields/#disabled

Аргумент відключеного булевого аргументу, коли встановлено значення True, вимикає поле форми за допомогою відключеного атрибута HTML, щоб користувачі не могли його редагувати. Навіть якщо користувач підробляє значення поля, подане на сервер, воно буде ігноруватися на користь значення з початкових даних форми.


Нічого за 1,8 LTS?
dnit13

9
будь-яка ідея, як ми можемо використовувати це в UpdateView? Оскільки він генерує поля з моделі ...
bcsanches

6
Правильна відповідь. Мій клас рішення MyChangeForm (form.ModelForm): def __init __ (self, * args, ** kwargs): super (MyChangeForm, self) .__ init __ (* args, ** kwargs) self.fields ['my_field']. Щоправда
Віджай Катам

8
Це проблемна відповідь - налаштування disabled=Trueпризведе до того, що модель буде переплетена назад до користувача з помилками перевірки.
Бен

1
Буде дивовижно, якщо ви можете включити приклад
геодезичний

95

Налаштування readonlyвіджета робить вхід у браузері лише для читання. Додавання clean_skuповернення instance.skuгарантує, що значення поля не зміниться на рівні форми.

def clean_sku(self):
    if self.instance: 
        return self.instance.sku
    else: 
        return self.fields['sku']

Таким чином ви можете використовувати модель (немодифікований збереження) і уникати отримання необхідної помилки в полі.


15
+1 Це прекрасний спосіб уникнути складніших перестановок save (). Однак ви хочете зробити перевірку екземпляра перед поверненням (в режимі коментарів, що не містять рядків): "if self.instance: return self.instan.sku; else: return self.fields ['sku']"
Daniel Нааб

2
Останній рядок був return self.cleaned_data['sku']би таким же хорошим чи кращим? У документах , здається, пропонують використовувати cleaned_data: «Значення, що повертається цього методу замінює існуюче значення в cleaned_data, тому воно повинно бути значенням поля з cleaned_data(навіть якщо цей метод не змінить його) або нове значення очищені.»
pianoJames

67

відповідь набудника мені дуже допомогла!

Я змінив його приклад для роботи з Django 1.3, використовуючи get_readonly_fields .

Зазвичай слід заявити щось подібне в app/admin.py:

class ItemAdmin(admin.ModelAdmin):
    ...
    readonly_fields = ('url',)

Я адаптувався таким чином:

# In the admin.py file
class ItemAdmin(admin.ModelAdmin):
    ...
    def get_readonly_fields(self, request, obj=None):
        if obj:
            return ['url']
        else:
            return []

І це чудово працює. Тепер, якщо ви додаєте елемент, urlполе читається і записується, але при зміні воно стає лише для читання.


55

Щоб зробити цю роботу для ForeignKeyполя, потрібно внести кілька змін. По-перше, у SELECT HTMLтегу немає атрибута readonly. Нам потрібно використовувати disabled="disabled"замість цього. Однак тоді браузер не надсилає назад жодних даних форми для цього поля. Тому нам потрібно встановити це поле, щоб воно не було обов'язковим, щоб поле перевіряло правильно. Потім нам потрібно повернути значення назад до того, яким воно було раніше, щоб воно не було порожнім.

Тож для іноземних ключів вам потрібно буде зробити щось на кшталт:

class ItemForm(ModelForm):

    def __init__(self, *args, **kwargs):
        super(ItemForm, self).__init__(*args, **kwargs)
        instance = getattr(self, 'instance', None)
        if instance and instance.id:
            self.fields['sku'].required = False
            self.fields['sku'].widget.attrs['disabled'] = 'disabled'

    def clean_sku(self):
        # As shown in the above answer.
        instance = getattr(self, 'instance', None)
        if instance:
            return instance.sku
        else:
            return self.cleaned_data.get('sku', None)

Таким чином браузер не дозволить користувачеві змінити поле, і завжди буде POSTтаким, яким він був порожнім. Потім ми перекриваємо cleanметод, щоб встановити значення поля таким, яке було спочатку в екземплярі.


Я намагався використовувати його як форму в TabularInline, але не вдалося, оскільки attrsїх ділили між собою widgetекземпляри та всі, крім першого рядка, включаючи нещодавно доданий, виведений лише для читання.
забій

Прекрасне (оновлення) рішення! На жаль, це та інше мають проблеми, коли виникають помилки форми, оскільки всі "відключені" значення спорожняються.
Майкл Томпсон

28

У програмі Django 1.2+ ви можете змінити поле так:

sku = forms.CharField(widget = forms.TextInput(attrs={'readonly':'readonly'}))

6
Це не дозволяє редагувати поле також у доданий час, про що йдеться у вихідному питанні.
Метт С.

Це відповідь, яку я шукаю. Field disabledне робить те, що я хочу, тому що це відключає поле, але також видаляє мітку / робить її невидимою.
sivabudh

18

Я створив клас MixIn, який ви можете успадкувати, щоб мати змогу додати поле read_only iterable, яке відключить та захистить поля при першому редагуванні:

(На основі відповідей Даніеля та Мухука)

from django import forms
from django.db.models.manager import Manager

# I used this instead of lambda expression after scope problems
def _get_cleaner(form, field):
    def clean_field():
         value = getattr(form.instance, field, None)
         if issubclass(type(value), Manager):
             value = value.all()
         return value
    return clean_field

class ROFormMixin(forms.BaseForm):
    def __init__(self, *args, **kwargs):
        super(ROFormMixin, self).__init__(*args, **kwargs)
        if hasattr(self, "read_only"):
            if self.instance and self.instance.pk:
                for field in self.read_only:
                    self.fields[field].widget.attrs['readonly'] = "readonly"
                    setattr(self, "clean_" + field, _get_cleaner(self, field))

# Basic usage
class TestForm(AModelForm, ROFormMixin):
    read_only = ('sku', 'an_other_field')

11

Я тільки що створив найпростіший можливий віджет для поля для читання - я не розумію, чому форми вже не мають цього:

class ReadOnlyWidget(widgets.Widget):
    """Some of these values are read only - just a bit of text..."""
    def render(self, _, value, attrs=None):
        return value

У формі:

my_read_only = CharField(widget=ReadOnlyWidget())

Дуже просто - і отримує мені просто вихід. Зручний у наборі форматів з купою значень лише для читання. Звичайно - ви також можете бути трохи розумнішими і дати йому діву з attrs, щоб ви могли додавати до неї класи.


2
Виглядає сексуально, але як поводитися із зовнішнім ключем?
andilabs

Зробіть це unicode(value)взамін замість того, можливо. Якщо припустити, що датчик Unicode є розумним, то ви це отримаєте.
Danny Staple

Для сторонніх ключів вам потрібно буде додати атрибут "model" і використовувати "get (value)". Перевір мою суть
шаді

10

Я зіткнувся з подібною проблемою. Схоже, мені вдалося це вирішити, визначивши метод "get_readonly_fields" у моєму класі ModelAdmin.

Щось на зразок цього:

# In the admin.py file

class ItemAdmin(admin.ModelAdmin):

    def get_readonly_display(self, request, obj=None):
        if obj:
            return ['sku']
        else:
            return []

Приємно, що це obj при додаванні нового Елементу буде "Ні", або це буде об'єкт, що редагується, коли ви змінюєте існуючий Елемент.

get_readonly_display задокументовано тут: http://docs.djangoproject.com/en/1.2/ref/contrib/admin/#modeladmin-methods


6

Один простий варіант - просто ввести form.instance.fieldNameшаблон замість form.fieldName.


А як щодо поля verbos_nameчи labelполя? Як я можу показати `label у шаблоні django? @alzclarke
Кіт 52 Гц

6

Як це зробити з Django 1,11:

class ItemForm(ModelForm):
    disabled_fields = ('added_by',)

    class Meta:
        model = Item
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super(ItemForm, self).__init__(*args, **kwargs)
        for field in self.disabled_fields:
            self.fields[field].disabled = True

це блокується лише з фасаду. будь-який може обійти. це створить проблему безпеки, якщо ви працюєте з конфіденційними даними
Сарат Ак

Це безпечно; він також блокує в бекенді,
timdiels

5

Як корисне доповнення до посади Хамфрі , у мене виникли деякі проблеми з реверсією джанго, оскільки вона все ще реєструвала відключені поля як "змінені". Наступний код вирішує проблему.

class ItemForm(ModelForm):

    def __init__(self, *args, **kwargs):
        super(ItemForm, self).__init__(*args, **kwargs)
        instance = getattr(self, 'instance', None)
        if instance and instance.id:
            self.fields['sku'].required = False
            self.fields['sku'].widget.attrs['disabled'] = 'disabled'

    def clean_sku(self):
        # As shown in the above answer.
        instance = getattr(self, 'instance', None)
        if instance:
            try:
                self.changed_data.remove('sku')
            except ValueError, e:
                pass
            return instance.sku
        else:
            return self.cleaned_data.get('sku', None)

5

Оскільки я поки не можу коментувати ( рішення мухука ), відповім як окрему відповідь. Це повний приклад коду, який працював для мене:

def clean_sku(self):
  if self.instance and self.instance.pk:
    return self.instance.sku
  else:
    return self.cleaned_data['sku']

5

Знову ж таки, я збираюся запропонувати ще одне рішення :) Я використовував код Хамфрі , тому це базується на цьому.

Однак я зіткнувся з проблемами, поля яких були "a" ModelChoiceField. Все працювало б за першим запитом. Однак якщо набір форматів намагався додати новий елемент і не вдалося перевірити, щось було не так із "існуючими" формами, де SELECTEDпараметр був скинутий до типового--------- .

У всякому разі, я не міг зрозуміти, як це виправити. Тому замість цього (і я думаю, що це насправді чистіше у формі), я зробив поляHiddenInputField() . Це просто означає, що вам доведеться зробити трохи більше роботи над шаблоном.

Тож виправленням для мене було спрощення форми:

class ItemForm(ModelForm):

    def __init__(self, *args, **kwargs):
        super(ItemForm, self).__init__(*args, **kwargs)
        instance = getattr(self, 'instance', None)
        if instance and instance.id:
            self.fields['sku'].widget=HiddenInput()

І тоді в шаблоні вам потрібно буде зробити певну циклічність набору форматів вручну .

Отже, у цьому випадку ви зробите щось подібне в шаблоні:

<div>
    {{ form.instance.sku }} <!-- This prints the value -->
    {{ form }} <!-- Prints form normally, and makes the hidden input -->
</div>

Це працювало трохи краще і з меншою формою маніпуляцій.


4

Я зіткнувся з тією ж проблемою, тому створив Mixin, який, здається, працює для моїх випадків використання.

class ReadOnlyFieldsMixin(object):
    readonly_fields =()

    def __init__(self, *args, **kwargs):
        super(ReadOnlyFieldsMixin, self).__init__(*args, **kwargs)
        for field in (field for name, field in self.fields.iteritems() if name in self.readonly_fields):
            field.widget.attrs['disabled'] = 'true'
            field.required = False

    def clean(self):
        cleaned_data = super(ReadOnlyFieldsMixin,self).clean()
        for field in self.readonly_fields:
           cleaned_data[field] = getattr(self.instance, field)

        return cleaned_data

Використання, просто визначте, які з них потрібно читати лише:

class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
    readonly_fields = ('field1', 'field2', 'fieldx')

Я вважаю, що це трохи читабельніше, ніж мій власний міксин, який я запропонував тут. Навіть, мабуть, ефективніше, оскільки ці чистки не викликають помилок перевірки…
christophe31,

Я отримую помилку:'collections.OrderedDict' object has no attribute 'iteritems'
геодезичний

4

якщо вам потрібні кілька полів, доступних лише для читання. Ви можете скористатися будь-яким із наведених нижче методів

метод 1

class ItemForm(ModelForm):
    readonly = ('sku',)

    def __init__(self, *arg, **kwrg):
        super(ItemForm, self).__init__(*arg, **kwrg)
        for x in self.readonly:
            self.fields[x].widget.attrs['disabled'] = 'disabled'

    def clean(self):
        data = super(ItemForm, self).clean()
        for x in self.readonly:
            data[x] = getattr(self.instance, x)
        return data

метод 2

метод успадкування

class AdvancedModelForm(ModelForm):


    def __init__(self, *arg, **kwrg):
        super(AdvancedModelForm, self).__init__(*arg, **kwrg)
        if hasattr(self, 'readonly'):
            for x in self.readonly:
                self.fields[x].widget.attrs['disabled'] = 'disabled'

    def clean(self):
        data = super(AdvancedModelForm, self).clean()
        if hasattr(self, 'readonly'):
            for x in self.readonly:
                data[x] = getattr(self.instance, x)
        return data


class ItemForm(AdvancedModelForm):
    readonly = ('sku',)

3

Ще два (схожі) підходи з одним узагальненим прикладом:

1) перший підхід - видалення поля методом save (), наприклад (не перевірено;)):

def save(self, *args, **kwargs):
    for fname in self.readonly_fields:
        if fname in self.cleaned_data:
            del self.cleaned_data[fname]
    return super(<form-name>, self).save(*args,**kwargs)

2) другий підхід - поле скидання до початкового значення в чистому методі:

def clean_<fieldname>(self):
    return self.initial[<fieldname>] # or getattr(self.instance, fieldname)

На основі другого підходу я узагальнив це так:

from functools                 import partial

class <Form-name>(...):

    def __init__(self, ...):
        ...
        super(<Form-name>, self).__init__(*args, **kwargs)
        ...
        for i, (fname, field) in enumerate(self.fields.iteritems()):
            if fname in self.readonly_fields:
                field.widget.attrs['readonly'] = "readonly"
                field.required = False
                # set clean method to reset value back
                clean_method_name = "clean_%s" % fname
                assert clean_method_name not in dir(self)
                setattr(self, clean_method_name, partial(self._clean_for_readonly_field, fname=fname))

    def _clean_for_readonly_field(self, fname):
        """ will reset value to initial - nothing will be changed 
            needs to be added dynamically - partial, see init_fields
        """
        return self.initial[fname] # or getattr(self.instance, fieldname)

3

Щодо версії адміністратора, я думаю, що це більш компактний спосіб, якщо у вас є більше одного поля:

def get_readonly_fields(self, request, obj=None):
    skips = ('sku', 'other_field')
    fields = super(ItemAdmin, self).get_readonly_fields(request, obj)

    if not obj:
        return [field for field in fields if not field in skips]
    return fields

3

На основі відповіді Yamikep я знайшов краще та дуже просте рішення, яке також обробляє ModelMultipleChoiceFieldполя.

Видалення поля із form.cleaned_dataзапобігає збереженню полів:

class ReadOnlyFieldsMixin(object):
    readonly_fields = ()

    def __init__(self, *args, **kwargs):
        super(ReadOnlyFieldsMixin, self).__init__(*args, **kwargs)
        for field in (field for name, field in self.fields.iteritems() if
                      name in self.readonly_fields):
            field.widget.attrs['disabled'] = 'true'
            field.required = False

    def clean(self):
        for f in self.readonly_fields:
            self.cleaned_data.pop(f, None)
        return super(ReadOnlyFieldsMixin, self).clean()

Використання:

class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
    readonly_fields = ('field1', 'field2', 'fieldx')

2

Ось трохи причетніша версія, заснована на відповіді christophe31 . Він не покладається на атрибут "readonly". Це робить його проблеми, як-от вибираючі поля, які все ще змінюються, і відбірники даних, що все ще спливають, відходять.

Натомість він загортає віджет полів форми у віджет, який читається, таким чином робить форму ще валідною. Вміст оригінального віджета відображається всередині <span class="hidden"></span>тегів. Якщо у віджеті є render_readonly()метод, він використовує його як видимий текст, інакше він розбирає HTML оригінального віджета і намагається відгадати найкраще представлення.

import django.forms.widgets as f
import xml.etree.ElementTree as etree
from django.utils.safestring import mark_safe

def make_readonly(form):
    """
    Makes all fields on the form readonly and prevents it from POST hacks.
    """

    def _get_cleaner(_form, field):
        def clean_field():
            return getattr(_form.instance, field, None)
        return clean_field

    for field_name in form.fields.keys():
        form.fields[field_name].widget = ReadOnlyWidget(
            initial_widget=form.fields[field_name].widget)
        setattr(form, "clean_" + field_name, 
                _get_cleaner(form, field_name))

    form.is_readonly = True

class ReadOnlyWidget(f.Select):
    """
    Renders the content of the initial widget in a hidden <span>. If the
    initial widget has a ``render_readonly()`` method it uses that as display
    text, otherwise it tries to guess by parsing the html of the initial widget.
    """

    def __init__(self, initial_widget, *args, **kwargs):
        self.initial_widget = initial_widget
        super(ReadOnlyWidget, self).__init__(*args, **kwargs)

    def render(self, *args, **kwargs):
        def guess_readonly_text(original_content):
            root = etree.fromstring("<span>%s</span>" % original_content)

            for element in root:
                if element.tag == 'input':
                    return element.get('value')

                if element.tag == 'select':
                    for option in element:
                        if option.get('selected'):
                            return option.text

                if element.tag == 'textarea':
                    return element.text

            return "N/A"

        original_content = self.initial_widget.render(*args, **kwargs)
        try:
            readonly_text = self.initial_widget.render_readonly(*args, **kwargs)
        except AttributeError:
            readonly_text = guess_readonly_text(original_content)

        return mark_safe("""<span class="hidden">%s</span>%s""" % (
            original_content, readonly_text))

# Usage example 1.
self.fields['my_field'].widget = ReadOnlyWidget(self.fields['my_field'].widget)

# Usage example 2.
form = MyForm()
make_readonly(form)

1

Це найпростіший спосіб?

Прямо у коді перегляду щось подібне:

def resume_edit(request, r_id):
    .....    
    r = Resume.get.object(pk=r_id)
    resume = ResumeModelForm(instance=r)
    .....
    resume.fields['email'].widget.attrs['readonly'] = True 
    .....
    return render(request, 'resumes/resume.html', context)

Це чудово працює!


1

Для django 1.9+
ви можете використовувати аргумент Fields disabled, щоб зробити поле відключеним. Наприклад, у наступному фрагменті коду з файлу form.py я вимкнув поле_код співробітника

class EmployeeForm(forms.ModelForm):
    employee_code = forms.CharField(disabled=True)
    class Meta:
        model = Employee
        fields = ('employee_code', 'designation', 'salary')

Довідка https://docs.djangoproject.com/en/2.0/ref/forms/fields/#disabled


1

Якщо ви працюєте з Django ver < 1.9( 1.9доданий Field.disabledатрибут), ви можете спробувати додати наступного декоратора до вашого __init__методу форми :

def bound_data_readonly(_, initial):
    return initial


def to_python_readonly(field):
    native_to_python = field.to_python

    def to_python_filed(_):
        return native_to_python(field.initial)

    return to_python_filed


def disable_read_only_fields(init_method):

    def init_wrapper(*args, **kwargs):
        self = args[0]
        init_method(*args, **kwargs)
        for field in self.fields.values():
            if field.widget.attrs.get('readonly', None):
                field.widget.attrs['disabled'] = True
                setattr(field, 'bound_data', bound_data_readonly)
                setattr(field, 'to_python', to_python_readonly(field))

    return init_wrapper


class YourForm(forms.ModelForm):

    @disable_read_only_fields
    def __init__(self, *args, **kwargs):
        ...

Основна ідея полягає в тому, що якщо поле є, readonlyви не потребуєте жодного іншого значення, крім initial.

PS: Не забудьте встановити yuor_form_field.widget.attrs['readonly'] = True


0

Якщо ви використовуєте адміністратора Django, тут найпростіше рішення.

class ReadonlyFieldsMixin(object):
    def get_readonly_fields(self, request, obj=None):
        if obj:
            return super(ReadonlyFieldsMixin, self).get_readonly_fields(request, obj)
        else:
            return tuple()

class MyAdmin(ReadonlyFieldsMixin, ModelAdmin):
    readonly_fields = ('sku',)

0

Я думаю , що найкращий варіант був би просто включити атрибут тільки для читання в шаблоні наданого в <span>або , <p>а не включати його в формі , якщо це незмінне.

Форми призначені для збору даних, а не для їх відображення. Незважаючи на це, варіанти відображення у readonlyвіджеті та очищення даних POST - це чудові рішення.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.