فصل هشتم: اپلیکیشن شبکه اجتماعی (بخش اول)
فهرست مطالب
- ایجاد اپلیکیشن و شخصی سازی User
- استفاده همزمان از شماره تلفن و یوزر نیم برای لاگین
- ثبت نام و اعتبار سنجی شماره موبایل و یوزرنیم
- ارسال ایمیل با جنگو (ارسال تیکت با smtp)
- تغییر و بازنشانی پسورد با ایمیل
- پیاده سازی سیستم تگ برای پست ها
- نمایش تمام پست های یک تگ
- ذخیره تگ ها هنگام ایجاد پست
- نمایش مشابه ترین پست ها با کمک تگ
- تمرینات فصل هشتم (مهم)
ایجاد اپلیکیشن و شخصی سازی User
در این فصل میخواهیم یک اپلیکیشن شبکه اجتماعی ایجاد کنیم؛ بدلیل اینکه این پروژه نسبت به پروژه وبلاگ تفاوت های زیادی دارد، برای همین یک پروژه جنگو جدید ایجاد میکنیم.
1- با توجه به توضیحات فصل 1، برای اپلیکیشن شبکه اجتماعی یک پروژه جدید ایجاد میکنیم، سپس یک اپ(app) ساخته و آنرا به پروژه معرفی میکنیم.
2- در دایرکتوری اپ (app)، یک فایل پایتونی بنام urls.py ایجاد میکنیم تا URL های مربوط به app را آنجا ایجاد کنیم.
app directory/urls.py
from django.urls import path from . import views app_name = 'social' urlpatterns = []
3- برای اینکه url های ایجاد شده کار کنند لازم است در فایل urls.py پروژه، url های اپ(app) را معرفی کنیم.
project directory/urls.py
from django.contrib import admin from django.urls import path, include urlpatterns = [Z path('admin/', admin.site.urls), path('', include('social.urls'), name='social'), ]
برای شخصی سازی مدل User و اضافه کردن فیلدهای دلخواه به آن، باید از یکی از کلاس های AbstractUser و یا AbstractBaseUser ارث بری کنیم.
AbstractBaseUser
برای شخصی سازی خیلی زیاد، از AbstractBaseUser استفاده میکنیم.
تمام فیلدها را خودمان بای تعریف کنیم. / پسورد و احراز هویت را خودش پشتیبانی میکند.
برای AbstractBaseUser حتما باید یک manager بنویسیم.
AbstractUser
استفاده از AbstractUser ساده تر هست.
این کلاس فیلدهای username, password, firstname, lastname, email را دارد؛ بنابراین فقط کافیست فیلدهای جدید را به آنها اضافه کنیم.
1- برای شخصی سازی مدل User، AbstractUser را ایمپورت میکنیم.
2- یک کلاس(مدل) جدید ایجاد میکنیم، که از AbstractUser ارث بری میکند.
پس از ارث بری کلاس جدید تمام فیلدهای username, password, firstname, lastname, email را خواهد داشت.
3- حالا فیلدهای جدید را به مدل User شخصی سازی شده اضافه میکنیم.
app directory/models.py
from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): profile_image = models.ImageField(upload_to='profile_images', blank=True, null=True) bio = models.TextField(max_length=500, blank=True, null=True) birth_date = models.DateField(blank=True, null=True) job = models.CharField(max_length=100, blank=True, null=True) phone_number = models.CharField(max_length=11, blank=True, null=True) class Meta: ordering = ['-username'] indexes = [models.Index(fields=['-username'])] def __str__(self): return self.username
پس از ایجاد مدل User شخصی سازی شده، لازم است آن مدل را به پروژه معرفی کنیم:
در انتهای settings.py یک متغیر بنام AUTH_USER_MODEL ایجاد کرده و مدل User را بهش معرفی میکنیم.
project directory/settings.py
AUTH_USER_MODEL = 'app_name.model_name' # -------------------------------------- AUTH_USER_MODEL = 'social.User'
چون در مدل از تصویر استفاده میکنیم لازم است pillow نصب شود.
terminal
pip install pillow
دستورات makemigrations و migrate فراموش نشه!!!
تنظیمات مربوط به تصاویر (MEDIA_URL, MEDIA_ROOT) را باید برای پروژه انجام دهیم:
1- تغییرات در settings.py:
project directory/settings.py
MEDIA_URL = '/images/' MEDIA_ROOT = os.path.join(BASE_DIR, 'images')
2- تغییرات در urls.py:
project directory/urls.py
# changes 1: from django.conf.urls.static import static from django.conf import settings # ---------------------------------------- urlpatterns = [ path('admin/', admin.site.urls), path('', include('social.urls'), namespace='social'), ] # changes 2: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
نمایش User شخصی سازی شده در پنل ادمین:
در admin.py مواردی باید ایمپورت شوند:
app directory/admin.py
from django.contrib import admin from django.contrib.auth.admin import UserAdmin from .models import *
برای نمایش فیلدهای جدید (شخصی سازی شده)، در پنل ادمین لازم است در admin.py اقداماتی انجام دهیم:
از fieldset برای گروه بندی مجموعه ای از فیلدها استفاده میشود.
app directory/admin.py
@admin.register(User) class UserAdmin(UserAdmin): list_display = ('username', 'profile_image', 'first_name', 'last_name', 'phone_number') fieldsets = UserAdmin.fieldsets + ( ('additional info', {'fields': ('profile_image', 'bio', 'birth_date', 'job', 'location', 'gender', 'phone_number')}), )
توضیحات:
1- از اتریبیوت fieldsets برای نمایش فیلدهای جدید در پنل ادمین استفاده میکنیم.
در پنل ادمین فیلدها در fieldset های مختلفی وجود دارند؛ حالا برای نمایش فیلدهای جدید در پنل ادمین باید یک fieldset جدید به مجموعه fieldset های مدل User اضافه کنیم و فیلدهای جدید را در آن نمایش دهیم.
2- حالا باید یک fieldset جدید اضافه کنیم؛ برای این کار با دستور UserAdmin.fieldsets همه فیلدست ها را فراخوانی کرده و یک tupple به مجموعه تاپل های آن اضافه میکنیم.
fieldset ها به صورت تاپل میباشند.
3- داخل تاپلی که در مرحله 2 ایجاد کردیم، یک تاپل دیگه ایجاد میکنیم؛ حالا داخل این یکی تاپل، باید فیلدست جدید را معرفی کنیم.
(fieldset_name, {"fields": (fields_name, ...)})
این تاپل دو مقدار دارد:
مقدار اول: اسم فیلدست میباشد مقدار دوم: یک دیکشنری میباشد که فیلدها در آن نوشته میشوند.
4- این دیکشنری، یک کلید و مقدار دارد:
کلید آن عبارت "fields" میباشد. ومقدار آن یک تاپل هست. / داخل این تاپل اسم فیلدهای جدید را مینویسیم.
استفاده همزمان از شماره تلفن و یوزر نیم برای لاگین
میخواهیم قابلیت لاگین کردن با شماره تلفن و یا ایمیل را به پروژه اضافه کنیم. / کاربرها بتوانند افزون بر username با شماره تلفن و یا ایمیل هم لاگین کنند.
ساختار پیشفرض جنگو برای لاگین کردن در وبسایت، استفاده از username و password میباشد.
که از تنظیمات مسیر زیر استفاده میکند.
django.contrib.auth.backends.ModelBackend
برای اینکه بتوانیم با ایمیل و شماره تلفن هم در وبسایت لاگین کنیم، باید مراحل زیر را انجام دهیم:
1- در یک فایل پایتونی که در ادامه ایجاد میکنیم، برای هر روش لاگین کردن یک کلاس ایجاد میکنیم؛ سپس داخل بدنه این کلاس ها از دو متد authenticate و get_user استفاده میکنیم.
2- بعد باید این کلاس های ایجاد شده را به پروژه معرفی کنیم.
لاگین و لاگ اوت را به این پروژه هم اضافه میکنیم. / url, form, template, view
مراحل بالا را در ادامه انجام خواهیم داد ولی قبلش login , logout را به پروژه اضافه کنیم:
با ارث بری فرم login از AuthenticationForm اعتبارسنجی های لازم به صورت خودکار اعمال میوند.
اضافه کردن فرم لاگین:
app directory/forms.py
from django import forms from django.contrib.auth.forms import AuthenticationForm from .models import * class LoginForm(AuthenticationForm): username = forms.CharField(label='Username', required=True) password = forms.CharField(label='Password', required=True, widget=forms.PasswordInput)
ایجاد view برای logout:
app directory/views.py
from django.contrib.auth import logout from django.shortcuts import redirect def log_out(request): logout(request) return redirect('social:login')
URL های لاگین و لاگ اوت:
app directory/urls.py
urlpatterns = [ path('login/', auth_views.LoginView.as_view(authentication_form=LoginForm), name='login'), # path('logout/', auth_views.LogoutView.as_view(), name='logout'), path('logout/', views.log_out, name='logout'), ]
ایجاد url برای صفحه پروفایل:
app directory/urls.py
urlpatterns = [ path('profile/', views.profile, name='profile'), ]
اعمال تنظیمات مربوطه جهت شناسایی url های لاگین و لاگ اوت:
project directory/settings.py
LOGIN_REDIRECT_URL = "/profile/" LOGIN_URL = "/login/" LOGOUT_URL = "/logout/"
قابلیت لاگین با شماره تلفن و ایمیل:
1- در دایرکتوری app یک فایل پایتونی بنام authentication.py ایجاد میکنیم.
مدل User را باید ایمپورت کنیم.
2- برای لاگین کردن با شماره تلفن یک کلاس ایجاد میکنیم. / این کلاس از کلاس دیگری ارث بری نمیکند.
حالا از دو متد get_user و authenticate در کلاس استفاده میکنیم:
authenticate: برای لاگین کردن کاربر به سیستم استفاده میشود. این متد در فرآیند احراز هویت استفاده میشود تا بررسی کند آیا اطلاعات وارد شده توسط کاربر معتبر است یا خیر.
get_user: برای بازیابی اطلاعات کاربر از دیتابیس با استفاده از شناسه کاربر. این متد میتواند در بخشهای مختلف سیستم برای دسترسی به اطلاعات کاربر استفاده شود، مثل زمانهایی که سیستم نیاز دارد اطلاعات کاربر لاگین شده را از session ID دریافت کند.
3- در متد authenticate، آرگومان های username, password درواقع اسم فیلدهای فرم login هستند. / مقادیر این آرگومان ها را اتوماتیک از فرم دریافت میکند.
-
خب الآن میخواهیم با شماره تلفن لاگین کنیم بنابراین بررسی میکنیم کاربری با شماره تلفن وارد شده(در آرگومان username) وجود دارد یا نه!
-
اگه کاربر وجود داشت؛ بررسی میکنیم که پسورد وارد شده ، تطابق دارد یا نه اگه تطابق داشت که کاربر را برمیگرداند و کاربر در وبسایت لاگین میشود. / اگر هم پسورد تطابق نداشت None را برمیگرداند.
با متد checkpassword() بررسی میکند که آیا هش پسورد وارد شده با هش پسورد ذخیره شده در دیتابیس؛ برای آن کاربر برابر هست یا نه!
- حالا اگر کاربر وجود نداشت و یا چند کاربر با آن شماره تلفن وارد شده وجود داشت، None را برمیگرداند.
4- در متد get_user بررسی میکنیم کاربری با این id وجود دارد یا نه!
app directory/authentication.py
from .models import User # login with phone-number: class PhoneAuthBackend: def authenticate(self, request, username=None, password=None): # username = phoneNumber try: user = User.objects.get(phone_number=username) if user.check_password(password): return user return None except (User.DoesNotExist, User.MultipleObjectsReturned): return None def get_user(self, user_id): try: return User.objects.get(pk=user_id) except User.DoesNotExist: return None # login with Email: class EmailAuthBackend: def authenticate(self, request, username=None, password=None): # username = Email try: user = User.objects.get(email=username) if user.check_password(password): return user return None except (User.DoesNotExist, User.MultipleObjectsReturned): return None def get_user(self, user_id): try: return User.objects.get(pk=user_id) except User.DoesNotExist: return None
معرفی روش های لاگین کردن به پروژه:
یک متغیر بنام AUTHENTICATION_BACKENDS در انتهای تنظیمات اضافه میکنیم؛ حالا روش های لاگین کردن را در لیست، برایش مشخص میکنیم، اولین مورد حالت پیشفرض لاگین با username هست بعد از آن لاگین با ایمیل و شماره تلفن را مشخص میکنیم.
روش های لاگین با ایمیل و شماره تلفن را به صورت زیر به پروژه معرفی میکینم:
# structure: 'app_name.script_name.class_name'
project directory/settings.py
AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.ModelBackend', 'social.authentication.PhoneAuthBackend', 'social.authentication.EmailAuthBackend', ]
ثبت نام و اعتبار سنجی شماره موبایل و یوزرنیم
میخواهیم از ثبت شماره تلفن تکراری در پروژه جلوگیری کنیم.
فرم ثبت نام و ویرایش اطلاعات شخصی را به این پروژه نیز اضافه میکنیم. / url, view, form, template
ایجاد URL های ثبت نام و ادیت اطلاعات کاربری:
app diectory/urls.py
urlpatterns = [ path('register/', views.register, name='register'), path('user/edit/', views.edit_profile, name='edit_profile'), ]
ایجاد فرم برای ثبت نام:
فرم را از مدل User ایجاد میکنیم ولی فیلدهای پسورد و تکرار پسورد را مجزا ایجاد میکنیم:
app directory/forms.py
class RegisterForm(forms.ModelForm): password = forms.CharField(widget=forms.PasswordInput, required=True) password_repeat = forms.CharField(widget=forms.PasswordInput, required=True) class Meta: model = User fields = ('username', 'email', 'phone_number') def clean_password_repeat(self): password = self.cleaned_data.get('password') password_repeat = self.cleaned_data.get('password_repeat') if password != password_repeat: raise forms.ValidationError('Passwords must match') else: return password_repeat # جلوگیری از ثبت نام با شماره تکراری def clean_phone_number(self): phone = self.cleaned_data.get('phone_number') if User.objects.filter(phone_number=phone).exists(): raise forms.ValidationError('Phone number already exists') else: return phone
برای جلوگیری از ثبت شماره تلفن تکراری؛ بررسی میکنیم که آیا کاربری با این شماره تلفن در دیتابیس وجود دارد یا نه !
اگر شماره تلفن وجود داشت که به کاربر یک خطا نمایش میدهد که نمیتوانید با این شماره ثبت نام کنید؛ اگر هم وجود نداشت کاربر میتواند ثبت نام کند.
ایجاد view برای ثبت نام:
app directory/views.py
def register(request): if request.method == 'POST': form = RegisterForm(request.POST) if form.is_valid(): user = form.save(commit=False) user.set_password(form.cleaned_data['password']) user.save() return redirect('social:profile') else: form = RegisterForm() return render(request, 'registration/register.html', {"form": form})
اعتبارسنجی فرم ویرایش اطلاعات شخصی:
در زمان ثبت نام(ایجاد کاربر جدید)، اگه username که وارد میکنیم از قبل وجود داشته باشد، اجازه ثبت نام نمیدهد ولی در فرم ویرایش اطلاعات ایرادی نمیگیرد بنابراین لازم است در کنار اعتبارسنجی شماره تلفن، username را هم اعتبارسنجی کنیم تا از وارد کردن(ثبت) اطلاعات تکراری جلوگیری کند.
فرم ویرایش اطلاعات شخصی
فیلدهای مربوطه را جهت ویرایش مشخص میکنیم.
app directory/forms.py
class EditProfileForm(forms.ModelForm): class Meta: model = User fields = ('profile_image', 'username', 'first_name', 'last_name', 'email', 'phone_number', 'bio', 'job', 'location', 'gender', 'birth_date') def clean_phone_number(self): phone = self.cleaned_data.get('phone_number') if User.objects.exclude(id=self.instance.id).filter(phone_number=phone).exists(): raise forms.ValidationError('Phone number already exists') else: return phone def clean_username(self): username = self.cleaned_data.get('username') if User.objects.exclude(id=self.instance.id).filter(username=username).exists(): raise forms.ValidationError('Username already exists') else: return username
آرگومان instance از طریق view به فرم ارسال میشود و برای استفاده از مقدار آن از دستور self.instance استفاده میکنیم که در اینجا کاربر فعلی در وبسایت میباشد.
اعتبارسنجی شماره تلفن و username شبیه به اعتبارسنجی در فرم ثبت نام هست ولی یه تفاوت مهم دارد.
شماره تلفن و username کاربر در دیتابیس وجود دارد؛ بنابراین حتی اگر کاربر آنها را تغییر ندهد چون مشخص کردیم اگه کاربری با این شماره تلفن و یا username وجود داشت خطا نمایش دهد؛ کاربر را به عنوان مورد تکراری تشخیص داده و به کاربر خطا نشان میدهد.
برای جلوگیری از این مشکل از متد exclude() استفاده کرده و مشخص میکنیم که بجز این کاربر جستجو کن ببین اطلاعات تکراری وجود دارد یا نه!!!
ایجاد view برای فرم ویرایش اطلاعات شخصی:
app directory/views.py
@login_required def edit_profile(request): if request.method == 'POST': form = EditProfileForm(request.POST, files=request.FILES, instance=request.user) if form.is_valid(): form.save() return redirect('social:profile') else: form = EditProfileForm(instance=request.user) return render(request, 'registration/edit_profile.html', {"form": form})
ارسال ایمیل با جنگو (ارسال تیکت با smtp)
ارسال ایمیل از طریق SMTP انجام میشود. / SMTP یک پروتکل میباشد.
عبارت SMTP مخفف: Simple Mail Transfer Protocol میباشد.
میخواهیم تیکتی که کاربر مینویسد را به ایمیل خود ارسال کنیم. / پس form, template, url, view تیکت را ایجاد میکنیم.
ایجاد URL برای تیکت:
app directory/urls.py
urlpatterns = [ path('ticket/', views.ticket, name='ticket'), ]
ایجاد form برای تیکت:
app directory/forms.py
class TicketForm(forms.Form): title = forms.CharField(max_length=100, required=True) name = forms.CharField(max_length=100, required=True) content = forms.CharField(widget=forms.Textarea, required=True) email = forms.CharField(required=True)
انجام تنظیمات مربوطه، برای ارسال ایمیل:
project directory/settings.py
EMAIL_HOST = 'smtp.gmail.com' EMAIL_HOST_USER = 'example@gmail.com' EMAIL_PORT = 587 EMAIL_USE_TLS = True EMAIL_HOST_PASSWORD = '---- ---- ---- ----' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
توضیحات:
1- EMAIL_HOST: مشخص میکنیم از چه سروری، برای خدمات ایمیل استفاده میکنیم؛ در حال حاضر از جیمیل استفاده میکنیم.
2- EMAIL_HOST_USER: آدرس جیمیلی که میخواهیم از آن برای ارسال ایمیل استفاده کنیم. / (فرستنده ایمیل)
3- EMAIL_PORT: مقدار ثابت 587 را برایش مشخص میکنیم.
4- EMAIL_USE_TLS و EMAIL_USE_SSL: برای موارد امنیتی استفاده میشود. / TLS پرکاربردتر میباشد.
5- EMAIL_BACKEND: مشخص میکنیم که از smtp برای ارسال ایمیل استفاده کند.
6- EMAIL_HOST_PASSWORD: این پسورد را در ادامه بدست می آوریم
بدست آوردن پسورد برای EMAIL_HOST_PASSWORD:
-
وارد My_Account حساب جیمیلی که برای EMAIL_HOST_USER مشخص کردیم میشویم.
-
منوی security را انتخاب میکینم.
نکته: تایید دو مرحله ای باید فعال باشه در غیر این صورت پسوردی برای ما، ست نمیکند.
-
عبارت app password را در فیلد search، جستجو میکنیم. / پس از وارد کردن پسورد حساب وارد app password میشود.
-
یک اسم دلخواه برای app password مشخص کرده و اپ خود را ایجاد میکنیم، یک پسورد برای ما ایجاد میکند؛ آنرا کپی کرده و برای EMAIL_HOST_PASSWORD تنظیمش میکینم.
ایجاد view برای تیکت:
app directory/views.py
from django.core.mail import send_mail def ticket(request): sent = False if request.method == 'POST': form = TicketForm(request.POST) if form.is_valid(): cd = form.cleaned_data message = (f"sender: {cd['name']},\n" f"email: {cd['email']}\n\n" f"content: {cd['content']}" ) send_mail( subject=cd['title'], message=message, from_email='sender@gmail.com', recipient_list=['recipient@gmail.com'], fail_silently=False, ) sent = True return redirect("social:index") else: form = TicketForm() return render(request, 'forms/ticket.html', {"form": form, "sent": sent})
برای ارسال ایمیل از پکیج send_mail استفاده میکنیم.
بریم با برخی از آرگومان های send_mail آشنا بشیم:
1- subject: موضوع ایمیل را برایش مشخص میکنیم.
2- message: محتوای متن ایمیل که ارسال میشود.
3- from_email: فرستنده ایمیل میباشد؛ این همان ایمیلی هست که در تنظیمات مشخص کردیم. / نباید ایمیل دیگری وارد شود مگر اینکه تنظیمات را تغییر دهیم.
4- recipient_list: یک لیست از گیرندگان میباشد. / میتوانیم یک یا چند ایمیل برایش مشخص کنیم.
5- fail_silently=False: اگر خطایی وجود داشته باشد آنرا نمایش میدهد.
با استفاده از متغیر sent در تمپلیت، یک شرط میگذاریم که وقتی True بود پیغام "تیکت شما برای پشتیبانی ارسال شد" را نمایش دهد.
تغییر و بازنشانی پسورد با ایمیل
تغییر و بازنشانی پسورد را برای این پروژه نیز پیاده سازی میکنیم.
app directory/urls.py
urlpatterns = [ # password_change path('password-change/', auth_views.PasswordChangeView.as_view(success_url='done'), name='password_change'), path('password-change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'), # password_reset path('password-reset', auth_views.PasswordResetView.as_view(success_url='/password-reset/done'), name='password_reset'), path('password-reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'), path('password-reset/confirm/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(success_url='/password-reset/complete'), name='password_reset_confirm'), path('password-reset/complete/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'), ]
تمپلیت ها را در دایرکتوری registration ایجاد میکنیم.
نکته 1: برای اینکه تمپلیت های ما را نمایش دهد باید در بخش INSTALLED_APPS اپ(app) را در بالا قرار دهیم.
نکته 2: همان طور که قبلا هم گفته شد؛ اگر کاربر ایمیلی ثبت نکرده باشد، ایمیل ارسال نمیشود.
پیاده سازی سیستم تگ برای پست ها
برای ایجاد تگ زیر پست ها؛ بجای اینکه یک مدل برای تگ ها از نوع رابطه Many To Many ایجاد کنیم، از سیستم django-taggit استفاده میکنیم.
برای استفاده از تگ لازم است یک مدل برای پست های خود ایجاد کنیم:
app directory/models.py
class Post(models.Model): author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_posts') description = models.TextField() created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) # ordering & indexing class Meta: ordering = ['-created'] indexes = [ models.Index(fields=['-created']), models.Index(fields=['-total_likes']), ] def __str__(self): return self.description
نمایش مدل Post در پنل ادمین:
app directory/admin.py
@admin.register(Post) class PostAdmin(admin.ModelAdmin): list_display = ['author', 'updated', 'created'] ordering = ['-created', '-author'] search_fields = ['author', 'description']
ایجاد URL برای لیست پست ها:
app directory/urls.py
urlpatterns = [ path('posts-list/', views.post_list, name='post_list'), ]
ایجاد view برای لیست پست ها:
app directory/views.py
def post_list(request): posts = Post.objects.all() return render(request, 'social/post_list.html', {'posts': posts})
برای استفاده از taggit باید آنرا نصب کنیم:
pip install django-taggit
پس از نصب باید آنرا به پروژه معرفی کنیم:
project directory/settings.py
INSTALLED_APPS = [ # ... 'taggit', ]
برای ایجاد فیلد tags در مدل Post باید TaggableManager را ایمپورت کنیم.
app directory/models.py
from taggit.managers import TaggableManager
حالا از آن استفاده کرده و فیلد tags را در مدل Post ایجاد میکنیم:
app directory/models.py
from taggit.managers import TaggableManager class Post(models.Model): author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_posts') description = models.TextField() created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) tags = TaggableManager() # ordering & indexing class Meta: ordering = ['-created'] indexes = [ models.Index(fields=['-created']), models.Index(fields=['-total_likes']), ] def __str__(self): return self.description
با استفاده از متدهای remove(), add(), all() میتوانیم، تگ اضافه و یا حذف کنیم یا اینکه تمام تگ ها را نمایش دهیم.
post.tags.add("holidays")
post.tags.remove("Halloween")
post.tags.all()
ایجاد تمپلیت برای لیست پست ها:
templates/social/post_list.html
{% for post in posts %} {{ post.description | truncatewords:20 | linebreaks }} <br> published at {{ post.created }} by {{ post.author }} <br> {{ post.tags.all | join:', ' }} {% endfor %}
برای نمایش تمام تگ ها، فیلد tags را برای post صدا زده و با متد all() همه تگ ها را نمایش میدهیم.
نمایش تمام پست های یک تگ
میخواهیم کاری شبیه به ساختار دسته بندی (category) که در فصل های قبل اجرا کردیم را پیاده سازی کنیم.
- یک url دیگه برای لیست پست ها ایجاد میکنیم؛ این URL متغیر tag_slug را برای view ارسال میکند.
app directory/urls.py
urlpatterns = [ path('posts-list/', views.post_list, name='post_list'), path('posts-list/<slug:tag_slug>', views.post_list, name='post_list_tags'), ]
نکته: رابطه بین تگ و پست از نوع Many To Many میباشد.
کلاس Tag باید ایمپورت شود.
- برای view لیست پست ها، یک آرگومان اختیاری بنام tag_slug با مقدار None مشخص میکنیم. / تا زمانیکه تگ ارسال شد بتوانیم با استفاده از آن، پست های مربوط به آن تگ را نمایش دهیم.
app directory/views.py
from taggit.models import Tag def post_list(request, tag_slug=None): tag = None posts = Post.objects.all() if tag_slug: tag = get_object_or_404(Tag, slug=tag_slug) posts = Post.objects.filter(tags__in=[tag]) return render(request, 'social/post_list.html', {'posts': posts, 'tag': tag})
چون حالا در تمپلیت لیست پست ها؛ پست ها به دو صورت نمایش داده میشوند، تغییراتی در تمپلیت ایجاد میکنیم:
templates/social/post_list.html
{% if tag %} <h2>posts tagged with {{ tag.name }}</h2> {% endif %} {% for post in posts %} {{ post.description | truncatewords:20 | linebreaks }} published at {{ post.created }} by {{ post.author }} <br> {% for tag in post.tags.all %} <a href="{% url 'social:post_list_tags' tag.slug %}">{{ tag.name }}</a> {% if not forloop.last %}, {% endif %} {% endfor %} {% endfor %}
ذخیره تگ ها هنگام ایجاد پست
میخواهیم فرمی برای ایجاد پست نوشته و برای پست تگ ها هم ذخیره کنیم.
ایجاد URL برای افزودن پست:
app directory/urls.py
urlpatterns = [ path('create-post/', views.create_post, name='create_post'), ]
ایجاد form برای افزودن پست:
app directory/forms.py
class CreatePostForm(forms.ModelForm): class Meta: model = Post fields = ('description', 'tags')
ایجاد view برای افزودن پست:
app directory/views.py
@login_required def create_post(request): if request.method == 'POST': form = CreatePostForm(request.POST) if form.is_valid(): post = form.save(commit=False) post.author = request.user post.save() form.save_m2m() return redirect('social:post_list') else: form = CreatePostForm() return render(request, 'forms/create_post.html', {'form': form})
همان طور که گفته شد رابطه بین Tag و Post از نوع Many To Many میباشد.
زمانی که میخواهیم یک فیلد m2m را ذخیره کنیم، چنانچه از آرگومان "commit=False" برای متد save() استفاده کرده باشیم؛ پس از ذخیره اطلاعات در دیتابیس، باید از متد save_m2m() برای آن فرم استفاده کنیم.
نمایش مشابه ترین پست ها با کمک تگ
خب بریم برای این پروژه هم جزئیات پست را اضافه کنیم.
ایجاد URL برای post_detail:
app directory/urls.py
urlpatterns = [ path('posts/detail/<int:post_id>', views.post_detail, name='post_detail'), ]
ایجاد لینک برای پست ها در تمپلیت post_list:
templates/social/post_list.html
{% if tag %} <h2>posts tagged with {{ tag.name }}</h2> {% endif %} {% for post in posts %} <!-- لینک انتقال به صفحه جزئیات پست --> <a href="{% url 'social:post_detail' post.id %}"> {{ post.description | truncatewords:20 | linebreaks }} </a> published at {{ post.created }} by {{ post.author }} <br> {% for tag in post.tags.all %} <a href="{% url 'social:post_list_tags' tag.slug %}">{{ tag.name }}</a> {% if not forloop.last %}, {% endif %} {% endfor %} {% endfor %}
میخواهیم در تمپلیت post_detail چند پست مشابه را نمایش دهیم؛ (پست هایی که تعداد تگ مشابه بیشتری دارند.)
ایجاد view برای post_detail:
app directory/views.py
def post_detail(request, post_id): post = get_object_or_404(Post, pk=pk) post_tags_ids = post.tags.values_list('id', flat=True) similar_posts = Post.objects.filter(tags__in=post_tags_ids) similar_posts = similar_posts.annotate(same_tags=Count('tags')).exclude(id=pk).order_by('-same_tags', '-created')[:3] context = { 'post': post, 'similar_posts': similar_posts } return render(request, 'social/post_detail.html', context=context)
توضیحات:
1- با استفاده از id ارسال شده توسط url، آن پست را در صورت وجود، از دیتابیس دریافت میکنیم.
2- id تگ های پست را به کمک متد values_list() دریافت میکنیم. / اگه از flat استفاده نکنیم در خروجی هر id به صورت تاپل خواهد بود که استفاده ازش کمی سخت میشود.
3- با استفاده از field lookups(فیلد لوکاپ) tags__in، تمام پست هایی که حداقل یک تگ مشابه، با تگ های پست فعلی دارند را دریافت کرده و در یک متغیر ذخیره میکنیم.
برخی از پست ها ممکن است چند تگ مشابه داشته باشند، در نتیجه برخی پست ها به صورت تکراری در این کوئری ست وجود خواهند داشت.
4- حالا روی کوئری ستی که در مرحله قبل بدست آوردیم، از annotate(same_tags=Count('tags')) استفاده میکنیم تا تعداد تگ های مشترک برای هر پست را محاسبه کنیم، و این مقدار را به عنوان same_tags به پست های کوئری ست اضافه میکنیم.
از متد exclude() استفاده میکنیم تا پست فعلی را جز پست های مشابه انتخاب نکند.
تابع Count('tags') در اینجا حساب میکند هر پست، چند بار در کوئری ست تکرار شده است؛ و چون هر پست به تعداد تگ های مشابهی که دارد تکرار میشود، در نتیجه عددی که نشان میدهد تعداد تگ های مشابه میباشد.
- با استفاده از متد order_by()، نتایج را براساس تعداد تگ های مشابه مرتب سازی میکنیم و چنانچه تعداد تگ مشابه یکسان بود، براساس تاریخ ایجاد مرتب میکنیم.
حالا پست هایی که تعداد تگ مشابه بیشتری دارند به این پست شبیه تر خواهند بود.
نمایش پست های مشابه در تمپلیت post_detail:
templates/social/post_detail.html
{{ post.description | truncatewords:20 | linebreaks }} published at {{ post.created }} by {{ post.author }} <br> {% for tag in post.tags.all %} <a href="{% url 'social:post_list_tags' tag.slug %}">{{ tag.name }}</a> {% if not forloop.last %}, {% endif %} {% endfor %} <h2>Similar Posts</h2> {% for post in similar_posts %} <p> <a href="{% url 'social:post_detail' post.id %}"> {{ post.description | truncatewords:10 | linebreaks }} </a> </p> {% empty %} There are no similar posts! {% endfor %}
تمرینات فصل هشتم (مهم)
T1- پیاده سازی قابلیت جستجو(براساس توضیحات و تگ های پست):
همانند پروژه قبلی، تمپلیت پایه(base.html) را برای این پروژه نیز ایجاد میکنیم.
باید برای جستجو یک فرم ایجاد کنیم و آنرا در تمپلیت base.html قرار دهیم تا در همه صفحات دیده شود.
ایجاد فرم برای جستجو:
app directory/forms.py
class SearchForm(forms.Form): query = forms.CharField(max_length=250)
ایجاد url برای جستجو:
app directory/urls.py
urlpatterns = [ path('search/', views.post_search, name='post_search'), ]
ایجاد view برای جستجو:
app directory/views.py
def post_search(request): query = None results = [] if 'query' in request.GET: form = SearchForm(request.GET) if form.is_valid(): query = form.cleaned_data['query'] results = Post.objects.annotate( similarity=TrigramSimilarity('tags', query) + TrigramSimilarity('description', query)).filter(similarity__gt=0.18).order_by('-similarity') context = { "query": query, "results": results, } return render(request, 'blog/search_result.html', context=context)
برای استفاده از قابلیت های postgres لازمه توی settings.py و بخش INSTALLED_APPS آنرا معرفی کنیم.
project directory/settings.py
INSTALLED_APPS = [ # ... 'django.contrib.postgres', ]
T2- پیاده سازی قابلیت کامنت گذاشتن برای پست ها:
برای قابلیت کامنت گذاری لازم است، model, url, view, form ایجاد کنیم:
ایجاد مدل برای کامنت گذاشتن:
app directory/models.py
class Comment(models.Model): post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments') name = models.CharField(max_length=250) content = models.TextField() # date created = models.DateTimeField(auto_now_add=True) # status for show in template active = models.BooleanField(default=False) class Meta: ordering = ['-created'] indexes = [models.Index(fields=['-created'])] def __str__(self): return f'Written by: {self.name}\n{self.post} Post'
ایجاد فرم برای کامنت:
app directory/forms.py
class CommentForm(forms.ModelForm): class Meta: model = Comment fields = ['name', 'content']
ایجاد URL برای کامنت:
app directory/urls.py
urlpatterns = [ path('post-detail/<int:post_id>/comment/', views.post_comment, name='post_comment'), ]
ایجاد view برای کامنت:
app directory/views.py
def post_comment(request, post_id): post = get_object_or_404(Post, id=post_id) comment = None if request.method == 'POST': form = CommentForm(request.POST) if form.is_valid(): comment = form.save(commit=False) comment.post = post comment.save() else: form = CommentForm() context = { 'comment': comment, 'post': post, 'form': form } return render(request, "forms/comment.html", context=context)
T3- صفحه بندی برای پست ها:
ایجاد ساختار تمپلیت صفحه بندی:
templates/partials/pagination.html
{% load static %} <link rel="stylesheet" href="{% static 'css/pagination.css' %}"> <nav class="my-5" aria-label="navigation"> <ul class="pagination d-inline-block d-md-flex justify-content-center"> {% if page.has_previous %} <li class="page-item"> <a class="page-link1" href="?page={{ page.previous_page_number }}" tabindex="-1" aria-disabled="true">«</a> </li> {% if page.number > 3 %} <li class="page-item"><a class="page-link" href="?page=1">1</a></li> {% if page.number > 4 %} <li class="page-item disabled"><a class="page-link" href="#">...</a></li> {% endif %} {% endif %} {% endif %} {% for num in page.paginator.page_range %} {% if page.number == num %} <li class="page-item"><a class="page-link active" href="?page={{ num }}">{{ num }}</a></li> {% elif num > page.number|add:'-3' and num < page.number|add:'3' %} <li class="page-item"><a class="page-link" href="?page={{ num }}">{{ num }}</a></li> {% endif %} {% endfor %} {% if page.has_next %} {% if page.number < page.paginator.num_pages|add:'-3' %} <li class="page-item disabled"><a class="page-link" href="#">...</a></li> <li class="page-item"><a class="page-link" href="?page={{ page.paginator.num_pages }}">{{ page.paginator.num_pages }}</a></li> {% elif page.number < page.paginator.num_pages|add:'-2' %} <li class="page-item"><a class="page-link" href="?page={{ page.paginator.num_pages }}">{{ page.paginator.num_pages }}</a></li> {% endif %} <li class="page-item"> <a class="page-link1" href="?page={{ page.next_page_number }}">»</a> </li> {% endif %} </ul> </nav>
افزودن ساختار pagination به تمپلیت post_list:
templates/social/post_list.html
{% if tag %} <h2>posts tagged with {{ tag.name }}</h2> {% endif %} {% for post in posts %} <!-- لینک انتقال به صفحه جزئیات پست --> <a href="{% url 'social:post_detail' post.id %}"> {{ post.description | truncatewords:20 | linebreaks }} </a> published at {{ post.created }} by {{ post.author }} <br> {% for tag in post.tags.all %} <a href="{% url 'social:post_list_tags' tag.slug %}">{{ tag.name }}</a> {% if not forloop.last %}, {% endif %} {% endfor %} {% endfor %} <!-- ساختار صفحه بندی --> {% include 'partials/pagination.html' with page=posts %}
تغییر view برای لیست پست ها:
app directory/views.py
from taggit.models import Tag from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger def post_list(request, tag_slug=None): tag = None posts = Post.objects.all() if tag_slug: tag = get_object_or_404(Tag, slug=tag_slug) posts = Post.objects.filter(tags__in=[tag]) # صفحه بندی برای پست ها paginator = Paginator(posts, 10) page_number = request.GET.get('page', 1) try: posts = paginator.page(page_number) except EmptyPage: posts = paginator.page(paginator.num_pages) except PageNotAnInteger: posts = paginator.page(1) return render(request, 'social/post_list.html', {'posts': posts, 'tag': tag})
همان طور که قبلا هم گفته شد؛ متغیر posts که در try, except ایجاد شده یک آبجکت از صفحه میباشد که اطلاعات صفحه و پست ها را در خود دارد.
T4- پیاده سازی قابلیت ویرایش و حذف برای پست ها:
ویرایش اطلاعات پست:
برای ویرایش پست از فرم و تمپلیت ایجاد پست استفاده میکنیم.
ایجاد URL برای ویرایش پست:
app directory/urls.py
urlpatterns = [ path('profile/edit-post/<post_id>', views.edit_post, name='edit_post'), ]
ایجاد view برای ویرایش پست:
app directory/views.py
from django.contrib.auth.decorators import login_required @login_required def edit_post(request, post_id): post = get_object_or_404(Post, pk=post_id, author=request.user) if request.method == 'POST': form = CreatePostForm(request.POST, instance=post) if form.is_valid(): post = form.save(commit=False) post.author = request.user post.save() return redirect('social:index') else: form = CreatePostForm(instance=post) return render(request, 'forms/create_post.html', {'post': post, 'form': form})
برای پستی که دریافت میکنیم، مشخص میکنیم که نویسنده(author) آن، کاربر فعلی باشد؛ چون هر کاربر فقط میتواند پست های خود را ویرایش کند نه پست های دیگری را.
حذف پست:
ایجاد url برای حذف پست ها:
app directory/urls.py
urlpatterns = [ path('profile/delete-post/<post_id>', views.delete_post, name='delete_post'), ]
برای هر پست یک دکمه حذف ایجاد میکنیم:
templates/social/post_list.html
{% if tag %} <h2>posts tagged with {{ tag.name }}</h2> {% endif %} {% for post in posts %} <!-- لینک انتقال به صفحه جزئیات پست --> <a href="{% url 'social:post_detail' post.id %}"> {{ post.description | truncatewords:20 | linebreaks }} </a> published at {{ post.created }} by {{ post.author }} <br> {% for tag in post.tags.all %} <a href="{% url 'social:post_list_tags' tag.slug %}">{{ tag.name }}</a> {% if not forloop.last %}, {% endif %} {% endfor %} <a href="{% url 'social:delete_post' post.id %}">delete post</a> {% endfor %} <!-- ساختار صفحه بندی --> {% include 'partials/pagination.html' with page=posts %}
ایجاد view برای حذف پست ها:
app directory/views.py
def delete_post(request, post_id): post = get_object_or_404(Post, pk=post_id) if request.method == 'POST': post.delete() return redirect('social:post_list') return render(request, 'forms/delete-post.html', {'post': post})
ایجاد template برای حذف پست ها:
templates/forms/delete_post.html
<h2>آیا از حذف پست مطمئن هستید؟!</h2> <br> <a href="{% url 'social:post_list' %}">بازگشت به صفحه لیست پست ها</a> <form method="post"> {% csrf_token %} <input type="submit" value="حذف"> </form>
T5- قابلیت افزودن تصویر برای پست ها:
ایجاد مدل Images برای ذخیره تصاویر:
app directory/models.py
class Image(models.Model): post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='images', null=True, blank=True) image = ResizedImageField(upload_to='post_images/', size=[125, 125], scale=1, crop=['middle', 'center'], null=True, blank=True) title = models.CharField(max_length=250, null=True, blank=True) description = models.TextField(null=True, blank=True) created = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-created'] indexes = [models.Index(fields=['-created'])] def __str__(self): return f"Title: {self.title}" if self.title else f"Image_name: {self.image}"
تغییر فرم ایجاد پست برای افزودن تصویر به آن:
app directory/forms.py
class CreatePostForm(forms.ModelForm): img1 = forms.ImageField(label='pic1', required=False) img2 = forms.ImageField(label='pic2', required=False) class Meta: model = Post fields = ('description', 'tags')
تغییر view برای ایجاد پست:
app directory/views.py
@login_required def create_post(request): if request.method == 'POST': form = CreatePostForm(request.POST) if form.is_valid(): post = form.save(commit=False) post.author = request.user post.save() form.save_m2m() # اسم فیلدهای تصویر در فرم all_images = ['img1', 'img2'] for img in all_images: img_file = form.cleaned_data.get(img) if img_file: Image.objects.create(image=img_file, post=post) return redirect('social:post_list') else: form = CreatePostForm() return render(request, 'forms/create_post.html', {'form': form})