├── .env.example ├── .gitignore ├── .vscode └── settings.json ├── DatabaseManagement └── docker-compose.yml ├── Dockerfile ├── README.md ├── chatapi ├── __init__.py ├── asgi.py ├── custom_methods.py ├── settings.py ├── storage_backends.py ├── urls.py └── wsgi.py ├── docker-compose.yml ├── manage.py ├── message_control ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20201115_1340.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py ├── urls.py └── views.py ├── requirements.txt └── user_control ├── __init__.py ├── admin.py ├── apps.py ├── authentication.py ├── migrations ├── 0001_initial.py ├── 0002_favorite.py ├── 0003_auto_20201224_0759.py └── __init__.py ├── models.py ├── serializers.py ├── tests.py ├── urls.py └── views.py /.env.example: -------------------------------------------------------------------------------- 1 | AWS_STORAGE_BUCKET_NAME=your_s3_bucket_name 2 | AWS_S3_ACCESS_KEY_ID=aws_access_id 3 | AWS_S3_SECRET_ACCESS_KEY=aws_access_secret 4 | AWS_HOST_REGION=s3.us-east-1.amazonaws.com 5 | S3_BUCKET_URL="https://.s3.amazonaws.com" 6 | 7 | DB_NAME=db_name 8 | DB_USER=db_user 9 | DB_PASSWORD=db_password 10 | DB_HOST=db_host 11 | DB_PORT=5432 12 | 13 | SOCKET_SERVER=your_socket_server -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | .installed.cfg 4 | bin 5 | develop-eggs 6 | dist 7 | downloads 8 | eggs 9 | parts 10 | src/*.egg-info 11 | lib 12 | lib64 13 | .idea 14 | .idea/* 15 | 16 | .env 17 | db.sqlite3 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/home/adefemigreat/.virtualenvs/chatapp_env/bin/python" 3 | } -------------------------------------------------------------------------------- /DatabaseManagement/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | postgres: 5 | container_name: postgres_container 6 | image: postgres 7 | environment: 8 | POSTGRES_USER: "devtot" 9 | POSTGRES_PASSWORD: "chatapp123" 10 | PGDATA: /data/postgres 11 | volumes: 12 | - postgres:/data/postgres 13 | ports: 14 | - "5432:5432" 15 | networks: 16 | - postgres 17 | restart: unless-stopped 18 | 19 | networks: 20 | postgres: 21 | driver: bridge 22 | 23 | volumes: 24 | postgres: -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6 2 | 3 | ENV PYTHONDONTWRITEBYTECODE 1 4 | ENV PYTHONUNBUFFERED 1 5 | 6 | RUN mkdir /chatapi 7 | 8 | WORKDIR /chatapi 9 | 10 | COPY . /chatapi/ 11 | 12 | RUN pip install --upgrade pip && pip install pip-tools && pip install -r requirements.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This is a simple chat api 2 | 3 | This is the backend section for the chat api platform to showcase how to handle Chat messages with Django 4 | -------------------------------------------------------------------------------- /chatapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adefemi/chatapi/d009da53ccae3b21f61614799a689bcfa70a9c1c/chatapi/__init__.py -------------------------------------------------------------------------------- /chatapi/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for chatapi project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chatapi.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /chatapi/custom_methods.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import BasePermission, SAFE_METHODS 2 | from django.utils import timezone 3 | from rest_framework.views import exception_handler 4 | from rest_framework.response import Response 5 | from django.contrib.auth import authenticate 6 | 7 | 8 | class IsAuthenticatedCustom(BasePermission): 9 | 10 | def has_permission(self, request, view): 11 | from user_control.views import decodeJWT 12 | user = decodeJWT(request.META['HTTP_AUTHORIZATION']) 13 | if not user: 14 | return False 15 | request.user = user 16 | if request.user and request.user.is_authenticated: 17 | from user_control.models import CustomUser 18 | CustomUser.objects.filter(id=request.user.id).update( 19 | is_online=timezone.now()) 20 | return True 21 | return False 22 | 23 | 24 | class IsAuthenticatedOrReadCustom(BasePermission): 25 | def has_permission(self, request, view): 26 | if request.method in SAFE_METHODS: 27 | return True 28 | 29 | if request.user and request.user.is_authenticated: 30 | from user_control.models import CustomUser 31 | CustomUser.objects.filter(id=request.user.id).update( 32 | is_online=timezone.now()) 33 | return True 34 | return False 35 | 36 | 37 | def custom_exception_handler(exc, context): 38 | 39 | response = exception_handler(exc, context) 40 | 41 | if response is not None: 42 | return response 43 | 44 | exc_list = str(exc).split("DETAIL: ") 45 | 46 | return Response({"error": exc_list[-1]}, status=403) 47 | -------------------------------------------------------------------------------- /chatapi/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for chatapi project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | import os 15 | from decouple import config 16 | 17 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 18 | BASE_DIR = Path(__file__).resolve(strict=True).parent.parent 19 | 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = '10y83$&yi^2_g_y*r^eeabge40t&0ioyd9=m-4h-sc!0aq4rjm' 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = True 29 | 30 | ALLOWED_HOSTS = ['*'] 31 | 32 | AUTH_USER_MODEL = "user_control.CustomUser" 33 | 34 | REST_FRAMEWORK = { 35 | 'EXCEPTION_HANDLER': 'chatapi.custom_methods.custom_exception_handler', 36 | "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", 37 | "PAGE_SIZE": 20, 38 | } 39 | 40 | 41 | # Application definition 42 | 43 | INSTALLED_APPS = [ 44 | 'django.contrib.admin', 45 | 'django.contrib.auth', 46 | 'django.contrib.contenttypes', 47 | 'django.contrib.sessions', 48 | 'django.contrib.messages', 49 | 'django.contrib.staticfiles', 50 | 'rest_framework', 51 | 'user_control', 52 | 'message_control', 53 | 'chatapi', 54 | 'corsheaders', 55 | ] 56 | 57 | MIDDLEWARE = [ 58 | 'django.middleware.security.SecurityMiddleware', 59 | 'django.contrib.sessions.middleware.SessionMiddleware', 60 | 'corsheaders.middleware.CorsMiddleware', 61 | 'django.middleware.common.CommonMiddleware', 62 | 'django.middleware.csrf.CsrfViewMiddleware', 63 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 64 | 'django.contrib.messages.middleware.MessageMiddleware', 65 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 66 | ] 67 | 68 | ROOT_URLCONF = 'chatapi.urls' 69 | 70 | TEMPLATES = [ 71 | { 72 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 73 | 'DIRS': [], 74 | 'APP_DIRS': True, 75 | 'OPTIONS': { 76 | 'context_processors': [ 77 | 'django.template.context_processors.debug', 78 | 'django.template.context_processors.request', 79 | 'django.contrib.auth.context_processors.auth', 80 | 'django.contrib.messages.context_processors.messages', 81 | ], 82 | }, 83 | }, 84 | ] 85 | 86 | WSGI_APPLICATION = 'chatapi.wsgi.application' 87 | 88 | CORS_ORIGIN_ALLOW_ALL = True 89 | CORS_ALLOW_HEADERS = ( 90 | 'x-requested-with', 91 | 'content-type', 92 | 'accept', 93 | 'origin', 94 | 'authorization', 95 | 'accept-encoding', 96 | 'x-csrftoken', 97 | 'access-control-allow-origin', 98 | 'content-disposition' 99 | ) 100 | CORS_ALLOW_CREDENTIALS = False 101 | CORS_ALLOW_METHODS = ('GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS') 102 | 103 | 104 | # Database 105 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 106 | 107 | DB_NAME = config("DB_NAME") 108 | DB_USER = config("DB_USER") 109 | DB_PASSWORD = config("DB_PASSWORD") 110 | DB_HOST = config("DB_HOST") 111 | DB_PORT = config("DB_PORT") 112 | 113 | DATABASES = { 114 | 'default': { 115 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 116 | 'NAME': DB_NAME, 117 | 'USER': DB_USER, 118 | 'PASSWORD': DB_PASSWORD, 119 | 'HOST': DB_HOST, 120 | 'PORT': DB_PORT, 121 | } 122 | } 123 | 124 | 125 | # Password validation 126 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 127 | 128 | AUTH_PASSWORD_VALIDATORS = [ 129 | { 130 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 131 | }, 132 | { 133 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 134 | }, 135 | { 136 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 137 | }, 138 | { 139 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 140 | }, 141 | ] 142 | 143 | 144 | # Internationalization 145 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 146 | 147 | LANGUAGE_CODE = 'en-us' 148 | 149 | TIME_ZONE = 'UTC' 150 | 151 | USE_I18N = True 152 | 153 | USE_L10N = True 154 | 155 | USE_TZ = True 156 | 157 | 158 | # Static files (CSS, JavaScript, Images) 159 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 160 | 161 | 162 | S3_BUCKET_URL = config('S3_BUCKET_URL') 163 | STATIC_ROOT = 'staticfiles' 164 | 165 | AWS_ACCESS_KEY_ID = config('AWS_S3_ACCESS_KEY_ID') 166 | AWS_SECRET_ACCESS_KEY = config('AWS_S3_SECRET_ACCESS_KEY') 167 | AWS_STORAGE_BUCKET_NAME = config('AWS_STORAGE_BUCKET_NAME') 168 | AWS_HOST_REGION = config('AWS_HOST_REGION') 169 | AWS_S3_CUSTOM_DOMAIN = '%s.s3.amazonaws.com' % AWS_STORAGE_BUCKET_NAME 170 | AWS_DEFAULT_ACL = None 171 | 172 | AWS_LOCATION = 'static' 173 | 174 | MEDIA_URL = '/media/' 175 | 176 | 177 | STATICFILES_DIRS = [ 178 | os.path.join(BASE_DIR, 'chatapi/static'), 179 | ] 180 | STATIC_URL = 'https://%s/%s/' % (AWS_S3_CUSTOM_DOMAIN, AWS_LOCATION) 181 | STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' 182 | 183 | 184 | AWS_S3_OBJECT_PARAMETERS = { 185 | 'CacheControl': 'max-age=86400', 186 | } 187 | 188 | 189 | DEFAULT_FILE_STORAGE = 'chatapi.storage_backends.MediaStorage' 190 | 191 | SOCKET_SERVER = config("SOCKET_SERVER") 192 | -------------------------------------------------------------------------------- /chatapi/storage_backends.py: -------------------------------------------------------------------------------- 1 | from storages.backends.s3boto3 import S3Boto3Storage 2 | 3 | 4 | class MediaStorage(S3Boto3Storage): 5 | location = 'media' 6 | file_overwrite = False 7 | -------------------------------------------------------------------------------- /chatapi/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from django.contrib import admin 3 | from django.urls import path, include 4 | from django.conf.urls.static import static 5 | from django.conf import settings 6 | 7 | urlpatterns = [ 8 | path('admin/', admin.site.urls), 9 | path('user/', include('user_control.urls')), 10 | path('message/', include('message_control.urls')) 11 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 12 | -------------------------------------------------------------------------------- /chatapi/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for chatapi 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/3.1/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', 'chatapi.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | api: 5 | build: . 6 | command: bash -c "python manage.py runserver 0.0.0.0:8000" 7 | container_name: chatapi 8 | restart: unless-stopped 9 | volumes: 10 | - .:/chatapi 11 | ports: 12 | - "8000:8000" 13 | networks: 14 | - postgres 15 | 16 | networks: 17 | postgres: 18 | driver: bridge 19 | -------------------------------------------------------------------------------- /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 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chatapi.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /message_control/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adefemi/chatapi/d009da53ccae3b21f61614799a689bcfa70a9c1c/message_control/__init__.py -------------------------------------------------------------------------------- /message_control/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /message_control/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MessageControlConfig(AppConfig): 5 | name = 'message_control' 6 | -------------------------------------------------------------------------------- /message_control/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1 on 2020-11-15 13:40 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='GenericFileUpload', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('file_upload', models.FileField(upload_to='')), 20 | ('created_at', models.DateTimeField(auto_now_add=True)), 21 | ], 22 | ), 23 | migrations.CreateModel( 24 | name='Message', 25 | fields=[ 26 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('message', models.TextField(blank=True, null=True)), 28 | ('is_read', models.BooleanField(default=False)), 29 | ('created_at', models.DateTimeField(auto_now_add=True)), 30 | ('updated_at', models.DateTimeField(auto_now=True)), 31 | ], 32 | options={ 33 | 'ordering': ('-created_at',), 34 | }, 35 | ), 36 | migrations.CreateModel( 37 | name='MessageAttachment', 38 | fields=[ 39 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 40 | ('caption', models.CharField(blank=True, max_length=255, null=True)), 41 | ('created_at', models.DateTimeField(auto_now_add=True)), 42 | ('attachment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='message_uploads', to='message_control.genericfileupload')), 43 | ('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='message_attachments', to='message_control.message')), 44 | ], 45 | options={ 46 | 'ordering': ('created_at',), 47 | }, 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /message_control/migrations/0002_auto_20201115_1340.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1 on 2020-11-15 13:40 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 | ('message_control', '0001_initial'), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name='message', 20 | name='receiver', 21 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='message_receiver', to=settings.AUTH_USER_MODEL), 22 | ), 23 | migrations.AddField( 24 | model_name='message', 25 | name='sender', 26 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='message_sender', to=settings.AUTH_USER_MODEL), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /message_control/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adefemi/chatapi/d009da53ccae3b21f61614799a689bcfa70a9c1c/message_control/migrations/__init__.py -------------------------------------------------------------------------------- /message_control/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class GenericFileUpload(models.Model): 5 | file_upload = models.FileField() 6 | created_at = models.DateTimeField(auto_now_add=True) 7 | 8 | def __str__(self): 9 | return f"{self.file_upload}" 10 | 11 | 12 | class Message(models.Model): 13 | sender = models.ForeignKey( 14 | "user_control.CustomUser", related_name="message_sender", on_delete=models.CASCADE) 15 | receiver = models.ForeignKey( 16 | "user_control.CustomUser", related_name="message_receiver", on_delete=models.CASCADE) 17 | message = models.TextField(blank=True, null=True) 18 | is_read = models.BooleanField(default=False) 19 | created_at = models.DateTimeField(auto_now_add=True) 20 | updated_at = models.DateTimeField(auto_now=True) 21 | 22 | def __str__(self): 23 | return f"message between {self.sender.username} and {self.receiver.username}" 24 | 25 | class Meta: 26 | ordering = ("-created_at",) 27 | 28 | 29 | class MessageAttachment(models.Model): 30 | message = models.ForeignKey( 31 | Message, related_name="message_attachments", on_delete=models.CASCADE) 32 | attachment = models.ForeignKey( 33 | GenericFileUpload, related_name="message_uploads", on_delete=models.CASCADE) 34 | caption = models.CharField(max_length=255, null=True, blank=True) 35 | created_at = models.DateTimeField(auto_now_add=True) 36 | 37 | class Meta: 38 | ordering = ("created_at",) 39 | -------------------------------------------------------------------------------- /message_control/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import GenericFileUpload, Message, MessageAttachment 3 | 4 | 5 | class GenericFileUploadSerializer(serializers.ModelSerializer): 6 | 7 | class Meta: 8 | model = GenericFileUpload 9 | fields = "__all__" 10 | 11 | 12 | class MessageAttachmentSerializer(serializers.ModelSerializer): 13 | attachment = GenericFileUploadSerializer() 14 | 15 | class Meta: 16 | model = MessageAttachment 17 | fields = "__all__" 18 | 19 | 20 | class MessageSerializer(serializers.ModelSerializer): 21 | sender = serializers.SerializerMethodField("get_sender_data") 22 | sender_id = serializers.IntegerField(write_only=True) 23 | receiver = serializers.SerializerMethodField("get_receiver_data") 24 | receiver_id = serializers.IntegerField(write_only=True) 25 | message_attachments = MessageAttachmentSerializer( 26 | read_only=True, many=True) 27 | 28 | class Meta: 29 | model = Message 30 | fields = "__all__" 31 | 32 | def get_receiver_data(self, obj): 33 | from user_control.serializers import UserProfileSerializer 34 | return UserProfileSerializer(obj.receiver.user_profile).data 35 | 36 | def get_sender_data(self, obj): 37 | from user_control.serializers import UserProfileSerializer 38 | return UserProfileSerializer(obj.sender.user_profile).data 39 | -------------------------------------------------------------------------------- /message_control/tests.py: -------------------------------------------------------------------------------- 1 | from rest_framework.test import APITestCase 2 | from django.core.files.base import ContentFile 3 | from django.core.files.uploadedfile import SimpleUploadedFile 4 | from six import BytesIO 5 | from pil import Image 6 | import json 7 | 8 | 9 | def create_image(storage, filename, size=(100, 100), image_mode='RGB', image_format='PNG'): 10 | data = BytesIO() 11 | Image.new(image_mode, size).save(data, image_format) 12 | data.seek(0) 13 | if not storage: 14 | return data 15 | image_file = ContentFile(data.read()) 16 | return storage.save(filename, image_file) 17 | 18 | 19 | class TestFileUpload(APITestCase): 20 | file_upload_url = "/message/file-upload" 21 | 22 | def test_file_upload(self): 23 | # definition 24 | 25 | avatar = create_image(None, 'avatar.png') 26 | avatar_file = SimpleUploadedFile('front1.png', avatar.getvalue()) 27 | data = { 28 | "file_upload": avatar_file 29 | } 30 | 31 | # processing 32 | response = self.client.post(self.file_upload_url, data=data) 33 | result = response.json() 34 | 35 | # assertions 36 | self.assertEqual(response.status_code, 201) 37 | self.assertEqual(result["id"], 1) 38 | 39 | 40 | class TestMessage(APITestCase): 41 | message_url = "/message/message" 42 | file_upload_url = "/message/file-upload" 43 | login_url = "/user/login" 44 | 45 | def setUp(self): 46 | from user_control.models import CustomUser, UserProfile 47 | 48 | payload = { 49 | "username": "sender", 50 | "password": "sender123", 51 | "email": "adefemigreat@yahoo.com" 52 | } 53 | 54 | # sender 55 | self.sender = CustomUser.objects._create_user(**payload) 56 | UserProfile.objects.create( 57 | first_name="sender", last_name="sender", user=self.sender, caption="sender", about="sender") 58 | 59 | # login 60 | response = self.client.post(self.login_url, data=payload) 61 | result = response.json() 62 | 63 | self.bearer = { 64 | 'HTTP_AUTHORIZATION': 'Bearer {}'.format(result['access'])} 65 | 66 | # receiver 67 | self.receiver = CustomUser.objects._create_user( 68 | "receiver", "receiver123", email="ade123@yahoo.com") 69 | UserProfile.objects.create( 70 | first_name="receiver", last_name="receiver", user=self.receiver, caption="receiver", about="receiver") 71 | 72 | def test_post_message(self): 73 | 74 | payload = { 75 | "sender_id": self.sender.id, 76 | "receiver_id": self.receiver.id, 77 | "message": "test message", 78 | 79 | } 80 | 81 | # processing 82 | response = self.client.post( 83 | self.message_url, data=payload, **self.bearer) 84 | result = response.json() 85 | 86 | # assertions 87 | self.assertEqual(response.status_code, 201) 88 | self.assertEqual(result["message"], "test message") 89 | self.assertEqual(result["sender"]["user"]["username"], "sender") 90 | self.assertEqual(result["receiver"]["user"]["username"], "receiver") 91 | 92 | def test_post_with_file(self): 93 | 94 | # create a file 95 | avatar = create_image(None, 'avatar.png') 96 | avatar_file = SimpleUploadedFile('front1.png', avatar.getvalue()) 97 | data = { 98 | "file_upload": avatar_file 99 | } 100 | response = self.client.post( 101 | self.file_upload_url, data=data, **self.bearer) 102 | file_content = response.json()["id"] 103 | 104 | payload = { 105 | "sender_id": self.sender.id, 106 | "receiver_id": self.receiver.id, 107 | "message": "test message", 108 | "attachments": [ 109 | { 110 | "caption": "nice stuff", 111 | "attachment_id": file_content 112 | }, 113 | { 114 | "attachment_id": file_content 115 | } 116 | ] 117 | } 118 | 119 | # processing 120 | response = self.client.post(self.message_url, data=json.dumps( 121 | payload), content_type='application/json', **self.bearer) 122 | result = response.json() 123 | 124 | # assertions 125 | self.assertEqual(response.status_code, 201) 126 | self.assertEqual(result["message"], "test message") 127 | self.assertEqual(result["sender"]["user"]["username"], "sender") 128 | self.assertEqual(result["receiver"]["user"]["username"], "receiver") 129 | self.assertEqual(result["message_attachments"] 130 | [0]["attachment"]["id"], 1) 131 | self.assertEqual(result["message_attachments"] 132 | [0]["caption"], "nice stuff") 133 | 134 | def test_update_message(self): 135 | 136 | # create message 137 | payload = { 138 | "sender_id": self.sender.id, 139 | "receiver_id": self.receiver.id, 140 | "message": "test message", 141 | 142 | } 143 | self.client.post(self.message_url, data=payload, **self.bearer) 144 | 145 | # update message 146 | payload = { 147 | "message": "test message updated", 148 | "is_read": True 149 | } 150 | response = self.client.patch( 151 | self.message_url+"/1", data=payload, **self.bearer) 152 | result = response.json() 153 | 154 | # assertions 155 | self.assertEqual(response.status_code, 200) 156 | self.assertEqual(result["message"], "test message updated") 157 | self.assertEqual(result["is_read"], True) 158 | 159 | def test_delete_message(self): 160 | 161 | # create message 162 | payload = { 163 | "sender_id": self.sender.id, 164 | "receiver_id": self.receiver.id, 165 | "message": "test message", 166 | 167 | } 168 | self.client.post(self.message_url, data=payload, **self.bearer) 169 | 170 | response = self.client.delete( 171 | self.message_url+"/1", data=payload, **self.bearer) 172 | 173 | # assertions 174 | self.assertEqual(response.status_code, 204) 175 | 176 | def test_get_message(self): 177 | 178 | response = self.client.get( 179 | self.message_url+f"?user_id={self.receiver.id}", **self.bearer) 180 | result = response.json() 181 | 182 | self.assertEqual(response.status_code, 200) 183 | -------------------------------------------------------------------------------- /message_control/urls.py: -------------------------------------------------------------------------------- 1 | from rest_framework.routers import DefaultRouter 2 | from .views import GenericFileUploadView, MessageView, ReadMultipleMessages 3 | from django.urls import path, include 4 | 5 | router = DefaultRouter(trailing_slash=False) 6 | 7 | router.register("file-upload", GenericFileUploadView) 8 | router.register("message", MessageView) 9 | 10 | urlpatterns = [ 11 | path("", include(router.urls)), 12 | path("read-messages", ReadMultipleMessages.as_view()), 13 | ] 14 | -------------------------------------------------------------------------------- /message_control/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.viewsets import ModelViewSet 2 | from rest_framework.views import APIView 3 | from .serializers import GenericFileUpload, GenericFileUploadSerializer, Message, MessageAttachment, MessageSerializer 4 | from chatapi.custom_methods import IsAuthenticatedCustom 5 | from rest_framework.response import Response 6 | from django.db.models import Q 7 | from django.conf import settings 8 | import requests 9 | import json 10 | 11 | 12 | def handleRequest(serializerData): 13 | notification = { 14 | "message": serializerData.data.get("message"), 15 | "from": serializerData.data.get("sender"), 16 | "receiver": serializerData.data.get("receiver").get("id") 17 | } 18 | 19 | headers = { 20 | 'Content-Type': 'application/json', 21 | } 22 | try: 23 | requests.post(settings.SOCKET_SERVER, json.dumps( 24 | notification), headers=headers) 25 | except Exception as e: 26 | pass 27 | return True 28 | 29 | 30 | class GenericFileUploadView(ModelViewSet): 31 | queryset = GenericFileUpload.objects.all() 32 | serializer_class = GenericFileUploadSerializer 33 | 34 | 35 | class MessageView(ModelViewSet): 36 | queryset = Message.objects.select_related( 37 | "sender", "receiver").prefetch_related("message_attachments") 38 | serializer_class = MessageSerializer 39 | permission_classes = (IsAuthenticatedCustom, ) 40 | 41 | def get_queryset(self): 42 | data = self.request.query_params.dict() 43 | user_id = data.get("user_id", None) 44 | 45 | if user_id: 46 | active_user_id = self.request.user.id 47 | return self.queryset.filter(Q(sender_id=user_id, receiver_id=active_user_id) | Q( 48 | sender_id=active_user_id, receiver_id=user_id)).distinct() 49 | return self.queryset 50 | 51 | def create(self, request, *args, **kwargs): 52 | 53 | try: 54 | request.data._mutable = True 55 | except: 56 | pass 57 | attachments = request.data.pop("attachments", None) 58 | 59 | if str(request.user.id) != str(request.data.get("sender_id", None)): 60 | raise Exception("only sender can create a message") 61 | 62 | serializer = self.serializer_class(data=request.data) 63 | serializer.is_valid(raise_exception=True) 64 | serializer.save() 65 | 66 | if attachments: 67 | MessageAttachment.objects.bulk_create([MessageAttachment( 68 | **attachment, message_id=serializer.data["id"]) for attachment in attachments]) 69 | 70 | message_data = self.get_queryset().get(id=serializer.data["id"]) 71 | return Response(self.serializer_class(message_data).data, status=201) 72 | 73 | handleRequest(serializer) 74 | 75 | return Response(serializer.data, status=201) 76 | 77 | def update(self, request, *args, **kwargs): 78 | 79 | try: 80 | request.data._mutable = True 81 | except: 82 | pass 83 | attachments = request.data.pop("attachments", None) 84 | instance = self.get_object() 85 | 86 | serializer = self.serializer_class( 87 | data=request.data, instance=instance, partial=True) 88 | serializer.is_valid(raise_exception=True) 89 | serializer.save() 90 | 91 | MessageAttachment.objects.filter(message_id=instance.id).delete() 92 | 93 | if attachments: 94 | MessageAttachment.objects.bulk_create([MessageAttachment( 95 | **attachment, message_id=instance.id) for attachment in attachments]) 96 | 97 | message_data = self.get_object() 98 | return Response(self.serializer_class(message_data).data, status=200) 99 | 100 | handleRequest(serializer) 101 | 102 | return Response(serializer.data, status=200) 103 | 104 | 105 | class ReadMultipleMessages(APIView): 106 | 107 | def post(self, request): 108 | data = request.data.get("message_ids", None) 109 | 110 | Message.objects.filter(id__in=data).update(is_read=True) 111 | return Response("success") 112 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.2.10 2 | astroid==2.4.2 3 | autopep8==1.5.4 4 | boto==2.49.0 5 | boto3==1.14.56 6 | botocore==1.17.56 7 | certifi==2020.6.20 8 | chardet==3.0.4 9 | colorama==0.4.4 10 | Django==3.1 11 | django-cors-headers==3.5.0 12 | django-storages==1.10 13 | djangorestframework==3.11.1 14 | docutils==0.15.2 15 | idna==2.10 16 | image==1.5.33 17 | isort==5.4.2 18 | jmespath==0.10.0 19 | lazy-object-proxy==1.4.3 20 | mccabe==0.6.1 21 | pillow==8.0.1 22 | psycopg2==2.8.6 23 | pycodestyle==2.6.0 24 | PyJWT==1.7.1 25 | pylint==2.6.0 26 | python-dateutil==2.8.1 27 | python-decouple==3.3 28 | pytz==2020.1 29 | requests==2.24.0 30 | s3transfer==0.3.3 31 | six==1.15.0 32 | sqlparse==0.3.1 33 | toml==0.10.1 34 | typed-ast==1.4.1 35 | urllib3==1.25.10 36 | wrapt==1.12.1 37 | -------------------------------------------------------------------------------- /user_control/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adefemi/chatapi/d009da53ccae3b21f61614799a689bcfa70a9c1c/user_control/__init__.py -------------------------------------------------------------------------------- /user_control/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import CustomUser, Jwt, Favorite 3 | 4 | 5 | admin.site.register((CustomUser, Jwt, Favorite)) 6 | -------------------------------------------------------------------------------- /user_control/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UserControlConfig(AppConfig): 5 | name = 'user_control' 6 | -------------------------------------------------------------------------------- /user_control/authentication.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | from django.conf import settings 3 | from datetime import datetime 4 | from rest_framework.authentication import BaseAuthentication 5 | from .models import CustomUser, Jwt 6 | 7 | 8 | class Authentication(BaseAuthentication): 9 | 10 | def authenticate(self, request): 11 | data = self.validate_request(request.headers) 12 | if not data: 13 | return None, None 14 | 15 | return self.get_user(data["user_id"]), None 16 | 17 | def get_user(self, user_id): 18 | try: 19 | user = CustomUser.objects.get(id=user_id) 20 | return user 21 | except Exception: 22 | return None 23 | 24 | def validate_request(self, headers): 25 | authorization = headers.get("Authorization", None) 26 | if not authorization: 27 | return None 28 | token = headers["Authorization"][7:] 29 | decoded_data = Authentication.verify_token(token) 30 | 31 | if not decoded_data: 32 | return None 33 | 34 | return decoded_data 35 | 36 | @staticmethod 37 | def verify_token(token): 38 | # decode the token 39 | try: 40 | decoded_data = jwt.decode( 41 | token, settings.SECRET_KEY, algorithm="HS256") 42 | except Exception: 43 | return None 44 | 45 | # check if token as exipired 46 | exp = decoded_data["exp"] 47 | 48 | if datetime.now().timestamp() > exp: 49 | return None 50 | 51 | return decoded_data 52 | -------------------------------------------------------------------------------- /user_control/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1 on 2020-11-15 13:40 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('message_control', '0001_initial'), 15 | ('auth', '0012_alter_user_first_name_max_length'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='CustomUser', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('password', models.CharField(max_length=128, verbose_name='password')), 24 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 25 | ('username', models.CharField(max_length=100, unique=True)), 26 | ('email', models.EmailField(max_length=254, unique=True)), 27 | ('created_at', models.DateTimeField(auto_now_add=True)), 28 | ('updated_at', models.DateTimeField(auto_now=True)), 29 | ('is_staff', models.BooleanField(default=False)), 30 | ('is_superuser', models.BooleanField(default=False)), 31 | ('is_active', models.BooleanField(default=True)), 32 | ('is_online', models.DateTimeField(default=django.utils.timezone.now)), 33 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 34 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 35 | ], 36 | options={ 37 | 'ordering': ('created_at',), 38 | }, 39 | ), 40 | migrations.CreateModel( 41 | name='UserProfile', 42 | fields=[ 43 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 44 | ('first_name', models.CharField(max_length=100)), 45 | ('last_name', models.CharField(max_length=100)), 46 | ('caption', models.CharField(max_length=250)), 47 | ('about', models.TextField()), 48 | ('created_at', models.DateTimeField(auto_now_add=True)), 49 | ('updated_at', models.DateTimeField(auto_now=True)), 50 | ('profile_picture', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user_image', to='message_control.genericfileupload')), 51 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_profile', to=settings.AUTH_USER_MODEL)), 52 | ], 53 | options={ 54 | 'ordering': ('created_at',), 55 | }, 56 | ), 57 | migrations.CreateModel( 58 | name='Jwt', 59 | fields=[ 60 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 61 | ('access', models.TextField()), 62 | ('refresh', models.TextField()), 63 | ('created_at', models.DateTimeField(auto_now_add=True)), 64 | ('updated_at', models.DateTimeField(auto_now=True)), 65 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='login_user', to=settings.AUTH_USER_MODEL)), 66 | ], 67 | ), 68 | ] 69 | -------------------------------------------------------------------------------- /user_control/migrations/0002_favorite.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1 on 2020-12-12 09:37 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 | ('user_control', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Favorite', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('created_at', models.DateTimeField(auto_now_add=True)), 20 | ('favorite', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_favoured', to=settings.AUTH_USER_MODEL)), 21 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_favorites', to=settings.AUTH_USER_MODEL)), 22 | ], 23 | options={ 24 | 'ordering': ('created_at',), 25 | }, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /user_control/migrations/0003_auto_20201224_0759.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1 on 2020-12-24 06:59 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 | ('user_control', '0002_favorite'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='favorite', 17 | name='favorite', 18 | ), 19 | migrations.AddField( 20 | model_name='favorite', 21 | name='favorite', 22 | field=models.ManyToManyField(related_name='user_favoured', to=settings.AUTH_USER_MODEL), 23 | ), 24 | migrations.AlterField( 25 | model_name='favorite', 26 | name='user', 27 | field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_favorites', to=settings.AUTH_USER_MODEL), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /user_control/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adefemi/chatapi/d009da53ccae3b21f61614799a689bcfa70a9c1c/user_control/migrations/__init__.py -------------------------------------------------------------------------------- /user_control/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager 3 | from message_control.models import GenericFileUpload 4 | from django.utils import timezone 5 | 6 | 7 | class CustomUserManager(BaseUserManager): 8 | 9 | def create_user(self, username, password, **extra_fields): 10 | if not username: 11 | raise ValueError("Username field is required") 12 | 13 | user = self.model(username=username, **extra_fields) 14 | user.set_password(password) 15 | user.save() 16 | return user 17 | 18 | def create_superuser(self, username, password, **extra_fields): 19 | extra_fields.setdefault('is_staff', True) 20 | extra_fields.setdefault('is_superuser', True) 21 | extra_fields.setdefault('is_active', True) 22 | 23 | if extra_fields.get("is_staff") is not True: 24 | raise ValueError("Superuser must have is_staff=True.") 25 | 26 | if extra_fields.get("is_superuser") is not True: 27 | raise ValueError("Superuser must have is_superuser=True.") 28 | 29 | return self._create_user(username, password, **extra_fields) 30 | 31 | 32 | class CustomUser(AbstractBaseUser, PermissionsMixin): 33 | username = models.CharField(unique=True, max_length=100) 34 | email = models.EmailField(unique=True) 35 | created_at = models.DateTimeField(auto_now_add=True) 36 | updated_at = models.DateTimeField(auto_now=True) 37 | is_staff = models.BooleanField(default=False) 38 | is_superuser = models.BooleanField(default=False) 39 | is_active = models.BooleanField(default=True) 40 | is_online = models.DateTimeField(default=timezone.now) 41 | 42 | USERNAME_FIELD = "username" 43 | objects = CustomUserManager() 44 | 45 | def __str__(self): 46 | return self.username 47 | 48 | class Meta: 49 | ordering = ("created_at",) 50 | 51 | 52 | class UserProfile(models.Model): 53 | user = models.OneToOneField( 54 | CustomUser, related_name="user_profile", on_delete=models.CASCADE) 55 | first_name = models.CharField(max_length=100) 56 | last_name = models.CharField(max_length=100) 57 | caption = models.CharField(max_length=250) 58 | about = models.TextField() 59 | profile_picture = models.ForeignKey( 60 | GenericFileUpload, related_name="user_image", on_delete=models.SET_NULL, null=True) 61 | created_at = models.DateTimeField(auto_now_add=True) 62 | updated_at = models.DateTimeField(auto_now=True) 63 | 64 | def __str__(self): 65 | return self.user.username 66 | 67 | class Meta: 68 | ordering = ("created_at",) 69 | 70 | 71 | class Favorite(models.Model): 72 | user = models.OneToOneField(CustomUser, related_name="user_favorites", on_delete=models.CASCADE) 73 | favorite = models.ManyToManyField(CustomUser, related_name="user_favoured") 74 | created_at = models.DateTimeField(auto_now_add=True) 75 | 76 | def __str__(self): 77 | return f"{self.user.username}" 78 | 79 | class Meta: 80 | ordering = ("created_at",) 81 | 82 | 83 | class Jwt(models.Model): 84 | user = models.OneToOneField( 85 | CustomUser, related_name="login_user", on_delete=models.CASCADE) 86 | access = models.TextField() 87 | refresh = models.TextField() 88 | created_at = models.DateTimeField(auto_now_add=True) 89 | updated_at = models.DateTimeField(auto_now=True) 90 | -------------------------------------------------------------------------------- /user_control/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import UserProfile, CustomUser, Favorite 3 | from message_control.serializers import GenericFileUploadSerializer 4 | 5 | 6 | class LoginSerializer(serializers.Serializer): 7 | username = serializers.CharField() 8 | password = serializers.CharField() 9 | 10 | 11 | class RegisterSerializer(serializers.Serializer): 12 | email = serializers.EmailField() 13 | username = serializers.CharField() 14 | password = serializers.CharField() 15 | 16 | 17 | class RefreshSerializer(serializers.Serializer): 18 | refresh = serializers.CharField() 19 | 20 | 21 | class CustomUserSerializer(serializers.ModelSerializer): 22 | 23 | class Meta: 24 | model = CustomUser 25 | exclude = ("password", ) 26 | 27 | 28 | class UserProfileSerializer(serializers.ModelSerializer): 29 | user = CustomUserSerializer(read_only=True) 30 | user_id = serializers.IntegerField(write_only=True) 31 | profile_picture = GenericFileUploadSerializer(read_only=True) 32 | profile_picture_id = serializers.IntegerField( 33 | write_only=True, required=False) 34 | message_count = serializers.SerializerMethodField("get_message_count") 35 | 36 | class Meta: 37 | model = UserProfile 38 | fields = "__all__" 39 | 40 | def get_message_count(self, obj): 41 | try: 42 | user_id = self.context["request"].user.id 43 | except Exception as e: 44 | user_id = None 45 | 46 | from message_control.models import Message 47 | message = Message.objects.filter(sender_id=obj.user.id, receiver_id=user_id, is_read=False).distinct() 48 | 49 | return message.count() 50 | 51 | 52 | class FavoriteSerializer(serializers.Serializer): 53 | favorite_id = serializers.IntegerField() 54 | -------------------------------------------------------------------------------- /user_control/tests.py: -------------------------------------------------------------------------------- 1 | from rest_framework.test import APITestCase 2 | from .views import get_random, get_access_token, get_refresh_token 3 | from .models import CustomUser, UserProfile 4 | from message_control.tests import create_image, SimpleUploadedFile 5 | 6 | 7 | class TestGenericFunctions(APITestCase): 8 | 9 | def test_get_random(self): 10 | 11 | rand1 = get_random(10) 12 | rand2 = get_random(10) 13 | rand3 = get_random(15) 14 | 15 | # check that we are getting a result 16 | self.assertTrue(rand1) 17 | 18 | # check that rand1 is not equal to rand2 19 | self.assertNotEqual(rand1, rand2) 20 | 21 | # check that the length of result is what is expected 22 | self.assertEqual(len(rand1), 10) 23 | self.assertEqual(len(rand3), 15) 24 | 25 | def test_get_access_token(self): 26 | payload = { 27 | "id": 1 28 | } 29 | 30 | token = get_access_token(payload) 31 | 32 | # check that we obtained a result 33 | self.assertTrue(token) 34 | 35 | def test_get_refresh_token(self): 36 | 37 | token = get_refresh_token() 38 | 39 | # check that we obtained a result 40 | self.assertTrue(token) 41 | 42 | 43 | class TestAuth(APITestCase): 44 | login_url = "/user/login" 45 | register_url = "/user/register" 46 | refresh_url = "/user/refresh" 47 | 48 | def test_register(self): 49 | payload = { 50 | "username": "adefemigreat", 51 | "password": "ade123", 52 | "email": "adefemigreat@yahoo.com" 53 | } 54 | 55 | response = self.client.post(self.register_url, data=payload) 56 | 57 | # check that we obtain a status of 201 58 | self.assertEqual(response.status_code, 201) 59 | 60 | def test_login(self): 61 | payload = { 62 | "username": "adefemigreat", 63 | "password": "ade123", 64 | "email": "adefemigreat@yahoo.com" 65 | } 66 | 67 | # register 68 | self.client.post(self.register_url, data=payload) 69 | 70 | # login 71 | response = self.client.post(self.login_url, data=payload) 72 | result = response.json() 73 | 74 | # check that we obtain a status of 200 75 | self.assertEqual(response.status_code, 200) 76 | 77 | # check that we obtained both the refresh and access token 78 | self.assertTrue(result["access"]) 79 | self.assertTrue(result["refresh"]) 80 | 81 | def test_refresh(self): 82 | payload = { 83 | "username": "adefemigreat", 84 | "password": "ade123", 85 | "email": "adefemigreat@yahoo.com" 86 | } 87 | 88 | # register 89 | self.client.post(self.register_url, data=payload) 90 | 91 | # login 92 | response = self.client.post(self.login_url, data=payload) 93 | refresh = response.json()["refresh"] 94 | 95 | # get refresh 96 | response = self.client.post( 97 | self.refresh_url, data={"refresh": refresh}) 98 | result = response.json() 99 | 100 | # check that we obtain a status of 200 101 | self.assertEqual(response.status_code, 200) 102 | 103 | # check that we obtained both the refresh and access token 104 | self.assertTrue(result["access"]) 105 | self.assertTrue(result["refresh"]) 106 | 107 | 108 | class TestUserInfo(APITestCase): 109 | profile_url = "/user/profile" 110 | file_upload_url = "/message/file-upload" 111 | login_url = "/user/login" 112 | 113 | def setUp(self): 114 | payload = { 115 | "username": "adefemigreat", 116 | "password": "ade123", 117 | "email": "adefemigreat@yahoo.com" 118 | } 119 | 120 | self.user = CustomUser.objects._create_user(**payload) 121 | 122 | # login 123 | response = self.client.post(self.login_url, data=payload) 124 | result = response.json() 125 | 126 | self.bearer = { 127 | 'HTTP_AUTHORIZATION': 'Bearer {}'.format(result['access'])} 128 | 129 | def test_post_user_profile(self): 130 | 131 | payload = { 132 | "user_id": self.user.id, 133 | "first_name": "Adefemi", 134 | "last_name": "Greate", 135 | "caption": "Being alive is different from living", 136 | "about": "I am a passionation lover of ART, graphics and creation" 137 | } 138 | 139 | response = self.client.post( 140 | self.profile_url, data=payload, **self.bearer) 141 | result = response.json() 142 | 143 | self.assertEqual(response.status_code, 201) 144 | self.assertEqual(result["first_name"], "Adefemi") 145 | self.assertEqual(result["last_name"], "Greate") 146 | self.assertEqual(result["user"]["username"], "adefemigreat") 147 | 148 | def test_post_user_profile_with_profile_picture(self): 149 | 150 | # upload image 151 | avatar = create_image(None, 'avatar.png') 152 | avatar_file = SimpleUploadedFile('front.png', avatar.getvalue()) 153 | data = { 154 | "file_upload": avatar_file 155 | } 156 | 157 | # processing 158 | response = self.client.post( 159 | self.file_upload_url, data=data, **self.bearer) 160 | result = response.json() 161 | 162 | payload = { 163 | "user_id": self.user.id, 164 | "first_name": "Adefemi", 165 | "last_name": "Greate", 166 | "caption": "Being alive is different from living", 167 | "about": "I am a passionation lover of ART, graphics and creation", 168 | "profile_picture_id": result["id"] 169 | } 170 | 171 | response = self.client.post( 172 | self.profile_url, data=payload, **self.bearer) 173 | result = response.json() 174 | 175 | self.assertEqual(response.status_code, 201) 176 | self.assertEqual(result["first_name"], "Adefemi") 177 | self.assertEqual(result["last_name"], "Greate") 178 | self.assertEqual(result["user"]["username"], "adefemigreat") 179 | self.assertEqual(result["profile_picture"]["id"], 1) 180 | 181 | def test_update_user_profile(self): 182 | # create profile 183 | 184 | payload = { 185 | "user_id": self.user.id, 186 | "first_name": "Adefemi", 187 | "last_name": "Greate", 188 | "caption": "Being alive is different from living", 189 | "about": "I am a passionation lover of ART, graphics and creation" 190 | } 191 | 192 | response = self.client.post( 193 | self.profile_url, data=payload, **self.bearer) 194 | result = response.json() 195 | 196 | # --- created profile 197 | 198 | payload = { 199 | "first_name": "Ade", 200 | "last_name": "Great", 201 | } 202 | 203 | response = self.client.patch( 204 | self.profile_url + f"/{result['id']}", data=payload, **self.bearer) 205 | result = response.json() 206 | 207 | self.assertEqual(response.status_code, 200) 208 | self.assertEqual(result["first_name"], "Ade") 209 | self.assertEqual(result["last_name"], "Great") 210 | self.assertEqual(result["user"]["username"], "adefemigreat") 211 | 212 | def test_user_search(self): 213 | 214 | UserProfile.objects.create(user=self.user, first_name="Adefemi", last_name="oseni", 215 | caption="live is all about living", about="I'm a youtuber") 216 | 217 | user2 = CustomUser.objects._create_user( 218 | username="tester", password="tester123", email="adefemi@yahoo.com") 219 | UserProfile.objects.create(user=user2, first_name="Vester", last_name="Mango", 220 | caption="it's all about testing", about="I'm a youtuber") 221 | 222 | user3 = CustomUser.objects._create_user( 223 | username="vasman", password="vasman123", email="adefemi@yahoo.com2") 224 | UserProfile.objects.create(user=user3, first_name="Adeyemi", last_name="Boseman", 225 | caption="it's all about testing", about="I'm a youtuber") 226 | 227 | # test keyword = adefemi oseni 228 | url = self.profile_url + "?keyword=adefemi oseni" 229 | 230 | response = self.client.get(url, **self.bearer) 231 | result = response.json()["results"] 232 | 233 | self.assertEqual(response.status_code, 200) 234 | self.assertEqual(len(result), 0) 235 | 236 | # test keyword = ade 237 | url = self.profile_url + "?keyword=ade" 238 | 239 | response = self.client.get(url, **self.bearer) 240 | result = response.json()["results"] 241 | 242 | self.assertEqual(response.status_code, 200) 243 | self.assertEqual(len(result), 2) 244 | self.assertEqual(result[1]["user"]["username"], "vasman") 245 | 246 | # test keyword = vester 247 | url = self.profile_url + "?keyword=vester" 248 | 249 | response = self.client.get(url, **self.bearer) 250 | result = response.json()["results"] 251 | 252 | self.assertEqual(response.status_code, 200) 253 | self.assertEqual(len(result), 1) 254 | self.assertEqual(result[0]["user"]["username"], "tester") 255 | -------------------------------------------------------------------------------- /user_control/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from .views import ( 3 | LoginView, RegisterView, RefreshView, UserProfileView, MeView, LogoutView, 4 | UpdateFavoriteView, CheckIsFavoriteView 5 | ) 6 | from rest_framework.routers import DefaultRouter 7 | 8 | router = DefaultRouter(trailing_slash=False) 9 | 10 | router.register("profile", UserProfileView) 11 | 12 | urlpatterns = [ 13 | path('', include(router.urls)), 14 | path('login', LoginView.as_view()), 15 | path('register', RegisterView.as_view()), 16 | path('refresh', RefreshView.as_view()), 17 | path('me', MeView.as_view()), 18 | path('logout', LogoutView.as_view()), 19 | path('update-favorite', UpdateFavoriteView.as_view()), 20 | path('check-favorite/', CheckIsFavoriteView.as_view()), 21 | ] 22 | -------------------------------------------------------------------------------- /user_control/views.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | from .models import Jwt, CustomUser, Favorite 3 | from datetime import datetime, timedelta 4 | from django.conf import settings 5 | import random 6 | import string 7 | from rest_framework.views import APIView 8 | from .serializers import ( 9 | LoginSerializer, RegisterSerializer, RefreshSerializer, UserProfileSerializer, UserProfile, FavoriteSerializer 10 | ) 11 | from django.contrib.auth import authenticate 12 | from rest_framework.response import Response 13 | from .authentication import Authentication 14 | from chatapi.custom_methods import IsAuthenticatedCustom 15 | from rest_framework.viewsets import ModelViewSet 16 | import re 17 | from django.db.models import Q, Count, Subquery, OuterRef 18 | 19 | 20 | def get_random(length): 21 | return ''.join(random.choices(string.ascii_uppercase + string.digits, k=length)) 22 | 23 | 24 | def get_access_token(payload): 25 | return jwt.encode( 26 | {"exp": datetime.now() + timedelta(minutes=5), **payload}, 27 | settings.SECRET_KEY, 28 | algorithm="HS256" 29 | ) 30 | 31 | 32 | def get_refresh_token(): 33 | return jwt.encode( 34 | {"exp": datetime.now() + timedelta(days=365), "data": get_random(10)}, 35 | settings.SECRET_KEY, 36 | algorithm="HS256" 37 | ) 38 | 39 | 40 | def decodeJWT(bearer): 41 | if not bearer: 42 | return None 43 | 44 | token = bearer[7:] 45 | decoded = jwt.decode(token, key=settings.SECRET_KEY) 46 | if decoded: 47 | try: 48 | return CustomUser.objects.get(id=decoded["user_id"]) 49 | except Exception: 50 | return None 51 | 52 | 53 | class LoginView(APIView): 54 | serializer_class = LoginSerializer 55 | 56 | def post(self, request): 57 | serializer = self.serializer_class(data=request.data) 58 | serializer.is_valid(raise_exception=True) 59 | 60 | user = authenticate( 61 | username=serializer.validated_data['username'], 62 | password=serializer.validated_data['password']) 63 | 64 | if not user: 65 | return Response({"error": "Invalid username or password"}, status="400") 66 | 67 | Jwt.objects.filter(user_id=user.id).delete() 68 | 69 | access = get_access_token({"user_id": user.id}) 70 | refresh = get_refresh_token() 71 | 72 | Jwt.objects.create( 73 | user_id=user.id, access=access.decode(), refresh=refresh.decode() 74 | ) 75 | 76 | return Response({"access": access, "refresh": refresh}) 77 | 78 | 79 | class RegisterView(APIView): 80 | serializer_class = RegisterSerializer 81 | 82 | def post(self, request): 83 | serializer = self.serializer_class(data=request.data) 84 | serializer.is_valid(raise_exception=True) 85 | 86 | username = serializer.validated_data.pop("username") 87 | 88 | CustomUser.objects.create_user(username=username, **serializer.validated_data) 89 | 90 | return Response({"success": "User created."}, status=201) 91 | 92 | 93 | class RefreshView(APIView): 94 | serializer_class = RefreshSerializer 95 | 96 | def post(self, request): 97 | serializer = self.serializer_class(data=request.data) 98 | serializer.is_valid(raise_exception=True) 99 | 100 | try: 101 | active_jwt = Jwt.objects.get( 102 | refresh=serializer.validated_data["refresh"]) 103 | except Jwt.DoesNotExist: 104 | return Response({"error": "refresh token not found"}, status="400") 105 | if not Authentication.verify_token(serializer.validated_data["refresh"]): 106 | return Response({"error": "Token is invalid or has expired"}) 107 | 108 | access = get_access_token({"user_id": active_jwt.user.id}) 109 | refresh = get_refresh_token() 110 | 111 | active_jwt.access = access.decode() 112 | active_jwt.refresh = refresh.decode() 113 | active_jwt.save() 114 | 115 | return Response({"access": access, "refresh": refresh}) 116 | 117 | 118 | class UserProfileView(ModelViewSet): 119 | queryset = UserProfile.objects.all() 120 | serializer_class = UserProfileSerializer 121 | permission_classes = (IsAuthenticatedCustom, ) 122 | 123 | def get_queryset(self): 124 | if self.request.method.lower() != "get": 125 | return self.queryset 126 | 127 | data = self.request.query_params.dict() 128 | data.pop("page", None) 129 | keyword = data.pop("keyword", None) 130 | 131 | if keyword: 132 | search_fields = ( 133 | "user__username", "first_name", "last_name", "user__email" 134 | ) 135 | query = self.get_query(keyword, search_fields) 136 | try: 137 | return self.queryset.filter(query).filter(**data).exclude( 138 | Q(user_id=self.request.user.id) | 139 | Q(user__is_superuser=True) 140 | ).annotate( 141 | fav_count=Count(self.user_fav_query(self.request.user)) 142 | ).order_by("-fav_count") 143 | except Exception as e: 144 | raise Exception(e) 145 | 146 | result = self.queryset.filter(**data).exclude( 147 | Q(user_id=self.request.user.id) | 148 | Q(user__is_superuser=True) 149 | ).annotate( 150 | fav_count=Count(self.user_fav_query(self.request.user)) 151 | ).order_by("-fav_count") 152 | return result 153 | 154 | @staticmethod 155 | def user_fav_query(user): 156 | try: 157 | return user.user_favorites.favorite.filter(id=OuterRef("user_id")).values("pk") 158 | except Exception: 159 | return [] 160 | 161 | 162 | @staticmethod 163 | def get_query(query_string, search_fields): 164 | query = None # Query to search for every search term 165 | terms = UserProfileView.normalize_query(query_string) 166 | for term in terms: 167 | or_query = None # Query to search for a given term in each field 168 | for field_name in search_fields: 169 | q = Q(**{"%s__icontains" % field_name: term}) 170 | if or_query is None: 171 | or_query = q 172 | else: 173 | or_query = or_query | q 174 | if query is None: 175 | query = or_query 176 | else: 177 | query = query & or_query 178 | return query 179 | 180 | @staticmethod 181 | def normalize_query(query_string, findterms=re.compile(r'"([^"]+)"|(\S+)').findall, normspace=re.compile(r'\s{2,}').sub): 182 | return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)] 183 | 184 | 185 | class MeView(APIView): 186 | permission_classes = (IsAuthenticatedCustom, ) 187 | serializer_class = UserProfileSerializer 188 | 189 | def get(self, request): 190 | data = {} 191 | try: 192 | data = self.serializer_class(request.user.user_profile).data 193 | except Exception: 194 | data = { 195 | "user": { 196 | "id": request.user.id 197 | } 198 | } 199 | return Response(data, status=200) 200 | 201 | 202 | class LogoutView(APIView): 203 | permission_classes = (IsAuthenticatedCustom, ) 204 | 205 | def get(self, request): 206 | user_id = request.user.id 207 | 208 | Jwt.objects.filter(user_id=user_id).delete() 209 | 210 | return Response("logged out successfully", status=200) 211 | 212 | 213 | class UpdateFavoriteView(APIView): 214 | permission_classes = (IsAuthenticatedCustom,) 215 | serializer_class = FavoriteSerializer 216 | 217 | def post(self, request, *args, **kwargs): 218 | serializer = self.serializer_class(data=request.data) 219 | serializer.is_valid(raise_exception=True) 220 | try: 221 | favorite_user = CustomUser.objects.get(id=serializer.validated_data["favorite_id"]) 222 | except Exception: 223 | raise Exception("Favorite user does not exist") 224 | 225 | try: 226 | fav = request.user.user_favorites 227 | except Exception: 228 | fav = Favorite.objects.create(user_id=request.user.id) 229 | 230 | favorite = fav.favorite.filter(id=favorite_user.id) 231 | if favorite: 232 | fav.favorite.remove(favorite_user) 233 | return Response("removed") 234 | 235 | fav.favorite.add(favorite_user) 236 | return Response("added") 237 | 238 | 239 | class CheckIsFavoriteView(APIView): 240 | permission_classes = (IsAuthenticatedCustom,) 241 | 242 | def get(self, request, *args, **kwargs): 243 | favorite_id = kwargs.get("favorite_id", None) 244 | try: 245 | favorite = request.user.user_favorites.favorite.filter(id=favorite_id) 246 | if favorite: 247 | return Response(True) 248 | return Response(False) 249 | except Exception: 250 | return Response(False) 251 | 252 | --------------------------------------------------------------------------------