فیلد چندگزینه‌ای پویا در Django REST

سلام!اوّل از همه! این اوّلین پست من توی ویرگوله. و درست نمی‌دونم که چه جوری می‌شه باهاش کار کرد و چیزایی مثل سینتکس هایلایتینگ و... به قضیه اضافه کرد. حتّی اصلا نمی‌دونم چنین کاری ممکنه یا نه. :-بابابزرگبریم سراغ اصل مطلب!اگر کمی تجربه‌ی برنامه‌نویسی با جنگو داشته باشید، حتما می‌دونید که بعضی وقت‌ها ما توی مدل‌هامون یک فیلد داریم که می‌خوایم چند تا گزینه‌ی محدود داشته باشه. خب توی چنین شرایطی از آرگومان `choices` استفاده می‌کنیم.از اون جایی که مثال‌ها به درک مطلب خیلی کمک می‌کنن، بیاید با مدل «دانشجو» و فیلد «دانشگاه» مثال بزنیم!class Student(models.Model): first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) university = models.CharField( max_length=100, choices=( ('UT', _('University of Tehran')), ('SUT', _('Sharif University of Technology')), ), )خب... توی همین مثال، تصوّر کنید ما بخوایم لیست همه‌ی دانشگاه‌های کشور رو وسط تعریف مدل‌هامون اضافه کنیم... فکر می‌کنم شدّت کثافت(!) این روش بر کسی پوشیده نباشه.حالا ممکنه ایده بزنیم و بگیم خب اوکی این tuple مربوط به گزینه‌ها رو می‌بریم توی یه فایل دیگه تعریف می‌کنیم و این جا ایمپورتش می‌کنیم که کدمون تمیز بشه. این روش شاید در نگاه اوّل خیلی خوب و منطقی به نظر بیاد، ولی اگر یه روز ادمین سایت تصمیم بگیره که یه تغییری توی لیست دانشگاه‌ها اعمال کنه، باید بره کد منبع سایت رو ادیت کنه. پس فکر نمی‌کنم این هم روش جالبی باشه!توی این شرایط اوّلین راهی که به ذهنمون می‌رسه احتمالا اضافه کردن یک مدل «دانشگاه» به پایگاه داده‌مون هست. این طوری می‌تونیم یه لیست از دانشگاه‌ها رو به عنوان instanceهای این مدل ذخیره کنیم و فیلد university که توی مدل «دانشجو» داشتیم، تبدیل به یک کلید خارجی به اون مدل بشه. یعنی در عمل همچین کدی داشته باشیم:class University(models.Model): name = models.CharField(max_length=100, unique=True) class Student(models.Model): first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) university = models.ForeignKey(University, on_delete=models.SET_NULL, null=True)تا این جای کد به نظر خیلی خوب بوده. امّا تصوّر کنید می‌خوایم بریم سراغ serialize کردن Student. بدیهی‌ترین روش اینه که یه سریالایزر ساده بسازیم.class StudentSerializer(serializers.ModelSerializer): class Meta: model = Student fields = ['first_name', 'last_name', 'university']این روش به عنوان university، آیدی دانشگاه رو برمی‌گردونه نه اسم اون رو. این امر بعضی جاها خوبه! امّا بعضی جاها هم این برای ما مطلوب نیست. یک راه دم دستی که می‌تونه این جا استفاده بشه، اینه که توی پایگاه داده‌مون، Primary Key این مدل رو روی فیلد name ست کنیم.ولی باز هم ممکنه که بعضی جاها این کار مطلوب نباشه. یه روش دیگه هم که می‌شه بهش فکر کرد اینه که همچین کدی داشته باشیم برای سریالایزمون:class StudentSerializer(serializers.ModelSerializer): university = serializers.CharField(source='university.name') class Meta: model = Student fields = ['first_name', 'last_name', 'university']امّا توی این روش موقع اجرای متد save روی سریالایزرمون ممکنه به خاطر وجود `.` توی منبع فیلد university به مشکلی بخوریم.چندین و چند روش دیگه هم می‌شه ارائه داد برای حل این مشکلات. یکی از تمیزترین راه‌هایی که دیدم این هست که با متدهای to_internal_value و to_representation بازی کنیم!در مورد این مثال خاص دانشگاه، می‌شه چنین کاری کرد:class UniversityField(fields.Field): default_error_messages = { 'invalid': _('Not a valid university.'), 'nonstring': _('Not a string.'), } def run_validation(self, data=empty): if data is not None and not isinstance(data, str): self.fail('nonstring') return super().run_validation(data) def to_internal_value(self, data): u = University.objects.filter(name=data) if u.count() != 1: self.fail('invalid') return u.get() def to_representation(self, value): return value.nameیعنی یک فیلد می‌سازیم که قرار هست مدل دانشگاه رو برامون سریالایز کنه. این جا ما از Field ارث می‌بریم چون دانشگاه فقط و فقط یک مشخّصه‌ی `name` داره. اگر تا حالا این کار رو نکردید، پنیک نکنید! ارث بردن از Field هم مثل ارث‌بری از Serializer و ModelSerializer هست. اگر سورس کد فریم‌ورک جنگو رست رو بخونیم، می‌بینیم که ModelSerializer هم به طور غیرمستقیم از Field ارث‌بری می‌کنه حتّی!حالا سریالایزر Student رو به این شکل بازنویسی می‌کنیم:class StudentSerializer(serializers.ModelSerializer): university = UniversityField() class Meta: model = Student fields = ['first_name', 'last_name', 'university']این روش رو از این نظر می‌پسندم(!) که متدهای to_representation، to_internal_value و run_validation به طور اتوماتیک توسّط باقی متدها در حین validate کردن و serialize کردن و deserialize کردن و... صدا زده می‌شن و لازم نیست ما جایی کد اضافه‌ای بزنیم.البته دقّت کنید که چون ما فیلد دانشگاه رو می‌تونستیم با یه رشته مشخّص کنیم، برای سریالایز کردنش از کلاس Field ارث‌بری کردیم. فرض کنید فیلد چندگزینه‌ای‌مون، محل زندگی بود: یک شیء با ۳ فیلد شهر، استان و کشور. در این حالت باید برای سریالایز کردن محل زندگی، سریالایزری بسازیم که از ModelSerializer و یا Serializer ارث‌بری کنه.این مطلب همین جا تموم می‌شه! ولی اگر روش تمیزی برای حل چنین مسائلی داشتید، خوش‌حال می‌شم توی کامنت‌ها بگید.خوش بگذره! ;)