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

بناء تطبيق مهام باستخدام جانغو Django وريآكت React


Mostafa Almahmoud

سنبني في هذه المقالة تطبيق المهام To-Do application باستخدام جانغو Django وريآكت React.

ريآكت هي مكتبة مبنية بلغة جافا سكربت، وتُستخدم لتطوير تطبيقات الصفحة الواحدة Single-page applications -أو اختصارًا SPA. تتمتع ريآكت بتوثيق قوي ومنظومة بيئية حيَّة؛ أما جانغو فهو إطار عمل ويب مبني بلغة بايثون طُوّر لتبسيط الممارسات الشائعة المتبعة لتطوير الويب، وهو إطار عمل موثوق ويتمتع هو الآخر بمنظومة بيئية حيَّة من المكتبات البرمجية المستقرة التي تدعم احتياجات التطوير الشهيرة.

ستلعب ريآكت في التطبيق الذي سنطوّره دور الواجهة الأمامية frontend، أو ما يسمى "إطار عمل من جهة العميل"، إذ ستتولى مسؤولية التعامل مع واجهة المستخدم إلى جانب جلب ووضع قيم البيانات من خلال الطلبات المُرسلة إلى الواجهة الخلفية لجانغو، التي تمثّل واجهة برمجة تطبيقات API بُنِيَت باستخدام إطار عمل جانغو ريست Django REST -أو اختصارًا DRF.

ستحصل في نهاية هذا المقال على تطبيق فعال ونظامي.

نتيجة بناء تطبيق مهام باستخدام جانغو Django وريآكت React

ملاحظة: إذا أردت الحصول على الشيفرة المصدرية لهذا المقال، فستجدها هنا.

تنبيه: الشيفرة الواردة هي للأغراض التعليمية وليست للاستخدام المهني للشركات.

سيسمح التطبيق لمستخدميه بإنشاء قائمة بالمهام التي يودون إنجازها، وتُنجز كل مهمة بوضع علامة "مكتمل complete" عليها، وفي حال عدم إنجازها تُوضع عليها علامة "غير مكتمل incomplete".

المتطلبات الأساسية

سنحتاج لكي نتمكن من تنفيذ ما في هذه المقالة إلى ما يلي:

  1. تثبيت بايثون 3 وإعداد بيئة برمجية محلية له.
  2. تثبيت 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" في متصفح الويب الذي تستخدمه، وستظهر لك الصفحة التالية:

صفحة العنوان 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" باستخدام هذه الواجهة:

التعديل على عناصر "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" في متصفح الويب الذي تستخدمه.

استخدام http://localhost:8000/api/todos في متصفح الويب المستعمل

يمكنك إنشاء عنصر مهام Todo جديد بهذه الواجهة باستخدام العملية CREATE كما يلي:

007_New_Todo_Item.png

إذا كانت عناصر قائمة المهام Todo قد أنشئت بنجاح، فستحصل على استجابة ناجحة.

نتيجة إنشاء عناصر قائمة المهام Todo

يمكنك أيضًا تنفيذ عمليتي الحذف DELETE والتحديث UPDATE على بعض عناصر Todo بناءً على المفاتيح الأساسية id التي تزودها. استخدم بنية العنوان "‎/api/todos/{id}‎" وحدد المفتاح الأساسي "id" الذي تريد تطبيق هذه العمليات عليه.

أضف "1" إلى نهاية عنوان محدد الموارد الموحد URL لمعرفة عنصر Todo الذي يحمل مفتاحه الأساسي القيمة "1"، ثم انتقل إلى "http://localhost:8000/api/todos/1" في متصفح الويب الذي تستخدمه. تبين الشاشة التالية النتيجة التي حصلنا عليها:

009_Todo_Instance.png

إذا ظهرت لك هذه الشاشة فذلك يعني أنك انتهيت من بناء الواجهة الخلفية للتطبيق.

الخطوة الثالثة - إعداد الواجهة الأمامية

بعد أن انتهيت من إعداد الواجهة الخلفية، حان وقت تجهيز الواجهة الأمامية وتمكينها من التواصل مع الواجهة الخلفية عبر الواجهة التي أنشأتها.

افتح نافذة طرفية واذهب إلى مجلد المشروع "django-todo-react".

سنعتمد في إعداد الواجهة الأمامية على التطبيق Create React الذي تتنوع طرق استخدامه، ومن أشهرها استخدام مشغّل npx لتشغيل الحزمة وإنشاء المشروع:

$ npx create-react-app frontend

وللاطلاع أكثر على هذه الطريقة يرجى مراجعة بدء العمل مع مكتبة ريآكت.

يمكنك الآن بعد أن أنشأت المشروع الانتقال إلى المجلد الذي أنشأته للتو "frontend":

$ cd frontend

الآن، شغّل التطبيق:

$ npm start

سيفتح متصفحك الرابط "http://localhost:3000" وستظهر لك الشاشة الابتدائية لتطبيق "Create React".

الشاشة الابتدائية لتطبيق "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 المهام غير المكتملة في المقابل.

احفظ الآن التغييرات التي أجريتها وتأمل النتيجة في متصفحك:

نتيجة العمل على تطبيق to do

هناك أفعال معينة داخل تطبيق المهام، مثل إضافة المهام وتحريرها، ولكي تتمكن من التعامل معها ستحتاج إلى إنشاء مكوّن شرطي 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.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...