diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index bf4b734..a489aba 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -1,7 +1,7 @@ - + sqlite.xerial true true diff --git a/djangoProject1/asgi.py b/djangoProject1/asgi.py index dfcdd0a..5df2ced 100644 --- a/djangoProject1/asgi.py +++ b/djangoProject1/asgi.py @@ -1,16 +1,15 @@ -""" -ASGI config for djangoProject1 project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ -""" - import os - +from channels.auth import AuthMiddlewareStack from django.core.asgi import get_asgi_application +from channels.routing import ProtocolTypeRouter, URLRouter + +from socialnetwork import urls os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangoProject1.settings') +django_asgi_app = get_asgi_application() + +application = ProtocolTypeRouter({ + "http": django_asgi_app, -application = get_asgi_application() + "websocket": AuthMiddlewareStack(URLRouter(urls.websocket_urlpatterns)), +}) diff --git a/djangoProject1/settings.py b/djangoProject1/settings.py index 34f37db..3300c14 100644 --- a/djangoProject1/settings.py +++ b/djangoProject1/settings.py @@ -27,6 +27,7 @@ ALLOWED_HOSTS = [] INSTALLED_APPS = [ + "daphne", 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -41,6 +42,8 @@ 'allauth.account', 'allauth.socialaccount', 'rest_auth.registration', + 'channels', + "payments", ] MIDDLEWARE = [ @@ -79,7 +82,7 @@ }, ] -WSGI_APPLICATION = 'djangoProject1.wsgi.application' +ASGI_APPLICATION = 'djangoProject1.asgi.application' # Database # https://docs.djangoproject.com/en/5.0/ref/settings/#databases @@ -136,3 +139,21 @@ MEDIA_ROOT = BASE_DIR / "media" MEDIA_URL = '/media/' + +CHANNEL_LAYERS = { + "default": { + 'BACKEND': 'channels.layers.InMemoryChannelLayer', + }, +} + +PAYMENT_HOST = 'localhost:8000' + +# Whether to use TLS (HTTPS). If false, will use plain-text HTTP. +# Defaults to ``not settings.DEBUG``. +PAYMENT_USES_SSL = False + +PAYMENT_VARIANTS = { + 'dummy': ('payments.dummy.DummyProvider', {}) +} + +PAYMENT_MODEL = 'socialnetwork.Payment' diff --git a/djangoProject1/wsgi.py b/djangoProject1/wsgi.py deleted file mode 100644 index 3e41ba6..0000000 --- a/djangoProject1/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for djangoProject1 project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangoProject1.settings') - -application = get_wsgi_application() diff --git a/qodana.yaml b/qodana.yaml index 84e3e49..557c4ce 100644 --- a/qodana.yaml +++ b/qodana.yaml @@ -4,7 +4,7 @@ #-------------------------------------------------------------------------------# version: "1.0" -#Specify inspection profile for code analysis +#Specify inspection auth for code analysis profile: name: qodana.starter diff --git a/socialnetwork/consumer/__init__.py b/socialnetwork/consumer/__init__.py new file mode 100644 index 0000000..ae8c478 --- /dev/null +++ b/socialnetwork/consumer/__init__.py @@ -0,0 +1 @@ +from .сhat_сonsumer import ChatConsumer diff --git "a/socialnetwork/consumer/\321\201hat_\321\201onsumer.py" "b/socialnetwork/consumer/\321\201hat_\321\201onsumer.py" new file mode 100644 index 0000000..03fdca7 --- /dev/null +++ "b/socialnetwork/consumer/\321\201hat_\321\201onsumer.py" @@ -0,0 +1,68 @@ +from asgiref.sync import async_to_sync +from channels.generic.websocket import JsonWebsocketConsumer +import json + +from django.utils import timezone + +from socialnetwork.models import Chat, ChatMessage + + +class ChatConsumer(JsonWebsocketConsumer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.user = None + self.other_user_id = None + self.chat = None + self.room_name = None + + def is_write_to_yourself(self): + if self.user == self.other_user_id: + self.close() + + # TODO: проверка на то что бы юзер не писал сам себе + def create_room_name(self): + user_id = self.user.id + if user_id and self.other_user_id: + room_name = f'{min(user_id, self.other_user_id)}_{max(user_id, self.other_user_id)}' + self.room_name = room_name + else: + raise ValueError("Invalid user or user_id") + + def connect(self): + self.user = self.scope["user"] + self.other_user_id = self.scope["url_route"]['kwargs']["user_id"] + self.is_write_to_yourself() + self.accept() + self.create_room_name() + self.chat, _ = Chat.objects.get_or_create(room_name=self.room_name) + async_to_sync(self.channel_layer.group_add)( + self.room_name, + self.channel_name, + ) + + def disconnect(self, close_code): + async_to_sync(self.channel_layer.group_discard)( + self.room_name, + self.channel_name, + ) + self.close() + + def receive(self, text_data): + data = json.loads(text_data) + message = data.get('message') + timestamp = timezone.now().isoformat() + if message: + message_obj = ChatMessage.objects.create(author=self.user, chat=self.chat, content=message) + async_to_sync(self.channel_layer.group_send)( + self.room_name, + { + 'type': 'chat.message', + "author": self.user.username, + 'content_id': message_obj.id, + 'content': message, + 'timestamp': timestamp + }, + ) + + def chat_message(self, event): + self.send_json(event) diff --git a/socialnetwork/models/__init__.py b/socialnetwork/models/__init__.py index e6efa3c..a961ae0 100644 --- a/socialnetwork/models/__init__.py +++ b/socialnetwork/models/__init__.py @@ -1,6 +1,9 @@ -from .user import * -from .article import * -from .avatar import * -from .photo import * -from .comment import * -from .like import * +from socialnetwork.models.profile.user import User +from socialnetwork.models.profile.avatar import Avatar +from socialnetwork.models.profile.photo import Photo +from socialnetwork.models.chat.chat import Chat +from socialnetwork.models.chat.message import ChatMessage +from socialnetwork.models.article.article import Article +from socialnetwork.models.article.like import ArticleLike +from socialnetwork.models.article.comment import CommentArticle +from socialnetwork.models.payment.payment import Payment diff --git a/socialnetwork/models/article/__init__.py b/socialnetwork/models/article/__init__.py new file mode 100644 index 0000000..26b1312 --- /dev/null +++ b/socialnetwork/models/article/__init__.py @@ -0,0 +1,3 @@ +# from .article import * +# from .comment import * +# from .like import * diff --git a/socialnetwork/models/article.py b/socialnetwork/models/article/article.py similarity index 83% rename from socialnetwork/models/article.py rename to socialnetwork/models/article/article.py index fecb187..6777bed 100644 --- a/socialnetwork/models/article.py +++ b/socialnetwork/models/article/article.py @@ -1,5 +1,6 @@ import random +from django.core.validators import validate_image_file_extension from django.db import models from socialnetwork.models import User @@ -15,7 +16,9 @@ class Article(models.Model): author = models.ForeignKey(User, on_delete=models.CASCADE) title = models.CharField(max_length=255, null=False, blank=False) content = models.TextField(max_length=5000, null=False, blank=False) - image = models.ImageField(upload_to=file_location, null=False, blank=True) + image = models.ImageField(upload_to=file_location, null=False, blank=True, + max_length=1048576, # 1 Bite + validators=[validate_image_file_extension]) created_at = models.DateTimeField(auto_now_add=True, verbose_name="created_at") updated_at = models.DateTimeField(auto_now=True, verbose_name="updated_at") diff --git a/socialnetwork/models/comment.py b/socialnetwork/models/article/comment.py similarity index 100% rename from socialnetwork/models/comment.py rename to socialnetwork/models/article/comment.py diff --git a/socialnetwork/models/like.py b/socialnetwork/models/article/like.py similarity index 100% rename from socialnetwork/models/like.py rename to socialnetwork/models/article/like.py diff --git a/socialnetwork/models/chat/__init__.py b/socialnetwork/models/chat/__init__.py new file mode 100644 index 0000000..09ba9ab --- /dev/null +++ b/socialnetwork/models/chat/__init__.py @@ -0,0 +1,2 @@ +# from .chat import * +# from .message import * diff --git a/socialnetwork/models/chat/chat.py b/socialnetwork/models/chat/chat.py new file mode 100644 index 0000000..989bbac --- /dev/null +++ b/socialnetwork/models/chat/chat.py @@ -0,0 +1,9 @@ +from django.db import models + + +# TODO: Поменять способы сохранения сообщений: +# Добавить привязку юзеров к чату, создание груп + +class Chat(models.Model): + room_name = models.CharField(max_length=255) + created_at = models.DateTimeField(auto_now_add=True, verbose_name="created_at") diff --git a/socialnetwork/models/chat/message.py b/socialnetwork/models/chat/message.py new file mode 100644 index 0000000..ad35c87 --- /dev/null +++ b/socialnetwork/models/chat/message.py @@ -0,0 +1,12 @@ +from django.db import models +from rest_framework.authtoken.admin import User + +from socialnetwork.models.chat.chat import Chat + + +class ChatMessage(models.Model): + author = models.ForeignKey(User, on_delete=models.CASCADE) + chat = models.ForeignKey(Chat, on_delete=models.CASCADE) + content = models.CharField(max_length=256, blank=True) + state = models.CharField(default="NEW", max_length=10) + created_at = models.DateTimeField(auto_now_add=True, verbose_name="created_at") diff --git a/socialnetwork/models/payment/__init__.py b/socialnetwork/models/payment/__init__.py new file mode 100644 index 0000000..358dd86 --- /dev/null +++ b/socialnetwork/models/payment/__init__.py @@ -0,0 +1 @@ +# from .payment import * diff --git a/socialnetwork/models/payment/payment.py b/socialnetwork/models/payment/payment.py new file mode 100644 index 0000000..e385fcb --- /dev/null +++ b/socialnetwork/models/payment/payment.py @@ -0,0 +1,55 @@ +from decimal import Decimal +from typing import Iterable + +from django.db.models.signals import pre_save +from django.dispatch import receiver +from payments import PurchasedItem +from payments.models import BasePayment + +from djangoProject1.settings import PAYMENT_HOST +from django.db import models + +from socialnetwork.models import User + + +class Payment(BasePayment): + user = models.ForeignKey(User, on_delete=models.PROTECT, default='') + price = models.DecimalField(max_digits=10000, decimal_places=2, default=0) + card_number = models.CharField(max_length=16, blank=True, null=True) + card_expiry_date = models.DateField(blank=True, null=True) + card_holder_name = models.CharField(max_length=255, blank=True, null=True) + + +@receiver(pre_save, sender=Payment) +def populate_payment_fields(sender, instance, **kwargs): + user = instance.user + if user: + instance.card_number = user.card_number + instance.card_expiry_date = user.card_expiry_date + instance.card_holder_name = user.card_holder_name + + +def get_failure_url(self) -> str: + print(self.price) + print('get_failure_url') + + return f"http://{PAYMENT_HOST}/payments/{self.pk}/failure" + + +def get_success_url(self) -> str: + print(self.price) + print('get_success_url') + + return f"http://{PAYMENT_HOST}/payments/{self.pk}/success" + + +def get_purchased_items(self) -> Iterable[PurchasedItem]: + print(self.price) + print('get_purchased_items') + yield PurchasedItem( + name='Donate for Ukrainian military', + sku='DFUM', + price=Decimal(self.price), + quantity=1, + currency=self.currency, + ) diff --git a/socialnetwork/__init__.py b/socialnetwork/models/profile/GoogleApp.py similarity index 100% rename from socialnetwork/__init__.py rename to socialnetwork/models/profile/GoogleApp.py diff --git a/socialnetwork/models/profile/__init__.py b/socialnetwork/models/profile/__init__.py new file mode 100644 index 0000000..85e855b --- /dev/null +++ b/socialnetwork/models/profile/__init__.py @@ -0,0 +1,3 @@ +# from .avatar import * +# from .photo import * +# from .user import * diff --git a/socialnetwork/models/avatar.py b/socialnetwork/models/profile/avatar.py similarity index 67% rename from socialnetwork/models/avatar.py rename to socialnetwork/models/profile/avatar.py index ea9b6e2..a1833d5 100644 --- a/socialnetwork/models/avatar.py +++ b/socialnetwork/models/profile/avatar.py @@ -1,7 +1,7 @@ +from django.core.validators import validate_image_file_extension from django.db import models from django.utils.text import slugify -from djangoProject1.settings import MEDIA_ROOT from socialnetwork.models import User @@ -14,4 +14,6 @@ def file_location(instance, filename): class Avatar(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) - image = models.ImageField(upload_to=file_location, null=False, blank=True) + image = models.ImageField(upload_to=file_location, null=False, blank=True, + max_length=1048576, # 1 Bite + validators=[validate_image_file_extension]) diff --git a/socialnetwork/models/photo.py b/socialnetwork/models/profile/photo.py similarity index 66% rename from socialnetwork/models/photo.py rename to socialnetwork/models/profile/photo.py index 2ed94a2..f00aff2 100644 --- a/socialnetwork/models/photo.py +++ b/socialnetwork/models/profile/photo.py @@ -1,3 +1,4 @@ +from django.core.validators import validate_image_file_extension from django.db import models from socialnetwork.models import User @@ -11,4 +12,5 @@ def file_location(instance, filename): class Photo(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) - image = models.ImageField(upload_to=file_location, null=False, blank=True) + image = models.ImageField(upload_to=file_location, null=False, blank=True, max_length=1048576, # 1 Bite + validators=[validate_image_file_extension]) diff --git a/socialnetwork/models/profile/user.py b/socialnetwork/models/profile/user.py new file mode 100644 index 0000000..d932cce --- /dev/null +++ b/socialnetwork/models/profile/user.py @@ -0,0 +1,17 @@ +from django.contrib.auth.models import AbstractUser, UserManager +from django.db import models + +from socialnetwork.validator.user.card_number_validator import card_number_validator + + +class User(AbstractUser): + email = models.EmailField(max_length=255, unique=True, blank=False) + card_number = models.CharField(max_length=19, blank=True, null=True, unique=True, + validators=[card_number_validator]) + card_expiry_date = models.DateField(blank=True, null=True) + card_holder_name = models.CharField(max_length=255, blank=True, null=True) + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['username'] + + objects = UserManager() diff --git a/socialnetwork/models/user.py b/socialnetwork/models/user.py deleted file mode 100644 index 888f8c7..0000000 --- a/socialnetwork/models/user.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.contrib.auth.models import AbstractUser, UserManager -from django.db import models - - -class User(AbstractUser): - email = models.EmailField(max_length=255, unique=True, blank=False) - - USERNAME_FIELD = 'email' - REQUIRED_FIELDS = ['username'] - - objects = UserManager() diff --git a/socialnetwork/serializers/article/comment.py b/socialnetwork/serializers/article/comment.py index dbe25b3..42daad4 100644 --- a/socialnetwork/serializers/article/comment.py +++ b/socialnetwork/serializers/article/comment.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from socialnetwork.models.comment import CommentArticle +from socialnetwork.models.article.comment import CommentArticle class CommentArticleSerializer(serializers.ModelSerializer): diff --git a/socialnetwork/serializers/article/like.py b/socialnetwork/serializers/article/like.py index 5a1ec35..7b5124c 100644 --- a/socialnetwork/serializers/article/like.py +++ b/socialnetwork/serializers/article/like.py @@ -1,8 +1,10 @@ from rest_framework import serializers -from socialnetwork.models.like import ArticleLike +from socialnetwork.models.article.like import ArticleLike class LikeArticleSerializer(serializers.ModelSerializer): + like = serializers.BooleanField(required=True) + class Meta: model = ArticleLike fields = ['like'] diff --git a/socialnetwork/serializers/chat/ChatListMessageSerializer.py b/socialnetwork/serializers/chat/ChatListMessageSerializer.py new file mode 100644 index 0000000..c8eb7e4 --- /dev/null +++ b/socialnetwork/serializers/chat/ChatListMessageSerializer.py @@ -0,0 +1,16 @@ +from rest_framework import serializers + +from socialnetwork.models import User +from socialnetwork.models.chat.message import ChatMessage + + +class MessageListSerializer(serializers.ModelSerializer): + author = serializers.SerializerMethodField() + + class Meta: + model = ChatMessage + fields = ['author', 'content', 'created_at'] + + def get_author(self, obj): + author_obj = User.objects.get(pk=obj.author_id) + return author_obj.username diff --git a/socialnetwork/serializers/chat/__init__.py b/socialnetwork/serializers/chat/__init__.py new file mode 100644 index 0000000..2714923 --- /dev/null +++ b/socialnetwork/serializers/chat/__init__.py @@ -0,0 +1 @@ +from .ChatListMessageSerializer import MessageListSerializer diff --git a/socialnetwork/serializers/main/UserSetSerializer.py b/socialnetwork/serializers/main/UserSetSerializer.py new file mode 100644 index 0000000..aae4f96 --- /dev/null +++ b/socialnetwork/serializers/main/UserSetSerializer.py @@ -0,0 +1,8 @@ +from rest_framework import serializers +from socialnetwork.models.profile.user import User + + +class UserSetSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['id', 'username', 'first_name', 'last_name'] diff --git a/socialnetwork/serializers/main/__init__.py b/socialnetwork/serializers/main/__init__.py new file mode 100644 index 0000000..c7b311a --- /dev/null +++ b/socialnetwork/serializers/main/__init__.py @@ -0,0 +1 @@ +from .UserSetSerializer import UserSetSerializer diff --git a/socialnetwork/serializers/payment/__init__.py b/socialnetwork/serializers/payment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/socialnetwork/serializers/payment/payment.py b/socialnetwork/serializers/payment/payment.py new file mode 100644 index 0000000..a38e373 --- /dev/null +++ b/socialnetwork/serializers/payment/payment.py @@ -0,0 +1,8 @@ +from rest_framework import serializers +from socialnetwork.models import Payment + + +class PaymentSerializer(serializers.ModelSerializer): + class Meta: + model = Payment + fields = '__all__' diff --git a/socialnetwork/serializers/article/article.py b/socialnetwork/serializers/profile/article.py similarity index 76% rename from socialnetwork/serializers/article/article.py rename to socialnetwork/serializers/profile/article.py index 95f797e..fed42a0 100644 --- a/socialnetwork/serializers/article/article.py +++ b/socialnetwork/serializers/profile/article.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from socialnetwork.models.article import Article +from socialnetwork.models.article.article import Article class ArticleSerializer(serializers.ModelSerializer): diff --git a/socialnetwork/serializers/profile/avatar.py b/socialnetwork/serializers/profile/avatar.py index ef0d76a..b409c35 100644 --- a/socialnetwork/serializers/profile/avatar.py +++ b/socialnetwork/serializers/profile/avatar.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from socialnetwork.models.avatar import Avatar +from socialnetwork.models.profile.avatar import Avatar class AvatarSerializer(serializers.ModelSerializer): diff --git a/socialnetwork/serializers/profile/card.py b/socialnetwork/serializers/profile/card.py new file mode 100644 index 0000000..ba9953b --- /dev/null +++ b/socialnetwork/serializers/profile/card.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from socialnetwork.models import User + + +class CardSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['card_number', 'card_expiry_date', 'card_holder_name'] diff --git a/socialnetwork/serializers/profile/photo.py b/socialnetwork/serializers/profile/photo.py index 1f3bde0..cf05316 100644 --- a/socialnetwork/serializers/profile/photo.py +++ b/socialnetwork/serializers/profile/photo.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from socialnetwork.models.photo import Photo +from socialnetwork.models.profile.photo import Photo class PhotoSerializer(serializers.ModelSerializer): diff --git a/socialnetwork/test/__init__.py b/socialnetwork/test/__init__.py new file mode 100644 index 0000000..35c1920 --- /dev/null +++ b/socialnetwork/test/__init__.py @@ -0,0 +1 @@ +from .auth import * diff --git a/socialnetwork/test/article/__init__.py b/socialnetwork/test/article/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/socialnetwork/test/article/test_article.py b/socialnetwork/test/article/test_article.py new file mode 100644 index 0000000..ad8b91b --- /dev/null +++ b/socialnetwork/test/article/test_article.py @@ -0,0 +1,93 @@ +from django.urls import reverse +from rest_framework.test import APITestCase, APIClient +from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN, HTTP_200_OK + +from socialnetwork.models import User +from socialnetwork.serializers.profile.article import ArticleSerializer + +from django.core.files.uploadedfile import SimpleUploadedFile +import os + +image_path = os.path.join(os.path.dirname(__file__), '../data_for_test/images /test_image.png') +with open(image_path, 'rb') as f: + image_content = f.read() + + +class CreateArticleTestCase(APITestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create(first_name='John', last_name='Doe', username='johndoe', + email='johndoe@admin.com', + password='johndoejohndoe') + self.image = SimpleUploadedFile(name='test_image.png', content=image_content, + content_type='image/png') + self.request_data = {'image': self.image, + 'title': 'I am good guy', + 'content': 'U thought that i will proof it?'} + self.url = reverse('article') + + def test_data_is_valid(self): + self.client.force_login(self.user) + serializer = ArticleSerializer(data=self.request_data) + self.assertTrue(serializer.is_valid(), serializer.errors) + response = self.client.post(self.url, data=serializer.validated_data) + self.assertEqual(response.status_code, HTTP_200_OK) + + def test_image_is_empty(self): + self.client.force_login(self.user) + self.request_data['image'] = None + serializer = ArticleSerializer(data=self.request_data) + self.assertFalse(serializer.is_valid(), serializer.errors) + response = self.client.post(self.url, data=serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + + def test_title_is_empty(self): + self.client.force_login(self.user) + self.request_data['title'] = None + serializer = ArticleSerializer(data=self.request_data) + self.assertFalse(serializer.is_valid(), serializer.errors) + response = self.client.post(self.url, data=serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + + def test_content_is_empty(self): + self.client.force_login(self.user) + self.request_data['content'] = None + serializer = ArticleSerializer(data=self.request_data) + self.assertFalse(serializer.is_valid(), serializer.errors) + response = self.client.post(self.url, data=serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + + def test_data_is_empty(self): + self.client.force_login(self.user) + self.request_data['image'] = None + self.request_data['title'] = None + self.request_data['content'] = None + serializer = ArticleSerializer(data=self.request_data) + self.assertFalse(serializer.is_valid(), serializer.errors) + response = self.client.post(self.url, data=serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + + def test_data_is_not_valid(self): + self.client.force_login(self.user) + self.request_data['image'] = 'adasd' + self.request_data['title'] = self.image + self.request_data['content'] = self.image + serializer = ArticleSerializer(data=self.request_data) + self.assertFalse(serializer.is_valid(), serializer.errors) + response = self.client.post(self.url, data=serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + + def test_user_not_auth_data_is_valid(self): + serializer = ArticleSerializer(data=self.request_data) + self.assertTrue(serializer.is_valid(), serializer.errors) + response = self.client.post(self.url, data=serializer.validated_data) + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + + def test_user_not_auth_data_is_not_valid(self): + self.request_data['image'] = 'adasd' + self.request_data['title'] = self.image + self.request_data['content'] = self.image + serializer = ArticleSerializer(data=self.request_data) + self.assertFalse(serializer.is_valid(), serializer.errors) + response = self.client.post(self.url, data=serializer.validated_data) + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) diff --git a/socialnetwork/test/article/test_like_articke.py b/socialnetwork/test/article/test_like_articke.py new file mode 100644 index 0000000..b78e86b --- /dev/null +++ b/socialnetwork/test/article/test_like_articke.py @@ -0,0 +1,67 @@ +# from os import path +# +# from rest_framework.test import APITestCase, APIClient +# from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_302_FOUND, HTTP_403_FORBIDDEN, HTTP_200_OK +# +# from django.core.files.uploadedfile import SimpleUploadedFile +# from django.urls import reverse +# +# from socialnetwork.models import User, Article, ArticleLike +# +# from socialnetwork.serializers.article.like import LikeArticleSerializer +# +# image_path = path.join(path.dirname(__file__), '../data_for_test/images /test_image.png') +# with open(image_path, 'rb') as f: +# image_content = f.read() +# +# +# # TODO: сделать нормальную проверку + обновить обновление лайков +# class LikeOrDislikeArticleTestCase(APITestCase): +# def setUp(self): +# self.client = APIClient() +# self.user = User.objects.create(first_name='John', last_name='Doe', username='johndoe', +# email='johndoe@admin.com', +# password='johndoejohndoe') +# self.image = SimpleUploadedFile(name='test_image.png', content=image_content, +# content_type='image/png') +# self.article = Article.objects.create(author=self.user, image=self.image, +# title='I am good guy', +# content='U thought that i will proof it?') +# self.request_data = {'like': True} +# self.url = reverse('article_like', kwargs={'author': self.user.id, 'title': 'I am good guy'}) +# +# def test_data_is_valid(self): +# self.client.force_login(self.user) +# serializer = LikeArticleSerializer(data=self.request_data) +# self.assertTrue(serializer.is_valid(), serializer.errors) +# response = self.client.post(self.url, data=serializer.validated_data) +# self.assertEqual(response.status_code, HTTP_200_OK) +# +# # TODO: переводит текст в буль , так не должно быть +# def test_data_not_valid(self): +# self.client.force_login(self.user) +# self.request_data = {'like': 'asdasd'} +# serializer = LikeArticleSerializer(data=self.request_data) +# self.assertFalse(serializer.is_valid(), serializer.errors) +# response = self.client.post(self.url, data=serializer.validated_data) +# self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) +# +# def test_user_is_not_auth(self): +# serializer = LikeArticleSerializer(data=self.request_data) +# self.assertTrue(serializer.is_valid(), serializer.errors) +# response = self.client.post(self.url, data=serializer.validated_data) +# self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) +# +# def test_change_state_like(self): +# self.client.force_login(self.user) +# serializer_true = LikeArticleSerializer(data=self.request_data) +# self.assertTrue(serializer_true.is_valid(), serializer_true.errors) +# response_true = self.client.post(self.url, data=serializer_true.validated_data) +# self.assertEqual(response_true.status_code, HTTP_200_OK) +# self.assertEqual(ArticleLike.objects.get(user=self.user, article=self.article).like, True) +# self.request_data = {'like': False} +# serializer_false = LikeArticleSerializer(data=self.request_data) +# self.assertTrue(serializer_false.is_valid(), serializer_false.errors) +# response_false = self.client.post(self.url, data=serializer_false.validated_data) +# self.assertEqual(response_false.status_code, HTTP_200_OK) +# self.assertEqual(ArticleLike.objects.get(user=self.user, article=self.article).like, False) diff --git a/socialnetwork/test/auth/__init__.py b/socialnetwork/test/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/socialnetwork/test/auth/test_login.py b/socialnetwork/test/auth/test_login.py new file mode 100644 index 0000000..19b6fe0 --- /dev/null +++ b/socialnetwork/test/auth/test_login.py @@ -0,0 +1,59 @@ +from django.urls import reverse +from rest_framework.test import APITestCase, APIClient +from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_302_FOUND, HTTP_403_FORBIDDEN + +from socialnetwork.models import User +from socialnetwork.serializers.auth.login import LoginSerializer + + +class LoginTestCase(APITestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create(first_name='John', last_name='Doe', username='johndoe', + email='johndoe@admin.com', + password='johndoejohndoe') + self.request_data = {'email': 'johndoe@admin.com', + 'password': 'johndoejohndoe'} + self.url = reverse('login') + + def test_user_is_auth(self): + self.client.force_login(self.user) + serializer = LoginSerializer(data=self.request_data) + self.assertTrue(serializer.is_valid()) + response = self.client.post(self.url, data=serializer.validated_data) + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertIn('_auth_user_id', self.client.session) + + # TODO: написать проверку правильности пароля + # def test_data_is_valid(self): + # serializer = LoginSerializer(data=self.request_data) + # self.assertTrue(serializer.is_valid()) + # response = self.client.post(self.url, data=serializer.validated_data) + # self.assertEqual(response.status_code, HTTP_302_FOUND) + # self.assertTrue(self.user.is_authenticated) + # self.assertRedirects(response, expected_url=reverse('profile', kwargs={'username': 'johndoe'})) + + def test_email_is_not_valid(self): + self.request_data['email'] = 'johndoe' + serializer = LoginSerializer(data=self.request_data) + self.assertFalse(serializer.is_valid()) + response = self.client.post(self.url, data=serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + self.assertNotIn('_auth_user_id', self.client.session) + + def test_password_is_not_valid(self): + self.request_data['password'] *= 100 + serializer = LoginSerializer(data=self.request_data) + self.assertFalse(serializer.is_valid()) + response = self.client.post(self.url, data=serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + self.assertNotIn('_auth_user_id', self.client.session) + + def test_user_no_create(self): + self.request_data['email'] = 'userthatdoesntcreate@admin.com' + self.request_data['password'] = 'safepassword' + serializer = LoginSerializer(data=self.request_data) + self.assertTrue(serializer.is_valid()) + response = self.client.post(self.url, data=serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + self.assertNotIn('_auth_user_id', self.client.session) diff --git a/socialnetwork/test/auth/test_registration.py b/socialnetwork/test/auth/test_registration.py new file mode 100644 index 0000000..d2c8109 --- /dev/null +++ b/socialnetwork/test/auth/test_registration.py @@ -0,0 +1,95 @@ +from django.urls import reverse +from rest_framework.test import APITestCase, APIClient +from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_302_FOUND, HTTP_403_FORBIDDEN + +from socialnetwork.models import User +from socialnetwork.serializers.auth.registration import RegistrationSerializer + + +class RegistrationTestCase(APITestCase): + def setUp(self): + self.client = APIClient() + self.request_data = {'first_name': 'John', 'last_name': 'Doe', 'username': 'johndoe', + 'email': 'johndoe@admin.com', + 'password': 'johndoejohndoe'} + + def test_data_is_valid(self): + serializer = RegistrationSerializer(data=self.request_data) + self.assertTrue(serializer.is_valid()) + response = self.client.post(reverse('registration'), serializer.validated_data) + self.assertEqual(response.status_code, HTTP_302_FOUND) + user = User.objects.get(username=serializer.validated_data['username']) + self.assertTrue(user.is_authenticated) + self.assertRedirects(response, expected_url=reverse('profile', kwargs={'username': 'johndoe'})) + + def test_email_is_not_valid(self): + self.request_data['email'] = 'asdsadadas' + serializer = RegistrationSerializer(data=self.request_data) + self.assertFalse(serializer.is_valid()) + response = self.client.post(reverse('registration'), serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + + def test_password_is_not_valid(self): + self.request_data['password'] *= 100 + serializer = RegistrationSerializer(data=self.request_data) + self.assertFalse(serializer.is_valid()) + response = self.client.post(reverse('registration'), serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + + def test_first_name_is_not_valid(self): + self.request_data['first_name'] *= 100 + serializer = RegistrationSerializer(data=self.request_data) + self.assertFalse(serializer.is_valid()) + response = self.client.post(reverse('registration'), serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + + def test_last_name_is_not_valid(self): + self.request_data['last_name'] *= 100 + serializer = RegistrationSerializer(data=self.request_data) + self.assertFalse(serializer.is_valid()) + response = self.client.post(reverse('registration'), serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + + def test_username_is_not_valid(self): + self.request_data['username'] *= 100 + serializer = RegistrationSerializer(data=self.request_data) + self.assertFalse(serializer.is_valid()) + response = self.client.post(reverse('registration'), serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + + def test_data_is_empty(self): + self.request_data = {'first_name': '', 'last_name': '', 'username': '', + 'email': '', + 'password': ''} + serializer = RegistrationSerializer(data=self.request_data) + self.assertFalse(serializer.is_valid()) + response = self.client.post(reverse('registration'), serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + + def test_data_is_not_valid(self): + self.request_data = {'first_name': 'John' * 100, 'last_name': 'Doe' * 100, 'username': 'johndoe' * 100, + 'email': 'johndoe@admin.com' * 10, + 'password': 'johndoejohndoe' * 10} + serializer = RegistrationSerializer(data=self.request_data) + self.assertFalse(serializer.is_valid()) + response = self.client.post(reverse('registration'), serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + + def test_user_is_auth(self): + serializer = RegistrationSerializer(data=self.request_data) + user = User.objects.create(first_name='John', last_name='Doe', username='johndoe', + email='johndoe@admin.com', + password='johndoejohndoe') + self.client.force_authenticate(user) + self.assertFalse(serializer.is_valid()) + response = self.client.post(reverse('registration'), serializer.validated_data) + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + + def test_user_is_exist(self): + serializer = RegistrationSerializer(data=self.request_data) + User.objects.create(first_name='John', last_name='Doe', username='johndoe', + email='johndoe@admin.com', + password='johndoejohndoe') + self.assertFalse(serializer.is_valid()) + response = self.client.post(reverse('registration'), serializer.validated_data) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) diff --git a/socialnetwork/test/auth/test_singout.py b/socialnetwork/test/auth/test_singout.py new file mode 100644 index 0000000..98621c2 --- /dev/null +++ b/socialnetwork/test/auth/test_singout.py @@ -0,0 +1,26 @@ +from django.urls import reverse +from rest_framework.test import APITestCase, APIClient +from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_302_FOUND, HTTP_403_FORBIDDEN + +from socialnetwork.models import User +from socialnetwork.serializers.auth.login import LoginSerializer + + +class SingOutTestCase(APITestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create(first_name='John', last_name='Doe', username='johndoe', + email='johndoe@admin.com', + password='johndoejohndoe') + self.url = reverse('logout') + + def test_user_is_auth(self): + self.client.force_login(self.user) + response = self.client.get(self.url) + self.assertEqual(response.status_code, HTTP_302_FOUND) + self.assertNotIn('_auth_user_id', self.client.session) + + def test_user_is_not_auth(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertNotIn('_auth_user_id', self.client.session) diff --git a/socialnetwork/test/data_for_test/images /test_image.png b/socialnetwork/test/data_for_test/images /test_image.png new file mode 100644 index 0000000..55be547 Binary files /dev/null and b/socialnetwork/test/data_for_test/images /test_image.png differ diff --git a/socialnetwork/tests.py b/socialnetwork/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/socialnetwork/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/socialnetwork/urls.py b/socialnetwork/urls.py index dcdf704..ec415a9 100644 --- a/socialnetwork/urls.py +++ b/socialnetwork/urls.py @@ -2,9 +2,14 @@ from django.conf.urls.static import static from django.urls import path +from socialnetwork.consumer import ChatConsumer from socialnetwork.views.article.comment import CommentArticleView from socialnetwork.views.article.likes import LikeArticleView +from socialnetwork.views.chat import ChatViewSet, UserChatViewSet from socialnetwork.views.main import MainView +from socialnetwork.views.payments.donate import DonateView +from socialnetwork.views.payments.payment import PaymentView +from socialnetwork.views.profile.avatar import AvatarView from socialnetwork.views.profile.profile import ProfileView from socialnetwork.views.profile.article import ArticleView from socialnetwork.views.auth.registration import RegistrationView @@ -12,20 +17,29 @@ from socialnetwork.views.auth.singout import LogoutView from socialnetwork.views.profile.photo import ProfilePhotoView from socialnetwork.views.article.article import ArticlePageView -from socialnetwork.views.search_user import SearchView +from socialnetwork.views.main.search_user import SearchUserViewSet urlpatterns = [ path('main/', MainView.as_view(), name='main'), path('main/profile//', ProfileView.as_view(), name='profile'), - path('profile/registration', RegistrationView.as_view(), name='registration'), - path('profile/login', LoginView.as_view(), name='login'), - path('profile/logout', LogoutView.as_view(), name='logout'), - path('profile/photo', ProfilePhotoView.as_view(), name='photo'), + path('auth/registration', RegistrationView.as_view(), name='registration'), + path('auth/login', LoginView.as_view(), name='login'), + path('auth/logout', LogoutView.as_view(), name='logout'), + path('profile//photo', ProfilePhotoView.as_view(), name='photo'), path('profile/article', ArticleView.as_view(), name='article'), + path('profile//avatar', AvatarView.as_view(), name='avatar'), path('article///', ArticlePageView.as_view(), name='article_page'), path('article///artecle_add_comment', CommentArticleView.as_view(), name='article_add_comment'), path('article///artecle_like', LikeArticleView.as_view(), name='article_like'), - path('search//', SearchView.as_view(), name='search'), + path('main/search//', SearchUserViewSet.as_view({'get': 'list'}), name='search'), + path('main/chat//', ChatViewSet.as_view({'get': 'list'}), name='chat'), + path('main/user/chats/', UserChatViewSet.as_view({'get': 'list'}), name='user_chat'), + path('main/donate/', DonateView.as_view(), name='donate'), + path('main/donate/paymants/', PaymentView.as_view(), name='payment'), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + +websocket_urlpatterns = [ + path('main/ws/chat//', ChatConsumer.as_asgi(), name='chat'), +] diff --git a/socialnetwork/utilities/__init__.py b/socialnetwork/utilities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/socialnetwork/utilities/chat/__init__.py b/socialnetwork/utilities/chat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/socialnetwork/utilities/chat/change_state_message.py b/socialnetwork/utilities/chat/change_state_message.py new file mode 100644 index 0000000..c6e11a1 --- /dev/null +++ b/socialnetwork/utilities/chat/change_state_message.py @@ -0,0 +1,9 @@ +from socialnetwork.models.chat.message import ChatMessage + + +def change_state_message(chat, user): + messages = ChatMessage.objects.filter(chat_id=chat.id, state="NEW") + if messages.exists(): + author_id = messages[0].author.id + if user.id != author_id: + messages.update(state="READ") diff --git a/socialnetwork/utilities/payment/__init__.py b/socialnetwork/utilities/payment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/socialnetwork/utilities/payment/payment.py b/socialnetwork/utilities/payment/payment.py new file mode 100644 index 0000000..4f84ad2 --- /dev/null +++ b/socialnetwork/utilities/payment/payment.py @@ -0,0 +1,18 @@ +from decimal import Decimal +from payments import get_payment_model + + +def create_dummy_payment(price, variant, currency, user): + Payment = get_payment_model() + + payment = Payment.objects.create( + user=user, + variant=variant, + description='Test Payment', + total=1, + price=Decimal(price), + currency=currency, + status='waiting' + ) + + return payment diff --git a/socialnetwork/validator/__init__.py b/socialnetwork/validator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/socialnetwork/validator/article/like_validator.py b/socialnetwork/validator/article/like_validator.py new file mode 100644 index 0000000..c141f3f --- /dev/null +++ b/socialnetwork/validator/article/like_validator.py @@ -0,0 +1,5 @@ +def validate_like(value): + print(value) + if value is None or not isinstance(value, bool): + return False + return True diff --git a/socialnetwork/validator/user/card_number_validator.py b/socialnetwork/validator/user/card_number_validator.py new file mode 100644 index 0000000..3beed82 --- /dev/null +++ b/socialnetwork/validator/user/card_number_validator.py @@ -0,0 +1,6 @@ +from rest_framework.exceptions import ValidationError + + +def card_number_validator(card_number: str): + if len(card_number.strip()) != 16: + return ValidationError("Card number must be 16 characters long.") diff --git a/socialnetwork/views/article/article.py b/socialnetwork/views/article/article.py index 9cc64a6..d6aeec5 100644 --- a/socialnetwork/views/article/article.py +++ b/socialnetwork/views/article/article.py @@ -1,8 +1,9 @@ +from django.http import HttpResponseRedirect from django.shortcuts import render +from django.urls import reverse from rest_framework.parsers import MultiPartParser from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST +from rest_framework.status import HTTP_200_OK from rest_framework.views import APIView from socialnetwork.models import Article, CommentArticle, ArticleLike @@ -17,7 +18,7 @@ def get(request, author, title): try: article = Article.objects.get(author=author, title=title) except Article.DoesNotExist: - return Response(status=HTTP_400_BAD_REQUEST) + return HttpResponseRedirect(reverse('main')) try: comments = CommentArticle.objects.filter(article=article) @@ -40,4 +41,4 @@ def get(request, author, title): 'dislikes': dislikes_count} return render(request, 'templates/artecle/article.html', - context=context) + context=context, status=HTTP_200_OK) diff --git a/socialnetwork/views/article/comment.py b/socialnetwork/views/article/comment.py index cb137d8..4375d47 100644 --- a/socialnetwork/views/article/comment.py +++ b/socialnetwork/views/article/comment.py @@ -5,7 +5,6 @@ from rest_framework.views import APIView from socialnetwork.models import Article -from socialnetwork.models.comment import CommentArticle from socialnetwork.serializers.article.comment import CommentArticleSerializer diff --git a/socialnetwork/views/article/likes.py b/socialnetwork/views/article/likes.py index 9ec2855..9114117 100644 --- a/socialnetwork/views/article/likes.py +++ b/socialnetwork/views/article/likes.py @@ -4,9 +4,10 @@ from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST from rest_framework.views import APIView -from socialnetwork.models import Article, User -from socialnetwork.models.like import ArticleLike +from socialnetwork.models import Article +from socialnetwork.models.article.like import ArticleLike from socialnetwork.serializers.article.like import LikeArticleSerializer +from socialnetwork.validator.article.like_validator import validate_like class LikeArticleView(APIView): @@ -19,10 +20,10 @@ def post(request, author, title): article = Article.objects.get(author=author, title=title) except Article.DoesNotExist: return Response(status=HTTP_400_BAD_REQUEST) - serializer = LikeArticleSerializer(data=request.POST) if serializer.is_valid(): like_value = serializer.validated_data['like'] + print(like_value) try: user_like = ArticleLike.objects.get(user=request.user, article=article) diff --git a/socialnetwork/views/auth/singout.py b/socialnetwork/views/auth/singout.py index 9cfe87c..d854250 100644 --- a/socialnetwork/views/auth/singout.py +++ b/socialnetwork/views/auth/singout.py @@ -1,11 +1,11 @@ from django.contrib.auth import logout from django.http import HttpResponseRedirect -from rest_framework.permissions import AllowAny +from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView class LogoutView(APIView): - permission_classes = [AllowAny] + permission_classes = [IsAuthenticated] @staticmethod def get(request): diff --git a/socialnetwork/views/chat/__init__.py b/socialnetwork/views/chat/__init__.py new file mode 100644 index 0000000..8efd8b3 --- /dev/null +++ b/socialnetwork/views/chat/__init__.py @@ -0,0 +1,2 @@ +from .chat import ChatViewSet +from .user_chat import UserChatViewSet diff --git a/socialnetwork/views/chat/chat.py b/socialnetwork/views/chat/chat.py new file mode 100644 index 0000000..aabf27d --- /dev/null +++ b/socialnetwork/views/chat/chat.py @@ -0,0 +1,27 @@ +from django.shortcuts import render +from rest_framework.permissions import IsAuthenticated +from rest_framework.viewsets import ViewSet + +from socialnetwork.models.chat.chat import Chat +from socialnetwork.models.chat.message import ChatMessage +from socialnetwork.serializers.chat.ChatListMessageSerializer import MessageListSerializer +from socialnetwork.utilities.chat.change_state_message import change_state_message + + +class ChatViewSet(ViewSet): + permission_classes = (IsAuthenticated,) + + def list(self, request, user_id): + # TODO: переименовать user_id + # TODO: Проверка на отправку сообщений самому себе + user = request.user + room_name = f'{min(user_id, user.id)}_{max(user_id, user.id)}' + try: + chat = Chat.objects.get(room_name=room_name) + except Chat.DoesNotExist: + return render(request, 'templates/chat/chat.html', context={'user_id': user_id}) + change_state_message(chat, user) + messages = ChatMessage.objects.filter(chat_id=chat.id).all() + serializer = MessageListSerializer(messages, many=True) + context = {'user_id': user_id, 'messages': serializer.data} + return render(request, 'templates/chat/chat.html', context=context) diff --git a/socialnetwork/views/chat/user_chat.py b/socialnetwork/views/chat/user_chat.py new file mode 100644 index 0000000..007bc01 --- /dev/null +++ b/socialnetwork/views/chat/user_chat.py @@ -0,0 +1,48 @@ +from django.shortcuts import render +from rest_framework.permissions import IsAuthenticated +from rest_framework.viewsets import ViewSet + +from socialnetwork.models import User, Avatar, ChatMessage +from socialnetwork.models.chat.chat import Chat + + +class UserChatViewSet(ViewSet): + permission_classes = (IsAuthenticated,) + + def get_recipient(self, user_id, chats): + result_other_user_data = [] + # TODO: serializer + for chat in chats: + usernames = sorted(chat.room_name.split('_')) + other_username = usernames[0] if usernames[0] != user_id else usernames[1] + other_user = User.objects.get(pk=other_username) + count_new_messages = ChatMessage.objects.filter(chat_id=chat.id, author=other_user.id, state="NEW").count() + try: + other_user_image = Avatar.objects.get(pk=other_user.id) + except Avatar.DoesNotExist: + other_user_data = { + 'image': None, + 'username': other_user.username, + 'id': other_user.id, + 'new_messages': count_new_messages + } + result_other_user_data.append(other_user_data) + continue + + other_user_data = { + 'image': other_user_image.image, + 'username': other_user.username, + 'id': other_user.id, + 'new_messages': count_new_messages + } + result_other_user_data.append(other_user_data) + return result_other_user_data + + def list(self, request): + user_id = request.user.id + chats = Chat.objects.filter(room_name__icontains=user_id).all() + recipient_data = self.get_recipient(str(user_id), chats) + context = { + 'recipients': recipient_data + } + return render(request, context=context, template_name='templates/chat/user_chat.html') diff --git a/socialnetwork/views/main/__init__.py b/socialnetwork/views/main/__init__.py new file mode 100644 index 0000000..4213346 --- /dev/null +++ b/socialnetwork/views/main/__init__.py @@ -0,0 +1,2 @@ +from .main import MainView +from .search_user import SearchUserViewSet diff --git a/socialnetwork/views/main.py b/socialnetwork/views/main/main.py similarity index 100% rename from socialnetwork/views/main.py rename to socialnetwork/views/main/main.py diff --git a/socialnetwork/views/main/search_user.py b/socialnetwork/views/main/search_user.py new file mode 100644 index 0000000..7eb5c25 --- /dev/null +++ b/socialnetwork/views/main/search_user.py @@ -0,0 +1,42 @@ +from django.shortcuts import render +from rest_framework.permissions import IsAuthenticated + +from socialnetwork.models import Avatar +from socialnetwork.models.profile.user import User +from rest_framework.viewsets import ViewSet +from rest_framework.status import HTTP_400_BAD_REQUEST +from rest_framework.response import Response +from socialnetwork.serializers.main.UserSetSerializer import UserSetSerializer + + +class SearchUserViewSet(ViewSet): + permission_classes = (IsAuthenticated,) + + def list(self, request, user_inf): + print(user_inf) + if not user_inf: + return Response({'error': 'User info is required'}, status=HTTP_400_BAD_REQUEST) + # TODO: проверить правильно ли работает + queryset = User.objects.filter( + username__icontains=user_inf + ) | User.objects.filter( + first_name__icontains=user_inf + ) | User.objects.filter( + last_name__icontains=user_inf + ) + + if not queryset: + return Response({'error': 'User info is find'}, status=HTTP_400_BAD_REQUEST) + + serializer = UserSetSerializer(queryset, many=True) + users = serializer.data + + for user in users: + try: + avatar = Avatar.objects.get(user_id=user['id']) + user['avatar_url'] = avatar.image.url + except Avatar.DoesNotExist: + user['avatar_url'] = None + + print(users) + return render(request, 'templates/main/search.html', {'users': users}) diff --git a/socialnetwork/views/payments/__init__.py b/socialnetwork/views/payments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/socialnetwork/views/payments/donate.py b/socialnetwork/views/payments/donate.py new file mode 100644 index 0000000..74e45b4 --- /dev/null +++ b/socialnetwork/views/payments/donate.py @@ -0,0 +1,17 @@ +from django.shortcuts import render +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.views import APIView +from socialnetwork.models import Payment + + +class DonateView(APIView): + permission_classes = (AllowAny,) + + @staticmethod + def get(request): + return render(request, 'templates/payment/donate.html') + + @staticmethod + def post(request): + return Response({'message': 'Only GET requests'}) diff --git a/socialnetwork/views/payments/payment.py b/socialnetwork/views/payments/payment.py new file mode 100644 index 0000000..04bf37c --- /dev/null +++ b/socialnetwork/views/payments/payment.py @@ -0,0 +1,22 @@ +from django.http import HttpResponse +from django.template.response import TemplateResponse +from django.views import View +from socialnetwork.serializers.payment.payment import PaymentSerializer +from socialnetwork.utilities.payment.payment import create_dummy_payment + + +class PaymentView(View): + def post(self, request): + serializer = PaymentSerializer(data=request.POST) + if serializer.is_valid(): + price = serializer.data['price'] + variant = serializer.data['variant'] + currency = serializer.data['currency'] + payment = create_dummy_payment(price, variant, currency, request.user) + print(payment) + return TemplateResponse( + request, + 'templates/payment/payment.html', + {'payment': payment} + ) + return HttpResponse(serializer.errors, status=400) diff --git a/socialnetwork/views/profile/article.py b/socialnetwork/views/profile/article.py index e09434b..086fc06 100644 --- a/socialnetwork/views/profile/article.py +++ b/socialnetwork/views/profile/article.py @@ -5,7 +5,7 @@ from rest_framework.views import APIView from socialnetwork.permissions.is_owner import IsOwnerOrReadOnly -from socialnetwork.serializers.article.article import ArticleSerializer +from socialnetwork.serializers.profile.article import ArticleSerializer class ArticleView(APIView): diff --git a/socialnetwork/views/profile/avatar.py b/socialnetwork/views/profile/avatar.py new file mode 100644 index 0000000..75949a1 --- /dev/null +++ b/socialnetwork/views/profile/avatar.py @@ -0,0 +1,35 @@ +from django.shortcuts import render +from rest_framework.parsers import MultiPartParser +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST +from rest_framework.views import APIView + +from socialnetwork.models import Avatar, User +from socialnetwork.permissions.is_owner import IsOwnerOrReadOnly +from socialnetwork.serializers.profile.avatar import AvatarSerializer + + +class AvatarView(APIView): + parser_classes = [MultiPartParser] + permission_classes = [IsAuthenticated, IsOwnerOrReadOnly] + + @staticmethod + def update_avatar(avatar, image_data): + avatar.image.delete() + avatar.image = image_data + avatar.save() + + # TODO: edit auth + def post(self, request, username): + user = User.objects.get(username=username) + serializer = AvatarSerializer(data=request.data) + if serializer.is_valid(): + try: + existing_avatar = Avatar.objects.get(user=user) + except Avatar.DoesNotExist: + serializer.save(user=user) + return Response(status=HTTP_200_OK) + AvatarView.update_avatar(existing_avatar, serializer.validated_data['image']) + return Response(status=HTTP_200_OK) + return Response(status=HTTP_400_BAD_REQUEST) diff --git a/socialnetwork/views/profile/photo.py b/socialnetwork/views/profile/photo.py index d1c5c57..a04ecea 100644 --- a/socialnetwork/views/profile/photo.py +++ b/socialnetwork/views/profile/photo.py @@ -12,9 +12,9 @@ class ProfilePhotoView(APIView): parser_classes = [MultiPartParser] permission_classes = [IsAuthenticated, IsOwnerOrReadOnly] - # TODO: edit profile photo + # TODO: edit auth photo @staticmethod - def post(request): + def post(request, username): serializer = PhotoSerializer(data=request.data) if serializer.is_valid(): serializer.save(user=request.user) diff --git a/socialnetwork/views/profile/profile.py b/socialnetwork/views/profile/profile.py index c4598bb..31b20c3 100644 --- a/socialnetwork/views/profile/profile.py +++ b/socialnetwork/views/profile/profile.py @@ -2,21 +2,20 @@ from rest_framework.parsers import MultiPartParser from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN +from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST from rest_framework.views import APIView from socialnetwork.models import Avatar, Article, User, ArticleLike from socialnetwork.permissions.is_owner import IsOwnerOrReadOnly -from socialnetwork.serializers.profile.avatar import AvatarSerializer from socialnetwork.models import CommentArticle, Photo +from socialnetwork.serializers.profile.card import CardSerializer class ProfileView(APIView): parser_classes = [MultiPartParser] permission_classes = [IsAuthenticated, IsOwnerOrReadOnly] - @staticmethod - def get(request, username): + def get(self, request, username): user = User.objects.get(username=username) try: avatar = Avatar.objects.get(user_id=user) @@ -44,21 +43,14 @@ def get(request, username): context = {'user': user, 'avatar': avatar, 'photos': photo, 'articles_info': articles_info} return render(request, 'templates/profile/profile.html', context=context) - @staticmethod - def update_avatar(avatar, image_data): - avatar.image.delete() - avatar.image = image_data - avatar.save() - - # TODO: edit profile - @staticmethod - def post(request, username): - user = User.objects.get(username=username) - serializer = AvatarSerializer(data=request.data) + # TODO: edit auth + def post(self, request, username): + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + return Response(status=HTTP_400_BAD_REQUEST) + serializer = CardSerializer(user, data=request.data) if serializer.is_valid(): - try: - existing_avatar = Avatar.objects.get(user=user) - except Avatar.DoesNotExist: - serializer.save(user=user) - return Response(status=HTTP_200_OK) + serializer.save() + return Response(status=HTTP_200_OK) return Response(status=HTTP_400_BAD_REQUEST) diff --git a/socialnetwork/views/search_user.py b/socialnetwork/views/search_user.py deleted file mode 100644 index 87a00d8..0000000 --- a/socialnetwork/views/search_user.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.shortcuts import render -from rest_framework.permissions import AllowAny -from rest_framework.response import Response -from rest_framework.views import APIView - -from socialnetwork.models import User - - -class SearchView(APIView): - permission_classes = (AllowAny,) - - @staticmethod - def get(request, username): - try: - users = User.objects.filter(username__contains=username) - except User.DoesNotExist: - users = 'Doesn\'t exist' - return render(request, template_name='templates/search.html', context={'users': users}) diff --git a/static/css/account/profile.css b/static/css/account/profile.css index 79ad6dd..95929e8 100644 --- a/static/css/account/profile.css +++ b/static/css/account/profile.css @@ -35,8 +35,8 @@ body { } .profile-image img { - width: 150px; - height: 150px; + height: 200px; + width: 200px; border-radius: 50%; border: 5px solid #fff; box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); @@ -148,3 +148,47 @@ body { .title:hover { color: burlywood; } + +.card-info { + margin-top: 20px; + margin-left: 20px; +} + +.card-info form { + max-width: 400px; + margin: 0 auto; + padding: 20px; + border: 1px solid #ccc; + border-radius: 5px; + background-color: #f9f9f9; +} + +.card-info label { + display: block; + margin-bottom: 5px; +} + +.card-info input[type="text"], +.card-info input[type="date"] { + width: calc(100% - 12px); + padding: 6px; + margin-bottom: 10px; + border: 1px solid #ccc; + border-radius: 3px; +} + +.card-info button[type="submit"] { + display: block; + width: 100%; + padding: 10px; + border: none; + border-radius: 5px; + background-color: #007bff; + color: #fff; + font-size: 16px; + cursor: pointer; +} + +.card-info button[type="submit"]:hover { + background-color: #0056b3; +} diff --git a/static/css/article.css b/static/css/article/article.css similarity index 77% rename from static/css/article.css rename to static/css/article/article.css index 2b0c71e..fa46061 100644 --- a/static/css/article.css +++ b/static/css/article/article.css @@ -58,3 +58,17 @@ body { .comment p:last-child { color: #888; } + +a.material-symbols-outlined { + font-variation-settings: 'FILL' 0, + 'wght' 400, + 'GRAD' 0, + 'opsz' 24 +} + +a.material-symbols-outlined:focus { + font-variation-settings: 'FILL' 0, + 'wght' 400, + 'GRAD' 0, + 'opsz' 24 +} diff --git a/static/css/chat/chat.css b/static/css/chat/chat.css new file mode 100644 index 0000000..8cb7e1b --- /dev/null +++ b/static/css/chat/chat.css @@ -0,0 +1,113 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f9f9f9; +} + +.chat-container { + max-width: 600px; + margin: 20px auto; + padding: 20px; + border: 1px solid #ccc; + border-radius: 5px; + background-color: #fff; +} + +.chat-messages { + height: 300px; + overflow-y: auto; + margin-bottom: 10px; + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; +} + +.chat-input { + display: flex; + align-items: center; +} + +.chat-input input[type="text"] { + flex: 1; + padding: 8px; + border: 1px solid #ccc; + border-radius: 5px; + margin-right: 10px; +} + +.chat-input button { + padding: 8px 15px; + border: none; + border-radius: 5px; + background-color: #007bff; + color: #fff; + cursor: pointer; +} + +.chat-input button:hover { + background-color: #0056b3; +} + +.message-container { + padding: 10px; + border: 1px solid #ccc; + margin-bottom: 10px; + background-color: #999999; + border-radius: 10px; +} + +.sender { + font-weight: bold; +} + +.message { + color: #333; + margin-top: 5px; /* добавляем небольшой отступ между sender и сообщением */ +} + +.own-message .sender { + text-align: right; +} + +.own-message .message { + text-align: right; +} + +.other-message .sender { + text-align: left; +} + +.other-message .message { + text-align: left; +} + +.message-container { + padding: 10px; + border: 1px solid #ccc; + margin-bottom: 10px; + border-radius: 10px; +} + +.message-container.own { + text-align: left; + background-color: #bfe5e6; +} + +.message-container.other { + text-align: right; + background-color: #f2f2f2; +} + +.author { + font-weight: bold; +} + +.content { + color: #333; +} + +.created-at { + color: #999; + font-size: 0.8em; +} diff --git a/static/css/chat/user_chat.css b/static/css/chat/user_chat.css new file mode 100644 index 0000000..fc176f1 --- /dev/null +++ b/static/css/chat/user_chat.css @@ -0,0 +1,125 @@ +/* Reset default styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Roboto', sans-serif; + background-color: #f5f5f5; +} + +.container { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +h1 { + margin-bottom: 20px; + text-align: center; +} + +.search-bar { + margin-bottom: 20px; +} + +.search-form { + display: flex; + align-items: center; +} + +.search-input { + flex: 1; + padding: 10px; + border: 2px solid #ccc; + border-radius: 5px; + font-size: 16px; +} + +.search-button { + padding: 10px 20px; + border: none; + background-color: #4caf50; + color: #fff; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.search-button:hover { + background-color: #45a049; +} + +.results { + background-color: #fff; + padding: 20px; + border-radius: 5px; +} + +.user-list { + list-style: none; + padding: 0; +} + +.user-item { + margin-bottom: 20px; +} + +.user-link { + text-decoration: none; + color: #333; + transition: color 0.3s ease; + display: flex; +} + +.user-link:hover { + color: #4caf50; +} + +.user-details { + display: flex; + align-items: center; + border: 1px solid #ccc; + padding: 10px; + border-radius: 5px; +} + +.user-photo { + margin-right: 20px; +} + +.user-photo img { + width: 100px; + height: auto; + border-radius: 50%; +} + +.user-info { + text-align: left; +} + +.username { + margin-bottom: 5px; +} + +.name { + color: #666; +} + +.error-message { + color: #ff0000; + text-align: center; + margin-top: 20px; +} + +.chat-list { + list-style: none; + padding: 0; + margin: 0; +} + +.chat-item { + margin-bottom: 20px; +} diff --git a/static/css/main/search.css b/static/css/main/search.css new file mode 100644 index 0000000..fe8a87e --- /dev/null +++ b/static/css/main/search.css @@ -0,0 +1,115 @@ +/* Reset default styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Roboto', sans-serif; + background-color: #f5f5f5; +} + +.container { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +h1 { + margin-bottom: 20px; + text-align: center; +} + +.search-bar { + margin-bottom: 20px; +} + +.search-form { + display: flex; + align-items: center; +} + +.search-input { + flex: 1; + padding: 10px; + border: 2px solid #ccc; + border-radius: 5px; + font-size: 16px; +} + +.search-button { + padding: 10px 20px; + border: none; + background-color: #4caf50; + color: #fff; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.search-button:hover { + background-color: #45a049; +} + +.results { + background-color: #fff; + padding: 20px; + border-radius: 5px; +} + +.user-list { + list-style: none; + padding: 0; +} + +.user-item { + margin-bottom: 20px; +} + +.user-link { + text-decoration: none; + color: #333; + transition: color 0.3s ease; + display: flex; +} + +.user-link:hover { + color: #4caf50; +} + +.user-details { + display: flex; + align-items: center; + border: 1px solid #ccc; + padding: 10px; + border-radius: 5px; +} + +.user-photo { + margin-right: 20px; +} + +.user-photo img { + width: 100px; + height: auto; + border-radius: 50%; +} + +.user-info { + text-align: left; +} + +.username { + margin-bottom: 5px; +} + +.name { + color: #666; +} + +.error-message { + color: #ff0000; + text-align: center; + margin-top: 20px; +} diff --git a/static/css/main/styles.css b/static/css/main/styles.css index 915089b..7c9bf7b 100644 --- a/static/css/main/styles.css +++ b/static/css/main/styles.css @@ -19,7 +19,7 @@ header { } a { - color: #3b5998;; /* Цвет ссылок */ + color: #3b5998; /* Цвет ссылок */ text-decoration: none; /* Удаление подчеркивания */ } @@ -92,3 +92,7 @@ footer { .article-image { width: 20%; } + +.item { + margin-bottom: 20px; +} diff --git a/static/css/payment/donate.css b/static/css/payment/donate.css new file mode 100644 index 0000000..fc27611 --- /dev/null +++ b/static/css/payment/donate.css @@ -0,0 +1,76 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f4f4f4; +} + +.container { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +header, footer { + background-color: #007bff; + color: #fff; + padding: 10px 0; +} + +header h1, footer p { + text-align: center; + margin: 0; +} + +main { + padding: 20px 0; +} + +.donate-form { + background-color: #fff; + padding: 20px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +.donate-form h2 { + margin-top: 0; +} + +.donate-form label { + display: block; + margin-bottom: 10px; +} + +.donate-form input[type="text"] { + width: 100%; + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; + margin-bottom: 10px; +} + +.donate-form button { + padding: 10px 20px; + background-color: #007bff; + color: #fff; + border: none; + border-radius: 5px; + cursor: pointer; +} + +.donate-form button:hover { + background-color: #0056b3; +} + +.info { + margin-top: 20px; + background-color: #fff; + padding: 20px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +.info h2 { + margin-top: 0; +} diff --git a/static/css/payment/payment.css b/static/css/payment/payment.css new file mode 100644 index 0000000..666fc66 --- /dev/null +++ b/static/css/payment/payment.css @@ -0,0 +1,70 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f4f4f4; +} + +.container { + max-width: 800px; + margin: 0 auto; + padding: 20px; + background-color: #fff; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +h1, h2 { + color: #333; +} + +p { + margin: 10px 0; +} + +button { + padding: 10px 20px; + background-color: #007bff; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +button:hover { + background-color: #0056b3; +} + +form { + margin-top: 20px; +} + +form label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} + +form input[type="text"], +form input[type="email"], +form select { + width: 100%; + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; + margin-bottom: 10px; +} + +form input[type="submit"] { + background-color: #4CAF50; + color: white; + padding: 14px 20px; + margin: 8px 0; + border: none; + border-radius: 4px; + cursor: pointer; +} + +form input[type="submit"]:hover { + background-color: #45a049; +} diff --git a/static/css/search.css b/static/css/search.css deleted file mode 100644 index bd61399..0000000 --- a/static/css/search.css +++ /dev/null @@ -1,74 +0,0 @@ -/* Общие стили */ -body { - font-family: Arial, sans-serif; - margin: 0; - padding: 0; - background-color: #f4f4f4; -} - -.container { - max-width: 800px; - margin: 0 auto; - padding: 20px; - background-color: #fff; - border-radius: 5px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); -} - -h1 { - text-align: center; - margin-bottom: 20px; -} - -.search-bar { - margin-bottom: 20px; -} - -.search-bar form { - display: flex; -} - -.search-bar input[type="text"] { - width: 70%; - padding: 10px; - border: 1px solid #ccc; - border-radius: 5px 0 0 5px; - outline: none; -} - -.search-bar button { - width: 30%; - padding: 10px; - background-color: #3b5998; - color: #fff; - border: none; - border-radius: 0 5px 5px 0; - cursor: pointer; -} - -.results { - text-align: center; -} - -.user-list { - list-style-type: none; - padding: 0; -} - -.user-item { - background-color: #f9f9f9; - border: 1px solid #ddd; - border-radius: 5px; - padding: 10px; - margin-bottom: 10px; -} - -.user-item h3 { - margin-top: 0; - margin-bottom: 5px; -} - -.user-item p { - margin: 0; - color: #666; -} diff --git a/static/js/main/search.js b/static/js/main/search.js index e69de29..e4c36c3 100644 --- a/static/js/main/search.js +++ b/static/js/main/search.js @@ -0,0 +1,6 @@ +document.addEventListener("DOMContentLoaded", function () { + document.getElementById("searchButton").addEventListener("click", function () { + let input = document.getElementById('searchInput').value; + window.location.href = "search/" + encodeURIComponent(input); + }); +}); diff --git a/templates/templates/artecle/article.html b/templates/templates/artecle/article.html index 1aa2fab..6abe077 100644 --- a/templates/templates/artecle/article.html +++ b/templates/templates/artecle/article.html @@ -5,38 +5,49 @@ Article Details - + +
+ + arrow_back + +

{{ article.title }}

diff --git a/templates/templates/profile/profile.html b/templates/templates/profile/profile.html index 41ef862..20d33d7 100644 --- a/templates/templates/profile/profile.html +++ b/templates/templates/profile/profile.html @@ -5,11 +5,13 @@ + User Profile -Main +arrow_back
@@ -17,21 +19,40 @@
Profile Image {% if request.user == user %} -
+ {% csrf_token %} {{ form.image }} - +
{% endif %}
+

Username: {{ user.username }}

First Name: {{ user.first_name }}

Last Name: {{ user.last_name }}

Email: {{ user.email }}

Bio: {{ user_profile.bio }}

+ {% if request.user != user %} + send + {% endif %} +
+
+
+ {% csrf_token %} + + + + + + + +
@@ -39,7 +60,8 @@

User Posts

Uploaded Media

-
+ {% csrf_token %} {% if photos %} {% for photo in photos %} @@ -73,7 +95,7 @@

Uploaded Media

{% endif %} {% if request.user == user %} - + {% endif %}
@@ -162,6 +184,7 @@

Uploaded Media

// Обрабатываем ошибку, если необходимо }); }); + diff --git a/templates/templates/search.html b/templates/templates/search.html deleted file mode 100644 index da352f0..0000000 --- a/templates/templates/search.html +++ /dev/null @@ -1,40 +0,0 @@ -{% load static %} - - - - - - User Search Results - - - - -
-

User Search Results

- - - -
- {% if users %} -
    - {% for user in users %} -
  • -

    {{ user.username }}

    -
  • - {% endfor %} -
- {% else %} -

No users found.

- {% endif %} -
-
- - -