Django-rest-framework: Panggilan PUT tidak sepenuhnya "mengganti status sumber daya target"

Dibuat pada 30 Jun 2016  ·  68Komentar  ·  Sumber: encode/django-rest-framework

EDIT: Untuk status masalah saat ini, lewati ke https://github.com/encode/Django-rest-framework/issues/4231#issuecomment -332935943

===

Saya mengalami masalah dalam mengimplementasikan perpustakaan Konkurensi Optimis pada aplikasi yang menggunakan DRF untuk berinteraksi dengan database. Saya mencoba untuk:

  • Konfirmasikan bahwa perilaku yang saya lihat disebabkan oleh DRF
  • Konfirmasikan bahwa ini adalah perilaku yang dimaksudkan
  • Tentukan apakah ada cara praktis untuk mengatasi perilaku ini

Saya baru-baru ini menambahkan konkurensi optimis ke aplikasi Django saya. Untuk menyelamatkan Anda dari pencarian Wiki:

  • Setiap model memiliki bidang versi
  • Saat editor mengedit objek, mereka mendapatkan versi objek yang sedang mereka edit
  • Saat editor menyimpan objek, nomor versi yang disertakan dibandingkan dengan database
  • Jika versinya cocok, editor memperbarui dokumen terbaru dan penyimpanan berhasil
  • Jika versi tidak cocok, kami menganggap hasil edit yang "bertentangan" telah dikirimkan antara waktu editor dimuat dan disimpan, jadi kami menolak hasil edit
  • Jika versi tidak ada, kami tidak dapat melakukan pengujian dan harus menolak hasil edit

Saya memiliki UI lama yang berbicara melalui DRF. UI lama tidak menangani nomor versi. Saya berharap ini menyebabkan kesalahan konkurensi, tetapi ternyata tidak. Jika saya memahami diskusi di #3648 dengan benar:

  • DRF menggabungkan PUT dengan catatan yang ada. Ini menyebabkan ID versi yang hilang diisi dengan ID database saat ini
  • Karena ini selalu memberikan kecocokan, menghilangkan variabel ini akan selalu merusak sistem konkurensi optimis yang berkomunikasi di seluruh DRF
  • ~Tidak ada opsi yang mudah (seperti membuat bidang "wajib") untuk memastikan data dikirimkan setiap saat.~ (edit: Anda dapat mengatasi masalah dengan membuatnya diperlukan seperti yang ditunjukkan dalam komentar ini )

Langkah-langkah untuk mereproduksi

  1. Siapkan bidang Konkurensi Optimis pada model
  2. Buat instance baru dan perbarui beberapa kali (untuk memastikan Anda tidak lagi memiliki nomor versi default)
  3. Kirim pembaruan (PUT) melalui DRF tidak termasuk ID versi

    Perilaku yang diharapkan

ID versi yang hilang tidak boleh cocok dengan database dan menyebabkan masalah konkurensi.

Perilaku sebenarnya

ID versi yang hilang diisi oleh DRF dengan ID saat ini sehingga pemeriksaan konkurensi lolos.

Enhancement

Semua 68 komentar

Oke, tidak bisa menjanjikan saya akan dapat meninjau tiket yang cukup mendalam ini segera, karena rilis 3.4 yang akan datang lebih diprioritaskan. Tapi terima kasih untuk masalah yang begitu terperinci dan dipikirkan dengan matang. Ini kemungkinan besar akan dilihat dalam skala minggu, bukan hari atau bulan. Jika Anda membuat kemajuan, memiliki pemikiran lebih lanjut, harap perbarui tiket dan beri tahu kami.

OKE. Saya cukup yakin masalah saya adalah kombinasi dari dua faktor:

  1. DRF tidak memerlukan bidang di PUT (meskipun diperlukan dalam model) karena memiliki default (versi = 0)
  2. DRF menggabungkan bidang PUT dengan objek saat ini (tanpa menyuntikkan default)

Akibatnya, DRF menggunakan nilai (database) saat ini dan memutus kontrol konkurensi. Bagian kedua dari masalah ini terkait dengan diskusi di #3648 (juga dikutip di atas) dan ada diskusi (sebelum 3.x) di #1445 yang tampaknya masih relevan.

Saya berharap kasus konkret (dan semakin umum) di mana perilaku default sesat akan cukup untuk membuka kembali diskusi tentang perilaku "ideal" dari ModelSerializer. Jelas, saya hanya sedalam satu inci di DRF, tetapi intuisi saya adalah bahwa perilaku berikut sesuai untuk bidang yang diperlukan dan PUT:

  • Saat menggunakan serializer non-parsial, kita harus menerima nilai, menggunakan default, atau (jika tidak ada default yang tersedia) memunculkan kesalahan validasi. Validasi seluruh model harus berlaku hanya untuk input/default.
  • Saat menggunakan serializer parsial, kita harus menerima nilai atau mundur pada nilai saat ini. Validasi seluruh model harus berlaku untuk data gabungan tersebut.
  • Saya percaya serializer "non-parsial" saat ini benar-benar semi-parsial:

    • Ini non-parsial untuk bidang yang diperlukan dan tidak memiliki default

    • Ini sebagian untuk bidang yang diperlukan dan memiliki default (karena default tidak digunakan)

    • Ini sebagian untuk bidang yang tidak wajib

Kami tidak dapat mengubah poin (1) di atas atau default menjadi tidak berguna (kami memerlukan input meskipun kami tahu default). Itu berarti kita harus memperbaiki masalah dengan mengubah #2 di atas. Saya setuju dengan argumen Anda di #2683 bahwa:

Default model adalah default model. Serializer harus menghilangkan nilai dan menunda tanggung jawab ke Model.object.create() untuk menangani ini.

Agar konsisten dengan pemisahan masalah itu, pembaruan harus membuat instance baru (mendelegasikan semua default ke model) dan menerapkan nilai yang dikirimkan ke instance baru itu. Ini menghasilkan perilaku yang diminta di #3648.

Mencoba menjelaskan jalur migrasi membantu menyoroti betapa anehnya perilaku saat ini. Tujuan akhirnya adalah untuk

  1. Perbaiki ModelSerializer,
  2. Tambahkan bendera untuk status kuasi-parsial ini, dan
  3. Jadikan bendera itu sebagai default (untuk kompatibilitas mundur)

Apa nama bendera itu? Model Serializer saat ini sebenarnya adalah serializer parsial yang (agak sewenang-wenang) membutuhkan bidang yang memenuhi kondisi required==True and default==None . Kami tidak dapat secara eksplisit menggunakan flag partial tanpa merusak kompatibilitas mundur sehingga kami memerlukan flag baru (semoga sementara). Saya memiliki quasi_partial , tetapi ketidakmampuan saya untuk mengungkapkan persyaratan arbitrer required==True and default==None adalah mengapa sangat jelas bagi saya bahwa perilaku ini harus segera ditinggalkan.

Anda dapat menambahkan extra_kwargs di Meta serializer, menjadikan version sebagai bidang wajib.

class ConcurrentModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = ConcurrentModel
        extra_kwargs = {'version': {'required': True}}

Terima kasih @anoopmalev. Itu akan membuat saya tetap di cabang produksi.

Setelah "tidur di atasnya" saya menyadari ada kerutan ekstra. Semua yang saya katakan harus berlaku untuk bidang serializer. Jika bidang tidak disertakan dalam serializer, itu tidak boleh diubah. Dengan cara ini, semua serilaizer (dan harus) parsial untuk bidang yang tidak disertakan. Ini sedikit lebih rumit daripada "buat contoh baru" saya di atas.

Saya percaya masalah ini perlu direduksi menjadi proposal yang lebih terbatas untuk bergerak maju.
Tampaknya luas untuk dapat ditindaklanjuti dalam kondisinya saat ini.
Untuk saat ini saya menutup ini - jika ada yang bisa menguranginya menjadi pernyataan perilaku yang diinginkan yang ringkas dan dapat ditindaklanjuti, maka kita dapat mempertimbangkan kembali. Sampai saat itu saya pikir itu hanya untuk luas.

Berikut proposal singkat... untuk serializer non-parsial:

  1. Untuk bidang apa pun yang tidak tercantum dalam serializer (secara implisit atau eksplisit) atau ditandai sebagai hanya-baca, pertahankan nilai yang ada
  2. Untuk semua bidang lainnya, gunakan opsi pertama yang tersedia:

    1. Isi dengan nilai yang dikirimkan

    2. Isi dengan default, termasuk nilai yang tersirat oleh blank dan/atau null

    3. Naikkan pengecualian

Untuk kejelasan, validasi dijalankan pada produk akhir dari proses ini.

Yaitu Anda ingin mengatur required=True pada bidang serializer apa pun yang tidak memiliki model default, untuk pembaruan?

Apakah saya sudah benar?

Ya (dan banyak lagi). Begitulah cara saya memahami perbedaan partial (semua bidang opsional) vs. non-partial (semua bidang wajib diisi). Satu-satunya saat serializer non-partial tidak memerlukan bidang adalah adanya default (didefinisikan secara sempit atau luas) _karena serializer dapat menggunakan default itu jika tidak ada nilai yang diberikan._

Bagian yang dicetak miring adalah apa yang saat ini tidak dilakukan DRF dan perubahan yang lebih penting dalam proposal saya. Implementasi saat ini hanya melewati bidang.

Saya memiliki proposal kedua yang tercampur, tetapi ini benar-benar pertanyaan terpisah tentang seberapa murah hati Anda dengan gagasan "default". Perilaku saat ini "ketat" karena hanya default yang diperlakukan seperti itu. Jika Anda _benar-benar_ ingin mengurangi jumlah data yang diperlukan, Anda juga dapat membuat bidang blank=True opsional... dengan asumsi bahwa nilai yang tidak ada adalah nilai kosong.

@claytondaley Saya menggunakan OOL dengan DRF sejak 2x dengan cara ini:

class VersionModelSerializer(serializers.ModelSerializer, BaseSerializer):
    _initial_version = 0

    _version = VersionField()

    def __init__(self, *args, **kwargs):
        super(VersionModelSerializer, self).__init__(*args, **kwargs)

        # version field should not be required if there is no object
        if self.instance is None and '_version' in self.fields and\
                getattr(self, 'parent', None) is None:
            self.fields['_version'].read_only = True
            self.fields['_version'].required = False

        # version field is required while updating instance
        if self.instance is not None and '_version' in self.fields:
            self.fields['_version'].required = True

        if self.instance is not None and hasattr(self.instance, '_version'):
            self._initial_version = self.instance._version

    def validate__version(self, value):
        if self.instance is not None:
            if not value and not isinstance(value, int):
                raise serializers.ValidationError(_(u"This field is required"))

        return value
   # more code & helpers

ini bekerja sangat baik dengan semua jenis logika bisnis dan tidak pernah menyebabkan masalah.

Apakah ini dibiarkan tertutup karena kecelakaan? Saya menanggapi pertanyaan spesifik dan tidak mendengar alasan apa yang salah dengan proposal tersebut.

@claytondaley mengapa OOL harus menjadi bagian dari DRF? Periksa kode saya - ini berfungsi hanya temukan di aplikasi besar (1400 tes). VersionField hanyalah IntegerField .

Anda telah mengkodekan OOL ke dalam serializer. Ini adalah tempat yang salah untuk melakukannya karena Anda memiliki kondisi balapan. Pembaruan paralel (dengan versi sebelumnya yang sama) semuanya akan lolos di serializer ... tetapi hanya satu yang akan menang di tindakan simpan.

Saya menggunakan django-concurrency yang menempatkan logika OOL ke dalam tindakan simpan (di tempatnya). Pada dasarnya UPDATE... WHERE version = submitted_version . Ini adalah atom sehingga tidak ada kondisi balapan. Namun, ini memperlihatkan cacat dalam logika serialisasi ::

  • Jika default diatur pada bidang dalam model, DRF menetapkan required=False . Gagasan (valid) adalah bahwa DRF dapat menggunakan default itu jika tidak ada nilai yang dikirimkan.
  • Namun, jika bidang itu tidak ada, DRF tidak menggunakan default. Alih-alih menggabungkan data yang dikirimkan dengan versi objek saat ini.

Ketika kami tidak memerlukan bidang, kami melakukannya karena kami memiliki default untuk digunakan. DRF tidak memenuhi kontrak itu karena tidak menggunakan default... ia menggunakan nilai yang ada.

Masalah mendasar telah dibahas sebelumnya, tetapi mereka tidak memiliki kasus yang bagus dan konkret. OOL adalah kasus yang ideal. Nilai bidang versi yang ada selalu melewati OOL sehingga Anda dapat melewati seluruh sistem OOL dengan mengabaikan versi. Itu (jelas) bukan perilaku yang diinginkan dari sistem OOL.

@claytondale

Anda telah mengkodekan OOL ke dalam serializer.

Apakah saya? Sudahkah Anda menemukan logika OOL di serializer saya di samping persyaratan bidang?

Ini adalah tempat yang salah untuk melakukannya karena Anda memiliki kondisi balapan.

Sory, saya hanya tidak bisa melihat di mana kondisi balapan di sini.

Saya menggunakan Django-concurrency yang menempatkan logika OOL ke dalam tindakan simpan (di tempatnya).

Saya juga menggunakan django-concurrency :) Tapi itu level model, bukan serializer. Pada level serializer Anda hanya perlu:

  • pastikan bidang _version selalu diperlukan (ketika seharusnya)
  • pastikan serializer Anda tahu cara menangani kesalahan OOL (bagian ini saya hilangkan)
  • pastikan apiview Anda tahu cara menangani kesalahan OOL dan memunculkan HTTP 409 dengan kemungkinan konteks yang berbeda

sebenarnya, saya tidak menggunakan django-concurrency karena masalah yang ditandai oleh autor sebagai "tidak akan diperbaiki": ia melewati OOL ketika obj.save(update_fields=['one', 'two', 'tree']) digunakan yang menurut saya praktik buruk, jadi saya melakukan fork paket.

inilah metode save yang hilang dari serializer yang telah saya sebutkan sebelumnya. yang seharusnya menyelesaikan semua masalah Anda:

    def save(self, **kwargs):
        try:
            self.instance = super(VersionModelSerializer, self).save(**kwargs)
            return self.instance
        except VersionException:
            # Use select_for_update so we have some level of guarantee
            # that object won't be modified at least here at the same time
            # (but it may be modified somewhere else, where select_for_update
            # is not used!)
            with transaction.atomic():
                db_instance = self.instance.__class__.objects.\
                    select_for_update().get(pk=self.instance.pk)
                diff = self._get_serializer_diff(db_instance)

                # re-raise exception, so api client will receive friendly
                # printed diff with writable fields of current serializer
                if diff:
                    raise VersionException(diff)

                # otherwise re-try saving using db_instance
                self.instance = db_instance
                if self.is_valid():
                    return super(VersionModelSerializer, self).save(**kwargs)
                else:
                    # there are errors that could not be displayed to a user
                    # so api client should refresh & retry by itself
                    raise VersionException

        # instance.save() was interrupted by application error
        except ApplicationException as logic_exc:
            if self._initial_version != self.instance._version:
                raise VersionException

            raise logic_exc

Maaf. Saya tidak membaca kode Anda untuk mengetahui apa yang Anda lakukan. Saya melihat serializer. Anda jelas dapat mengatasi masalah ini dengan meretas serializer tetapi Anda tidak harus .... karena cacat dalam logika DRF berdiri sendiri. Saya hanya menggunakan OOL untuk menjelaskan maksudnya.

Dan Anda harus mencoba kode itu terhadap versi terbaru dari Django-concurrency (menggunakan IGNORE_DEFAULT=False ). Django-concurrency juga mengabaikan nilai default, tetapi saya mengirimkan tambalan. Ada kasus sudut aneh yang harus saya buru untuk membuatnya bekerja untuk kasus normal.

Saya pikir itu disebut memperluas fungsionalitas default, tidak benar-benar meretas. Saya pikir tempat terbaik untuk dukungan fitur tersebut adalah di paket django-concurrency .

Saya telah membaca ulang seluruh diskusi masalah dan menemukan proposal Anda terlalu luas dan itu akan gagal di banyak tempat (karena secara ajaib menggunakan nilai default dari sumber yang berbeda dalam kondisi yang berbeda). DRF 3.x menjadi jauh lebih mudah dan dapat diprediksi daripada 2.x, biarkan tetap seperti itu :)

Anda tidak dapat memperbaiki ini di lapisan model karena rusak di serializer (sebelum sampai ke model). Sisihkan OOL... mengapa kita tidak memerlukan sebuah bidang jika default disetel?

Serializer non-partial "membutuhkan" semua bidang (pada dasarnya) namun kami membiarkan ini berlalu. Apakah itu bug? Atau apakah kita punya alasan logis?

seperti yang Anda lihat dalam contoh kode saya – bidang _version selalu diperlukan dengan benar dalam semua kasus yang memungkinkan.

btw, ternyata saya telah meminjam kode model lvl dari https://github.com/gavinwahl/Django-optimistic-lock dan bukan dari django-concurrency yang merupakan cara rumit untuk hampir tanpa alasan.

... jadi bugnya adalah "serializer non-partial salah mengatur beberapa bidang ke tidak wajib". Itu alternatifnya. Karena itulah komitmen (implisit) yang dibuat oleh serializer non-partial.

Saya dapat mengutipnya :

Secara default, serializers harus melewati nilai untuk semua bidang yang diperlukan atau mereka akan meningkatkan kesalahan validasi.

Ini tidak mengatakan apa-apa tentang yang diperlukan (kecuali ketika default disediakan).

(dan saya mengerti bahwa saya sedang berbicara tentang dua level yang berbeda, tetapi ModelSerializer seharusnya tidak menghapus bidang jika tidak akan bertanggung jawab atas keputusan itu)

Saya pikir saya telah kehilangan poin Anda ..

(dan saya mengerti bahwa saya sedang berbicara tentang dua level yang berbeda, tetapi ModelSerializer seharusnya tidak menghapus bidang jika tidak akan bertanggung jawab atas keputusan itu)

Apa yang salah dengan itu?

OK biarkan saya mencoba sudut yang berbeda.

  • Asumsikan saya memiliki Serializer Model non-parsial (edit: semua default) yang mencakup semua bidang dalam model saya.

Haruskah CREATE atau UPDATE dengan data yang sama menghasilkan objek yang berbeda (dikurangi ID)

Bisakah Anda menggambarkan ide-ide Anda menggunakan beberapa model & serializer yang sangat sederhana dan beberapa baris yang menunjukkan perilaku yang gagal/diharapkan?

Saya akan menyusun sesuatu besok karena sudah larut di sini... tetapi semakin dalam saya memahaminya, semakin masuk akal #3648 untuk serializer non-parsial. Sementara itu, mengapa ModelSerializer tidak memerlukan semua bidang dalam model? Mungkin alasanmu berbeda denganku.

ModelSerializer memeriksa model yang dibatasi dan memutuskan apakah itu harus diperlukan bukan?

Maksud saya bukan secara mekanis bagaimana . Asumsi dasar untuk Serializer non-parsial adalah membutuhkan semuanya (dikutip di atas). Jika get_field_kwargs akan menyimpang dari asumsi ini (khususnya, di sini ), itu harus memiliki alasan yang bagus. Apa alasan itu?

Jawaban pilihan saya adalah jawaban yang terus saya berikan, "karena dapat menggunakan default itu jika tidak ada nilai yang dikirimkan" (tetapi kemudian DRF harus benar-benar menggunakan default). Apakah ada jawaban lain yang saya lewatkan? Alasan mengapa bidang dengan default tidak diperlukan?

Jelas, saya lebih suka solusi "lengkap". Namun, saya akan mengakui bahwa ada jawaban kedua. Kami dapat meminta bidang ini secara default. Itu menghilangkan kasus khusus (saat ini sewenang-wenang). Ini menyederhanakan/mengurangi kode. Ini konsisten secara internal. Ini mengatasi kekhawatiran saya.

Pada dasarnya, itu membuat serializer non-partial benar-benar non-partial.

Sekarang saya setidaknya tahu apa yang Anda maksud. sudahkah Anda memeriksa apa perilaku ModelForm dalam kasus seperti itu? (Tidak dapat melakukannya sendiri di ponsel)

Django docs mengatakan bahwa 'kosong' mengontrol apakah bidang diperlukan atau tidak. Saya sarankan Anda harus membuka tiket terpisah untuk masalah ini karena yang ini berisi banyak komentar yang tidak terkait. Menurut saya modelserializer mungkin berfungsi seperti modelform: kontrol opsi kosong diperlukan, 'null' memberi tahu apakah Tidak ada input yang dapat diterima dan 'default' tidak berpengaruh pada logika itu.

Saya bersedia membuka tiket kedua, tetapi saya khawatir bahwa kosong memerlukan kode yang sama. Dari grup diskusi Django :

jika kita mengambil formulir model yang ada dan template yang berfungsi, tambahkan bidang karakter opsional ke model tetapi gagal menambahkan bidang yang sesuai ke template HTML (misalnya kesalahan manusia, lupa tentang template, tidak memberi tahu pembuat template untuk membuat perubahan, tidak menyadari perubahan yang perlu dilakukan pada templat), ketika formulir itu dikirimkan, Django akan menganggap bahwa pengguna telah memberikan nilai string kosong untuk bidang yang hilang dan menyimpannya ke model, menghapus nilai apa pun yang ada .

Agar konsisten, kami memiliki kewajiban untuk memenuhi paruh kedua kontrak, menetapkan nilai absen menjadi kosong. Ini sedikit kurang bermasalah karena kosong dapat diisi tanpa referensi ke model, tetapi sangat mirip (dan, saya pikir, konsisten dengan #3648).

@tomchristie dapatkah Anda memberikan masukan singkat tentang ini: Mengapa status required bergantung pada bidang model defaults properti?

Mengapa status yang diperlukan bergantung pada properti default bidang model?

Sederhananya ini: Jika bidang model memiliki default, maka Anda dapat menghilangkan menyediakannya sebagai input.

Sebenarnya saya setuju dengan perilaku ini. ModelForm meskipun kodenya melakukan hal yang sama (html yang dihasilkan akan memberikan default). Jika DRF akan berbeda logika maka 'default' tidak akan pernah berlaku. Saya selesai dengan masalah ini.

@pySilver sebenarnya, inilah perilaku ModelForm:

# models.py

from django.db import models

class MyModel(models.Model):
    no_default = models.CharField(max_length=100)
    has_default = models.CharField(max_length=100, default="iAmTheDefault")

Untuk kejelasan, barang masih bernama "sebagian" karena _update_ bersifat parsial. Saya juga sedang menguji pembaruan lengkap ("penuh"), tetapi kode tersebut tidak diperlukan untuk menunjukkan perilaku:

# in manage.py shell
>>> from django import forms
>>> from django.conf import settings
>>> from form_serializer.models import MyModel
>>>
>>> class MyModelForm(forms.ModelForm):
...     class Meta:
...         model = MyModel
...         fields = ['no_default', 'has_default']
...
>>>
>>> partial = MyModel.objects.create()
>>> partial.id = 2
>>> partial.no_default = "Must replace me"
>>> partial.has_default = "I should be replaced"
>>> partial.save()
>>>
>>>
>>> POST_PARTIAL = {
...     "id": 2,
...     "no_default": "must change me",
... }
>>>
>>>
>>> form_partial = MyModelForm(POST_PARTIAL)
>>> form_partial.is_valid()
False
>>> form_partial._errors
{'has_default': [u'This field is required.']}

ModelForm membutuhkan input ini meskipun memiliki default. Ini adalah salah satu dari dua perilaku yang konsisten secara internal.

Mengapa status yang diperlukan bergantung pada properti default bidang model?

Sederhananya ini: Jika bidang model memiliki default, maka Anda dapat menghilangkan menyediakannya sebagai input.

@tomchristie setuju pada prinsipnya. Tapi apa perilaku yang diharapkan?

  • Saat membuat, saya mendapatkan default (sepele, semua orang setuju ini benar)
  • Pada pembaruan, apa yang harus saya dapatkan?

Sepertinya saya harus mendapatkan pembaruan default juga. Saya tidak mengerti mengapa serializer non-parsial harus berperilaku berbeda dalam dua kasus. Non-parsial berarti saya mengirim catatan "lengkap". Dengan demikian catatan lengkap harus diganti.

Saya berharap nilainya tidak berubah jika tidak disediakan saat pembaruan. Saya mengerti maksudnya, tetapi menimpa secara transparan dengan nilai default akan menjadi kontra-intuitif dari POV saya.

(Jika ada yang saya pikir mungkin akan lebih baik untuk semua pembaruan menjadi semantik parsial untuk semua bidang - PUT masih akan idempoten, yang merupakan aspek penting, meskipun mungkin canggung untuk mengubah mengingat perilaku saat ini)

Saya tentu saja tidak membagikan preferensi Anda; Saya ingin semua antarmuka saya ketat kecuali saya sengaja membuatnya sebaliknya. Namun, perbedaan PARTIAL vs. NON-PARTIAL Anda sudah memberikan (secara teori) apa yang kita berdua inginkan.

Saya percaya sebagian berperilaku persis seperti yang Anda inginkan:

  • PEMBARUAN 100% sebagian
  • CREATE (saya berasumsi) sebagian sehubungan dengan default dan blank (pengecualian logis). Dalam semua kasus lain, batasan model/database mengikat.

Saya hanya mencoba untuk mendapatkan konsistensi dalam serializer non-parsial. Jika Anda menghilangkan kasus khusus untuk default , serializer non-partial Anda yang ada menjadi serializer ketat yang saya inginkan. Mereka juga mencapai paritas dengan ModelForm.

Saya menyadari ini menciptakan diskontinuitas kecil dalam proyek, tetapi ini bukan pertama kalinya seseorang membuat perubahan seperti ini. Tambahkan bendera "warisan" secara default ke perilaku saat ini, tambahkan peringatan (bahwa perilaku default akan berubah), dan ubah default di rilis utama berikutnya.

Lebih penting lagi, jika Anda ingin pembuat serial Anda menjadi de facto baru untuk Django, Anda akan tetap membuat perubahan ini. Jumlah orang yang mengonversi dari ModelForm akan jauh melebihi basis pengguna yang ada dan mereka akan mengharapkan setidaknya perubahan ini.

Memasukkan dua sen saya:
Saya cenderung setuju dengan @claytondaley. PUT adalah pengganti sumber daya idempoten, PATCH adalah pembaruan untuk sumber daya yang ada. Ambil contoh berikut:

class Profiles(models.Model):
    username = models.CharField()
    role = models.CharField(default='member', choices=(
        ('member', 'Member'), 
        ('moderator', 'Moderator'),
        ('admin', 'Admin'), 
    ))

Profil baru masuk akal memiliki peran anggota default. Mari kita ambil permintaan berikut:

POST /profiles username=moe
PUT /profiles/1 username=curly
PATCH /profiles/1 username=larry&role=admin
PUT /profiles/1 username=curly

Seperti saat ini, setelah PUT pertama, data profil akan berisi {'username': 'curly', 'role': 'member'} . Setelah PUT kedua, Anda akan memiliki {'username': 'curly', 'role': 'admin'} . Apakah ini tidak merusak idempotensi? (Saya tidak sepenuhnya yakin - saya bertanya secara sah)

Sunting:
Saya pikir semua orang berada di halaman yang sama tentang semantik PATCH.

Setelah PUT kedua, Anda akan memiliki {'username': 'curly', 'role': 'admin'}

Saya, secara pribadi akan terkejut jika peran akan beralih kembali ke default (meskipun saya melihat alasan diskusi objek replace ini, saya belum pernah memiliki masalah dunia nyata dengannya)

saya belum pernah memiliki masalah dunia nyata dengannya

Sama di sini, tetapi sejauh ini proyek kami mengandalkan PATCH :)
Yang mengatakan, kasus penggunaan OP dengan versi model untuk menangani konkurensi memang masuk akal bagi saya. Saya berharap PUT menggunakan nilai default (jika nilai dihilangkan), meningkatkan pengecualian konkurensi.

Mari saya mulai dengan mengakui bahwa serializer tidak harus mengikuti RFC RESTful. Namun, mereka setidaknya harus menawarkan mode yang _are_ kompatibel -- terutama dalam paket yang menawarkan dukungan REST.

Argumen asli saya berasal dari prinsip pertama, tetapi RFC (bagian 4.3.4) secara khusus mengatakan (penekanan ditambahkan):

Perbedaan mendasar antara metode POST dan PUT disorot oleh maksud yang berbeda untuk representasi terlampir. Sumber daya target dalam permintaan POST dimaksudkan untuk menangani representasi terlampir sesuai dengan semantik sumber daya itu sendiri, sedangkan representasi terlampir dalam permintaan PUT didefinisikan sebagai menggantikan status sumber daya target.
...
Server asal yang mengizinkan PUT pada sumber daya target tertentu HARUS mengirim respons 400 (Permintaan Buruk) ke permintaan PUT yang berisi bidang header Rentang Konten (Bagian 4.2 dari [RFC7233]), karena muatannya kemungkinan sebagian konten yang telah keliru PUT sebagai representasi penuh . Pembaruan konten sebagian dimungkinkan dengan menargetkan sumber daya yang diidentifikasi secara terpisah dengan status yang tumpang tindih dengan sebagian dari sumber daya yang lebih besar, atau dengan menggunakan metode berbeda yang telah ditentukan secara khusus untuk pembaruan sebagian (misalnya, metode PATCH yang ditentukan di [RFC5789])

Jadi PUT tidak boleh parsial (lihat juga, di sini ). Namun, bagian tentang PUT juga menjelaskan:

Metode PUT meminta agar status sumber daya target dibuat atau diganti dengan status yang ditentukan oleh representasi yang disertakan dalam muatan pesan permintaan. PUT yang berhasil dari representasi yang diberikan akan menyarankan bahwa GET berikutnya pada sumber daya target yang sama akan menghasilkan representasi yang setara yang dikirim dalam respons 200 (OK).

Poin tentang GET (meskipun tidak wajib) mendukung solusi "kompromi" saya. Meskipun menyuntikkan blank/default nyaman, itu tidak akan memberikan perilaku ini. Paku di peti mati mungkin bahwa solusi ini meminimalkan kebingungan karena tidak akan ada bidang yang hilang untuk menimbulkan keraguan.

Jelas, PATCH adalah opsi yang ditentukan untuk pembaruan sebagian, tetapi itu digambarkan sebagai "set instruksi" daripada hanya sebagian PUT sehingga selalu membuat saya sedikit gelisah. Bagian pada POST (4.3.3) sebenarnya menyatakan:

Metode POST meminta sumber daya target memproses representasi yang disertakan dalam permintaan sesuai dengan semantik spesifik sumber daya itu sendiri. Misalnya, POST digunakan untuk fungsi berikut (antara lain):

  • Menyediakan blok data, seperti bidang yang dimasukkan ke dalam formulir HTML, ke proses penanganan data;

...

  • Menambahkan data ke representasi sumber daya yang ada.

Saya pikir ada argumen untuk menggunakan POST untuk pembaruan sebagian karena:

  • secara konseptual, mengubah data tidak berbeda dengan menambahkan
  • POST diizinkan untuk menggunakan aturannya sendiri sehingga aturan tersebut dapat berupa pembaruan sebagian
  • operasi ini dapat dengan mudah dibedakan dari CREATE dengan adanya ID

Bahkan jika DRF tidak menginginkan kepatuhan penuh, kita memerlukan serializer yang kompatibel dengan operasi PUT spesifikasi (yaitu mengganti seluruh objek). Jawaban paling sederhana (dan jelas paling tidak membingungkan) adalah meminta semua bidang. Ini juga menyarankan bahwa PUT harus non-parsial secara default dan pembaruan parsial harus menggunakan kata kunci yang berbeda (PATCH atau bahkan POST).

Saya pikir saya baru saja mendapatkan masalah PUT pertama saya saat memigrasikan aplikasi kami ke drf3.4.x :)

<strong i="6">@cached_property</strong>
    def _writable_fields(self):
        return [
            field for field in self.fields.values()
            if (not field.read_only) or (field.default is not empty)
        ]

Ini membuat .validated_data saya berisi data yang tidak saya berikan dalam permintaan PUT dan tidak saya berikan secara manual dalam serializer. Nilai diambil dari default= pada level serializer. Jadi pada dasarnya sementara dimaksudkan untuk memperbarui bidang tertentu, saya juga menimpa beberapa bidang itu dengan nilai default secara tiba-tiba.

Senang bagi saya, saya menggunakan ModelSerializer khusus, jadi saya dapat memperbaiki masalah dengan mudah.

@pySilver Saya tidak mengerti isi komentar terbaru.

@rpkilby "Mari kita ambil permintaan berikut... Apakah ini tidak merusak idempotensi"

Tidak, setiap permintaan PUT bersifat idempoten karena dapat diulang beberapa kali sehingga menghasilkan status yang sama. Itu tidak berarti bahwa jika beberapa bagian lain dari keadaan telah dimodifikasi sementara itu, itu entah bagaimana akan diatur ulang.

Berikut beberapa opsi berbeda untuk perilaku PUT .

  • Bidang diperlukan kecuali required=False atau mereka memiliki default . (Yang ada)
  • Semua bidang yang diperlukan. (Semantik yang lebih ketat dan lebih selaras dari pembaruan lengkap _but_ canggung karena sebenarnya lebih ketat daripada semantik pembuatan awal untuk POST)
  • Tidak ada bidang yang diperlukan (Yaitu. Hanya mencerminkan perilaku PATCH)

Jelas bahwa tidak ada _absolute_ jawaban yang benar, tetapi saya yakin kita memiliki perilaku yang paling praktis seperti yang ada saat ini.

Saya yakin beberapa kasus penggunaan mungkin merasa bermasalah jika ada bidang yang tidak perlu disediakan untuk permintaan POST , tetapi kemudian melakukannya untuk permintaan PUT . Selain itu, PUT-as-create itu sendiri adalah operasi yang valid, jadi sekali lagi, akan aneh jika itu memiliki semantik "kebutuhan" yang berbeda dengan POST.

Jika seseorang ingin meneruskan ini, saya _sangat_ menyarankan untuk memulai sebagai paket pihak ketiga, yang mengimplementasikan kelas serializer dasar yang berbeda. Kami kemudian dapat menautkannya dari dokumentasi serializers. Jika kasusnya dibuat dengan baik, maka kita dapat mempertimbangkan untuk mengadaptasi perilaku default di beberapa titik di masa mendatang.

@tomchristie apa yang ingin saya katakan:

Saya memiliki serializer dengan bidang language readonly dan model:

class Book(models.Model):
      title = models.CharField(max_length=100)
      language = models.ChoiceField(default='en', choices=(('pl', 'Polish'), ('en', 'English'))

class BookUpdateSerialzier(serializers.ModelSerializer):
      # language is readonly, I dont want to let users update that field using this serializer
      language = serializers.ChoiceField(default='en', choices=(('pl', 'Polish'), ('en', 'English'), read_only=True)
      class Meta:
          model = MyModel
          fields = ('title', 'language', )

book = Book(title="To be or 42", language="pl")
book.save()

s = BookUpdateSerialzier(book, data={'title': 'Foobar'}, partial=True)
s.is_valid()
assert 'language' in s.validated_data # !!! 
assert 'pl' == s.validated_data # AssertionError... here :(
  • Saya tidak lulus language dalam permintaan dan saya tidak berharap untuk melihat ini dalam data yang divalidasi. Diteruskan ke update itu akan menimpa instance saya dengan default meskipun faktanya objek sudah memiliki beberapa nilai non-default yang ditetapkan.
  • Akan lebih sedikit masalah jika validated_data['language'] akan menjadi book.language dalam kasus itu.

@pySilver - Yup, itu telah diselesaikan di https://github.com/tomchristie/Django-rest-framework/pull/4346 baru hari ini.

Kebetulan Anda tidak perlu default= pada bidang serializer dalam contoh yang Anda miliki, karena Anda memiliki default pada ModelField .

@tomchristie Apakah Anda setidaknya setuju bahwa perilaku PUT saat ini bukan spesifikasi RFC? Dan kedua saran saya (memerlukan semua atau menyuntikkan default) akan membuatnya begitu?

@tomchristie berita bagus!

Kebetulan Anda tidak memerlukan default= pada bidang serializer dalam contoh yang Anda miliki, karena Anda memiliki default pada ModelField.

Ya, saya hanya ingin membuatnya sangat eksplisit untuk demo.

Akhirnya tenggelam bahwa @tomchristie tidak berdebat untuk/melawan perilaku serializer secara terpisah. Saya percaya keberatannya berasal (secara implisit) dari persyaratan bahwa serializer tunggal mendukung semua mode REST. Ini menunjukkan dirinya dalam keluhannya tentang bagaimana serializer yang ketat akan mempengaruhi POST. Karena mode REST tidak kompatibel, solusi saat ini adalah serializer yang bukan spesifikasi untuk mode tunggal apa pun.

Jika itu akar keberatan yang sebenarnya, mari kita hadapi. Bagaimana serializer tunggal memberikan perilaku spesifikasi untuk semua mode REST? Jawaban spontan saya adalah bahwa PARTIAL vs. NON-PARTIAL diimplementasikan pada level yang salah:

  • Kami memiliki serializer parsial dan non-parsial. Pendekatan ini berarti kita memerlukan beberapa serializer untuk mendukung perilaku spesifikasi untuk semua mode.
  • Kami sebenarnya membutuhkan validasi parsial vs non-parsial (atau sesuatu dalam nada ini). Mode REST yang berbeda perlu meminta mode validasi yang berbeda dari serializer.

Untuk memberikan pemisahan masalah, pembuat serial tidak boleh mengetahui mode REST sehingga tidak dapat diimplementasikan sebagai pembuat serial pihak ke-3 (juga, saya curiga, apakah pembuat serial bahkan memiliki akses ke mode). Sebagai gantinya, DRF harus memberikan informasi tambahan ke pembuat serial (kira-kira replace=True untuk PUT ). Serializer dapat memutuskan bagaimana menerapkan ini (memerlukan semua bidang atau menyuntikkan default).

Jelas, ini hanya proposal kasar, tapi mungkin akan memecahkan kebuntuan.

Selain itu, PUT-as-create itu sendiri adalah operasi yang valid, jadi sekali lagi, akan aneh jika itu memiliki semantik "kebutuhan" yang berbeda dengan POST.

Saya setuju bahwa Anda dapat membuat dengan PUT, tetapi saya tidak setuju bahwa semantiknya sama. PUT bekerja pada sumber daya tertentu:

Metode PUT meminta agar status sumber daya target dibuat atau diganti dengan status yang ditentukan oleh representasi yang disertakan dalam muatan pesan permintaan.

Oleh karena itu, saya percaya bahwa semantik create sebenarnya berbeda:

  • POSke /citizen/ mengharapkan SSN (nomor jaminan sosial) dibuat
  • MELETAKKANke /citizen/<SSN> memperbarui data untuk SSN tertentu. Jika tidak ada data di SSN itu, itu menghasilkan file create.

Karena "id" harus disertakan dalam URI PUT, Anda dapat memperlakukannya sesuai kebutuhan. Sebaliknya, "id" adalah opsional dalam POST.

Karena "id" harus disertakan dalam URI PUT, Anda dapat memperlakukannya sesuai kebutuhan. Sebaliknya, "id" adalah opsional dalam POST.

Memang. Saya merujuk secara khusus pada fakta bahwa perubahan yang diusulkan dari "membuat PUT secara ketat memerlukan _semua_ bidang" akan berarti bahwa PUT-as-create akan memiliki perilaku yang berbeda dengan POST-as-create wrt. jika bidang diperlukan atau tidak.

Setelah mengatakan bahwa saya sampai pada nilai dalam memiliki opsi perilaku PUT-is-strict.

(Terapkan bahwa _semua_ bidang sangat diperlukan dalam kasus ini, terapkan bahwa bidang _no_ diperlukan dalam PATCH, dan gunakan tanda required= untuk POST)

Bagaimana serializer tunggal memberikan perilaku spesifikasi untuk semua mode REST?

Kita dapat membedakan antara create, update, dan partial update mengingat bagaimana serializer dibuat, jadi saya rasa itu bukan masalah.

Anda telah menyatakan bahwa Anda dapat create menggunakan PUT atau POST . Mereka memiliki semantik yang berbeda dan persyaratan yang berbeda sehingga create harus agnostik ke mode REST. Saya pikir perbedaan itu benar-benar terjadi sebagai bagian dari is_valid . Kami meminta mode validasi khusus:

  • tidak ada validasi keberadaan lapangan (PATCH)
  • validasi berdasarkan flag required (POST)
  • validasi kehadiran lapangan (PUT) yang ketat

Dengan menjaga logika khusus kata kunci keluar dari operasi CRUD, kami juga mengurangi sambungan antara serializer dan DRF. Jika mode validasi dapat dikonfigurasi, mode tersebut akan sepenuhnya digunakan untuk tujuan umum (bahkan jika kami hanya menerapkan 3 kasus khusus untuk 3 kata kunci kami).

Anda melakukan pekerjaan yang baik dengan memperdebatkan saya tentang fungsi ini, di sana. :)

Perbedaan "mode validasi" saat memanggil .is_valid() adalah pergolakan yang tidak akan terjadi.

Kami _could_ mempertimbangkan 'complete=True' padanan untuk kwarg unit 'partial=True' yang ada mungkin. Itu cukup cocok dengan cara kerja saat ini dan masih akan mendukung kasus "bidang ketat".

Apakah serializer tempat yang tepat untuk menyelesaikan masalah ini? Persyaratan ini terkait erat dengan kata kunci REST jadi mungkin itu tempat yang tepat untuk menerapkannya. Untuk mendukung pendekatan ini, pembuat serial hanya perlu mengekspos daftar bidang yang diterimanya sebagai input,

Lebih dari itu ... apakah ada diskusi yang bagus tentang pemisahan (alokasi) masalah Django di suatu tempat? Saya mengalami kesulitan membatasi diri pada jawaban ramah Django karena saya tidak tahu jawaban untuk pertanyaan seperti "mengapa validasi merupakan bagian dari serialisasi". Dokumen serialisasi untuk 1.9 bahkan tidak menyebutkan validasi. Dan, secara ketat dari prinsip pertama, sepertinya:

  1. Model harus bertanggung jawab untuk memvalidasi konsistensi internal dan
  2. "Tampilan" (dalam hal ini, prosesor mode REST) ​​harus bertanggung jawab untuk menegakkan aturan bisnis (seperti RFC) yang terkait dengan tampilan itu.

Jika tanggung jawab untuk validasi hilang, serializer dapat menjadi 100% parsial (secara default) dan khusus untuk aturan I/O seperti "hanya baca". ModelSerializer yang dibangun dengan cara ini akan mendukung berbagai macam tampilan.

Apakah serializer tempat yang tepat untuk menyelesaikan masalah ini?

Ya.

Dokumen serialisasi untuk 1.9 bahkan tidak menyebutkan validasi.

Serialisasi bawaan Django tidak berguna untuk Web APIS, itu benar-benar terbatas pada perlengkapan dumping dan pemuatan.

Anda mengetahui asumsi arsitektur Django dan DRF lebih baik daripada saya jadi saya harus tunduk kepada Anda tentang caranya. Tentu saja kwarg init memiliki perasaan yang tepat untuk itu... mengkonfigurasi ulang serializer "sesuai permintaan". Satu-satunya batasan adalah mereka tidak dapat dikonfigurasi ulang "on the fly", tetapi saya menganggap instance tersebut sekali pakai jadi ini bukan masalah yang signifikan.

Aku akan de-tonggak ini untuk saat ini. Kami dapat menilai kembali setelah v3.7

Terserah kalian, tapi saya ingin memastikan Anda jelas bahwa ini bukan Tiket untuk menambahkan dukungan konkurensi. Masalah sebenarnya adalah bahwa serializer tunggal tidak dapat memvalidasi PUT dan POST dengan benar dalam arsitektur saat ini. Concurrency baru saja memberikan "tes gagal".

TL;DR Anda dapat melihat mengapa masalah ini diblokir dengan memulai perbaikan yang diusulkan Tom .

Singkatnya, solusi yang diusulkan adalah membuat semua bidang diperlukan untuk permintaan PUT . Ada (setidaknya) dua masalah dengan pendekatan ini:

  1. Serializers berpikir dalam tindakan bukan metode HTTP sehingga tidak ada pemetaan satu-ke-satu. Contoh yang jelas adalah create karena dibagikan oleh PUT dan POST . Perhatikan bahwa create-by- PUT dinonaktifkan secara default sehingga perbaikan yang diusulkan mungkin lebih baik daripada tidak sama sekali.
  2. Kami tidak perlu meminta semua bidang dalam PUT (sentimen yang dibagikan oleh #3648, #4703). Jika bidang nillable tidak ada, kita tahu itu bisa menjadi None. Jika bidang dengan default tidak ada, kami tahu kami dapat menggunakan default. PUT s sebenarnya memiliki persyaratan bidang (berasal dari model) yang sama dengan POST .

Masalah sebenarnya adalah bagaimana kami menangani data yang hilang dan proposal dasar di #3648, #4703, dan di sini tetap menjadi solusi yang tepat. Kami dapat mendukung semua mode HTTP (termasuk create-by- PUT ) jika kami memperkenalkan konsep seperti if_missing_use_default . Proposal asli saya menyajikannya sebagai pengganti partial , tetapi lebih mudah (dan mungkin perlu) untuk menganggapnya sebagai konsep ortogonal.

jika kami memperkenalkan konsep seperti if_missing_use_default.

Tidak ada yang mencegah siapa pun untuk mengimplementasikan ini, atau "memerlukan semua bidang" yang ketat sebagai kelas serializer dasar, dan membungkusnya sebagai perpustakaan pihak ketiga.

Pendapat saya adalah bahwa mode "memerlukan semua bidang" yang ketat mungkin juga dapat membuatnya menjadi inti, itu adalah perilaku yang sangat jelas, dan saya dapat melihat mengapa itu berguna.

Saya tidak yakin bahwa "izinkan bidang menjadi opsional, tetapi ganti semuanya, gunakan default model jika ada" - Sepertinya itu akan menghadirkan beberapa perilaku yang sangat kontra-intuitif (mis. bidang "created_at", yang secara otomatis berakhir up memperbarui diri). Jika kita menginginkan perilaku yang lebih ketat, kita harus memiliki perilaku yang lebih ketat.

Either way, cara yang tepat untuk mendekati ini adalah dengan memvalidasinya sebagai paket pihak ketiga, lalu perbarui dokumen kami sehingga kami dapat menautkannya.

Atau, jika Anda yakin bahwa kami kehilangan perilaku dari inti yang benar-benar dibutuhkan pengguna kami, maka Anda dipersilakan untuk membuat permintaan tarik, memperbarui perilaku dan dokumentasi, sehingga kami dapat menilai manfaat dengan cara yang sangat cara konkrit.

Senang menerima permintaan tarik sebagai titik awal untuk ini, dan bahkan lebih bahagia untuk menyertakan paket pihak ketiga yang menunjukkan perilaku ini.

datang ke nilai dalam memiliki opsi perilaku PUT-is-strict.

Ini masih berdiri. Saya pikir kita dapat mempertimbangkan aspek itu sebagai inti, jika seseorang cukup peduli tentang hal itu untuk membuat permintaan tarik di sepanjang garis itu. Itu harus menjadi perilaku opsional.

Sepertinya itu akan menghadirkan beberapa perilaku yang sangat kontra-intuitif (mis. bidang "created_at", yang secara otomatis akhirnya memperbarui sendiri).

Bidang created_at harus read_only (atau dikecualikan dari serializer). Dalam kedua kasus ini, itu tidak akan berubah (perilaku serializer normal). Dalam kasus kontra-intuitif bahwa bidang tidak hanya-baca di serializer, Anda akan mendapatkan perilaku kontra-intuitif untuk mengubahnya secara otomatis.

Senang menerima permintaan tarik sebagai titik awal untuk ini, dan bahkan lebih bahagia untuk menyertakan paket pihak ketiga yang menunjukkan perilaku ini.

Sangat. Variasi "gunakan default" adalah kasus yang ideal untuk paket pihak ke-3 karena perubahannya adalah pembungkus sepele di sekitar (salah satu metode) perilaku yang ada dan (jika Anda membeli argumen default) berfungsi untuk semua serializer non-parsial.

tomchristie tutup ini 4 jam yang lalu

Mungkin Anda akan mempertimbangkan untuk menambahkan label seperti "PR Welcome" atau "3rd Party Plugin" dan membiarkan masalah yang valid/diakui seperti ini tetap terbuka. Saya sering mencari masalah terbuka untuk melihat apakah masalah telah dilaporkan dan kemajuannya menuju penyelesaian. Saya menganggap masalah tertutup sebagai "tidak valid" atau "diperbaiki". Menggabungkan beberapa masalah "valid tapi tertutup" ke dalam ribuan masalah tidak valid/perbaikan tidak mengundang pencarian yang efisien (bahkan jika Anda tahu mereka mungkin ada di sana).

Mungkin Anda akan mempertimbangkan untuk menambahkan label seperti "PR Welcome" atau "3rd Party Plugin"

Itu cukup masuk akal, tetapi kami ingin pelacak masalah kami mencerminkan pekerjaan yang aktif atau dapat ditindaklanjuti pada proyek itu sendiri.

Sangat penting bagi kami untuk mencoba menjaga agar masalah kami tetap tertutup rapat. Mengubah prioritas mungkin berarti bahwa kami terkadang memilih untuk membuka kembali masalah yang sebelumnya telah kami tutup. Saat ini saya pikir ini telah keluar dari "tim inti ingin mengatasi ini dalam waktu dekat".

Jika muncul berulang kali, dan terus tidak ada solusi pihak ketiga, maka mungkin kami akan menilai kembali.

membiarkan masalah yang valid/diakui seperti ini terbuka.

Sedikit lebih banyak konteks tentang gaya manajemen masalah - https://www.dababps.com/blog/sustainable-open-source-management/

Apakah halaman ini membantu?
0 / 5 - 0 peringkat