اذهب إلى المحتوى

Mostafa Almahmoud

الأعضاء
  • المساهمات

    6
  • تاريخ الانضمام

  • تاريخ آخر زيارة

آخر الزوار

لوحة آخر الزوار معطلة ولن تظهر للأعضاء

إنجازات Mostafa Almahmoud

عضو مبتدئ

عضو مبتدئ (1/3)

1

السمعة بالموقع

  1. جانغو هو إطار عمل ويب قوي يساعدك في إطلاق تطبيق بايثون Python أو موقعك على الويب بسرعة. ومع أنه يتضمن خادم تطوير مبسط لاختبار شيفرتك محليًّا، فإنك تحتاج إلى خادم ويب أقوى وأكثر أمانًا عندما يتعلق الأمر ولو قليلًا بعمليات الإنتاج أو الإطلاق الرسمي للتطبيقات أو المواقع. ستثبت وتضبط في هذه المقالة بعض المكونات على أوبنتو 18.04 لدعم وخدمة تطبيقات جانغو، وتجهّز قاعدة بيانات PostgreSQL بدلًا من قاعدة بيانات SQLite الافتراضية. بعد ذلك، ستضبط خادم تطبيقات غوني كورن للربط مع تطبيقاتك، ثم في الختام ستجهز إنجن إكس ليعمل مثل خادم وكيل لغوني كورن مما يعطيك وصولًا إلى مزاياه الأمنية ومزايا الأداء الخاصة التي يتصف بها لخدمة تطبيقاتك. المتطلبات الأساسية ستحتاج قبل المتابعة في هذه المقالة إلى خادم أوبنتو 18.04 مُعد وجاهز وإلى مستخدم غير جذر يتمتع بصلاحيات "sudo" مهيأة وإلى جدار ناري مُفعّل. راجع دليل إعداد خادم أوبنتو 18.04 مبدئي للبدء. الخطوة الأولى - تثبيت الحزم البرمجية من مستودعات أوبنتو ابدأ بتنزيل وتثبيت جميع العناصر التي تحتاجها من مستودعات أوبنتو. استخدم مدير حزم بايثون pip لتثبيت مكونات إضافية لاحقة. حدّث في البداية دليل الحزم apt المحلي: $ sudo apt update ثم ثبّت الحزم بحسب إصدار بايثون الذي يستخدمه مشروعك، فإذا كنت تستخدم جانغو مع بايثون 3 فنفذ الأمر التالي: $ sudo apt install python3-pip python3-dev libpq-dev postgresql postgresql-contrib nginx curl ننبه إلى أن جانغو 1.11 هو آخر إصدار من جانغو يدعم بايثون 2. لذلك، إذا كنت ستتفتح مشاريع جديدة، فننصحك باختيار بايثون 3، لكن إذا كنت ما تزال تريد استخدام بايثون 2 فنفّذ الأمر التالي: $ sudo apt install python-pip python-dev libpq-dev postgresql postgresql-contrib nginx curl سيثبت هذا pip وملفات تطوير بايثون اللازمة لبناء غوني كورن لاحقًا، وسيثبت أيضًا نظام قواعد بيانات Postgres والمكتبات اللازمة للتفاعل معها وخادم الويب إنجن إكس. الخطوة الثانية - إنشاء قاعدة بيانات PostgreSQL ومستخدم لها ستنشئ في هذه الخطوة قاعدة بيانات مع مستخدم قاعدة بيانات لتطبيق جانغو الخاص بك مع PostgreSQL -أو اختصارًا Postgres. تستخدم Postgres افتراضيًا مخطط استيثاق يُطلق عليه اسم استيثاق النظير peer authentication في حالة الاتصالات المحلية، والذي يعني أنه إذا طابق اسم مستخدم نظام التشغيل للمستخدم اسم مستخدم Postgres صالح، فيمكن لمستخدم نظام التشغيل تسجيل الدخول إلى قاعدة البيانات دون أي شروط أمنية أخرى. أُنشئ أثناء تثبيتك لنظام Postgres مستخدم نظام تشغيل اسمه "postgres" ليكون نظيرًا للمستخدم الإداري لنظام PostgreSQL الذي اسمه "postgres"، إذ ستحتاج لحساب هذا المستخدم لإنجاز المهام الإدارية. سجل الدخول إلى جلسة Postgres تفاعلية باستخدام sudo ومرّر اسم المستخدم مستعملًا الخيار u-: $ sudo -u postgres psql سيظهر لك محث PostgreSQL، إذ يمكنك إعداد متطلباتك. أنشئ أولًا قاعدة بيانات لمشروعك: postgres=# CREATE DATABASE myproject; ملاحظة: يجب أن تنتهي كل تعليمة Postgres بفاصلة منقوطة، لذا تأكد من أنك طبقت هذه القاعدة إذا واجهت أي مشاكل. أنشئ مستخدم قاعدة بيانات مشروعك واختر كلمة مرور قوية: postgres=# CREATE USER myprojectuser WITH PASSWORD 'password'; وسيكون الخرج على النحو التالي: CREATE ROLE ستعدّل الآن بعض معاملات الاتصال للمستخدم الذي أنشأته، والهدف من هذه التعديلات هو تسريع عمليات قاعدة البيانات بحيث لا يبقى حاجة إلى الاستعلام عن القيم الصحيحة ووضع قيمها في كل مرة نحتاج فيها إلى الاتصال بقاعدة البيانات. بدايةً اضبط قيمة الترميز الافتراضي default encoding إلى "UTF-8" التي يتوقعها جانغو: postgres=# ALTER ROLE myprojectuser SET client_encoding TO 'utf8'; وسيكون الخرج على النحو التالي: ALTER ROLE ثم حدّد مخطط عزل الإجراءات الافتراضي بالقيمة "read committed" لمنع عمليات القراءة من المعاملات التي لم تودع بعد uncommitted: postgres=# ALTER ROLE myprojectuser SET default_transaction_isolation TO 'read committed'; وسيكون الخرج على النحو التالي: ALTER ROLE نظرًا لأن مشاريع جانغو ستُضبط على استخدام النطاق الزمني "UTC" افتراضيًّا، لذا حدد النطاق الزمني المناسب: postgres=# ALTER ROLE myprojectuser SET timezone TO 'UTC'; وسيكون الخرج على النحو التالي: ALTER ROLE كل هذه طبعًا توصيات من مشروع جانغو. الآن، اِمنح مستخدمك الجديد وصولًا إلى قاعدة بياناتك الجديدة ليتمكن من إدارتها: postgres=# GRANT ALL PRIVILEGES ON DATABASE myproject TO myprojectuser; وسيكون الخرج على النحو التالي: GRANT وبعد أن تنتهي، غادر محث PostgreSQL بتنفيذ الأمر التالي: postgres=# \q وبهذا يكون POstgreSQL الآن معدًّا بنجاح بحيث يمكن لجانغو الاتصال بمعلومات قاعدة البيانات وإدارتها. الخطوة الثالثة - إنشاء بيئة بايثون افتراضية لمشروعك الآن وقد أصبح لديك قاعدة بياناتك، يمكنك البدء في تجهيز بقية متطلبات مشروعك. يستلزم هذا تثبيت متطلبات بايثون ضمن بيئة افتراضية لكي تكون الإدارة أكثر فاعلية، إذ سيسمح تثبيت جانغو داخل بيئة افتراضية مخصصة لمشروعك بمعالجة كلٌّ مشروع من مشاريعك ومتطلباته بمعزل عن المشاريع الأخرى. ستحتاج لإنجاز ذلك إلى وصول إلى الأمر virtualenv الذي ينشئ البيئات الافتراضية. ابدأ بتثبيت هذا الأمر مستخدمًا pip، وإذا كنت تستخدم بايثون 3 فلا بُد من ترقية pip: $ sudo -H pip3 install --upgrade pip ثم ثبّت الحزمة: $ sudo -H pip3 install virtualenv إذا كنت تستخدم بايثون 2 فلا بُد من ترقية pip: $ sudo -H pip install --upgrade pip ثم ثبّت الحزمة: $ sudo -H pip install virtualenv مع تثبيت virtualenv، يمكنك البدء في تشكيل مشروعك. أنشئ أولًا المجلد الذي ستحفظ فيه ملفات مشروعك. سنسميه نحن هنا "myprojectdir" ويمكنك تسميته ما تشاء. $ mkdir ~/myprojectdir ثم انتقل إلى داخل المجلد: $ cd ~/myprojectdir ثم أنشئ بيئة بايثون الافتراضية داخل مجلد المشروع: $ virtualenv myprojectenv سينشئ هذا مجلدًا اسمه "myprojectenv" ضمن مجلد "myprojectdir"، وسيثبت داخله نسخةً محليةً من بايثون ونسخةً محليةً من pip. يمكنك استخدام هذا لتثبيت وضبط بيئة بايثون تعزل مشروعك عن بقية المشاريع. لكن قبل تثبيت متطلبات بايثون لمشروعك ستحتاج إلى تفعيل البيئة الافتراضية: $ source myprojectenv/bin/activate يجب أن يتغير المحث الآن ليشير إلى أنك الآن تعمل ضمن بيئة بايثون افتراضية. سيبدو هذا مشابهًا لما يلي: (myprojectenv)user@host:~/myprojectdir$ وهكذا بعد أن أصبحت بيئتك الافتراضية فعالة، ثبّت جانغو وغوني كورن وموائم PostgreSQL adaptor، اسمه psycopg2 بالنسخة المحلية من pip: pip install django gunicorn psycopg2-binary ملاحظة::عندما تُفعّل البيئة الافتراضية، أي عندما يُسبق المحث باسم المجلد "(myprojectenv)"، استخدم pip بدلًا من pip3 حتى لو كنت تستخدم بايثون 3. تُسمى نسخة الأداة للبيئة الافتراضية دائمًا باسم "pip"، بغض النظر عن نسخة بايثون. الآن أصبح لديك كل البرمجيات اللازمة للبدء في مشروع جانغو. الخطوة الرابعة - إنشاء وضبط مشروع جانغو جديد بعد أن ثُبّتت مكونات بايثون، أصبح بإمكانك إنشاء ملفات مشروع جانغو الفعلية، وبما أن مجلد المشروع أصبح جاهزًا فيمكنك أن تطلب من جانغو أن يثبت الملف هنا، إذ سينشئ عندئذٍ مجلدًا في المستوى الثاني مع الشيفرة الفعلية وهذا أمرٌ طبيعي وسيضع سكريبت إدارة في هذا المجلد؛ وهذا مهمٌّ أنك تعرّف المجلد صراحةً بدلًا من السماح لجانغو باتخاذ قرارات متعلقة بمجلدك الحالي: (myprojectenv) $ django-admin.py startproject myproject ~/myprojectdir في هذه النقطة ستكون محتويات مجلد المشروع (في حالتنا هو "myprojectdir/~") هي: "myprojectdir/manage.py/~": سكريبت إدارة مشروع جانغو. "/myprojectdir/myproject/~": حزمة مشروع جانغو. سيحتوي هذا على الملفات: "init__.py__" و "settings.py" و "urls.py" و "wsgi.py". "/myprojectdir/myprojectenv/~": مجلد البيئة الافتراضية الذي أنشأته من قبل. تتمثل المرحلة التالية في ضبط الإعدادات لملفات المشروع الجديدة. افتح ملف الإعدادات بمحرر النصوص المفضل لديك. سنستخدم نحن محرر النصوص نانو nano: (myprojectenv) $ nano ~/myprojectdir/myproject/settings.py اعثر أولًا على الموجّه ALLOWED_HOSTS الذي يعرّف قائمةً بعناوين الخوادم، أو أسماء النطاقات التي ستُستخدمُ للاتصال بنسخة جانغو، وسيظهر استثناء exception إذا وردت أي طلبات قادمة تحتوي على ترويسة Host غير مذكورة ضمن القائمة، ولذا يشدّد جانغو على تحديد هذا تفاديًا لصنف معين من الثغرات الأمنية. أدرِج داخل الأقواس المربعة عناوين IP أو أسماء النطاقات المترافقة مع خادم جانغو ضمن علامتي اقتباس مع الفصل بين كل عنصرين بفاصلة. إذا كنت تفضّل طلبات نطاق كامل وأي نطاقات فرعية، ضع نقطةً في بداية الإدخال. ألقِ نظرةً على الشيفرة التالية التي تحتوي على بعض الأمثلة الموضوعة على هيئة تعليقات بهدف التوضيح: . . . # The simplest case: just add the domain name(s) and IP addresses of your Django server # ALLOWED_HOSTS = [ 'example.com', '203.0.113.5'] # To respond to 'example.com' and any subdomains, start the domain with a dot # ALLOWED_HOSTS = ['.example.com', '203.0.113.5'] ALLOWED_HOSTS = ['your_server_domain_or_IP', 'second_domain_or_IP', . . ., 'localhost'] ملاحظة: تأكد من تضمين "localhost" ضمن الخيارات، لأنك ستحيلُ proxying الاتصالات من خلال نسخة إنجن إكس محلية. ثم أوجد القسمَ الذي يضبط الوصول إلى قاعدة البيانات، وهو يبدأ بالكلمة DATABASES.الضبط الموجود مسبقًا في الملف هو لقاعدة بيانات SQLite، ولكن نظرًا لأنك أنشأت قاعدة بيانات PostgreSQL لمشروعك فلا بُد من ضبط هذه الإعدادات. حدّث الإعدادات بمعلومات قاعدة بيانات PostgreSQL، ثم أخبر جانغو أن يستخدم الملائم psycopg2 الذي ثبتته باستخدام pip. تحتاج أيضًا إلى التزويد باسم قاعدة البيانات واسم المستخدم لمستخدم قاعدة البيانات الذي أنشأته توًّا وكلمة المرور لمستخدم قاعدة البيانات والتحديد أيضًا بإمكانية العثور على قاعدة البيانات على الحاسوب المحلي، أما الإعداد PORT فيمكنك تركه سلسلةً فارغة: . . . DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'myproject', 'USER': 'myprojectuser', 'PASSWORD': 'password', 'HOST': 'localhost', 'PORT': '', } } . . . ثم اذهب إلى نهاية الملف وأضف إعدادًا يشير إلى المكان الذي ينبغي أن توضع فيه الملفات الساكنة؛ وهذا ضروري ليتمكن إنجن إكس من معالجة الطلبات لتلك العناصر. يخبر السطر التالي جانغو أن يضع تلك الملفات الساكنة في مجلدٍ اسمه "static" في مجلد المشروع الأساسي: . . . STATIC_URL = '/static/' import os STATIC_ROOT = os.path.join(BASE_DIR, 'static/') احفظ الملف ثم أغلقه عندما تنتهي، وإذا كنت تستخدم محرر النصوص نانو يمكنك فعل ذلك بالضغط على المفتاحين "CTRL + X"، ثم المفتاح "Y و "ENTER". الخطوة الخامسة - إكمال الإعداد المبدئي للمشروع ستكون الخطوة التالية تهجير مخطط قاعدة البيانات المبدئي إلى قاعدة بيانات PostgreSQL باستخدام سكريبت الإدارة التالي: (myprojectenv) $ ~/myprojectdir/manage.py makemigrations (myprojectenv) $ ~/myprojectdir/manage.py migrate أنشئ مستخدمًا إداريًّا للمشروع: (myprojectenv) $ ~/myprojectdir/manage.py createsuperuser سيطلب منك اسم مستخدم وبريد إلكتروني وكلمة مرور مع تأكيدها. اجمع كل المحتوى الساكن في موقع المجلد الذي ضبطته بتنفيذ الأمر التالي: (myprojectenv) $ ~/myprojectdir/manage.py collectstatic عندئذ ستوضع الملفات الساكنة في مجلد اسمه "static" ضمن مجلد المشروع. إذا اتبعت دليل إعداد الخادم المبدئي فيفترض أن لديك جدار ناري UFW يحمي خادمك، ولكي تختبر خادم التطوير، ينبغي عليك السماح بالوصول إلى المنفذ الذي ستستخدمه. في هذه الحالة أنشئ استثناءً للمنفذ "8000": (myprojectenv) $ sudo ufw allow 8000 أخيرًا افحص مشروعك بتشغيل خادم تطوير جانغو من خلال الأمر التالي: (myprojectenv) $ ~/myprojectdir/manage.py runserver 0.0.0.0:8000 في متصفح الويب استعرض اسم نطاق الخادم أو عنوان IP متبوعًا برقم المنفذ "8000:"، كما يلي: http://server_domain_or_IP:8000 عندئذ ستظهر لك الصفحة التالية وهي صفحة دليل جانغو الافتراضية: إذا ألحقت "admin/" بنهاية عنوان URL في شريط العنوان، فسيُطلب منك اسم المستخدم وكلمة المرور الذين أنشأتهما بالأمر createsuperuser: يمكنك بعد الاستيثاق الوصول إلى واجهة جانغو الإدارية الافتراضية: بعد الانتهاء من استكشاف الواجهة الظاهرة أمامك، اضغط على المفتاحين "CTRL + C" في نافذة الطرفية لإيقاف تشغيل خادم التطوير. الخطوة السادسة - اختبار قدرة غوني كورن على خدمة المشروع اختبر الخادم غوني كورن قبل مغادرة البيئة الافتراضية للتأكد من أن بإمكانه أن يخدم التطبيق، وذلك من خلال الانتقال أولًا إلى داخل مجلد مشروعك: (myprojectenv) $ cd ~/myprojectdir ثم شغّل gunicorn ليحمّل وحدة WSGI للمشروع: (myprojectenv) $ gunicorn --bind 0.0.0.0:8000 myproject.wsgi سيشغل هذا الأمر خادم غوني كورن على نفس الواجهة التي كان يعمل عليها خادم تطوير جانغو. يمكنك العودة واختبار التطبيق مجددًا. ملاحظة: لن يكون في الواجهة الإدارية أي أنماط مُطبّقة، لأن الخادم غوني كورن لا يعرف كيف يعثر على محتوى CSS الساكن المسؤول عن هذا الأمر. لنلخص ما أنُجز، فقد مرّرتَ وحدةً إلى الخادم غوني كورن بتحديد مسار المجلد النسبي إلى ملف جانغو المسمى "wsgi.py" والذي يمثّل نقطة الدخول إلى تطبيقك باستخدام بنية وحدة بايثون. يُستخدم الخادم غوني كورن مثل واجهة لتطبيقك ويترجم طلبات العملاء من نوع HTTP إلى استدعاءات بايثون يمكن لتطبيقك معالجتها. أما داخل هذا الملف، فقد عُرّفت دالة application، التي تُستخدم للتواصل مع تطبيقك. وبإمكانك معرفة المزيد عن مواصفات WSGI إذا كنت مهتمًا بهذا الموضوع. بعد أن تفرغ من الاختبار، اضغط على المفتاحين "CTRL + C" في نافذة الطرفية لإيقاف تشغيل الخادم غوني كورن، وبهذا تكون انتهيت من ضبط تطبيق جانغو وبإمكانك إلغاء تفعيل بيئتك الافتراضية: (myprojectenv) $ deactivate سيُحذف مؤشر البيئة الافتراضية في المحثّ الظاهر أمامك. الخطوة السابعة - إنشاء مقبس systemd وملفات خدمة لخادم غوني كورن الآن بعد أن اختبرت وتأكدت أن بإمكان الخادم غوني كورن التفاعل مع تطبيقك المنشأ بجانغو، يجب أن تنفذ طريقةً أقوى في تشغيل وإطفاء خادم التطبيقات. ستنشئ من أجل هذا خدمة systemd وملفات مقبس. سينُشأ مقبس غوني كورن عند الإقلاع ويبدأ بالاستماع للاتصالات، وعندما يحدث اتصال ستشغِّل خدمة systemd تلقائيًّا عملية غوني كورن لمعالجة الاتصال. ابدأ بإنشاء وفتح ملف مقبس systemd لخادم غوني كورن مع صلاحيات sudo في محرر النصوص المفضل لديك: $ sudo nano /etc/systemd/system/gunicorn.socket أنشئ داخل الملف القسم [Unit] لوصف المقبس والقسم [Socket] لتعريف موقع المقبس والقسم [Install] للتأكد من أن المقبس يُنشأ في الوقت المناسب: [Unit] Description=gunicorn socket [Socket] ListenStream=/run/gunicorn.sock [Install] WantedBy=sockets.target احفظ الملف وأغلقه عندما تنتهي، ثم أنشئ وافتح ملف خدمة systemd من أجل غوني كورن مع صلاحيات sudo في محرر النصوص المفضّل لديك، ويجب أن يطابق اسم ملف الخدمة اسم ملف المقبس باستثناء الامتداد: $ sudo nano /etc/systemd/system/gunicorn.service ابدأ بالقسم [Unit] المُستَخدم لتحديد البيانات الوصفية metadata والاعتماديات. أضف وصفًا لخدمتك هنا وأخبر نظام التهيئة "init system" أن يشغّل الخدمة فقط بعد الوصول إلى مستوى التشبيك المستهدف networking target. نظرًا لأن خدمتك تعتمد على المقبس من ملف المقبس، فستحتاج إلى تضمين الموجّه Requires للإشارة إلى تلك العلاقة: [Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target ثم أضف القسم [Service] وحدّد المستخدم والمجموعة اللذين تريد أن تعمل العملية تحتهما. زوّد ملكيتك للعملية من خلال حساب مستخدم نظامي لأنها تمتلك كل الملفات المرتبطة، ثم زوّد ملكية مجموعتك إلى مجموعة "www-data" ليتمكن الخادم إنجن إكس من التواصل مع خادم غوني كورن. بعد ذلك، ارسم خطة المجلد العامل وحدّد الأمر الذي ستنفّذه لتشغيل العملية، وفي حالتنا هذه حدّد المسار الكامل للملف التنفيذي للخادم غوني كورن المثبت ضمن بيئتك الافتراضية، ثم اربط العملية بمقبس يونيكس Unix الذي أنشأته ضمن المجلد "run/" لكي تتمكن العملية من التواصل مع خادم إنجن إكس. سجّل كافة البيانات على أداة الخرج المعياري لكي تتمكن العملية journald من جمع سجلات الخادم غوني كورن. يمكنك أيضًا هنا تحديد أي إضافات اختيارية لخادم غوني كورن، إذ حدّدنا في مثالنا ثلاث عمليات عاملة على النحو التالي: [Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target [Service] User=sammy Group=www-data WorkingDirectory=/home/sammy/myprojectdir ExecStart=/home/sammy/myprojectdir/myprojectenv/bin/gunicorn \ --access-logfile - \ --workers 3 \ --bind unix:/run/gunicorn.sock \ myproject.wsgi:application أخيرًا، أضف القسم [Install] الذي سيخبر systemd بماذا ستربط هذه الخدمة إذا فعّلتها للعمل عند الإقلاع، إذ أن الأفضل أن تعمل هذه الخدمة عندما يكون نظام المستخدمين المتعددين النظامي يعمل على النحو الصحيح والمطلوب: [Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target [Service] User=sammy Group=www-data WorkingDirectory=/home/sammy/myprojectdir ExecStart=/home/sammy/myprojectdir/myprojectenv/bin/gunicorn \ --access-logfile - \ --workers 3 \ --bind unix:/run/gunicorn.sock \ myproject.wsgi:application [Install] WantedBy=multi-user.target وبهذا يكتمل ملف خدمة systemd، والآن احفظ الملف وأغلقه، ثم شغّل مقبس غوني كورن. سينشئ هذا ملف المقبس في المسار "run/gunicorn.sock/" الآن وكذلك عند الإقلاع: $ sudo systemctl start gunicorn.socket ثم فعّله بحيث تشغّل systemd تلقائيًا خدمة gunicorn.service عندما يحدث اتصال مع ذاك المقبس لتتولى معالجته: $ sudo systemctl enable gunicorn.socket يمكنك التحقق أن العملية نجحت من خلال التحقق من وجود ملف المقبس، وهذا موضوع الخطوة القادمة. الخطوة الثامنة - التحقق من وجود ملف مقبس غوني كورن افحص حالة العملية لتعرف ما إذا كانت قابلة للتشغيل: $ sudo systemctl status gunicorn.socket وسيكون الخرج على النحو التالي: ● gunicorn.socket - gunicorn socket Loaded: loaded (/etc/systemd/system/gunicorn.socket; enabled; vendor prese> Active: active (listening) since Thu 2021-12-02 19:58:48 UTC; 14s ago Triggers: ● gunicorn.service Listen: /run/gunicorn.sock (Stream) Tasks: 0 (limit: 1136) Memory: 0B CGroup: /system.slice/gunicorn.socket Dec 02 19:58:48 gunicorn systemd[1]: Listening on gunicorn socket. ثم تحقق من وجود الملف "gunicorn.sock" ضمن المجلد "run/": $ file /run/gunicorn.sock وسيكون الخرج على النحو التالي: /run/gunicorn.sock: socket إذا أشار الأمرsystemctl status إلى حدوث خطأ، أو إذا لم تجد الملف "gunicorn.sock" في المجلد فإن هذه إشارة أننا لم ننجح في إنشاء مقبس غوني كورن. تفقّد سجلات مقبس غوني كورن بتنفيذ الأمر التالي: $ sudo journalctl -u gunicorn.socket افحص الملف "etc/systemd/system/gunicorn.socket/" لإصلاح أي مشاكل قبل المتابعة. الخطوة التاسعة - فحص تنشيط المقبس إذا شغلت فقط الوحدة gunicorn.socket، لن تكون الخدمة gunicorn.service نشطةً لأن المقبس لن يكون قد استقبل أي اتصالات. لذلك افحص الحالة: $ sudo systemctl status gunicorn وسيكون الخرج على النحو التالي: ● gunicorn.service - gunicorn daemon Loaded: loaded (/etc/systemd/system/gunicorn.service; disabled; vendor preset: enabled) Active: inactive (dead) لفحص آلية تنشيط المقبس، أرسل اتصالًا إلى المقبس عبر curl: $ curl --unix-socket /run/gunicorn.sock localhost يفترض أن تتلقى خرج HTML من تطبيقك في الطرفية، وهذا يؤكد إقلاع خادم غوني كورن وأنه قادر على خدمة تطبيق جانغو الخاص بك. يمكنك التحقق من أن خدمة غوني كورن تعمل من خلال فحص الحالة مرة أخرى: $ sudo systemctl status gunicorn وسيكون الخرج على النحو التالي: ● gunicorn.service - gunicorn daemon Loaded: loaded (/etc/systemd/system/gunicorn.service; disabled; vendor preset Active: active (running) since Tue 2021-11-23 22:55:12 UTC; 11s ago Main PID: 11002 (gunicorn) Tasks: 4 (limit: 1151) CGroup: /system.slice/gunicorn.service ├─11002 /home/sammy/myprojectdir/myprojectenv/bin/python /home/sammy/ ├─11018 /home/sammy/myprojectdir/myprojectenv/bin/python /home/sammy/ ├─11020 /home/sammy/myprojectdir/myprojectenv/bin/python /home/sammy/ └─11022 /home/sammy/myprojectdir/myprojectenv/bin/python /home/sammy/ . . . إذا أشار الخرج من جرّاء تنفيذ curl أو الخرج من systemctl status إلى أن مشكلةً قد وقعت، افحص سجلات التفاصيل الإضافية: $ sudo journalctl -u gunicorn افحص الملف "etc/systemd/system/gunicorn.service/" بحثًا عن المشاكل، وإذا أجريت تغييرات على الملف "etc/systemd/system/gunicorn.service/"، فأعد تحميل العملية الخفية daemon لإعادة قراءة تعريف الخدمة: $ sudo systemctl daemon-reload ثم أعد تشغيل خدمة غوني كورن: $ sudo systemctl restart gunicorn إذا ظهرت أي مشاكل مثل هذه، فأصلحها قبل المتابعة. الخطوة العاشرة - ضبط الخادم إنجن إكس لتمرير الخادم الوكيل إلى خادم غوني كورن الآن بعد إعداد غوني كورن، ستضبط الخادم إنجن إكس لتمرير حركة البيانات في الشبكة إلى العملية. ابدأ بإنشاء وفتح كتلة خادم جديدة في مجلد إنجن إكس المسمى "sites-available" كما يلي: $ sudo nano /etc/nginx/sites-available/myproject افتح داخله كتلة خادم جديدة، وابدأ بتحديد أن هذه الكتلة ينبغي أن تستمع على المنفذ الطبيعي 80 وأن تستجيب لاسم نطاق خادمك أو عنوان IP الخاص به: server { listen 80; server_name server_domain_or_IP; } ثم اطلب من الخادم إنجن إكس أن يتجاهل أي مشاكل متعلقة بإيجاد الأيقونة المفضلة favicon، وأخبره أيضًا أين يجد الأصول الساكنة التي جمعتها في مجلدك المسمى "myprojectdir/static/~". تمتلك جميع هذه الملفات سابقة URI معيارية بقيمة "static/"، لذا يمكنك إنشاء كتلة موقع لمطابقة تلك الطلبات: server { listen 80; server_name server_domain_or_IP; location = /favicon.ico { access_log off; log_not_found off; } location /static/ { root /home/sammy/myprojectdir; } } أخيرًا، أنشئ كتلة location / {}‎ لمطابقة كافة الطلبات الأخرى، وضمّن داخلَ هذا الموقع الملفَّ المعياري "proxy_params" من تثبيت إنجن إكس، ثم حدّد حركة مرور بيانات الشبكة مباشرةً إلى مقبس غوني كورن: server { listen 80; server_name server_domain_or_IP; location = /favicon.ico { access_log off; log_not_found off; } location /static/ { root /home/sammy/myprojectdir; } location / { include proxy_params; proxy_pass http://unix:/run/gunicorn.sock; } } احفظ الملف وأغلقه عند الانتهاء. يمكنك الآن تفعيل الملف بربطه مع المجلد "sites-enabled": $ sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled أجرِ اختبارًا لتتأكد من خلو ضبط الخادم إنجن إكس من الأخطاء الكتابية: $ sudo nginx -t إذا لم تظهر أي أخطاء، فتابع وأعد تشغيل خادم إنجن إكس: $ sudo systemctl restart nginx ونظرًا لأنك لا تحتاج بعد الآن إلى وصول إلى خادم التطوير، اضبط إعدادات الجدار الناري بحذف قاعدة المنفذ المفتوح "8000": $ sudo ufw delete allow 8000 ثم اسمح لبيانات الشبكة العادية بالمرور من المنفذين "80" و"443" مما سيسمح بحدوث اتصالات HTTP و HTTPS على التوالي، وذلك باستخدام الأمر التالي: $ sudo ufw allow 'Nginx Full' يُفترض أن يكون بمقدورك الآن الذهاب إلى نطاق خادمك أو عنوان IP الخاص به لمشاهدة تطبيقك. ملاحظة: بعد الانتهاء من ضبط إنجن إكس ستكون الخطوة التالية هي تأمين بيانات الشبكة إلى الخادم باستخدام بروتوكول SSL/TLS، وهذا مهم لأنه بدونه ستُرسل جميع المعلومات بما فيها كلمات المرور عبر الشبكة بصيغة نصيّةٍ صرفة plain text. إذا كان لديك اسم نطاق فأسرعُ طريقة للحصول على شهادة SSL لتأمين بيانات الشبكة هو استخدام Let's Encrypt. يمكنك إعداده من خلال متابعة دليلنا كيف تؤمن Let's Encrypt باستخدام إنجن إكس على نظام أبونتو 18.04. اتبع الدليل باستخدام كتلة خادم إنجن إكس التي أنشأتها في هذا الدليل. إذا لم يكن لديك اسم نطاق، فلا يزال بإمكانك تأمين موقعك للاختبار والتعلم باستخدام self-signed SSL Certificate، ونشدّد على اتباع العملية باستخدام كتلة خادم إنجن إكس التي أنشأتها في هذه المقالة. الخطوة الحادية عشرة - استكشاف المشاكل وإصلاحها في خادمي إنجن إكس وغوني كورن إذا كنت تواجه مشكلات في تطبيقك، فقد أضفنا الأقسام التالية لتدلك على طرق إصلاح مشكلات التثبيت. يظهر الخادم إنجن إكس الصفحة الافتراضية بدلا من إظهار تطبيق جانغو إذا كان إنجن إكس يعرض الصفحة الافتراضية بدلًا من إحالة الخادم الوكيل إلى تطبيقك، فذلك يعني غالبًا أنك بحاجة إلى ضبط server_name ضمن الملف "etc/nginx/sites-available/myproject/" ليشير إلى عنوان IP لخادمك، أو اسم نطاقه. يستخدم إنجن إكس الموجه server_name ليحدد كتلة الخادم التي سيستخدمها للاستجابة للطلبات؛ فإذا كانت تظهر لك صفحة إنجن إكس الافتراضية ففي هذا دلالة على الخادم إنجن إكس لم يستطع مطابقة الطلب مع كتلة الخادم صراحةً لذا تراجع إلى الكتلة الافتراضية المعرّفة في الملف "etc/nginx/sites-available/default/". يجب أن يكون الموجه server_name في كتلة خادم المشروع أكثر تحديدًا من الموجّه المُختار في كتلة الخادم الافتراضية. يعرض الخادم إنجن إكس الخطأ 502 Bad Gateway بدلا من عرضه تطبيق جانغو يشير الخطأ 502 إلى أن الخادم إنجن إكس لم ينجح في إحالة الطلب مثل خادم وكيل. وهناك عدد كبير من الأخطاء التي تعبر عن نفسها بالخطأ 502، لذا ستحتاج إلى معلومات أكثر لتصل إلى الخطأ الفعلي وتصلحه. والمكان الأساسي الذي يجب أن تبحث فيه عن مزيد من المعلومات هو سجل أخطاء إنجن إكس؛ فهو يخبرنا عمومًا عن الظروف التي سببت الخطأ أثناء حدث الإحالة من الخادم الوكيل Proxying. تفقد سجل أخطاء الخادم إنجن إكس بالتعليمة التالية: $ sudo tail -F /var/log/nginx/error.log والآن أسس طلبًا آخرًا في متصفحك لتوليد خطأ جديد (حاول إنعاش refresh الصفحة). يُفترض أن تتلقى رسالة خطأ جديدة مسجلة في السجل. إذا قرأت الرسالة وفهمتها فستضيّق نطاق المشكلة. ربما تتلقى رسالة تشبه ما يلي: connect() to unix:/run/gunicorn.sock failed (2: No such file or directory) التي تشير إلى أن إنجن إكس لم يستطع العثور على الملف "gunicorn.sock" في الموقع المُعطى. ينبغي مقارنة الموقع "proxy_pass" المعرف ضمن الملف "etc/nginx/sites-available/myproject/" مع الموقع الفعلي للملف "gunicorn.sock" الذي ولدته وحدة systemd المسماة "gunicorn.socket". إذا لم تتمكن من العثور على الملف "gunicorn.sock" ضمن المجلد "run/"، فهذا يعني عمومًا أن ملف مقبس systemd لم يتمكن من إنشائه. عُد إلى القسم من مقالتنا هذه الذي يتكلم عن البحث عن ملف مقبس غوني كورن للسير وفق خطوات الإصلاح لمشكلات الخادم غوني كورن. وقد تقرأ رسالةً أخرى تشبه التالية: connect() to unix:/run/gunicorn.sock failed (13: Permission denied) التي تشير هذه إلى أن الخادم إنجن إكس لم يتمكن من الاتصال بمقبس غوني كورن بسبب مشكلات تتعلق بالأذونات، ويُحتمل أن يحدث هذا عندما يُتبع الإجراء باستخدام المستخدم الجذر "root" بدلًا من المستخدم "sudo". وقد تدل الرسالة إلى أنه في الوقت الذي تستطيع systemd إنشاء ملف مقبس غوني كورن، لم يتمكن الخادم إنجن إكس من الوصول إليه. يمكن لهذا أن يحدث إذا كانت هناك محدودية في الأذونات في أي نقطة بين المجلد الجذر والملف "gunicorn.sock"، ويمكنك استعراض الأذونات وقيم الملكية لملف المقبس وكل من المجلدات الآباء له بتمرير المسار المطلق إلى ملف المقبس الخاص بك إلى الأمر namei: $ namei -l /run/gunicorn.sock وسيكون الخرج على النحو التالي: f: /run/gunicorn.sock drwxr-xr-x root root / drwxr-xr-x root root run srw-rw-rw- root root gunicorn.sock يعرض الخرج أذونات كل مكون من مكونات المجلد، ويمكنك تصور نوع الوصول المسموح إلى ملف المقبس باستعراض الأذونات(العمود الأول) والمالك (العمود الثاني) ومالك المجموعة(العمود الثالث). يملك ملف المقبس في هذا المثال وكل مجلد من المجلدات التي تؤدي إليه أذونات قراءة read وتنفيذ execute (ينتهي عمود الأذونات للمجلدات بالامتداد r-x بدلًا من ---). يجب أن تكون العملية إنجن إكس قادرة على الوصول إلى المقبس بنجاح. إذا لم يمتلك أي من المجلدات التي تؤدي إلى المقبس إذن قراءة أو تنفيذ عامة، فلن يتمكن الخادم إنجن إكس من الوصول إلى المقبس دون السماح بأذونات قراءة أو تنفيذ عامة أو بإعطاء ملكية المجموعة إلى مجموعة ينتمي إليها الخادم إنجن إكس. جانغو يعرض الرسالة could not connect to server: Connection refused من الرسائل التي يمكن أن تتلقاها عند محاولة الوصول إلى أجزاء من التطبيق في متصفح الويب هي: OperationalError at /admin/login/ could not connect to server: Connection refused Is the server running on host "localhost" (127.0.0.1) and accepting TCP/IP connections on port 5432? التي يشير هذا إلى أن جانغو لم يتمكن من الاتصال بقاعدة بيانات Postgres. لذا تأكد أن نسخة Postgres تعمل كما يلي: $ sudo systemctl status postgresql إذا لم تكن تعمل، فيمكنك تشغيلها وضبط إمكانية تشغيلها تلقائيًا عند الإقلاع (إذا لم تكن معدةً هكذا من قبل): $ sudo systemctl start postgresql $ sudo systemctl enable postgresql إذا استمر الخلل، فتأكد أن إعدادات قاعدة البيانات المعرفة في الملف "myprojectdir/myproject/settings.py/~" صحيحة. مزيد من استكشاف الأخطاء وإصلاحها إذا أردت المزيد من استكشاف الأخطاء وإصلاحها فيمكن للسجلات أن تساعدك على تضييق نطاق البحث في الأسباب الجذرية. تفقد كلًّا منها وسجّل ملاحظاتك عن أي رسائل تشير إلى منطقة مشاكل. قد تساعدك السجلات التالية في عملك: فحص سجلات عملية إنجن إكس كما يلي: sudo journalctl -u nginx فحص سجلات الوصول إلى إنجن إكس كما يلي: sudo less /var/log/nginx/access.log فحص سجلات الخطأ لإنجن إكس كما يلي: sudo less /var/log/nginx/error.log فحص سجلات تطبيق غوني كورن كما يلي: sudo journalctl -u gunicorn فحص سجلات مقابس غوني كورن كما يلي: sudo journalctl -u gunicorn.socket ستحتاج غالبًا أثناء تحديثك للتهيئة أو التطبيق إلى إعادة تشغيل العمليات لضبط تغييراتك. إذا حدثت تطبيق جانغو، يمكن إعادة تشغيل العملية غوني كورن لتطبيق التغييرات بتنفيذ التالي: $ sudo systemctl restart gunicorn إذا غيرت ملفات خدمة أو مقبس غوني كورن أعد تحميل العملية الخفية daemon وأعد تشغيل العملية بتنفيذ التعليمات التالية: $ sudo systemctl daemon-reload $ sudo systemctl restart gunicorn.socket gunicorn.service إذا غيرت تهيئة كتلة خادم إنجن إكس، افحص الضبط ثم إنجن إكس بتنفيذ ما يلي: $ sudo nginx -t && sudo systemctl restart nginx هذه الأوامر مفيدة جدًا لتطبيق التغييرات أثناء الضبط. خاتمة أعددت في هذه المقالة مشروع جانغو في بيته الافتراضية الخاصة وضبطت خادم غوني كورن لترجمة طلبات العملاء لتمكين جانغو من معالجتها. بعد ذلك، أعددت خادم إنجن إكس ليعمل مثل خادم وكيل معكوس ليتولى معالجة اتصالات العملاء ويخدم المشروع المناسب اعتمادًا على طلب العميل. يبسّط جانغو عملية إنشاء الكائنات والتطبيقات من خلال تزويدك بكثير من القطع المشتركة، مما يسمح لك بالتركيز على العناصر المتفردة unique. وبرفع سلسلة الأدوات العامة الموصوفة في هذه المقالة يمكنك خدمة التطبيقات التي أنشأتها من خادم وحيد. ترجمة - وبتصرف - للمقالة How To Set Up Django with Postgres, Nginx, and Gunicorn on Ubuntu 18.04 لصاحبها Justin Ellingwood. اقرأ أيضًا إعداد تطبيق ASGI Django للنشر مع Postgres وخادم Nginx مع Uvicorn إعداد تطبيق جانغو جاهز للنشر مع قاعدة بيانات Postgres وخادم Nginx و Gunicorn إنشاء تطبيق جانغو وتوصيله بقاعدة بيانات
  2. يُعد جانغو Django إطار عمل ويب قوي يساعدك على إطلاق مشروعك سواءٌ كان تطبيق بايثون Python أو موقع ويب، ويتضمن خادم تطوير بسيط يُستخدم لاختبار الشيفرة محليًا ولكن من المعلوم أنه عندما يتعلق الأمر بالإطلاق الاحترافي للتطبيق للشركات أو الأعمال فإننا سنحتاج دومًا إلى خادم ويب قوي وأكثر أمانًا. الطريقة التقليدية لنشر تطبيق جانغو هي استخدام واجهة بوابة خادم الويب Web Server Gateway Interface -أو اختصارًا WSGI- لكن مع ظهور بايثون 3 وتوفُّر ميزة دعم التنفيذ غير المتزامن أصبح بإمكانك تنفيذ تطبيقات بايثون من خلال المستَدعيات غير المتزامنة asynchronous callables مستخدمًا واجهة بوابة خادم غير متزامنة Asynchronous Server Gateway Interface -أو اختصارًا ASGI - ومع كون تقنية ASGI خلفًا لتقنية WSGI فمواصفات ASGI في بايثون قد ورثتها من WSGI وبالتالي يمكن أن تكون بديلًا عنها. يوفّر جانغو النمط "غير متزامن في الخارج async outside، متزامن في الداخل sync inside" الذي يسمح بأن تكون الشيفرة متزامنة داخليًًا بينما يعالج خادم ASGI الطلبات بصورةٍ غير متزامنة، وبالتالي يمكن لخادم الويب معالجة العديد من الأحداث الواردة من والصادرة إلى كل التطبيقات بمجرد السماح له أن يمتلك مستدعًى callable غير متزامن. يبقى تطبيق جانغو متزامنًا بهدف الحفاظ على التوافقية العكسية backward compatibility وتجنُّب تعقيد الحوسبة التفرعية؛ وهذا يعني أيضًا أن تطبيق جانغو الذي تعمل عليه لا يلزمه أي تغييرات لينتقل من WSGI إلى ASGI. ستعمل في هذا الدليل على تثبيت وضبط بعض المكونات على نظام أوبنتو 20.04 لدعم وخدمة تطبيقات جانغو؛ إذ ستثبّت قاعدة بيانات PostgreSQL بدلًا من SQLite المثبتة افتراضيًا، وستهيء أيضًا خادم تطبيقات غوني كورن Gunicorn المقترن بالخادم يوفي كورن Uvicorn، الذي يُعد أحد تنفيذات ASGI، وذلك بهدف ربطه مع تطبيقاتك بصورةٍ غير متزامنة. بعد ذلك، ستعدّ الخادم إنجن إكس Nginx ليعكس الخادم الوكيل proxy لخادم التطبيقات غوني كورن، الأمر الذي يعطيك وصولًا إلى مزاياه الأمنية ومزايا الأداء لخدمة تطبيقاتك. المتطلبات الأساسية ستحتاج لإكمال هذه المقالة إلى ما يلي: خادم أوبنتو 20.04 server واحد معدٌّ باتباع التعليمات الواردة في دليل إعداد خادم مبدئي لنظام أوبنتو، ويتضمن مستخدمًا غيرَ جذري non-root، لكنه يتمتع بصلاحيات sudo. الخطوة الأولى - تثبيت الحزم البرمجية من مستودعات أدوات أوبنتو ستعمل بدايةً على تنزيل وتثبيت جميع العناصر التي تحتاجها من مستودعات أدوات أوبنتو، وستستخدم مدير حزم بايثون pip لتثبيت المكونات الإضافية في وقتٍ لاحق. ستحتاج أولًا لتحديث دليل الحزم المحلي apt، وتبدأ بعدها في تنزيل الحزم وتثبيتها، إذ تعتمد الحزم التي تثبتها على رقم إصدار بايثون الذي سيستخدمه مشروعك. نفّذ الأمر التالي لتثبيت حزم النظام المطلوبة لمشروعك: $ sudo apt update $ sudo apt install python3-venv libpq-dev postgresql postgresql-contrib nginx curl سيثبّت هذا الأمر مكتبات بايثون اللازمة لإعداد بيئة افتراضية، ونظام قاعدة بيانات Postgres، والمكتبات اللازمة للتفاعل معها، وخادم الويب إنجن إكس Nginx. ستنشئ في الخطوة التالية قاعدة بيانات PostgreSQL ومستخدمًا لها من أجل استخدامها في تطبيق جانغو الذي ستعمل عليه. الخطوة الثانية - إنشاء قاعدة بيانات PostgreSQL ومستخدم لها ستنشئ الآن قاعدة بيانات ومستخدمًا لها من أجل استخدامهما في تطبيق جانغو الذي ستعمل عليه. يستخدم نظام قاعدة بيانات Postgres في الحالة الطبيعية مخطط استيثاق يُدعى استيثاق النظير peer authentication من أجل الاتصالات المحلية، ويقضي هذا النوع من الاستيثاق بأنه إذا طابق اسم مستخدم username نظام التشغيل اسم مستخدم صالح ضمن قاعدة بيانات Postgres، فسيُعطَى عندئذ صلاحية الدخول إلى قاعدة البيانات دون الحاجة إلى استيثاق إضافي. لذلك، أثناء تثبيت Postgres، يُنشأ مستخدم نظام التشغيل باسم postgres ليطابق اسم المستخدم الإداري لنظام إدارة قواعد البيانات PostgreSQL، وهو أيضًا باسم postgres، إذ ستحتاج هذا المستخدم لإنجاز المهام الإدارية، علمًا بأنه يمكنك استخدام sudo وتمرير الخيار u- مع اسم المستخدم. الآن سجّل الدخول إلى جلسة Postgres تفاعلية من خلال الأمر التالي: $ sudo -u postgres psql سيظهر لك محث PostgreSQL وهذا يعني أن صار بإمكانك تجهيز متطلباتك. أولًا، أنشئ قاعدة بيانات للمشروع الذي تعمل عليه: postgres=# CREATE DATABASE myproject; ملاحظة: تنتهي كل تعليمة من تعليمات Postgres بفاصلة منقوطة، ولذا إذا واجهتك أي مشاكل فتحقق من أنك ختمت كل التعليمات بفواصل منقوطة. بعد ذلك، أنشئ مستخدم قاعدة بيانات لمشروعك، واختر كلمة مرور قوية: postgres=# CREATE USER myprojectuser WITH PASSWORD 'password'; ستعدّل بعد ذلك بعض معاملات الاتصال للمستخدم الذي أنشأته للتو، والهدف من هذه التعديلات هو تسريع عمليات قاعدة البيانات بحيث لا تضطر للاستعلام عن القيم الصحيحة ثم إعدادها في كل مرة يُؤسَّس فيها اتصال. ستضع التشفير الافتراضي بالقيمة "UTF-8"، وهي التي يتوقعها جانغو، وستحدّد مخطط عزل الإجراءات الافتراضي بالقيمة "read committed" التي تؤدي إلى حجب عمليات القراءة من الإجراءات التي لم تودع بعد uncommitted transactions. أخيرًا، ستحدد المنطقة الزمنية، والقيمة الافتراضية لها في جانغو هي "UTC". جميع هذه التوصيات هي من القائمين على مشروع جانغو. postgres=# ALTER ROLE myprojectuser SET client_encoding TO 'utf8'; postgres=# ALTER ROLE myprojectuser SET default_transaction_isolation TO 'read committed'; postgres=# ALTER ROLE myprojectuser SET timezone TO 'UTC'; يمكنك الآن منح المستخدم الجديد الوصولَ إلى قاعدة البيانات الجديدة ليتمكن من إدارتها: postgres=# GRANT ALL PRIVILEGES ON DATABASE myproject TO myprojectuser; وعقب انتهائك، اخرج من محث PostgreSQL بالتعليمة التالية: postgres=# \q وبهذا نكون أعددنا نظام Postgres بحيث يتمكن جانغو من الاتصال به وإدارة معلومات قاعدة البيانات فيه. الخطوة الثالثة- إنشاء بيئة بايثون افتراضية للمشروع الآن بعد أن أصبح لديك قاعدة بيانات أصبح بإمكانك تجهيز ما تبقى من متطلبات المشروع. ستثبت فيما يلي متطلبات بايثون ضمن بيئة افتراضية بهدف تسهيل الإدارة. أنشئ أولًا مجلدًا تحفظ فيه ملفات المشروع وانتقل إلى داخله: $ mkdir ~/myprojectdir $ cd ~/myprojectdir ثم استخدم أداة البيئة الافتراضية المبنية مسبقًا من بايثون لإنشاء بيئة افتراضية جديدة. $ python3 -m venv myprojectenv سينشئ هذا مجلدًا اسمه "myprojectenv" ضمن المجلد "myprojectdir"، إذ سيثبت داخله إصدارًا محليًا من بايثون وإصدارًا محليًا من pip. يمكنك استخدامه لتثبيت وضبط بيئة بايثون معزولة عن المشاريع الأخرى. ستحتاج قبل تثبيت متطلبات بايثون للمشروع إلى تفعيل البيئة الافتراضية باستخدام الأمر التالي: $ source myprojectenv/bin/activate يجب أن يتغير شكل المحث الآن ليشير إلى أنك تعمل داخل البيئة الافتراضية لبايثون. سيبدو هذا مشابهًا لما يلي: (myprojectenv)user@host:~/myprojectdir$ لا بُد من تثبيت جانغو داخل البيئة الافتراضية، إذ يسمح تثبيته داخل بيئة مخصصة لمشروعك للمشاريع الأخرى وبيئاتها أن تُعالج بمعزلٍ عن بعضها بعضًا. الآن وبعد تفعيل البيئة الافتراضية، استخدم النسخة المحلية من الأمر pip لتثبيت كلٍّ من جانغو وغوني كورن ويوفي كورن وموائم PostgreSQL الذي اسمه "psycopg2". ملاحظة: استخدم pip بدلًا من pip3 عند تفعيل البيئة الافتراضية، أي عندما يكون المُحث مسبوقًا بالاسم "(myprojectenv)"، حتى لو كنت تستخدم بايثون 3، إذ يُطلق دائمًا يطلق على نسخة الأداة للبيئة الافتراضية اسم pip بغض النظر عن إصدار بايثون. (myprojectenv) $ pip install django gunicorn uvicorn psycopg2-binary وبهذا يكون أصبح لديك كل البرمجيات اللازمة لبدء مشروع جانغو. الخطوة الرابعة - إنشاء وضبط مشروع جانغو جديد يمكنك الآن بعد الانتهاء من تثبيت مكونات بايثون أن تنشئ ملفات مشروع جانغو الفعلية. إنشاء مشروع جانغو نظرًا لأن مجلد المشروع موجود فعلًا، يمكنك أن تطلب من جانغو أن يثبت الملفات فيه، إذ سينشئ مجلدًا في المستوى الثاني مع الشيفرة الفعلية، وهو أمر طبيعي، ويضع سكريبت script الإدارة في هذا المجلد. الفكرة الرئيسية وراك ذلك هو أن تعرّف المجلد صراحةً بدلًا من ترك المجال لجانغو باتخاذ قرارات لها علاقةٌ بمجلدك الحالي نيابةً عنك: (myprojectenv) $ django-admin startproject myproject ~/myprojectdir الآن، لا بُدّ أن يحتوي مجلد المشروع ("myprojectdir/~" في مقالتنا) على المكونات التالية: "myprojectdir/manage.py/~": سكربت إدارة مشروع جانغو. "/myprojectdir/myproject/~": حزمة مشروع جانغو التي يجب أن تحتوي على الملفات: "init__.py__" و "asgi.py" و "settings.py" و "urls.py" و "wsgi.py". "/myprojectdir/myprojectenv/~": مجلد البيئة الافتراضية الذي أنشأته من قبل. ضبط إعدادات المشروع ستحتاج بعد إنشاء ملفات المشروع إلى ضبط بعض الإعدادات. افتح ملف الإعدادات بأي محرر نصوص تشاء، واستخدمنا هنا محرر النصوص نانو nano: (myprojectenv) $ nano ~/myprojectdir/myproject/settings.py ابحث في الملف عن الموجه ALLOWED_HOSTS الذي يعرّف قائمةً بعناوين وأسماء النطاقات للخادم التي يمكن أن تُستخدم للاتصال بنسخة جانغو. لذا ستؤدي أي طلبات واردة بترويسة مضيف وغير موجودة ضمن تلك القائمة إلى ظهور استثناء exception، ويطلب جانغو منك هذا منعًا لصنف معين من الثغرات الأمنية. أدرج داخل الأقواس المربعة قائمة عناوين IP أو أسماء النطاقات المرافقة لخادم جانغو، وضع كل عنصر بين علامتي اقتباس مع الفصل بين كل عنصرين بفاصلة. للسماح بنطاق كامل وأي نطاقات فرعية، ضع نقطةً في بداية العنصر. لفهم هذا الكلام وضعنا في الشيفرة التالية بعض الأمثلة وسبقناها بعلامة التعليق لتُعامل مثل تعليقات. ملاحظة: تأكد من إضافة localhost ضمن القائمة لأنك ستحتاج إلى خادم وكيل من أجل الاتصالات من خلال نسخة إنجن إكس محلية. . . . # The simplest case: just add the domain name(s) and IP addresses of your Django server # ALLOWED_HOSTS = [ 'example.com', '203.0.113.5'] # To respond to 'example.com' and any subdomains, start the domain with a dot # ALLOWED_HOSTS = ['.example.com', '203.0.113.5'] ALLOWED_HOSTS = ['your_server_domain_or_IP', 'second_domain_or_IP', . . ., 'localhost'] ثم اعثر على القسم الذي يضبط الوصول إلى قاعدة البيانات، إذ سيبدأ بـكلمة "DATABASES". الضبط الموجود في الملف يخص قاعدة بيانات SQLite التي عدَلنا عن استخدامها إلى البديل PostgreSQL لذا لا بد من ضبط الإعدادات. غيّر الإعدادات بمعلومات قاعدة بيانات PostgreSQL، وأخبر جانغو بأن يستخدم موائم psycopg2 الذي ثبتَّه بواسطة pip. سيُطلب منك التزويد باسم قاعدة البيانات واسم مستخدم قاعدة البيانات وكلمة المرور لمستخدم قاعدة البيانات، ثم تحديد أن قاعدة البيانات موجودة على الجهاز المحلي؛ أما إعدادات المنفذ PORT، فيمكنك تركها سلسلةً نصيةً فارغة. . . . DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'myproject', 'USER': 'myprojectuser', 'PASSWORD': 'password', 'HOST': 'localhost', 'PORT': '', } } . . . انتقل بعد ذلك إلى آخر الملف وأضف إعدادًا يشير إلى الموضع الذي يجب أن توضع فيه الملفات الساكنة، وهذا ضروري لكي يتمكن إنجن إكس من معالجة الطلبات على هذه العناصر. يطلب السطر التالي من جانغو أن يضعهم في مجلد اسمه "static" في مجلد المشروع الأساسي: . . . STATIC_URL = '/static/' import os STATIC_ROOT = os.path.join(BASE_DIR, 'static/') احفظ وأغلق الملف عندما تنتهي. إتمام الإعداد الأولي للمشروع يمكنك الآن تهجير migrate مخطط قاعدة البيانات الأولي إلى قاعدة بيانات PostgreSQL باستخدام سكريبت الإدارة: (myprojectenv) $ ~/myprojectdir/manage.py makemigrations (myprojectenv) $ ~/myprojectdir/manage.py migrate أنشئ مستخدمًا إدرايًّا للمشروع على النحو التالي: (myprojectenv) $ ~/myprojectdir/manage.py createsuperuser ستحتاج إلى التزويد باسم مستخدم وبريد إلكتروني وتختار كلمة مرور ثم تؤكدها. يمكنك أن تجمع كل المحتوى الساكن في موقع المجلد الذي هيّأته وذلك على النحو التالي: (myprojectenv) $ ~/myprojectdir/manage.py collectstatic ستحتاج إلى تأكيد العملية. بعد ذلك ستُوضع الملفات الساكنة في مجلد اسمه "static" ضمن مجلد المشروع. يُفترض إذا كنت قد اتبعت دليل إعداد الخادم الأولي أن يكون لديك جدار ناري UFW يحمي خادمك، وستحتاج للسماح بالوصول إلى المنفذ الذي تستخدمه إذا أردت اختبار خادم التطوير. أنشئ استثناءً للمنفذ 8000 على النحو التالي: (myprojectenv) $ sudo ufw allow 8000 يمكنك الآن وأخيرًا اختبار مشروعك بتشغيل خادم تطوير جانغو على النحو التالي: (myprojectenv) $ ~/myprojectdir/manage.py runserver 0.0.0.0:8000 اذهب إلى اسم نطاق الخادم الذي تستخدمه أو عنوان IP متبوعًا برقم المنفذ 8000 في متصفحك: http://server_domain_or_IP:8000 يفترض الآن أن تظهر لك صفحة فهرس جانغو الافتراضية: سيُطلب منك إدخال اسم المستخدم وكلمة المرور اللذين أنشأتهما بالأمر createsuperuser،إذا ألحقت "admin/" بنهاية عنوان URL في شريط العنوان : يمكنك بعد انتهاء التحقق الوصولُ إلى الواجهة الافتراضية لإدارة جانغو: اضغط على المفتاحين "CTRL+C" بعد انتهاء عملية الاستكشاف في النافذة الطرفية لإيقاف تشغيل خادم التطوير. اختبار قدرة خادم غوني كورن على تخديم المشروع سنستخدم في مقالتنا هذه خادم "غوني كورن Gunicorn" جنبًا إلى جنب مع خادم "يوفي كورن Uvicorn" لتشغيل التطبيق. ومع أن غوني كورن يُستخدم عادةً لنشر تطبيقات WSGI لكنه يُزوَّد بواجهة قابلة للتوصيل pluggable لتأمين نشر ASGI، وهو يفعل هذا من خلال السماح لك باستهلاك صنف عامل يظهر بواسطة خادم ASGI (وهو يوفي كورن). ونظرًا لأن الخادم "غوني كورن" هو منتج أعلى نضجًا من الخادم "يوفي كورن" ويزود بضبطٍ أكثر منه، ينصح أنصار الخادم "يوفي كورن" باستخدام الخادم "غوني كورن" مع الصنف العامل للخادم "يوفي كورن" لأنه خادم ومدير عمليات ذي مزايا مكتملة. ستفحص خادم غوني كورن قبل مغادرة البيئة الافتراضية لتتأكد من أن بإمكانه تخديم التطبيق. الآن لكي تستخدم الأصناف العاملة لخادم يوفي كورن مع خادم غوني كورن، ادخل إلى مجلد المشروع واستخدم الأمر gunicorn التالي لتحميل وحدة ASGI للمشروع. (myprojectenv) $ cd ~/myprojectdir (myprojectenv) $ gunicorn --bind 0.0.0.0:8000 myproject.asgi -w 4 -k uvicorn.workers.UvicornWorker سيشغّل هذا الأمر خادم غوني كورن على نفس الواجهة التي كان يعمل عليها خادم تطوير جانغو. يمكنك الآن العودة واختبار التطبيق من جديد. ملاحظة: ليس مطلوبًا استخدام خادم غوني كورن لتشغيل تطبيق ASGI، ولكي تستخدم فقط خادم يوفي كورن، استعن بالأمر التالي: uvicorn myproject.asgi:application --host 0.0.0.0 --port 8080 ملاحظة: لن يكون في واجهة الإدارة أي تنسيقات CSS مطبقة، لأن الخادم غوني كورن لا يعرف كيف يعثر على محتوى CSS الساكن المسؤول عن هذا. إذا كانت صفحة ترحيب جانغو لا تزال ظاهرة حتى بعد تنفيذ هذه الأوامر، فإن هذا يعدّ تأكيدًا على أن الخادم غوني كورن قد خدّم الصفحة فعلًا وأنه يعمل على النحو المطلوب، وما قد حصل هو أنك عندما استخدمت الأمر gunicorn، فقد مرّرت وحدة module إلى خادم غوني كورن بتحديد مسار المجلد ذي الصلة إلى الملف asgi.py الخاص بـجانغو والذي يعدّ نقطة الدخول إلى تطبيقك، باستخدام صيغة وحدة بايثون. عُرّف داخل هذا الملف دالةٌ اسمها "application"، ستُستخدم للتواصل مع التطبيق. إذا أردت معرفة المزيد عن مواصفات ASGI، زر الموقع الرسمي للخادم ASGI. اضغط بعد انتهائك من الاختبار على المفتاحين "CTRL+C" في نافذة الطرفية لإيقاف تشغيل خادم غوني كورن. وبهذا تكون انتهيت من ضبط تطبيق جانغو، ويمكنك الآن الخروج من البيئة الافتراضية بكتابة ما يلي: (myprojectenv) $ deactivate عندئذٍ سيُحذف مؤشر البيئة الافتراضية في محث النافذة التي تعمل فيها. الخطوة الخامسة - إنشاء مقبس systemd وملفات خدمة لـخادم غوني كورن تأكدتَ في القسم السابق من قدرة خادم غوني كورن على التفاعل مع تطبيق جانغو، وستنفّذ في الخطوة الحالية طريقةً أقوى لتشغيل وإطفاء خادم التطبيقات عبر إنشاء خدمة systemd وملفات المقبس socket files. سيُنشأ مقبس خادم غوني كورن عند الإقلاع وسيستمع إلى الاتصالات، وعندما يحدث اتصال ستشغِّل خدمة systemd تلقائيًّا عمليةَ الخادم غوني كورن لتولي مهمة التعامل مع الاتصال. الآن ابدأ بإنشاء ملف مقبس systemd لخادم غوني كورن وافتحه بمحرر النصوص الذي تريد وبصلاحيات sudo كما يلي: $ sudo nano /etc/systemd/system/gunicorn.socket ستنشئ في داخل الملف القسم [Unit] لوصف المقبس والقسم [Socket] لتعريف موضع المقبس والقسم [Install] للتأكد أن المقبس يُنشأ في الوقت المناسب: [Unit] Description=gunicorn socket [Socket] ListenStream=/run/gunicorn.sock [Install] WantedBy=sockets.target احفظ الملف، ثم أغلقه عندما تنتهي. بعد ذلك أنشئ ملف خدمة systemd للخادم غوني كورن، وافتحه بصلاحيات sudo في محرر النصوص الذي تستخدمه، وتنبه إلى أنه يجب أن يطابق اسمُ ملف الخدمة استثناءَ اسم ملف المقبس للامتداد: $ sudo nano /etc/systemd/system/gunicorn.service ابدأ بالقسم [Unit] الذي يستخدم لتحديد البيانات الوصفية metadata والاعتماديات. ستحدد هنا وصفًا لخدمتك وتخبر نظام التهيئة init system أن يشغل هذا فقط عند الوصول إلى مستوى التشبيك المستهدف، ونظرًا لأن الخدمة تعتمد على المقبس والموجود وصفه في ملف المقبس، فستحتاج إلى تضمين الموجّه Requires للإشارة إلى تلك العلاقة: [Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target افتح بعد ذلك القسم [Service]، إذ ستحدّد في هذا القسم المستخدم والمجموعة اللتين تريد أن تُشغّل العملية من أجلهما. ستمنح حساب المستخدم النظامي ملكيةَ العملية لامتلاكه كافة الملفات اللازمة، وستعطي ملكية المجموعة للمجموعة "www-data" ليتمكنَ الخادم إنجن إكس من التواصل بسهولة مع خادم غوني كورن. بعد ذلك، ستعدّ المجلد العامل وتحدد الأمر الذي ستستخدمه لتشغيل الخدمة، إذ ستحتاج في حالتنا هذه لتحديد المسار الكامل للملف التنفيذي لخادم غوني كورن المثبت ضمن البيئة الافتراضية. ستربِط العملية مع مقبس يونيكس Unix الذي أنشأته ضمن المجلد "run/" لتتمكن العملية من التواصل مع الخادم إنجن إكس، وستُرسل كل سجلات البيانات إلى جهاز الخرج القياسي لتتمكن العملية journald من جمع سجلات الخادم غوني كورن. يمكنك أيضًا أن تحدد أي إعدادات من اختيارك للخادم غوني كورن. وفيما يلي مثالٌ عن تحديد ثلاث عمليات عاملة: [Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target [Service] User=sammy Group=www-data WorkingDirectory=/home/sammy/myprojectdir ExecStart=/home/sammy/myprojectdir/myprojectenv/bin/gunicorn \ --access-logfile - \ -k uvicorn.workers.UvicornWorker \ --workers 3 \ --bind unix:/run/gunicorn.sock \ myproject.asgi:application القسم الأخير هو [Install]، الذي سيخبر systemd عما سيربطه مع هذه الخدمة إذا اخترنا تشغيلها عند الإقلاع. سترغب غالبًا بتشغيل هذه الخدمة عندما يكون نظام المتعدد المستخدمين في حالة عمل: [Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target [Service] User=sammy Group=www-data WorkingDirectory=/home/sammy/myprojectdir ExecStart=/home/sammy/myprojectdir/myprojectenv/bin/gunicorn \ --access-logfile - \ -k uvicorn.workers.UvicornWorker \ --workers 3 \ --bind unix:/run/gunicorn.sock \ myproject.asgi:application [Install] WantedBy=multi-user.target وبهذا يكون ملف خدمة systemd قد اكتمل. بإمكانك الآن أن تحفظه وتغلقه. يمكنك الآن تشغيل وتفعيل مقبس الخادم غوني كورن، إذ سينشئ هذا ملف المقبس في المسار "run/gunicorn.sock/" الآن وعند الإقلاع. عندما يُنشأ اتصال مع ذلك المقبس، ستشغّل systemd الخدمة "gunicorn.service" تلقائيًا لمعالجتها. $ sudo systemctl start gunicorn.socket $ sudo systemctl enable gunicorn.socket الآن وبعد أن أنشأت ملفات المقبس وخدمة systemd، بإمكانك التأكد من نجاح ما أنجزته من خلال التأكد من وجود ملف المقبس. الخطوة السادسة - التأكد من وجود ملف مقبس الخادم غوني كورن ستتحقق في هذه الخطوة من ملف مقبس خادم غوني كورن. ابدأ أولًا بفحص حالة العملية لمعرفة ما إذا كانت قادرة على البدء: $ sudo systemctl status gunicorn.socket سيبدو خرج العملية السابقة قريبًا من التالي: ● gunicorn.socket - gunicorn socket Loaded: loaded (/etc/systemd/system/gunicorn.socket; enabled; vendor prese> Active: active (listening) since Fri 2020-06-26 17:53:10 UTC; 14s ago Triggers: ● gunicorn.service Listen: /run/gunicorn.sock (Stream) Tasks: 0 (limit: 1137) Memory: 0B CGroup: /system.slice/gunicorn.socket ثم تحقق من وجود ملف "gunicorn.sock" ضمن المجلد "run/": $ file /run/gunicorn.sock وسيكون الخرج على النحو التالي: /run/gunicorn.sock: socket إذا أشار الأمر systemctl status إلى أن خطأً ما قد حدث، أو إذا لم تجد الملف gunicorn.sock في المجلد، سيدّل هذا على أن مقبس غوني كورن لم يُنشأ على نحوٍ صحيح. تفّقد سجلات مقبس غوني كورن بالأمر التالي: $ sudo journalctl -u gunicorn.socket ألق نظرةً أخرى على الملف في الرابط التالي لإصلاح أيّ مشاكل قبل المتابعة: "etc/systemd/system/gunicorn.socket/" الخطوة السابعة - فحص تنشيط المقبس ستفحص في هذه الخطوة تنشيط المقبس، فإذا كنت في الوقت الحالي قد شغّلت فقط الوحدة gunicorn.socket، فلن تكون الخدمة gunicorn.service نشطةً لأن المقبس لم يكن قد استلم أي اتصالات بعد، ويمكنك فحص هذه الحالة بالأمر التالي: $ sudo systemctl status gunicorn ويكون الخرج على النحو التالي: ● gunicorn.service - gunicorn daemon Loaded: loaded (/etc/systemd/system/gunicorn.service; disabled; vendor preset: enabled) Active: inactive (dead) لاختبار آلية تنشيط المقبس يمكنك إرسال اتصال إلى المقبس عبر الأمر curl على النحو التالي: $ curl --unix-socket /run/gunicorn.sock localhost يفترض أن تتلقى الخرج على صيغة HTML من تطبيقك في الطرفية، والذي يشير إلى أن خادم غوني كورن قد شُغّل وأنه كان قادرًا على تخديم تطبيق جانغو الذي تعمل عليه. يمكن فحص ما إذا كانت خدمة غوني كورن في وضع التشغيل بكتابة الأمر التالي: $ sudo systemctl status gunicorn وسيكون الخرج على النحو التالي: ● gunicorn.service - gunicorn daemon Loaded: loaded (/etc/systemd/system/gunicorn.service; disabled; vendor preset: enabled) Active: active (running) since Thu 2021-06-10 21:03:29 UTC; 13s ago TriggeredBy: ● gunicorn.socket Main PID: 11682 (gunicorn) Tasks: 4 (limit: 4682) Memory: 98.5M CGroup: /system.slice/gunicorn.service ├─11682 /home/sammy/myprojectdir/myprojectenv/bin/python3 /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 -k uvicorn.workers.UvicornWorker --bind unix:/run/gunicorn.sock myproject.asgi:application ├─11705 /home/sammy/myprojectdir/myprojectenv/bin/python3 /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 -k uvicorn.workers.UvicornWorker --bind unix:/run/gunicorn.sock myproject.asgi:application ├─11707 /home/sammy/myprojectdir/myprojectenv/bin/python3 /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 -k uvicorn.workers.UvicornWorker --bind unix:/run/gunicorn.sock myproject.asgi:application └─11708 /home/sammy/myprojectdir/myprojectenv/bin/python3 /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 -k uvicorn.workers.UvicornWorker --bind unix:/run/gunicorn.sock myproject.asgi:application Jun 10 21:03:29 django gunicorn[11705]: [2021-06-10 21:03:29 +0000] [11705] [INFO] ASGI 'lifespan' protocol appears unsupported. Jun 10 21:03:29 django gunicorn[11705]: [2021-06-10 21:03:29 +0000] [11705] [INFO] Application startup complete. Jun 10 21:03:30 django gunicorn[11707]: [2021-06-10 21:03:30 +0000] [11707] [INFO] Started server process [11707] Jun 10 21:03:30 django gunicorn[11707]: [2021-06-10 21:03:30 +0000] [11707] [INFO] Waiting for application startup. Jun 10 21:03:30 django gunicorn[11707]: [2021-06-10 21:03:30 +0000] [11707] [INFO] ASGI 'lifespan' protocol appears unsupported. Jun 10 21:03:30 django gunicorn[11707]: [2021-06-10 21:03:30 +0000] [11707] [INFO] Application startup complete. Jun 10 21:03:30 django gunicorn[11708]: [2021-06-10 21:03:30 +0000] [11708] [INFO] Started server process [11708] Jun 10 21:03:30 django gunicorn[11708]: [2021-06-10 21:03:30 +0000] [11708] [INFO] Waiting for application startup. Jun 10 21:03:30 django gunicorn[11708]: [2021-06-10 21:03:30 +0000] [11708] [INFO] ASGI 'lifespan' protocol appears unsupported. Jun 10 21:03:30 django gunicorn[11708]: [2021-06-10 21:03:30 +0000] [11708] [INFO] Application startup complete. إذا كان الخرج من curl أو الخرج من systemctl status يشير إلى أن مشكلة ما قد حدثت فراجع السجلات للحصول على تفاصيل أكثر كما يلي: $ sudo journalctl -u gunicorn افحص الملف "etc/systemd/system/gunicorn.service/" بحثًا عن أي مشكلات، وإذا أجريت أي تغييرات على هذا الملف، فأعد تحميل العملية الخفية daemon لكي تعيد قراءة تعريف الخدمة، وأعد تشغيل العملية غوني كورن بكتابة ما يلي: $ sudo systemctl daemon-reload $ sudo systemctl restart gunicorn تأكد من فحص المسائل المذكورة أعلاه بحثًا عن أي أخطاء قبل المتابعة. الخطوة الثامنة - ضبط إنجن إكس لتمرير الخادم الوكيل إلى غوني كورن ستحتاج الآن بعد أن فرغت من إعداد غوني كورن إلى ضبط إنجن إكس Nginx لتمرير البيانات إلى العملية. ستعد في هذه الخطوة إنجن إكس أمام غوني كورن لتستفيد من آليات معالجة الاتصال عالية الأداء والمزايا الأمنية سهلة التنفيذ التي يتمتع بها. ابدأ العمل بإنشاء وفتح كتلة خادم جديد في مجلد sites-available الخاص بخادم إنجن إكس: $ sudo nano /etc/nginx/sites-available/myproject افتتح في الداخل كتلة خادم جديدة، وحدّد أن على هذه الكتلة الاستماع إلى المنفذ الاعتيادي 80 والاستجابة لاسم نطاق الخادم أو عنوان IP، كما يلي: server { listen 80; server_name server_domain_or_IP; } ثم اطلب من إنجن إكس أن يتجاهل أي مشاكل بخصوص العثور على أيقونة مفضلة favicon، وأخبره أيضًا أين سيجد الأصول الساكنة static assets التي جمعتها في المجلد "myprojectdir/static/~". تمتلك جميع هذه الملفات سابقة URI prefix معيارية هي "static/"، لذا يمكنك أن تنشئ كتلة موقع location لوضع هذه الطلبات: server { listen 80; server_name server_domain_or_IP; location = /favicon.ico { access_log off; log_not_found off; } location /static/ { root /home/sammy/myprojectdir; } } أخيرًا أنشئ كتلة مواقع" location / {}‎" لوضع سائر الطلبات الأخرى، وضمِّن داخل هذا الموقع الملف المعياري proxy_params المُضمَّن مع تثبيت إنجن إكس، ثم مرّر حركة المرور traffic مباشرةً إلى مقبس غوني كورن : server { listen 80; server_name server_domain_or_IP; location = /favicon.ico { access_log off; log_not_found off; } location /static/ { root /home/sammy/myprojectdir; } location / { include proxy_params; proxy_pass http://unix:/run/gunicorn.sock; } } احفظ الملف وأغلقه عند انتهائك، ويمكنك الآن تفعيل الملف بربطه بالمجلد "sites-enabled": $ sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled تأكد من خلو ضبط إنجن إكس من الأخطاء الكتابية بكتابة ما يلي: $ sudo nginx -t إذا لم يُبلَّغ عن أي أخطاء، استمر وأعد تشغيل إنجن إكس من خلال كتابة: $ sudo systemctl restart nginx أخيرًا ستحتاج إلى فتح الجدار الناري أمام حركة المرور العادية على المنفذ 80، ونظرًا لعدم وجود حاجة إلى الوصول إلى خادم التطوير، فيمكنك إزالة القاعدة لفتح المنفذ 8000 أيضًا: $ sudo ufw delete allow 8000 $ sudo ufw allow 'Nginx Full' يفترض أن تكون الآن قادرًا على الذهاب إلى نطاق خادمك أو عنوان IP له لرؤية صفحة الترحيب لجانغو التي تظهر عليها صورة الصاروخ. ملاحظة: الخطوة التالية بعد أن تفرغ من إعداد إنجن إكس هي تأمين حركة المرور الذاهبة إلى الخادم باستخدام SSL/TLS؛ وهذا مهم إذ ستُرسل جميع المعلومات بما فيها كلمات المرور عبر الشبكة بصيغة نص صرف plain text دون استخدامه، وأبسط طريقة للحصول على شهادة SSL لتأمين المرور إذا كان لديك اسم نطاق هي استخدام موقع Let's Encrypt. اتبع هذا الدليل لإعداد Let's Encrypt مع إنجن إكس على أوبنتو 20.04، واتبع خطوات الإجراء باستخدام كتلة خادم إنجن إكس التي أنشأتها في هذه المقالة. الخطوة التاسعة - استكشاف أخطاء إنجن إكس وغوني كورن وإصلاحها إذا لم تُظهِر الخطوة الأخيرة تطبيقك، فستحتاج حينئذٍ إلى استكشاف الأخطاء في التثبيت وإصلاحها. وفيما يلي نعرض أبرز الأخطاء التي يمكن أن تظهر: يظهر إنجن إكس الصفحة الافتراضية بدلًا من إظهار تطبيق جانغو إذا كان إنجن إكس يعرض الصفحة الافتراضية بدلًا من الإحالة إلى تطبيقك، فهذا يعني غالبًا أنك تحتاج إلى ضبط قيمة "server_name" ضمن الملف: "etc/nginx/sites-available/myproject/" بحيث يشير إلى عنوان IP أو اسم النطاق للخادم؛ إذ يستخدم إنجن إكس القيمة "server_name لتحديد كتلة الخادم التي سيستخدمها للاستجابة للطلبات. إذا استلمت صفحة إنجن إكس الافتراضية، فهذه إشارة إلى أن إنجن إكس لم يكن قادرًا على مطابقة الطلب مع كتلة الخادم صراحةً ولذلك انسحب إلى الكتلة الافتراضية المعرّفة في الملف: "etc/nginx/sites-available/default/" يجب أن يكون اسم الخادم "server_name" في كتلة خادم المشروع محددًا أكثر من الموجود في كتلة الخادم الافتراضي الذي جرى اختياره. يعرض إنجن إكس الخطأ 502 Bad Gateway Error بدلا من عرض تطبيق جانغو يشير الخطأ 502 إلى أن إنجن إكس غير قادر على إحالة الطلب. كثيرًا ما تُظهِر مشكلات الضبط نفسها بإبراز الخطأ 502، لذلك فإننا نحتاج إلى مزيد من المعلومات لنستشكف الأخطاء ونصلحها بالطريقة الصحيحة. وأول مكان يجب أن نبحث فيه عن مزيد من المعلومات هو سجل أخطاء إنجن إكس، الذي سيخبرك عمومًا عن الظروف التي سببت المشكلات أثناء حدث الإحالة. تابع سجلات الخطأ لإنجن إكس بكتابة ما يلي: $ sudo tail -F /var/log/nginx/error.log والآن أنشئ طلبًا جديدًا في متصفحك لتوليد خطأ جديد (حاول إنعاش refresh الصفحة). يفترض أن تستلم رسالة خطأ جديدة تظهر في السجل، وإذا نظرت إلى الرسالة فيفترض أن تساعدك على تضييق نطاق المشكلة. قد تستلم الرسالة التالية: connect() to unix:/run/gunicorn.sock failed (2: No such file or directory) يشير هذا إلى أن إنجن إكس لم يستطع العثور على الملف "gunicorn.sock" في الموضع المُعطى. ينبغي أن تقارن موقع "proxy_pass" المعرف ضمن الملف "etc/nginx/sites-available/myproject/" مع الموقع الفعلي للملف "gunicorn.sock" الذي ولدته وحدة "gunicorn.socket" التابعة لخدمة systemd. إذا لم تعثر على الملف ضمن المجلد، فهذا يعني غالبًا أن ملف مقبس خدمة systemd لم يستطع إنشاءه، لذلك عُد إلى القسم الذي فحصنا فيه ملف المقبس لغوني كورن لكي تتابع في خطوات استكشاف أخطاء غوني كورن وإصلاحها. connect() to unix:/run/gunicorn.sock failed (13: Permission denied) يشير هذا إلى أن إنجن إكس لم يستطع الاتصال بمقبس غوني كورن بسبب مشكلات تتعلق بالصلاحيات، إذ يمكن أن يحدث هذا عندما يستخدِم الإجراءُ المستخدِم الجذر بدلًا من مستخدم "sudo"؛ فمع قدرة systemd على إنشاء ملف مقبس غوني كورن، لا يكون إنجن إكس قادرًا على الوصول إليه. يمكن أن يحدث هذا إذا كان هناك أذونات محدودة في أي نقطة بين المجلد الجذر "/" والملف "gunicorn.sock". يمكنك مراجعة الأذونات وقيم الملكية لملف المقبس وكل من مجلداته الآباء بتمرير المسار المطلق المؤدي إلى ملف المقبس إلى الأمر namei كما يلي: $ namei -l /run/gunicorn.sock وسيكون الخرج على النحو التالي: f: /run/gunicorn.sock drwxr-xr-x root root / drwxr-xr-x root root run srw-rw-rw- root root gunicorn.sock يظهر في الخرج أذونات كل مكون من مكونات المجلد، ويمكنك بالنظر إلى الأذونات (العمود الأول) والمالك (العمود الثاني) ومالك المجموعة (العمود الثالث)، أن تكتشف نوع الوصول المسموح لملف المقبس. يملك ملف المقبس وكل المجلدات التي تؤدي إلى ملف المقبس في المثال الوارد أعلاه أذونات قراءة وتنفيذ (ينتهي عمود الأذونات للمجلدات بالخيارات r-x بدلًا من ---). ينبغي أن تكون العملية إنجن إكس قادرة على الوصول إلى المقبس بنجاح. إذا لم تمتلك أي من المجلدات التي تؤدي إلى المقبس أذونات قراءة وتنفيذ عامّة، لن يتمكن إنجن إكس من الوصول إلى المقبس دون السماح بأذونات قراءة وتنفيذ عامة، أو التأكد من أن ملكية المجموعات معطاة لمجموعة يكون إنجن إكس جزءًا منها. جانغو يعرض رسالة could not connect to server: Connection refused إحدى الرسائل المتوقع أن تتلقاها من جانغو عند محاولة الوصول إلى أجزاء من التطبيق في متصفح الويب هي: OperationalError at /admin/login/ could not connect to server: Connection refused Is the server running on host "localhost" (127.0.0.1) and accepting TCP/IP connections on port 5432? تشير هذه الرسالة إلى أن جانغو لا يستطيع الاتصال بقاعدة بيانات Postgres. تأكد من أن نسخة Postgres تعمل بكتابة ما يلي: $ sudo systemctl status postgresql إذا لم تكن تعمل، يمكنك تشغيلها وتفعيلها تلقائيًا عند الإقلاع -إذا لم تكن مضبوطة مسبقًا لذلك- من خلال كتابة ما يلي: $ sudo systemctl start postgresql $ sudo systemctl enable postgresql إذا بقي لديك مشكلات، تأكد من أن إعدادات قاعدة البيانات المعرفة في الملف التالي صحيحة: "myprojectdir/myproject/settings.py/~" المزيد من استكشاف الأخطاء وإصلاحها لمزيد من استكشاف الأخطاء وإصلاحها، يمكنك الاستعانة بالسجلات لأنها تساعد في تضييق نطاق الأسباب الجذرية. افحص كلًا منها بالترتيب وابحث عن رسائل تشير إلى مناطق المشكلة. يمكن أن تساعدك السجلات التالية: افحص سجلات العملية إنجن إكس من خلال كتابة الأمر التالي: sudo journalctl -u nginx افحص سجلات الوصول إلى إنجن إكس من خلال كتابة الأمر التالي: sudo less /var/log/nginx/access.log افحص سجلات أخطاء إنجن إكس من خلال كتابة الأمر التالي: sudo less /var/log/nginx/error.log افحص سجلات تطبيق غوني كورن من خلال كتابة الأمر التالي: sudo journalctl -u gunicorn افحص سجلات مقبس غوني كورن من خلال كتابة الأمر التالي: sudo journalctl -u gunicorn.socket أثناء تحديثك للضبط أو التطبيق الخاص بك، ستحتاج غالبًا لإعادة تشغيل العمليات لتُضبط مع التغييرات التي أجريتها. إذا حدّثتَ تطبيق جانغو يمكنك إعادة تشغيل عملية "غوني كورن" لتطبيق التغييرات من خلال كتابة: $ sudo systemctl restart gunicorn إذا غيرت مقبس غوني كورن أو ملفات الخدمة، أعد تحميل العفريت daemon ثم أعد تشغيل العملية من خلال كتابة: $ sudo systemctl daemon-reload $ sudo systemctl restart gunicorn.socket gunicorn.service إذا غيّرت ضبط كتلة الخادم إنجن إكس، افحص الضبط ثم إنجن إكس من خلال كتابة: $ sudo nginx -t && sudo systemctl restart nginx ستساعدك هذه الأوامر لتطبيق التغييرات التي تجريها أثناء الضبط. الخاتمة أعددت في هذا الدليل مشروع جانغو لخادم ASGI في بيئته الافتراضية الخاصة، وهيأت خادمي غوني كورن ويوفي كورن لترجمة طلبات العميل بنسق غير متزامن ليتمكن جانغو من التعامل مع هذه الطلبات. بعد ذلك، أعددت إنجن إكس ليأخذ دور خادم وكيل عكسي لمعالجة اتصالات العميل وخدمة المشروع الصحيح اعتماداً على طلب العميل. يبسّط جانغو عملية إنشاء الكائنات والتطبيقات من خلال تزويدك بكثير من القطع المشتركة، مما يسمح لك بالتركيز على العناصر المتفردة unique. وهكذا بالاستفادة من سلسلة الأدوات العامة التي وردت في هذه المقالة، أصبح بإمكانك خدمة التطبيقات التي ستنشئها مستقبلًا من خادم وحيد. ترجمة - وبتصرف - للمقالة How To Set Up an ASGI Django App with Postgres, Nginx, and Uvicorn on Ubuntu 20.04 لصاحبيها Mason Egger و Erin Glass. اقرأ أيضًا البدء مع إطار العمل جانغو لإنشاء تطبيق ويب إنشاء تطبيق جانغو وتوصيله بقاعدة بيانات إعداد تطبيق جانغو جاهز للنشر مع قاعدة بيانات Postgres وخادم Nginx و Gunicorn
  3. يستخدم الناس أنواعًا مختلفة من الأجهزة للاتصال بالإنترنت وتصفح الويب، ولهذا تحتاج إلى تمكين المستخدمين من الوصول إلى التطبيقات مهما كان موقعهم؛ ففي حالة المواقع التقليدية، يكفي غالبًا امتلاك واجهة مستخدم متجاوبة؛ بينما تحتاج في حالة التطبيقات الأعقد إلى استخدام تقنيات ومعماريات أخرى، منها امتلاك تطبيقات ريست REST مستقلة للواجهة الأمامية والواجهة الخلفية والتي يمكن تنفيذها مثل تطبيقات ويب من جهة العميل أو تطبيقات ويب تقدّميّة Progressive Web Apps -اختصارًا PWA- أو تطبيقات جوال أصلية. نذكر فيما يلي بعض الأدوات الممكن استخدامها لبناء تطبيقات أعقد: ريآكت، وهو إطار عمل مبني بلغة جافا سكريبت يتيح للمطورين بناء تطبيقات ويب بواجهات أمامية وأصلية للواجهات الخلفية لواجهة برمجة التطبيقات API ريست. جانغو، وهو إطار عمل ويب مبني بلغة بايثون مفتوح المصدر ومجاني يتبع نموذج معمارية البرمجيات نموذج عرض متحكم Model View Controller -أو اختصارًا MVC-. إطار عمل جانغو ريست، الذي يمثّل أدوات قوية ومرنة لبناء واجهة برمجة تطبيقات ريست APIs بجانغو. ستبني في هذه المقالة تطبيق ويب حديث مع واجهة REST API خلفية منفصلة وواجهة أمامية مستخدمًا لهذا الغرض ريآكت وجانغو وإطار عمل جانغو ريست، إذ يمكنك باستخدام ريآكت وجانغو معًا الاستفادة من أحدث الترقيات في جافا سكريبت وتطوير الواجهات الأمامية، وبدلًا من بناء تطبيق جانغو يستخدم محرك قوالب داخلي، ستستخدم ريآكت مثل مكتبة واجهة مستخدم UI library مستفيدًا من نموذج كائن الوثيقة Document Object Model الافتراضي -أو اختصارًا DOM-، والطريقة التصريحية declarative والمكونات التي تصيّر render سريعًا التغيرات على البيانات. يخزن تطبيق الويب الذي ستبنيه سجلات العملاء في قاعدة بيانات، ويمكنك استخدامها بمثابة نقطة بداية لتطبيق إدارة علاقات العملاء CRM، وعند انتهائك ستكون قادرًا على إنشاء وقراءة وتحديث وحذف السجلات باستخدام واجهة ريآكت المُنسقة مستخدمًا بوتستراب 4. المتطلبات الأساسية ستحتاج لإتمام هذه المقالة وتطبيق ما فيها إلى ما يلي: جهاز تطوير مع نظام تشغيل أوبنتو Ubuntu 18.04. تثبيت بايثون 3 و pip و venv على جهازك باتباع الخطوتين الأولى والثانية من مقالة كيف تثبت بايثون 3 وتعد بيئة برمجية محلية على نظام أبونتو 18.04. تثبيت الإصدار 6 أو ما بعده من نود جي إس Node.js و npm 5.2 أو أعلى منه على جهازك، ويمكنك تثبيت كليهما باتباع التعليمات الواردة في مقالة كيف تثبت نود جي إس على نظام أوبنتو 18.04 حول التثبيت من مستودعات البرمجيات التي يطلق عليها اسم أرشيف الحزم الشخصية personal package archive -أو اختصارًا PPA. الخطوة الأولى - إنشاء بيئة بايثون افتراضية وتثبيت الاعتماديات ستنشئ في هذه الخطوة بيئةً افتراضيةً وتثبت الاعتمادات اللازمة لتطبيقك بما فيها جانغو وإطار عمل جانغو ريست وحزمة django-cors-headers. سيستخدم تطبيقنا خادمي تطوير مختلفين لجانغو وريآكت، وسيعملان على منفذين مختلفين وبمثابة نطاقين منفصلين، ولهذا نحتاج تفعيل سياسة تعدد الموارد cross-origin resource sharing -أو اختصارًا CORS- لإرسال طلبات من نوع HTTP من ريآكت إلى جانغو دون التعرض للحظر على يدي المتصفح. انتقل إلى المجلد الأساسي وأنشئ بيئة افتراضية باستخدام وحدة venv في بايثون 3: $ cd ~ $ python3 -m venv ./env فعّل البيئة الافتراضية التي أنشأتها مستخدمًا الأمر source: $ source env/bin/activate ثم ثبّت اعتماديات المشروع مستخدمًا pip، وهذه الاعتمادات هي: جانغو: وهو إطار عمل ويب المشروع. إطار عمل جانغو ريست: تطبيق جانبي (منشأ من طرف ثالث) يبني واجهات برمجة التطبيقات لريست بواسطة جانغو. حزمة django-cors-headers: وهي حزمة تفعّل سياسة تعدد الموارد CORS. ثبّت إطار عمل جانغو بالأمر التالي: (env) ali@ubuntu:$ pip install django djangorestframework django-cors-headers والآن أصبح بإمكانك بعد أن ثبتّت اعتمادات المشروع أن تنشئ مشروع جانغو وواجهة ريآكت الأمامية. الخطوة الثانية - إنشاء مشروع جانغو سنولّد في هذه الخطوة مشروع جانغو باستخدام التعليمات والأدوات التالية: أداة django-admin startproject project-name، إذ تُعد django-admin أداةً تعمل في بيئة سطر الأوامر وتُستخدم لإتمام المهام بواسطة جانغو. ينشئ الأمرstartproject مشروع جانغو جديد. أداة سكريبت python manage.py startapp myapp، إذ يُعد manage.py سكريبت برمجي يضاف تلقائيًا لكل مشروع جانغو وهو يؤدي عددًا من المهام الإدارية، مثل إنشاء تطبيقات جديدة وتهجير ملفات قاعدة البيانات وخدمة مشروع جانغو محلي. ينشئ الأمر startapp التابع لهذا السكريبت تطبيق جانغو داخل مشروع جانغو. يُقصد بالمصطلح "تطبيق application" في جانغو حزمة بايثون توفّر بعض المزايا في المشروع. ولكي تبدأ، أنشئ مشروع جانغو باستخدام الأمر django-admin startproject، وسنطلق على التطبيق اسم "djangoreactproject": (env) ali@ubuntu:$ django-admin startproject djangoreactproject سننظر إلى بنية المجلد لمشروع جانغو الذي نعمل عليه باستخدام الأمر tree قبل المتابعة. نصيحة: الأمر tree هو أمرٌ مفيد لعرض بُنى الملفات والمجلدات من مستوى سطر الأوامر، يمكنك تثبيته مستخدمًا الأمر التالي: $ sudo apt-get install tree ولكي تستخدمه انتقل إلى المجلد الذي تريد رؤية بنيته واكتب tree أو اكتب المسار كاملًا وصولًا إلى نقطة البداية "tree /home/ali/ali-project". انتقل إلى المجلد djangoreactproject ضمن جذر مشروعك ونفذ الأمر tree: (env) ali@ubuntu:$ cd ~/djangoreactproject (env) ali@ubuntu:$ tree سيظهر لك الخرج التالي: ├── djangoreactproject │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── manage.py يمثّل المجلد "djangoreactproject/~" جذر المشروع، وتوجد داخله عدة ملفات مهمة لمشروعك، هي: "manage.py": وهو سكريبت خدمي يؤدي عددًا من المهام الإدارية. "settings.py": هو ملف التهيئة الرئيسي لمشروع جانغو، حيث يمكنك تعديل إعدادات المشروع، إذ تتضمن هذه الإعدادات متحولاتٍ، مثل INSTALLED_APPS الذي يحدد قائمة التطبيقات النشطة في مشروعك. ويوجد في توثيق جانغو معلومات أعمق عن الإعدادات المتاحة. "urls.py": يحتوي هذا الملف على قائمة بنماذج محددات الموارد الموحدة URL والعروض المرتبطة بها، إذ يُنظِّم كل نموذج اتصالًا بين عنوان محدد موارد موحد URL والدالة التي يجب أن تُستدعى لذلك العنوان URL. أولى خطواتنا للعمل في المشروع هي ضبط الحزم التي ثبتناها في الخطوة السابقة ومن بينها إطار عمل جانغو ريست وحزمة جانغو CORS، وذلك بإضافتهما إلى الملف "settings.py". افتح الملف مستخدمًا الأمر nano أو محرر النصوص المفضل لديك: (env) ali@ubuntu:$ nano ~/djangoreactproject/djangoreactproject/settings.py انتقل إلى الإعداد INSTALLED_APPS وأضف التطبيقين "rest_framework" و "corsheaders" أسفل القائمة: ... INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'corsheaders' ] ثم أضف البرمجية الوسيطة corsheaders.middleware.CorsMiddleware من حزمة سياسة تعدد الموارد CORS التي ثبتناها سابقًا إلى الإعداد MIDDLEWARE، الذي يمثّل قائمةً بالبرمجيات الوسيطة middlewares؛ وهو صنف بايثون يحتوي على شيفرة تُعالج في كل مرة يتعامل تطبيق الويب مع طلب أو استجابة: ... MIDDLEWARE = [ ... 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'corsheaders.middleware.CorsMiddleware' ] يمكنك بعد ذلك تفعيل CORS، إذ يحدّد الإعداد CORS_ORIGIN_ALLOW_ALL ما إذا كنت ستسمح بسياسة تعدد الموارد لكل النطاقات أم لا، والإعداد CORS_ORIGIN_WHITELIST هو صف بايثون يحتوي على عناوين URL المسموحة. سيعمل خادم تطوير ريآكت على العنوان "http://localhost:3000"، لذلك سنضيف الإعدادين التاليين إلى ملف الإعدادات "settings.py": CORS_ORIGIN_ALLOW_ALL = False (,'CORS_ORIGIN_WHITELIST('localhost:3000 حيث يمكنك إضافة هذين الإعدادين في أي مكان تشاء من هذا الملف: ... CORS_ORIGIN_ALLOW_ALL = False CORS_ORIGIN_WHITELIST = ( 'localhost:3000', ) … ويمكنك العثور على المزيد من خيارات التهيئة في وثائق django-cors-headers. احفظ الملف واخرج من محرر النصوص عندما تنتهي. نحتاج إلى إنشاء تطبيق جانغو جديد داخل المجلد "djangoreactproject/~" اسمه "customers": (env) ali@ubuntu:$ python manage.py startapp customers سيحتوي هذا التطبيق على النماذج models والعروض views لإدارة العملاء؛ إذ تعرّف النماذجُ الحقولَ والسلوكات لبيانات التطبيق؛ بينما تمكّن العروض تطبيقنا من التعامل مع الطلبات على النحو المناسب وإعادة الاستجابات المطلوبة على النحو المناسب أيضًا. بعد ذلك، أضف التطبيق إلى قائمة التطبيقات المثبتة في ملف المشروع "settings.py" لكي يتعرف جانغو عليه على أنه جزءٌ من المشروع. افتح الملف "settings.py" مرةً أخرى: (env) ali@ubuntu:$ nano ~/djangoreactproject/djangoreactproject/settings.py أضف التطبيق "customers": ... INSTALLED_APPS = [ ... 'rest_framework', 'corsheaders', 'customers' ] … بعد ذلك، هجّر قاعدة البيانات وشغّل خادم التطوير المحلي، إذ تُعد التهجيرات طريقة جانغو في نشر التغييرات التي تجريها على نماذجك إلى مخطط قاعدة بياناتك، ويمكن أن تتضمن هذه التغييرات أشياء مختلفة، مثل إضافة حقل، أو حذف نموذج. هجّر قاعدة البيانات كما يلي: (env) ali@ubuntu:$ python manage.py migrate وشغّل خادم التطوير المحلي: (env) ali@ubuntu:$ python manage.py runserver سترى خرجًا يشابه التالي: Performing system checks... System check identified no issues (0 silenced). October 22, 2018 - 15:14:50 Django version 2.1.2, using settings 'djangoreactproject.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. سيعمل تطبيق الويب من خلال العنوان "http://127.0.0.1:8000"، وإذا انتقلت إلى هذا العنوان في متصفحك فيفترض أن ترى الصفحة التالية: دع التطبيق يعمل وافتح نافذة طرفية جديدة لمتابعة تطوير المشروع. الخطوة الثالثة - إنشاء الواجهة الأمامية لريآكت سننشئ في هذا القسم تطبيق الواجهة الأمامية لمشروعنا مستخدمين ريآكت؛ إذ يوجد لدى ريآكت برنامج خدمي رسمي يسمح لك بتوليد مشاريع ريآكت بسرعة دون الحاجة إلى ضبط مجمع الوحدات Webpack، التي تمثّل مجمع وحدات module bundler يُستخدم لتجميع أصول الويب، مثل شيفرة جافا سكريبت و CSS والصور. قبل أن تتمكن من استخدام Webpack، ستحتاج عادةً إلى تحديد خيارات ضبط متعددة، لكن بفضل الأداة create-react-app لن تحتاج للتعامل مع Webpack مباشرةً حتى تقرر أو عندما تكون بحاجةٍ لمزيدٍ من التحكم. يمكنك استخدام أداة npx (وهي أداة تنفّذ الملفات الثنائية لحزم npm) لتشغيل create-react-app. تأكد في نافذة الطرفية الثانية أنك موجود داخل مجلد المشروع: $ cd ~/djangoreactproject أنشئ مشروع ريآكت اسمه "frontend" باستخدام create-react-app و npx: $ npx create-react-app frontend ثم انتقل إلى داخل تطبيق ريآكت وشغّل خادم التطوير: $ cd ~/djangoreactproject/frontend $ npm start سيعمل تطبيقك باستخدام العنوان "/http://localhost:3000": دع خادم تطوير ريآكت يعمل وافتح نافذة طرفية جديدة للمتابعة. ولرؤية بنية مجلدات كامل المشروع عند هذه النقطة، انتقل إلى المجلد الجذر ونفّذ الأمر tree مرةً أخرى: $ cd ~/djangoreactproject $ tree سترى بنيةً تشبه التالية: ├── customers │ ├── admin.py │ ├── apps.py │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ├── djangoreactproject │ ├── __init__.py │ ├── __pycache__ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── frontend │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── README.md │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── index.css │ │ ├── index.js │ │ ├── logo.svg │ │ └── registerServiceWorker.js │ └── yarn.lock └── manage.py سيستخدم تطبيقنا بوتستراب 4 لتحيدد تنسيق style واجهة ريآكت، لذلك سنضمّنه في الملف "frontend/src/App.css" الذي يدير إعدادات CSS. افتح الملف: $ nano ~/djangoreactproject/frontend/src/App.css أضف تعليمة الاستيراد import إلى بداية الملف. يمكن حذف المحتوى الحالي للملف رغم أن هذا غير مطلوب: @import 'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css'; تعليمة import@ هي تعليمة CSS تُستخدم لاستيراد قواعد التنسيق style rules من صفحات تنسيق أخرى. الآن بعد أن انتهينا من إنشاء تطبيق الواجهة الأمامية والواجهة الخلفية، دعنا ننشئ نموذج العميل Customer وبعض البيانات التجريبية. الخطوة الرابعة - إنشاء نموذج العميل وبيانات التهيئة بعد أن انتهينا من إنشاء تطبيق جانغو والواجهة الأمامية لريآكت، ستكون خطوتنا التالية هي إنشاء نموذج العميل Customer، الذي يمثّل جدول قاعدة البيانات الذي سيخزن معلومات العملاء. لن تحتاج إلى أي أوامر SQL، إذ سيتولى الرابط العلائقي للكائنات في جانغو Object Relational Mapper -أو اختصارًا ORM- أمر العمليات التي تجري على قاعدة البيانات من خلال إسقاط أصناف بايثون ومتحولاته على جداول SQL وأعمدته، وبهذه الطريقة يكون الرابط العلائقي للكائنات في جانغو قد جرّد تفاعلات SQL مع قاعدة البيانات من خلال واجهة بايثون. فعّل البيئة الافتراضية من جديد: $ cd ~ $ source env/bin/activate انتقل إلى المجلد "customers" وافتح الملف "models.py"، وهو ملف بايثون يخزن كافة نماذج تطبيقك: (env) ali@ubuntu:$ cd ~/djangoreactproject/customers/ (env) ali@ubuntu:$ nano models.py سيحتوي الملف على المحتوى التالي: from django.db import models # Create your models here. نشير إلى أنه قد استوردت واجهة برمجة التطبيقات لنموذج العملاء سلفًا ضمن الملف بفضل تعليمة الاستيرادfrom django.db import models. ستضيف الآن الصنف Customer المشتق من الصنف models.Model. كل نموذج في جانغو هو صنف بايثون مشتق من django.db.models.Model. سيكون في نموذج العميل "Customer" حقول قاعدة البيانات التالية: first_name: الاسم الأول للعميل. last_name: اسم العائلة للعميل. email: البريد الإلكتروني للعميل. phone: رقم هاتف العميل. address: عنوان العميل. description: وصف العميل. createdAt: تاريخ إضافة العميل. سنضيف أيضًا الدالة ()__str__ التي تعرّف طريقة عرض النموذج، والذي سيكون في حالتنا مع الاسم الأول للعميل. ولمزيدٍ من التفاصيل عن إنشاء الأصناف وتعريف الكائنات يرجى الاطلاع على المقالة كيف تبني الأصناف وتعرف الكائنات في بايثون 3. أضف الشيفرة التالية إلى الملف "models.py": from django.db import models class Customer(models.Model): first_name = models.CharField("First name", max_length=255) last_name = models.CharField("Last name", max_length=255) email = models.EmailField() phone = models.CharField(max_length=20) address = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True) createdAt = models.DateTimeField("Created At", auto_now_add=True) def __str__(self): return self.first_name ثم هجّر قاعدة البيانات لإنشاء جداول قاعدة البيانات. ينشئ الأمر makemigrations ملفات التهجير التي تضاف فيها التغييرات على النموذج، في حين يطبّق migrate التغييرات في ملفات التهجير إلى قاعدة البيانات. عُد الآن إلى المجلد الجذر للمشروع: (env) ali@ubuntu:$ cd ~/djangoreactproject ونفذ ما يلي من أجل إنشاء ملفات التهجير: (env) ali@ubuntu:$ python manage.py makemigrations ستحصل على خرج يشبه التالي: customers/migrations/0001_initial.py - Create model Customer طبّق هذه التغييرات على قاعدة البيانات: (env) ali@ubuntu:$ python manage.py migrate وسترى خرجًا يشير إلى نجاح عملية التهجير: Operations to perform: Apply all migrations: admin, auth, contenttypes, customers, sessions Running migrations: Applying customers.0001_initial... OK ستستخدم الآن ملف تهجير البيانات لإنشاء بيانات العميل المبدئية،إن أن ملف تهجير البيانات هو تهجير يضيف البيانات أو يعدّلها في قاعدة البيانات. أنشئ ملف تهجير بيانات فارغ للتطبيق customers: (env) ali@ubuntu:$ python manage.py makemigrations --empty --name customers customers سترى التأكيد التالي مع اسم ملف التهجير: Migrations for 'customers': customers/migrations/0002_customers.py لاحظ أن اسم ملف التهجير هو "customers.py‏_0002". انتقل بعد ذلك إلى داخل مجلد التهجيرات للتطبيق customers. (env) ali@ubuntu:$ cd ~/djangoreactproject/customers/migrations افتح ملف التهجير المنشأ للتو: (env) ali@ubuntu:$ nano 0002_customers.py وهذا هو محتوى التهيئة للملف: from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('customers', '0001_initial'), ] operations = [ ] تستورد تعليمة الاستيراد واجهة برمجة التطبيقات للملف "migrations" وواجهة برمجة التطبيقات لجانغو من أجل إنشاء التهجيرات من الملف "django.db" والتي هي حزمة مبنية مسبقًا built-in تحتوي على الأصناف اللازمة للعمل مع قواعد البيانات. الصنف Migration هو صنف بايثون يصف العمليات التي تُنفذ عند تهجير قواعد البيانات، وهو امتداد للصنف migrations.Migration، ويملك قائمتين: dependencies: تحتوي على التهجيرات المعتمدة. operations: تحتوي على العمليات التي ستُنفذ عندما نطبّق التهجير. الآن، أضف تابعًا لإنشاء بيانات تجريبية للعملاء قبل تعريف الصنف Migration: ... def create_data(apps, schema_editor): Customer = apps.get_model('customers', 'Customer') Customer(first_name="Customer 001", last_name="Customer 001", email="customer001@email.com", phone="00000000", address="Customer 000 Address", description= "Customer 001 description").save() ... أمسكنا grab في هذا التابع الصنف Customer التابع للتطبيق customers وأنشأنا عميلًا تجريبيًّا لإدخاله إلى قاعدة البيانات. للحصول على الصنف Customer، الذي سيمكّن من إنشاء عملاء جدد، نستخدم التابع ()get_model للكائن apps، الذي يمثّل مسجلًا registry للتطبيقات المثبتة ونماذج قواعد البيانات الخاصة بها. سيمرّر الكائن apps من التابع ()RunPython عندما نستخدمه لتشغيل ()create_data. أضف التابع ()migrations.RunPython إلى القائمة الفارغة operations: ... operations = [ migrations.RunPython(create_data), ] يُعد التابع ()RunPython جزءًا من واجهة برمجة التطبيقات للتهجيرات التي تسمح لك بتشغيل شيفرة بايثون معينة في تهجير ما. تحدد القائمة operations أن هذا التابع سيُنفّذ عندما نطبق التهجير. وفيما يلي الملف كاملًا: from django.db import migrations def create_data(apps, schema_editor): Customer = apps.get_model('customers', 'Customer') Customer(first_name="Customer 001", last_name="Customer 001", email="customer001@email.com", phone="00000000", address="Customer 000 Address", description= "Customer 001 description").save() class Migration(migrations.Migration): dependencies = [ ('customers', '0001_initial'), ] operations = [ migrations.RunPython(create_data), ] ولمزيدٍ من المعلومات عن تهجيرات البيانات، راجع التوثيق تهجير البيانات في جانغو. الآن لتهجير قاعدة بياناتك، عُد أولًا إلى مجلد الجذر لمشروعك: (env) ali@ubuntu:$ cd ~/djangoreactproject هجّر قاعدة بياناتك لتنشئ البيانات التجريبية: (env) ali@ubuntu:$ python manage.py migrate سنرى خرجًا يؤكد على نجاح عملية التهجير: Operations to perform: Apply all migrations: admin, auth, contenttypes, customers, sessions Running migrations: Applying customers.0002_customers... OK ولمزيدٍ من المعلومات عن العملية، راجع مقالة كيف تنشئ نماذج جانغو. وهكذا بعد إنشاء نموذج العميل والبيانات التوضيحية، يمكننا المتابعة إلى بناء واجهة برمجة التطبيقات لريست REST API. الخطوة الخامسة - إنشاء واجهة برمجة التطبيقات لريست REST API سننشئ في هذه الخطوة واجهة برمجة التطبيقات لريست باستخدام إطار عمل جانغو ريست، إذ سننشئ عدة عروض مختلفة لواجهة برمجة التطبيقات؛ إذ يُقصد بعرض واجهة برمجة التطبيقات API view دالة تعالج طلب واجهة برمجة تطبيقات أو استدعاء واجهة برمجة التطبيقات؛ في حين أن نقطة نهاية واجهة برمجة التطبيقات هي عنوان URL متفرد يمثّل نقطة تماس مع نظام ريست. على سبيل المثال، عندما يرسل المستخدم طلب من نوع GET إلى نقطة نهاية واجهة برمجة التطبيقات، يستدعي جانغو الدالة المقابلة، أو عرض واجهة برمجة التطبيقات لمعالجة الطلب وإعادة أي نتائج محتملة. سنستفيد أيضًا من المسلسِلات Serializers؛ إذ يسمح المسلسِل في إطار عمل جانغو بأن تُحوّل نسخ النماذج المعقدة ومجموعات الاستعلام QuerySets إلى صيغة محتوى JSON من أجل استهلاك واجهة برمجة التطبيقات، ويمكن أن يعمل صنف المسلسِل في الاتجاه الآخر أيضًا، إذ يزوِّد بآليات لتحليل وإلغاء سَلسَلة البيانات إلى نماذج جانغو ومجموعات استعلامه. ستتضمن نقاط نهاية واجهة برمجة التطبيقات ما يلي: "api/customers": تُستخدم نقطة النهاية هذه لإنشاء عملاء وإعادة مجموعات العملاء المرقمة. "<api/customers/<pk": تُستخدم نقطة النهاية هذه للحصول على عملاء منفردين وتحديثهم وحذفهم بالاستعانة بمفتاح أساسي أو معرّف id. سننشئ أيضًا عناوين URL في الملف "urls.py" التابع للمشروع من أجل نقاط النهاية المقابلة، أي "api/customers" و "<api/customers/<pk". دعنا نبدأ بإنشاء الصنف serializer من أجل نموذجنا Customer. إضافة صنف المسسلسل Serializer يُعد إنشاء صنف مسلسِل لنموذج "Customer" ضروري لتحويل نسخ العملاء ومجموعات الاستعلام QuerySets من وإلى JSON، ولإنشاء الصنف المسلسِل، أنشئ أولًا الملف "serializers.py" داخل التطبيق "customers": (env) ali@ubuntu:$ cd ~/djangoreactproject/customers/ (env) ali@ubuntu:$ nano serializers.py أضف الشيفرة التالية لاستيراد واجهة برمجة التطيبقات للمسلسِلات ونموذج العميل Customer: from rest_framework import serializers from .models import Customer ثم أنشئ صنف مسلسِل مُمتد عن الصنف serializers.ModelSerializer ويحدد الحقول التي ستُسلسَل: ... class CustomerSerializer(serializers.ModelSerializer): class Meta: model = Customer fields = ('pk','first_name', 'last_name', 'email', 'phone','address','description') يحدد الصنف Meta النموذج والحقول التي ستُسلسَل، وهي: pk first_name last_name email phone address description وهذا هو المحتوى الكامل لملف serializers.py: from rest_framework import serializers from .models import Customer class CustomerSerializer(serializers.ModelSerializer): class Meta: model = Customer fields = ('pk','first_name', 'last_name', 'email', 'phone','address','description') الآن وبعد أن انتهينا من إنشاء صنف المسلسِل، يمكننا إضافة عروض واجهة برمجة التطبيقات. إضافة عروض واجهة برمجة التطبيقات سننشئ في هذا القسم عروض API لتطبيقنا الذي سيستدعيه جانغو عندما يزور المستخدمُ نقطةَ النهاية المقابلة لدالة العرض. افتح الملف التالي: (env) ali@ubuntu:$ nano ~/djangoreactproject/customers/views.py احذف محتوى الملف وأضف أوامر الاستيراد التالية: from rest_framework.response import Response from rest_framework.decorators import api_view from rest_framework import status from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from .models import Customer from .serializers import * نحن نستورد المسلسِل الذي أنشأناه إلى جانب نموذج العميل Customer وجانغو وواجهات برمجة التطبيقات لجانغو ريست. بعد ذلك، أضف العرض الذي سيتولى معالجة طلبات من نوع POST و GET HTTP: ... @api_view(['GET', 'POST']) def customers_list(request): """ List customers, or create a new customer. """ if request.method == 'GET': data = [] nextPage = 1 previousPage = 1 customers = Customer.objects.all() page = request.GET.get('page', 1) paginator = Paginator(customers, 10) try: data = paginator.page(page) except PageNotAnInteger: data = paginator.page(1) except EmptyPage: data = paginator.page(paginator.num_pages) serializer = CustomerSerializer(data,context={'request': request} ,many=True) if data.has_next(): nextPage = data.next_page_number() if data.has_previous(): previousPage = data.previous_page_number() return Response({'data': serializer.data , 'count': paginator.count, 'numpages' : paginator.num_pages, 'nextlink': '/api/customers/?page=' + str(nextPage), 'prevlink': '/api/customers/?page=' + str(previousPage)}) elif request.method == 'POST': serializer = CustomerSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) نستخدم أولًا المزخرِف (['api_view(['GET', 'POST@ لإنشاء عرض واجهة برمجة تطبيقات يمكنه قبول طلبات GET و POST، إذ يُعد المزخرِف decorator دالة تأخذ دالةً أخرى وتمدّدها extend تلقائيًا. نستخدم المتغيّر request.method داخل متن التابع لفحص تابع HTTP الحالي وتنفيذ المنطق المقابل اعتمادًا على نوع الطلب: إذا كان الطلب من النوع GET، يرقّم التابع البيانات باستخدام مرقّم جانغو Django Paginator ويعيد الصفحة الأولى من البيانات بعد السَّلسَلَة، وعدد العملاء المتوفرين وعدد الصفحات المتوفرة والروابط إلى الصفحات السابقة واللاحقة. المرقّم paginator هو صنف جانغو داخلي يقسّم قوائم البيانات في عدة صفحات ويزوّد بتوابع للوصول إلى عناصر كل صفحة. إذا كان الطلب من النوع POST، يسلسِل التابع بيانات العميل المستلمة ثم يستدعي التابع ()save لكائن المسلسِل، ثم يعيد كائن استجابة Response وهو نسخة عن استجابة http أو HttpResponse مع رمز الحالة 201. كل عرض تنشؤه مسؤول عن إعادة كائن HttpResponse. يحفظ التابع ()save البيانات المسلسَلة في قاعدة البيانات. أضف الآن عرض واجهة برمجة التطبيقات الذي سيكون مسؤولًا عن معالجة طلبات GET و PUT و DELETE من أجل جلب وتحديث وحذف العملاء اعتمادًا على المفتاح الأساسي pk: ... @api_view(['GET', 'PUT', 'DELETE']) def customers_detail(request, pk): """ Retrieve, update or delete a customer by id/pk. """ try: customer = Customer.objects.get(pk=pk) except Customer.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) if request.method == 'GET': serializer = CustomerSerializer(customer,context={'request': request}) return Response(serializer.data) elif request.method == 'PUT': serializer = CustomerSerializer(customer, data=request.data,context={'request': request}) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == 'DELETE': customer.delete() return Response(status=status.HTTP_204_NO_CONTENT) زُخرِف التابعُ باستخدام (['api_view(['GET', 'PUT', 'DELETE@ للإشارة إلى أنه عرض واجهة برمجة تطبيقات API view يمكنه قبول طلبات من النوع GET و PUT و DELETE. التحقق الموجود في الحقل request.method يتحقق من تابع الطلب واعتمادًا على قيمته يستدعي المنطق المناسب: إذا كان الطلب من النوع GET، تُسَلسَلُ بياناتُ العميل وترسلُ باستخدام كائن استجابة Response. إذا كان الطلب من النوع PUT، ينشئ التابع مسلسِلًا لبيانات العميل الجديد، ثم يستدعي التابع ()save لكائن المسلسَل المُنشأ، وأخيرًا يرسل كائن الاستجابة Response مع العميل المحدّث. إذا كان الطلب من النوع DELETE، يستدعي التابعُ التابعَ ()delete لكائن العميل لحذفه، ثم يعيد كائن استجابة دون بيانات. يبدو ملف "views.py" المكتمل كما يلي: from rest_framework.response import Response from rest_framework.decorators import api_view from rest_framework import status from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from .models import Customer from .serializers import * @api_view(['GET', 'POST']) def customers_list(request): """ List customers, or create a new customer. """ if request.method == 'GET': data = [] nextPage = 1 previousPage = 1 customers = Customer.objects.all() page = request.GET.get('page', 1) paginator = Paginator(customers, 5) try: data = paginator.page(page) except PageNotAnInteger: data = paginator.page(1) except EmptyPage: data = paginator.page(paginator.num_pages) serializer = CustomerSerializer(data,context={'request': request} ,many=True) if data.has_next(): nextPage = data.next_page_number() if data.has_previous(): previousPage = data.previous_page_number() return Response({'data': serializer.data , 'count': paginator.count, 'numpages' : paginator.num_pages, 'nextlink': '/api/customers/?page=' + str(nextPage), 'prevlink': '/api/customers/?page=' + str(previousPage)}) elif request.method == 'POST': serializer = CustomerSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @api_view(['GET', 'PUT', 'DELETE']) def customers_detail(request, pk): """ Retrieve, update or delete a customer by id/pk. """ try: customer = Customer.objects.get(pk=pk) except Customer.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) if request.method == 'GET': serializer = CustomerSerializer(customer,context={'request': request}) return Response(serializer.data) elif request.method == 'PUT': serializer = CustomerSerializer(customer, data=request.data,context={'request': request}) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == 'DELETE': customer.delete() return Response(status=status.HTTP_204_NO_CONTENT) يمكننا الآن المتابعة إلى إنشاء نقاط النهاية. إضافة نقاط النهاية لواجهة برمجة التطبيقات سننشئ الآن نقطتي النهاية لواجهة برمجة التطبيقات، الأولى /‎api/customers من أجل الاستعلام عن العملاء وإنشائهم، والثانية <api/customers/<pk من أجل جلب وتحديث وحذف العملاء المنفردين اعتمادًا على المفاتيح الأساسية لكل منهم. افتح الملف التالي: (env) ali@ubuntu:$ nano ~/djangoreactproject/djangoreactproject/urls.py دع المحتويات كما هي ولكن أضف الاستيراد إلى عروض customers في أعلى الملف: from django.contrib import admin from django.urls import path from customers import views from django.conf.urls import url ثم أضف عنواني URL لكلٍ من /api/customers و <api/customers/<pk إلى قائمة نماذج عناوين URL urlpatterns التي تحتوي على عناوين URLs للتطبيق: ... urlpatterns = [ path('admin/', admin.site.urls), url(r'^api/customers/$', views.customers_list), url(r'^api/customers/(?P<pk>[0-9]+)$', views.customers_detail), ] الآن بعد أن انتهينا من إنشاء نقاط النهاية لريست، لنرى كيف نستهلكها. الخطوة السادسة - استهلاك واجهة برمجة التطبيقات لريست باستخدام اكسيوس Axios سنثبت في هذه الخطوة Axios، عميل HTTP الذي سنستخدمه لإنشاء استدعاءات واجهة برمجة التطبيقات. سننشئ أيضًا صنفًا لاستهلاك نقاط النهاية لواجهة برمجة التطبيقات التي أنشأناها. نبدأ بإيقاف تنشيط البيئة الافتراضية: (env) ali@ubuntu:$ deactivate ثم ننتقل إلى مجلد الواجهة الأمامية "frontend": (env) ali@ubuntu:$ cd ~/djangoreactproject/frontend ثبت اكسيوس axios من npm باستخدام الأمر التالي: (env) ali@ubuntu:$ npm install axios --save يضيف الخيار save– اعتمادية axios إلى ملف التطبيق "package.json". ثم أنشئ ملف جافا سكريبت اسمه "CustomersService.js" الذي سيحتوي على الشيفرة التي ستستدعي واجهات برمجة التطبيقات لريست. سنفعّل هذا داخل مجلد "src"، حيث توجد شيفرة التطبيق لمشروعنا: $ cd src $ nano CustomersService.js أضف الشيفرة التالية التي تحتوي على توابع ستتصل بواجهات برمجة التطبيقات لجانغو ريست: import axios from 'axios'; const API_URL = 'http://localhost:8000'; export default class CustomersService{ constructor(){} getCustomers() { const url = `${API_URL}/api/customers/`; return axios.get(url).then(response => response.data); } getCustomersByURL(link){ const url = `${API_URL}${link}`; return axios.get(url).then(response => response.data); } getCustomer(pk) { const url = `${API_URL}/api/customers/${pk}`; return axios.get(url).then(response => response.data); } deleteCustomer(customer){ const url = `${API_URL}/api/customers/${customer.pk}`; return axios.delete(url); } createCustomer(customer){ const url = `${API_URL}/api/customers/`; return axios.post(url,customer); } updateCustomer(customer){ const url = `${API_URL}/api/customers/${customer.pk}`; return axios.put(url,customer); } } سيستدعي الصنف CustomersService توابع اكسيوس Axios التالية: ()getCustomers: يحصل على الصفحة الأولى من العملاء. ()getCustomersByURL: يحصل على العملاء بواسطة عنوان URL، ويتيح هذا بدوره الحصول على الصفحات التالية من العملاء بتمرير روابط، مثل "api/customers/?page=2/". ()getCustomer: يحصل على عميل بواسطة مفتاحه الأساسي. ()createCustomer: ينشئ عميلًا. ()updateCustomer: يحدّث عميلًا. ()deleteCustomer: يحذف عميلًا. نستطيع الآن عرض البيانات من واجهة برمجة التطبيقات الخاصة بنا في واجهة المستخدم لريآكت بإنشاء مكوّن CustomersList. الخطوة السابعة - عرض البيانات من واجهة برمجة التطبيقات في تطبيق ريآكت سننشئ في هذه الخطوة مكون ريآكت اسمه: CustomersList، الذي يمثّل جزءًا من واجهة المستخدم، ويمكنك أيضًا من تقسيم واجهة المستخدم إلى أقسام منفصلة وقابلة للاستخدام مجددًا. أنشئ أولًا الملف "CustomersList.js" في المجلد "frontend/src": $ nano ~/djangoreactproject/frontend/src/CustomersList.js ثم ابدأ في استيراد React و Component لإنشاء مكون ريآكت: import React, { Component } from 'react'; ثم استورد واستنسخ الوحدة CustomersService التي أنشأتها في الخطوة السابقة، والتي توفر توابع تُربط مع الواجهة الخلفية لواجهة برمجة تطبيقات ريست REST API: ... import CustomersService from './CustomersService'; const customersService = new CustomersService(); ثم أنشئ المكون CustomersList المُمتد عن الصنف Component، الذي سيستدعي واجهة برمجة التطبيقات لريست، إذ يجب أن يمتد مكون ريآكت عن الصنف Component أو أن يكون صنفًا فرعيًا عنه. أضف الشيفرة التالية إلى الملف "CustomersList.js" لإنشاء مكون ريآكت الذي يمتدّ عن المكوّن react.Component: ... class CustomersList extends Component { constructor(props) { super(props); this.state = { customers: [], nextPageURL: '' }; this.nextPage = this.nextPage.bind(this); this.handleDelete = this.handleDelete.bind(this); } } export default CustomersList; هيأنا في الشيفرة كائن الحالة state داخل الباني constructor؛ إذ يحتفظ هذا الكائن بحالة متغيرات مكوّننا باستخدام مصفوفة عملاء فارغة. ستحتفظ هذه المصفوفة بالعملاء والمتغير nextPageURL الذي يحتفظ بعنوان URL للصفحة التالية التي ستستعيدها من واجهة برمجة تطبيقات الواجهة الخلفية. ربطنا أيضًا التابعين ()nextPage و ()handleDelete بالمؤشر this بحيث يستطاع الوصول إليهما من شيفرة HTML، ثم أضفنا التابع ()componentDidMount واستدعاءً للتابع ()getCustomers ضمن الصنف CustomersList قبل قوس الإغلاق. يُعد التابع ()componentDidMount تابع دورة حياة المكوّن ويُستدعى عند إنشاء المكوّن وإدخاله إلى نموذج كائن الوثيقة DOM. يستدعي التابع ()getCustomers كائن خدمة العملاء Customers Service للحصول على صفحة البيانات الأولى ورابط الصفحة التالية من الواجهة الخلفية لجانغو: ... componentDidMount() { var self = this; customersService.getCustomers().then(function (result) { self.setState({ customers: result.data, nextPageURL: result.nextlink}) }); } أضف تحت التابع ()componentDidMount بالضبط التابع ()handleDelete، الذي يتولى مهمة حذف عميل: ... handleDelete(e,pk){ var self = this; customersService.deleteCustomer({pk : pk}).then(()=>{ var newArr = self.state.customers.filter(function(obj) { return obj.pk !== pk; }); self.setState({customers: newArr}) }); } يستدعي التابعُ ()handleDelete التابعَ ()deleteCustomer لحذف عميل باستخدام مفتاحه الأساسي، وإذا كانت العملية ناجحة، ستُرشّح filtered المصفوفة لتعكس آخر حالة بعد حذف ذلك العميل. بعد ذلك، أضف التابع ()nextPage لتحصل على بيانات الصفحة التالية وحدّث رابط الصفحة التالية: ... nextPage(){ var self = this; customersService.getCustomersByURL(this.state.nextPageURL).then((result) => { self.setState({ customers: result.data, nextPageURL: result.nextlink}) }); } يستدعي التابعُ ()nextPage التابعَ ()getCustomersByURL، الذي يأخذ عنوان URL للصفحة التالية من كائن الحالة this.state.nextPageURL ويحدّث مصفوفة العملاء customers بالبيانات المعادة. وأخيرًا، أضف التابع ()render، الذي يصيّر جدولًا للعملاء من حالة المكوّن: ... render() { return ( <div className="customers--list"> <table className="table"> <thead key="thead"> <tr> <th>#</th> <th>First Name</th> <th>Last Name</th> <th>Phone</th> <th>Email</th> <th>Address</th> <th>Description</th> <th>Actions</th> </tr> </thead> <tbody> {this.state.customers.map( c => <tr key={c.pk}> <td>{c.pk} </td> <td>{c.first_name}</td> <td>{c.last_name}</td> <td>{c.phone}</td> <td>{c.email}</td> <td>{c.address}</td> <td>{c.description}</td> <td> <button onClick={(e)=> this.handleDelete(e,c.pk) }> Delete</button> <a href={"/customer/" + c.pk}> Update</a> </td> </tr>)} </tbody> </table> <button className="btn btn-primary" onClick= { this.nextPage }>Next</button> </div> ); } وفيما يلي يظهر المحتوى الكامل للملف "CustomersList.js": import React, { Component } from 'react'; import CustomersService from './CustomersService'; const customersService = new CustomersService(); class CustomersList extends Component { constructor(props) { super(props); this.state = { customers: [], nextPageURL: '' }; this.nextPage = this.nextPage.bind(this); this.handleDelete = this.handleDelete.bind(this); } componentDidMount() { var self = this; customersService.getCustomers().then(function (result) { console.log(result); self.setState({ customers: result.data, nextPageURL: result.nextlink}) }); } handleDelete(e,pk){ var self = this; customersService.deleteCustomer({pk : pk}).then(()=>{ var newArr = self.state.customers.filter(function(obj) { return obj.pk !== pk; }); self.setState({customers: newArr}) }); } nextPage(){ var self = this; console.log(this.state.nextPageURL); customersService.getCustomersByURL(this.state.nextPageURL).then((result) => { self.setState({ customers: result.data, nextPageURL: result.nextlink}) }); } render() { return ( <div className="customers--list"> <table className="table"> <thead key="thead"> <tr> <th>#</th> <th>First Name</th> <th>Last Name</th> <th>Phone</th> <th>Email</th> <th>Address</th> <th>Description</th> <th>Actions</th> </tr> </thead> <tbody> {this.state.customers.map( c => <tr key={c.pk}> <td>{c.pk} </td> <td>{c.first_name}</td> <td>{c.last_name}</td> <td>{c.phone}</td> <td>{c.email}</td> <td>{c.address}</td> <td>{c.description}</td> <td> <button onClick={(e)=> this.handleDelete(e,c.pk) }> Delete</button> <a href={"/customer/" + c.pk}> Update</a> </td> </tr>)} </tbody> </table> <button className="btn btn-primary" onClick= { this.nextPage }>Next</button> </div> ); } } export default CustomersList; الآن وبعد أنشأنا المكون CustomersList المسؤول عن عرض قائمة العملاء يمكننا الآن إضافة المكون الذي يتولى مهمة إنشاء العملاء وتحديثهم. الخطوة الثامنة - إضافة مكون ريآكت لإنشاء العملاء وتحديثهم سننشئ في هذه الخطوة المكون CustomerCreateUpdate الذي سيتولى مهمة إنشاء وتحديث العملاء، وذلك بإتاحة نموذج تعبئة form للمستخدمين لملئها ببيانات عميل جديد أو تحديث بيانات عميل موجود. أنشئ الملف "CustomerCreateUpdate.js" داخل المجلد "frontend/src": $ nano ~/djangoreactproject/frontend/src/CustomerCreateUpdate.js أضف الشيفرة التالية لإنشاء مكون ريآكت الذي يستورد React و Component: import React, { Component } from 'react'; يمكنك أيضًا استيراد واستنساخ الصنف CustomersService الذي أنشأناه في الخطوة السابقة والذي يزود بتوابع توصل مع الواجهة الخلفية لواجهة برمجة تطبيقات ريست: ... import CustomersService from './CustomersService'; const customersService = new CustomersService(); ثم أنشئ المكون CustomerCreateUpdate المُمتدّ عن Component لإنشاء العملاء وتحديثهم: ... class CustomerCreateUpdate extends Component { constructor(props) { super(props); } } export default CustomerCreateUpdate; أضف ضمن تعريف الصنف تابعَ المكوّن ()render الذي يصيّر نموذج HTML الذي يجمع معلومات عن العميل. ... render() { return ( <form onSubmit={this.handleSubmit}> <div className="form-group"> <label> First Name:</label> <input className="form-control" type="text" ref='firstName' /> <label> Last Name:</label> <input className="form-control" type="text" ref='lastName'/> <label> Phone:</label> <input className="form-control" type="text" ref='phone' /> <label> Email:</label> <input className="form-control" type="text" ref='email' /> <label> Address:</label> <input className="form-control" type="text" ref='address' /> <label> Description:</label> <textarea className="form-control" ref='description' ></textarea> <input className="btn btn-primary" type="submit" value="Submit" /> </div> </form> ); } يضيف هذا التابع الخاصية ref للوصول إلى عنصر النموذج التعبئة ووضع قيمة له وذلك من أجل كل عنصر إدخال ضمن نموذج التعبئة. الآن، عرّف التابعَ (handleSubmit(event فوق التابع ()render لكي تمتلك الوظائف المناسبة عندما ينقر عميل على زر الإرسال submit: ... handleSubmit(event) { const { match: { params } } = this.props; if(params && params.pk){ this.handleUpdate(params.pk); } else { this.handleCreate(); } event.preventDefault(); } … يعالج التابع (handleSubmit(event إرسال النموذج ويستدعي -اعتمادًا على المسار- إما التابع (handleUpdate(pk لتحديث العميل بالمفتاح الأساسي الممرر، أو التابع ()handleCreate لإنشاء عميل جديد. سنعرّف هذين التابعين باختصار. عد إلى باني المكون، واربط التابع ()handleSubmit بالمؤشر this لكي تتمكن من الوصول إليه في نموذجك: ... class CustomerCreateUpdate extends Component { constructor(props) { super(props); this.handleSubmit = this.handleSubmit.bind(this); } ... ثم عرّف التابع ()handleCreate لإنشاء عميل من بيانات نموذج التعبئة، وأضف الشيفرة التالية فوق التابع (handleSubmit(event: ... handleCreate(){ customersService.createCustomer( { "first_name": this.refs.firstName.value, "last_name": this.refs.lastName.value, "email": this.refs.email.value, "phone": this.refs.phone.value, "address": this.refs.address.value, "description": this.refs.description.value }).then((result)=>{ alert("Customer created!"); }).catch(()=>{ alert('There was an error! Please re-check your form.'); }); } … سوف يُستخدَم التابع ()handleCreate من أجل إنشاء عميل من البيانات المُدخلة، إذ يستدعي التابع المقابل ()CustomersService.createCustomer الذي يجري استدعاءً لواجهة برمجة التطبيقات الفعلية للواجهة الخلفية لينشئ عميلًا جديدًا. بعد ذلك عرّف التابع (handleUpdate(pk الذي يجري التحديثات أسفل التابع ()handleCreate: ... handleUpdate(pk){ customersService.updateCustomer( { "pk": pk, "first_name": this.refs.firstName.value, "last_name": this.refs.lastName.value, "email": this.refs.email.value, "phone": this.refs.phone.value, "address": this.refs.address.value, "description": this.refs.description.value } ).then((result)=>{ alert("Customer updated!"); }).catch(()=>{ alert('There was an error! Please re-check your form.'); }); } سيحدّث التابع ()updateCustomer العميل ذا المفتاح الأساسي pk بالمعلومات الجديدة التي جلبها من نموذج معلومات العميل، ويستدعي التابع ()customersService.updateCustomer. الآن، أضف التابع ()componentDidMount، وبالتالي إذا زار المستخدم الوجهة "customer/:pk"، فإننا نريد تعبئة النموذج بمعلومات مرتبطة بالعميل باستخدام المفتاح الأساسي من عنوان URL، ولفعل ذلك يمكننا إضافة التابع (getCustomer(pk بعد أن يُثبّت المكون في حدث دورة الحياة للتابع ()componentDidMount. أضف الشيفرة التالية أسفل باني المكون لإضافة هذا التابع: ... componentDidMount(){ const { match: { params } } = this.props; if(params && params.pk) { customersService.getCustomer(params.pk).then((c)=>{ this.refs.firstName.value = c.first_name; this.refs.lastName.value = c.last_name; this.refs.email.value = c.email; this.refs.phone.value = c.phone; this.refs.address.value = c.address; this.refs.description.value = c.description; }) } } وفيما يلي المحتوى الكامل للملف "CustomerCreateUpdate.js": import React, { Component } from 'react'; import CustomersService from './CustomersService'; const customersService = new CustomersService(); class CustomerCreateUpdate extends Component { constructor(props) { super(props); this.handleSubmit = this.handleSubmit.bind(this); } componentDidMount(){ const { match: { params } } = this.props; if(params && params.pk) { customersService.getCustomer(params.pk).then((c)=>{ this.refs.firstName.value = c.first_name; this.refs.lastName.value = c.last_name; this.refs.email.value = c.email; this.refs.phone.value = c.phone; this.refs.address.value = c.address; this.refs.description.value = c.description; }) } } handleCreate(){ customersService.createCustomer( { "first_name": this.refs.firstName.value, "last_name": this.refs.lastName.value, "email": this.refs.email.value, "phone": this.refs.phone.value, "address": this.refs.address.value, "description": this.refs.description.value } ).then((result)=>{ alert("Customer created!"); }).catch(()=>{ alert('There was an error! Please re-check your form.'); }); } handleUpdate(pk){ customersService.updateCustomer( { "pk": pk, "first_name": this.refs.firstName.value, "last_name": this.refs.lastName.value, "email": this.refs.email.value, "phone": this.refs.phone.value, "address": this.refs.address.value, "description": this.refs.description.value } ).then((result)=>{ console.log(result); alert("Customer updated!"); }).catch(()=>{ alert('There was an error! Please re-check your form.'); }); } handleSubmit(event) { const { match: { params } } = this.props; if(params && params.pk){ this.handleUpdate(params.pk); } else { this.handleCreate(); } event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <div className="form-group"> <label> First Name:</label> <input className="form-control" type="text" ref='firstName' /> <label> Last Name:</label> <input className="form-control" type="text" ref='lastName'/> <label> Phone:</label> <input className="form-control" type="text" ref='phone' /> <label> Email:</label> <input className="form-control" type="text" ref='email' /> <label> Address:</label> <input className="form-control" type="text" ref='address' /> <label> Description:</label> <textarea className="form-control" ref='description' ></textarea> <input className="btn btn-primary" type="submit" value="Submit" /> </div> </form> ); } } export default CustomerCreateUpdate; وهكذا بعد إنشاء المكوّن CustomerCreateUpdate، أصبح بإمكاننا تحديث المكوّن App الرئيسي لإضافة روابط للمكونات المختلفة التي أنشأناها. الخطوة التاسعة - تحديث مكون التطبيق الرئيسي سنحدّث في هذا القسم المكوّن App من تطبيقنا لإنشاء روابط إلى المكونات التي أنشأناها في الخطوات السابقة. نفذ الأمر التالي من المجلد "frontend"، لتثبيت موجه ريآكت الذي يمكّنك من إضافة المسارات والتنقلات بين مكونات ريآكت المتعددة. $ cd ~/djangoreactproject/frontend $ npm install --save react-router-dom والآن افتح الملف "djangoreactproject/frontend/src/App.js/~": $ nano ~/djangoreactproject/frontend/src/App.js احذف كل شيء هناك وأضف الشيفرة التالية لاستيراد الأصناف اللازمة لإضافة التوجيه، إذ تتضمن هذه الأصناف الصنفَ BrowserRouter الذي ينشئ مكوّن الموجّه والصنفَ Route الذي ينشئ مكوّن الوُجهة route: import React, { Component } from 'react'; import { BrowserRouter } from 'react-router-dom' import { Route, Link } from 'react-router-dom' import CustomersList from './CustomersList' import CustomerCreateUpdate from './CustomerCreateUpdate' import './App.css'; ملاحظة: يحافظ BrowserRouter على التزامن بين واجهة المستخدم وعنوان URL باستخدام واجهة برمجة تطبيقات تاريخ HTML5. أنشئ نمطًا أساسيًّا ليزودنا بالمكون الأساسي الذي سيُغلِّفُه المكون BrowserRouter. ... const BaseLayout = () => ( <div className="container-fluid"> <nav className="navbar navbar-expand-lg navbar-light bg-light"> <a className="navbar-brand" href="#">Django React Demo</a> <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation"> <span className="navbar-toggler-icon"></span> </button> <div className="collapse navbar-collapse" id="navbarNavAltMarkup"> <div className="navbar-nav"> <a className="nav-item nav-link" href="/">CUSTOMERS</a> <a className="nav-item nav-link" href="/customer">CREATE CUSTOMER</a> </div> </div> </nav> <div className="content"> <Route path="/" exact component={CustomersList} /> <Route path="/customer/:pk" component={CustomerCreateUpdate} /> <Route path="/customer/" exact component={CustomerCreateUpdate} /> </div> </div> ) استخدمنا بالشيفرة أعلاه المكون Route لتعريف وجهات تطبيقنا، وهو المكون الذي يتعيّن على الموجّه تحميله عندما يجد تطابقًا. تحتاج كل وجهة إلى مسار path لتحديد المسار الذي سيجري مطابَقته، وإلى مكوّن component لتحديد المكون الذي سيحمّله. تخبر الخاصّيّة exact الموجّه أن يطابق المسار الصحيح. وأخيرًا أنشئ المكون App المكوّن الجذر أو مكوّن مستوى القمة في تطبيق ريآكت: ... class App extends Component { render() { return ( <BrowserRouter> <BaseLayout/> </BrowserRouter> ); } } export default App; وبما أننا نريد أن يعمل التطبيق في المتصفح، فقد غلّفنا المكوّن BaseLayout بالمكوّن BrowserRouter. الآن يبدو ملف "App.js" المكتمل شبيهًا بما يلي: import React, { Component } from 'react'; import { BrowserRouter } from 'react-router-dom' import { Route, Link } from 'react-router-dom' import CustomersList from './CustomersList' import CustomerCreateUpdate from './CustomerCreateUpdate' import './App.css'; const BaseLayout = () => ( <div className="container-fluid"> <nav className="navbar navbar-expand-lg navbar-light bg-light"> <a className="navbar-brand" href="#">Django React Demo</a> <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation"> <span className="navbar-toggler-icon"></span> </button> <div className="collapse navbar-collapse" id="navbarNavAltMarkup"> <div className="navbar-nav"> <a className="nav-item nav-link" href="/">CUSTOMERS</a> <a className="nav-item nav-link" href="/customer">CREATE CUSTOMER</a> </div> </div> </nav> <div className="content"> <Route path="/" exact component={CustomersList} /> <Route path="/customer/:pk" component={CustomerCreateUpdate} /> <Route path="/customer/" exact component={CustomerCreateUpdate} /> </div> </div> ) class App extends Component { render() { return ( <BrowserRouter> <BaseLayout/> </BrowserRouter> ); } } export default App; بعد إضافة التوجيه إلى تطبيقنا، أصبحنا الآن جاهزين لاختبار التطبيق. انتقل إلى العنوان "http://localhost:3000"، ويفترض أن ترى الصفحة الأولى من التطبيق: وبهذا يكتمل التطبيق، وأصبح لديك الآن الأساس لتطبيق إدارة علاقات العملاء CRM. الخاتمة أنشأت في هذا المقال تطبيقًا تجريبيًّا باستخدام جانغو وريآكت، واستخدمت إطار عمل جانغو ريست لبناء واجهة برمجة التطبيقات لريست REST واكسيوس Axios لاستهلاك واجهة برمجة التطبيقات وبوتستراب 4 لتنسيق CSS. يمكنك العثور على الشيفرة المصدرية لهذا المشروع في مستودع غيت هب GitHub. ترجمة - وبتصرف - للمقالة How To Build a Modern Web Application to Manage Customer Information with Django and React on Ubuntu 18.04 لصاحبها Ahmed Bouchefra. اقرأ أيضًا بناء تطبيق مهام باستخدام جانغو Django وريآكت React البدء مع إطار العمل جانغو لإنشاء تطبيق ويب بناء تطبيق ويب من طرف الخادم باستخدام Nuxt.js وجانغو Django بناء تطبيق يعرض أحوال الطقس باستخدام جانغو Django
  4. سننشئ في هذه المقالة تطبيق جانغو مهمته عرض أحوال الطقس الحالية لعدّة مدن. ستتولى واجهة برمجة التطبيقات لخرائط الطقس المفتوحة Open Weather Map API مسؤولية توفير بيانات الطقس الحالية. سنستخدم في عملنا قاعدة بيانات وننشئ نموذجًا، بحيث يمكن تطبيق ما سنتعلمه هنا لاحقًا في المشاريع الأعقد. المتطلبات الأساسية يلزم لهذا المشروع تثبيت بايثون Python على جهازك، وأن تتمكن من الرجوع إلى هذه السلسلة من المقالات التعليمية إذا احتجت إلى معلومات أكثر. كُتبت الشيفرة المكتوبة في مقالنا هذا ببايثون 3 وجانغو 3.0، ولذلك لا بُد أن تكون ملمًا بهما جيدًا. الخطوة الأولى - تجهيز المشروع من السهل تثبيت جانغو كما هو حال أي مكتبة من مكتبات بايثون، بأحد الطريقتين التاليتين: فتح بيئة افتراضية وتنفيذ الأمر pip لتثبيت جانغو. إنشاء مجلد مشروع وتشغيل أداة pipenv، ثم تفعيل صدفة pipenv. وتؤدي كلتا الطريقتين الغرض، لكننا سنستخدم في مقالتنا هذه أداة pipenv. ملاحظة: يجب أن تكون قادرًا على الرجوع إلى هذه السلسلة من المقالات التعليمية إذا أردت استخدام طرق تثبيت جانغو الأخرى. يحتوي التوثيق الرسمي لجانغو على تعليمات تثبيت pipenv، إما باستخدام Homebrew، أو Linuxbrew. يمكنك أيضًا تثبيت pipenv باستخدام pip. أنشئ باستخدام نافذة الطرفية مجلدًا للبيئة: $ mkdir the_weather_env ثم انتقل إلى داخله: $ cd the_weather_env ثم استخدم pipenv لتثبيت جانغو: $ pipenv install django سيؤدي هذا إلى تثبيت آخر إصدار من جانغو على جهازك، وفي حالتنا كان آخر إصدار هو 3.0.5. استخدم أيضًا pipenv لتثبيت مكتبة الطلبات Request التي ستستخدمها فيما بعد: $ pipenv install requests فعّل البيئة الافتراضية virtualenv للمشروع بتنفيذ الأمر التالي في الطرفية: $ pipenv shell سيولد هذا عمليةً فرعيةً جديدةً للصدفة. الخطوة الثانية - افتتاح مشروع جانغو بعد أن تنتهي من تثبيت جانغو أنشئ مجلدًا لهذا المشروع وانتقل إليه (إذا لم تكن قد فعلت ذلك). يمكنك تنفيذ الأمر startproject الذي يوفره جانغو لتولّد المشروع. (the_weather_env) django-admin startproject the_weather سيكون جانغو قد أنشأ بعد هذا الأمر بضعة ملفات في المجلد الذي تعمل فيه. لنجرب الآن تشغيل خادم التطوير، انتقل إلى المجلد الجديد في الطرفية: (the_weather_env) cd the_weather والآن استخدم manage.py لتنفيذ الأمر unserver في الطرفية: (the_weather_env) python manage.py runserver سترى إذا دققت في الطرفية عنوان محدد الموارد الموحد URL لتطبيقك الذي يجب أن تكون قيمته الافتراضية هي "127.0.0.1:8000"، كما هو موضح في الصورة التالية: افتح الآن متصفح الويب الذي تستخدمه واذهب إلى ذلك العنوان: إذا كانت الصفحة صفحة تهنئة فاعلم أنك نجحت في إعداد جانغو بسلام. الخطوة الثالثة - تسجيل الدخول إلى لوحة تحكم الإدارة الآن سندخل إلى لوحة الإدارة التي يوفرها لنا جانغو. لكن يجب أولًا تهجير قاعدة البيانات التي نعمل عليها وهذا يعني أن ينشئ جانغو الجداول المعرفة مسبقًا والضرورية للتطبيقات الافتراضية. نحتاج أولًا لإيقاف تشغيل الخادم بالضغط إما على المفتاحين "CONTROL+C"، أو "CTRL+C" بحسب البيئة المُستخدمة، ثم ننفذ الأمر migrate في الطرفية: (the_weather_env) python manage.py migrate سينشئ جانغو عندئذٍ قاعدة بيانات SQLite وهي الافتراضية في الإعدادات، وسيضيف عدة جداول إليها. إذا رأيت في مجلد المشروع ملفًا جديدًا اسمه "db.sqlite3" فاعلم أن قاعدة البيانات قد أنشئت. من الجداول التي يوفرها جانغو: جدول المستخدمين user table، الذي يخزن بيانات مستخدمي التطبيق، ولن نحتاج في تطبيقنا الذي نعمل عليه لأي مستخدمين باستثناء المستخدم المدير admin الذي يمكّننا من الوصول إلى لوحة تحكم الإدارة. وعلى هذا، لننشئ مستخدم المدير بتنفيذ الأمر createsuperuser في الطرفية: (the_weather_env) python manage.py createsuperuser سيُطلب منك تحديد اسم المستخدم وعنوان البريد الإلكتروني وكلمة المرور، اكتبها ثم أعد تشغيل الخادم في الطرفية: (the_weather_env) python manage.py runserver اذهب من شاشة المتصفح إلى العنوان "127.0.0.1:8000‎/admin" الذي هو عنوان لوحة تحكم المدير، وننوه هنا إلى أن إعداد المستخدم admin ضمن الملف "urls.py" هو الذي مكّنك من الذهاب إلى تلك الصفحة. وعند تسجيل دخولك باسم المستخدم وكلمة المرور اللذين اخترتهما للتو فستدخل إلى لوحة إدارة جانغو. يظهر في الصورة السابقة نموذجان يتيح لك جانغو الوصول إليهما، وهما: Groups و Users؛ إذ أن النماذج في حقيقتها ما هي إلا تمثيلات لجداول قاعدة البيانات. لذلك، هذان النموذجان انعكاسًا لجدولي قاعدة البيانات المقابلين، ومع أن جانغو قد أنشأ جداول أخرى غيرهما، إلا أنه لا حاجة بنا للوصول إليها مباشرةً لذا لم تُنشأ نماذج تقابلها. إذا نقرت على Users ستظهر لك تفاصيل أوسع عن جدول المستخدمين وسترى المستخدم الذي أنشاته. اقض بعض الوقت في استكشاف لوحة الإدارة بالنقر على الروابط المختلفة لرؤية ما هو موجود، لكن حذارِ أن تحذف المستخدم لكي لا تضطر إلى تنفيذ الأمر createsuperuser مجددًا. دعنا نغادر لوحة الإدارة الآن ونركز على كتابة الشيفرة. ستحتاج إلى إنشاء تطبيق داخل مشروعك ليجلب لنا معلومات حالة الطقس. الخطوة الرابعة - إنشاء التطبيق يمكّننا جانغو من فصل الوظائف البرمجية عن بعضها بعضًا في المشروع الواحد باستخدام التطبيقات، فكل تطبيق يؤدي وظيفة معينة مُكلفٌ بها ضمن المشروع الواحد؛ فإذا نظرنا إلى الملف "settings.py" على سبيل المثال، فسنرى قائمة التطبيقات المثبتة "INSTALLED_APPS". وأول التطبيقات المثبتة هو "django.contrib.admin" الذي استخدمته للتو، إذ يتولى هذا التطبيق مسؤولية وظائف الإدارة فقط ولا شيء غير ذلك. مثال آخر على تطبيق موجود في المشروع افتراضيًّا، هو التطبيق "django.contrib.auth" الذي سمح لنا بتسجيل الدخول إلى لوحة الإدارة. نحتاج في حالتنا إلى إنشاء تطبيق جديد يتولى مهمة إظهار حالة الطقس. أوقف تشغيل الخادم أولًا، ثم نفذ الأمر startapp في الطرفية: (the_weather_env) python manage.py startapp weather سيضيف جانغو عند تنفيذ هذا الأمر مجلدًا جديدًا وبعض الملفات إلى المشروع. دعنا ننشئ مع الملفات التي وُلِّدت للتو ملفًا جديدًا اسمه "urls.py" في مجلد التطبيق weather: from django.urls import path urlpatterns = [ ] هذا الملف مماثل للملف "urls.py" في مجلد "the_weather"، ويتمثّل الفرق بينهما هو أن هذا الملف "urls.py" يحتوي على جميع عناوين URL المرتبطة بالتطبيق نفسه. لم تحدِّد أي عناوين URL بعد، لكن يمكنك إعداد المشروع لكي يتعرف على تطبيقك ويوجه أي عناوين URL معينة لتطبيقك إلى ملف التطبيق "urls.py". اذهب إلى قائمة التطبيقات المثبتة INSTALLED_APPS في الملف settings.py وأضف اسم التطبيق weather إلى القائمة: ... INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'weather', ] ... وبهذا تخبر جانغو أنك تريد استخدام التطبيق "weather" في مشروعك، وعندئذ سيعلم جانغو أين سيبحث عن ملفات التهجير وعناوين URL. ستحتاج الآن إلى تعديل الملف الأصلي urls.py بحيث يشير إلى ملف urls.py الذي في التطبيق، ولذلك أضف سطرًا تحت المسار الموجود للوحة الإدارة، كما ستحتاج أيضًا إلى استيراد المكتبة include لتتمكن من الإشارة إلى ملف urls.py للتطبيق: from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('weather.urls')), ] تعني السلسلة الفارغة أنك لن تحتاج لاستخدام نقطة نهاية من أجل نقطة الدخول إلى تطبيقنا، بل سنترك للتطبيق مهمة معالجة أي نقاط نهاية محددة، وبالرفم من أنه كان بإمكاننا وضع شيء على نحو (...,'/path('weather، والذي كان سيُلزِمنا بكتابة /127.0.0.1:8000‎/weather للحصول على أي شيء مرافق لتطبيق الطقس، لكن لأن مشروعنا بسيط أصلًا فلن نفعل هذا هنا. الخطوة الخامسة - إضافة القالب والعرض سنحتاج الآن لإضافة القالب إلى المشروع الذي نعمل عليه؛ والقالب في جانغو ما هو إلا ملف HTML يتيح لنا تعليمات أوسع في شيفرته، الأمر الذي يجعل القالب ديناميكيًا، إذ سنتمكن فيه من معالجة وظائف، مثل إضافة المتغيرات والتعليمات الشرطية if والحلقات. نذهب في الطرفية إلى مجلد التطبيق "weather": (the_weather_env) cd weather ثم ننشئ المجلد "templates": (the_weather_env) mkdir templates ثم ننتقل إلى داخله: (the_weather_env) cd templates أنشئ مجلدًا آخر يحمل نفس اسم التطبيق، لأن جانغو يدمج جميع مجلدات القوالب من التطبيقات الموجودة سابقًا لدينا معًا، وللحيلولة دون تكرار أسماء الملفات، يمكنك استخدام اسم التطبيق: (the_weather_env) mkdir weather من داخل مجلد weather هذا، أنشئ ملفًا جديدًا اسمه index.html الذي سيكون القالب الأساسي. وفيما يلي شيفرة HTML التي ستستخدمها للقالب: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>What's the weather like?</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.css" /> </head> <body> <section class="hero is-primary"> <div class="hero-body"> <div class="container"> <h1 class="title"> What's the weather like? </h1> </div> </div> </section> <section class="section"> <div class="container"> <div class="columns"> <div class="column is-offset-4 is-4"> <form method="POST"> <div class="field has-addons"> <div class="control is-expanded"> <input class="input" type="text" placeholder="City Name"> </div> <div class="control"> <button class="button is-info"> Add City </button> </div> </div> </form> </div> </div> </div> </section> <section class="section"> <div class="container"> <div class="columns"> <div class="column is-offset-4 is-4"> <div class="box"> <article class="media"> <div class="media-left"> <figure class="image is-50x50"> <img src="http://openweathermap.org/img/w/10d.png" alt="Image"> </figure> </div> <div class="media-content"> <div class="content"> <p> <span class="title">Las Vegas</span> <br> <span class="subtitle">29° F</span> <br> thunderstorm with heavy rain </p> </div> </div> </article> </div> </div> </div> </div> </section> <footer class="footer"> </footer> </body> </html> ملاحظة: استخدمنا مكتبة بولما Bulma خلف الكواليس لمعالجة التنسيقات styling والتخطيطات layout. بعد أن فرغت من إنشاء القالب ستنشئ مزيجًا يضم عرضًا view وعنوان URL معًا، لتتمكن من رؤية هذا فعليًا في التطبيق؛ والعروض في جانغو هي إما دوال أو أصناف، وفي حالتنا هذه ونظرًا لأنك تنشئ عرضًا بسيطًا فستنشئ دالةً، ثم تضيف هذه الدالة إلى ملف views.py: from django.shortcuts import render def index(request): return render(request, 'weather/index.html') #returns the index.html template سُمّي العرض باسم "index" لأنه سيكون في فهرس التطبيق الذي هو عنوان URL الجذر. لا بُد من إعادة request لتصيير القالب، لأنه ضروري لدالة render واسم ملف القالب الذي تريد تصييره، والذي هو في حالتنا weather/index.html. دعنا الآن نضيف عنوان URL الذي سيرسل الطلب إلى هذا العرض. لذلك، حدِّث القائمة urlpatterns في ملف urls.py للتطبيق كما يلي: from django.urls import path from . import views urlpatterns = [ path('', views.index), #the path for our index view ] يتيح لك هذا فهرسة العرض الذي أنشأته للتو. سيطابق جانغو أي عنوان URL ليس لديه نقطة نهاية ويوجهه إلى تابع العرض الذي أنشأته. الآن، اذهب إلى نافذة الطرفية وعد فيها إلى جذر المشروع (the_weather)، ثم شغّل الخادم: (the_weather_env) python manage.py runserver ثم افتح متصفح الويب واذهب للعنوان 127.0.0.1:8000 مرةً أخرى. سترى صفحة HTML المُصيرة للملف index.html، ويوجد فيها حقل يمكّنك من إضافة مدينة، ويظهر أيضًا قيمةٌ ضمنيةٌ مسبقةٌ لطقس لاس فيجاس، لكن ما يزال النموذج غير جاهز والطقس في الوقت الحالي هو مجرد موضع مؤقت placeholder، لذا سنعمل على هذه الأمور في الخطوات التالية. الخطوة السادسة - استخدام واجهة برمجة تطبيقات الطقس Weather API يجب عليك أولًا إنشاء حساب في موقع واجهة برمجة تطبيقات الطقس. وهو التطبيق الذي سيوفر لك معلومات الطقس في الزمن الحقيقي لأي مدن تختار إضافتها إلى التطبيق. لذا، اذهب إلى الموقع وأنشئ حسابًا، ثم اذهب إلى مفاتيح واجهة برمجة التطبيقات في لوحة التحكم التي لديهم، ويمكنك استخدام المفتاح الافتراضي الذي يزودونك به، أو أن تنشئ مفتاحًا جديدًا لواجهة برمجة التطبيقات؛ إذ سيتيح لك هذا المفتاح استخدام واجهة برمجة التطبيقات للحصول على حالة الطقس. ملاحظة: من المهم الحفاظ على سرية مفاتيح واجهة برمجة التطبيقات الطقس لمنع الأطراف الخارجية من استخدامها. تخيل ماذا سيحدث لو رُبِط مفتاح واجهة برمجة التطبيقات بمستودع بعيد مثل مستودعات Github. لا أعتقد أن أحدًا منا يرغب في أن يحدث هذا معه. نقطة النهاية الوحيدة التي ستستخدمها موضحةٌ فيما يلي، إذ يمكنك رؤية البيانات المعادة بتعديل عنوان URL التالي بحيث يحتوي مفتاح واجهة برمجة التطبيقات، ثم الذهاب إلى عنوان URL في المتصفح: http://api.openweathermap.org/data/2.5/weather?q=las%20vegas&units=imperial&appid=YOUR_APP_KEY يستغرق تفعيل مفتاح واجهة برمجة التطبيقات بعض الوقت، لذا إذا لم يعمل في بادئ الأمر فانتظر بضع دقائق، ثم كرّر المحاولة. من المتوقع الحصول على استجابة بصيغة JSON تحتوي على الإحداثيات ودرجات الحرارة وأحوال الطقس. دعنا الآن نضيف طلبًا للحصول على البيانات وإخراجها إلى التطبيق، ونعدّل العرض index لإرسال طلب إلى عنوان URL الذي لديك. from django.shortcuts import render import requests def index(request): url = 'http://api.openweathermap.org/data/2.5/weather?q={}&units=imperial&appid=YOUR_APP_KEY' city = 'Las Vegas' city_weather = requests.get(url.format(city)).json() #request the API data and convert the JSON to Python data types return render(request, 'weather/index.html') #returns the index.html template أضف الآن import requests و url و city و city_weather. ما فعلته في السطور السابقة الجديدة هو أنك أضفت عنوان URL الذي سترسِل الطلب إليه، ونلاحظ اختلاف عنوان URL قليلًا عن عنوان URL الذي اختبرته من قبل في المتصفح، فالمدينة هنا ليست جزءًا من URL بل أصبحت داخل متغير، وبهذا سيمكنك هذا النمط من تبديل أسماء مدن أخرى في المستقبل. سنعطي في الوقت الحالي القيمة الثابتة "Las Vegas" للمدينة city، لكن في المستقبل ستحدّد بقيم المدن الموجودة في قاعدة البيانات. أخيرًا، سترسل الطلب إلى عنوان URL باستخدام المدينة وتحصل على تمثيل JSON لتلك المدينة. إذا جربت طباعة print النتيجة إلى الطرفية، ستتمكن من رؤية نفس البيانات التي حصلت عليها عندما وضعت عنوان URL في شريط العنوان: ... def index(request): ... print(city_weather) #temporarily view output return render(request, 'weather/index.html') #returns the index.html template إذا أعدت تحميل الصفحة في المتصفح فسترى أن البيانات طُبعت إلى الطرفية. كان الغرض من إضافة تعليمة print هو التحقق من صحة ما ذكرناه. الآن وقد حصل ذلك، سنحذف تعليمة print من الشيفرة. الخطوة السابعة - عرض البيانات في القالب ستحتاج في هذه الخطوة لتمرير البيانات إلى القالب لعرضها على المستخدم. لننشئ قاموسًا يخزن كافة البيانات التي تحتاجها. ستحتاج من بين البيانات المعادة temp و description و icon. ... def index(request): ... weather = { 'city' : city, 'temperature' : city_weather['main']['temp'], 'description' : city_weather['weather'][0]['description'], 'icon' : city_weather['weather'][0]['icon'] } return render(request, 'weather/index.html') #returns the index.html template الآن وقد بات لديك كل المعلومات التي تريدها، يمكنك أن تمررها إلى القالب، ولذلك ستنشئ متغيرًا اسمه context سيكون قاموسًا يسمح لك باستخدام قيَمِهِ داخل القالب، ثم ستضيف في الدالة render وسيطًا ثالثًا هو context كما يلي: ... def index(request): ... context = {'weather' : weather} return render(request, 'weather/index.html', context) #returns the index.html template وهكذا بعد أن أصبحت بيانات الطقس داخل الـمتغير context، دعنا نذهب إلى القالب لنضيف هذه البيانات؛ فكل ما تحتاج فعله داخل القالب index.html هو تعديل شيفرة HTML لتستخدم المتغيرات بدلًا من استخدام القيم المسبقة الضمنية، إذ ستستخدِم هذه المتغيّرات الوسوم {{ }}، وستسنِد reference كل شيء داخل قاموس context. نلاحظ أن جانغو يحوّل قيم القاموس، بحيث لا يمكن الوصول إليها إلا باستخدام صيغة الاستدعاء النقطية dot notation، إذ سنحصل مثلًا عند استخدام weather.city على اسم المدينة. لا تستخدم ['weather['city كما هو مُستخدم في بايثون. ابحث عن الصندوق <div> وعدّله ليستخدم المتغيرات: ... <div class="box"> <article class="media"> <div class="media-left"> <figure class="image is-50x50"> <img src="http://openweathermap.org/img/w/{{ weather.icon }}.png" alt="Image"> </figure> </div> <div class="media-content"> <div class="content"> <p> <span class="title">{{ weather.city }}</span> <br> <span class="subtitle">{{ weather.temperature }}° F</span> <br> {{ weather.description }} </p> </div> </div> </article> </div> ... وهكذا بعد استبدال جميع المتغيرات، ستحصل على قيم الطقس الحالية للمدينة التي حددتها. لكن ما تزال المدينة تظهر قيمتها ضمنية المسبقة، لذا فما ستفعله الآن هو سحب القيم من قاعدة البيانات وعرض بيانات المدن الموجودة فيها. أنشئ لهذا الغرض جدولًا في قاعدة البيانات يحتوي أسماء المدن التي تريد معرفة حالة طقسها، وهذا يعني بطبيعة الحال أننا سننشئ نموذجًا مقابلًا له. اذهب إلى الملف "models.py" في التطبيق "weather" وأضف الشيفرة التالية: from django.db import models class City(models.Model): name = models.CharField(max_length=25) def __str__(self): #show the actual city name on the dashboard return self.name class Meta: #show the plural of city as cities instead of citys verbose_name_plural = 'cities' سينشئ هذه الشيفرة جدولًا في قاعدة البيانات بعمود اسمه name يدل على اسم المدينة، وسيكون نوع المدينة هو charfield أي سلسلة عاديّة. ولقبول هذه التغيرات وتطبيقها على قاعدة البيانات، ستحتاج لتنفيذ الأمر makemigrations الذي سيولد الشيفرة التي ستحدِّث قاعدة البيانات وتجري عمليات التهجير إليها. دعنا نوقف تشغيل الخادم وننفذ تلك التهجيرات في نافذة الطرفية: (the_weather_env) python manage.py makemigrations ثم نهجّر: (the_weather_env) python manage.py migrate نحتاج لتمكين رؤية النموذج في لوحة الإدارة، ولهذا لا بُد من تسجيله في الملف "admin.py" كما يلي: from django.contrib import admin from .models import City admin.site.register(City) الآن نعيد تشغيل الخادم وننظر في لوحة الإدارة في المتصفح: كما نرى فقد أصبحت City الآن أحد الخيارات. يمكن بعد ذلك الذهاب إلى لوحة الإدارة وإضافة بعض المدن. على سبيل المثال: لندن وطوكيو ولاس فيجاس. مع وجود هذه الإدخالات في قاعدة البيانات، ستحتاج الآن إلى الاستعلام عنها وإظهارها في العرض. ابدأ باستيراد النموذج City ومن ثم الاستعلام منه عن جميع الكائنات. from django.shortcuts import render import requests from .models import City ثم نحدّث الطلب request بالمدن cities: ... def index(request): url = 'http://api.openweathermap.org/data/2.5/weather?q={}&units=imperial&appid=YOUR_APP_KEY' cities = City.objects.all() #return all the cities in the database ... الآن وقد أصبحت لديك قائمة المدن، ستحتاج إلى المرور فيها جميعًا بحلقة تكرار وجلب حالة الطقس لكل منها وإضافتها جميعًا إلى قائمةٍ ستُمرّر في نهاية الأمر إلى القالب، وسيكون هذا مجرد شكلٍ آخر لما فعلناه في خطوة سابقة؛ والفرق هو أنك تستخدم هنا المرور بحلقة مع إلحاق كل قاموس بقائمة. ستنشئ أولًا قائمة بيانات الطقس weather_data التي ستحتفظ بقيمة الطقس weather لكل مدينة city، ثم نستبدل متغيّر المدينة city الأصلي بحلقة تمر بكل المدن cities. بعد ذلك، يجب إلحاق كل استجابة weather لكل مدينةcity ببيانات الطقس weather_data. سنحتاج أيضًا إلى تحديث السياق context لتمرير هذه القائمة بدلًا من تمرير قاموس وحيد. في هذه اللحظة يُفترض أن يكون الملف views.py شبيهًا بالتالي: ... def index(request): ... cities = City.objects.all() #return all the cities in the database weather_data = [] for city in cities: city_weather = requests.get(url.format(city)).json() #request the API data and convert the JSON to Python data types weather = { 'city' : city, 'temperature' : city_weather['main']['temp'], 'description' : city_weather['weather'][0]['description'], 'icon' : city_weather['weather'][0]['icon'] } weather_data.append(weather) #add the data for the current city into our list context = {'weather_data' : weather_data} return render(request, 'weather/index.html', context) #returns the index.html template ثم ستحتاج داخل قالب index.html للمرور في هذه القائمة مستخدمًا حلقة، وتوليد شيفرة HTML لكل مدينة city من القائمة. يمكنك لتحقيق ذلك وضع حلقة for حول شيفرة HTML التي تولد صندوقًا وحيدًا <div> للمدينة city. ... <div class="column is-offset-4 is-4"> {% for weather in weather_data %} <div class="box"> <article class="media"> <div class="media-left"> <figure class="image is-50x50"> <img src="http://openweathermap.org/img/w/{{ weather.icon }}.png" alt="Image"> </figure> </div> <div class="media-content"> <div class="content"> <p> <span class="title">{{ weather.city }}</span> <br> <span class="subtitle">{{ weather.temperature }}° F</span> <br> {{ weather.description }} </p> </div> </div> </article> </div> {% endfor %} </div> … يمكنك الآن معاينة بيانات كافة المدن التي لديك في قاعدة البيانات. الخطوة الثامنة - إنشاء نموذج التعبئة الخطوة الأخيرة هي السماح للمستخدم بإضافة مدينة ما مباشرةً من خلال نموذج تعبئة، ونحتاج لتحقيق هذا إلى إنشاء نموذج تعبئة. ومع أنه يمكنك تعبئته يدويًا، لكن نظرًا لأن نموذج التعبئة هذا سيحتوي نفس حقول نموذج قاعدة البيانات تمامًا، فالأسهل لك استخدام ModelForm. أنشئ ملفًا جديدًا باسم "forms.py" في تطبيق "weather": from django.forms import ModelForm, TextInput from .models import City class CityForm(ModelForm): class Meta: model = City fields = ['name'] widgets = { 'name': TextInput(attrs={'class' : 'input', 'placeholder' : 'City Name'}), } #updates the input class to have the correct Bulma class and placeholder ستحتاج لإظهار النموذج إلى إنشائه في العرض وتمريره إلى القالب، ومن أجل هذا حدّث الفهرس لإنشاء نموذج التعبئة، وستحتاج أيضًا إلى تحديث السياقcontext لكي يُمرَّر نموذج التعبئة إلى القالب. ... from .forms import CityForm def index(request): ... form = CityForm() weather_data = [] ... context = {'weather_data' : weather_data, 'form' : form} والآن لنحدّث قسم نموذج التعبئة ضمن القالب index.html ليستخدِم النموذج من العرض ومفتاح التشفير المساعد المُعد من أجل هجمات تزوير الطلب عبر المواقع csrf_token، وهو ضروري لطلبات من نوع POST في جانغو. ... <form method="POST"> {% csrf_token %} <div class="field has-addons"> <div class="control is-expanded"> {{ form.name }} </div> <div class="control"> <button class="button is-info"> Add City </button> </div> </div> </form> ... ملاحظة: تشير CSRF إلى هجمات تزوير الطلب عبر المواقع Cross-Site Request Forgery، وهي مقياس أمني لضمان إرسال بيانات نموذج التعبئة من مصدر متوقع وموثوق. الآن وقد أصبح نموذج التعبئة في HTML يعمل، ستحتاج إلى التعامل مع بياناته لحظة دخولها إلى النموذج. ستنشئ لأجل هذا كتلة if تتحقق من وجود طلب من نوع POST، وستحتاج أيضًا إلى إضافة التحقق من نوع الطلب قبل مباشرة التعامل مع بيانات الطقس، لتحصل آنيًا على بيانات المدينة التي تضيفها. ... def index(request): url = 'http://api.openweathermap.org/data/2.5/weather?q={}&units=imperial&appid=YOUR_APP_KEY' cities = City.objects.all() #return all the cities in the database if request.method == 'POST': # only true if form is submitted form = CityForm(request.POST) # add actual request data to form for processing form.save() # will validate and save if validate form = CityForm() … وهكذا بتمرير request.POST ستتمكن من التحقق من صلاحية بيانات النموذج. يُفترض الآن في هذه النقطة أن تتمكن من كتابة اسم المدينة ثم النقر على إضافة "Add" ورؤيتها وهي تظهر. أضف على سبيل المثال "Miami" اسمًا للمدينة التالية. عندما تخرج من كتلة if، سيُعاد إنشاء نموذج التعبئة مما يمكنك من إضافة مدينة جديدة إن شئت، وستسلك بقية الشيفرة نفس الطريقة. وبهذا بات لدينا طريقة نتابع بها حالة الطقس لعدة مدن ضمن التطبيق. خاتمة عملنا في هذه المقالة مع العديد من عناصر جانغو، مثل العروض ونماذج قاعدة البيانات ونماذج التعبئة والقوالب. واحتجنا لاستخدام مكتبة بايثون "requests" لجلب بيانات الطقس الحقيقية. على الرغم من أن التطبيق بسيط، سنستخدم المفاهيم نفسها مرةً تلو الأخرى حتى في المشاريع الأعقد. ترجمة - وبتصرف - للمقالة How To Build a Weather App in Django لصاحبها Anthony Herbert. اقرأ أيضًا بناء تطبيق مهام باستخدام جانغو Django وريآكت React بناء تطبيق ويب من طرف الخادم باستخدام Nuxt.js وجانغو Django البدء مع إطار العمل جانغو لإنشاء تطبيق ويب
  5. غيَّر التحسُّن الذي أُجري على مكتبات جافا سكريبت الحديثة مثل مكتبة ريآكت React.js ومكتبة فيو جي إس Vue.js من تطوير واجهات الويب الأمامية نحو الأفضل، إذ وفًّرت لنا هذه المكتبات بعض الميزات، مثل دعم تطبيقات الصفحة الواحدة Single-Page Application -أو اختصارًا SPA-؛ وهي تقنيةٌ تحمّل محتوى صفحات الويب ديناميكيًّا دون اضطرار إلى جلبها مرة أخرى من موقع الخادم إلى المتصفح على جهاز العميل. والمفهوم الذي تُبنى عليه معظم تطبيقات الصفحة الواحدة هو مفهوم التصيير rendering في طرف العميل، إذ يُصيّر أغلب المحتوى في شاشة المتصفح بجافا سكريبت، ولكن لن يُحمّل هذا المحتوى في البداية حتى ينتهي تنزيل download شيفرة جافا سكريبت كاملةً، ثم تتكفل هي بتصيير باقي الموقع. يُعد مفهوم التكوين في طرف العميل حديثًا نسبيًا، ورغم تميزُّه ففيه قصور، إذ يؤدي عدم تصيير محتوى الصفحة حتى تحديث الصفحة باستخدام جافا سكريبت إلى عرقلة عملية تحسين محرك البحث Search Engine Optimization -أو اختصارًا SEO- على الموقع، إذ يحتاج SEO إلى بيانات لتتبع الارتباطات، ولن يكون هناك أي بيانات في هذه التقنية لتتبع الارتباطات فيها. وعلى الطرف الآخر هناك مفهوم التصيير في طرف الخادم server-side، الذي يُعد الطريقة التقليدية لتصيير صفحات HTML على المتصفح؛ إذ يُبنى تطبيق الويب في هذا النوع من التطبيقات بلغة تعمل في طرف الخادم مثل PHP، وعندما يطلب المتصفح صفحة ويب، يضيف الخادمُ البعيدُ كاملَ المحتوى الديناميكي، ثم يرسل صفحة HTML مملوءة بالمحتوى إلى طرف العميل. يعاني أيضًا التصيير في طرف الخادم من بعض أوجه القصور، مثل إرسال طلبات كثيرةٍ إلى الخادم، وتكرار تحميل الصفحات كاملةً المرة تلو المرة رغم احتوائها نفس البيانات. وهناك نوعٌ ثالث من أطر العمل، مبنيٌّ بجافا سكريبت يعتمد على مبدأ تحميل صفحة الويب ابتداءً بطريقة التصيير في طرف الخادم، ثم يستخدم إطار عمل آخر لمعالجة التوجيه الديناميكي الإضافي والاقتصار على إحضار البيانات المهمة، ويطلق على التطبيقات الناتجة اسم التطبيقات العمومية Universal Applications كونها تشمل مزيجًا من الحلّين: التصيير عند الخادم وعند العميل. يُستخدمُ مصطلح "التطبيق العمومي" لوصف شيفرة جافا سكريبت التي يمكن أن تنفّذَ عند كلا الطرفين: العميل والخادم. سنبني في هذه المقالة تطبيقًا عموميًّا موضوعه "وصفات الطبخ"، مستعينين بإطار عمل نُكست Nuxt.js؛ وهو إطار عمل من مستوى أعلى لتطوير تطبيقات فيو Vue.js عمومية، واستُلهمت فكرته من نِكست Next.js الذي تقدمه ريآكت وهو يساعد على تجريد التعقيدات التي تظهر لدى إعداد تطبيقات فيو جي إس التي تُصيَّر في طرف الخادم، مثل إعدادات الخادم، وتوزيع شيفرة العميل، ويأتي مع إطار العمل نُكست عدة مزايا تسهل عمليات التطوير بين جهتي العميل والخادم، مثل مزامنة البيانات async data والبرمجيات الوسيطة middleware والأنساق layouts ونحو ذلك. ملاحظة: رغم إمكانية إدراج التطبيق الذي نبنيه تحت تصنيف التطبيقات التي تُصيَّر في طرف الخادم Server-Side Rendering - أو اختصارًا SSR -،لأن فيو Vue يتولى بطبيعة الحال مهمة التصيير عند العميل عندما ننشئ تطبيق صفحة واحدة، ومع ذلك فالتطبيق الذي نعمل عليه هو في حقيقته يندرج تحت التطبيقات العمومية. سننشئ في هذه المقالة تطبيقًا عموميًّا مستخدمين لتحقيق ذلك جانغو ونُكست، إذ سيتولى جانغو مسؤولية معالجة العمليات في طرف الخادم ويوفر واجهات برمجة التطبيقات التي تستخدِم إطار عمل جانغو ريست Django REST، بينما ينشئ نُكست الواجهة الأمامية. تبين الصورة التالية لقطةً توضيحيةً للتطبيق النهائي: والتطبيق النهائي هو كما نرى تطبيق دليل لوصفات تحضير أطباق أو دليل طبخ ويمكّن مستخدميه من إجراء عمليات الإنشاء Create والقراءة Read والتحديث Update والحذف Delete- أو اختصارًا CRUD-. المتطلبات الأساسية ستحتاج للمضيّ في هذه المقالة إلى ما يلي: تثبيت نود جي إس Node.js محلي على جهازك، وإذا لم تكن تعرف ذلك، يمكنك مراجعة مقالة مقدمة إلى Node.js. تثبيت بايثون على جهازك أيضًا والطريقة هنا. سيستخدم هذا المشروع Pipenv. وهي أداة معدة للاستخدام المهني توفر لنا أفضل طرق التحزيم Packaging في عالم بايثون. فهي تحزم Pipfile و pip و virtualenv في أمرٍ واحد فقط. وسنفترض أيضًا معرفة القارئ بما يلي: العملية الأساسية بكل من جانغو وإطار عمل جانغو ريست Django REST Framework.العملية الأساسية بمكتبة ـفيو جي إس Vue.js. وقد تحققنا من عمل التطبيق في هذه المقالة باستخدام الاعتماديات التالية: Python v3.7.7 Django v3.0.7 Node v14.4.0 npm v6.14.5 nuxt v2.13.0 الخطوة الأولى - إعداد الواجهة الخلفية سنعدّ الآن الواجهة الخلفية وننشئ كافة المجلدات التي نحتاجها لتجهيز التطبيق وتشغيله، لذا شغّل نسخة من الطرفية وأنشئ مجلد المشروع بتنفيذ الأمر التالي: $ mkdir recipes_app ثم انتقل إلى داخل المجلد: $ cd recipes_app الآن سنثبت أداة Pipenv باستخدام الأمر pip: $ pip install pipenv ونفعّل بيئة افتراضية جديدة: $ pipenv shell ملاحظة: تجاوز الأمر الأول إذا كانت Pipenv مثبتةً على حاسوبك. الآن، نثبت جانغو وبقية الاعتمادات باستخدام الأداة Pipenv: (recipes_app)$ pipenv install django django-rest-framework django-cors-headers ملاحظة: بعد تفعيل بيئة افتراضية جديدة باستخدام Pipenv، سيُسبق كل سطر أوامر في الطرفية باسم مجلد العمل الحالي، وهو في حالتنا هذه (recipes_app). سننشئ الآن مشروع جانغو جديدًا ونسميه api: (recipes_app)$ django-admin startproject api انتقل إلى مجلد المشروع: (recipes_app)$ cd api أنشئ تطبيق جانغو وسمِّه core: (recipes_app)$ python manage.py startapp core دعنا الآن نسجّل التطبيق "core" مع "rest_framework" و "cors-headers" ليتمكن مشروع جانغو من التعرُّف عليه. افتح الملف "api/settings.py" وعدّله كما يلي: # ... # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', # add this 'corsheaders', # add this 'core' # add this ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # add this 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] # add this block below MIDDLEWARE CORS_ORIGIN_WHITELIST = ( 'http://localhost:3000', ) # ... # add the following just below STATIC_URL MEDIA_URL = '/media/' # add this MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # add this أضفنا هنا العنوان http://localhost:3000 إلى القائمة البيضاء لأن تطبيق العميل سيُخدَّم من هذا المنفذ ونريد منع حدوث أي خطأ من أخطاء سياسة تعدد الموارد Cross-Origin Resource Sharing - أو اختصارًا CORS-؛ كما أضفنا أيضًا MEDIA_URL و MEDIA_ROOT لأننا سنحتاج إليهما من أجل خدمة الصور في التطبيق. تعريف نموذج وصفات تحضير الأطباق لننشئ الآن النموذج Recipe الذي سيعرّف طريقة تخزين عناصر وصفات التحضير في قاعدة البيانات. افتح الملف "core/models.py" وبدّل محتواه بالشيفرة التالية: from django.db import models # Create your models here. class Recipe(models.Model): DIFFICULTY_LEVELS = ( ('Easy', 'Easy'), ('Medium', 'Medium'), ('Hard', 'Hard'), ) name = models.CharField(max_length=120) ingredients = models.CharField(max_length=400) picture = models.FileField() difficulty = models.CharField(choices=DIFFICULTY_LEVELS, max_length=10) prep_time = models.PositiveIntegerField() prep_guide = models.TextField() def __str_(self): return "Recipe for {}".format(self.name) تصف الشيفرة السابقة ست خاصّيات في نموذج Recipe، هي: name ingredients picture difficulty prep_time prep_guide إنشاء المسلسلات لنموذج وصفات تحضير الأطباق نحتاج إلى المسلسِلات لتحويل نسخ النموذج إلى نوع المحتوى JSON لتمكين الواجهة الأمامية من العمل مع البيانات التي ستستقبلها. سننشئ الملف "core/serializers.py" ونضيف إليه السطور التالية: from rest_framework import serializers from .models import Recipe class RecipeSerializer(serializers.ModelSerializer): class Meta: model = Recipe fields = ("id", "name", "ingredients", "picture", "difficulty", "prep_time", "prep_guide") حدّدنا في الشيفرة السابقة النموذج الذي سنعمل معه والحقول التي نريد تحويلها إلى محتوًى من النوع JSON. تجهيز لوحة الإدارة يوفر لنا جانغو واجهة إدارة جاهزة للعمل فورًا وتسهل اختبار عمليات CRUD على نموذج Recipe الذي أنشأناه للتو، لكن قبل ذلك سنجري بعض الضبط. افتح الملف "core/admin.py" وبدّل محتواه كليًّا بالأسطر التالية: from django.contrib import admin from .models import Recipe # add this # Register your models here. admin.site.register(Recipe) # add this إنشاء العروض لننشئ الصنف RecipeViewSet في الملف core/views.py. بدّل محتواه كليًا بالشيفرة التالية: from rest_framework import viewsets from .serializers import RecipeSerializer from .models import Recipe class RecipeViewSet(viewsets.ModelViewSet): serializer_class = RecipeSerializer queryset = Recipe.objects.all() يوفّر الصنف viewsets.ModelViewSet توابع لمعالجة عمليات CRUD افتراضيًا، ولا نحتاج سوى تحديد صنف المسلسل ومجموعة الاستعلام queryset. تجهيز عناوين محدد الموارد الموحد URL اذهب إلى الملف "api/urls.py"، وبدّل محتواه كليًا بالسطور التالية التي تحدد مسار عنوان واجهة برمجة التطبيقات: from django.contrib import admin from django.urls import path, include # add this from django.conf import settings # add this from django.conf.urls.static import static # add this urlpatterns = [ path('admin/', admin.site.urls), path("api/", include('core.urls')) # add this ] # أضف ما يلي if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) والآن أنشئ الملف "urls.py" في المجلد "core" وانسخ إليه السطور التالية: from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import RecipeViewSet router = DefaultRouter() router.register(r'recipes', RecipeViewSet) urlpatterns = [ path("", include(router.urls)) ] يولد الصنف router في الشيفرة السابقة أنماط عناوين URL التالية: "/recipes/": يمكن أن تُنفذ عمليتي الإنشاء CREATE والقراءة READ على هذا الاتجاه route. "{recipes/{id/": يمكن أن تُنفذ عمليات القراءة READ والتحديث UPDATE والحذف DELETE على هذا الاتجاه. تنفيذ عمليات التهجير بما أننا أنشأنا النموذج Recipe مؤخرًا وعرّفنا بنيته، سنحتاج لإنشاء ملف تهجير وتطبيق التغييرات المُجراة على النموذج على قاعدة البيانات، لذا سننفِّذ الأمرين التاليين: (recipes_app)$ python manage.py makemigrations (recipes_app)$ python manage.py migrate ننشئ الآن حساب مستخدم مميّز superuser للوصول إلى واجهة الإدارة: (recipes_app)$ python manage.py createsuperuser سيطلب منك إدخال اسم المستخدم وكلمة المرور والبريد الإلكتروني للمستخدم المميز، لذا احرص على اختيار تفاصيل سهلة التذكر لأنك ستحتاجها مرارًا للدخول إلى واجهة الإدارة. وبهذا نكون انتهينا من التهيئة اللازمة للواجهة الخلفية. نستطيع الآن اختبار واجهات برمجة التطبيقات APIs التي أنشاناها، لذلك نبدأ بتشغيل خادم جانغو: (recipes_app)$ python manage.py runserver بعد أن يعمل الخادم، تستطيع التحقق من عمله بالذهاب إلى العنوان "/localhost:8000/api/recipes". نستطيع إنشاء عنصر وصفة تحضير جديدة بهذه الواجهة: نستطيع أيضًا إجراء عمليات DELETE و PUT و PATCH على بضعة عناصر من عناصر وصفات التحضير وذلك من خلال استخدام مفاتيحها الأساسية id؛ ولفعل ذلك نكتب في المتصفح عناوينًا تأخذ الشكل العام التالي: "{api/recipe/{id/". لنجرب الآن العنوان "localhost:8000/api/recipes/1": وبهذا يكتمل إعداد الواجهة الخلفية للتطبيق وحان الآن وقت الواجهة الأمامية. الخطوة الثانية- تحضير الواجهة الأمامية سنبني الآن الواجهة الأمامية للتطبيق. نريد وضع مجلد شيفرة الواجهة الأمامية في جذر المجلد "recipes_app"، لذا اخرج أولًا من المجلد "api"، أو شغّل نافذة طرفية جديدة تعمل جنبًا إلى جنب مع نافذة الطرفية الأولى قبل تنفيذ الأوامر التي ستأتي في هذا الجزء من المقالة. لننشئ تطبيق nuxt ونطلق عليه اسم "client" مستخدمين الأمر التالي: $ npx create-nuxt-app client ملاحظة: سيؤدي وضع npx قبل create-nuxt-app في التعليمة إلى تثبيت الحزمة إذا لم تكن مثبّتة للعموم من قبل على جهازك. بعد اكتمال التثبيت سيسألك "create-nuxt-app" بضعة أسئلة حول بعض الأدوات التي ستضاف، وقد اخترنا منها ما يلي: ? Project name: client ? Programming language: JavaScript ? Package manager: Npm ? UI framework: Bootstrap Vue ? Nuxt.js modules: Axios ? Linting tools: ? Testing framework: None ? Rendering mode: Universal (SSR / SSG) ? Deployment target: Server (Node.js hosting) ? Development tools: سيعطي هذا إشارةً لمدير الحزم لكي يبدأ في تثبيت الاعتمادات. انتقل الآن إلى المجلد client: $ cd client لننفذ الآن الأمر التالي لتشغيل التطبيق في وضع التطوير: npm run dev بعد أن يقلع خادم التطوير، توجّه إلى العنوان "localhost:3000" لرؤية التطبيق، إذ يفترض أن يكون كما في الصورة التالية: الآن، لنلقِ نظرةً على بنية المجلد client: ├── client ├── assets/ ├── components/ ├── layouts/ ├── middleware/ ├── node_modules/ ├── pages/ ├── plugins/ ├── static/ └── store/ نلاحظ احتواءه على بضعة مجلدات نشرحها فيما يلي: Assets: يحتوي على ملفات غير مصرّفة uncompiled، مثل ملفات الصور و CSS و Sass وجافاسكريبت Javascript. Components: يحتوي على مكونات فيو جي إس Vue.js. Layouts: يحتوي على أنساق التطبيق، المُستخدمة لتغيير مظهر الصفحة وقد تُستخدم لعدة صفحات معًا. Middleware: يحتوي على البرمجيات الوسيطة للتطبيق، وهي دوال مخصصة تُشغّل قبل تصيير الصفحة على المتصفح. Pages: تحتوي على عروض ومسارات التطبيق. يقرأ نُكست كل الملفات ذات الامتداد "vue." في هذا المجلد ويستخدم المعلومات التي يحصل عليها لإنشاء موجّه التطبيق. Plugins: يحتوي على إضافات جافا سكريبت البرمجية التي ستُشغّل قبل استنساخ تطبيق فيو جي إس الجذر. Static: يحتوي على الملفات الساكنة (التي غالبًا لن تتغير) وكل الملفات المرتبطة بجذر التطبيق "/". Store: يحتوي على ملفات المتجر إذا كنا سنستخدم فيو إكس Vuex مع نُكست. وهناك أيضًا الملف nuxt.config.js ضمن المجلد client، إذ يحتوي هذا الملف على تهيئة مخصصة لتطبيق نُكست. وقبل المتابعة نزّل الملف، وفك ضغطه وضع مجلد images داخل المجلد static. بنية الصفحات سنضيف الآن بعض ملفات ذات الامتداد "vue." إلى المجلد pages لكي يحتوي تطبيقنا على خمس صفحات: Homepage All Recipes list page Single Recipe view page Single Recipe edit page Add Recipe page لنضف الآن ملفات ومجلدات "vue." التالية إلى المجلد pages ليكون لدينا نفس البنية التالية: ├── pages/ ├── recipes/ ├── _id/ └── edit.vue └── index.vue └── add.vue └── index.vue └── index.vue ستولد بنية الملفات السابقة الوجهات routes التالية: "/" الذي يُعالج بواسطة "pages/index.vue". "recipes/add/" الذي يُعالج بواسطة "pages/recipes/add.vue". "/recipes/" الذي يُعالج بواسطة "pages/recipes/index.vue". "/{recipes/{id/" الذي يُعالج بواسطة pages/recipes/_id/index.vue. "recipes/{id}/edit/" الذي يُعالج بواسطة "pages/recipes/_id/edit.vue". ينشئ أي ملف أو مجلد "vue." يبدأ اسمه بشرطة سفلية underscore مسارًا ديناميكيًا، وهذا مفيدٌ في تطبيقنا لأنه سيسهل عرض وصفات الطبخ المختلفة وذلك بناءً على معرّفاتها IDs. على سبيل المثال: "/recipes/1" و "/recipes/2" وهكذا. إنشاء الصفحة الرئيسية Homepage تساعدك الأنساق layout في نُكست كثيرًا عندما تريد تغيير مظهر تطبيقك والشعور الذي يبثّه لمستخدميه. تأتي كل نسخة من تطبيق نُكست مع تنسيق افتراضي، ونريد إزالة جميع التنسيقات styles لكي لا تتداخل وتتعارض مع تطبيقنا. افتح الملف layouts/default.vue وبدّل محتواه بالشيفرة التالية: <template> <div> <nuxt/> </div> </template> <style> </style> دعنا نحدّث الملف pages/index.vue بالشيفرة التالية: <template> <header> <div class="text-box"> <h1>La Recipes ?</h1> <p class="mt-3">Recipes for the meals we love ❤️</p> <nuxt-link class="btn btn-outline btn-large btn-info" to="/recipes"> View Recipes <span class="ml-2">→</span> </nuxt-link> </div> </header> </template> <script> export default { head() { return { title: "Home page" }; }, }; </script> <style> header { min-height: 100vh; background-image: linear-gradient( to right, rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0.4) ), url("/images/banner.jpg"); background-position: center; background-size: cover; position: relative; } .text-box { position: absolute; top: 50%; left: 10%; transform: translateY(-50%); color: #fff; } .text-box h1 { font-family: cursive; font-size: 5rem; } .text-box p { font-size: 2rem; font-weight: lighter; } </style> لدينا في الشيفرة السابقة <nuxt-link>، وهو مكون نُكست يُستخدم للتنقل بين الصفحات، وهو مشابهٌ كثيرًا للمكون <router-link> من Vue Router لنشغّل خادم التطوير للواجهة الأمامية، إذا لم يكن في وضع التشغيل فعليًا: $ npm run dev اذهب إلى العنوان localhost:3000 وتأمل الصفحة الرئيسية: تأكد دومًا من عمل خادم الواجهة الخلفية جانغو في نسخة أخرى من الطرفية، لأنه لن يلبث طويلًا حتى تبدأ الواجهة الأمامية في التواصل معه طلبًا للبيانات. ستكون كل صفحة في هذا التطبيق مكونًا من مكونات فيو، ويزود نُكست بسمات ودوال خاصة لتسهيل تطوير التطبيقات. يمكنك العثور على كل تلك السمات الخاصة في التوثيق الرسمي. سنستعمل لأغراض هذه المقالة دالتين من هذه الدوال، هما: ()head: يُستخدم هذا التابع لوضع وسوم وصفية محددة <meta> للصفحة الحالية. ()asyncData: يُستخدم هذا التابع لجلب البيانات قبل أن يُحمّل مكون الصفحة. بعد ذلك، يُدمج الكائن المعاد مع بيانات مكون الصفحة، وسنستفيد من هذا لاحقًا في هذه المقالة. إنشاء صفحة قائمة وصفات التحضير دعنا ننشئ مكون فيو اسمه "RecipeCard.vue" داخل المجلد components ونحدّثه بالشيفرة التالية: <template> <div class="card recipe-card"> <img :src="recipe.picture" class="card-img-top" > <div class="card-body"> <h5 class="card-title">{{ recipe.name }}</h5> <p class="card-text"> <strong>Ingredients:</strong> {{ recipe.ingredients }} </p> <div class="action-buttons"> <nuxt-link :to="`/recipes/${recipe.id}/`" class="btn btn-sm btn-success">View</nuxt-link> <nuxt-link :to="`/recipes/${recipe.id}/edit/`" class="btn btn-sm btn-primary">Edit</nuxt-link> <button @click="onDelete(recipe.id)" class="btn btn-sm btn-danger">Delete</button> </div> </div> </div> </template> <script> export default { props: ["recipe", "onDelete"] }; </script> <style> .recipe-card { box-shadow: 0 1rem 1.5rem rgba(0,0,0,.6); } </style> يقبل المكون الظاهر أعلاه خاصّيتين، هما: كائن recipe يحتوي على معلومات عن وصفة تحضير معينة. التابع onDelete الذي سيعمل عندما يضغط مستخدم التطبيق على زر حذف وصفة تحضير. افتح الآن الوجهة "pages/recipes/index.vue" وحدثها بأسطر الشيفرة التالية: <template> <main class="container mt-5"> <div class="row"> <div class="col-12 text-right mb-4"> <div class="d-flex justify-content-between"> <h3>La Recipes</h3> <nuxt-link to="/recipes/add" class="btn btn-info">Add Recipe</nuxt-link> </div> </div> <template v-for="recipe in recipes"> <div :key="recipe.id" class="col-lg-3 col-md-4 col-sm-6 mb-4"> <recipe-card :onDelete="deleteRecipe" :recipe="recipe"></recipe-card> </div> </template> </div> </main> </template> <script> import RecipeCard from "~/components/RecipeCard.vue"; const sampleData = [ { id: 1, name: "Jollof Rice", picture: "/images/food-1.jpeg", ingredients: "Beef, Tomato, Spinach", difficulty: "easy", prep_time: 15, prep_guide: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum " }, { id: 2, name: "Macaroni", picture: "/images/food-2.jpeg", ingredients: "Beef, Tomato, Spinach", difficulty: "easy", prep_time: 15, prep_guide: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum " }, { id: 3, name: "Fried Rice", picture: "/images/banner.jpg", ingredients: "Beef, Tomato, Spinach", difficulty: "easy", prep_time: 15, prep_guide: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum " } ]; export default { head() { return { title: "Recipes list" }; }, components: { RecipeCard }, asyncData(context) { let data = sampleData; return { recipes: data }; }, data() { return { recipes: [] }; }, methods: { deleteRecipe(recipe_id) { console.log(deleted `${recipe.id}`) } } }; </script> <style scoped> </style> شغّل الآن خادم التطوير للواجهة الأمامية إذا لم يكن يعمل فعلًا: $ npm run dev ثم اذهب إلى العنوان localhost:3000/recipes وتأمل في صفحة قائمة وصفات الطبخ: نلاحظ في الصورة السابقة ظهور ثلاث بطاقات وصفات تحضير رغم أننا حددنا قيمة recipes بمصفوفة فارغة في قسم البيانات من المكون، وتفسير هذا هو أن التابع asyncData يُنفّذ قبل أن تُحمّل الصفحة ويعيد كائنًا يحدّث بيانات المكون. كل ما نحتاجه الآن هو أن نعدل التابع asyncData لينشئ طلب api إلى الواجهة الخلفية لجانغو ويحدّث بيانات المكون بالنتيجة. ولكن يجب علينا تجهيز آكسيوس Axios قبل تفعيل ذلك، لذا افتح الملف nuxt.config.js وحدثه كما يلي: // أضف كائن أكسيوس التالي axios: { baseURL: "http://localhost:8000/api" }, ملاحظة: يفترِض هذا أنك اخترت "Axios" عند استخدام "create-nuxt-app"، فإذا لم تكن قد اخترته فعلًا، فستحتاج إلى تثبيت وتهيئة المصفوفة modules يدويًا. افتح الآن الملف "pages/recipes/index.vue" وبدّل القسم <script> بالشيفرة التالية: [...] <script> import RecipeCard from "~/components/RecipeCard.vue"; export default { head() { return { title: "Recipes list" }; }, components: { RecipeCard }, async asyncData({ $axios, params }) { try { let recipes = await $axios.$get(`/recipes/`); return { recipes }; } catch (e) { return { recipes: [] }; } }, data() { return { recipes: [] }; }, methods: { async deleteRecipe(recipe_id) { try { await this.$axios.$delete(`/recipes/${recipe_id}/`); // delete recipe let newRecipes = await this.$axios.$get("/recipes/"); // get new list of recipes this.recipes = newRecipes; // update list of recipes } catch (e) { console.log(e); } } } }; </script> [...] يستقبل التابع ()asyncDataفي الشيفرة السابقة كائنًا اسمه context، الذي نفكّكه للحصول على axios$. ولمعرفة المزيد عن بقية سمات الكائن context يرجى مراجعة توثيقه الرسمي. أضفنا ()asyncData داخل كتلة try...catch لأننا نريد منع ظهور الخطأ الذي يظهر عندما لا يكون خادم الواجهة الخلفية في حالة عمل ويفشل آكسيوس في استعادة البيانات. وهكذا، مع هذه الإضافة فإنه في كل مرة يحدث هذا الخطأ، توضع قيمة recipes بمصفوفة فارغة بدلًا من ذلك. هذا السطر من الشيفرة: let recipes = await $axios.$get("/recipes/") هو نسخةٌ أقصر من السطر التالي: let response = await $axios.get("/recipes") let recipes = response.data يحذف التابع ()deleteRecipe وصفة تحضير معينة ويحضر أحدث قائمة لوصفات التحضير من الواجهة الخلفية لجانغو، ثم يحدّث أخيرًا بيانات المكوّن. يمكننا الآن بدء تشغيل خادم التطوير للواجهة الأمامية إذا لم يكن يعمل فعلًا وسنرى أن بطاقات وصفات التحضير يجري الآن ملؤها بالبيانات من الواجهة الخلفية لجانغو؛ ولكي يعمل هذا يجب أن يكون خادم الواجهة الخلفية لجانغو في وضع تشغيل، وأن تتوفر بعض البيانات لعناصر وصفات التحضير Recipe، إذ تكون هذه البيانات مُدخلةً من واجهة الإدارة. $ npm run dev لنذهب الآن إلى الصفحة localhost:3000/recipes: جرب حذف عناصر الوصفات وانظر ما سيحدث. إضافة وصفات طبخ جديدة نريد أن نتمكن كما ذكرنا من قبل من إضافة وصفات جديدة من الواجهة الأمامية للتطبيق، لذا افتح الملف "‎/pages/recipes/add" وحدّثه بمقطع الشيفرة التالي: <template> <main class="container my-5"> <div class="row"> <div class="col-12 text-center my-3"> <h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2> </div> <div class="col-md-6 mb-4"> <img v-if="preview" class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="preview" alt > <img v-else class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" src="@/static/images/placeholder.png" > </div> <div class="col-md-4"> <form @submit.prevent="submitRecipe"> <div class="form-group"> <label for>Recipe Name</label> <input type="text" class="form-control" v-model="recipe.name"> </div> <div class="form-group"> <label for>Ingredients</label> <input v-model="recipe.ingredients" type="text" class="form-control"> </div> <div class="form-group"> <label for>Food picture</label> <input type="file" name="file" @change="onFileChange"> </div> <div class="row"> <div class="col-md-6"> <div class="form-group"> <label for>Difficulty</label> <select v-model="recipe.difficulty" class="form-control"> <option value="Easy">Easy</option> <option value="Medium">Medium</option> <option value="Hard">Hard</option> </select> </div> </div> <div class="col-md-6"> <div class="form-group"> <label for> Prep time <small>(minutes)</small> </label> <input v-model="recipe.prep_time" type="number" class="form-control"> </div> </div> </div> <div class="form-group mb-3"> <label for>Preparation guide</label> <textarea v-model="recipe.prep_guide" class="form-control" rows="8"></textarea> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </div> </main> </template> <script> export default { head() { return { title: "Add Recipe" }; }, data() { return { recipe: { name: "", picture: "", ingredients: "", difficulty: "", prep_time: null, prep_guide: "" }, preview: "" }; }, methods: { onFileChange(e) { let files = e.target.files || e.dataTransfer.files; if (!files.length) { return; } this.recipe.picture = files[0]; this.createImage(files[0]); }, createImage(file) { // let image = new Image(); let reader = new FileReader(); let vm = this; reader.onload = e => { vm.preview = e.target.result; }; reader.readAsDataURL(file); }, async submitRecipe() { const config = { headers: { "content-type": "multipart/form-data" } }; let formData = new FormData(); for (let data in this.recipe) { formData.append(data, this.recipe[data]); } try { let response = await this.$axios.$post("/recipes/", formData, config); this.$router.push("/recipes/"); } catch (e) { console.log(e); } } } }; </script> <style scoped> </style> وفقًا للتابع ()submitRecipe: حالما تُنشر بيانات نموذج التعبئة وتنشأ وصفة التحضير بنجاح، يُعاد توجيه التطبيق إلى المجلد "/recipes/" باستخدام this.$router. إنشاء صفحة استعراض وصفة تحضير واحدة لننشئ الآن العرض الذي يسمح للمستخدم باستعراض عنصر وصفة طبخ واحدة. افتح الملف "pages/recipes/_id/index.vue/" وضع فيه مقطع الشيفرة التالية: <template> <main class="container my-5"> <div class="row"> <div class="col-12 text-center my-3"> <h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2> </div> <div class="col-md-6 mb-4"> <img class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="recipe.picture" alt > </div> <div class="col-md-6"> <div class="recipe-details"> <h4>Ingredients</h4> <p>{{ recipe.ingredients }}</p> <h4>Preparation time ⏱</h4> <p>{{ recipe.prep_time }} mins</p> <h4>Difficulty</h4> <p>{{ recipe.difficulty }}</p> <h4>Preparation guide</h4> <textarea class="form-control" rows="10" v-html="recipe.prep_guide" disabled /> </div> </div> </div> </main> </template> <script> export default { head() { return { title: "View Recipe" }; }, async asyncData({ $axios, params }) { try { let recipe = await $axios.$get(`/recipes/${params.id}`); return { recipe }; } catch (e) { return { recipe: [] }; } }, data() { return { recipe: { name: "", picture: "", ingredients: "", difficulty: "", prep_time: null, prep_guide: "" } }; } }; </script> <style scoped> </style> قدمنا هنا المفتاح params الذي رأيناه في التابع ()asyncData، ونستخدمه في حالتنا هذه للحصول على المعرف ID لوصفة الطبخ التي نريد مشاهدتها. نستخرج المفتاح params من الرابط URL ونحضر بياناته قبل عرضها على الصفحة. نلاحظ ظهور عنصر وصفة طبخ واحدة على شاشة المتصفح. إنشاء صفحة تعديل وصفة تحضير واحدة نحتاج لإنشاء العرض الذي يسمح للمستخدم بتحرير وتعديل عنصر وصفة طبخ واحدة. افتح الملف "pages/recipes/_id/edit.vue/" وضع فيه مقطع الشيفرة التالية: <template> <main class="container my-5"> <div class="row"> <div class="col-12 text-center my-3"> <h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2> </div> <div class="col-md-6 mb-4"> <img v-if="!preview" class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="recipe.picture"> <img v-else class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="preview"> </div> <div class="col-md-4"> <form @submit.prevent="submitRecipe"> <div class="form-group"> <label for>Recipe Name</label> <input type="text" class="form-control" v-model="recipe.name" > </div> <div class="form-group"> <label for>Ingredients</label> <input type="text" v-model="recipe.ingredients" class="form-control" name="Ingredients" > </div> <div class="form-group"> <label for>Food picture</label> <input type="file" @change="onFileChange"> </div> <div class="row"> <div class="col-md-6"> <div class="form-group"> <label for>Difficulty</label> <select v-model="recipe.difficulty" class="form-control" > <option value="Easy">Easy</option> <option value="Medium">Medium</option> <option value="Hard">Hard</option> </select> </div> </div> <div class="col-md-6"> <div class="form-group"> <label for> Prep time <small>(minutes)</small> </label> <input type="text" v-model="recipe.prep_time" class="form-control" name="Ingredients" > </div> </div> </div> <div class="form-group mb-3"> <label for>Preparation guide</label> <textarea v-model="recipe.prep_guide" class="form-control" rows="8"></textarea> </div> <button type="submit" class="btn btn-success">Save</button> </form> </div> </div> </main> </template> <script> export default { head(){ return { title: "Edit Recipe" } }, async asyncData({ $axios, params }) { try { let recipe = await $axios.$get(`/recipes/${params.id}`); return { recipe }; } catch (e) { return { recipe: [] }; } }, data() { return { recipe: { name: "", picture: "", ingredients: "", difficulty: "", prep_time: null, prep_guide: "" }, preview: "" }; }, methods: { onFileChange(e) { let files = e.target.files || e.dataTransfer.files; if (!files.length) { return; } this.recipe.picture = files[0] this.createImage(files[0]); }, createImage(file) { let reader = new FileReader(); let vm = this; reader.onload = e => { vm.preview = e.target.result; }; reader.readAsDataURL(file); }, async submitRecipe() { let editedRecipe = this.recipe if (editedRecipe.picture.name.indexOf("http://") != -1){ delete editedRecipe["picture"] } const config = { headers: { "content-type": "multipart/form-data" } }; let formData = new FormData(); for (let data in editedRecipe) { formData.append(data, editedRecipe[data]); } try { let response = await this.$axios.$patch(`/recipes/${editedRecipe.id}/`, formData, config); this.$router.push("/recipes/"); } catch (e) { console.log(e); } } } }; </script> <style scoped> </style> يحتوي التابع ()submitRecipe في الشيفرة السابقة على تعليمة شرطية لحذف صورة عنصر وصفة طبخ مُحررة من البيانات التي ستُرسل، إذا لم تكن الصورة قد تغيرت، ويُعاد توجيه التطبيق إلى صفحة قائمة وصفات الطبخ "/recipes/"عقب تحديث عنصر وصفة طبخ. إعداد الانتقالات مع أن التطبيق يعمل جيدًا، لكن يمكننا أيضًا إعطاءه مظهرًا أسلس إذا أضفنا الانتقالات التي تسمح لنا بتغيير قيم خصائص CSS من قيمة إلى أخرى بسلاسة ضمن فترة زمنية محددة. وسنجهز الانتقالات في الملف nuxt.config.js. ويأخذ اسم الانتقال افتراضيًّا الاسم page الذي يعني أن الانتقالات التي نعرّفها ستكون ساريةً على جميع الصفحات. لنضمّن نمط الانتقال، لذا أنشئ مجلدًا وسمِّه "css" في المجلد "assets" وأضف الملف "transitions.css" داخله. افتح الآن الملف "transitions.css" وضع فيه مقطع الشيفرة التالية: .page-enter-active, .page-leave-active { transition: opacity .3s ease; } .page-enter, .page-leave-to { opacity: 0; } افتح الملف nuxt.config.js وحدِّثه كما يلي لتحميل ملف CSS الذي أنشأناه للتو: /* ** Global CSS */ css: [ '~/assets/css/transitions.css', // update this ], احفظ التغييرات وافتح التطبيق في متصفحك: وبهذا سيغير التطبيقُ الإطاراتِ عند كل انتقال بطريقة أنيقة وجميلة. خاتمة بدأنا هذه المقالة بالتعرُّف على الفرق بين التطبيقات التي يجري تصييرها من طرف الخادم وكذلك من طرف العميل، ثم تعرفنا على مفهوم التطبيق العمومي وأخيرًا رأينا كيف نبني تطبيقًا عموميًّا مستخدمين في سبيل ذلك كلًّا من نُكست وجانغو. تتوفر الشيفرة المصدرية لهذه المقالة على موقع Github. ترجمة -وبتصرف- للمقالة How To Build a Universal Application with Nuxt.js and Django لصاحبها Jordan Irabor. اقرأ أيضًا مقدمة إلى أطر عمل تطوير الويب من طرف العميل البدء مع إطار العمل جانغو لإنشاء تطبيق ويب مدخل إلى إطار العمل Django
  6. سنبني في هذه المقالة تطبيق المهام To-Do application باستخدام جانغو Django وريآكت React. ريآكت هي مكتبة مبنية بلغة جافا سكربت، وتُستخدم لتطوير تطبيقات الصفحة الواحدة Single-page applications -أو اختصارًا SPA. تتمتع ريآكت بتوثيق قوي ومنظومة بيئية حيَّة؛ أما جانغو فهو إطار عمل ويب مبني بلغة بايثون طُوّر لتبسيط الممارسات الشائعة المتبعة لتطوير الويب، وهو إطار عمل موثوق ويتمتع هو الآخر بمنظومة بيئية حيَّة من المكتبات البرمجية المستقرة التي تدعم احتياجات التطوير الشهيرة. ستلعب ريآكت في التطبيق الذي سنطوّره دور الواجهة الأمامية frontend، أو ما يسمى "إطار عمل من جهة العميل"، إذ ستتولى مسؤولية التعامل مع واجهة المستخدم إلى جانب جلب ووضع قيم البيانات من خلال الطلبات المُرسلة إلى الواجهة الخلفية لجانغو، التي تمثّل واجهة برمجة تطبيقات API بُنِيَت باستخدام إطار عمل جانغو ريست Django REST -أو اختصارًا DRF. ستحصل في نهاية هذا المقال على تطبيق فعال ونظامي. ملاحظة: إذا أردت الحصول على الشيفرة المصدرية لهذا المقال، فستجدها هنا. تنبيه: الشيفرة الواردة هي للأغراض التعليمية وليست للاستخدام المهني للشركات. سيسمح التطبيق لمستخدميه بإنشاء قائمة بالمهام التي يودون إنجازها، وتُنجز كل مهمة بوضع علامة "مكتمل complete" عليها، وفي حال عدم إنجازها تُوضع عليها علامة "غير مكتمل incomplete". المتطلبات الأساسية سنحتاج لكي نتمكن من تنفيذ ما في هذه المقالة إلى ما يلي: تثبيت بايثون 3 وإعداد بيئة برمجية محلية له. تثبيت Node.js وإنشاء بيئة تطوير محلية. إضافةً إلى الاعتماديات التالية: Python v3.9.1 pip v20.2.4 Django v3.1.6 djangorestframework v3.12.2 django-cors-headers v3.7.0 Node v15.8.0 npm v7.5.4 React v17.0.1 axios v0.21.0 الخطوة الأولى - إعداد الواجهة الخلفية سننشئ الآن مجلد مشروع جديد ونثبّت جانغو وفق ما يلي: افتح نافذة طرفية جديدة terminal، ونفّذ الأمر التالي الذي سينشئ مجلد مشروع جديدًا: $ mkdir django-todo-react ثم انتقل إلى داخل المجلد الذي أنشأته: $ cd django-todo-react الآن ثبّت pipenv مستخدمًا الأمر التالي: $ pip install pipenv ملاحظة: قد تحتاج إلى استخدام pip3 بدلًا من pip وهذا يعتمد على نوع التثبيت الذي تستخدمه. فعّل الآن بيئة افتراضية جديدة: $ pipenv shell وثبّت جانغو باستخدام pipenv : $ pipenv install django ثم أنشئ مشروع جانغو جديد وسمِّه backend: $ django-admin startproject backend ثم انتقل إلى داخل ذلك المجلد: $ cd backend وافتتح تطبيقًا جديدًا وسمِّه todo: python manage.py startapp todo نفّذ عمليات التهجير migrations لكي تُطبق التغييرات التي أجريت للتو على مخطط قاعدة البيانات: python manage.py migrate وشغّل الخادم: python manage.py runserver اذهب إلى العنوان "http://localhost:8000" في متصفح الويب الذي تستخدمه، وستظهر لك الصفحة التالية: سيظهر لك في هذه النقطة نسخةً من تطبيق جانغو في وضعية التشغيل. يمكنك الآن بعد أن انتهيت إيقاف الخادم باستخدام المفتاحين التاليين: "CONTROL+C"، أو "CTRL+C". تسجيل تطبيق المهام todo يمكنك الآن بعد أن انتهيت من إعداد الواجهة الخلفية أن تبدأ في تسجيل تطبيق المهام todo، ونعني بذلك أن تذكره مع التطبيقات المثبّتة ليتمكن جانغو من التعرُّف عليه. افتح الملف "backend/settings.py" بمحرر الشيفرة الذي تستخدمه وأضف التطبيق "todo" إلى قائمة التطبيقات المثبّتة "INSTALLED_APPS": # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'todo', ] ثم احفظ التغييرات التي أجريتها على الملف. تعريف نموذج تطبيق المهام Todo لننشئ الآن نموذجًا نتمكن به من تعريف كيفية تخزين عناصر "Todo" في قاعدة البيانات. افتح الملف "todo/models.py" بمحرر الشيفرة وأضف السطور التالية: from django.db import models # Create your models here. class Todo(models.Model): title = models.CharField(max_length=120) description = models.TextField() completed = models.BooleanField(default=False) def _str_(self): return self.title تعرّف الشيفرة السابقة ثلاث خاصيّات ضمن النموذج Todo وهي: title description completed تدل الخاصية completed على حالة المهمة في لحظة ما، فهي إما مكتملة أو غير مكتملة. ستحتاج الآن إلى إنشاء ملف تهجير بما أنك أنشأت نموذج "Todo": $ python manage.py makemigrations todo ثم طبّق التغييرات على قاعدة البيانات: $ python manage.py migrate todo يمكنك التأكد من إمكانية تنفيذ عمليات الإنشاء والقراءة والتحديث والحذف Create Read Update Delete - أو اختصارًا CRUD- على نموذج "Todo" باستخدام واجهة الإدارة التي يوفرها جانغو لمستخدميه. ولفعل ذلك، افتح الملف "todo/admin.py" بمحرر الشيفرة، وأضف الأسطر التالية: from django.contrib import admin from .models import Todo class TodoAdmin(admin.ModelAdmin): list_display = ('title', 'description', 'completed') # Register your models here. admin.site.register(Todo, TodoAdmin) ثم احفظ التغييرات. ستحتاج إلى حساب مستخدم مميز superuser لكي تتمكن من الدخول إلى واجهة الإدارة، لذا نفذ الأمر التالي في نافذة الطرفية لديك: $ python manage.py createsuperuser سيُطلبُ منك إدخال اسم المستخدم والبريد الإلكتروني وكلمة المرور لحساب المستخدم المميز. ولأنك ستحتاجها دومًا للدخول إلى لوحة تحكم الإدارة، ننصحك بأن تختار تفاصيل يسهل عليك تذكرها. شغّل الخادم مرةً أخرى: $ python manage.py runserver والآن انتقل إلى العنوان "http://localhost:8000/admin" في متصفح الويب الذي تستخدمه، وسجِّل الدخول مستعملًا اسم المستخدم وكلمة المرور اللذين أنشأتهما للتو. يمكنك إنشاء وتحرير وحذف عناصر "Todo" باستخدام هذه الواجهة: بعد إجراء تجاربك على هذه الواجهة، يمكنك إيقاف الخادم باستخدام المفتاحين "CONTROL+C" أو "CTRL+C". الخطوة الثانية - إعداد واجهة برمجة التطبيقات API سننشئ هنا واجهة برمجة تطبيقات مستعينين بإطار عمل جانغو ريست، وسنثبّت لتحقيق ذلك كلًّا من djangorestframework و django-cors-headers مستخدمين pipenv: pipenv install djangorestframework django-cors-headers يجب أن تضيف اسمي التطبيقين "rest_framework" و "corsheaders" إلى قائمة التطبيقات المثبّتة ضمن ملف الإعدادات. لذلك، افتح ملف الإعدادات "backend/settings.py" بمحرر الشيفرة، وحدّث قسمي التطبيقات المثبّتة "INSTALLED_APPS"، والبرمجيات الوسيطة "MIDDLEWARE" كما يلي: # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'corsheaders', 'rest_framework', 'todo', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'corsheaders.middleware.CorsMiddleware', ] ثم ألحق أسطر الشيفرة التالية بنهاية الملف "backend/settings.py": CORS_ORIGIN_WHITELIST = [ 'http://localhost:3000' ] تُعد مكتبة "django-cors-headers" مكتبة بايثون تمنع من حدوث الأخطاء التي تنتج عادةً عن استخدام بنود سياسة تعدد الموارد CORS، وهذا ما جعلك تضيف "localhost:3000" ضمن القائمة البيضاء "CORS_ORIGIN_WHITELIST"، لأنك تريد أن تمكِّن الواجهة الأمامية للتطبيق -التي تستخدم ذلك المنفذ- من التفاعل مع واجهة برمجة التطبيقات. إنشاء المسلسلات serializers ستحتاج للمسلسِلات لتحويل نسخ النموذج إلى محتوى من النوع JSON، وذلك لكي تتمكن الواجهة الأمامية من العمل مع البيانات التي تستلمها. أنشئ الملف "todo/serializers.py" باستخدام محرر الشيفرة، ثم افتحه وأضف إليه أسطر الشيفرة التالية: from rest_framework import serializers from .models import Todo class TodoSerializer(serializers.ModelSerializer): class Meta: model = Todo fields = ('id', 'title', 'description', 'completed') تحدد الشيفرة السابقة النموذج الذي سيجري العمل عليه إلى جانب الحقول التي ستُحوّل إلى محتوى من النوع JSON. إنشاء العرض ستحتاج إلى إنشاء الصنف TodoView في الملف "todo/views.py". افتح الملف "todo/views.py" بمحرر الشيفرة الذي تستخدمه وأضف أسطر الشيفرة التالية: from django.shortcuts import render from rest_framework import viewsets from .serializers import TodoSerializer from .models import Todo # Create your views here. class TodoView(viewsets.ModelViewSet): serializer_class = TodoSerializer queryset = Todo.objects.all() يوفِّر الصنف الأساسي viewsets تنفيذًا لعمليات CRUD افتراضيًا. تحدد الشيفرة السابقة صنف المسلسِل serializer_class و مجموعة الاستعلام queryset. افتح الملف "backend/urls.py" بمحرر الشيفرة وبدّل محتوياته بالأسطر التالية: from django.contrib import admin from django.urls import path, include from rest_framework import routers from todo import views router = routers.DefaultRouter() router.register(r'todos', views.TodoView, 'todo') urlpatterns = [ path('admin/', admin.site.urls), path('api/', include(router.urls)), ] تحدد الشيفرة السابقة مسار محدّد الموارد الموحد URL لواجهة برمجة التطبيقات API، وبهذه الخطوة نكون قد انتهينا من بناء واجهة برمجة التطبيقات. أصبح بإمكانك الآن إنجاز عمليات CRUD على نموذج المهام "Todo"، إذ يتيح لك صنف الموجِّه router class إجراء الاستعلامات التالية: /todos/: يعيد قائمةً بكافة عناصر Todo، ويمكن تنفيذ عمليتي الإنشاء CREATE والقراءة READ هنا. todos/id/ - يعيد عنصر Todo واحدًا اعتمادًا على المفتاح الأساسي id، ويمكن تنفيذ عمليتي التحديث UPDATE والحذف DELETE هنا. دعنا الآن نعيد تشغيل الخادم: $ python manage.py runserver اذهب إلى العنوان "http://localhost:8000/api/todos" في متصفح الويب الذي تستخدمه. يمكنك إنشاء عنصر مهام Todo جديد بهذه الواجهة باستخدام العملية CREATE كما يلي: إذا كانت عناصر قائمة المهام Todo قد أنشئت بنجاح، فستحصل على استجابة ناجحة. يمكنك أيضًا تنفيذ عمليتي الحذف DELETE والتحديث UPDATE على بعض عناصر Todo بناءً على المفاتيح الأساسية id التي تزودها. استخدم بنية العنوان "‎/api/todos/{id}‎" وحدد المفتاح الأساسي "id" الذي تريد تطبيق هذه العمليات عليه. أضف "1" إلى نهاية عنوان محدد الموارد الموحد URL لمعرفة عنصر Todo الذي يحمل مفتاحه الأساسي القيمة "1"، ثم انتقل إلى "http://localhost:8000/api/todos/1" في متصفح الويب الذي تستخدمه. تبين الشاشة التالية النتيجة التي حصلنا عليها: إذا ظهرت لك هذه الشاشة فذلك يعني أنك انتهيت من بناء الواجهة الخلفية للتطبيق. الخطوة الثالثة - إعداد الواجهة الأمامية بعد أن انتهيت من إعداد الواجهة الخلفية، حان وقت تجهيز الواجهة الأمامية وتمكينها من التواصل مع الواجهة الخلفية عبر الواجهة التي أنشأتها. افتح نافذة طرفية واذهب إلى مجلد المشروع "django-todo-react". سنعتمد في إعداد الواجهة الأمامية على التطبيق Create React الذي تتنوع طرق استخدامه، ومن أشهرها استخدام مشغّل npx لتشغيل الحزمة وإنشاء المشروع: $ npx create-react-app frontend وللاطلاع أكثر على هذه الطريقة يرجى مراجعة بدء العمل مع مكتبة ريآكت. يمكنك الآن بعد أن أنشأت المشروع الانتقال إلى المجلد الذي أنشأته للتو "frontend": $ cd frontend الآن، شغّل التطبيق: $ npm start سيفتح متصفحك الرابط "http://localhost:3000" وستظهر لك الشاشة الابتدائية لتطبيق "Create React". ثم ثبّت بوتستراب bootstrap وريآكتستراب reactstrap لتصبح أدوات واجهة المستخدم في حوزتك: $ npm install bootstrap@4.6.0 reactstrap@8.9.0 --legacy-peer-deps ملاحظة: ربما تتلقى رسائل خطأ مثل "unable to resolve dependency tree" بحسب إصدارات كل من ريآكت وبوتستراب وريآكتستراب. أصبحت النسخة الأحدث للملف "popper.js" في لحظة كتابة هذه المقالة مهملة وتتعارض مع الإصدار 17 وما بعده من ريآكت، وهذه مشكلة معروفة، ويمكن استخدام الخيار legacy-peer-deps-- عند التثبيت. افتح الملف "index.js" في محرر الشيفرة الذي تستخدمه وأضف bootstrap.min.css: import React from 'react'; import ReactDOM from 'react-dom'; import 'bootstrap/dist/css/bootstrap.css'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); إذا واجهت أي صعوبة أثناء هذه الخطوة، فننصحك بالاستعانة بموقع التوثيق الرسمي لإضافة بوتستراب. افتح الملف "App.js" بمحرر الشيفرة وأضف أسطر الشيفرة التالية: import React, { Component } from "react"; const todoItems = [ { id: 1, title: "Go to Market", description: "Buy ingredients to prepare dinner", completed: true, }, { id: 2, title: "Study", description: "Read Algebra and History textbook for the upcoming test", completed: false, }, { id: 3, title: "Sammy's books", description: "Go to library to return Sammy's books", completed: true, }, { id: 4, title: "Article", description: "Write article on how to use Django with React", completed: false, }, ]; class App extends Component { constructor(props) { super(props); this.state = { viewCompleted: false, todoList: todoItems, }; } displayCompleted = (status) => { if (status) { return this.setState({ viewCompleted: true }); } return this.setState({ viewCompleted: false }); }; renderTabList = () => { return ( <div className="nav nav-tabs"> <span className={this.state.viewCompleted ? "nav-link active" : "nav-link"} onClick={() => this.displayCompleted(true)} > Complete </span> <span className={this.state.viewCompleted ? "nav-link" : "nav-link active"} onClick={() => this.displayCompleted(false)} > Incomplete </span> </div> ); }; renderItems = () => { const { viewCompleted } = this.state; const newItems = this.state.todoList.filter( (item) => item.completed == viewCompleted ); return newItems.map((item) => ( <li key={item.id} className="list-group-item d-flex justify-content-between align-items-center" > <span className={`todo-title mr-2 ${ this.state.viewCompleted ? "completed-todo" : "" }`} title={item.description} > {item.title} </span> <span> <button className="btn btn-secondary mr-2" > Edit </button> <button className="btn btn-danger" > Delete </button> </span> </li> )); }; render() { return ( <main className="container"> <h1 className="text-white text-uppercase text-center my-4">Todo app</h1> <div className="row"> <div className="col-md-6 col-sm-10 mx-auto p-0"> <div className="card p-3"> <div className="mb-4"> <button className="btn btn-primary" > Add task </button> </div> {this.renderTabList()} <ul className="list-group list-group-flush border-top-0"> {this.renderItems()} </ul> </div> </div> </div> </main> ); } } export default App; تحتوي هذه الشيفرة على بعض المتوفرة مسبقًا hardcoded لأربعة عناصر، وستبقى هذه القيم إلى حين إحضار العناصر من الواجهة الخلفية. تعرض الدالة ()renderTabList امتدادين spans يظهِر كل منهما عند الضغط عليه مجموعةً من المهام، إذ يظهر الضغط على الامتداد Completed المهام المكتملة، ويظهر الامتداد Incomplete المهام غير المكتملة في المقابل. احفظ الآن التغييرات التي أجريتها وتأمل النتيجة في متصفحك: هناك أفعال معينة داخل تطبيق المهام، مثل إضافة المهام وتحريرها، ولكي تتمكن من التعامل معها ستحتاج إلى إنشاء مكوّن شرطي modal component. أولًا، أنشئ مجلد المكوّنات "components" داخل المجلد "src": $ mkdir src/components ثم أنشئ الملف "Modal.js" وافتحه بمحرر الشيفرة وأضف إليه الأسطر التالية: import React, { Component } from "react"; import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Input, Label, } from "reactstrap"; export default class CustomModal extends Component { constructor(props) { super(props); this.state = { activeItem: this.props.activeItem, }; } handleChange = (e) => { let { name, value } = e.target; if (e.target.type === "checkbox") { value = e.target.checked; } const activeItem = { ...this.state.activeItem, [name]: value }; this.setState({ activeItem }); }; render() { const { toggle, onSave } = this.props; return ( <Modal isOpen={true} toggle={toggle}> <ModalHeader toggle={toggle}>Todo Item</ModalHeader> <ModalBody> <Form> <FormGroup> <Label for="todo-title">Title</Label> <Input type="text" id="todo-title" name="title" value={this.state.activeItem.title} onChange={this.handleChange} placeholder="Enter Todo Title" /> </FormGroup> <FormGroup> <Label for="todo-description">Description</Label> <Input type="text" id="todo-description" name="description" value={this.state.activeItem.description} onChange={this.handleChange} placeholder="Enter Todo description" /> </FormGroup> <FormGroup check> <Label check> <Input type="checkbox" name="completed" checked={this.state.activeItem.completed} onChange={this.handleChange} /> Completed </Label> </FormGroup> </Form> </ModalBody> <ModalFooter> <Button color="success" onClick={() => onSave(this.state.activeItem)} > Save </Button> </ModalFooter> </Modal> ); } } تنشئ الشيفرة السابقة الصنف CustomModal وهو يتداخل مع المكون Modal المشتق من المكتبة reactstrap. وقد عرَّفت هذه الشيفرة أيضًا ثلاثة حقول ضمن نموذج التعبئة form هي: title description completed هذه الحقول هي نفسها التي عرفناها مثل خصائص في نموذج قائمة المهام "Todo" عند إعداد الواجهة الخلفية. يستقبل CustomModal القيم activeItem و toggle و onSave مثل خاصيّات، بحيث يمثِّل: activeItem عنصر قائمة المهام الذي سيُحرّر. toggle الدالة التي ستُستخدم للتحكم في حالة المكون Modal، أي فتحه أو غلقه. onSave دالةً تُستدعى لحفظ القيم التي حرِّرت لعنصر قائمة المهام. الآن، سنستورد المكون CustomModal إلى داخل الملف "App.js". عد إلى الملف "src/App.js" وافتحه بمحرر الشيفرة واستبدل محتوياته بأسطر الشيفرة التالية: import React, { Component } from "react"; import Modal from "./components/Modal"; const todoItems = [ { id: 1, title: "Go to Market", description: "Buy ingredients to prepare dinner", completed: true, }, { id: 2, title: "Study", description: "Read Algebra and History textbook for the upcoming test", completed: false, }, { id: 3, title: "Sammy's books", description: "Go to library to return Sammy's books", completed: true, }, { id: 4, title: "Article", description: "Write article on how to use Django with React", completed: false, }, ]; class App extends Component { constructor(props) { super(props); this.state = { viewCompleted: false, todoList: todoItems, modal: false, activeItem: { title: "", description: "", completed: false, }, }; } toggle = () => { this.setState({ modal: !this.state.modal }); }; handleSubmit = (item) => { this.toggle(); alert("save" + JSON.stringify(item)); }; handleDelete = (item) => { alert("delete" + JSON.stringify(item)); }; createItem = () => { const item = { title: "", description: "", completed: false }; this.setState({ activeItem: item, modal: !this.state.modal }); }; editItem = (item) => { this.setState({ activeItem: item, modal: !this.state.modal }); }; displayCompleted = (status) => { if (status) { return this.setState({ viewCompleted: true }); } return this.setState({ viewCompleted: false }); }; renderTabList = () => { return ( <div className="nav nav-tabs"> <span className={this.state.viewCompleted ? "nav-link active" : "nav-link"} onClick={() => this.displayCompleted(true)} > Complete </span> <span className={this.state.viewCompleted ? "nav-link" : "nav-link active"} onClick={() => this.displayCompleted(false)} > Incomplete </span> </div> ); }; renderItems = () => { const { viewCompleted } = this.state; const newItems = this.state.todoList.filter( (item) => item.completed === viewCompleted ); return newItems.map((item) => ( <li key={item.id} className="list-group-item d-flex justify-content-between align-items-center" > <span className={`todo-title mr-2 ${ this.state.viewCompleted ? "completed-todo" : "" }`} title={item.description} > {item.title} </span> <span> <button className="btn btn-secondary mr-2" onClick={() => this.editItem(item)} > Edit </button> <button className="btn btn-danger" onClick={() => this.handleDelete(item)} > Delete </button> </span> </li> )); }; render() { return ( <main className="container"> <h1 className="text-white text-uppercase text-center my-4">Todo app</h1> <div className="row"> <div className="col-md-6 col-sm-10 mx-auto p-0"> <div className="card p-3"> <div className="mb-4"> <button className="btn btn-primary" onClick={this.createItem} > Add task </button> </div> {this.renderTabList()} <ul className="list-group list-group-flush border-top-0"> {this.renderItems()} </ul> </div> </div> </div> {this.state.modal ? ( <Modal activeItem={this.state.activeItem} toggle={this.toggle} onSave={this.handleSubmit} /> ) : null} </main> ); } } export default App; احفظ التغييرات وتأمل ما حصل للتطبيق في متصفحك: إذا حاولت تحرير وحفظ عنصر من عناصر قائمة المهام Todo، سيظهر لك تنبيه يُظهِرُ الكائن المرتبط بعنصر المهام Todo. سيؤدي النقر على حفظ Save أو حذف Delete إلى تنفيذ الفعل المقابل على هذا العنصر. ملاحظة: قد تواجه أخطاءً في واجهة الطرفية وذلك بحسب نسختي ريآكت وريآكتستراب اللتين تستخدمهما، وأثناء عمل هذه المراجعة، كان الخطآن التاليان من بين الأخطاء الشائعة في ريآكتستراب: Warning: Legacy context API has been detected within a strict-mode tree Warning: findDOMNode is deprecated in StrictMode ستعدل الآن التطبيق ليتمكن من التفاعل مع واجهة برمجة التطبيقات لجانغو التي بنيتها في القسم السابق. أعد فتح نافذة الطرفية الأولى وتأكد من أن الخادم يعمل، وإذا لم يكن يعمل، استخدم الأمر التالي: $ python manage.py runserver ملاحظة: إذا كنت قد أغلقت نافذة الطرفية هذه، تذكر أنك ستحتاج للذهاب إلى المجلد "backend" واستخدام صدفة Pipenv الافتراضية. لإنشاء طلبات توجه إلى نقاط النهاية لواجهة برمجة التطبيقات في خادم الواجهة الخلفية، سنثبّت واحدةً من مكتبات جافا سكريبت المهمة وهي مكتبة "axios". في نافذة الطرفية الثانية، تأكد أنك موجود داخل المجلد "frontend"، ثم ثبّت مكتبة axios: $ npm install axios@0.21.1 ثم افتح الملف "frontend/package.json" بمحرر الشيفرة وأضف خادمًا وسيطًا proxy كما يلي: [...] "name": "frontend", "version": "0.1.0", "private": true, "proxy": "http://localhost:8000", "dependencies": { "axios": "^0.18.0", "bootstrap": "^4.1.3", "react": "^16.5.2", "react-dom": "^16.5.2", "react-scripts": "2.0.5", "reactstrap": "^6.5.0" }, [...] تتركز مهمة الخادم الوسيط في المساعدة في إنشاء نفق tunneling لطلبات واجهة برمجة التطبيقات إلى شبكة أخرى وإرسالها إلى العنوان "http://localhost:8000"، ليستلمها تطبيق جانغو هناك ويعالجها، وبدون هذا الخادم الوسيط، ستضطر إلى تحديد المسارات الكاملة كما يلي: axios.get("http://localhost:8000/api/todos/") أما مع استخدام الخادم الوسيط، فستحتاج للتزويد بمسارات نسبية فقط: axios.get("/api/todos/") ملاحظة: ربما تحتاج إلى إعادة تشغيل خادم التطوير لكي يسجل الخادم الوسيط في التطبيق. عد إلى الملف "frontend/src/App.js" وافتحه بمحرر الشيفرة، إذ ستحذف في هذه الخطوة عناصر قائمة المهام todoItems ذات القيم الموفرة hardcoded وتستخدم البيانات من الطلبات الموجهة إلى خادم الواجهة الخلفية مستخدمًا handleSubmit و handleDelete. افتح الملف App.js واستبدل محتوياته بالنسخة النهائية التالية: import React, { Component } from "react"; import Modal from "./components/Modal"; import axios from "axios"; class App extends Component { constructor(props) { super(props); this.state = { viewCompleted: false, todoList: [], modal: false, activeItem: { title: "", description: "", completed: false, }, }; } componentDidMount() { this.refreshList(); } refreshList = () => { axios .get("/api/todos/") .then((res) => this.setState({ todoList: res.data })) .catch((err) => console.log(err)); }; toggle = () => { this.setState({ modal: !this.state.modal }); }; handleSubmit = (item) => { this.toggle(); if (item.id) { axios .put(`/api/todos/${item.id}/`, item) .then((res) => this.refreshList()); return; } axios .post("/api/todos/", item) .then((res) => this.refreshList()); }; handleDelete = (item) => { axios .delete(`/api/todos/${item.id}/`) .then((res) => this.refreshList()); }; createItem = () => { const item = { title: "", description: "", completed: false }; this.setState({ activeItem: item, modal: !this.state.modal }); }; editItem = (item) => { this.setState({ activeItem: item, modal: !this.state.modal }); }; displayCompleted = (status) => { if (status) { return this.setState({ viewCompleted: true }); } return this.setState({ viewCompleted: false }); }; renderTabList = () => { return ( <div className="nav nav-tabs"> <span onClick={() => this.displayCompleted(true)} className={this.state.viewCompleted ? "nav-link active" : "nav-link"} > Complete </span> <span onClick={() => this.displayCompleted(false)} className={this.state.viewCompleted ? "nav-link" : "nav-link active"} > Incomplete </span> </div> ); }; renderItems = () => { const { viewCompleted } = this.state; const newItems = this.state.todoList.filter( (item) => item.completed === viewCompleted ); return newItems.map((item) => ( <li key={item.id} className="list-group-item d-flex justify-content-between align-items-center" > <span className={`todo-title mr-2 ${ this.state.viewCompleted ? "completed-todo" : "" }`} title={item.description} > {item.title} </span> <span> <button className="btn btn-secondary mr-2" onClick={() => this.editItem(item)} > Edit </button> <button className="btn btn-danger" onClick={() => this.handleDelete(item)} > Delete </button> </span> </li> )); }; render() { return ( <main className="container"> <h1 className="text-white text-uppercase text-center my-4">Todo app</h1> <div className="row"> <div className="col-md-6 col-sm-10 mx-auto p-0"> <div className="card p-3"> <div className="mb-4"> <button className="btn btn-primary" onClick={this.createItem} > Add task </button> </div> {this.renderTabList()} <ul className="list-group list-group-flush border-top-0"> {this.renderItems()} </ul> </div> </div> </div> {this.state.modal ? ( <Modal activeItem={this.state.activeItem} toggle={this.toggle} onSave={this.handleSubmit} /> ) : null} </main> ); } } export default App; الدالة ()refreshList معدّةٌ ليُعاد استخدامها المرة تلو المرة، وتُستدعى بعد كل مرة يكتمل فيها طلب واجهة برمجة تطبيقات، ومهمتها تحديث قائمة المهام لتعرِض دومًا أحدث حالة لها؛ أما الدالة ()handleSubmit فهي مسؤولة عن عمليتي الإنشاء والتحديث، فإذا لم يمتلك العنصر الممرّر مثل معامل parameter مفتاحًا أساسيًّا، فالأرجح عندها أنه لم يُنشأ، لذا يتولى مهمة إنشائه. تأكد الآن من أن خادم الواجهة الخلفية لديك يعمل في أول نافذة طرفية استخدمتها. $ python manage.py runserver ملاحظة: إذا أغلقت نافذة الطرفية هذه، فستحتاج للذهاب إلى المجلد "backend" واستخدام صدفة Pipenv الافتراضية. وتأكد في نافذة الطرفية الثانية أنك في مجلد الواجهة الأمامية "frontend" وشغّل تطبيق الواجهة الأمامية: $ npm start الآن عندما تذهب في متصفحك إلى العنوان "http://localhost:3000"، سيسمح لك التطبيق بإنشاء المهام وقراءتها وتعديلها وحذفها. وبهذا تكتمل الواجهة الأمامية والواجهة الخلفية لتطبيق المهام. خاتمة بنينا في هذه المقالة تطبيق مهام باستخدام جانغو وريآكت، وقد حققنا هذا بالاستعانة بالمكتبات: djangorestframework و django-cors-headers و axios و bootstrap و reactstrap. إذا أردت تعلم المزيد عن جانغو ننصحك بمراجعة صفحة مواضيع جانغو في أكاديمية حسوب. وإذا أردت تعلم المزيد عن ريآكت ننصحك بزيارة دلبل ريآكت في موسوعة حسوب. ترجمة -وبتصرف- للمقالة How To Build a To-Do application Using Django and React لصاحبها Jordan Irabor. اقرأ أيضًا إنشاء تطبيق قائمة مهام باستخدام React إنشاء تطبيق جانغو وتوصيله بقاعدة بيانات البدء مع إطار العمل جانغو لإنشاء تطبيق ويب
×
×
  • أضف...