Начнем с добавления LoginView
. Хорошо, что в Django это уже есть для нас: https://docs.djangoproject.com/en/2.0/topics/auth/default/
Нам просто нужно изменить hnews/urls.py
from django.contrib import admin from django.contrib.auth import views as auth_views from django.urls import include, path urlpatterns = [ path('admin/', admin.site.urls), path('posts/', include('hnews.posts.urls')), path('accounts/login/', auth_views.LoginView.as_view(), name='login'), ]
Теперь нам нужно установить, куда будет перенаправляться представление входа в систему. В settings.py
LOGIN_REDIRECT_URL = '/posts/'
Давайте добавим шаблон для представления входа в систему (скопируйте макароны из документации Django).
$ mkdir -p templates/registration $ touch templates/registration/login.html $ cat templates/registration/login.html <html> <body> {% if form.errors %} <p>Your username and password didn't match. Please try again.</p> {% endif %} {% if next %} {% if user.is_authenticated %} <p>Your account doesn't have access to this page. To proceed, please login with an account that has access.</p> {% else %} <p>Please login to see this page.</p> {% endif %} {% endif %} <form method="post" action="{% url 'login' %}"> {% csrf_token %} <table> <tr> <td>{{ form.username.label_tag }}</td> <td>{{ form.username }}</td> </tr> <tr> <td>{{ form.password.label_tag }}</td> <td>{{ form.password }}</td> </tr> </table> <input type="submit" value="login" /> <input type="hidden" name="next" value="{{ next }}" /> </form> </body> </html>
Для входа перейдите по этому адресу: http: // localhost: 8000 / accounts / login /
Давайте изменим наш шаблон списка.
<html> <body> {% if user.is_authenticated %} {{ user }} {% else %} <a href="{% url 'login' %}">Log in</a> {% endif %} {% if posts %} <ul> {% for post in posts %} <li>{{ post.title }} - {{ post.how_long_ago }} - {{ post.get_domain_name }}</li> {% endfor %} </ul> {% endif %} </body> </html>
Теперь мы можем проверить, вошли ли мы в систему. Давайте добавим представление выхода.
urls.py
from django.contrib import admin from django.contrib.auth import views as auth_views from django.urls import include, path urlpatterns = [ path('admin/', admin.site.urls), path('posts/', include('hnews.posts.urls')), path('accounts/login/', auth_views.LoginView.as_view(), name='login'), path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'), ]
settings.py
LOGOUT_REDIRECT_URL = '/posts/'
list.html
<html> <body> {% if user.is_authenticated %} {{ user }} - <a href="{% url 'logout' %}">Log out</a> {% else %} <a href="{% url 'login' %}">Log in</a> {% endif %} {% if posts %} <ul> {% for post in posts %} <li>{{ post.title }} - {{ post.how_long_ago }} - {{ post.get_domain_name }}</li> {% endfor %} </ul> {% endif %} </body> </html>
Хорошо, теперь у нас есть эта часть. Давайте добавим возможность голосовать за. Сначала давайте добавим VueJS.
<html> <body> {% if user.is_authenticated %} {{ user }} - <a href="{% url 'logout' %}">Log out</a> {% else %} <a href="{% url 'login' %}">Log in</a> {% endif %} <div id="app"> <ul> <li v-for="post in posts"> [[ post.title ]] - [[ post.how_long_ago ]] - [[ post.domain_name ]] </li> </ul> </div> <!-- development version, includes helpful console warnings --> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> var app = new Vue({ el: '#app', delimiters: ['[[', ']]'], data: { posts: {{ posts }}, } }) </script> </body> </html>
Давайте изменим наше представление, чтобы отправить JSON в шаблон.
class PostListView(ListView): template_name = 'posts/list.html' model = Post context_object_name = 'posts' def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=object_list, **kwargs) context['posts'] = json.dumps([ { 'title': post.title, 'how_long_ago': post.how_long_ago(), 'domain_name': post.get_domain_name(), } for post in context['posts'] ]) return context
Если мы перейдем на http: // localhost: 8000 / posts /, это не сработает. Когда вы смотрите на HTML-код страницы, вы видите, что JSON экранирован.
var app = new Vue({ el: '#app', delimiters: ['[[', ']]'], data: { posts: [{"title": "Microsoft Is Said to Have Agreed to Acquire GitHub", "how_long_ago": "20 hours ago", "domain_name": "bloomberg.com"}, {"title": "GitLab sees huge spike in project imports", "how_long_ago": "20 hours ago", "domain_name": "monitor.gitlab.net"}, {"title": "Facebook Gave Device Makers Deep Access to Data on Users and Friends", "how_long_ago": "20 hours ago", "domain_name": "nytimes.com"}], } })
Давайте исправим это с помощью фильтра safe
.
<script> var app = new Vue({ el: '#app', delimiters: ['[[', ']]'], data: { posts: {{ posts|safe }}, } }) </script>
Теперь это работает! Давайте отправим данные в шаблон, чтобы проголосовать за него.
def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=object_list, **kwargs) context['posts'] = json.dumps([ { 'title': post.title, 'how_long_ago': post.how_long_ago(), 'domain_name': post.get_domain_name(), 'upvoted': post.upvotes.filter(id=self.request.user.id).count() > 0, 'upvote_url': reverse('posts:set_upvoted_post', kwargs={'post_id': post.id}), } for post in context['posts'] ]) return context
Теперь о шаблоне:
<html> <body> {% if user.is_authenticated %} {{ user }} - <a href="{% url 'logout' %}">Log out</a> {% else %} <a href="{% url 'login' %}">Log in</a> {% endif %} <div id="app"> <ul> <li v-for="post in posts"> [[ post.title ]] - [[ post.how_long_ago ]] - [[ post.domain_name ]] {% if user.is_authenticated %} - <span v-if="post.upvoted">Upvoted</span><span v-else>Not Upvoted</span> - <button v-on:click="upvote(post)">Toggle upvoted</button> {% endif %} </li> </ul> </div> <!-- development version, includes helpful console warnings --> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <script> function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie !== '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = jQuery.trim(cookies[i]); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } function csrfSafeMethod(method) { // these HTTP methods do not require CSRF protection return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } var csrftoken = getCookie('csrftoken'); $.ajaxSetup({ beforeSend: function(xhr, settings) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) { xhr.setRequestHeader("X-CSRFToken", csrftoken); } } }); var app = new Vue({ el: '#app', delimiters: ['[[', ']]'], data: { posts: {{ posts|safe }}, }, methods: { upvote: function (post) { $.post({ url: post.upvote_url, data: JSON.stringify({ upvoted: !post.upvoted }), success: function (data, text_status, jq_XHR) { post.upvoted = !post.upvoted }, }) } } }) </script> </body> </html>
Мы используем некоторый код отсюда для отправки токена CSRF при выполнении вызова AJAX.
При тестировании голоса за, у меня была ошибка, мой пользователь продолжал выходить из системы. Затем я понял, что удаляю пользователя каждый раз, когда отменяю голосование.
Менеджер "многие ко многим" возвращает экземпляры User, а не PostUpvote. Я удалю менеджера, чтобы не запутаться. Теперь мой models.py выглядит так:
from datetime import timedelta from urllib.parse import urlparse from django.contrib.auth.models import User from django.db import models from django.template.defaultfilters import pluralize from django.utils import timezone class Post(models.Model): creator = models.ForeignKey( User, related_name='posts', on_delete=models.SET_NULL, null=True, ) creation_date = models.DateTimeField(auto_now_add=True) url = models.URLField() title = models.CharField(max_length=256) def how_long_ago(self): how_long = timezone.now() - self.creation_date if how_long < timedelta(minutes=1): return f'{how_long.seconds} second{pluralize(how_long.seconds)} ago' elif how_long < timedelta(hours=1): # total_seconds returns a float minutes = int(how_long.total_seconds()) // 60 return f'{minutes} minute{pluralize(minutes)} ago' elif how_long < timedelta(days=1): hours = int(how_long.total_seconds()) // 3600 return f'{hours} hour{pluralize(hours)} ago' else: return f'{how_long.days} day{pluralize(how_long.days)} ago' def get_domain_name(self): name = urlparse(self.url).hostname if name.startswith('www.'): return name[len('www.'):] else: return name def set_upvoted(self, user, *, upvoted): if upvoted: PostUpvote.objects.get_or_create(post=self, user=user) else: self.upvotes.filter(user=user).delete() class PostUpvote(models.Model): post = models.ForeignKey(Post, related_name='upvotes', on_delete=models.CASCADE) user = models.ForeignKey(User, related_name='post_upvotes', on_delete=models.CASCADE) class Meta: unique_together = ('post', 'user') class Comment(models.Model): creation_date = models.DateTimeField(auto_now_add=True) creator = models.ForeignKey( User, related_name='comments', on_delete=models.SET_NULL, null=True, ) post = models.ForeignKey(Post, related_name='comments', on_delete=models.CASCADE) parent = models.ForeignKey('Comment', related_name='replies', on_delete=models.CASCADE, null=True, default=None) content = models.TextField(null=True) def set_upvoted(self, user, *, upvoted): if upvoted: CommentUpvote.objects.get_or_create(comment=self, user=user) else: self.upvotes.filter(user=user).delete() class CommentUpvote(models.Model): comment = models.ForeignKey(Comment, related_name='upvotes', on_delete=models.CASCADE) user = models.ForeignKey(User, related_name='comment_upvotes', on_delete=models.CASCADE) class Meta: unique_together = ('comment', 'user')
Мне также пришлось изменить свой get_context_data
:
def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=object_list, **kwargs) context['posts'] = json.dumps([ { 'title': post.title, 'how_long_ago': post.how_long_ago(), 'domain_name': post.get_domain_name(), 'upvoted': post.upvotes.filter(user=self.request.user).count() > 0, 'upvote_url': reverse('posts:set_upvoted_post', kwargs={'post_id': post.id}), } for post in context['posts'] ]) return context
Хорошо, на сегодня хорошо!
Увидимся!
- Немного хакерских новостей в Django (Часть 1)
- Немного хакерских новостей в Django (Часть 2)
- Немного хакерских новостей в Django (часть 3)
- Немного хакерских новостей в Django (часть 4)
- Немного хакерских новостей в Django (часть 5)
- Немного хакерских новостей в Django (часть 7)
- Немного хакерских новостей в Django (часть 8)
- Немного хакерских новостей в Django (часть 9)
- Репозиторий Github: hnews