├── yatube ├── api │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── apps.py │ ├── permissions.py │ ├── urls.py │ ├── serializers.py │ └── views.py ├── core │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── templatetags │ │ ├── __init__.py │ │ └── user_filters.py │ ├── context_processors │ │ ├── __init__.py │ │ └── year.py │ ├── apps.py │ └── views.py ├── about │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── apps.py │ ├── urls.py │ └── views.py ├── posts │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_models.py │ │ ├── test_urls.py │ │ ├── test_forms.py │ │ └── test_views.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0003_auto_20211017_1611.py │ │ ├── 0008_auto_20220122_1420.py │ │ ├── 0005_post_image.py │ │ ├── 0007_follow.py │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20211016_1532.py │ │ ├── 0006_comment.py │ │ └── 0004_auto_20211126_1732.py │ ├── apps.py │ ├── forms.py │ ├── admin.py │ ├── urls.py │ ├── models.py │ └── views.py ├── users │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── apps.py │ ├── views.py │ ├── forms.py │ └── urls.py ├── yatube │ ├── __init__.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── static │ └── img │ │ └── logo.png ├── tmpxm0zp75c │ ├── posts │ │ └── small.gif │ └── cache │ │ └── 92 │ │ └── fc │ │ └── 92fc9ce05ed8d55fe1a4fc232d150188.jpg ├── templates │ ├── core │ │ ├── 500.html │ │ ├── 403.html │ │ ├── 403csrf.html │ │ └── 404.html │ ├── includes │ │ ├── footer.html │ │ └── header.html │ ├── posts │ │ ├── includes │ │ │ ├── comments.html │ │ │ ├── switcher.html │ │ │ └── paginator.html │ │ ├── group_list.html │ │ ├── follow.html │ │ ├── index.html │ │ ├── profile.html │ │ ├── post_detail.html │ │ └── create_post.html │ ├── about │ │ ├── author.html │ │ └── tech.html │ ├── users │ │ ├── logged_out.html │ │ ├── password_change_done.html │ │ ├── password_reset_done.html │ │ ├── password_reset_complete.html │ │ ├── password_reset_form.html │ │ ├── signup.html │ │ ├── password_change_form.html │ │ ├── login.html │ │ └── password_reset_confirm.html │ └── base.html ├── test.py └── manage.py ├── requirements.txt ├── README.md ├── LICENSE └── .gitignore /yatube/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yatube/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yatube/about/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yatube/posts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yatube/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yatube/yatube/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yatube/api/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yatube/posts/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yatube/about/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yatube/core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yatube/core/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yatube/posts/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yatube/users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yatube/core/context_processors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==2.2.19 2 | pytz==2021.3 3 | sqlparse==0.4.2 4 | -------------------------------------------------------------------------------- /yatube/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimi-lan/yatube_project/HEAD/yatube/static/img/logo.png -------------------------------------------------------------------------------- /yatube/api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | name = 'api' 6 | -------------------------------------------------------------------------------- /yatube/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = 'core' 6 | -------------------------------------------------------------------------------- /yatube/about/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AboutConfig(AppConfig): 5 | name = 'about' 6 | -------------------------------------------------------------------------------- /yatube/posts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PostsConfig(AppConfig): 5 | name = 'posts' 6 | -------------------------------------------------------------------------------- /yatube/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = 'users' 6 | -------------------------------------------------------------------------------- /yatube/tmpxm0zp75c/posts/small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimi-lan/yatube_project/HEAD/yatube/tmpxm0zp75c/posts/small.gif -------------------------------------------------------------------------------- /yatube/templates/core/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Custom 500{% endblock %} 3 | {% block content %} 4 |

Custom 500

5 | {% endblock %} -------------------------------------------------------------------------------- /yatube/templates/includes/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yatube/templates/core/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Custom 403{% endblock %} 3 | {% block content %} 4 |

Custom 403

5 | {% endblock %} 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /yatube/tmpxm0zp75c/cache/92/fc/92fc9ce05ed8d55fe1a4fc232d150188.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vladimi-lan/yatube_project/HEAD/yatube/tmpxm0zp75c/cache/92/fc/92fc9ce05ed8d55fe1a4fc232d150188.jpg -------------------------------------------------------------------------------- /yatube/templates/core/403csrf.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Custom CSRF check error{% endblock %} 3 | {% block content %} 4 |

Custom CSRF check error. 403

5 | {% endblock %} 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /yatube/core/context_processors/year.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | def year(request): 5 | """Добавляет переменную с текущим годом.""" 6 | y = datetime.today().year 7 | return { 8 | 'year': y, 9 | } 10 | -------------------------------------------------------------------------------- /yatube/test.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | def year(): 5 | """Добавляет переменную с текущим годом.""" 6 | y = datetime.today().year 7 | return { 8 | 'year': y, 9 | } 10 | 11 | 12 | print(year()) 13 | -------------------------------------------------------------------------------- /yatube/templates/core/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Custom 404{% endblock %} 3 | {% block content %} 4 |

Custom 404

5 |

Страницы с адресом {{ path }} не существует

6 | Идите на главную 7 | {% endblock %} -------------------------------------------------------------------------------- /yatube/about/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | 6 | app_name = 'about' 7 | 8 | urlpatterns = [ 9 | path('author/', views.AboutAuthorView.as_view(), name='author'), 10 | path('tech/', views.AboutTechView.as_view(), name='tech'), 11 | ] 12 | -------------------------------------------------------------------------------- /yatube/about/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.views.generic.base import TemplateView 3 | 4 | 5 | class AboutAuthorView(TemplateView): 6 | template_name = 'about/author.html' 7 | 8 | class AboutTechView(TemplateView): 9 | template_name = 'about/tech.html' 10 | -------------------------------------------------------------------------------- /yatube/users/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import CreateView 2 | from django.urls import reverse_lazy 3 | from .forms import CreationForm 4 | 5 | class SignUp(CreateView): 6 | form_class = CreationForm 7 | success_url = reverse_lazy('posts:index') 8 | template_name = 'users/signup.html' -------------------------------------------------------------------------------- /yatube/api/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsOwnerOrReadOnly(permissions.BasePermission): 5 | def has_object_permission(self, request, view, obj): 6 | if request.method in permissions.SAFE_METHODS: 7 | return True 8 | return obj.author == request.user 9 | -------------------------------------------------------------------------------- /yatube/core/templatetags/user_filters.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | # В template.Library зарегистрированы все встроенные теги и фильтры шаблонов; 3 | # добавляем к ним и наш фильтр. 4 | register = template.Library() 5 | 6 | 7 | @register.filter 8 | def addclass(field, css): 9 | return field.as_widget(attrs={'class': css}) 10 | -------------------------------------------------------------------------------- /yatube/users/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.forms import UserCreationForm 2 | from django.contrib.auth import get_user_model 3 | 4 | 5 | User = get_user_model() 6 | 7 | class CreationForm(UserCreationForm): 8 | class Meta(UserCreationForm.Meta): 9 | model = User 10 | fields = ('first_name', 'last_name', 'username', 'email') 11 | -------------------------------------------------------------------------------- /yatube/templates/posts/includes/comments.html: -------------------------------------------------------------------------------- 1 | {% for comment in comments %} 2 |
3 |
4 |
5 | 6 | {{ post.author.get_full_name }} 7 | 8 |
9 |

10 | {{ comment.text }} 11 |

12 |
13 |
14 | {% endfor %} -------------------------------------------------------------------------------- /yatube/yatube/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for yatube project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yatube.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /yatube/templates/about/author.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} 3 | Об авторе проекта 4 | {% endblock %} 5 | {% block content %} 6 |

Привет, я автор

7 |

8 | Тут я размещу информацию о себе используя свои умения верстать. 9 | Картинки, блоки, элементы бустрап. А может быть, просто напишу несколько абзацев текста.
10 | 11 | мой GitHub 12 | 13 |

14 | {% endblock %} -------------------------------------------------------------------------------- /yatube/posts/migrations/0003_auto_20211017_1611.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.19 on 2021-10-17 13:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('posts', '0002_auto_20211016_1532'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='group', 15 | name='slug', 16 | field=models.SlugField(blank=True, max_length=100), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /yatube/posts/migrations/0008_auto_20220122_1420.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.19 on 2022-01-22 11:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('posts', '0007_follow'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddConstraint( 14 | model_name='follow', 15 | constraint=models.UniqueConstraint(fields=('user', 'author'), name='unique_following'), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /yatube/posts/migrations/0005_post_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.19 on 2021-11-26 14:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('posts', '0004_auto_20211126_1732'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='post', 15 | name='image', 16 | field=models.ImageField(blank=True, upload_to='posts/', verbose_name='Картинка'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /yatube/templates/users/logged_out.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} 3 | Вы вышли из системы 4 | {% endblock %} 5 | {% block content %} 6 |
7 |
8 |
9 |
10 | Выход 11 |
12 |
13 |

14 | Вы вышли из своей учётной записи. Ждём вас снова! 15 |

16 |
17 |
18 |
19 |
20 | {% endblock %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yatube_project 2 | Социальная сеть блогеров 3 | ### Описание 4 | Это будет сайт, на котором можно создать свою страницу. Если на нее зайти, то можно посмотреть все записи автора. 5 | Пользователи смогут заходить на чужие страницы, подписываться на авторов и комментировать их записи. 6 | ### Технологии 7 | Python 3.7 8 | Django 2.2.19 9 | ### Запуск проекта в dev-режиме 10 | - Установите и активируйте виртуальное окружение 11 | - Установите зависимости из файла requirements.txt 12 | ``` 13 | pip install -r requirements.txt 14 | ``` 15 | - В папке с файлом manage.py выполните команду: 16 | ``` 17 | python3 manage.py runserver 18 | ``` -------------------------------------------------------------------------------- /yatube/templates/posts/includes/switcher.html: -------------------------------------------------------------------------------- 1 | {% if user.is_authenticated %} 2 |
3 | 21 |
22 | {% endif %} -------------------------------------------------------------------------------- /yatube/templates/users/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} 3 | Пароль изменён 4 | {% endblock %} 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |
11 | Пароль изменён 12 |
13 |
14 |

Пароль изменён успешно

15 |
16 |
17 |
18 |
19 |
20 | {% endblock %} -------------------------------------------------------------------------------- /yatube/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yatube.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /yatube/templates/users/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} 3 | Сброс пароля прошёл успешно 4 | {% endblock %} 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |
11 | Отправлено письмо 12 |
13 |
14 |

Проверьте свою почту, вам должно прийти письмо со ссылкой для восстановления пароля

15 |
16 |
17 |
18 |
19 |
20 | {% endblock %} -------------------------------------------------------------------------------- /yatube/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | from rest_framework.authtoken import views 4 | from rest_framework.routers import DefaultRouter 5 | 6 | from .views import CommentViewSet, GroupViewSet, PostViewSet, UserViewSet 7 | 8 | app_name = 'api' 9 | 10 | router = DefaultRouter() 11 | router.register('posts', PostViewSet) 12 | router.register('groups', GroupViewSet) 13 | router.register( 14 | r'posts/(?P\d+)/comments', 15 | CommentViewSet, basename='comment') 16 | router.register('createuser', UserViewSet) 17 | 18 | urlpatterns = [ 19 | path('admin/', admin.site.urls), 20 | path('v1/', include(router.urls)), 21 | path('v1/api-token-auth/', views.obtain_auth_token), 22 | ] 23 | -------------------------------------------------------------------------------- /yatube/templates/users/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} 3 | Сброс пароля прошёл успешно 4 | {% endblock %} 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |
11 | Восстановление пароля завершено 12 |
13 |
14 |

Ваш пароль был сохранен. Используйте его для входа

15 | войти 16 |
17 |
18 |
19 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /yatube/core/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from http import HTTPStatus 3 | 4 | 5 | def page_not_found(request, exception): 6 | # Переменная exception содержит отладочную информацию; 7 | # выводить её в шаблон пользователской страницы 404 мы не станем 8 | return render( 9 | request, 'core/404.html', 10 | {'path': request.path}, 11 | status=HTTPStatus.NOT_FOUND 12 | ) 13 | 14 | 15 | def server_error(request): 16 | return render( 17 | request, 'core/500.html', 18 | status=HTTPStatus.INTERNAL_SERVER_ERROR 19 | ) 20 | 21 | 22 | def permission_denied(request, exception): 23 | return render( 24 | request, 'core/403.html', 25 | status=HTTPStatus.FORBIDDEN 26 | ) 27 | 28 | 29 | def csrf_failure(request, reason=''): 30 | return render( 31 | request, 'core/403csrf.html', 32 | ) 33 | -------------------------------------------------------------------------------- /yatube/posts/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ModelForm 2 | from .models import Post, Comment 3 | 4 | 5 | class PostForm(ModelForm): 6 | 7 | class Meta: 8 | model = Post 9 | fields = ('text', 'group', 'image',) 10 | labels = { 11 | 'text': 'Текст поста', 12 | 'group': 'Группа', 13 | 'image': 'Картинка', 14 | } 15 | help_texts = { 16 | 'text': 'Введите текст поста', 17 | 'group': 'Группа, к которой будет относиться пост', 18 | 'image': 'Загрузите картинку поста', 19 | } 20 | 21 | 22 | class CommentForm(ModelForm): 23 | 24 | class Meta: 25 | model = Comment 26 | fields = ('text',) 27 | labels = { 28 | 'text': 'Текст комментария', 29 | } 30 | help_texts = { 31 | 'text': 'Введите текст комментария', 32 | } 33 | -------------------------------------------------------------------------------- /yatube/templates/about/tech.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} 3 | Технологии 4 | {% endblock %} 5 | {% block content %} 6 | {% comment %} Колонки с отступом сверху и снизу {% endcomment %} 7 |
8 |

Вот что я умею

9 | {% comment %} 10 | Боковой блок со списком технологий 11 | Займет всю ширину блока на мобильном 12 | и 25% при размерах экрана ≥768px 13 | {% endcomment %} 14 | 24 | {% comment %} 25 | Статья 26 | Займет всю ширину блока на мобильном 27 | и 75% при размерах экрана ≥768px 28 | {% endcomment %} 29 |
30 |

Здесь будет текст

31 |
32 |
33 | {% endblock %} -------------------------------------------------------------------------------- /yatube/posts/migrations/0007_follow.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.19 on 2021-12-02 17:39 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('posts', '0006_comment'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Follow', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following', to=settings.AUTH_USER_MODEL, verbose_name='автор')), 21 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='follower', to=settings.AUTH_USER_MODEL, verbose_name='Подписчик')), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /yatube/templates/posts/group_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load thumbnail %} 3 | {% block title %} 4 | Записи сообщества {{group.title}} 5 | {% endblock %} 6 | {% block content %} 7 | 8 |
9 |

Записи сообщества {{ group.title }}

10 |

{% if group.description %} {{ group.description }} {% endif %}

11 | {% for post in page_obj %} 12 | 20 | {% thumbnail post.image "960x339" crop="center" upscale=True as im %} 21 | 22 | {% endthumbnail %} 23 |

{{ post.text }}

24 | {% if not forloop.last %}
{% endif %} 25 | {% endfor %} 26 | 27 | {% include 'posts/includes/paginator.html' %} 28 |
29 | {% endblock %} -------------------------------------------------------------------------------- /yatube/posts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.19 on 2021-10-15 13:23 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Post', 19 | fields=[ 20 | ('id', models.AutoField( 21 | auto_created=True, 22 | primary_key=True, 23 | serialize=False, 24 | verbose_name='ID')), 25 | ('text', models.TextField()), 26 | ('pub_date', models.DateTimeField(auto_now_add=True)), 27 | ('author', models.ForeignKey( 28 | on_delete=django.db.models.deletion.CASCADE, 29 | related_name='posts', 30 | to=settings.AUTH_USER_MODEL)), 31 | ], 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vladimi-lan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /yatube/posts/migrations/0002_auto_20211016_1532.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.19 on 2021-10-16 12:32 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('posts', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Group', 16 | fields=[ 17 | ('id', models.AutoField( 18 | auto_created=True, 19 | primary_key=True, 20 | serialize=False, 21 | verbose_name='ID')), 22 | ('title', models.CharField(max_length=200)), 23 | ('slug', models.FilePathField()), 24 | ('description', models.TextField()), 25 | ], 26 | ), 27 | migrations.AddField( 28 | model_name='post', 29 | name='group', 30 | field=models.ForeignKey( 31 | blank=True, 32 | null=True, 33 | on_delete=django.db.models.deletion.CASCADE, 34 | to='posts.Group'), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /yatube/posts/migrations/0006_comment.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.19 on 2021-11-27 14:44 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('posts', '0005_post_image'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Comment', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('text', models.TextField(help_text='Введите текст комментария')), 21 | ('created', models.DateTimeField(auto_now_add=True)), 22 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL)), 23 | ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='posts.Post', verbose_name='Комментарий')), 24 | ], 25 | options={ 26 | 'ordering': ('created',), 27 | }, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /yatube/posts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Follow, Group, Post, Comment 4 | 5 | 6 | class PostAdmin(admin.ModelAdmin): 7 | list_display = ( 8 | 'pk', 9 | 'text', 10 | 'pub_date', 11 | 'author', 12 | 'group', 13 | ) 14 | list_editable = ('group',) 15 | search_fields = ('text',) 16 | list_filter = ('pub_date',) 17 | empty_value_display = '-пусто-' 18 | 19 | 20 | class GroupAdmin(admin.ModelAdmin): 21 | list_display = ( 22 | 'pk', 23 | 'title', 24 | 'slug', 25 | 'description', 26 | ) 27 | search_fields = ('title',) 28 | empty_value_display = '-пусто-' 29 | 30 | 31 | class CommentAdmin(admin.ModelAdmin): 32 | list_display = ( 33 | 'pk', 34 | 'author', 35 | 'text', 36 | 'created', 37 | ) 38 | search_fields = ('text', 'author', 'post') 39 | list_filter = ('created', 'post') 40 | 41 | class FollowAdmin(admin.ModelAdmin): 42 | list_display = ('pk', 'user', 'author') 43 | search_fields = ('user', 'author') 44 | 45 | admin.site.register(Post, PostAdmin) 46 | admin.site.register(Group, GroupAdmin) 47 | admin.site.register(Comment, CommentAdmin) 48 | admin.site.register(Follow, FollowAdmin) -------------------------------------------------------------------------------- /yatube/posts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'posts' 6 | 7 | urlpatterns = [ 8 | # Главная страница 9 | path('', views.index, name='index'), 10 | # Страница групп 11 | path('group//', views.group_posts, name='group_list'), 12 | # Профайл пользователя 13 | path('profile//', views.profile, name='profile'), 14 | # Просмотр записи 15 | path('posts//', views.post_detail, name='post_detail'), 16 | # Создание новой записи 17 | path('create/', views.post_create, name='post_create'), 18 | # Редактирование записи 19 | path('posts//edit/', views.post_edit, name='post_edit'), 20 | # Добавление комментария 21 | path('posts//comment/', views.add_comment, name='add_comment'), 22 | # Просмотр постов авторов на которых подписан 23 | path('follow/', views.follow_index, name='follow_index'), 24 | # Подписка 25 | path( 26 | 'profile//follow/', 27 | views.profile_follow, 28 | name='profile_follow' 29 | ), 30 | # Отписка 31 | path( 32 | 'profile//unfollow/', 33 | views.profile_unfollow, 34 | name='profile_unfollow' 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /yatube/templates/posts/includes/paginator.html: -------------------------------------------------------------------------------- 1 | {% if page_obj.has_other_pages %} 2 | 37 | {% endif %} -------------------------------------------------------------------------------- /yatube/templates/users/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} 3 | Сброс пароля 4 | {% endblock %} 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |
11 | Чтобы сбросить старый пароль — введите адрес электронной почты, под которым вы регистрировались 12 |
13 |
14 |
15 | {% csrf_token %} 16 |
17 | 21 | 22 |
23 |
24 | 27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {% endblock %} -------------------------------------------------------------------------------- /yatube/templates/posts/follow.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load cache %} 3 | {% load thumbnail %} 4 | {% block title %} 5 | Подписки 6 | {% endblock %} 7 | {% block content %} 8 | 9 |
10 |

Подписки

11 | {% include 'posts/includes/switcher.html' %} 12 | {% cache 20 follow_page page_obj.number %} 13 | {% for post in page_obj %} 14 | 23 | {% thumbnail post.image "960x339" crop="center" upscale=True as im %} 24 | 25 | {% endthumbnail %} 26 |

{{ post.text }}

27 | Подробная информация
28 | {% if post.group %} 29 | все записи группы 30 | {% endif %} 31 | {% if not forloop.last %}
{% endif %} 32 | 33 | {% endfor %} 34 | {% endcache %} 35 | {% include 'posts/includes/paginator.html' %} 36 |
37 | {% endblock %} -------------------------------------------------------------------------------- /yatube/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from posts.models import Comment, Group, Post, User 4 | 5 | 6 | class UserSerializer(serializers.ModelSerializer): 7 | password = serializers.CharField(write_only=True) 8 | 9 | class Meta: 10 | fields = ('id', 'username', 'password',) 11 | model = User 12 | 13 | def create(self, validated_data): 14 | user = User.objects.create_user( 15 | username=validated_data['username'], 16 | password=validated_data['password'], 17 | ) 18 | return user 19 | 20 | 21 | class PostSerializer(serializers.ModelSerializer): 22 | author = serializers.SlugRelatedField( 23 | read_only=True, 24 | slug_field='username', 25 | ) 26 | 27 | class Meta: 28 | fields = ('id', 'text', 'author', 'image', 'group', 'pub_date') 29 | model = Post 30 | 31 | 32 | class GroupSerializer(serializers.ModelSerializer): 33 | 34 | class Meta: 35 | fields = ('id', 'title', 'slug', 'description') 36 | model = Group 37 | 38 | 39 | class CommentSerializer(serializers.ModelSerializer): 40 | author = serializers.SlugRelatedField( 41 | read_only=True, 42 | slug_field='username', 43 | ) 44 | post = serializers.PrimaryKeyRelatedField(read_only=True) 45 | 46 | class Meta: 47 | fields = ('id', 'author', 'post', 'text', 'created') 48 | model = Comment 49 | -------------------------------------------------------------------------------- /yatube/templates/posts/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load cache %} 3 | {% load thumbnail %} 4 | {% block title %} 5 | Последние обновления на странице 6 | {% endblock %} 7 | {% block content %} 8 | 9 |
10 |

Последние обновления на сайте

11 | {% include 'posts/includes/switcher.html' %} 12 | {% cache 20 index_page page_obj.number %} 13 | {% for post in page_obj %} 14 | 23 | {% thumbnail post.image "960x339" crop="center" upscale=True as im %} 24 | 25 | {% endthumbnail %} 26 |

{{ post.text }}

27 | Подробная информация
28 | {% if post.group %} 29 | все записи группы 30 | {% endif %} 31 | {% if not forloop.last %}
{% endif %} 32 | 33 | {% endfor %} 34 | {% endcache %} 35 | {% include 'posts/includes/paginator.html' %} 36 |
37 | {% endblock %} -------------------------------------------------------------------------------- /yatube/posts/migrations/0004_auto_20211126_1732.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.19 on 2021-11-26 14:32 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('posts', '0003_auto_20211017_1611'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='post', 17 | options={'ordering': ('-pub_date',)}, 18 | ), 19 | migrations.AlterField( 20 | model_name='group', 21 | name='slug', 22 | field=models.SlugField(blank=True, max_length=100, unique=True), 23 | ), 24 | migrations.AlterField( 25 | model_name='post', 26 | name='author', 27 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL, verbose_name='Автор'), 28 | ), 29 | migrations.AlterField( 30 | model_name='post', 31 | name='group', 32 | field=models.ForeignKey(blank=True, help_text='Выберите группу', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='group_posts', to='posts.Group', verbose_name='Группа'), 33 | ), 34 | migrations.AlterField( 35 | model_name='post', 36 | name='text', 37 | field=models.TextField(help_text='Введите текст поста'), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /yatube/yatube/urls.py: -------------------------------------------------------------------------------- 1 | """yatube URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.contrib import admin 18 | from django.urls import path, include 19 | from django.conf.urls.static import static 20 | 21 | import api.urls 22 | 23 | urlpatterns = [ 24 | path('', include('posts.urls', namespace='posts')), 25 | path('admin/', admin.site.urls), 26 | path('auth/', include('users.urls', namespace='users')), 27 | path('auth/', include('django.contrib.auth.urls')), 28 | path('about/', include('about.urls', namespace='about')), 29 | path('api/', include(api.urls)), 30 | ] 31 | 32 | if settings.DEBUG: 33 | import debug_toolbar 34 | 35 | urlpatterns += static( 36 | settings.MEDIA_URL, document_root=settings.MEDIA_ROOT 37 | ) 38 | urlpatterns += (path('__debug__/', include(debug_toolbar.urls)),) 39 | 40 | 41 | handler404 = 'core.views.page_not_found' 42 | handler500 = 'core.views.server_error' 43 | handler403 = 'core.views.permission_denied' 44 | -------------------------------------------------------------------------------- /yatube/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% load static %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% block title %} 19 | Заголовок не подвезли :( 20 | {% endblock %} 21 | 22 | 23 | 24 |
25 | {% include 'includes/header.html' %} 26 |
27 |
28 | {% block content %} 29 | Контент не подвезли :( 30 | {% endblock %} 31 |
32 | 33 | 34 | 35 | 36 |
37 | {% include 'includes/footer.html' %} 38 |
39 | 40 | -------------------------------------------------------------------------------- /yatube/users/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.views import LoginView, LogoutView, PasswordChangeDoneView, PasswordChangeView, PasswordResetCompleteView, PasswordResetConfirmView, PasswordResetDoneView, PasswordResetView 2 | from django.urls import path 3 | from . import views 4 | 5 | app_name = 'users' 6 | 7 | urlpatterns = [ 8 | path( 9 | 'logout/', 10 | LogoutView.as_view(template_name='users/logged_out.html'), 11 | name='logout' 12 | ), 13 | path('signup/', views.SignUp.as_view(), name='signup'), 14 | path( 15 | 'login/', 16 | LoginView.as_view(template_name='users/login.html'), 17 | name='login' 18 | ), 19 | path( 20 | 'password_reset/', 21 | PasswordResetView.as_view(template_name='users/password_reset_form.html'), 22 | name='password_reset' 23 | ), 24 | path( 25 | 'password_reset/done/', 26 | PasswordResetDoneView.as_view(template_name='users/password_reset_done.html'), 27 | name='password_reset_done' 28 | ), 29 | path( 30 | 'password_change/', 31 | PasswordChangeView.as_view(template_name='users/password_change_form.html'), 32 | name='password_change' 33 | ), 34 | path( 35 | 'password_change/done/', 36 | PasswordChangeDoneView.as_view(template_name='users/password_change_done.html'), 37 | name='password_change_done' 38 | ), 39 | path( 40 | 'reset///', 41 | PasswordResetConfirmView.as_view(template_name='users/password_reset_confirm.html'), 42 | name='password_reset_confirm' 43 | ), 44 | path( 45 | 'reset/done/', 46 | PasswordResetCompleteView.as_view(template_name='users/password_reset_complete.html'), 47 | name='password_reset_complete' 48 | ), 49 | ] -------------------------------------------------------------------------------- /yatube/templates/posts/profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load thumbnail %} 3 | {% block title %} 4 | Профайл пользователя {{ author.get_full_name }} 5 | {% endblock %} 6 | {% block content %} 7 |
8 |

Все посты пользователя {{ author.get_full_name }}

9 |

Всего постов: {{ post_count }}

10 | {% if following %} 11 | 15 | Отписаться 16 | 17 | {% else %} 18 | 22 | Подписаться 23 | 24 | {% endif %} 25 |
26 | {% for post in page_obj %} 27 |
    28 |
  • 29 | Дата публикации: {{ post.pub_date|date:"d E Y" }} 30 |
  • 31 |
32 | {% thumbnail post.image "960x339" crop="center" upscale=True as im %} 33 | 34 | {% endthumbnail %} 35 |

{{ post.text }}

36 | подробная информация
37 |
38 | {% if post.group %} 39 | все записи группы 40 | {% endif %} 41 | {% if not forloop.last %}
{% endif %} 42 | 43 | {% endfor %} 44 | 45 | {% include 'posts/includes/paginator.html' %} 46 |
47 | {% endblock %} -------------------------------------------------------------------------------- /yatube/posts/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.test import TestCase 3 | 4 | from ..models import Group, Post 5 | 6 | User = get_user_model() 7 | 8 | 9 | class PostModelTest(TestCase): 10 | @classmethod 11 | def setUpClass(cls): 12 | super().setUpClass() 13 | cls.user = User.objects.create_user(username='auth') 14 | cls.group = Group.objects.create( 15 | title='Тестовая группа', 16 | slug='Тестовый слаг', 17 | description='Тестовое описание', 18 | ) 19 | cls.post = Post.objects.create( 20 | author=cls.user, 21 | text='Тестовая группа', 22 | group=cls.group 23 | ) 24 | 25 | def test_models_have_correct_object_names(self): 26 | """Проверяем, что у моделей корректно работает __str__.""" 27 | expected_object_name_group = self.group.title 28 | expected_object_name_post = self.post.text[:15] 29 | self.assertEqual(expected_object_name_group, str(self.group)) 30 | self.assertEqual(expected_object_name_post, str(self.post)) 31 | 32 | def test_verbose_name(self): 33 | """verbose_name в полях совпадает с ожидаемым.""" 34 | post = self.post 35 | field_verboses = { 36 | 'author': 'Автор', 37 | 'group': 'Группа', 38 | } 39 | for value, expected in field_verboses.items(): 40 | with self.subTest(value=value): 41 | self.assertEqual( 42 | post._meta.get_field(value).verbose_name, expected) 43 | 44 | def test_help_text(self): 45 | """help_text в полях совпадает с ожидаемым.""" 46 | post = self.post 47 | field_help_texts = { 48 | 'text': 'Введите текст поста', 49 | 'group': 'Выберите группу', 50 | } 51 | for value, expected in field_help_texts.items(): 52 | with self.subTest(value=value): 53 | self.assertEqual( 54 | post._meta.get_field(value).help_text, expected) 55 | -------------------------------------------------------------------------------- /yatube/templates/users/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} 3 | Зарегистрироваться 4 | {% endblock %} 5 | {% block content %} 6 |
7 |
8 |
9 |
10 | Зарегистрироваться 11 |
12 |
13 | {% load user_filters %} 14 | {% if form.errors %} 15 | {% for field in form %} 16 | {% for error in field.errors %} 17 |
18 | {{ error|escape }} 19 |
20 | {% endfor %} 21 | {% endfor %} 22 | {% for error in form.non_field_errors %} 23 |
24 | {{ error|escape }} 25 |
26 | {% endfor %} 27 | {% endif %} 28 | 29 |
30 | {% csrf_token %} 31 | {% for field in form %} 32 |
33 | 39 | {{ field|addclass:'form-control' }} 40 | {% if field.help_text %} 41 | 42 | {{ field.help_text|safe }} 43 | 44 | {% endif %} 45 |
46 | {% endfor %} 47 |
48 | 51 |
52 |
53 |
54 |
55 |
56 |
57 | {% endblock %} -------------------------------------------------------------------------------- /yatube/templates/includes/header.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | -------------------------------------------------------------------------------- /yatube/posts/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.db import models 3 | 4 | User = get_user_model() 5 | 6 | 7 | class Group(models.Model): 8 | title = models.CharField(max_length=200) 9 | slug = models.SlugField(max_length=100, unique=True, blank=True) 10 | description = models.TextField() 11 | 12 | def __str__(self): 13 | return self.title 14 | 15 | 16 | class Post(models.Model): 17 | text = models.TextField( 18 | help_text='Введите текст поста' 19 | ) 20 | pub_date = models.DateTimeField(auto_now_add=True) 21 | author = models.ForeignKey( 22 | User, 23 | on_delete=models.CASCADE, 24 | related_name='posts', 25 | verbose_name='Автор' 26 | ) 27 | group = models.ForeignKey( 28 | Group, 29 | on_delete=models.SET_NULL, 30 | related_name='group_posts', 31 | blank=True, 32 | null=True, 33 | verbose_name='Группа', 34 | help_text='Выберите группу' 35 | ) 36 | image = models.ImageField( 37 | 'Картинка', 38 | upload_to='posts/', 39 | blank=True 40 | ) 41 | 42 | class Meta: 43 | ordering = ('-pub_date',) 44 | 45 | def __str__(self) -> str: 46 | return self.text[:15] 47 | 48 | 49 | class Comment(models.Model): 50 | post = models.ForeignKey( 51 | Post, 52 | on_delete=models.CASCADE, 53 | related_name='comments', 54 | verbose_name='Комментарий' 55 | ) 56 | author = models.ForeignKey( 57 | User, 58 | on_delete=models.CASCADE, 59 | related_name='comments' 60 | ) 61 | text = models.TextField( 62 | help_text='Введите текст комментария' 63 | ) 64 | created = models.DateTimeField(auto_now_add=True) 65 | 66 | class Meta: 67 | ordering = ('created',) 68 | 69 | def __str__(self) -> str: 70 | return f'Комментарий {self.author.username} к посту {self.post.id}' 71 | 72 | 73 | class Follow(models.Model): 74 | user = models.ForeignKey( 75 | User, 76 | on_delete=models.CASCADE, 77 | related_name='follower', 78 | verbose_name='Подписчик' 79 | ) 80 | author = models.ForeignKey( 81 | User, 82 | on_delete=models.CASCADE, 83 | related_name='following', 84 | verbose_name='автор' 85 | ) 86 | 87 | class Meta: 88 | constraints = [ 89 | models.UniqueConstraint( 90 | fields=['user', 'author'], 91 | name='unique_following' 92 | ), 93 | ] 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # Visual Studio Code 133 | .vscode/ 134 | 135 | # Media 136 | yatube/media/ -------------------------------------------------------------------------------- /yatube/templates/users/password_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} 3 | Изменение пароля 4 | {% endblock %} 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |
11 | Изменить пароль 12 |
13 |
14 |
15 | {% csrf_token %} 16 |
17 | 21 | 22 |
23 |
24 | 28 | 29 | 30 |
  • Ваш пароль не должен совпадать с вашим именем или другой персональной информацией или быть слишком похожим на неё.
  • Ваш пароль должен содержать как минимум 8 символов.
  • Ваш пароль не может быть одним из широко распространённых паролей.
  • Ваш пароль не может состоять только из цифр.
31 |
32 |
33 |
34 | 38 | 39 |
40 |
41 | 44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | {% endblock %} -------------------------------------------------------------------------------- /yatube/templates/users/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} 3 | Войти 4 | {% endblock %} 5 | {% block content %} 6 | {% load user_filters %} 7 |
8 |
9 |
10 |
11 | Войти на сайт 12 |
13 |
14 | {% if form.errors %} 15 | {% for field in form %} 16 | {% for error in field.errors %} 17 |
18 | {{ error|escape }} 19 |
20 | {% endfor %} 21 | {% endfor %} 22 | {% for error in form.non_field_errors %} 23 |
24 | {{ error|escape }} 25 |
26 | {% endfor %} 27 | {% endif %} 28 | 29 |
34 | {% csrf_token %} 35 | 36 | {% for field in form %} 37 |
44 | 50 |
51 | {{ field|addclass:'form-control' }} 52 | {% if field.help_text %} 53 | 54 | {{ field.help_text|safe }} 55 | 56 | {% endif %} 57 |
58 |
59 | {% endfor %} 60 |
61 | 64 | 66 | 67 | Забыли пароль? 68 | 69 |
70 |
71 |
72 |
73 |
74 |
75 | {% endblock %} -------------------------------------------------------------------------------- /yatube/api/views.py: -------------------------------------------------------------------------------- 1 | # from django.core.exceptions import PermissionDenied 2 | from django.shortcuts import get_object_or_404 3 | from rest_framework import viewsets, permissions 4 | from rest_framework.permissions import AllowAny 5 | 6 | from posts.models import Group, Post, User 7 | from .serializers import (CommentSerializer, GroupSerializer, PostSerializer, 8 | UserSerializer) 9 | from .permissions import IsOwnerOrReadOnly 10 | 11 | 12 | class PostViewSet(viewsets.ModelViewSet): 13 | queryset = Post.objects.all() 14 | serializer_class = PostSerializer 15 | permission_classes = [ 16 | permissions.IsAuthenticated, 17 | IsOwnerOrReadOnly 18 | ] 19 | 20 | def perform_create(self, serializer): 21 | serializer.save( 22 | author=self.request.user, 23 | ) 24 | 25 | # def perform_update(self, serializer): 26 | # if serializer.instance.author != self.request.user: 27 | # raise PermissionDenied('Изменение чужого контента запрещено!') 28 | # super(PostViewSet, self).perform_update(serializer) 29 | 30 | # def perform_destroy(self, instance): 31 | # if instance.author != self.request.user: 32 | # raise PermissionDenied('Изменение чужого контента запрещено!') 33 | # super(PostViewSet, self).perform_destroy(instance) 34 | 35 | 36 | class GroupViewSet(viewsets.ReadOnlyModelViewSet): 37 | queryset = Group.objects.all() 38 | serializer_class = GroupSerializer 39 | 40 | 41 | class CommentViewSet(viewsets.ModelViewSet): 42 | serializer_class = CommentSerializer 43 | permission_classes = [ 44 | permissions.IsAuthenticated, 45 | IsOwnerOrReadOnly 46 | ] 47 | 48 | def get_queryset(self): 49 | post_id = self.kwargs['post_id'] 50 | post = get_object_or_404(Post, pk=post_id) 51 | new_queryset = post.comments.all() 52 | return new_queryset 53 | 54 | def perform_create(self, serializer): 55 | serializer.save( 56 | author=self.request.user, 57 | post=get_object_or_404(Post, pk=self.kwargs['post_id']), 58 | ) 59 | 60 | # def perform_update(self, serializer): 61 | # if serializer.instance.author != self.request.user: 62 | # raise PermissionDenied('Изменение чужого контента запрещено!') 63 | # super(CommentViewSet, self).perform_update(serializer) 64 | 65 | # def perform_destroy(self, instance): 66 | # if instance.author != self.request.user: 67 | # raise PermissionDenied('Изменение чужого контента запрещено!') 68 | # super(CommentViewSet, self).perform_destroy(instance) 69 | 70 | 71 | class UserViewSet(viewsets.ModelViewSet): 72 | queryset = User.objects.all() 73 | serializer_class = UserSerializer 74 | 75 | def get_permissions(self): 76 | if self.request.method == 'POST': 77 | self.permission_classes = (AllowAny,) 78 | 79 | return super(UserViewSet, self).get_permissions() 80 | -------------------------------------------------------------------------------- /yatube/templates/users/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} 3 | Новый пароль 4 | {% endblock %} 5 | {% block content %} 6 | {% load user_filters %} 7 | 8 | {% if validlink %} 9 |
10 |
11 |
12 |
13 |
14 | Введите новый пароль 15 |
16 |
17 |
18 | 19 |
20 | 24 | 25 | 26 |
  • Ваш пароль не должен совпадать с вашим именем или другой персональной информацией или быть слишком похожим на неё.
  • Ваш пароль должен содержать как минимум 8 символов.
  • Ваш пароль не может быть одним из широко распространённых паролей.
  • Ваш пароль не может состоять только из цифр.
27 |
28 |
29 |
30 | 34 | 35 |
36 |
37 | 40 |
41 |
42 |
43 |
44 |
45 |
46 | {% else %} 47 | 48 |
49 |
50 |
51 |
52 | Ошибка 53 |
54 |
55 |

Ссылка сброса пароля содержит ошибку или устарела.

56 |
57 |
58 |
59 |
60 | 61 |
62 | {% endif %} 63 | 64 | {% endblock %} -------------------------------------------------------------------------------- /yatube/templates/posts/post_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load thumbnail %} 3 | {% load user_filters %} 4 | {% block title %} 5 | Пост {{ post|truncatechars:30 }} 6 | {% endblock %} 7 | {% block content %} 8 |
9 | 36 |
37 | {% thumbnail post.image "960x339" crop="center" upscale=True as im %} 38 | 39 | {% endthumbnail %} 40 |

41 | {{ post.text }} 42 |

43 | {% if user == post.author %} 44 | 45 | редактировать запись 46 | 47 | {% endif %} 48 | {% if user.is_authenticated %} 49 |
50 |
Добавить комментарий:
51 |
52 | 53 |
54 | 55 | {% csrf_token %} 56 |
57 | {% if form.text.label_tag %} 58 | 62 | {% endif %} 63 | {{ form.text|addclass:"form-control" }} 64 | {% if form.text.help_text %} 65 | 66 | {{ form.text.help_text }} 67 | 68 | {% endif %} 69 |
70 |
71 | 74 |
75 |
76 |
77 |
78 | {% endif %} 79 | {% include 'posts/includes/comments.html' %} 80 |
81 |
82 | {% endblock %} -------------------------------------------------------------------------------- /yatube/templates/posts/create_post.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} 3 | {% if is_edit %} 4 | Редактировать пост 5 | {% else %} 6 | Новый пост 7 | {% endif %} 8 | {% endblock %} 9 | {% block content %} 10 |
11 |
12 |
13 |
14 |
15 | {% if is_edit %} 16 | Редактировать пост 17 | {% else %} 18 | Новый пост 19 | {% endif %} 20 |
21 |
22 | {% if is_edit %} 23 |
24 | {% else %} 25 | 26 | {% endif %} 27 | {% csrf_token %} 28 |
29 | {% if form.text.label_tag %} 30 | 34 | {% endif %} 35 | {{ form.text }} 36 | {% if form.text.help_text %} 37 | 38 | {{ form.text.help_text }} 39 | 40 | {% endif %} 41 |
42 |
43 | {% if form.group.label_tag %} 44 | 48 | {% endif %} 49 | 54 | {% if form.group.help_text %} 55 | 56 | {{ form.group.help_text }} 57 | 58 | {% endif %} 59 |
60 |
61 | {% if form.image.label_tag %} 62 | 66 | {% endif %} 67 | {{ form.image }} 68 | {% if form.image.help_text %} 69 | 70 | {{ form.image.help_text }} 71 | 72 | {% endif %} 73 |
74 |
75 | 82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | {% endblock %} 90 | -------------------------------------------------------------------------------- /yatube/posts/tests/test_urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.test import Client, TestCase 3 | from http import HTTPStatus 4 | 5 | from ..models import Group, Post 6 | 7 | User = get_user_model() 8 | 9 | 10 | class StaticURLTests(TestCase): 11 | def setUp(self): 12 | self.guest_client = Client() 13 | 14 | def test_homepage(self): 15 | # Делаем запрос к главной странице и проверяем статус 16 | response = self.guest_client.get('/') 17 | # Утверждаем, что для прохождения теста код должен быть равен 200 18 | self.assertEqual(response.status_code, HTTPStatus.OK) 19 | 20 | def test_authorpage(self): 21 | """ Тестируем страницу об авторе """ 22 | response = self.guest_client.get('/about/author/') 23 | self.assertEqual(response.status_code, HTTPStatus.OK) 24 | 25 | def test_techpage(self): 26 | """ Тестируем страницу технологии """ 27 | response = self.guest_client.get('/about/tech/') 28 | self.assertEqual(response.status_code, HTTPStatus.OK) 29 | 30 | 31 | class PostUrlsTests(TestCase): 32 | @classmethod 33 | def setUpClass(cls): 34 | super().setUpClass() 35 | cls.author = User.objects.create_user( 36 | username='Author' 37 | ) 38 | cls.post = Post.objects.create( 39 | text='Тестовый текст', 40 | author=cls.author, 41 | pub_date='20.11.2021', 42 | 43 | ) 44 | cls.group = Group.objects.create( 45 | title='Тестовая группа', 46 | slug='test-slug', 47 | ) 48 | 49 | def setUp(self): 50 | self.guest_client = Client() 51 | self.authorized_client = Client() 52 | self.authorized_client_author = Client() 53 | self.user = User.objects.create_user(username='NonAuthor') 54 | self.authorized_client.force_login(self.user) 55 | self.authorized_client_author.force_login(self.author) 56 | 57 | def test_urls_exists_at_desired_location_guest(self): 58 | """Тестируем доступность страниц для не авторизованного пользователя""" 59 | templates_url_names = { 60 | '/': HTTPStatus.OK, 61 | '/group/test-slug/': HTTPStatus.OK, 62 | '/profile/Author/': HTTPStatus.OK, 63 | '/posts/1/': HTTPStatus.OK, 64 | '/posts/1/edit/': HTTPStatus.FOUND, 65 | '/create/': HTTPStatus.FOUND, 66 | 'unexisting_page/': HTTPStatus.NOT_FOUND, 67 | '/follow/': HTTPStatus.FOUND, 68 | } 69 | for adress, code in templates_url_names.items(): 70 | with self.subTest(adress=adress): 71 | response = self.guest_client.get(adress) 72 | self.assertEqual(response.status_code, code) 73 | 74 | def test_urls_exists_at_desired_location_authorized(self): 75 | """ 76 | Тестируем доступность страниц 77 | для авторизованного пользователя не автора 78 | """ 79 | templates_url_names = { 80 | '/': HTTPStatus.OK, 81 | '/group/test-slug/': HTTPStatus.OK, 82 | '/profile/NonAuthor/': HTTPStatus.OK, 83 | '/posts/1/': HTTPStatus.OK, 84 | '/posts/1/edit/': HTTPStatus.FOUND, 85 | '/create/': HTTPStatus.OK, 86 | 'unexisting_page/': HTTPStatus.NOT_FOUND, 87 | '/follow/': HTTPStatus.OK, 88 | } 89 | for adress, code in templates_url_names.items(): 90 | with self.subTest(adress=adress): 91 | response = self.authorized_client.get(adress) 92 | self.assertEqual(response.status_code, code) 93 | 94 | def test_urls_exists_at_desired_location_authorized_author(self): 95 | """ 96 | Тестируем доступность страницы редактирования 97 | для авторизованного пользователя автора 98 | """ 99 | response = self.authorized_client_author.get('/posts/1/edit/') 100 | self.assertEqual(response.status_code, HTTPStatus.OK) 101 | 102 | def test_urls_uses_correct_template(self): 103 | """URL-адрес использует соответствующий шаблон.""" 104 | templates_url_names = { 105 | '/': 'posts/index.html', 106 | '/group/test-slug/': 'posts/group_list.html', 107 | '/profile/Author/': 'posts/profile.html', 108 | '/posts/1/': 'posts/post_detail.html', 109 | '/posts/1/edit/': 'posts/create_post.html', 110 | '/create/': 'posts/create_post.html', 111 | '/follow/': 'posts/follow.html', 112 | } 113 | for adress, template in templates_url_names.items(): 114 | with self.subTest(adress=adress): 115 | response = self.authorized_client_author.get(adress) 116 | self.assertTemplateUsed(response, template) 117 | -------------------------------------------------------------------------------- /yatube/posts/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.auth.decorators import login_required 4 | from django.core.paginator import Paginator 5 | from django.shortcuts import get_object_or_404, redirect, render 6 | 7 | from .forms import CommentForm, PostForm 8 | from .models import Follow, Group, Post 9 | 10 | User = get_user_model() 11 | 12 | 13 | def paginate(request, posts, pages): 14 | paginator = Paginator(posts, pages) 15 | page_number = request.GET.get('page') 16 | page_obj = paginator.get_page(page_number) 17 | return page_obj 18 | 19 | 20 | def index(request): 21 | post_list = Post.objects.all() 22 | page_obj = paginate(request, post_list, settings.PAGES) 23 | context = { 24 | 'page_obj': page_obj, 25 | } 26 | return render(request, 'posts/index.html', context) 27 | 28 | 29 | def group_posts(request, slug): 30 | group = get_object_or_404(Group, slug=slug) 31 | posts = group.group_posts.all() 32 | page_obj = paginate(request, posts, settings.PAGES) 33 | context = { 34 | 'group': group, 35 | 'page_obj': page_obj, 36 | } 37 | return render(request, 'posts/group_list.html', context) 38 | 39 | 40 | def profile(request, username): 41 | user = get_object_or_404(User, username=username) 42 | user_posts = Post.objects.filter(author__username=username) 43 | post_count = user_posts.count() 44 | page_obj = paginate(request, user_posts, settings.PAGES) 45 | following = user.is_authenticated and user.following.exists() 46 | context = { 47 | 'page_obj': page_obj, 48 | 'post_count': post_count, 49 | 'author': user, 50 | 'following': following, 51 | } 52 | return render(request, 'posts/profile.html', context) 53 | 54 | 55 | def post_detail(request, post_id): 56 | post = get_object_or_404(Post, pk=post_id) 57 | author = post.author 58 | post_count = author.posts.all().count() # используем related_name 'posts' поля author модели Post, чтобы получить все экземпляры модели Post автора 59 | form = CommentForm( 60 | request.POST or None, 61 | ) 62 | comments = post.comments.all() 63 | context = { 64 | 'post': post, 65 | 'post_count': post_count, 66 | 'author': author, 67 | 'form': form, 68 | 'comments': comments, 69 | } 70 | return render(request, 'posts/post_detail.html', context) 71 | 72 | 73 | @login_required 74 | def post_create(request): 75 | form = PostForm( 76 | request.POST or None, 77 | files=request.FILES or None 78 | ) 79 | if form.is_valid(): 80 | new_post = form.save(commit=False) 81 | new_post.author = request.user 82 | new_post.save() 83 | return redirect('posts:profile', request.user.username) 84 | return render(request, 'posts/create_post.html', {'form': form}) 85 | 86 | 87 | @login_required 88 | def post_edit(request, post_id): 89 | post = get_object_or_404(Post, id=post_id) 90 | if request.user != post.author: 91 | return redirect('posts:post_detail', post.id) 92 | form = PostForm( 93 | request.POST or None, 94 | instance=post, 95 | files=request.FILES or None 96 | ) 97 | if form.is_valid(): 98 | form.save() 99 | return redirect('posts:post_detail', post.id) 100 | context = { 101 | 'form': form, 102 | 'post': post, 103 | 'is_edit': True, 104 | } 105 | return render(request, 'posts/create_post.html', context) 106 | 107 | 108 | @login_required 109 | def add_comment(request, post_id): 110 | post = get_object_or_404(Post, id=post_id) # Получить пост 111 | form = CommentForm(request.POST or None) 112 | if form.is_valid(): 113 | comment = form.save(commit=False) 114 | comment.author = request.user 115 | comment.post = post 116 | comment.save() 117 | return redirect('posts:post_detail', post_id=post_id) 118 | 119 | 120 | @login_required 121 | def follow_index(request): 122 | post_list = Post.objects.filter(author__following__user=request.user) 123 | page_obj = paginate(request, post_list, settings.PAGES) 124 | context = { 125 | 'page_obj': page_obj, 126 | } 127 | return render(request, 'posts/follow.html', context) 128 | 129 | 130 | @login_required 131 | def profile_follow(request, username): 132 | # Подписаться на автора 133 | author = get_object_or_404(User, username=username) 134 | user = request.user 135 | if user != author: 136 | Follow.objects.get_or_create(user=user, author=author) 137 | return redirect( 138 | 'posts:profile', username=username 139 | ) 140 | 141 | 142 | @login_required 143 | def profile_unfollow(request, username): 144 | # Дизлайк, отписка 145 | user = request.user 146 | Follow.objects.get(user=user, author__username=username).delete() 147 | return redirect('posts:profile', username=username) 148 | -------------------------------------------------------------------------------- /yatube/yatube/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for yatube project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.19. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'p=zh647p(juib)$kvfd&eecdzx13c3pg)1#l22l1cxpcv)v#-#' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [ 29 | 'localhost', 30 | '127.0.0.1', 31 | '[::1]', 32 | 'www.vladimidev.pythonanywhere.com', 33 | 'vladimidev.pythonanywhere.com', 34 | ] 35 | 36 | 37 | # Application definition 38 | 39 | INSTALLED_APPS = [ 40 | 'posts.apps.PostsConfig', 41 | 'django.contrib.admin', 42 | 'django.contrib.auth', 43 | 'django.contrib.contenttypes', 44 | 'django.contrib.sessions', 45 | 'django.contrib.messages', 46 | 'django.contrib.staticfiles', 47 | 'users.apps.UsersConfig', 48 | 'core.apps.CoreConfig', 49 | 'about.apps.AboutConfig', 50 | 'sorl.thumbnail', 51 | 'debug_toolbar', 52 | 'rest_framework', 53 | 'rest_framework.authtoken', 54 | 'api', 55 | ] 56 | 57 | MIDDLEWARE = [ 58 | 'django.middleware.security.SecurityMiddleware', 59 | 'django.contrib.sessions.middleware.SessionMiddleware', 60 | 'django.middleware.common.CommonMiddleware', 61 | 'django.middleware.csrf.CsrfViewMiddleware', 62 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 63 | 'django.contrib.messages.middleware.MessageMiddleware', 64 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 65 | 'debug_toolbar.middleware.DebugToolbarMiddleware', 66 | ] 67 | 68 | INTERNAL_IPS = [ 69 | '127.0.0.1', 70 | ] 71 | 72 | ROOT_URLCONF = 'yatube.urls' 73 | 74 | TEMPLATES_DIR = os.path.join(BASE_DIR, 'templates') 75 | 76 | TEMPLATES = [ 77 | { 78 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 79 | 'DIRS': [TEMPLATES_DIR], 80 | 'APP_DIRS': True, 81 | 'OPTIONS': { 82 | 'context_processors': [ 83 | 'django.template.context_processors.debug', 84 | 'django.template.context_processors.request', 85 | 'django.contrib.auth.context_processors.auth', 86 | 'django.contrib.messages.context_processors.messages', 87 | 'core.context_processors.year.year', 88 | ], 89 | }, 90 | }, 91 | ] 92 | 93 | WSGI_APPLICATION = 'yatube.wsgi.application' 94 | 95 | 96 | # Database 97 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 98 | 99 | DATABASES = { 100 | 'default': { 101 | 'ENGINE': 'django.db.backends.sqlite3', 102 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 103 | } 104 | } 105 | 106 | 107 | # Password validation 108 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 109 | 110 | AUTH_PASSWORD_VALIDATORS = [ 111 | { 112 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 113 | }, 114 | { 115 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 116 | }, 117 | { 118 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 119 | }, 120 | { 121 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 122 | }, 123 | ] 124 | 125 | 126 | # Internationalization 127 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 128 | 129 | LANGUAGE_CODE = 'en-us' 130 | 131 | TIME_ZONE = 'UTC' 132 | 133 | USE_I18N = True 134 | 135 | USE_L10N = True 136 | 137 | USE_TZ = True 138 | 139 | 140 | # Static files (CSS, JavaScript, Images) 141 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 142 | 143 | STATIC_URL = '/static/' 144 | 145 | STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] 146 | 147 | LOGIN_URL = 'users:login' 148 | LOGIN_REDIRECT_URL = 'posts:index' 149 | # LOGOUT_REDIRECT_URL = 'posts:index' 150 | 151 | MEDIA_URL = '/media/' 152 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 153 | 154 | 155 | EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' 156 | EMAIL_FILE_PATH = os.path.join(BASE_DIR, 'sent_emails') 157 | 158 | 159 | CACHES = { 160 | 'default': { 161 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 162 | } 163 | } 164 | 165 | # Global variables 166 | PAGES = 10 167 | 168 | # CSRF handler 169 | CSRF_FAILURE_VIEW = 'core.views.csrf_failure' 170 | 171 | 172 | REST_FRAMEWORK = { 173 | 'DEFAULT_PERMISSION_CLASSES': [ 174 | 'rest_framework.permissions.IsAuthenticated', 175 | ], 176 | 177 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 178 | 'rest_framework.authentication.TokenAuthentication', 179 | ] 180 | } 181 | -------------------------------------------------------------------------------- /yatube/posts/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import tempfile 3 | from http import HTTPStatus 4 | 5 | from django.conf import settings 6 | from django.contrib.auth import get_user_model 7 | from django.core.files.uploadedfile import SimpleUploadedFile 8 | from django.test import Client, TestCase, override_settings 9 | from django.urls import reverse 10 | 11 | from ..models import Group, Post, Comment 12 | 13 | User = get_user_model() 14 | 15 | TEMP_MEDIA_ROOT = tempfile.mkdtemp(dir=settings.BASE_DIR) 16 | 17 | 18 | @override_settings(MEDIA_ROOT=TEMP_MEDIA_ROOT) 19 | class PostCreateFormTests(TestCase): 20 | @classmethod 21 | def setUpClass(cls): 22 | super().setUpClass() 23 | cls.author = User.objects.create_user( 24 | username='Zuev' 25 | ) 26 | cls.group = Group.objects.create( 27 | title='Тестовая группа', 28 | slug='test-slug' 29 | ) 30 | cls.post = Post.objects.create( 31 | text='Тестовый текст', 32 | author=cls.author, 33 | group=cls.group, 34 | ) 35 | 36 | @classmethod 37 | def tearDownClass(cls): 38 | super().tearDownClass() 39 | # Метод shutil.rmtree удаляет директорию и всё её содержимое 40 | shutil.rmtree(TEMP_MEDIA_ROOT, ignore_errors=True) 41 | 42 | def setUp(self): 43 | self.guest_client = Client() 44 | self.authorized_client = Client() 45 | self.authorized_client.force_login(self.author) 46 | 47 | def test_create_post(self): 48 | """Валидная форма создает запись в Post.""" 49 | posts_count = Post.objects.count() 50 | small_gif = ( 51 | b'\x47\x49\x46\x38\x39\x61\x02\x00' 52 | b'\x01\x00\x80\x00\x00\x00\x00\x00' 53 | b'\xFF\xFF\xFF\x21\xF9\x04\x00\x00' 54 | b'\x00\x00\x00\x2C\x00\x00\x00\x00' 55 | b'\x02\x00\x01\x00\x00\x02\x02\x0C' 56 | b'\x0A\x00\x3B' 57 | ) 58 | uploaded = SimpleUploadedFile( 59 | name='small.gif', 60 | content=small_gif, 61 | content_type='image/gif' 62 | ) 63 | form_data = { 64 | 'text': 'Тестовый текст 2', 65 | 'author': self.author, 66 | 'group': self.group.id, 67 | 'image': uploaded, 68 | } 69 | response = self.authorized_client.post( 70 | reverse('posts:post_create'), 71 | data=form_data, 72 | follow=True 73 | ) 74 | self.assertRedirects( 75 | response, 76 | reverse( 77 | 'posts:profile', 78 | kwargs={'username': self.author.username} 79 | ), 80 | status_code=HTTPStatus.FOUND, 81 | target_status_code=HTTPStatus.OK 82 | ) 83 | self.assertEqual(Post.objects.count(), posts_count + 1) 84 | post = Post.objects.first() 85 | self.assertEqual(post.text, form_data['text']) 86 | self.assertEqual(post.author, form_data['author']) 87 | self.assertEqual(post.group.id, form_data['group']) 88 | self.assertEqual(post.image, 'posts/small.gif') 89 | 90 | def test_edit_post(self): 91 | """Валидная форма при редактировании изменяет запись в Post.""" 92 | edit_data = { 93 | 'text': 'Я изменил этот текст', 94 | } 95 | response = self.authorized_client.post( 96 | reverse('posts:post_edit', kwargs={'post_id': self.post.id}), 97 | data=edit_data, 98 | follow=True 99 | ) 100 | post = Post.objects.first() 101 | self.assertRedirects( 102 | response, 103 | reverse('posts:post_detail', kwargs={'post_id': post.id}), 104 | status_code=HTTPStatus.FOUND, 105 | target_status_code=HTTPStatus.OK 106 | ) 107 | self.assertEqual(post.text, edit_data['text']) 108 | 109 | def test_create_post_guest(self): 110 | posts_count = Post.objects.count() 111 | response = self.guest_client.post( 112 | reverse('posts:post_create'), 113 | follow=True, 114 | ) 115 | self.assertRedirects( 116 | response, 117 | reverse('users:login') + '?next=' + reverse('posts:post_create'), 118 | status_code=HTTPStatus.FOUND, 119 | target_status_code=HTTPStatus.OK 120 | ) 121 | self.assertEqual(Post.objects.count(), posts_count) 122 | 123 | def test_comment_post_authorized(self): 124 | """ 125 | Комментировать посты может только авторизованный пользователь. 126 | """ 127 | response = self.guest_client.get( 128 | reverse('posts:add_comment', kwargs={'post_id': self.post.id}) 129 | ) 130 | self.assertRedirects( 131 | response, 132 | reverse('users:login') + '?next=' + reverse( 133 | 'posts:add_comment', 134 | kwargs={'post_id': self.post.id} 135 | ), 136 | status_code=HTTPStatus.FOUND, 137 | target_status_code=HTTPStatus.OK 138 | ) 139 | 140 | def test_comment_on_post_page(self): 141 | """ 142 | После успешной отправки комментарий появляется на странице поста. 143 | """ 144 | comments_count = Comment.objects.count() 145 | form_data = { 146 | 'text': 'Тестовый комментарий.', 147 | } 148 | self.authorized_client.post( 149 | reverse('posts:add_comment', kwargs={'post_id': self.post.id}), 150 | data=form_data, 151 | follow=True 152 | ) 153 | self.assertEqual(Comment.objects.count(), comments_count + 1) 154 | self.assertEqual(Comment.objects.first().text, form_data['text']) 155 | -------------------------------------------------------------------------------- /yatube/posts/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import tempfile 3 | 4 | from django import forms 5 | from django.conf import settings 6 | from django.contrib.auth import get_user_model 7 | from django.core.cache import cache 8 | from django.core.files.uploadedfile import SimpleUploadedFile 9 | from django.test import Client, TestCase, override_settings 10 | from django.urls import reverse 11 | from http import HTTPStatus 12 | 13 | from ..models import Follow, Group, Post 14 | 15 | User = get_user_model() 16 | 17 | TEMP_MEDIA_ROOT = tempfile.mkdtemp(dir=settings.BASE_DIR) 18 | 19 | 20 | @override_settings(MEDIA_ROOT=TEMP_MEDIA_ROOT) 21 | class PostPagesTest(TestCase): 22 | @classmethod 23 | def setUpClass(cls): 24 | super().setUpClass() 25 | cls.author = User.objects.create_user( 26 | username='Author' 27 | ) 28 | cls.group = Group.objects.create( 29 | title='Тестовая группа', 30 | slug='test-slug', 31 | ) 32 | cls.small_gif = ( 33 | b'\x47\x49\x46\x38\x39\x61\x02\x00' 34 | b'\x01\x00\x80\x00\x00\x00\x00\x00' 35 | b'\xFF\xFF\xFF\x21\xF9\x04\x00\x00' 36 | b'\x00\x00\x00\x2C\x00\x00\x00\x00' 37 | b'\x02\x00\x01\x00\x00\x02\x02\x0C' 38 | b'\x0A\x00\x3B' 39 | ) 40 | cls.uploaded = SimpleUploadedFile( 41 | name='small.gif', 42 | content=cls.small_gif, 43 | content_type='image/gif' 44 | ) 45 | cls.post = Post.objects.create( 46 | text='Тестовый текст', 47 | author=cls.author, 48 | group=cls.group, 49 | image=cls.uploaded 50 | ) 51 | cls.pub_date = cls.post.pub_date 52 | 53 | @classmethod 54 | def tearDownClass(cls): 55 | super().tearDownClass() 56 | # Метод shutil.rmtree удаляет директорию и всё её содержимое 57 | shutil.rmtree(TEMP_MEDIA_ROOT, ignore_errors=True) 58 | 59 | def setUp(self): 60 | self.user = User.objects.create_user(username='Zuev') 61 | self.guest_client = Client() 62 | self.authorized_client = Client() 63 | self.authorized_client_author = Client() 64 | self.authorized_client.force_login(self.user) 65 | self.authorized_client_author.force_login(self.author) 66 | 67 | def test_pages_use_correct_template(self): 68 | """URL-адрес использует соответствующий шаблон.""" 69 | templates_pages_names = { 70 | 'posts/index.html': reverse('posts:index'), 71 | 'posts/group_list.html': ( 72 | reverse('posts:group_list', kwargs={'slug': self.group.slug}) 73 | ), 74 | 'posts/profile.html': ( 75 | reverse( 76 | 'posts:profile', 77 | kwargs={'username': self.author.username} 78 | ) 79 | ), 80 | 'posts/post_detail.html': ( 81 | reverse('posts:post_detail', kwargs={'post_id': self.post.id}) 82 | ), 83 | 'posts/create_post.html': reverse('posts:post_create'), 84 | } 85 | for template, reverse_name in templates_pages_names.items(): 86 | with self.subTest(reverse_name=reverse_name): 87 | response = self.authorized_client.get(reverse_name) 88 | self.assertTemplateUsed(response, template) 89 | 90 | def test_pages_use_correct_template_author(self): 91 | """ 92 | URL-адрес страницы редактирования поста использует 93 | соответствующий шаблон. 94 | """ 95 | template = 'posts/create_post.html' 96 | reverse_name = ( 97 | reverse('posts:post_edit', kwargs={'post_id': self.post.id}) 98 | ) 99 | response = self.authorized_client_author.get(reverse_name) 100 | self.assertTemplateUsed(response, template) 101 | 102 | def test_all_page_show_correct_context(self): 103 | """ 104 | Шаблон index, group_list, profile 105 | сформирован с правильным контекстом. 106 | """ 107 | post_pages = [ 108 | reverse('posts:index'), 109 | reverse('posts:group_list', kwargs={'slug': self.group.slug}), 110 | reverse('posts:profile', kwargs={'username': self.author.username}) 111 | ] 112 | for page in post_pages: 113 | with self.subTest(page=page): 114 | response = self.authorized_client.get(page) 115 | self.assertEqual( 116 | (response.context['page_obj'][0]), self.post) 117 | 118 | def test_posts_group_list_show_correct_context(self): 119 | """Шаблон group_list сформирован с правильным контекстом.""" 120 | response = self.authorized_client.get( 121 | reverse('posts:group_list', kwargs={'slug': self.group.slug}) 122 | ) 123 | self.assertEqual(response.context['group'].title, self.group.title) 124 | 125 | def test_posts_profile_show_correct_context(self): 126 | """Шаблон profile сформирован с правильным контекстом.""" 127 | response = self.authorized_client.get( 128 | reverse('posts:profile', kwargs={'username': self.author.username}) 129 | ) 130 | self.assertEqual(response.context['post_count'], 1) 131 | self.assertEqual( 132 | response.context['author'].username, 133 | self.author.username 134 | ) 135 | 136 | def test_posts_post_detail_correct_context(self): 137 | """Шаблон post_detail сформирован с правильным контекстом.""" 138 | response = self.authorized_client.get( 139 | reverse('posts:post_detail', kwargs={'post_id': self.post.id}) 140 | ) 141 | self.assertEqual(response.context['post_count'], 1) 142 | self.assertEqual( 143 | response.context['author'].username, 144 | self.author.username 145 | ) 146 | 147 | def test_create_post_new_show_correct_context(self): 148 | """ 149 | Шаблон create_post для создания поста сформирован 150 | с правильным контекстом. 151 | """ 152 | response = self.authorized_client.get(reverse('posts:post_create')) 153 | form_fields = { 154 | 'text': forms.fields.CharField, 155 | 'group': forms.fields.ChoiceField, 156 | } 157 | for value, expected in form_fields.items(): 158 | with self.subTest(value=value): 159 | form_field = response.context.get('form').fields.get(value) 160 | self.assertIsInstance(form_field, expected) 161 | 162 | def test_create_post_edit_show_correct_context(self): 163 | """ 164 | Шаблон create_post для редактирования поста сформирован 165 | с правильным контекстом. 166 | """ 167 | response = self.authorized_client_author.get( 168 | reverse('posts:post_edit', kwargs={'post_id': self.post.id}) 169 | ) 170 | self.assertEqual( 171 | response.context.get('form').instance.text, self.post.text 172 | ) 173 | self.assertEqual( 174 | response.context.get('form').instance.group.title, 175 | self.post.group.title 176 | ) 177 | 178 | def test_pages_context_has_image(self): 179 | """ 180 | При выводе поста с картинкой в index, group_list, profile 181 | изображение передаётся в словаре. 182 | """ 183 | post_pages = [ 184 | reverse('posts:index'), 185 | reverse('posts:group_list', kwargs={'slug': self.group.slug}), 186 | reverse( 187 | 'posts:profile', 188 | kwargs={'username': self.author.username} 189 | ) 190 | ] 191 | for page in post_pages: 192 | with self.subTest(page=page): 193 | response = self.guest_client.get(page) 194 | self.assertEqual( 195 | (response.context['page_obj'][0].image), 196 | self.post.image 197 | ) 198 | 199 | def test_post_detail_page_context_has_image(self): 200 | """ 201 | При выводе поста с картинкой в post_detail 202 | изображение передаётся в словаре. 203 | """ 204 | response = self.guest_client.get( 205 | reverse( 206 | 'posts:post_detail', 207 | kwargs={'post_id': self.post.id} 208 | ) 209 | ) 210 | self.assertEqual( 211 | (response.context['post'].image), 212 | self.post.image 213 | ) 214 | 215 | def test_cache_on_index_page(self): 216 | """ При удалении записи из базы, пост будет доступен из кэш.""" 217 | cache.clear() 218 | post = Post.objects.create( 219 | author=self.author, 220 | text='Текст поста' 221 | ) 222 | response = self.guest_client.get( 223 | reverse('posts:index') 224 | ) 225 | post.delete() 226 | self.assertIn(post.text, response.content.decode()) 227 | cache.clear() 228 | response = self.guest_client.get( 229 | reverse('posts:index') 230 | ) 231 | self.assertNotIn(post.text, response.content.decode()) 232 | 233 | def test_follow(self): 234 | """ 235 | Авторизованный пользователь 236 | может подписываться на других пользователей. 237 | """ 238 | followings = Follow.objects.filter( 239 | user=self.user, author=self.author 240 | ).count() 241 | response = self.authorized_client.post( 242 | reverse( 243 | 'posts:profile_follow', 244 | kwargs={'username': self.author}, 245 | ), 246 | follow=True 247 | ) 248 | self.assertEqual(response.status_code, HTTPStatus.OK) 249 | self.assertEqual( 250 | Follow.objects.filter(user=self.user, author=self.author).count(), 251 | followings + 1 252 | ) 253 | self.assertEqual( 254 | Follow.objects.filter(user=self.user, author=self.author).exists(), 255 | True 256 | ) 257 | 258 | def test_unfollow(self): 259 | """Авторизованный пользователь может отписываться.""" 260 | self.authorized_client.post( 261 | reverse( 262 | 'posts:profile_follow', 263 | kwargs={'username': self.author}, 264 | ), 265 | follow=True 266 | ) 267 | self.authorized_client.post( 268 | reverse( 269 | 'posts:profile_unfollow', 270 | kwargs={'username': self.author}, 271 | ), 272 | follow=True 273 | ) 274 | self.assertEqual( 275 | Follow.objects.filter(user=self.user, author=self.author).exists(), 276 | False 277 | ) 278 | 279 | def test_follow_page_context_following(self): 280 | """ 281 | Новая запись пользователя появляется в ленте тех, 282 | кто на него подписан. 283 | """ 284 | Follow.objects.create( 285 | user=self.user, author=self.author 286 | ) 287 | post = Post.objects.create( 288 | author=self.author, 289 | text='Текст поста' 290 | ) 291 | response = self.authorized_client.get( 292 | reverse('posts:follow_index') 293 | ) 294 | self.assertIn(post, response.context['page_obj']) 295 | 296 | def test_follow_page_context_not_following(self): 297 | """ 298 | Новая запись пользователя НЕ появляется в ленте тех, кто не подписан. 299 | """ 300 | post = Post.objects.create( 301 | author=self.author, 302 | text='Текст поста' 303 | ) 304 | response = self.authorized_client.get( 305 | reverse('posts:follow_index') 306 | ) 307 | self.assertNotIn(post, response.context['page_obj']) 308 | 309 | 310 | class PaginatorViewsTest(TestCase): 311 | @classmethod 312 | def setUpClass(cls): 313 | super().setUpClass() 314 | cls.author = User.objects.create_user( 315 | username='Author' 316 | ) 317 | cls.group = Group.objects.create( 318 | title='Тестовая группа', 319 | slug='test-slug', 320 | ) 321 | cls.posts = [] 322 | for i in range(1, 14): 323 | cls.posts.append(Post( 324 | text=f'Тестовый текст {i}', 325 | author=cls.author, 326 | group=cls.group, 327 | )) 328 | Post.objects.bulk_create(cls.posts) 329 | cls.first_page_objs = 10 330 | cls.second_page_objs = 3 331 | 332 | def setUp(self): 333 | self.client = Client() 334 | 335 | def test_first_page_contains_ten_records(self): 336 | pages_names = ( 337 | reverse('posts:index'), 338 | reverse('posts:group_list', kwargs={'slug': self.group.slug}), 339 | reverse('posts:profile', kwargs={'username': self.author.username}) 340 | ) 341 | for page in pages_names: 342 | with self.subTest(page=page): 343 | response = self.client.get(page) 344 | self.assertEqual( 345 | len(response.context['page_obj']), 346 | self.first_page_objs 347 | ) 348 | 349 | def test_second_page_contains_three_records(self): 350 | pages_names = ( 351 | reverse('posts:index') + '?page=2', 352 | reverse('posts:group_list', kwargs={'slug': self.group.slug}) 353 | + '?page=2', 354 | reverse('posts:profile', kwargs={'username': self.author.username}) 355 | + '?page=2', 356 | ) 357 | for page in pages_names: 358 | with self.subTest(page=page): 359 | response = self.client.get(page) 360 | self.assertEqual( 361 | len(response.context['page_obj']), 362 | self.second_page_objs 363 | ) 364 | --------------------------------------------------------------------------------