From 58e0d3671102ccc7f6d5e24410e41f234f0a132a Mon Sep 17 00:00:00 2001 From: Marcin Kwiatkowski Date: Mon, 6 Oct 2025 07:29:45 +0200 Subject: [PATCH 01/16] feat: added canonical url for post --- .../Testtrack/M1 - Django Learning/mysite/blog/models.py | 7 +++++++ .../mysite/blog/templates/blog/post/list.html | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py index 9d8e590..023a3e7 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.urls import reverse from django.utils import timezone from django.conf import settings @@ -39,3 +40,9 @@ class Meta: def __str__(self): return self.title + + def get_absolute_url(self): + return reverse( + 'blog:post_detail', + args=[self.id] + ) diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html index 8f182df..16e5803 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html @@ -6,7 +6,7 @@

Blog Posts

{% for post in posts %}

- + {{ post.title }}

From 403e10100b7ae0a2d4b197654ce7add25f23d2ca Mon Sep 17 00:00:00 2001 From: Marcin Kwiatkowski Date: Mon, 6 Oct 2025 07:40:32 +0200 Subject: [PATCH 02/16] feat: add friendly URLs to Post --- .../blog/migrations/0002_alter_post_slug.py | 19 ++++++++++++++++++ .../mysite/blog/models.py | 12 +++++++++-- .../M1 - Django Learning/mysite/blog/urls.py | 6 +++++- .../M1 - Django Learning/mysite/blog/views.py | 9 ++++++--- .../M1 - Django Learning/mysite/db.sqlite3 | Bin 147456 -> 151552 bytes 5 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/blog/migrations/0002_alter_post_slug.py diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/migrations/0002_alter_post_slug.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/migrations/0002_alter_post_slug.py new file mode 100644 index 0000000..2f3c474 --- /dev/null +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/migrations/0002_alter_post_slug.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.7 on 2025-10-06 05:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='post', + name='slug', + field=models.SlugField( + max_length=250, unique_for_date='published_at'), + ), + ] diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py index 023a3e7..728b42d 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py @@ -16,7 +16,10 @@ class Status(models.TextChoices): PUBLISHED = 'PB', 'Published' title = models.CharField(max_length=250) - slug = models.SlugField(max_length=250) + slug = models.SlugField( + max_length=250, + unique_for_date='published_at' + ) author = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, @@ -44,5 +47,10 @@ def __str__(self): def get_absolute_url(self): return reverse( 'blog:post_detail', - args=[self.id] + args=[ + self.published_at.year, + self.published_at.month, + self.published_at.day, + self.slug + ] ) diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py index d560190..853f1e6 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py @@ -5,5 +5,9 @@ urlpatterns = [ path('', views.post_list, name='post_list'), - path('/', views.post_detail, name='post_detail') + path( + '////', + views.post_detail, + name='post_detail' + ) ] diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py index 45eb8dc..3d1694a 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py @@ -11,11 +11,14 @@ def post_list(request): ) -def post_detail(request, id): +def post_detail(request, year, month, day, post): post = get_object_or_404( Post, - id=id, - status=Post.Status.PUBLISHED + status=Post.Status.PUBLISHED, + slug=post, + published_at__year=year, + published_at__month=month, + published_at__day=day ) return render( diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/db.sqlite3 b/Projects/Testtrack/M1 - Django Learning/mysite/db.sqlite3 index 221ef97b4960e3126712b1a68c20b9ed723f5938..cbcd301ef1619ed7430ccbc8d58e8844d6dc2ec6 100644 GIT binary patch delta 703 zcmY+BO=uHA7>4(oNh!OnnMBebsWh9mOIUE5*_GUPS6O74@V_ z!c#Rhbg%I$15Xen8y9Eccl+dulx**jWw_l|$Gy)=82{!HoQ4x*@Fb}x7A&hxm@Wa7m znp_$qIylq~Bye&S)BElLeLel+`3T;Wo6d0mq6eKrIvMYGH2VK Tc!{kObh%@`St!e1$)5WIw%Nt; delta 367 zcmZozz}e8iIYC-bhk=1X1&Cq5ZlaDcqt3>JCH&07-13vz1=KeyDtzSHEHC|ng`a`{ zC4U0{ZvHaAwl9``ZEZk(U&45wefb$n`9nVh&2Ij!Yh5}vNcO@{+U}7}d9-Pehk9~ST zCZptbFDJ%+M&?E))9HV57!4U!rpxCsN=$dlWwc(e_+Z>pB__p`(F{Lq1 zKkLuLC1hz7;KT(C1ZMto4E*Q#Kl0z-ELd=WUr>Nql`%0dza%5INVg!rxP*E7Ie8|< uElllvn;itQ8D&_QIT@Kj1QRneCnv;&O$_{-_%HIm1e&mffBGhSCO-hl=4jRc From 1f02607989e10ac28fbbc0de7ff205019ac0b9af Mon Sep 17 00:00:00 2001 From: Marcin Kwiatkowski Date: Mon, 6 Oct 2025 08:01:51 +0200 Subject: [PATCH 03/16] feat: added pagination --- .../mysite/blog/templates/blog/post/list.html | 1 + .../mysite/blog/templates/pagination.html | 13 +++++++++++++ .../M1 - Django Learning/mysite/blog/views.py | 7 ++++++- .../M1 - Django Learning/mysite/db.sqlite3 | Bin 151552 -> 151552 bytes 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/pagination.html diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html index 16e5803..4082642 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html @@ -17,4 +17,5 @@

{% empty %}

No posts available.

{% endfor %} + {% include "pagination.html" with page=posts %} {% endblock %} diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/pagination.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/pagination.html new file mode 100644 index 0000000..08de57e --- /dev/null +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/pagination.html @@ -0,0 +1,13 @@ + diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py index 3d1694a..b7897fa 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py @@ -1,9 +1,14 @@ +from django.core.paginator import Paginator from django.shortcuts import get_object_or_404, render from .models import Post def post_list(request): - posts = Post.published.all() + post_list = Post.published.all() + paginator = Paginator(post_list, 3) + page_number = request.GET.get('page', 1) + posts = paginator.get_page(page_number) + return render( request, 'blog/post/list.html', diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/db.sqlite3 b/Projects/Testtrack/M1 - Django Learning/mysite/db.sqlite3 index cbcd301ef1619ed7430ccbc8d58e8844d6dc2ec6..bb8f934ee9c0ae7f7f1ddd4396a27c16cd259ce2 100644 GIT binary patch delta 685 zcmZ{hziSg=7{}lDe$@-fJqbllNVM0K(m}5G-p|B9kZWBO2?2)=B3MmoB9a8}5G9mW zI*1Nc>2T1g{so$LxUskhUFy)e{vb;_l!71*EyVXyI@H?bJG}4nd|p1^cX3TzTod1= zvGzVQjkV7Giy9VIo@qA(PG(sXuY8UVFYRdIj)wJCQh;3qhwvK?U=O;myOC;bL7ZnX z9ufx02YVlJn$C3*9Kj#>1^dv0E}i=dF^(0n5CtFZZ(=!_6eJhHCbVH%>Pc@UcVkph zrG?vqh@dE5#iDDKU--3VL#cbpgR1XWpDKQBwxQg*bGMzHtyHR&Y(Z(y&)*v|h+(Tb zQHi4vyI?y7%bv{TEyFfvcuE_`Gn8h!jiz6tkXF<48H#vFVULa>*|~c4NyVeQApIT7 zsS^${CJD(IIVbFW!0S^oR zO9uYm{9pOs^S|6ISa6en`pfl<(py=Wn)x<62&6G?f4iSiT!4p#?*#+@e*S8HXTBGk z6%}sqZGUl(QAUxAng0_5{~!ME{GWiLm3-UV`Iyoe6-^Zk4Xjw0IT@Kj1QRneCnpyJ z0|PVvIR>CT5BSdkZ92d|{hU0LF)vtr69fN4{*(NhfZ|K|r*G0{(wCECc4Ra%Ff!FO zG|)8w8fa)`WMO4ure|ScX>4S){l7jFs{j`>UpoVTCqD;YJJ@>mdS)&LRdvVdQ~j6> Qr=R9yvfcj2kBNm50IrZsIRF3v From 4d578c5766809a9b61dfd0945466b3d8c542f342 Mon Sep 17 00:00:00 2001 From: Marcin Kwiatkowski Date: Mon, 6 Oct 2025 08:14:00 +0200 Subject: [PATCH 04/16] refactor: refactor views to use classes --- .../mysite/blog/templates/blog/post/list.html | 2 +- .../M1 - Django Learning/mysite/blog/urls.py | 4 +- .../M1 - Django Learning/mysite/blog/views.py | 47 ++++++++----------- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html index 4082642..6c90493 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html @@ -17,5 +17,5 @@

{% empty %}

No posts available.

{% endfor %} - {% include "pagination.html" with page=posts %} + {% include "pagination.html" with page=page_obj %} {% endblock %} diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py index 853f1e6..49ea4d5 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py @@ -4,10 +4,10 @@ app_name = 'blog' urlpatterns = [ - path('', views.post_list, name='post_list'), + path('', views.PostListView.as_view(), name='post_list'), path( '////', - views.post_detail, + views.PostDetailView.as_view(), name='post_detail' ) ] diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py index b7897fa..a77fe62 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py @@ -1,33 +1,26 @@ -from django.core.paginator import Paginator -from django.shortcuts import get_object_or_404, render +from django.shortcuts import get_object_or_404 +from django.views.generic import ListView, DetailView from .models import Post -def post_list(request): - post_list = Post.published.all() - paginator = Paginator(post_list, 3) - page_number = request.GET.get('page', 1) - posts = paginator.get_page(page_number) +class PostListView(ListView): + queryset = Post.published.all() + context_object_name = 'posts' + paginate_by = 3 + template_name = 'blog/post/list.html' - return render( - request, - 'blog/post/list.html', - {'posts': posts} - ) +class PostDetailView(DetailView): + model = Post + template_name = 'blog/post/detail.html' + context_object_name = 'post' -def post_detail(request, year, month, day, post): - post = get_object_or_404( - Post, - status=Post.Status.PUBLISHED, - slug=post, - published_at__year=year, - published_at__month=month, - published_at__day=day - ) - - return render( - request, - 'blog/post/detail.html', - {'post': post} - ) + def get_object(self, queryset=None): + return get_object_or_404( + Post, + status=Post.Status.PUBLISHED, + slug=self.kwargs['post'], + published_at__year=self.kwargs['year'], + published_at__month=self.kwargs['month'], + published_at__day=self.kwargs['day'] + ) From 404e71e94531a1254c2063dfab3abad2a382856d Mon Sep 17 00:00:00 2001 From: Marcin Kwiatkowski Date: Tue, 7 Oct 2025 06:55:42 +0200 Subject: [PATCH 05/16] feat: added sharing via email form --- .gitignore | 1 + .../M1 - Django Learning/mysite/.env.example | 3 ++ .../M1 - Django Learning/mysite/.gitignore | 1 + .../M1 - Django Learning/mysite/blog/forms.py | 11 ++++++ .../blog/templates/blog/post/detail.html | 3 ++ .../blog/templates/blog/post/share.html | 20 ++++++++++ .../M1 - Django Learning/mysite/blog/urls.py | 3 +- .../M1 - Django Learning/mysite/blog/views.py | 39 ++++++++++++++++++- .../mysite/mysite/settings.py | 9 +++++ .../mysite/requirements.txt | 1 + 10 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/.env.example create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/.gitignore create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/blog/forms.py create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/share.html create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/requirements.txt diff --git a/.gitignore b/.gitignore index 97daa84..4f59cf1 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,7 @@ venv/ env/ ENV/ .env/ +.env # Testing .coverage diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/.env.example b/Projects/Testtrack/M1 - Django Learning/mysite/.env.example new file mode 100644 index 0000000..42b9fcf --- /dev/null +++ b/Projects/Testtrack/M1 - Django Learning/mysite/.env.example @@ -0,0 +1,3 @@ +EMAIL_HOST_USER= +EMAIL_HOST_PASSWORD= +DEFAULT_FROM_EMAIL= diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/.gitignore b/Projects/Testtrack/M1 - Django Learning/mysite/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/Projects/Testtrack/M1 - Django Learning/mysite/.gitignore @@ -0,0 +1 @@ +.env diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/forms.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/forms.py new file mode 100644 index 0000000..a1c23a9 --- /dev/null +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/forms.py @@ -0,0 +1,11 @@ +from django import forms + + +class EmailPostForm(forms.Form): + name = forms.CharField(max_length=25) + email = forms.EmailField() + to = forms.EmailField() + comments = forms.CharField( + required=False, + widget=forms.Textarea + ) diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html index 73f1030..112be94 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html @@ -8,4 +8,7 @@

{{ post.title }}

Published on {{ post.published_at }} by {{post.author}}

{{ post.body|linebreaks }}

+

+ Share via email +

{% endblock %} diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/share.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/share.html new file mode 100644 index 0000000..d85103b --- /dev/null +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/share.html @@ -0,0 +1,20 @@ +{% extends "blog/base.html" %} + +{% block title %}Share Post{% endblock %} + +{% block content %} + +{% if sent %} +

Message has been sent.

+

+ Sending post {{ post.title }} to {{ recipient_email }} was successful. +

+{% else %} +

Share Post: {{ post.title }} via email

+
+ {{ form.as_p }} + {% csrf_token %} + +
+{% endif %} +{% endblock %} diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py index 49ea4d5..c91dd74 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py @@ -9,5 +9,6 @@ '////', views.PostDetailView.as_view(), name='post_detail' - ) + ), + path('/share/', views.post_share, name='post_share') ] diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py index a77fe62..714e687 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py @@ -1,6 +1,8 @@ -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, render from django.views.generic import ListView, DetailView +from django.core.mail import send_mail from .models import Post +from .forms import EmailPostForm class PostListView(ListView): @@ -24,3 +26,38 @@ def get_object(self, queryset=None): published_at__month=self.kwargs['month'], published_at__day=self.kwargs['day'] ) + + +def post_share(request, post_id): + post = get_object_or_404( + Post, + id=post_id, + status=Post.Status.PUBLISHED + ) + sent = False + + if request.method == 'POST': + form = EmailPostForm(request.POST) + if form.is_valid(): + cd = form.cleaned_data + post_url = request.build_absolute_uri(post.get_absolute_url()) + subject = f"{cd['name']} recommends you read " f"{post.title}" + message = ( + f"Read {post.title} at {post_url}\n\n" + f"comments: {cd['name']}: {cd['comments']}" + ) + + send_mail( + subject, + message, + from_email=None, + recipient_list=[cd['to']] + ) + sent = True + else: + form = EmailPostForm() + + return render( + request, + 'blog/post/share.html', + {'post': post, 'form': form, 'sent': sent}) diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/mysite/settings.py b/Projects/Testtrack/M1 - Django Learning/mysite/mysite/settings.py index aec6ed2..65c129c 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/mysite/settings.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/mysite/settings.py @@ -11,6 +11,7 @@ """ from pathlib import Path +from decouple import config # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -121,3 +122,11 @@ # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_HOST_USER = config('EMAIL_HOST_USER') +EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD') +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL') diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/requirements.txt b/Projects/Testtrack/M1 - Django Learning/mysite/requirements.txt new file mode 100644 index 0000000..1d7168a --- /dev/null +++ b/Projects/Testtrack/M1 - Django Learning/mysite/requirements.txt @@ -0,0 +1 @@ +python-decouple>=3.8 From 4316650b693dbc28c14fd5cdeb64fa09acb76910 Mon Sep 17 00:00:00 2001 From: Marcin Kwiatkowski Date: Tue, 7 Oct 2025 08:43:06 +0200 Subject: [PATCH 06/16] feat: added comments --- .../M1 - Django Learning/mysite/blog/admin.py | 9 ++++- .../M1 - Django Learning/mysite/blog/forms.py | 7 ++++ .../mysite/blog/migrations/0003_comment.py | 33 ++++++++++++++++++ .../mysite/blog/models.py | 23 ++++++++++++ .../blog/templates/blog/post/comment.html | 12 +++++++ .../blog/templates/blog/post/detail.html | 16 +++++++++ .../blog/post/includes/comment_form.html | 14 ++++++++ .../M1 - Django Learning/mysite/blog/urls.py | 7 +++- .../M1 - Django Learning/mysite/blog/views.py | 33 +++++++++++++++++- .../M1 - Django Learning/mysite/db.sqlite3 | Bin 151552 -> 159744 bytes 10 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/blog/migrations/0003_comment.py create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/comment.html create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/includes/comment_form.html diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/admin.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/admin.py index a6157cd..095f9e4 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/admin.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Post +from .models import Post, Comment @admin.register(Post) @@ -12,3 +12,10 @@ class PostAdmin(admin.ModelAdmin): date_hierarchy = 'published_at' ordering = ('status', 'published_at') show_facets = admin.ShowFacets.ALWAYS + + +@admin.register(Comment) +class CommentAdmin(admin.ModelAdmin): + list_display = ('name', 'email', 'post', 'created_at', 'active') + list_filter = ('active', 'created_at', 'updated_at') + search_fields = ('name', 'email', 'body') diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/forms.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/forms.py index a1c23a9..b98117e 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/forms.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/forms.py @@ -1,4 +1,5 @@ from django import forms +from .models import Comment class EmailPostForm(forms.Form): @@ -9,3 +10,9 @@ class EmailPostForm(forms.Form): required=False, widget=forms.Textarea ) + + +class CommentForm(forms.ModelForm): + class Meta: + model = Comment + fields = ('name', 'email', 'body') diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/migrations/0003_comment.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/migrations/0003_comment.py new file mode 100644 index 0000000..ab10940 --- /dev/null +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/migrations/0003_comment.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.7 on 2025-10-06 13:05 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0002_alter_post_slug'), + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, + primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=80)), + ('email', models.EmailField(max_length=254)), + ('body', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('active', models.BooleanField(default=True)), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='comments', to='blog.post')), + ], + options={ + 'ordering': ('created_at',), + 'indexes': [models.Index(fields=['created_at'], name='blog_commen_created_4e025c_idx')], + }, + ), + ] diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py index 728b42d..82e8a07 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py @@ -54,3 +54,26 @@ def get_absolute_url(self): self.slug ] ) + + +class Comment(models.Model): + post = models.ForeignKey( + Post, + on_delete=models.CASCADE, + related_name='comments' + ) + name = models.CharField(max_length=80) + email = models.EmailField() + body = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + active = models.BooleanField(default=True) + + class Meta: + ordering = ('created_at',) + indexes = [ + models.Index(fields=['created_at']), + ] + + def __str__(self): + return f'Comment by {self.name} on {self.post}' diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/comment.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/comment.html new file mode 100644 index 0000000..00c9b7f --- /dev/null +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/comment.html @@ -0,0 +1,12 @@ +{% extends "blog/base.html" %} + +{% block title %}Add a comment{% endblock %} + +{% block content %} + {% if comment %} +

Your comment has been added.

+

Back to post

+ {% else %} + {% include "blog/post/includes/comment_form.html" %} + {% endif %} +{% endblock %} diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html index 112be94..3a99069 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html @@ -11,4 +11,20 @@

{{ post.title }}

Share via email

+ {% with comments.count as total_comments %} +

+ {{ total_comments }} Comment{{ total_comments|pluralize }} +

+ {% endwith %} + {% for comment in comments %} +
+

+ Comment {{ forloop.counter }} by {{ comment.name }} on {{ comment.created_at }} +

+ {{ comment.body|linebreaks }} +
+ {% empty %} +

No comments yet.

+ {% endfor %} + {% include "blog/post/includes/comment_form.html" %} {% endblock %} diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/includes/comment_form.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/includes/comment_form.html new file mode 100644 index 0000000..ed3e169 --- /dev/null +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/includes/comment_form.html @@ -0,0 +1,14 @@ +

Add new comment

+
+
+ {{ form.name.as_field_group }} +
+
+ {{ form.email.as_field_group }} +
+ {{ form.body.as_field_group }} + {% csrf_token %} +

+ +

+
diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py index c91dd74..440616a 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py @@ -10,5 +10,10 @@ views.PostDetailView.as_view(), name='post_detail' ), - path('/share/', views.post_share, name='post_share') + path('/share/', views.post_share, name='post_share'), + path( + '/comment/', + views.post_comment, + name='post_comment' + ) ] diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py index 714e687..c905598 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py @@ -1,8 +1,9 @@ from django.shortcuts import get_object_or_404, render from django.views.generic import ListView, DetailView +from django.views.decorators.http import require_POST from django.core.mail import send_mail from .models import Post -from .forms import EmailPostForm +from .forms import EmailPostForm, CommentForm class PostListView(ListView): @@ -27,6 +28,16 @@ def get_object(self, queryset=None): published_at__day=self.kwargs['day'] ) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + post = self.get_object() + + context['comments'] = post.comments.filter(active=True) + + context['form'] = CommentForm() + + return context + def post_share(request, post_id): post = get_object_or_404( @@ -61,3 +72,23 @@ def post_share(request, post_id): request, 'blog/post/share.html', {'post': post, 'form': form, 'sent': sent}) + + +@require_POST +def post_comment(request, post_id): + post = get_object_or_404( + Post, + id=post_id, + status=Post.Status.PUBLISHED + ) + comment = None + form = CommentForm(data=request.POST) + if form.is_valid(): + comment = form.save(commit=False) + comment.post = post + comment.save() + return render( + request, + 'blog/post/comment.html', + {'post': post, 'form': form, 'comment': comment} + ) diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/db.sqlite3 b/Projects/Testtrack/M1 - Django Learning/mysite/db.sqlite3 index bb8f934ee9c0ae7f7f1ddd4396a27c16cd259ce2..817fc69c091f5fecc40044a7bb6d4bbb1af577d6 100644 GIT binary patch delta 1896 zcmaJ?Z%i9y7{9l7*gH!5w3EU%0=(QTtl{7F52dXpqt=EkW8fSH1Cny|4ygS{=m{Fe z02edkvP{xsHGXj)h<-6KrXj?rnZ_?>`=I+|Fft4#W(KE=#y^Rg_+J0$wF8^nle>F= z&+qqp-#*WCE7!S|Z@AYQEWH#(wUMjz>tE||q2Bi<)`7NGZ)EglyRLd8=P+Nac63{- zD61_n8Ba|Mg5U~AQi+6|RGfmdr_&*H3Lflm`Gp?8%iZO2`vj*m_ubK^s#+EX8TcM1 z-~*V4%kT!g1B1C1?14jOBW*(}U5Sd$Omti8*oPUq9f{8?GvQfzE)h+qqp2j$T<$)3 zf+aRBoHiw`(9>2#cO&UJF*%(IC!*7HqCyhV)@lM~Ax^=Ma0|YIk072i3!`8$GuDQ@ zY;gl@X6eKw8XE>O`aU0P_}K8eVb(A}EHZ{=9#>Jw*2&IC<%N>VpqRw!GcM-0HWQ59 z!Af#mR%9)z@aA7!Qvny5Ax_J1g*PP}ZX~#4?W`zC^;$OaT!Q3wPxB0ojaM@ICw;8( zrg6h~-FSsq4jG3IG029>4C7h`*-?eR=EBQzGffr-9Yd;eNw_ih_4x5Q8bc_f|H8+@ z2%Lq}5F%24n+{je2(dUHoAa?L9~*PyJ8!ShRw|a&PoUPSiw25{t*!Z%SF0Y=*PpVX zReLlk$uDUB$8cm$78O|vyJb>Nk#JPft}1Q8r$T|@5I!~dMCcjJ|9>KnhX%_q3XhNT zM1adHhzf7t{AB`#b;!SYE`qM8W zv_f2ld%S||^T<;gOG@j3ZXe83&5l3lwn)$0UrPJ+^)?%N*`7D2mC@cmQj{DHJU$RA zr^^Itg2SDBSsPI7Tkmn;Z1lI&O~aXH3v6r4dLhzmG^g_(sX zU&Lp`xkxmL$!Iw#$KxqHlA4Q4y0JyR&=L~w$BX<_RE|q&ay!Nc3MV$cv^36^&pemU z?-sgT9#@Yb4C_@fuKhnTP|DS^w|3Cch6)Oy*Y9+9`Fu{N!?PK;PVzB&t50V(sq>zO`)Z;62KPyM?LvMYF|=BWjE<%`u7FvmU>81xb%MDD b&oS)rt@Zp5S*R8Z delta 647 zcmZp8z}c{XbAq&>J_7@TDiEsxF(VMWOw=)E)Zdt}gr8Z2>)B*>0nN>d3fs6gKa-MU z6=CO3VB~+vpTNJHzl^_|e-{55{wtdm4LtZKC+Lfcu?e#xn3tX(pPQLplvo1PP%Od) zRLss_%fSDR|1|$C{=NLQ{0sQIHwzY|^G~j|Z)cX_el(fg!4l}MMckVoc`LCn%kbWr z%pL#|nZvvJPOL@|vkZUiWcCXn5n2A}AL5x$<*_>lxjKfpDug&X`M4@<|D3=W&%|i9JvfV*xh) zyA1rl`M>hN2L|_J{<}cWpM>~h`or~%%0T(Y4E%rizw>|Mf6M=Z|1nVhJU=H3GbbY( zh?xB5zJw4ve+>gr_9p)+{yqHj`MdaQfU45?r`Mck>=0n%b7bJ(&tJ{&%=dz?pU)8( zP`Z5E9q%#j599*|KMW`{Fl^^DV7kjUIY8jJpB%FzqmhA;sji`cu7Rn7p`n$Lg_Vh! po`r>_v5^t8II}T|w1JhWv6ZPQlNhrxC$cu6xRI5q;W7s%0RSB=zEJ=G From 784300d4b4145cd9d42213f882e3f017bdd6e255 Mon Sep 17 00:00:00 2001 From: Marcin Kwiatkowski Date: Tue, 7 Oct 2025 19:20:25 +0200 Subject: [PATCH 07/16] feat: added tags --- .../mysite/blog/migrations/0004_post_tags.py | 21 ++++++++++++++++++ .../mysite/blog/models.py | 2 ++ .../mysite/blog/templates/blog/post/list.html | 9 ++++++++ .../M1 - Django Learning/mysite/blog/urls.py | 5 +++++ .../M1 - Django Learning/mysite/blog/views.py | 20 ++++++++++++++++- .../M1 - Django Learning/mysite/db.sqlite3 | Bin 159744 -> 212992 bytes .../mysite/mysite/settings.py | 3 ++- .../mysite/requirements.txt | 1 + 8 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/blog/migrations/0004_post_tags.py diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/migrations/0004_post_tags.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/migrations/0004_post_tags.py new file mode 100644 index 0000000..f212111 --- /dev/null +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/migrations/0004_post_tags.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.7 on 2025-10-07 11:52 + +import taggit.managers +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0003_comment'), + ('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'), + ] + + operations = [ + migrations.AddField( + model_name='post', + name='tags', + field=taggit.managers.TaggableManager( + help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + ] diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py index 82e8a07..441ae16 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/models.py @@ -2,6 +2,7 @@ from django.urls import reverse from django.utils import timezone from django.conf import settings +from taggit.managers import TaggableManager class PublishedManager(models.Manager): @@ -34,6 +35,7 @@ class Status(models.TextChoices): objects = models.Manager() published = PublishedManager() + tags = TaggableManager() class Meta: ordering = ('-published_at',) diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html index 6c90493..3bbc51a 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html @@ -4,12 +4,21 @@ {% block content %}

Blog Posts

+ {% if tag %} +

Posts tagged with "{{ tag.name }}"

+ {% endif %} {% for post in posts %}

{{ post.title }}

+

+ Tags: + {% for tag in post.tags.all %} + {{ tag.name }}{% if not forloop.last %}, {% endif %} + {% endfor %} +

Published on {{ post.published_at }} by {{post.author}}

diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py index 440616a..059c9c8 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/urls.py @@ -5,6 +5,11 @@ urlpatterns = [ path('', views.PostListView.as_view(), name='post_list'), + path( + 'tag//', + views.PostListView.as_view(), + name='post_list_by_tag' + ), path( '////', views.PostDetailView.as_view(), diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py index c905598..91a5ed7 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py @@ -2,16 +2,34 @@ from django.views.generic import ListView, DetailView from django.views.decorators.http import require_POST from django.core.mail import send_mail +from taggit.models import Tag from .models import Post from .forms import EmailPostForm, CommentForm class PostListView(ListView): - queryset = Post.published.all() context_object_name = 'posts' paginate_by = 3 template_name = 'blog/post/list.html' + def get_queryset(self): + queryset = Post.published.all() + tag_slug = self.kwargs.get('tag_slug', None) + + if tag_slug: + tag = get_object_or_404(Tag, slug=tag_slug) + queryset = queryset.filter(tags__in=[tag]) + self.tag = tag + else: + self.tag = None + + return queryset + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['tag'] = self.tag + return context + class PostDetailView(DetailView): model = Post diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/db.sqlite3 b/Projects/Testtrack/M1 - Django Learning/mysite/db.sqlite3 index 817fc69c091f5fecc40044a7bb6d4bbb1af577d6..3eb53d46988c1bba6aeebc1998fcfb0963ac72a9 100644 GIT binary patch delta 3602 zcmai1eQaCR6@S<7*)Q)o_OYR^>m+S$Cu!_Bsr~#B$0hx6LmE<=k3s`XqI$7o*SK}; zHgSM;11GIiiB?FhEcVbQjc)&JNJG;UO<}OAh<}E*Ngz!_W74)VCc0^`R_!R`V`JyN z=a1(&DYD*u_nrGY=l;&U=iGC>bd6hDGF@!9j#3mA#-|q_8lOim^@VJs)Xwvp^!TNN zJJ?cv@CDrYS@8^5@xv})W3!vE?Pz(e%vyYA{tSOV_h0TMF46LF%WErf%juSq(4nVn z-gq)s$OHm`uz0etP!x-a%t9~_jPwhE{y@|z2)iP|UE$cyP-HL`4bHb4D93Vl98sBU z5w!-y`E))pmzMQ1=~T9uo)c#a`C>YcroMSHEf$i;)3Ze}n^F*2WrBFIU{0Xu~-mMAs7e;!*VCGu68IIiH06`p!T=w+F>z~E2ihg zVrc`$mErm*y0*S~Ary#>Y(ewmb@L(ZN+eP#aWS7gwU`#O`Bb{m$H%HGi^f9(L-s}| zgHf)I%hfqHlH2B1$E&1z6B!v$rIW~P zaUr0+ldG`MOuusI!B-jD(|WtiPQ@*zk|}RGYML;`OfD1GdK-7;d#xoKLwscIT6*jZ z@ss9d>rv%ZDz)_5v56sGvSuB+%?#N}YCd;a7~&!tH#=oPJK^fq+$M&gpyG&Y<@uve z&C?#RmDzXV7T60h2thyefD78ehV-wPrJJIT>Ek_mnh?H6X?G}Xx7>1FO53TlZl!gt zjQr*53~i?0lGuG@J3~6C=cL|!L|lD2OZJe}U&YC;ZBaS!nMsP7glY4eraLBID{buO z&vCc7D=ilGPi&Xrx?zeQBfle4)LR(z7pO<5H>gK0#kc7~d&{pII~;O2s0XDOtoM6E zd*spfxSFG8vst`u7a^j^Vz5!9M9NUo~!#5=pXMI-elF zkd~4pwED&@d4)*dm?aT@+3I8ME*DvzD+i1Y>D?qbwK|t3r}cziA=l`@kC@BExrK)V z6nqTt!LQ+$@DiMdZ^H?A91h@u_Coh2o}nBrm6Y77^}xiT^xm3u^~Brch08y@=Dak; zayncsFFu|X>(UJ4rp(?3nm zwti+jYoz$6*^_L);iCS1{W$Y7ca}&S2}xYgtf#4252ZkRSi60qp9*OTiG~#q?M`?7u9B&;igUMuePEU*- zo*wCQx)*Yb8R=4%*sC({?s0?tuutBnh*lrf#o!u27?SH9SOld4_KvViE zO*WU>Mp^qb^4q-uzuE$e>0m0F5>m&Si21&bShI~9rOwJ(RmpaFmuqOZH-5l?4gHZNPpi? zwo2bGkWJEy88Rg2sC2PF+Gn^Fb(NXY-Gnxp zmg}faImqQZ9}OQ-@HyOuj}ZT7=rb^cBP^}Hbb9>aSou#34ewBJ6aI>?o%q?0j}J9o zg*_G;Yl}m@QL1gLj-hO}cBY{|1dC_#e$1+F= z*`)XX8gBIU+Xu;(n$4giz?IIzfh*GbN2QBsk)yFOSts@$sWZhnmGAayF1addb88P7 zR^KB6MmhY732fsTtmAEbfKkno*nw))KLoN`_a6ugde&kZ&lQ#U@nSwdQ7BE7Sj7&k zA5xiQK`F-bX=O55N*4=Lc2PeTFh}qQzQK<;k2yS#O{{B{Q)8&+_TFM;b42BX`}8+B zvlcn{gkxoCTICOSNLbRQkDZ-lWeltQdz}RSbkXHF!)YU*+6fln1?7X#%P}#yK{p2> zPOpxDt+6c7cm(?B%zpUQilu;ITCYN|bKO>UnAk~{h(o6P!h&Emelyw(c}8Q8vd@_^ z)R++Y*`-L|r37498utW@j+low60n9E6zHW33iOK= zixZ9|EAcW4RVLK;&A=~I@1>9nFz{fj09=Noz z)qN5k0$Xu!P?y+H>um-G3{E}abohL5*=3.8 +django-taggit>=6.1.0 From 833a30160b3110564ebe807eba23367c52948820 Mon Sep 17 00:00:00 2001 From: Marcin Kwiatkowski Date: Tue, 7 Oct 2025 19:31:26 +0200 Subject: [PATCH 08/16] feat: added similar posts --- .../blog/templates/blog/post/detail.html | 10 ++++++++++ .../M1 - Django Learning/mysite/blog/views.py | 9 +++++++++ .../M1 - Django Learning/mysite/db.sqlite3 | Bin 212992 -> 212992 bytes 3 files changed, 19 insertions(+) diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html index 3a99069..ffd940d 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html @@ -11,6 +11,16 @@

{{ post.title }}

Share via email

+

Similar posts

+ {% for post in similar_posts %} +

+ + {{ post.title }} + +

+ {% empty %} +

No similar posts found.

+ {% endfor %} {% with comments.count as total_comments %}

{{ total_comments }} Comment{{ total_comments|pluralize }} diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py index 91a5ed7..c95647f 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/views.py @@ -1,3 +1,4 @@ +from django.db.models import Count from django.shortcuts import get_object_or_404, render from django.views.generic import ListView, DetailView from django.views.decorators.http import require_POST @@ -54,6 +55,14 @@ def get_context_data(self, **kwargs): context['form'] = CommentForm() + post_tags_ids = post.tags.values_list('id', flat=True) + similar_posts = Post.published.filter( + tags__in=post_tags_ids).exclude(id=post.id) + similar_posts = similar_posts.annotate(same_tags=Count( + 'tags')).order_by('-same_tags', '-published_at')[:4] + + context['similar_posts'] = similar_posts + return context diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/db.sqlite3 b/Projects/Testtrack/M1 - Django Learning/mysite/db.sqlite3 index 3eb53d46988c1bba6aeebc1998fcfb0963ac72a9..ea15298592e12b64c29a0f4e49f445f3a0e6ad04 100644 GIT binary patch delta 598 zcmaKp%WD%+6vpqpGs9$t%zSAOjQD623rbC9Cc`u&JMF3qAE4r@sH-k52whZK0=nwL z2QDt;KX4Un7PBlUMO0`Fq)4HSNM#afq@m)gy6{YDi*DT9dpP{Q?;O5UsaTbY_3$ts zdvs)!k3GHdBG0wQr=0WLygTKDQx3mBnK7}*@DpFKg^$?4I$mQDZ_%g^Og@3^T^DLX zH#LQG-H?qM_erqK@C)CtjZf&1q=i*9u}p$GOjR*>|1rYV1wNe0n8_uE1>C`?-LW6o zXRS?Z)*4DK)sJWAlZmUQ!7#)xOWESXXRh3wxP4-LW@cP3xP>6+xjDC(^@`;}saz=L zT{rLp|C&LH)1>INc<$tX6kd>bJ>M^mgtP1X-Z}dvQ&DB8(#~%%PutExRvqEU_l`K2 zaLa*T4gwK&9Ki<%{u%4Jr9kdn5V!W*u{SkKBjNN#F?Y$<5bb5sMMU0Bite4zbS+L! zsNr@^q*H`#{FOCfjopN0yT*_1nQS7$SM>TO{Uo(GQ#JNGK+GD$4nCuc4_G7S6;is& z={kzG`_r)QmdM1znhf9RtPvfxN$4}AX&=V9q3F8WFN7{5D94ChVfcX_+H}noVxJ?O R(1?tE{}g209#jrxe*;*~s?Y!c delta 332 zcmXZWu}cC`00!{)-W_-Z@4mAfj3R9df}TlBV>vig{s%R+=HybyK@HJRNyCGi`VXWW z9!=5|Z8?3LTbj<$?8Vm20yWOU5s3qlnH zJx>_~iYjX|dSLwVHvv`r9 zid|&vu6<&!@K;`}%qF+FDmLiBs@) Date: Tue, 7 Oct 2025 19:38:52 +0200 Subject: [PATCH 09/16] feat: added total_posts template tag --- .../mysite/blog/templates/blog/base.html | 7 ++++--- .../mysite/blog/templatetags/__init__.py | 0 .../mysite/blog/templatetags/blog_tags.py | 9 +++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/__init__.py create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html index 2e4bba1..a90ceea 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html @@ -1,3 +1,4 @@ +{% load blog_tags %} {% load static %} @@ -9,11 +10,11 @@
- {% block content %}{% endblock %} + {% block content %}{% endblock %}
diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/__init__.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py new file mode 100644 index 0000000..e5cc20d --- /dev/null +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py @@ -0,0 +1,9 @@ +from django import template +from ..models import Post + +register = template.Library() + + +@register.simple_tag +def total_posts(): + return Post.published.count() From ebde27ba2570335e698f0bb21f588a12e41c1d9f Mon Sep 17 00:00:00 2001 From: Marcin Kwiatkowski Date: Tue, 7 Oct 2025 20:14:55 +0200 Subject: [PATCH 10/16] feat: added latest posts in sidebar --- .../mysite/blog/templates/blog/base.html | 2 ++ .../mysite/blog/templates/blog/post/latest_posts.html | 9 +++++++++ .../mysite/blog/templatetags/blog_tags.py | 6 ++++++ 3 files changed, 17 insertions(+) create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/latest_posts.html diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html index a90ceea..c8360d6 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html @@ -15,6 +15,8 @@ diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/latest_posts.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/latest_posts.html new file mode 100644 index 0000000..15060c6 --- /dev/null +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/latest_posts.html @@ -0,0 +1,9 @@ +
    + {% for post in latest_posts %} +
  • + {{ post.title }} - {{ post.published_at }} +
  • + {% empty %} +
  • No recent posts available.
  • + {% endfor %} +
diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py index e5cc20d..24ebb74 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py @@ -7,3 +7,9 @@ @register.simple_tag def total_posts(): return Post.published.count() + + +@register.inclusion_tag('blog/post/latest_posts.html') +def show_latest_posts(count=5): + latest_posts = Post.published.order_by('-published_at')[:count] + return {'latest_posts': latest_posts} From 61ede883282d3cbc67d84fadb5bd56113bec4b61 Mon Sep 17 00:00:00 2001 From: Marcin Kwiatkowski Date: Tue, 7 Oct 2025 20:19:07 +0200 Subject: [PATCH 11/16] feat: added most commented posts --- .../mysite/blog/templatetags/blog_tags.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py index 24ebb74..3fed0d0 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py @@ -1,4 +1,5 @@ from django import template +from django.db.models import Count from ..models import Post register = template.Library() @@ -13,3 +14,10 @@ def total_posts(): def show_latest_posts(count=5): latest_posts = Post.published.order_by('-published_at')[:count] return {'latest_posts': latest_posts} + + +@register.simple_tag() +def get_most_commented_posts(count=5): + return Post.published.annotate( + total_comments=Count('comments') + ).order_by('-total_comments')[:count] From ce677232f5c6454a775dc80a969c5ef8de5546de Mon Sep 17 00:00:00 2001 From: Marcin Kwiatkowski Date: Tue, 7 Oct 2025 20:29:16 +0200 Subject: [PATCH 12/16] feat: added markdown support --- .../mysite/blog/templates/blog/base.html | 10 ++++++++++ .../blog/templates/blog/post/detail.html | 3 ++- .../mysite/blog/templates/blog/post/list.html | 3 ++- .../mysite/blog/templatetags/blog_tags.py | 7 +++++++ .../M1 - Django Learning/mysite/db.sqlite3 | Bin 212992 -> 212992 bytes .../mysite/requirements.txt | 1 + 6 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html index c8360d6..353606e 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html @@ -17,6 +17,16 @@

My blog

Hello dude! I published {% total_posts %} posts so far.

Latest posts

{% show_latest_posts 3 %} +

Most commented posts

+ {% get_most_commented_posts as most_commented_posts %} +
    + {% for post in most_commented_posts %} +
  • + {{ post.title }} ({{ post.comments.count }}) +
  • + {% empty %} +
  • No posts available.
  • + {% endfor %} diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html index ffd940d..188c0ae 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/detail.html @@ -1,4 +1,5 @@ {% extends "blog/base.html" %} +{% load blog_tags %} {% block title %}{{ post.title }}{% endblock %} @@ -7,7 +8,7 @@

    {{ post.title }}

    Published on {{ post.published_at }} by {{post.author}}

    - {{ post.body|linebreaks }}

    + {{ post.body|markdown }}

    Share via email

    diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html index 3bbc51a..cc95c82 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/post/list.html @@ -1,4 +1,5 @@ {% extends "blog/base.html" %} +{% load blog_tags %} {% block title %}Blog Posts{% endblock %} @@ -22,7 +23,7 @@

    Published on {{ post.published_at }} by {{post.author}}

    - {{ post.body|truncatewords:30|linebreaks }}

    + {{ post.body|markdown|truncatewords_html:30 }}

    {% empty %}

    No posts available.

    {% endfor %} diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py index 3fed0d0..7eecdd5 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templatetags/blog_tags.py @@ -1,3 +1,5 @@ +import markdown +from django.utils.safestring import mark_safe from django import template from django.db.models import Count from ..models import Post @@ -21,3 +23,8 @@ def get_most_commented_posts(count=5): return Post.published.annotate( total_comments=Count('comments') ).order_by('-total_comments')[:count] + + +@register.filter(name='markdown') +def markdown_format(text): + return mark_safe(markdown.markdown(text)) diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/db.sqlite3 b/Projects/Testtrack/M1 - Django Learning/mysite/db.sqlite3 index ea15298592e12b64c29a0f4e49f445f3a0e6ad04..1a85d9bfb19218b82057262bbfdc8c462ff5d9ff 100644 GIT binary patch delta 389 zcmZo@;B9E&ogmHVHBrWy(Q9LZs6HcibD943GJVFr0x53({S5rS`M>hN=YPrnnEx*S zY5t@9`}wc)U)(HMu#kWHhj=CyCU)-WN7pmj0~McQ;Qzz_o&OX6TmBb7<@fk+@?YUU z&wmQ2cnv=*3o|Dp_xAJq8BGPGxOplVfCg3bJM+EZ>*sUiea&0PtIBhZr((0BLKF{A z2sbAKzcOPmBd4R`^!axg)l{7FQ!15WYincKjSP%Tbqx)44a^k`Ev$?ztPD-{EG*4T z4NSM6yTfSW%(sbwg?|x0B~?|Q~&?~ delta 242 zcmZo@;B9E&ogmHVIZ?)$(Q{*hs6HcCbD943GJVFr0tqhuqYV7N`M>hN=YPrnnEx*S zY5t@9*ZD7Q7A#oFKm9{I6U+3I>lv+q>drIp|Kb16|B3%C{|lhXd;B-~ukfD-s@u%J z{rrALQvnGso^uTR`}wQ+o%vqy_47IMzUHmtRpmLiSy5pV&-Qb77)_k{PBO6Y&tl-8 z1vKLV|0baBF8=AW^qC~&#F&jajSP%Tbqx)44NMgb4XjL!tV|6V7#P@r!rL$CGZ_l- o19>dG{~7o@`8oLdc>e=3.8 django-taggit>=6.1.0 +markdown>=3.9.0 From 1a32811286fc7c9f86a12356d5dd5184c84752a0 Mon Sep 17 00:00:00 2001 From: Marcin Kwiatkowski Date: Tue, 7 Oct 2025 20:45:31 +0200 Subject: [PATCH 13/16] feat: addesitemap --- .../mysite/blog/sitemaps.py | 13 +++++++++++++ .../M1 - Django Learning/mysite/db.sqlite3 | Bin 212992 -> 212992 bytes .../mysite/mysite/settings.py | 3 +++ .../mysite/mysite/urls.py | 8 ++++++++ 4 files changed, 24 insertions(+) create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/blog/sitemaps.py diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/sitemaps.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/sitemaps.py new file mode 100644 index 0000000..dbbd97a --- /dev/null +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/sitemaps.py @@ -0,0 +1,13 @@ +from django.contrib.sitemaps import Sitemap +from .models import Post + + +class PostSitemap(Sitemap): + changefreq = 'weekly' + priority = 0.9 + + def items(self): + return Post.published.all() + + def lastmod(self, obj): + return obj.updated_at diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/db.sqlite3 b/Projects/Testtrack/M1 - Django Learning/mysite/db.sqlite3 index 1a85d9bfb19218b82057262bbfdc8c462ff5d9ff..ab0463460b01dd61d35a016ede078bb8a911ccca 100644 GIT binary patch delta 1280 zcmaJ=O>7%g5Z<@D_IkbRoopd>$)BBC5n2BMt1Zt6>C!_`$VFhT@1Gy9_1r!MtmE-^_MTizr0SBnW*cOS+fx{ck zy!mFnnfGSOWkG2oG4~XdQnx?P+H5C^uy^)VM@#9(=++(3p0914MhS$HK0b_!B{*TiVvzghN7XJ zk)Ya*(S40*<6T#~+>F>6j17e=pKMEV9RkQ4`~(y5DI9^5@D_XkPk;>lFkIo;=jyRz zg95}TxCLLpw{Q`1Fbn69BL?HJ7qfQ&NUXD+)H|#R!PxuIMaIvcH@Jt#Y<3%ega4dA zU+Hy^@~?LJEfjIxFC5A0FQ#kY5iReo8Sa|l_Mz9~6Eb>EFXlVf@E*M;D%qIRIel1=W7qGOHI07tjkZB zRNB{XU+xlNk%GVAcest;_&t1wkG%{_@C|$gixqkLO{gcJ1<_o(K=vSfnS#IJPxuXf zftwhx2G?K}uEG^qM)><+VHuu?=9MK9$Ki-l2)l;P&)_V)0k6RGZ~#Ujir;r1fJ0y@ zXZyzgnWI<9r7ltAC@kP*7?EyDr=*1AXU8!|pSU6(6*=taN1?MI+9^=T93gxnm@6FA za+8H(DIUWqH&0zBoe%7;&$v6UP3hbH?(Ms?#p#^(Qo|mfogEii=YDW79t`hLqv4^b zT0MH5oG_|#nf?RjQ}#2bEf!d#U=4d)g^-)Dl%lj!wpz8RC2$L;mQqmd$GA1vA~Oo# ziu?E+P1>x?acT>_VsZ;~ua#b5hneHJyYqBpZr6U!IX8?V5FoDw=EA6XJgAklXLI_$ zi-d#Qq$+0W5ZTJ$rI8Syv^MtKT(8hQoWeg%Ej|@6=O@UveR*7fbW>b9_}?2Z!Y|bM zT>rz@-^>@W*Z3kSt8<^HPSe@K&}4YSkC{tZ(q%qcrR~+XKca82cI$pCz7<79lev>DKzZJBXl7 zc;z(}0u2_>z~Cbu;{`^LKsN?x!ag(@QdEZ4ux-TT7OFIUdzkilVlk>hQ_DN*yj z`omi!K%E5!KkyZw@d4A+`x=v&z!(bDIS9RwCtK(o>KQ6eqJaCjhVwXygJ?rN?dpME zKSoB&OT`RB6HcJcHf_6L3s`5YJ=XQ&Q!zuk?GPj%$D7Zol|xGHd|@t))ki#wtrzyo$U(p4!@uDcB`@diFh)8EV3~0KXX^q zc;~0l%8kY#cv@V{eA{MB{4f`Qhct2I#D6m+d!tm@x}*sOVLv)K`r_5LVvZeFa? zmyc{(Le$WkEDy_sQIhTPOu8$fsJ Date: Tue, 7 Oct 2025 20:54:38 +0200 Subject: [PATCH 14/16] feat: added rss feed --- .../M1 - Django Learning/mysite/blog/feeds.py | 23 +++++++++++++++++++ .../mysite/blog/templates/blog/base.html | 3 +++ .../M1 - Django Learning/mysite/blog/urls.py | 4 +++- 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 Projects/Testtrack/M1 - Django Learning/mysite/blog/feeds.py diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/feeds.py b/Projects/Testtrack/M1 - Django Learning/mysite/blog/feeds.py new file mode 100644 index 0000000..ba0971f --- /dev/null +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/feeds.py @@ -0,0 +1,23 @@ +import markdown +from django.contrib.syndication.views import Feed +from django.template.defaultfilters import truncatewords_html +from django.urls import reverse_lazy +from .models import Post + + +class LatestPostsFeed(Feed): + title = "My Blog" + link = reverse_lazy('blog:post_list') + description = "Updates on the latest blog posts." + + def items(self): + return Post.published.all()[:5] + + def item_title(self, item): + return item.title + + def item_description(self, item): + return truncatewords_html(markdown.markdown(item.body), 30) + + def item_pubdate(self, item): + return item.published_at diff --git a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html index 353606e..ebc5f98 100644 --- a/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html +++ b/Projects/Testtrack/M1 - Django Learning/mysite/blog/templates/blog/base.html @@ -15,6 +15,9 @@