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

تطبيق عملي لتعلم جانغو - الجزء السابع: استيثاق المستخدمين وأذوناتهم


Ola Abbas

سنوضح في هذا المقال كيفية السماح للمستخدمين بتسجيل الدخول إلى موقعك باستخدام حساباتهم الخاصة، وكيفية التحكم بما يمكنهم فعله ورؤيته بالاعتماد على ما إذا سجّلوا الدخول أم لا وبالاعتماد على أذوناتهم، إذ سنوسع موقع المكتبة المحلية LocalLibrary، ونضيف صفحات تسجيل الدخول والخروج وصفحات خاصة بالمستخدمين وأخرى خاصة بالموظفين لعرض الكتب المستعارة.

  • المتطلبات الأساسية: اطّلع على جميع المقالات السابقة من هذه السلسلة، بما في ذلك المقال الخاص إدارة الجلسات.
  • الهدف: فهم كيفية إعداد واستخدام استيثاق Authentication المستخدمين وأذوناتهم Permissions.

يوفر جانغو Django نظام استيثاق وترخيص Authorization ("إذن permission") مبني على إطار عمل الجلسة (ناقشناه في المقال السابق)، والذي يسمح بالتحقق من اعتماديات Credentials المستخدم وتحديد الإجراءات التي يُسمَح لكل مستخدم بتطبيقها.

يحتوي إطار العمل على نماذج مبنية مسبقًا للمستخدمين Users والمجموعات Groups (طريقة عامة لتطبيق الأذونات على أكثر من مستخدم في وقت واحد)، والأذونات أو الرايات التي تحدّد ما إذا كان يمكن للمستخدم تطبيق مهمة، والاستمارات والعروض لتسجيل دخول المستخدمين، وأدوات العرض لتقييد المحتوى.

ملاحظة: يهدف نظام الاستيثاق إلى أن يكون عامًا جدًا وفقًا لجانغو، إذ لا يوفر بعض الميزات المتوفرة في أنظمة استيثاق الويب الأخرى، ولكن تتوفر حلول لبعض المشاكل الشائعة بوصفها حزمًا خارجية مثل تقييد محاولات تسجيل الدخول والاستيثاق الخارجية، مثل بروتوكول OAuth.

سنوضّح في هذا المقال كيفية تفعيل استيثاق المستخدم في المكتبة المحلية LocalLibrary، وإنشاء صفحات تسجيل الدخول والخروج، وإضافة أذونات إلى نماذجك، والتحكم في الوصول إلى الصفحات. سنستخدم الاستيثاق أو الأذونات لعرض قوائم الكتب المُستعارة لكل من المستخدمين وأمناء المكتبة.

يُعَد نظام الاستيثاق مرنًا جدًا، إذ يمكنك إنشاء عناوين URL والاستمارات والعروض والقوالب الخاصة بك من البداية إن أردتَ ذلك، فما عليك سوى استدعاء واجهة برمجة التطبيقات API المتوفرة لتسجيل دخول المستخدم. سنستخدم في هذا المقال عروض واستمارات استيثاق خاصة بجانغو لصفحات تسجيل الدخول والخروج، ولكن يجب إنشاء بعض القوالب، ويُعَد ذلك سهلًا جدًا، وسنوضح أيضًا كيفية إنشاء الأذونات، والتحقق من حالة تسجيل الدخول والأذونات في كلِّ من العروض والقوالب.

تتألف هذه السلسلة الفرعية من السلسلة الأشمل تعلم تطوير الويب من المقالات التالية:

تفعيل الاستيثاق

جرى تفعيل الاستيثاق تلقائيًا عندما أنشأنا موقع الويب الهيكلي سابقًا لذلك لست بحاجة تطبيق أيّ شيء آخر حاليًا.

ملاحظة: أُنجِز الضبط Configuration الضروري عندما أنشأنا التطبيق باستخدام الأمر django-admin startproject، وأُنشِئت جداول قاعدة البيانات للمستخدمين وأذونات النموذج عندما استدعينا الأمر python manage.py migrate لأول مرة.

يُجرَى إعداد الضبط في الأقسام INSTALLED_APPS و MIDDLEWARE من ملف المشروع "locallibrary/locallibrary/settings.py" كما يلي:

INSTALLED_APPS = [
    # …
    'django.contrib.auth',  # إطار عمل الاستيثاق الأساسي ونماذجه الافتراضية
    'django.contrib.contenttypes',  # نظام نوع محتوى جانغو ‫(يسمح بربط الأذونات بالنماذج)
    # …

MIDDLEWARE = [
    # …
    'django.contrib.sessions.middleware.SessionMiddleware',  # يدير الجلسات عبر الطلبات
    # …
    'django.contrib.auth.middleware.AuthenticationMiddleware',  # يربط المستخدمين بالطلبات باستخدام الجلسات
    # …

إنشاء المستخدمين والمجموعات

أنشأنا مسبقًا أول مستخدم عندما تعرّفنا على موقع مدير جانغو، وهو مستخدم مميز Superuser أُنشِئ باستخدام الأمر:

python manage.py createsuperuser

جرى استيثاق مستخدمنا المميز الذي يمتلك جميع الأذونات، لذلك يجب إنشاء مستخدم تجريبي لتمثيل مستخدم موقع عادي، وسنستخدم موقع المدير لإنشاء مجموعات المكتبة المحلية locallibrary وتسجيلات الدخول إلى موقع الويب، وهذه من أسرع الطرق لذلك.

ملاحظة: يمكنك إنشاء مستخدمين برمجيًا كما هو موضح فيما يلي، إذ يجب عليك تطبيق ذلك في حالة تطوير واجهة للسماح للمستخدمين "العاديين" بإنشاء عمليات تسجيل الدخول الخاصة بهم مثلًا، ولا ينبغي أن يتمكّن معظم المستخدمين من الوصول إلى موقع المدير:

from django.contrib.auth.models import User

# إنشاء مستخدم وحفظه في قاعدة البيانات
user = User.objects.create_user('myusername', 'myemail@crazymail.com', 'mypassword')

# عدّل الحقول ثم احفظها مرةً أخرى
user.first_name = 'Tyrone'
user.last_name = 'Citizen'
user.save()

يوصَى بشدة بإعداد نموذج model مستخدم مخصص عند بدء مشروع فعلي، إذ ستتمكّن من تخصيصه بسهولة مستقبلًا إن دعت الحاجة. اطّلع على إنشاء نماذج جانغو Django Models وربطها بقاعدة البيانات على أكاديمية حسوب لمزيد من المعلومات.

أولًا، سننشئ فيما يلي مجموعة ثم سننشئ مستخدمًا. لا نمتلك أيّ أذونات لإضافتها إلى أعضاء مكتبتنا حتى الآن، ولكن سيكون من الأسهل إضافتها دفعةً واحدة إلى المجموعة بدلًا من إضافتها بصورة فردية إلى كل عضو إن احتجنا إلى ذلك لاحقًا.

شغّل خادم التطوير وانتقل إلى موقع المدير في متصفح الويب المحلي "http://127.0.0.1:8000/admin/‎"، ثم سجّل الدخول إلى الموقع باستخدام اعتماديات حساب مستخدمك المميز.

يعرض القسم العلوي من موقع المدير جميع نماذجك التي يرتبها تطبيق جانغو. يمكنك النقر على ارتباطات المستخدمين Users أو المجموعات Groups من قسم الاستيثاق والترخيص Authentication and Authorization لرؤية سجلاتها الحالية.

01_admin_authentication_add.png

لننشئ مجموعةً جديدةً لأعضاء مكتبتنا: انقر على زر "الإضافة Add" (بجانب المجموعة Group) لإنشاء مجموعة جديدة، ثم أدخِل الاسم Name "أعضاء المكتبة Library Members" لهذه المجموعة.

02_admin_authentication_add_group.png

لا نحتاج أيّ أذونات للمجموعة، لذا اضغط على حفظ SAVE فقط، وستنتقل إلى قائمة المجموعات.

لننشئ الآن مستخدمًا، لذا انتقل أولًا إلى الصفحة الرئيسية لموقع المدير.

ثانيًا، انقر على زر "الإضافة Add" الموجود بجانب "المستخدمين Users" لفتح نافذة "إضافة مستخدم Add user".

03_admin_authentication_add_user_prt1.png

ثالثًا، أدخِل اسم مستخدم Username مناسب وكلمة مرور Password وتأكيدها Password confirmation للمستخدم التجريبي.

رابعًا، اضغط على حفظ SAVE لإنشاء المستخدم. سينشئ موقع المدير المستخدم الجديد وينقلك مباشرةً إلى شاشة تغيير المستخدم Change user، إذ يمكنك تغيير اسم المستخدم username وإضافة معلومات للحقول الاختيارية لنموذج المستخدم، والتي تتضمن الاسم الأول واسم العائلة وعنوان البريد الإلكتروني وحالة المستخدم وأذوناته (يجب ضبط الراية Active فقط). يمكنك تحديد مجموعات المستخدم وأذوناته، والاطلاع على التواريخ المهمة المتعلقة بالمستخدم، مثل تاريخ انضمامه وتاريخ آخر تسجيل دخول.

04_admin_authentication_add_user_prt2.png

خامسًا، حدّد المجموعة Library Member من قائمة المجموعات المتاحة Available groups في قسم المجموعات Groups، ثم اضغط على السهم الأيمن بين المربعين لنقل هذه المجموعة إلى مربع المجموعات المختارة Chosen groups.

05_admin_authentication_user_add_group.png

أخيرًا، لا تحتاج تطبيق شيء آخر، لذا اضغط على حفظ SAVE مرةً أخرى للانتقال إلى قائمة المستخدمين.

هذا كل شيء. أصبح لديك الآن حساب "عضو مكتبة عادي" يمكنك استخدامه للاختبار (بعد أن نقدّم الصفحات لتمكين هذا العضو من تسجيل الدخول).

ملاحظة: يجب أن تحاول إنشاء مستخدم عضو مكتبة آخر، وأن تنشئ مجموعة لأمناء المكتبة، وتضيف مستخدمًا إليها.

إعداد عروض الاستيثاق

يوفر جانغو كل ما تحتاجه تقريبًا لإنشاء صفحات استيثاق للتعامل مع إدارة عمليات تسجيل الدخول وتسجيل الخروج وكلمات المرور، ويتضمن ذلك رابط عناوين URL والعروض والاستمارات، ولكنه لا يتضمن القوالب، إذ يجب إنشاء قوالبنا الخاصة.

سنوضح في هذا القسم كيفية دمج النظام الافتراضي في موقع المكتبة المحلية LocalLibrary وإنشاء القوالب التي سنضعها في عناوين URL الرئيسية للمشروع.

ملاحظة: لا يتوجّب عليك استخدام أيٍّ من هذه الشيفرة البرمجية، ولكن يُحتمَل أنك سترغب في ذلك لأنها ستسهّل الأمور كثيرًا. يجب بالتأكيد تغيير شيفرة معالجة الاستمارة إذا غيّرت نموذج مستخدمك، ولكن ستبقى قادرًا على استخدام دوال العرض.

ملاحظة: يمكن في حالتنا وضع صفحات الاستيثاق بما في ذلك عناوين URL والقوالب ضمن تطبيق الدليل Catalog، ولكن إذا كان لدينا تطبيقات متعددة، فيُفضَّل فصل سلوك تسجيل الدخول المشترك وإتاحته على كامل الموقع، لذلك هذا ما سنوضّحه في هذا القسم.

عناوين URL الخاصة بالمشروع

أضف ما يلي إلى نهاية الملف urls.py الخاص بالمشروع "locallibrary/locallibrary/urls.py":

# ‫أضف عناوين url لاستيثاق موقع جانغو (لتسجيل الدخول والخروج وإدارة كلمات المرور)

urlpatterns += [
    path('accounts/', include('django.contrib.auth.urls')),
]

انتقل إلى العنوان "http://127.0.0.1:8000/accounts/‎" (لاحظ الشرطة المائلة للأمام في نهاية العنوان)، وسيُظِهر جانغو خطأً مفاده أنه لم يتمكن من العثور على عنوان URL، ويسرد جميع عناوين URL التي جربها، والتي يمكن منها رؤية عناوين URL التي ستعمل.

ملاحظة: يؤدي استخدام الطريقة السابقة إلى إضافة عناوين URL التالية مع أسماء موضوعة بين أقواس معقوفة square bracket، والتي يمكن استخدامها لعكس روابط عناوين URL. لست مضطرًا إلى تطبيق أيّ شيء آخر، إذ تربط عملية ربط عنوان URL السابقة تلقائيًا عناوين URL التالية:

accounts/ login/ [name='login']
accounts/ logout/ [name='logout']
accounts/ password_change/ [name='password_change']
accounts/ password_change/done/ [name='password_change_done']
accounts/ password_reset/ [name='password_reset']
accounts/ password_reset/done/ [name='password_reset_done']
accounts/ reset/<uidb64>/<token>/ [name='password_reset_confirm']
accounts/ reset/done/ [name='password_reset_complete']

حاول الآن الانتقال إلى عنوان URL لتسجيل الدخول "http://127.0.0.1:8000/accounts/login/‎"، الذي سيفشل مرةً أخرى مع وجود خطأ يخبرك بعدم وجود القالب المطلوب "registration/login.html" في مسار بحث القالب، وسترى الأسطر التالية في القسم الأصفر في الأعلى:

Exception Type:    TemplateDoesNotExist
Exception Value:    registration/login.html

الخطوة التالية هي إنشاء المجلد registration على مسار البحث ثم إضافة الملف login.html.

مجلد القوالب Template

تتوقع عناوين URL (والعروض ضمنيًا) التي أضفناها مسبقًا العثورَ على القوالب المرتبطة بها في المجلد "/registration/" في مكان ما في مسار بحث القوالب.

سنضع في موقعنا صفحات HTML في المجلد "templates/registration/‎"، إذ يجب أن يكون هذا المجلد في المجلد الجذر لمشروعك، أي المجلد نفسه للمجلدات catalog و locallibrary. إذًا، لننشئ هذه المجلدات الآن.

ملاحظة: يجب أن تبدو بنية مجلدك الآن كما يلي:

locallibrary/   # مجلد مشروع جانغو
  catalog/
  locallibrary/
  templates/
    registration/

يمكن جعل مجلد القوالب "templates" مرئيًا لمحمِّل القوالب template loader من خلال إضافته في مسار بحث القوالب، لذا افتح إعدادات المشروع "‎/locallibrary/locallibrary/settings.py"، ثم استورد بعد ذلك الوحدة os (أضف السطر التالي بالقرب من بداية الملف):

import os # تحتاجه الشيفرة البرمجية الآتية 

عدّل السطر 'DIRS' الخاص بالقسم TEMPLATES كما يلي:

    # …
    TEMPLATES = [
      {
       # …
       'DIRS': [os.path.join(BASE_DIR, 'templates')],
       'APP_DIRS': True,
       # …

قالب تسجيل الدخول Login

تحذير: قوالب الاستيثاق المتوفرة في هذا المقال هي نسخة أساسية أو مُعدَّلة قليلًا من قوالب تسجيل الدخول التوضيحية لجانغو، لذا يمكن أن تخصّصها لاستخدامك الخاص.

أنشِئ ملف HTML جديد بالاسم "‎/locallibrary/templates/registration/login.html" وضع فيه المحتويات التالية:

{% extends "base_generic.html" %}

{% block content %}

  {% if form.errors %}
    <p>Your username and password didn't match. Please try again.</p>
  {% endif %}

  {% if next %}
    {% if user.is_authenticated %}
      <p>Your account doesn't have access to this page. To proceed,
      please login with an account that has access.</p>
    {% else %}
      <p>Please login to see this page.</p>
    {% endif %}
  {% endif %}

  <form method="post" action="{% url 'login' %}">
    {% csrf_token %}
    <table>
      <tr>
        <td>{{ form.username.label_tag }}</td>
        <td>{{ form.username }}</td>
      </tr>
      <tr>
        <td>{{ form.password.label_tag }}</td>
        <td>{{ form.password }}</td>
      </tr>
    </table>
    <input type="submit" value="login">
    <input type="hidden" name="next" value="{{ next }}">
  </form>

  {# Assumes you setup the password_reset view in your URLconf #}
  <p><a href="{% url 'password_reset' %}">Lost password?</a></p>

{% endblock %}

يشترك هذا القالب في بعض أوجه التشابه مع القوالب التي رأيناها سابقًا، فهو يوسّع قالبنا الأساسي ويعدّل الكتلة content. ما تبقّى من الشيفرة البرمجية هو شيفرة معالجة الاستمارة المعيارية إلى حدٍ ما، والتي سنناقشها في مقال لاحق، ولكن كل ما تحتاج إلى معرفته الآن هو أن هذه الشيفرة البرمجية ستعرض استمارة يمكنك من خلالها إدخال اسم المستخدم وكلمة المرور الخاصين بك، وأنه سيُطلَب منك إدخال القيم الصحيحة عند تحديث الصفحة إذا أدخلت قيمًا غير صالحة.

انتقل إلى صفحة تسجيل الدخول "http://127.0.0.1:8000/accounts/login/‎" بعد حفظ قالبك، وسترى شيئًا يشبه ما يلي:

06_library_login.png

إذا سجّلت الدخول باستخدام اعتماديات صالحة، فسيعاد توجيهك إلى صفحة أخرى (هي "http://127.0.0.1:8000/accounts/profile/‎" افتراضيًا). تكمن المشكلة في أن جانغو يتوقع افتراضيًا أنه عند تسجيل الدخول سترغب في نقلك إلى الصفحة الشخصية profile، ويمكن ألّا تكون راغبًا في ذلك، وبما أنك لم تعرّف هذه الصفحة بعد، فستتلقى خطأ آخر.

افتح إعدادات المشروع "‎/locallibrary/locallibrary/settings.py" وضِف إلى نهايته النص التالي، إذ يجب الآن إعادة توجيهك إلى صفحة الموقع الرئيسية افتراضيًا عند تسجيل الدخول:

# إعادة التوجيه إلى عنوان‫ URL للصفحة الرئيسية بعد تسجيل الدخول 
# ‫تكون إعادة التوجيه الافتراضية إلى عنوان /accounts/profile/
LOGIN_REDIRECT_URL = '/'

قالب تسجيل الخروج Logout

إذا انتقلتَ إلى عنوان URL لتسجيل الخروج "http://127.0.0.1:8000/accounts/logout/‎"، فسترى بعض السلوكيات الغريبة، إذ سيُسجَّل خروج مستخدمك بالتاكيد، ولكن ستُنقَل إلى صفحة تسجيل خروج المدير Admin، وهذا ما لا تريده، لأن رابط تسجيل الدخول login link في تلك الصفحة ينقلك إلى شاشة تسجيل دخول المدير وهذا متاح فقط للمستخدمين الذين لديهم الإذن is_staff.

أنشئ الملف التالي وافتحه، ثم انسخ الشيفرة التالية:

  • الملف "‎/locallibrary/templates/registration/logged_out.html":
{% extends "base_generic.html" %}

{% block content %}
  <p>Logged out!</p>
  <a href="{% url 'login'%}">Click here to login again.</a>
{% endblock %}

يُعَد هذا القالب بسيطًا جدًا، إذ يعرض فقط رسالةً تخبرك بتسجيل خروجك، ويوفر رابطًا يمكنك الضغط عليه للعودة إلى شاشة تسجيل الدخول، وإذا ذهبت إلى عنوان URL لتسجيل الخروج مرة أخرى، فسترى الصفحة التالية:

07_library_logout.png

قوالب إعادة تعيين كلمة المرور

يستخدم نظام إعادة تعيين كلمة المرور الافتراضي البريد الإلكتروني لإرسال رابط إعادة تعيين للمستخدم، لذا يجب أن تنشئ استمارات للحصول على عنوان بريد المستخدم الإلكتروني، وإرسال البريد الإلكتروني، والسماح له بإدخال كلمة مرور جديدة، وإعلامه عند اكتمال العملية. يمكنك استخدام القوالب التالية بمثابة بداية.

استمارة إعادة تعيين كلمة المرور

هذه هي الاستمارة المستخدمة للحصول على عنوان البريد الإلكتروني للمستخدم، لإرسال بريد إلكتروني لإعادة تعيين كلمة المرور.

أنشئ الملف التالي، وضع فيه المحتويات التالية:

  • الملف "‎/locallibrary/templates/registration/password_reset_form.html":
{% extends "base_generic.html" %}

{% block content %}
  <form action="" method="post">
  {% csrf_token %}
  {% if form.email.errors %}
    {{ form.email.errors }}
  {% endif %}
      <p>{{ form.email }}</p>
    <input type="submit" class="btn btn-default btn-lg" value="Reset password">
  </form>
{% endblock %}

اكتمال إعادة تعيين كلمة المرور

تُعرَض هذه الاستمارة بعد جمع عنوان بريدك الإلكتروني. أنشئ الملف "‎/locallibrary/templates/registration/password_reset_done.html"، وضع فيه المحتويات التالية:

{% extends "base_generic.html" %}

{% block content %}
  <p>We've emailed you instructions for setting your password. If they haven't arrived in a few minutes, check your spam folder.</p>
{% endblock %}

البريد الإلكتروني لإعادة تعيين كلمة المرور

يوفّر هذا القالب نص البريد الإلكتروني بتنسيق HTML الذي يحتوي على رابط إعادة التعيين الذي سنرسله إلى المستخدمين.

أنشئ الملف التالي وضع فيه المحتويات التالية:

  • الملف "‎"/locallibrary/templates/registration/password_reset_email.html:
Someone asked for password reset for email {{ email }}. Follow the link below:
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}

تأكيد إعادة تعيين كلمة المرور

تُعَد هذه الصفحة المكان الذي تدخِل فيه كلمة المرور الجديدة بعد النقر على الرابط الموجود في صفحة البريد الإلكتروني لإعادة تعيين كلمة المرور.

أنشئ الملف التالي، وضع فيه المحتويات التالية:

  • الملف "‎/locallibrary/templates/registration/password_reset_confirm.html":
{% extends "base_generic.html" %}

{% block content %}
    {% if validlink %}
        <p>Please enter (and confirm) your new password.</p>
        <form action="" method="post">
        {% csrf_token %}
            <table>
                <tr>
                    <td>{{ form.new_password1.errors }}
                        <label for="id_new_password1">New password:</label></td>
                    <td>{{ form.new_password1 }}</td>
                </tr>
                <tr>
                    <td>{{ form.new_password2.errors }}
                        <label for="id_new_password2">Confirm password:</label></td>
                    <td>{{ form.new_password2 }}</td>
                </tr>
                <tr>
                    <td></td>
                    <td><input type="submit" value="Change my password"></td>
                </tr>
            </table>
        </form>
    {% else %}
        <h1>Password reset failed</h1>
        <p>The password reset link was invalid, possibly because it has already been used. Please request a new password reset.</p>
    {% endif %}
{% endblock %}

اكتمال إعادة تعيين كلمة المرور

هذا هو آخر قالب لإعادة تعيين كلمة المرور، إذ يُعرَض لإعلامك بنجاح إعادة تعيين كلمة المرور.

أنشئ الملف التالي، وضع فيه المحتويات التالية:

  • الملف "‎/locallibrary/templates/registration/password_reset_complete.html":
{% extends "base_generic.html" %}

{% block content %}
  <h1>The password has been changed!</h1>
  <p><a href="{% url 'login' %}">log in again?</a></p>
{% endblock %}

اختبار صفحات الاستيثاق الجديدة

أضفتَ ضبط عناوين URL وأنشأتَ جميع القوالب، ويجب الآن أن تعمل صفحات الاستيثاق الجديدة، إذ يمكنك اختبارها من خلال محاولة تسجيل الدخول ثم تسجيل الخروج من حساب المستخدم المميز باستخدام عناوين URL التالية:

  • /http://127.0.0.1:8000/accounts/login
  • /http://127.0.0.1:8000/accounts/logout

ستتمكّن من اختبار وظيفة إعادة تعيين كلمة المرور من الرابط الموجود في صفحة تسجيل الدخول، ولكن انتبه إلى أن جانغو لن يرسل رسائل البريد الإلكتروني لإعادة التعيين إلّا إلى العناوين (المستخدمين) المُخزَّنة في قاعدة بياناته مسبقًا.

ملاحظة: يتطلب نظام إعادة تعيين كلمة المرور دعم موقعك للبريد الإلكتروني، وهو ما يتجاوز نطاق هذا المقال، وبالتالي لن يعمل هذا الجزء بعد، لذا ضع السطر التالي في نهاية الملف settings.py للسماح بالاختبار، إذ يسجل هذا السطر أيّ رسائل بريد إلكتروني مُرسَلة إلى الطرفية Console، بحيث يمكنك نسخ رابط إعادة تعيين كلمة المرور من الطرفية.

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

اطلع على صفحة إرسال بريد إلكتروني في توثيق جانغو لمزيد من المعلومات.

اختبار المستخدمين المستوثقين

سنوضَح في هذا القسم ما يمكننا تطبيقه للتحكم الانتقائي في المحتوى الذي يراه المستخدم بناءً على ما إذا كان قد سجل الدخول أم لا.

اختبار القوالب

يمكنك الحصول على معلومات حول المستخدم الذي سجّل الدخول حاليًا في القوالب باستخدام متغير القالب {{ user }}، إذ يُضاف هذا المتغير إلى سياق القالب افتراضيًا عند إعداد المشروع كما فعلنا سابقًا في الموقع الهيكلي.

ستختبر أولًا متغير القالب {{ user.is_authenticated }} لتحديد ما إذا كان المستخدم مؤهلًا لمشاهدة محتوى معين، لذلك سنحدّث الشريط الجانبي لعرض رابط "تسجيل الدخول Login" إذا كان المستخدم قد سجّل الخروج، ورابط "تسجيل الخروج Logout" إذا سجّل الدخول.

افتح القالب الأساسي "‎base_generic.html" من المسار "/locallibrary/catalog/templates/" وانسخ النص التالي في الكتلة sidebar قبل وسم القالب endblock مباشرةً:

  <ul class="sidebar-nav">

    …

   {% if user.is_authenticated %}
     <li>User: {{ user.get_username }}</li>
     <li><a href="{% url 'logout' %}?next={{ request.path }}">Logout</a></li>
   {% else %}
     <li><a href="{% url 'login' %}?next={{ request.path }}">Login</a></li>
   {% endif %}
  </ul>

نستخدم الوسوم if و else و endif لعرض نص بطريقة شرطية بناءً على ما إذا كان المتغير {{ user.is_authenticated }} صحيحًا أم لا؛ فإذا كان المستخدم مستوثقًا، فسنعلم أن لدينا مستخدمًا صالحًا، لذلك نستدعي {{ user.get_username }} لعرض اسمه.

ننشئ عناوين URL لروابط تسجيل الدخول والخروج باستخدام وسم القالب url وأسماء عمليات ضبط عناوين URL ذات الصلة. لاحظ كيف ألحقنا ‎?next={{ request.path }}‎ بنهاية عناوين URL، مما يؤدي إلى إضافة معامل URL وهو next (الذي يحتوي على عنوان URL للصفحة الحالية) إلى نهاية عنوان URL المتعلق بالرابط. ستستخدم العروض قيمة "next" هذه بعد أن يسجّل المستخدم الدخول أو الخروج بنجاح لإعادة توجيه المستخدم إلى الصفحة التي نقر فيها أولًا على ارتباط تسجيل الدخول أو الخروج.

ملاحظة: جرب ما فعلناه في هذا القسم، وفي حال كنت في الصفحة الرئيسية ونقرتَ على تسجيل الدخول أو تسجيل الخروج Login/Logout في الشريط الجانبي، فيجب أن ينتهي بك الأمر في الصفحة نفسها بعد اكتمال العملية.

اختبار العروض Views

إذا استخدمتَ العروض التي تستند إلى الدوال، فإن أسهل طريقة لتقييد الوصول إلى دوالك هي تطبيق المزخرف decorator الذي يدعى login_required على دالة عرضك؛ فإذا سجّل المستخدم دخوله، فستُنفَّذ شيفرة العرض كما هو معتاد؛ وإذا لم يسجّل المستخدم دخوله، فسيُعاد توجيهه إلى عنوان URL لتسجيل الدخول المُعرَّف في إعدادات المشروع settings.LOGIN_URL، مع تمرير المسار المطلق الحالي بوصفه معامل URL الذي هو next. إذا نجح المستخدم في تسجيل الدخول، فسيُعاد إلى هذه الصفحة، ولكن سيكون مستوثقًا هذه المرة.

from django.contrib.auth.decorators import login_required

@login_required
def my_view(request):
    # …

ملاحظة: يمكنك تطبيق الشيء نفسه يدويًا من خلال اختبار request.user.is_authenticated، ولكن استخدام المزخرف يُعَد ملائمًا أكثر.

وبالمثل، أسهل طريقة لتقييد وصول المستخدمين الذين سجلوا الدخول في عروضك المستندة إلى الأصناف هي الاشتقاق من LoginRequiredMixin، ولكن يجب أن تصرّح عن هذا المخلوط mixin أولًا في قائمة الصنف الأب قبل صنف العرض الرئيسي.

from django.contrib.auth.mixins import LoginRequiredMixin

class MyView(LoginRequiredMixin, View):
    # …

وهذا له سلوك إعادة التوجيه نفسه للمزخرف login_required. يمكنك أيضًا تحديد موقع بديل لإعادة توجيه المستخدم إليه إن لم يكون مستوثقًا login_url، وتحديد اسم معامل URL بدلًا من المعامل "next" لإدخال المسار المطلق الحالي redirect_field_name.

class MyView(LoginRequiredMixin, View):
    login_url = '/login/'
    redirect_field_name = 'redirect_to'

اطلع على توثيق جانغو لمزيد من التفاصيل.

تطبيق عملي: سرد كتب المستخدم الحالي

تعرّفنا على كيفية تقييد صفحة لمستخدم معين، إذًا لننشئ عرضًا للكتب التي استعارها المستخدم الحالي. لسوء الحظ، ليس لدينا حتى الآن أيّ طريقة يمكن للمستخدمين من خلالها استعارة الكتب، لذا سنوسّع أولًا نموذج BookInstance لدعم مفهوم الاستعارة واستخدام تطبيق مدير جانغو لإعارة عدد من الكتب لمستخدمنا التجريبي قبل أن نتمكن من إنشاء قائمة الكتب.

النماذج Models

أولًا، يجب أن نجعل من الممكن للمستخدمين الحصول على نسخة كتاب BookInstance على سبيل الإعارة. لدينا فعليًا حالة status وتاريخ الاسترجاع due_back، ولكن ليس لدينا حتى الآن أي ارتباط association بين هذا النموذج والمستخدم، لذا سننشئ هذا الارتباط باستخدام حقل ForeignKey (علاقة واحد إلى متعدد). نحتاج أيضًا إلى آلية سهلة لاختبار ما إذا كان الكتاب المُعار متأخرًا عن تاريخ استرجاعه أم لا.

افتح الملف catalog/models.py، واستورد نموذج المستخدم User من django.contrib.auth.models، وضِف ذلك مباشرةً بعد سطر الاستيراد السابق في بداية الملف، بحيث يكون User متاحًا للشيفرة البرمجية اللاحقة التي تستخدمه:

from django.contrib.auth.models import User

أضف بعد ذلك الحقل borrower إلى النموذج BookInstance كما يلي:

borrower = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)

لنضِف أيضًا خاصية يمكننا استدعاؤها من قوالبنا لمعرفة ما إذا كان هناك نسخة لكتاب معين متأخرة عن تاريخ استرجاعها، إذ يمكننا حساب ذلك في القالب نفسه، ولكن يُعَد استخدام خاصية property كما هو موضح لاحقًا أكثر كفاءة، لذلك أضف ما يلي في مكان ما بالقرب من بداية الملف:

from datetime import date

ضِف بعد ذلك تعريف الخاصية الآتي إلى الصنف BookInstance.

ملاحظة: تستخدم الشيفرة التالية دالة بايثون bool()‎ التي تقيّم كائنًا أو كائنًا ناتجًا عن تعبيرٍ ما، وتعيد القيمة True إلّا إذا كانت النتيجة "خاطئة"، فتعيد في هذه الحالة القيمة False. يكون الكائن خاطئًا في بايثون (يُقيَّم على أنه False) إذا كان فارغًا (مثل [] و () و {}) أو 0 أو None أو إذا كان False.

@property
def is_overdue(self):
    """Determines if the book is overdue based on due date and current date."""
    return bool(self.due_back and date.today() > self.due_back)

ملاحظة: نتحقق أولًا ما إذا كان due_back فارغًا قبل إجراء موازنة، إذ يمكن أن يتسبب حقل due_back الفارغ في أن يعطي جانغو خطأً بدلًا من إظهار الصفحة، فالقيم الفارغة غير قابلة للموازنة، ولا نريد أن يظهر ذلك الخطأ لمستخدمينا.

حدّثنا نماذجنا، ويجب الآن إجراء عمليات تهجير migrations جديدة في المشروع ثم تطبيقها كما يلي:

python3 manage.py makemigrations
python3 manage.py migrate

المدير Admin

افتح الملف "catalog/admin.py"، وضِف الحقل borrower إلى الصنف BookInstanceAdmin في كلٍ من list_display و fieldsets كما هو موضح فيما يلي، وسيجعل هذا الحقل مرئيًا في قسم المدير Admin، وبالتالي سيُسمَح لنا بإسناد مستخدم User إلى نسخة كتاب BookInstance عند الحاجة:

@admin.register(BookInstance)
class BookInstanceAdmin(admin.ModelAdmin):
    list_display = ('book', 'status', 'borrower', 'due_back', 'id')
    list_filter = ('status', 'due_back')

    fieldsets = (
        (None, {
            'fields': ('book', 'imprint', 'id')
        }),
        ('Availability', {
            'fields': ('status', 'due_back', 'borrower')
        }),
    )

إعارة بعض الكتب

أصبح من الممكن إعارة الكتب إلى مستخدم معين، لذا جرّب إعارة عددٍ من سجلات BookInstance. اضبط الحقل borrowed لمستخدمك التجريبي، واجعل الحالة status مُعار "On loan"، واضبط تواريخ الاسترجاع اللاحقة والسابقة.

ملاحظة: لن نشرح العملية بالتفصيل، لأنك تعرف مسبقًا كيفية استخدام موقع المدير.

عرض الإعارة On loan

سنضيف الآن عرضًا للحصول على قائمة بجميع الكتب المُعارة للمستخدم الحالي، إذ سنستخدم عالعروض Views العامة والتفصيلية نفسه الذي نعرفه، ولكن سنستورد ونشتق LoginRequiredMixin هذه المرة، بحيث لا يتمكن سوى المستخدم الذي سجّل الدخول من استدعاء هذا العرض. سنصرّح عن اسم القالب template_name بدلًا من استخدام القيمة الافتراضية، لأنه يمكن أن يكون لدينا عدة قوائم مختلفة من سجلات BookInstance مع عروض وقوالب مختلفة.

أضف ما يلي إلى الملف catalog/views.py:

from django.contrib.auth.mixins import LoginRequiredMixin

class LoanedBooksByUserListView(LoginRequiredMixin,generic.ListView):
    """Generic class-based view listing books on loan to current user."""
    model = BookInstance
    template_name = 'catalog/bookinstance_list_borrowed_user.html'
    paginate_by = 10

    def get_queryset(self):
        return (
            BookInstance.objects.filter(borrower=self.request.user)
            .filter(status__exact='o')
            .order_by('due_back')
        )

يمكننا تقييد استعلامنا عن كائنات BookInstance للمستخدم الحالي فقط من خلال إعادة تقديم التابع get_queryset()‎ كما هو موضح في الشيفرة السابقة. لاحظ أن الرمز "o" هو الرمز المُخزَّن الذي يمثل الإعارة "on loan" وتُرتب هذه الكتب المُعارة حسب تاريخ استرجاعها due_back بحيث تُعرَض العناصر الأقدم أولًا.

ضبط عناوين URL للكتب المعارة

افتح الملف "‎/catalog/urls.py" وأضِف التابع path()‎ الذي يشير إلى العرض السابق. يمكنك نسخ النص التالي إلى نهاية الملف:

urlpatterns += [
    path('mybooks/', views.LoanedBooksByUserListView.as_view(), name='my-borrowed'),
]

قالب الكتب المعارة

ما نحتاجه الآن هو إضافة قالب لهذه الصفحة، لذا أنشئ أولًا ملف القالب وضع فيه المحتويات التالية:

  • الملف "‎/catalog/templates/catalog/bookinstance_list_borrowed_user.html":
{% extends "base_generic.html" %}

{% block content %}
    <h1>Borrowed books</h1>

    {% if bookinstance_list %}
    <ul>

      {% for bookinst in bookinstance_list %}
      <li class="{% if bookinst.is_overdue %}text-danger{% endif %}">
        <a href="{% url 'book-detail' bookinst.book.pk %}">{{ bookinst.book.title }}</a> ({{ bookinst.due_back }})
      </li>
      {% endfor %}
    </ul>

    {% else %}
      <p>There are no books borrowed.</p>
    {% endif %}
{% endblock %}

يشبه هذا القالب إلى حدٍ كبير القوالب التي أنشأناها مسبقًا لكائنات Book و Author، ولكن الشيء الوحيد المختلف هنا هو أننا نتحقق من التابع الذي أضفناها في النموذج (bookinst.is_overdue) ونستخدمه لتغيير لون العناصر المتأخرة.

يجب أن تكون الآن قادرًا على عرض القائمة الخاصة بالمستخدم الذي سجّل الدخول في متصفحك على العنوان "http://127.0.0.1:8000/catalog/mybooks/‎" عند تشغيل خادم التطوير. جرّب ذلك عندما يسجّل المستخدم الدخول وعندما يسجّل خروجه، إذ يجب إعادة توجيهك إلى صفحة تسجيل الدخول في الحالة الثانية.

إضافة القائمة إلى الشريط الجانبي

الخطوة الأخيرة هي إضافة رابط لهذه الصفحة الجديدة إلى الشريط الجانبي، إذ سنضعه في نفس القسم لعرض معلومات أخرى للمستخدم الذي سجّل الدخول.

افتح القالب الأساسي "‎/locallibrary/catalog/templates/base_generic.html" وضِف سطر "My Borrowed" الذي يمثل الكتب التي استعارها المستخدم إلى الشريط الجانبي في الموضع الموضح فيما يلي:

<ul class="sidebar-nav">
   {% if user.is_authenticated %}
   <li>User: {{ user.get_username }}</li>

   <li><a href="{% url 'my-borrowed' %}">My Borrowed</a></li>

   <li><a href="{% url 'logout' %}?next={{ request.path }}">Logout</a></li>
   {% else %}
   <li><a href="{% url 'login' %}?next={{ request.path }}">Login</a></li>
   {% endif %}
 </ul>

إذا سجل أيّ مستخدم الدخول، فسيرى رابط "My Borrowed" في الشريط الجانبي، وقائمة الكتب المعروضة على النحو التالي (الكتاب الأول ليس له تاريخ استرجاع، وهو خطأ يجب إصلاحه في مقال لاحق):

08_library_borrowed_by_user.png

الأذونات

ترتبط الأذونات بالنماذج وتحدّد العمليات التي يمكن أن يجريها مستخدم لديه الإذن على نسخة النموذج، إذ يمنح جانغو افتراضيًا أذونات الإضافة والتغيير والحذف لجميع النماذج تلقائيًا، مما يسمح للمستخدمين الذين لديهم الأذونات بتنفيذ الإجراءات المرتبطة بها عبر موقع المدير. يمكنك تحديد أذونات النماذج الخاصة بك ومنحها لمستخدمين محددين، ويمكنك تغيير الأذونات المرتبطة بنسخ مختلفة من النموذج نفسه.

يُعَد اختبار الأذونات في العروض والقوالب مشابهًا جدًا للاختبار في حالة الاستيثاق، بل ويختبر اختبارُ الأذونات الاستيثاقَ أيضًا.

النماذج models

تُعرَّف الأذونات في قسم النموذج "class Meta" باستخدام الحقل permissions، ويمكنك تحديد العديد من الأذونات التي تريدها ضمن صف tuple، إذ يُعرَّف كل إذن في صف متداخل nested tuple يحتوي على اسم الإذن وقيمة عرضه، فمثلًا يمكن أن نعرّف إذنًا للسماح للمستخدم بتمييز الكتاب بوصفه مُعادًا كما يلي:

class BookInstance(models.Model):
    # …
    class Meta:
        # …
        permissions = (("can_mark_returned", "Set book as returned"),)

يمكننا بعد ذلك إسناد الإذن لمجموعة "أمناء المكتبة Librarian" في موقع المدير.

افتح الملف catalog/models.py، وضِف الإذن كما هو موضح سابقًا. ينبغي كذلك إعادة تشغيل عمليات التهجير (استدعِ الأمرين التاليين لتحديث قاعدة البيانات بصورة مناسبة:

python3 manage.py makemigrations
python3 manage.py migrate

القوالب Templates

تُخزَّن أذونات المستخدم الحالي في متغير قالب يسمى {{ perms }}، ويمكنك التحقق مما إذا كان المستخدم الحالي لديه إذن معين باستخدام اسم المتغير المحدد ضمن تطبيق جانغو المرتبط به، فمثلًا ستكون قيمة {{ perms.catalog.can_mark_returned }} هي True إذا كان المستخدم لديه هذا الإذن، و False في الحالات الأخرى.

نختبر عادةً الإذن باستخدام وسم القالب {% if %} كما يلي:

{% if perms.catalog.can_mark_returned %}
    <!-- We can mark a BookInstance as returned. -->
    <!-- Perhaps add code to link to a "book return" view here. -->
{% endif %}

العروض Views

يمكن اختبار الأذونات في عرض مستند إلى الدوال باستخدام المزخرف permission_required أو في عرض مستند إلى الأصناف باستخدام الصنف PermissionRequiredMixin. يكون النمط هنا مماثلًا لاستيثاق تسجيل الدخول، بالرغم من أنه يمكن أن تضطر إلى إضافة عدة أذونات.

إليك مزخرف عرض مستند إلى الدوال:

from django.contrib.auth.decorators import permission_required

@permission_required('catalog.can_mark_returned')
@permission_required('catalog.can_edit')
def my_view(request):
    # …

إليك PermissionRequiredMixin للعروض المستندة إلى الأصناف:

from django.contrib.auth.mixins import PermissionRequiredMixin

class MyView(PermissionRequiredMixin, View):
    permission_required = 'catalog.can_mark_returned'
    # أو أذونات متعددة
    permission_required = ('catalog.can_mark_returned', 'catalog.can_edit')
    # ‫لاحظ أن 'catalog.can_edit' هو مجرد مثال
    # ‫تطبيق الدليل catalog ليس لديه هذا الإذن

ملاحظة: يوجد اختلاف افتراضي بسيط في السلوك السابق، إذ يكون لمستخدم سجّل الدخول ولكنه انتهك إذنًا ما افتراضيًا ما يلي:

  • ‎@permission_required: الذي يعيد التوجيه إلى شاشة تسجيل الدخول (حالة HTTP التي هي 302).
  • PermissionRequiredMixin: الذي يعيد قيمة الحالة 403 (وهي حالة HTTP التي تمثل أن الوصول ممنوع Forbidden).

ستحتاج عادةً إلى استخدام سلوك PermissionRequiredMixin الذي يعيد القيمة 403 إذا سجّل المستخدم الدخول ولكنه لا يمتلك الإذن الصحيح، ويمكن تحقيق ذلك للعرض المستند إلى الدوال من خلال استخدام ‎@login_required و ‎@permission_required مع raise_exception=True كما يلي:

from django.contrib.auth.decorators import login_required, permission_required

@login_required
@permission_required('catalog.can_mark_returned', raise_exception=True)
def my_view(request):
    # …

تحدى نفسك

أوضحنا في هذا المقال كيفية إنشاء صفحة للمستخدم الحالي، مع سرد الكتب التي استعارها، ويتمثل التحدي الآن في إنشاء صفحة مماثلة تكون مرئية فقط لأمناء المكتبة وتعرض جميع الكتب المُستعارة وتتضمن اسم كل مستعير.

يجب أن تكون قادرًا على اتباع النمط نفسه المُتبَع في العرض السابق الذي نفّذناه، ولكن الاختلاف الرئيسي هو أنك ستحتاج إلى تقييد العرض لأمناء المكتبة فقط، ويمكنك تحقيق ذلك بناءً على ما إذا كان المستخدم موظفًا (مزخرف الدالة هو staff_member_required ومتغير القالب هو user.is_staff)، ولكننا نوصي باستخدام الإذن can_mark_returned و PermissionRequiredMixin كما هو موضح في القسم السابق.

ملاحظة: تذكّر عدم استخدام مستخدمك المميز للاختبار المستند إلى الأذونات، إذ تكون عمليات التحقق من الأذونات صحيحة للمستخدمين المميزين دائمًا، حتى إن لم يُعرَّف الإذن بعد، لذا أنشِئ مستخدمًا لأمين المكتبة، وأضِف له القدرات المطلوبة.

يجب أن تبدو صفحتك كما يلي عند الانتهاء:

09_library_borrowed_all.png

الخلاصة

أنشأتَ موقع ويب يمكن لأعضاء المكتبة فيه تسجيل الدخول وعرض محتواهم، ويمكن لأمناء المكتبة (مع الإذن الصحيح) عرض جميع الكتب المُعارة ومستعيري هذه الكتب. عرضنا المحتوى فقط، ولكن تُستخدَم المبادئ والأساليب نفسها عندما تريد تعديل البيانات وإضافتها.

سنتعرّف في المقال التالي على كيفية استخدام استمارات جانغو لجمع مدخلات المستخدم، ثم البدء في تعديل بعض بياناتنا المُخزَّنة.

ترجمة -وبتصرُّف- للمقال Django Tutorial Part 8: User authentication and permissions.

اقرأ المزيد


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...