diff --git a/en/authentication_authorization/README.md b/en/authentication_authorization/README.md index cd29f51..c128f96 100644 --- a/en/authentication_authorization/README.md +++ b/en/authentication_authorization/README.md @@ -8,12 +8,14 @@ First let's make things secure. We will protect our `post_new`, `post_edit`, `po So edit your `blog/views.py` and add these lines at the top along with the rest of the imports: +{% filename %}blog/views.py{% endfilename %} ```python from django.contrib.auth.decorators import login_required ``` Then add a line before each of the `post_new`, `post_edit`, `post_draft_list`, `post_remove` and `post_publish` views (decorating them) like the following: +{% filename %}blog/views.py{% endfilename %} ```python @login_required def post_new(request): @@ -37,6 +39,7 @@ We could now try to do lots of magical stuff to implement users and passwords an In your `mysite/urls.py` add a url `path('accounts/login/', views.LoginView.as_view(), name='login')`. So the file should now look similar to this: +{% filename %}mysite/urls.py{% endfilename %} ```python from django.urls import path, include from django.contrib import admin @@ -52,6 +55,7 @@ urlpatterns = [ Then we need a template for the login page, so create a directory `blog/templates/registration` and a file inside named `login.html`: +{% filename %}blog/templates/registration/login.html{% endfilename %} ```django {% extends "blog/base.html" %} @@ -83,6 +87,7 @@ You will see that this also makes use of our _base_ template for the overall loo The nice thing here is that this _just worksTM_. We don't have to deal with handling of the form submission nor with passwords and securing them. Only more thing is left to do. We should add a setting to `mysite/settings.py`: +{% filename %}mysite/settings.py{% endfilename %} ```python LOGIN_REDIRECT_URL = '/' ``` @@ -93,22 +98,27 @@ so that when the login page is accessed directly, it will redirect a successful We already set things up so that only authorized users (i.e. us) see the buttons for adding and editing posts. Now we want to make sure a login button appears for everybody else. -We will add a login button that looks like this: +We will add a login button using an unlock icon. + +Download the unlock image from [https://icons.getbootstrap.com/assets/icons/unlock.svg](https://icons.getbootstrap.com/assets/icons/unlock.svg) and save it in the folder `blog/templates/registration/icons/` + +We will add the login button like this ```django - + {% include 'registration/icons/unlock.svg' %} ``` -For this we need to edit the templates, so let's open up `blog/templates/blog/base.html` and change it so the part between the `` tags looks like this: +We want to ensure the button is only visible to non-authenticated users, so let's open up `base.html` and change it so the part between the `` tags looks like this: +{% filename %}blog/templates/blog/base.html{% endfilename %} ```django @@ -123,20 +133,21 @@ For this we need to edit the templates, so let's open up `blog/templates/blog/ba ``` -You might recognize the pattern here. There is an if-condition in the template that checks for authenticated users to show the add and edit buttons. Otherwise it shows a login button. +You might recognize the pattern here. There is an if-condition in the template that checks for authenticated users to show the add and edit buttons. {% raw %}`{% else %}`{% endraw %} it shows a login button. ## More on authenticated users -Let's add some sugar to our templates while we're at it. First we will add some details to show when we are logged in. Edit `blog/templates/blog/base.html` like this: +Let's add some sugar to our templates while we're at it. First we will add some details to show when we are logged in. Edit `base.html` like this: +{% filename %}blog/templates/blog/base.html{% endfilename %} ```django @@ -148,6 +159,7 @@ We decided to rely on Django to handle login, so let's see if Django can also ha Done reading? By now you may be thinking about adding a URL in `mysite/urls.py` pointing to Django's logout view (i.e. `django.contrib.auth.views.logout`), like this: +{% filename %}mysite/urls.py{% endfilename %} ```python from django.urls import path, include from django.contrib import admin @@ -162,6 +174,30 @@ urlpatterns = [ ] ``` +## Is there anything missing? + +We made sure non-logged in users can't access the `post_draft_list` view by using the `@login_required` decorator. We also decorated the `post_edit`, `post_remove` and `post_publish` views so they can't make changes. But could a non-logged in user still see a draft post? + +While logged out, try navigating to a draft post by editing the url address bar directly. Whoops! We can still see the draft post! + +This is because we use the `post_detail()` view method for both published and draft posts. We need to protect this view as well. We definitely want non-logged in users to see published posts, so using the decorator won't work. + +Instead, let's go to the `blog/views.py` file and update the `post_detail()` view to check for a `published_date` or whether a `user` `is_authenticated`. If neither condition is true, we will redirect the user to login. + +{% filename %}blog/views.py{% endfilename %} +``` +def post_detail(request, pk): + post = get_object_or_404(Post, pk=pk) + if post.published_date or request.user.is_authenticated: + return render(request, 'blog/post_detail.html', {'post': post}) + else: + return redirect("/accounts/login/") +``` + +Check again if you can navigate directly to a draft post using the address bar. + + + That's it! If you followed all of the above up to this point (and did the homework), you now have a blog where you - need a username and password to log in, diff --git a/en/homework/README.md b/en/homework/README.md index 227346b..ac14df2 100644 --- a/en/homework/README.md +++ b/en/homework/README.md @@ -6,6 +6,7 @@ Our blog has come a long way but there's still room for improvement. Next, we wi Currently when we're creating new posts using our *New post* form the post is published directly. To instead save the post as a draft, **remove** this line in `blog/views.py` in the `post_new` and `post_edit` methods: +{% filename %}blog/views.py{% endfilename %} ```python post.published_date = timezone.now() ``` @@ -18,20 +19,23 @@ Remember the chapter about querysets? We created a view `post_list` that display Time to do something similar, but for draft posts. -Let's add a link in `blog/templates/blog/base.html` in the header. We don't want to show our list of drafts to everybody, so we'll put it inside the {% raw %}`{% if user.is_authenticated %}`{% endraw %} check, right after the button for adding new posts. +Let's add a link to the header.of our `base.html` template. We don't want to show our list of drafts to everybody, so we'll put it inside the {% raw %}`{% if user.is_authenticated %}`{% endraw %} check, right after the button for adding new posts. +{% filename %}blog/templates/blog/base.html{% endfilename %} ```django - + {% include './icons/pencil-fill.svg' %} ``` -Next: urls! In `blog/urls.py` we add: +Next we define the url path! +{% filename %}blog/urls.py{% endfilename %} ```python path('drafts/', views.post_draft_list, name='post_draft_list'), ``` -Time to create a view in `blog/views.py`: +Time to create a view: +{% filename %}blog/views.py{% endfilename %} ```python def post_draft_list(request): posts = Post.objects.filter(published_date__isnull=True).order_by('created_date') @@ -40,8 +44,9 @@ def post_draft_list(request): The line ` posts = Post.objects.filter(published_date__isnull=True).order_by('created_date')` makes sure that we take only unpublished posts (`published_date__isnull=True`) and order them by `created_date` (`order_by('created_date')`). -Ok, the last bit is of course a template! Create a file `blog/templates/blog/post_draft_list.html` and add the following: +Ok, the last bit is of course a template! Create a new template file `post_draft_list.html` and add the following: +{% filename %}blog/templates/blog/post_draft_list.html{% endfilename %} ```django {% extends 'blog/base.html' %} @@ -66,8 +71,9 @@ Yay! Your first task is done! It would be nice to have a button on the blog post detail page that will immediately publish the post, right? -Let's open `blog/templates/blog/post_detail.html` and change these lines: +Let's open `post_detail.html` and change these lines: +{% filename %}blog/templates/blog/post_detail.html{% endfilename %} ```django {% if post.published_date %}
@@ -78,26 +84,29 @@ Let's open `blog/templates/blog/post_detail.html` and change these lines: into these: +{% filename %}blog/templates/blog/post_detail.html{% endfilename %} ```django {% if post.published_date %}
{{ post.published_date }}
{% else %} - Publish + Publish {% endif %} ``` -As you noticed, we added {% raw %}`{% else %}`{% endraw %} line here. That means, that if the condition from {% raw %}`{% if post.published_date %}`{% endraw %} is not fulfilled (so if there is no `published_date`), then we want to do the line {% raw %}`Publish`{% endraw %}. Note that we are passing a `pk` variable in the {% raw %}`{% url %}`{% endraw %}. +As you noticed, we added {% raw %}`{% else %}`{% endraw %} line here. That means, that if the condition from {% raw %}`{% if post.published_date %}`{% endraw %} is not fulfilled (so if there is no `published_date`), then we want to do the line {% raw %}`Publish`{% endraw %}. Note that we are passing a `pk` variable in the {% raw %}`{% url %}`{% endraw %}. -Time to create a URL (in `blog/urls.py`): +Time to create a URL: +{% filename %}blog/urls.py{% endfilename %} ```python path('post//publish/', views.post_publish, name='post_publish'), ``` -and finally, a *view* (as always, in `blog/views.py`): +and finally, a *view*: +{% filename %}blog/views.py{% endfilename %} ```python def post_publish(request, pk): post = get_object_or_404(Post, pk=pk) @@ -107,6 +116,7 @@ def post_publish(request, pk): Remember, when we created a `Post` model we wrote a method `publish`. It looked like this: +{% filename %}blog/models.py{% endfilename %} ```python def publish(self): self.published_date = timezone.now() @@ -123,22 +133,25 @@ Congratulations! You are almost there. The last step is adding a delete button! ## Delete post -Let's open `blog/templates/blog/post_detail.html` once again and add this line: +First let's download a trash icon and save with others in `blog/templates/blog/icons/`: [https://icons.getbootstrap.com/assets/icons/trash.svg](https://icons.getbootstrap.com/assets/icons/trash.svg) +Let's open `post_detail.html` once again and add this line just under the line with the edit button: + +{% filename %}blog/templates/blog/post_detail.html{% endfilename %} ```django - + {% include './icons/trash3.svg' %} ``` -just under a line with the edit button. - -Now we need a URL (`blog/urls.py`): +Now we need a URL: +{% filename %}blog/urls.py{% endfilename %} ```python path('post//remove/', views.post_remove, name='post_remove'), ``` -Now, time for a view! Open `blog/views.py` and add this code: +Now, time for a view! +{% filename %}blog/views.py{% endfilename %} ```python def post_remove(request, pk): post = get_object_or_404(Post, pk=pk) diff --git a/en/homework_create_more_models/README.md b/en/homework_create_more_models/README.md index e3b3583..4fcd188 100644 --- a/en/homework_create_more_models/README.md +++ b/en/homework_create_more_models/README.md @@ -6,6 +6,7 @@ Currently, we only have a Post model. What about receiving some feedback from yo Let's open `blog/models.py` and append this piece of code to the end of file: +{% filename %}blog/models.py{% endfilename %} ```python class Comment(models.Model): post = models.ForeignKey('blog.Post', on_delete=models.CASCADE, related_name='comments') @@ -55,6 +56,7 @@ Our Comment model exists in the database now! Wouldn't it be nice if we had acce To register the Comment model in the admin panel, go to `blog/admin.py` and add this line: +{% filename %}blog/admin.py{% endfilename %} ```python admin.site.register(Comment) ``` @@ -67,6 +69,7 @@ admin.site.register(Post) Remember to import the Comment model at the top of the file, too, like this: +{% filename %}blog/admin.py{% endfilename %} ```python from django.contrib import admin from .models import Post, Comment @@ -79,8 +82,9 @@ If you type `python manage.py runserver` on the command line and go to [http://1 ## Make our comments visible -Go to the `blog/templates/blog/post_detail.html` file and add the following lines before the {% raw %}`{% endblock %}`{% endraw %} tag: +Go to the `post_detail.html` file and add the following lines before the {% raw %}`{% endblock %}`{% endraw %} tag: +{% filename %}blog/templates/blog/post_detail.html{% endfilename %} ```django
{% for comment in post.comments.all %} @@ -96,22 +100,25 @@ Go to the `blog/templates/blog/post_detail.html` file and add the following line Now we can see the comments section on pages with post details. -But it could look a little bit better, so let's add some CSS to the bottom of the `static/css/blog.css` file: +But it could look a little bit better, so let's add some configuration to the CSS file: +{% filename %}static/css/blog.css{% endfilename %} ```css .comment { margin: 20px 0px 20px 20px; } ``` -We can also let visitors know about comments on the post list page. Go to the `blog/templates/blog/post_list.html` file and add the line: +We can also let visitors know about comments on the post list page. Go to the `post_list.html` file and add the line: +{% filename %}blog/templates/blog/post_list.html{% endfilename %} ```django Comments: {{ post.comments.count }} ``` After that our template should look like this: +{% filename %}blog/templates/blog/post_list.html{% endfilename %} ```django {% extends 'blog/base.html' %} @@ -135,6 +142,7 @@ Right now we can see comments on our blog, but we can't add them. Let's change t Go to `blog/forms.py` and add the following lines to the end of the file: +{% filename %}blog/forms.py{% endfilename %} ```python class CommentForm(forms.ModelForm): @@ -151,12 +159,14 @@ from .models import Post into: +{% filename %}blog/forms.py{% endfilename %} ```python from .models import Post, Comment ``` -Now, go to `blog/templates/blog/post_detail.html` and before the line {% raw %}`{% for comment in post.comments.all %}`{% endraw %}, add: +Now, go to `post_detail.html` and before the line {% raw %}`{% for comment in post.comments.all %}`{% endraw %}, add: +{% filename %}blog/templates/blog/post_detail.html{% endfilename %} ```django Add comment ``` @@ -167,6 +177,7 @@ If you go to the post detail page you should see this error: We know how to fix that! Go to `blog/urls.py` and add this pattern to `urlpatterns`: +{% filename %}blog/urls.py{% endfilename %} ```python path('post//comment/', views.add_comment_to_post, name='add_comment_to_post'), ``` @@ -175,8 +186,9 @@ Refresh the page, and we get a different error! ![AttributeError](images/views_error.png) -To fix this error, add this view to `blog/views.py`: +To fix this error, add this view: +{% filename %}blog/views.py{% endfilename %} ```python def add_comment_to_post(request, pk): post = get_object_or_404(Post, pk=pk) @@ -194,6 +206,7 @@ def add_comment_to_post(request, pk): Remember to import `CommentForm` at the beginning of the file: +{% filename %}blog/views.py{% endfilename %} ```python from .forms import PostForm, CommentForm ``` @@ -208,8 +221,9 @@ However, when you click that button, you'll see: ![TemplateDoesNotExist](images/template_error.png) -Like the error tells us, the template doesn't exist yet. So, let's create a new one at `blog/templates/blog/add_comment_to_post.html` and add the following code: +Like the error tells us, the template doesn't exist yet. So, let's create a new one at `add_comment_to_post.html` and add the following code: +{% filename %}blog/templates/blog/add_comment_to_post.html{% endfilename %} ```django {% extends 'blog/base.html' %} @@ -230,8 +244,9 @@ Not all of the comments should be displayed. As the blog owner, you probably wan > If you haven't already, you can download all the Bootstrap icons [here](https://github.com/twbs/icons/releases/download/v1.1.0/bootstrap-icons-1.1.0.zip). Unzip the file and copy all the SVG image files into a new folder inside `blog/templates/blog/` called `icons`. That way you can access an icon like `hand-thumbs-down.svg` using the file path `blog/templates/blog/icons/hand-thumbs-down.svg` -Go to `blog/templates/blog/post_detail.html` and change lines: +Go to `post_detail.html` and change lines: +{% filename %}blog/templates/blog/post_detail.html{% endfilename %} ```django {% for comment in post.comments.all %}
@@ -246,6 +261,7 @@ Go to `blog/templates/blog/post_detail.html` and change lines: to: +{% filename %}blog/templates/blog/post_detail.html{% endfilename %} ```django {% for comment in post.comments.all %} {% if user.is_authenticated or comment.approved_comment %} @@ -274,13 +290,15 @@ You should see `NoReverseMatch`, because no URL matches the `comment_remove` and To fix the error, add these URL patterns to `blog/urls.py`: +{% filename %}blog/urls.py{% endfilename %} ```python path('comment//approve/', views.comment_approve, name='comment_approve'), path('comment//remove/', views.comment_remove, name='comment_remove'), ``` -Now, you should see `AttributeError`. To fix this error, add these views in `blog/views.py`: +Now, you should see `AttributeError`. To fix this error, add these views: +{% filename %}blog/views.py{% endfilename %} ```python @login_required def comment_approve(request, pk): @@ -298,26 +316,30 @@ def comment_remove(request, pk): You'll need to import `Comment` at the top of the file: +{% filename %}blog/views.py{% endfilename %} ```python from .models import Post, Comment ``` Everything works! There is one small tweak we can make. In our post list page -- under posts -- we currently see the number of all the comments the blog post has received. Let's change that to show the number of *approved* comments there. -To fix this, go to `blog/templates/blog/post_list.html` and change the line: +To fix this, go to `post_list.html` and change the line: +{% filename %}blog/templates/blog/post_list.html{% endfilename %} ```django Comments: {{ post.comments.count }} ``` to: +{% filename %}blog/templates/blog/post_list.html{% endfilename %} ```django Comments: {{ post.approved_comments.count }} ``` -Finally, add this method to the `Post` model in `blog/models.py`: +Finally, add this method to the `Post` model: +{% filename %}blog/models.py{% endfilename %} ```python def approved_comments(self): return self.comments.filter(approved_comment=True)