0% found this document useful (0 votes)
65 views94 pages

Untitled Document-11

The document contains code for an API for a social media application. It defines models, serializers, views, tests, and URLs for the API. Specifically: - It defines models for users, profiles, posts and friends. - It defines serializers to serialize the models for the API. - It defines API views like lists, details, create and filters for users, profiles, posts and friends. - It includes test cases to test the API views. - It defines URLs to map to the API views.

Uploaded by

Fred
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
65 views94 pages

Untitled Document-11

The document contains code for an API for a social media application. It defines models, serializers, views, tests, and URLs for the API. Specifically: - It defines models for users, profiles, posts and friends. - It defines serializers to serialize the models for the API. - It defines API views like lists, details, create and filters for users, profiles, posts and friends. - It includes test cases to test the API views. - It defines URLs to map to the API views.

Uploaded by

Fred
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 94

from django.

contrib import admin

# Register your models here.

from .serializers import *


from rest_framework import generics
from socialnetwork.models import UserProfile
from rest_framework.permissions import IsAdminUser

class UserList(generics.ListAPIView):
"""
GET request
returns all users
"""
queryset = User.objects.all().distinct()
serializer_class = UserSerializer

class UserProfileDetails(generics.RetrieveAPIView):
"""
GET request
receives user as parameter
returns user profile data
"""

lookup_field = 'user__username'
queryset = UserProfile.objects.all()
serializer_class = UserProfileSerializer

class CreateUserProfile(generics.CreateAPIView):
"""
Post request
Creates new user profile
"""

serializer_class = UserProfileSerializer
class UserPostsList(generics.ListAPIView):
"""
GET request
returns all posts from an user
"""
queryset = Post.objects.all().distinct()
serializer_class = PostSerializer

"""
Filter the list of posts of a given author
"""
def filter_queryset(self, queryset):
return queryset.filter(author__username=self.kwargs.get('user__username'))

class UserFriendsDetails(generics.RetrieveAPIView):
"""
GET request
receives user as parameter
returns user's friends list
"""
lookup_field = 'user__username'
queryset = UserProfile.objects.all()
serializer_class = FriendSerializer

from django.apps import AppConfig

class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'

import factory
from django.contrib.auth.models import User
from socialnetwork.models import Post, UserProfile
from django.utils.crypto import get_random_string
class UserFactory(factory.django.DjangoModelFactory):
"""Creates User test fixture"""

username = get_random_string(length=6)
first_name = get_random_string(length=6)
last_name = get_random_string(length=6)

class Meta:
model = User

class UserProfileFactory(factory.django.DjangoModelFactory):
"""Creates UserProfile test fixture"""

user = factory.SubFactory(UserFactory)
birthdate = '2010-12-12'

class Meta:
model = UserProfile

class PostFactory(factory.django.DjangoModelFactory):
"""Creates Post test fixture"""

text = get_random_string(length=10)

class Meta:
model = Post
from django.db import models

# Create your models here.

from django.contrib.auth.models import User


from rest_framework import serializers
from socialnetwork.models import UserProfile, Post

class UserSerializer(serializers.ModelSerializer):

class Meta:
model = User
fields = ('username', 'first_name', 'last_name', 'email', 'date_joined')

class UserProfileSerializer(serializers.ModelSerializer):

user = UserSerializer(required=True)
friends = UserSerializer(many=True, read_only=True)

class Meta:
model = UserProfile
fields = ('user', 'birthdate', 'profile_picture', 'friends')

def create(self, validated_data):

# create user
user = User.objects.create(
username = validated_data['user']['username'],
first_name = validated_data['user']['first_name'],
last_name = validated_data['user']['last_name'],
email = validated_data['user']['email'],
)

# create profile
profile = UserProfile.objects.create(
user = user,
birthdate = validated_data['birthdate'],
profile_picture = validated_data['profile_picture'],
)

return profile

class PostSerializer(serializers.ModelSerializer):

class Meta:
model = Post
fields = ('text', 'image', 'created_date')

class FriendSerializer(serializers.ModelSerializer):

friends = UserSerializer(many=True, read_only=True)


class Meta:
model = UserProfile
fields = ('friends',)
from django.contrib.auth.models import User
from socialnetwork.models import UserProfile
from api.model_factories import UserFactory, UserProfileFactory
from django.test import TestCase
from rest_framework.test import APITestCase
from django.urls import reverse
import json

# Create your tests here.

class GetUserTest(APITestCase):
"""
Test module for /profile/<username> GET request
"""

user1 = None
good_url = ''
bad_url = ''

def setUp(self):
# Create user
self.user1 = UserProfileFactory.create()

# Set urls
self.good_url = reverse('api:profile_api', kwargs={'user__username':
self.user1.user.username})

def tearDown(self):

# Reset test tables


UserProfile.objects.all().delete()
User.objects.all().delete()

def test_userprofile_return_success(self):
"""
Ensure we get an 200 OK status code when making a valid GET request to
profile/<str:user__username>.
"""
response = self.client.get(self.good_url, format='json')
response.render()
self.assertEqual(response.status_code, 200)

def test_userprofile_return_correct_data(self):
"""
Ensure we get the birthdate of the requested user
"""
response = self.client.get(self.good_url, format='json')
response.render()
data = json.loads(response.content)
self.assertIn(data['birthdate'], self.user1.birthdate)

class GetAllUsersTest(APITestCase):
"""
Test module for /profiles GET request
"""

user1 = None
user2 = None
good_url = ''
bad_url = ''

def setUp(self):
# Create user
self.userprofile1 = UserProfileFactory.create()
self.user2 = UserFactory.create(username='test2', first_name='test2',
last_name='test2')
self.userprofile2 = UserProfileFactory.create(user=self.user2,
birthdate='2010-10-10')

# Set urls
self.good_url = reverse('api:profile_list_api')

def tearDown(self):

# Reset test tables


UserProfile.objects.all().delete()
User.objects.all().delete()

def test_userprofiles_return_success(self):
"""
Ensure we get an 200 OK status code when making a valid GET request to
/profiles
"""
response = self.client.get(self.good_url, format='json')
response.render()
self.assertEqual(response.status_code, 200)

def test_userprofiles_return_correct_data(self):
"""
Ensure we get the correct number of users
"""
response = self.client.get(self.good_url, format='json')
response.render()
data = json.loads(response.content)
self.assertEqual(len(data), 2)

from django.urls import path


from . import api

app_name = 'api'

urlpatterns = [
path('profiles/', api.UserList.as_view(), name='profile_list_api'),
path('profile/<str:user__username>/', api.UserProfileDetails.as_view(),
name='profile_api'),
path('profile/', api.CreateUserProfile.as_view(), name='create_profile_api'),
path('posts/<str:user__username>/', api.UserPostsList.as_view(), name='posts_api'),
path('friends/<str:user__username>/', api.UserFriendsDetails.as_view(),
name='friends_api'),
]
from django.shortcuts import render

# Create your views here.

{% include "socialnetwork/base.html" %}

{% block content %}
<div class="container">
<h4 class="my-4">Chat list</h4>
<p> Select a chat from your active chat list to see your messages with that friend.
</p>
<div>
{% if has_chats %}
<ul class="list-chat">
{% for chat_obj, chat_user in chat_list %}
<a href="{% url 'chat:chat_room' chat_user %}"
class="text-decoration-none text-reset">
<li class="list-chat-item d-flex justify-content-between my-3
border p-3" style="width: 60%;">
<div class="d-flex">
<div class="mx-3">
<h5 class="card-title"> {{ chat_user }} </h5>
<span>{{ chat_user.first_name }} {{ chat_user.last_name
}}</span>
</div>
</div>
<div>
<span class="text-left">{{ chat_obj.updated_at }}</span>
</div>
</li>
</a>
{% endfor %}
</ul>
{% else %}
<span>Looks like you haven't started a conversation. Start one.</span>
{% endif %}

<div class="my-3">
<span>Start a new conversation: </span>
<a class="btn btn-primary" href="{% url 'friend:friend_list' user.username
%}"> Go to friends list </a>
</div>
</div>

</div>

{% endblock %}
{% include "socialnetwork/base.html" %}
{% load tz %}
{% block content %}
<div class="container my-3 p-3" style="height: 80vh; width: 70vw; max-width:
500px;">
<h3>Chat between: {{user.username}} and {{friend.username}}</h3>
<div id="message-container" class="overflow-auto d-flex flex-md-column"
style="height: 80%;">
<div id="message-list">
{% for message in messages %}
<div class="text-start m-2 d-flex flex-column
justify-content-start">
<div>
<span class="fw-bold">{{message.sender.username}}</span>
<span class="chat-message-date">{{message.timestamp|localtime}}</span>
</div>
<div>
<span>{{message.content}}</span>
</div>
</div>
{% endfor %}
</div>
</div>
<form id="message-form">
{% csrf_token %}
<textarea class="form-control"name="message" id="message" rows="3"
required></textarea>
<input type="submit" value="Send" class="btn btn-primary my-3">
</form>
</div>
{% endblock content %}

{% block custom_javascript %}
<script>

// scroll to bottom of chat


const element = document.getElementById("message-container");
element.scrollTop = element.scrollHeight;

// web socket connection


const chatSocket = new WebSocket(
'ws://'
+ window.location.host
+ '/ws'
+ window.location.pathname
);

chatSocket.onopen = function(event) {
console.log('Connection is open')
}

chatSocket.onmessage = function(event) {
console.log('Message received', event)

const msg_list = document.getElementById('message-list')


const msg = document.createElement('div')
const msg_container = document.createElement('div')
msg_container.classList.add('m-2','d-flex','flex-column','justify-content-end')
const msg_sender_div = document.createElement('div')
const msg_sender_span = document.createElement('span')
msg_sender_span.classList.add('fw-bold')
const msg_date_span = document.createElement('span')
msg_date_span.classList.add('chat-message-date')
const msg_content_div = document.createElement('div')
const msg_content_span = document.createElement('span')

// set date
const currentdate = new Date().toDateString()
msg_date_span.append(document.createTextNode(
currentdate
))

const data = JSON.parse(event.data)


// set user
msg_sender_span.append(document.createTextNode(
data.username + ' '
))
msg_sender_div.append(msg_sender_span)
msg_sender_div.append(msg_date_span)
msg_container.append(msg_sender_div)

// set content
msg_content_span.append(document.createTextNode(
data.text
))
msg_content_div.append(msg_content_span)
msg_container.append(msg_content_div)

msg_list.append(msg_container)

chatSocket.onclose = function(event) {
console.log('Connection closed', event)
}

chatSocket.onerror = function(event) {
console.log('Something went wrong', event)
}

const messageForm = document.getElementById('message-form')


messageForm.addEventListener('submit', sendMessage)

function sendMessage(event){
if (event.preventDefault) {
event.preventDefault()
}

chatSocket.send(document.getElementById('message').value)
messageForm.reset()
return false
}

</script>
{% endblock custom_javascript %}
from django.contrib import admin
from chat.models import Chat, ChatMessage
# Register your models here.

class MessageInline(admin.StackedInline):
model = ChatMessage
fields = ('sender', 'content')
readonly_fields = ('sender', 'content')

class ChatAdmin(admin.ModelAdmin):
model = Chat
inlines = (MessageInline,)

admin.site.register(Chat, ChatAdmin)
from django.apps import AppConfig

class ChatConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'chat'

from channels.consumer import AsyncConsumer, StopConsumer


from channels.db import database_sync_to_async
from django.contrib.auth.models import User
from chat.models import Chat, ChatMessage
from asgiref.sync import sync_to_async
import json
import datetime

class ChatConsumer(AsyncConsumer):

async def websocket_connect(self, event):


logged_user = self.scope['user']
friend_username = self.scope['url_route']['kwargs']['username']
friend_user = await sync_to_async(User.objects.get)(username=friend_username)
# get or create chat object
self.chat_obj = await
sync_to_async(Chat.objects.get_or_create_chat)(logged_user, friend_user)
self.room_name = f'{self.chat_obj.room_name}'

await self.channel_layer.group_add(self.room_name, self.channel_name)


await self.send({
'type': 'websocket.accept'
})

print(f'[{self.channel_name}] - Connected')

async def websocket_receive(self, event):


print(f'[{self.channel_name}] - Received message - {event["text"]}')

msg = json.dumps({
'text': event.get('text'),
'username': self.scope['user'].username
})

await self.save_message(event.get('text'))

# send to room
await self.channel_layer.group_send(
self.room_name,
{
'type': 'websocket.message',
'text': msg
}
)

async def websocket_message(self, event):


print(f'[{self.channel_name}] - Sent message - {event["text"]}')
await self.send({
'type': 'websocket.send',
'text': event.get('text')
})

async def websocket_disconnect(self, event):


print(f'[{self.channel_name}] - Disconnected')
# remove channel from room name
await self.channel_layer.group_discard(self.room_name, self.channel_name)
raise StopConsumer()

@database_sync_to_async
def save_message(self, content):
"""
Save chat message into the database
"""
# store message in db
ChatMessage.objects.create(
chat = self.chat_obj,
sender = self.scope['user'],
content = content
)

chat = Chat.objects.get(id=self.chat_obj.id)
chat.updated_at = datetime.datetime.now()
chat.save()

from django.db import models


from django.contrib.auth.models import User
from django.db.models import Q

# Create your models here.

class ChatManager(models.Manager):

def get_or_create_chat(self, user1, user2):


"""
Filters and returns the chat between two users if its exists. Otherwise, it
creates it
"""
chat = self.get_queryset().filter((Q(user1=user1) and Q(user2=user2)) |
(Q(user1=user2) and Q(user2=user1)))
if chat.exists():
return chat.first()
else:
chat = self.create(
user1 = user1,
user2 = user2,
)
return chat

def by_user(self, user):


"""
Return list of chats by user
"""
return self.get_queryset().filter(Q(user1=user) | Q(user2=user))

class Chat(models.Model):

user1 = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user1')


user2 = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user2')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = ChatManager()

@property
def room_name(self):
"Returns chat room id"
return f'{self.id}'

class ChatMessageManager(models.Manager):

def order_messages(self, chat):


ordered_messages = ChatMessage.objects.filter(chat=chat).order_by("-timestamp")
return ordered_messages

class ChatMessage(models.Model):

chat = models.ForeignKey(Chat, on_delete=models.CASCADE)


sender = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField(blank=False, null=False)
timestamp = models.DateTimeField(auto_now_add=True)

objects = ChatMessageManager()

def __str__(self) -> str:


return self.content

from django.urls import path, include


from chat.views import ChatView, ChatList

app_name = 'chat'

urlpatterns = [
path('chat_list', ChatList.as_view(), name='chat_list'),
path('chat_room/<str:username>/', ChatView.as_view(), name='chat_room')
]

from django.shortcuts import render


from django.views import View
from django.contrib.auth import get_user_model
from django.shortcuts import Http404
from chat.models import Chat, ChatMessage
from django.utils import timezone

# Create your views here.

class ChatView(View):
template_name = 'chat/chat.html'

def get_object(self):
other_username = self.kwargs.get("username")
self.other_user = get_user_model().objects.get(username=other_username)
obj = Chat.objects.get_or_create_chat(self.request.user, self.other_user)
if obj == None:
raise Http404
return obj

def get(self, request, **kwargs):

context = {}
context['user'] = self.request.user
context['chat'] = self.get_object()
context['friend'] = self.other_user
context['messages'] = self.get_object().chatmessage_set.all()

return render(request, self.template_name, context=context)

class ChatList(View):
template_name = 'chat/chat_list.html'

def get(self, request, **kwargs):

context = {}
chat_list = Chat.objects.by_user(self.request.user)
chat_user_list = []

for chat in chat_list:


if chat.user1 != self.request.user:
chat_user_list.append(chat.user1)
elif chat.user2 != self.request.user:
chat_user_list.append(chat.user2)

context['chat_list'] = zip(chat_list, chat_user_list)

has_chats = True if len(chat_list) > 0 else False

context['has_chats'] = has_chats

return render(request, self.template_name, context)

Friend_list.html
{% include "socialnetwork/base.html" %}

{% block content %}

<div class="container">
<h4 class="my-4">Friends list</h4>
<div class="row row-cols-1 row-cols-md-6 g-6 my-3">
{% for friend in friends %}
<div class="card py-3">
<img src="{{friend.userprofile.profile_picture.url}}" class="rounded
mx-auto d-block" width="50" height="50" />
<div class="card-body text-center">
<h5 class="card-title text-center">
<a href="{% url 'profile' friend.username %}"
style="text-decoration: none">
{{friend.username}}
</a>
</h5>
<span>{{friend.first_name}} {{friend.last_name}}</span>
{% if is_logged_user_profile %}
<a href="{% url 'chat:chat_room' friend.username %}" class="btn
btn-primary my-2">Chat</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>

</div>
{% endblock %}

Friend_request_functions.html
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous"></script>

<script type="text/javascript">

function sendFriendRequest(username, updatedView) {

data = {
"csrfmiddlewaretoken": "{{csrf_token}}",
"receiver_username": username,
}
$.ajax({
type: "POST",
dataType: "json",
url: "{% url 'friend:send_friend_request' %}",
timeout: 3000,
data: data,
success: function(data){
console.log('success: ' + data.response)
},
error: function(data){
console.log('error: ' + data.response)
},
complete: function(data){
console.log('complete: ' + data.response)
updatedView()
},
})
}

function acceptFriendRequest(request_id, updatedView) {

data = {
"csrfmiddlewaretoken": "{{csrf_token}}",
"request_id": request_id,
}
$.ajax({
type: "POST",
dataType: "json",
url: "{% url 'friend:accept_friend_request' %}",
timeout: 3000,
data: data,
success: function(data){
console.log('success: ' + data.response)
},
error: function(data){
console.log('error: ' + data.response)
},
complete: function(data){
console.log('complete: ' + data.response)
updatedView()
},
})
}

function declineFriendRequest(request_id, updatedView) {

data = {
"csrfmiddlewaretoken": "{{csrf_token}}",
"request_id": request_id,
}

$.ajax({
type: "POST",
dataType: "json",
url: "{% url 'friend:decline_friend_request' %}",
timeout: 3000,
data: data,
success: function(data){
console.log('success: ' + data.response)
},
error: function(data){
console.log('error: ' + data.response)
},
complete: function(data){
console.log('complete: ' + data.response)
updatedView()
},
})
}

function cancelFriendRequest(request_id, updatedView) {

data = {
"csrfmiddlewaretoken": "{{csrf_token}}",
"request_id": request_id,
}

$.ajax({
type: "POST",
dataType: "json",
url: "{% url 'friend:cancel_friend_request' %}",
timeout: 3000,
data: data,
success: function(data){
console.log('success: ' + data.response)
},
error: function(data){
console.log('error: ' + data.response)
},
complete: function(data){
console.log('complete: ' + data.response)
updatedView()
},
})
}

</script>

Friend_request_list.html
{% include "socialnetwork/base.html" %}

{% block content %}

<div class="container my-5">


<h4 class="my-4">Friend Requests</h4>
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="pills-received-requests-tab"
data-bs-toggle="pill" data-bs-target="#pills-received-requests" type="button"
role="tab" aria-controls="pills-received-requests" aria-selected="true">Received
({{friend_requests_received|length}})</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-sent-requests-tab" data-bs-toggle="pill"
data-bs-target="#pills-sent-requests" type="button" role="tab"
aria-controls="pills-sent-requests" aria-selected="false">Sent
({{friend_requests_sent|length}})</button>
</li>
</ul>
</div>

<div class="tab-content" id="pills-tabContent">


<div class="tab-pane fade show active" id="pills-received-requests" role="tabpanel"
aria-labelledby="pills-received-requests-tab">
<div class="container">
{% if friend_requests_received %}
<ul class="list-group">
{% for request in friend_requests_received %}
<li class="list-group-item d-flex justify-content-between my-3"
style="width: 60%;">
<div class="request-user d-flex">
<img
src="{{request.sender.userprofile.profile_picture.url}}" class="rounded" width="50"
height="50" />
<div class="mx-3">
<h5 class="card-title">{{request.sender.username}}</h5>
<span>{{request.sender.first_name}}
{{request.sender.last_name}}</span>
</div>
</div>
<div>
<span class="text-left">{{request.datetime}}</span>
<div>
<button class="btn btn-primary btn-sm"
onclick='acceptFriendRequest("{{request.id}}", reloadPage)'>Accept</button>
<button class="btn btn-danger btn-sm"
onclick='declineFriendRequest("{{request.id}}", reloadPage)'>Decline</button>
</div>
</div>
</li>
{% endfor %}
</ul>
{% elif not friend_requests_received%}
<span>No pending friend requests</span>
{% endif %}
</div>
</div>
<div class="tab-pane fade" id="pills-sent-requests" role="tabpanel"
aria-labelledby="pills-sent-requests-tab">
<div class="container">
{% if friend_requests_sent %}
<ul class="list-group">
{% for request in friend_requests_sent %}
<li class="list-group-item d-flex justify-content-between my-3"
style="width: 60%;">
<div class="request-user d-flex">
<img
src="{{request.receiver.userprofile.profile_picture.url}}" class="rounded" width="50"
height="50" />
<div class="mx-3">
<h5
class="card-title">{{request.receiver.username}}</h5>
<span>{{request.receiver.first_name}}
{{request.receiver.last_name}}</span>
</div>
</div>
<div>
<span class="text-left">{{request.datetime}}</span>
<div>
<button class="btn btn-danger btn-sm"
onclick='cancelFriendRequest("{{request.id}}", reloadPage)'>Cancel</button>
</div>
</div>
</li>
{% endfor %}
</ul>
{% elif not friend_requests_sent%}
<span>No pending sent requests</span>
{% endif %}
</div>
</div>
</div>
{% include 'friend/friend_request_functions.html' %}

<script type="text/javascript">

function reloadPage(){
location.reload();
}

</script>

{% endblock %}

Admin.py
from django.contrib import admin

# Register your models here.


from friend.models import *

admin.site.register(FriendRequest)

Apps.py
from django.apps import AppConfig

class FriendConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'friend'

Helpers.py
from friend.models import FriendRequest

def find_friend_request(user_sender, user_receiver):


"""
Returns request by sender and receiver if found. Otherwise returns false
"""
try:
return FriendRequest.objects.get(sender=user_sender, receiver=user_receiver,
is_active=True)
except FriendRequest.DoesNotExist:
return False

Models.py
from django.db import models
from django.conf import settings
from django.utils import timezone
from django.contrib.auth.models import User
from socialnetwork.models import UserProfile

# Create your models here.


class FriendRequest(models.Model):
"""
Friend request involves user sending it and the one receiving it
"""
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sender')
receiver = models.ForeignKey(User, on_delete=models.CASCADE,
related_name='receiver')
datetime = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(blank=True, null=False, default=True)

def accept_request(self):
"""
Add both users to each other's friends list
"""
sender = self.sender
receiver = self.receiver

# add friend to friends list


if sender and receiver:
sender.userprofile.friends.add(receiver)
receiver.userprofile.friends.add(sender)
# deactivate request
self.is_active = False
self.save()

def deactivate_request(self):
"""
Cancel or decline friend request
"""
self.is_active = False
self.save()
def activate_request(self):
"""
Activate friend request. Shows up again
"""
self.is_active = True
self.save()

Tests.py
from django.test import TestCase

# Create your tests here.

Urls.py
from django.urls import path
from . import views
from django.contrib.auth.decorators import login_required

app_name = 'friend'

urlpatterns = [
path('send_friend_request/', views.send_friend_request, name =
'send_friend_request'),
path('accept_friend_request/', views.accept_friend_request, name =
'accept_friend_request'),
path('decline_friend_request/', views.decline_friend_request, name =
'decline_friend_request'),
path('cancel_friend_request/', views.cancel_friend_request, name =
'cancel_friend_request'),
path('friend_request_list/', views.friend_request_list, name =
'friend_request_list'),
path('friend_list/<str:username>', views.friend_list, name='friend_list'),
]

Views.py
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from django.contrib import messages
from django.http import JsonResponse
from django.views.generic.base import View

from socialnetwork.models import UserProfile


from friend.models import *
# Create your views here.

@login_required
def send_friend_request(request, *args, **kwargs):
"""
Creates a new friend request with the logged user as sender and provided user as
received
"""

user = request.user
data = {}

if request.method == "POST" and user.is_authenticated:


receiver_username = request.POST.get("receiver_username")

if receiver_username:
receiver = User.objects.get(username=receiver_username)
# get user friends list
user_friends = user.userprofile.friends.all()

is_friend = user_friends.filter(username=receiver.username)
# not friend
if not is_friend:

# get friend requests


friend_requests = FriendRequest.objects.filter(sender=user,
receiver=receiver)

# user has sent friend requests


if friend_requests:
for friend_request in friend_requests:
if friend_request.is_active:
raise Exception('Friend request sent')
else:
try:
# activate friend request
friend_request.activate_request()
data['response'] = "Friend request sent successfully"
except Exception as e:
data['response'] = str(e)
else:
# no friend requests
friend_request = FriendRequest(sender=user, receiver=receiver)
friend_request.save()
data['response'] = "Friend request sent successfully"
else:
data['response'] = "No receiver username provided"
else:
data['response'] = "User not authenticated"

return JsonResponse(data)

@login_required
def accept_friend_request(request, *args, **kwargs):
"""
Both users are added to each other's friendlist. Request is then set as not active
"""

user = request.user
data = {}

if request.method == "POST" and user.is_authenticated:


request_id = request.POST.get("request_id")

if request_id:
# get the request
friend_request = FriendRequest.objects.get(id=request_id)
# accept if logged user is the receiver of the request
if friend_request.receiver == user:

friend_request.accept_request()
data["response"] = "Friend request accepted"
# if logged user is not the receiver then its the wrong request
else:
data["response"] = "Error. not your request"

else:
data['response'] = "No request"
else:
data['response'] = "User not authenticated"
return JsonResponse(data)

@login_required
def decline_friend_request(request, *args, **kwargs):
"""
Friend request object is set to inactive
"""

user = request.user
data = {}

if request.method == "POST" and user.is_authenticated:


request_id = request.POST.get("request_id")

if request_id:
# get the request
friend_request = FriendRequest.objects.get(id=request_id)
# decline if logged user is the receiver of the request
if friend_request.receiver == user:

friend_request.deactivate_request()
data["response"] = "Friend request declined"

else:
data["response"] = "Error. not your request"

else:
data['response'] = "No request"
else:
data['response'] = "User not authenticated"

return JsonResponse(data)

@login_required
def cancel_friend_request(request, *args, **kwargs):
"""
Friend request object is set to inactive
"""

user = request.user
data = {}
if request.method == "POST" and user.is_authenticated:
request_id = request.POST.get("request_id")

if request_id:
# get the request
friend_request = FriendRequest.objects.get(id=request_id)
# inactivate request if logged user sent it and the request is still active
if friend_request.sender == user and friend_request.is_active:

friend_request.deactivate_request()
data["response"] = "Friend request cancelled"

else:
data["response"] = "Error. not your request"

else:
data['response'] = "No request"
else:
data['response'] = "User not authenticated"

return JsonResponse(data)

@login_required
def friend_request_list(request, *args, **kwargs):
"""
Returns list of friend requests sent and received of the logged user
"""

user = request.user
context = {}

if user.is_authenticated:

# get friend requests received


friend_requests_received = FriendRequest.objects.filter(receiver=user,
is_active=True)
context['friend_requests_received'] = friend_requests_received

# get friend requests sent


friend_requests_sent = FriendRequest.objects.filter(sender=user,
is_active=True)
context['friend_requests_sent'] = friend_requests_sent

return render(request, "friend/friend_request_list.html", context)

else:
messages.error(request, 'User no authenticated')

@login_required
def friend_list(request, username, *args, **kwargs):
"""
Returns list of friends of given username
"""

user = request.user

if user.is_authenticated:

user_profile = User.objects.get(username=username)

if user_profile:

# get friends
friends = user_profile.userprofile.friends.all()
is_logged_user_profile = True if user.username == username else False

context = {
'friends': friends,
'is_logged_user_profile': is_logged_user_profile
}

return render(request, "friend/friend_list.html", context)


else:
messages.error(request, 'User does not exist')

else:
messages.error(request, 'User no authenticated')
Templates
Base.html
{% load static %}
<!doctype html>
<html lang="en">
{% include "./header.html" %}
<body>
{% include "./navbar.html" %}

<div class="container-fluid">
{% block content %}
{% endblock content %}
</div>

{% include "./scripts.html" %}
</body>
</html>

Header.html
{% load static %}
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<!-- Bootstrap CSS -->


<link
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
crossorigin="anonymous">

<!-- Load css -->


<link rel="stylesheet" href = "{% static 'css/styles.css' %}">

<title>Social Network</title>
</head>

Home.html
{% extends "./base.html" %}

{% block content %}
<div class="container my-3">
{% if user.is_authenticated %}
<p class="text-center py-3">Welcome, {{ user.username }}. Thanks for logging
in.</p>

{% if messages %}
<ul class="messages">
{% for message in messages %}
<div {% if message.tags %}
{% if message.tags == "error" %}
class="alert alert-danger"
{% endif %}
class="alert alert-{{ message.tags }}"
{% endif %}
role="alert">
{{ message }}
</div>
{% endfor %}
</ul>
{% endif %}

<div class="container p-3" style="width: 70vw;">


<form class="mx-1 mx-md-4" id="post_form" method="post" action=""
enctype="multipart/form-data">

{% csrf_token %}

<div>
<div>
<label class="form-label" for="text"></label>
{{ post_form.text }}
</div>
<p class="text-danger"><small>{{ post_form.text.errors.0
}}</small></p>
</div>

<div class="ps-3 d-flex flex-row align-items-center">


<div class="d-flex">
<label class="form-label" for="image">Add image (Optional):
&nbsp </label>
{{ post_form.image }}
</div>
<p class="text-danger"><small>{{ post_form.image.errors.0
}}</small></p>
</div>

<div class="d-grip gap-2 d-flex justify-content-center">


<input type="submit" name="submit" class="btn btn-primary"
value="Post" />
</div>

</form>
</div>

<div class="container my-3" style="width: 70vw;">


<h5>Latest news from your friends</h5>
{% for post in post_list %}
<div class="card mb-3 my-3" style="max-width: 600px; margin: 0 auto;">
<div class="card-header d-flex justify-content-between
align-items-center">
<div class="author-details d-flex align-items-center">
<img src="{{ post.author.userprofile.profile_picture.url
}}" class="rounded" width="50" height="50" />
<a href="{% url 'profile' post.author.username %}"
class="text-decoration-none text-reset mx-3 my-0 fw-bold"> {{ post.author.username }}
</a>
</div>
<div>
<p class="card-text"><small class="text-muted">{{
post.created_date }}</small></p>
</div>
</div>
<div class="card-body">
<p class="card-text">{{ post.text }}</p>
{% if post.image %}
<img src="{{ post.image.url }}" class="rounded mx-auto
d-block" alt="..." style="height: 300px; width:250px;">
{% endif %}
</div>
</div>
{% endfor %}
</div>

{% else %}
<p>Welcome, new user. Please log in.</p>
{% endif %}
</div>

{% endblock %}

Login.html
{% load static %}
<!doctype html>
<html lang="en">
{% include "./header.html" %}
<body>
<div class="vh-100" >
<div class="container h-100">
<div class="row d-flex justify-content-center align-items-center
h-100">
<div class="col-md-10 col-lg-6">
<div class="card text-black" style="border-radius: 25px;">
<div class="card-body p-md-5">
<div class="row justify-content-center">

{% if messages %}
<ul class="messages">
{% for message in messages %}
<div {% if message.tags %}
{% if message.tags == "error" %}
class="alert alert-danger"
{% endif %}
class="alert alert-{{ message.tags }}"
{% endif %}
role="alert">
{{ message }}
</div>
{% endfor %}
</ul>
{% endif %}
<p class="text-center h1 fw-bold mb-5 mx-1 mx-md-4
mt-4">Login</p>

<form class="mx-1 mx-md-4" id="login_form" method="post"


action=""
enctype="multipart/form-data">

{% csrf_token %}

<div class="d-flex flex-row align-items-center">


<i class="fas fa-user fa-lg me-3 fa-fw"></i>
<div class="form-outline flex-fill mb-0">
<label class="form-label"
for="username">Username</label>
<input type="text" name="username"
class="form-control" />
</div>
</div>

<div class="d-flex flex-row align-items-center my-3">


<i class="fas fa-user fa-lg me-3 fa-fw"></i>
<div class="form-outline flex-fill mb-0">
<label class="form-label"
for="password">Password</label>
<input type="password" name="password"
class="form-control" />
</div>
</div>

<div class="d-flex justify-content-center mb-3">


<input type="submit" name="submit" class="btn
btn-primary btn-lg" value="Login" />
</div>

</form>

<div class="mt-4">
<div class="d-flex flex-column justify-content-center
align-items-center">
<p class="m-0">Create an account</p>
<a href="{% url 'signup' %}">Sign Up</a>
</div>
</div>

</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% include "./scripts.html" %}
</body>
</html>

Navbar.html
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'home' %}">Home</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse d-flex justify-content-between"
id="navbarSupportedContent">
<form class="d-flex" method="get" action="{% url 'user-search' %}">
<input class="form-control me-2" type="search" placeholder="Search
users" aria-label="Search" name="query" value={{request.GET.query}}>
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
<div class="navbar-nav d-flex align-items-center">
<div>
<a class="nav-item nav-link" href="{% url 'chat:chat_list' %}">
Messages
</a>
</div>
<div>
<a class="nav-link d-flex align-items-center" href="{% url
'profile' request.user.username %}">
<img src="{{ request.user.userprofile.profile_picture.url }}"
class="rounded" width="35" height="35" />
<p class="mx-2 my-0"> {{ request.user.username }} </p>
</a>
</div>
<div class="nav-item active">
<a class="nav-link" href="{% url 'logout' %}"> Logout </a>
</div>
</div>
</div>
</div>
</nav>

Post_list.html
{% include "./base.html" %}

{% block content %}
<h4>Home</h4>

<form class="mx-1 mx-md-4" id="post_form" method="post" action=""


enctype="multipart/form-data">

{% csrf_token %}
{{ post_form }}

<div class="d-grip gap-2 d-flex justify-content-center">


<input type="submit" name="submit" class="btn btn-primary btn-lg"
value="Post" />
</div>

</form>

{% for post in post_list %}


<h1> {{ post.text }} </h1>
<h1> {{ post.created_date }} </h1>
{% endfor %}

{% endblock %}

Profile_edit.html
{% include "./base.html" %}
{% block content %}

<div class="container my-5">


<div class="row">
<div class="col-md-3">
<div class="card card-body">
<h5 class="text-center card-title"> {{ profile.user.username }}</h5>

<img src="{{ profile.profile_picture.url }}" class="card-img-top"


width="100" height="300" />
</div>
</div>

<div class="col-md-9">
<div class="card card-body p-4">

<h4 class="text-center card-title"> Edit Profile </h4>

<form class="p-4 my-4" id="profile_form" method="post" action=""


enctype="multipart/form-data">

{% csrf_token %}

<div class="d-flex flex-row align-items-center">


<i class="fas fa-user fa-lg me-3 fa-fw"></i>
<div class="form-outline flex-fill mb-0">
<label class="form-label" for="first_name">First
Name</label>
{{ user_form.first_name }}
<p class="text-danger"><small>{{
user_form.first_name.errors.0 }}</small></p>
</div>
</div>

<div class="d-flex flex-row align-items-center">


<i class="fas fa-user fa-lg me-3 fa-fw"></i>
<div class="form-outline flex-fill mb-0">
<label class="form-label" for="last_name">Last Name</label>
{{ user_form.last_name }}
<p class="text-danger"><small>{{
user_form.last_name.errors.0 }}</small></p>
</div>
</div>

<div class="d-flex flex-row align-items-center">


<i class="fas fa-envelope fa-lg me-3 fa-fw"></i>
<div class="form-outline flex-fill mb-0">
<label class="form-label" for="email">Email</label>
{{ user_form.email }}
<p class="text-danger"><small>{{ user_form.email.errors.0
}}</small></p>
</div>
</div>

<div class="d-flex flex-row align-items-center">


<i class="fas fa-lock fa-lg me-3 fa-fw"></i>
<div class="form-outline flex-fill mb-0">
<label class="form-label" for="birthdate">Birthdate</label>
{{ profile_form.birthdate }}
<p class="text-danger"><small>{{
profile_form.birthdate.errors.0 }}</small></p>
</div>
</div>

<div class="ps-3 d-flex flex-row align-items-center">


<div class="d-flex flex-column">
<label class="form-label" for="profile_picture">Profile
picture</label>
{{ profile_form.profile_picture }}
</div>
<p class="text-danger"><small>{{
profile_form.profile_picture.errors.0 }}</small></p>
</div>

<div class="d-grip gap-2 d-flex justify-content-center pt-4">


<input type="submit" name="submit" class="btn btn-primary
btn-lg" value="Save Profile" />
</div>

</form>
</div>
</div>
</div>
</div>
{% endblock %}

Profile.html
{% include "./base.html" %}

{% block content %}

<div class="container my-5">

{% if messages %}
<ul class="messages">
{% for message in messages %}
<div {% if message.tags %}
{% if message.tags == "error" %}
class="alert alert-danger"
{% endif %}
class="alert alert-{{ message.tags }}"
{% endif %}
role="alert">
{{ message }}
</div>
{% endfor %}
</ul>
{% endif %}

<div class="d-flex shadow-sm p-3 mb-5 bg-body rounded">


<div class="d-flex" style="width: 70%;">
<div>
{% if profile.profile_picture %}
<img src="{{ profile.profile_picture.url }}" width="200" height="200"
/>
{% endif %}
</div>
<div class="m-4">
<h4> User: {{ profile.user.username }} </h4>

{% if profile.user.first_name %}
<h3>Name: {{ profile.user.first_name }} {{ profile.user.last_name
}}</h3>
{% endif %}

{% if profile.birthdate %}
<p>Birthdate: {{ profile.birthdate }} </p>
{% endif %}

{% if is_logged_user %}
<a class="btn btn-warning" href="{% url 'profile-edit' %}"> Edit
Profile</a>
{% endif %}

{% if is_friend %}
<span class="badge bg-primary">Friend</span>
{% endif %}

<!-- if not own profile and not a friend, then show request status -->
{% if not is_friend and not is_logged_user %}
{% if sent_request == 'no_request' %}
<button class="btn btn-primary btn-sm"
onclick='sendFriendRequest("{{profile.user.username}}", reloadPage)'> Send Friend
Request</button>
{% endif %}
{% if sent_request == 'sent_to_user' %}
<p>Pending friend request:</p>
<div class="d-flex">
<button class="btn btn-primary btn-sm"
onclick='acceptFriendRequest("{{pending_to_user_id}}", reloadPage)'> Accept </button>
<button class="btn btn-danger btn-sm mx-3"
onclick='declineFriendRequest("{{pending_to_user_id}}", reloadPage)'> Decline
</button>
</div>
{% endif %}
{% if sent_request == 'sent_to_them' %}
<div class="d-flex">
<button class="btn btn-danger btn-sm"
onclick='cancelFriendRequest("{{pending_to_them_id}}", reloadPage)'> Cancel Friend
Request</button>
</div>
{% endif %}
{% endif %}

</div>
</div>
<div class="d-flex flex-column justify-content-center">
<a class="my-3">
<div>
<a class="btn btn-primary" href="{% url 'friend:friend_list'
profile.user.username %}">Friends ({{ friends|length }})</a>
</div>
</a class="my-3">
{% if is_logged_user %}
<a>
<div>
<a class="btn btn-warning" href="{% url
'friend:friend_request_list' %}">Friend requests ({{ friend_requests_received|length
}})</a>
</div>
</a>
{% endif %}
</div>
</div>

<div class="d-flex flex-column shadow-sm p-3 mb-5 bg-body rounded">

<h3 class="my-3">Activity</h3>

{% if post_list %}
{% for post in post_list %}

<div class="card my-3">


<div class="card-header d-flex justify-content-between
align-items-center">
<div class="author-details d-flex align-items-center">
<img src="{{ post.author.userprofile.profile_picture.url }}"
class="rounded" width="50" height="50" />
<p class="mx-3 my-0 fw-bold">{{ post.author.first_name }} {{
post.author.last_name }}</p>
</div>
<div>
<p class="my-0"> {{ post.created_date }} </p>
</div>
</div>
<div class="card-body">
<p class="card-text"> {{ post.text }} </p>
{% if post.image %}
<img src="{{ post.image.url }}" class="rounded mx-auto d-block"
alt="..." style="height: 300px; width:250px;">
{% endif %}
</div>
</div>

{% endfor %}

{% else %}
<p>No posts to show</p>
{% endif %}

</div>

</div>
{% include 'friend/friend_request_functions.html' %}

<script type="text/javascript">

function reloadPage(){
location.reload();
}

</script>

{% endblock %}

Scripts.html*************
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous"></script>
Signup.html
{% load static %}
<!doctype html>
<html lang="en">
{% include "./header.html" %}
<body>
<div class="vh-100" >
<div class="container h-100">
<div class="row d-flex justify-content-center align-items-center
h-100">
<div class="col-lg-10">
<div class="card text-black" style="border-radius: 25px;">
<div class="card-body p-md-5">
<div class="row justify-content-center">

<div class="col-md-10 col-lg-6 order-2 order-lg-2">

<p class="text-center h1 fw-bold mb-5 mx-1 mx-md-4


mt-4">Sign up</p>

<form class="mx-1 mx-md-4" id="signup_form" method="post"


action=""
enctype="multipart/form-data">

{% csrf_token %}

<div class="d-flex flex-row align-items-center">


<i class="fas fa-user fa-lg me-3 fa-fw"></i>
<div class="form-outline flex-fill mb-0">
<label class="form-label"
for="first_name">First Name</label>
{{ user_form.first_name }}
<p class="text-danger"><small>{{
user_form.first_name.errors.0 }}</small></p>
</div>
</div>

<div class="d-flex flex-row align-items-center">


<i class="fas fa-user fa-lg me-3 fa-fw"></i>
<div class="form-outline flex-fill mb-0">
<label class="form-label" for="last_name">Last
Name</label>
{{ user_form.last_name }}
<p class="text-danger"><small>{{
user_form.last_name.errors.0 }}</small></p>
</div>
</div>

<div class="d-flex flex-row align-items-center">


<i class="fas fa-lock fa-lg me-3 fa-fw"></i>
<div class="form-outline flex-fill mb-0">
<label class="form-label"
for="birthdate">Birthdate</label>
{{ profile_form.birthdate }}
<p class="text-danger"><small>{{
profile_form.birthdate.errors.0 }}</small></p>
</div>
</div>

<div class="d-flex flex-row align-items-center">


<i class="fas fa-envelope fa-lg me-3 fa-fw"></i>
<div class="form-outline flex-fill mb-0">
<label class="form-label"
for="email">Email</label>
{{ user_form.email }}
<p class="text-danger"><small>{{
user_form.email.errors.0 }}</small></p>
</div>
</div>

<div class="d-flex flex-row align-items-center">


<i class="fas fa-envelope fa-lg me-3 fa-fw"></i>
<div class="form-outline flex-fill mb-0">
<label class="form-label"
for="email">Username</label>
{{ user_form.username }}
<p class="text-danger"><small>{{
user_form.username.errors.0 }}</small></p>
</div>
</div>

<div class="d-flex flex-row align-items-center">


<i class="fas fa-lock fa-lg me-3 fa-fw"></i>
<div class="form-outline flex-fill mb-0">
<label class="form-label"
for="password1">Password</label>
{{ user_form.password1 }}
<p class="text-danger"><small>{{
user_form.password1.errors.0 }}</small></p>
</div>
</div>

<div class="d-flex flex-row align-items-center">


<i class="fas fa-lock fa-lg me-3 fa-fw"></i>
<div class="form-outline flex-fill mb-0">
<label class="form-label"
for="password2">Confirm your password</label>
{{ user_form.password2 }}
<p class="text-danger"><small>{{
user_form.password2.errors.0 }}</small></p>
</div>
</div>

<div class="d-flex justify-content-center mx-4 mb-3


mb-lg-4">
<input type="submit" name="submit" class="btn
btn-primary btn-lg" value="Sign Up" />
</div>

<div class="d-flex justify-content-center mx-4 mb-3


mb-lg-4 d-flex align-items-center">
<span>Already have an account?</span>
&nbsp;
<a class="nav-link" href="{% url 'login'
%}">Login</a>
</div>

</form>

</div>

<div class="col-md-10 col-lg-5 d-flex align-items-center


order-1 order-lg-1">
<div class="d-flex flex-column ">
<h1 class="text-center fw-bold">Social Network</h1>
<img src="{% static 'img/network.png' %}"
class="img-fluid" alt="social">
</div>

</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% include "./scripts.html" %}
</body>
</html>

User_seaech.html
{% include "./base.html" %}

{% block content %}

<div class="container">
{% if users %}

<ul class="list-group">
{% for user in users %}
<li class="list-group-item d-flex justify-content-between my-3"
style="width: 60%;">
<div class="d-flex">
{% if user.userprofile.profile_picture %}
<img src="{{user.userprofile.profile_picture.url}}" class="rounded"
width="50" height="50" />
{% endif %}
<div class="mx-3">
<h5 class="card-title">
<a href="{% url 'profile' user.username %}"
style="text-decoration: none">{{user.username}}</a>
</h5>
<span>{{user.first_name}} {{user.last_name}}</span>
</div>
</div>
</li>
{% endfor %}
</ul>
{% endif %}
</div>

{% endblock %}

Admin.py
from django.contrib import admin
from .models import *

# Register your models here.

class UserProfileAdmin(admin.ModelAdmin):
list_filter = ['user']
list_display = ['user']
search_fields = ['user']

class Meta:
model = UserProfile

admin.site.register(UserProfile, UserProfileAdmin)
admin.site.register(Post)

Apps.py
from django.apps import AppConfig

class SocialnetworkConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'socialnetwork'

Forms.py
from django import forms
from django.forms import ModelForm
from .models import *
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm

class UserSignupForm(UserCreationForm):

first_name = forms.CharField(max_length=255, widget=forms.TextInput(attrs={'class':


'form-control'}))
last_name = forms.CharField(max_length=255, widget=forms.TextInput(attrs={'class':
'form-control'}))
password1 = forms.CharField(widget=forms.PasswordInput(attrs={'class':
'form-control', 'autocomplete': 'off'}))
password2 = forms.CharField(widget=forms.PasswordInput(attrs={'class':
'form-control', 'autocomplete': 'off'}))

class Meta:
model = User
fields = ('first_name', 'last_name', 'username', 'email', 'password1',
'password2')
widgets = {
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'username': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'password1': forms.PasswordInput(attrs={'class': 'form-control'}),
'password2': forms.PasswordInput(attrs={'class': 'form-control'}),
}

class UserProfileForm(forms.ModelForm):

birthdate = forms.DateField(widget=forms.DateInput(format=('%Y-%m-%d'),
attrs={'type': 'date', 'class': 'form-control'}))
profile_picture = forms.ImageField(required=False, error_messages = {'invalid':
"Image files only"}, widget=forms.FileInput)

class Meta:
model = UserProfile
fields = ('birthdate', 'profile_picture')

class UserForm(forms.ModelForm):
first_name = forms.CharField(max_length=255, widget=forms.TextInput(attrs={'class':
'form-control'}))
last_name = forms.CharField(max_length=255, widget=forms.TextInput(attrs={'class':
'form-control'}))

class Meta:
model = User
fields = ('first_name', 'last_name', 'email')
widgets = {
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
}

class PostForm(forms.ModelForm):

text = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control',


'placeholder': 'Whats on your mind', 'rows':3}))
image = forms.ImageField(required=False, error_messages = {'invalid': "Image files
only"}, widget=forms.FileInput)

class Meta:
model = Post
fields = ['text', 'image']

Models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from datetime import date
from django.core.validators import MaxValueValidator

# Create your models here.

class UserProfile(models.Model):
"""
Profile of a user, includes additional fields such as birthdate, picture
"""

user = models.OneToOneField(User, on_delete=models.CASCADE)


birthdate = models.DateField(null=False, blank=False,
validators=[MaxValueValidator(limit_value=date.today)])
profile_picture = models.ImageField(upload_to='uploads/profile_pictures',
null=True, default='uploads/profile_pictures/default.png')
friends = models.ManyToManyField(User, blank=True, related_name='friends')

def __str__(self):
return self.user.username

def is_friend(self, friend):


"""
Check if its in your friend list
"""
if friend in self.friends.all():
return True
return False

class Post(models.Model):
"""
Post includes author, text content, image and date it was created
"""

text = models.TextField()
image = models.ImageField(upload_to='uploads/posts_pictures', null=True)
created_date = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE)

Tests.py
from socialnetwork.models import UserProfile
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
import json

# Create your tests here.

class LoginTestCase(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('test', '[email protected]', 'mypassword')

def test_login(self):
response = self.client.post(reverse('login'), data={
'username': 'test',
'password': 'mypassword',
})
# test its redirected to home after login
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, '/home')

class SignupTestCase(TestCase):

def setUp(self):
self.client = Client()

def test_signup(self):
response = self.client.post(reverse('signup'), data={
'username': 'testuseragain',
'email': '[email protected]',
'first_name': 'test 1',
'last_name': 'test 1',
'birthdate': '2010-12-12',
'password1': 'stronglongpwd',
'password2': 'stronglongpwd'
})
self.assertEqual(response.status_code, 302)
user = User.objects.get(username='testuseragain')
self.assertEqual(user.email, '[email protected]')

class UserSearchTestCase(TestCase):

def setUp(self):
self.client = Client()
self.user = User.objects.create_user('test', '[email protected]', 'mypassword')
self.searched_user = User.objects.create_user('searched_test', '[email protected]',
'smypassword')

def test_search_user(self):
# login
self.client.post(reverse('login'), data={
'username': 'test',
'password': 'mypassword',
})

response = self.client.get(reverse('user-search'), {'query': 'searched_test'})


self.assertEqual(response.status_code, 200)

class UserProfileTestCase(TestCase):

def setUp(self):
self.client = Client()
self.user = User.objects.create_user('test', '[email protected]', 'mypassword')
self.userprofile = UserProfile.objects.create(user=self.user,
birthdate='2010-12-12')

def test_profile_user(self):
# login
self.client.post(reverse('login'), data={
'username': 'test',
'password': 'mypassword',
})

# returns profile
response = self.client.get(reverse('profile', kwargs={'username': 'test'}))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'socialnetwork/profile.html')

Urls.py
from django import urls
from django.urls import include, path
from . import views
from django.contrib.auth.decorators import login_required

urlpatterns = [
path('', login_required(login_url='/login')(views.Home.as_view()), name = 'home'),
path('home/', login_required(login_url='/login')(views.Home.as_view()), name =
'home'),
path('signup/', views.user_signup, name = 'signup'),
path('login/', views.user_login, name='login'),
path('logout/', views.user_logout, name='logout'),
path('profile/<str:username>', views.UserProfileView.as_view(), name='profile'),
path('profile/edit/', views.EditProfile.as_view(), name='profile-edit'),
path('search/', views.UserSearch.as_view(), name='user-search'),
]

Views.py
from django.contrib import auth
from django.shortcuts import get_object_or_404, redirect, render
from django.views import View
from .models import *
from .forms import *
from django.contrib.auth import authenticate, login, logout
from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.contrib.auth.forms import AuthenticationForm
from django.urls import reverse_lazy
from friend.helpers import find_friend_request
from friend.models import FriendRequest
from django.db.models.query_utils import Q
from django.db.models.functions import Concat
from django.db.models import Value as V

# Create your views here.

@login_required
def user_logout(request):
logout(request)
return redirect('/login')

def user_login(request):
"""
Authenticates user
"""
if request.user.is_authenticated:
return redirect('/home')
else:

if request.method == 'POST':

form = AuthenticationForm(request=request, data=request.POST)

if form.is_valid() and form.user_cache is not None:


user = form.user_cache
if user.is_active:
login(request, user)
messages.success(request, 'Logged in.')

return redirect('/home')
else:
messages.error(request, 'Your account is disabled.')
else:
messages.error(request, 'Invalid login details supplied.')

return render(request, 'socialnetwork/login.html')

def user_signup(request):
"""
Creates new User instance and UserProfile instance
"""

if request.user.is_authenticated:
return redirect('/home')
else:

registered = False

if request.method == 'POST':
user_form = UserSignupForm(data=request.POST)
profile_form = UserProfileForm(data=request.POST)

if user_form.is_valid() and profile_form.is_valid():


user_form.save()
user = user_form.save()
profile = profile_form.save(commit=False)
profile.user = user

if 'birthdate' in user_form.cleaned_data:
profile.birthdate = request.DATA['birthdate']

profile.save()
registered = True
messages.success(request, 'Account created.')

return redirect('/login')

else:
print(user_form.errors, profile_form.errors)
else:
user_form = UserSignupForm()
profile_form = UserProfileForm()

context = {
'user_form': user_form,
'profile_form': profile_form,
'registered': registered
}

return render(request, 'socialnetwork/signup.html', context)

class Home(View):
"""
Displays the homepage. News feed of the logged user
"""

def get(self, request, *args, **kwargs):


"""
Return list of posts from logged user's friends list
"""

user = request.user
profile = UserProfile.objects.filter(user=user).first()

if profile is not None:


user_friends = user.userprofile.friends.all()
# get logged user posts and friends posts
feed_posts = Post.objects.filter(Q(author__in=user_friends) |
Q(author=user)).order_by('-created_date')

post_form = PostForm()

context = {
'post_list': feed_posts,
'post_form': post_form
}

return render(request, 'socialnetwork/home.html', context)

else:
messages.error(request, 'Create an account.')
logout(request)
return redirect('/login')

def post(self, request, *args, **kwargs):


"""
Save post instance
"""

post_form = PostForm(request.POST, request.FILES)

if request.user.is_authenticated & post_form.is_valid():


new_post = post_form.save(commit=False)
user = request.user
new_post.author = user

new_post.save()

user_friends = user.userprofile.friends.all()
feed_posts = Post.objects.filter(Q(author__in=user_friends) |
Q(author=user)).order_by('-created_date')

messages.success(request, 'Post created.')

new_post_form = PostForm()
context = {
'post_list': feed_posts,
'post_form': new_post_form
}

return render(request, 'socialnetwork/home.html', context)

class UserProfileView(View):
"""
Returns info of the user profile such as posts, friends
"""

def get(self, request, username, *args, **kwargs):

context = {}
profile = UserProfile.objects.get(user__username=username)
profile_user = profile.user
profile_posts =
Post.objects.filter(author=profile_user).order_by('-created_date')
profile_friends = profile.friends.all()

user = request.user

is_logged_user = False
is_friend = False
if user.is_authenticated and user != profile_user:
is_logged_user = False
if profile_friends.filter(username=user.username):
is_friend = True
else:
is_friend = False
# if not friends, check if there is an active friend request
# logged user has pending request
if find_friend_request(user_sender=profile_user, user_receiver=user) !=
False:
friend_request_sent_to_user =
find_friend_request(user_sender=profile_user, user_receiver=user)
pending_to_user_id = friend_request_sent_to_user.id
context['pending_to_user_id'] = pending_to_user_id
context['sent_request'] = 'sent_to_user'
# the other user has a pending request
elif find_friend_request(user_sender=user, user_receiver=profile_user)
!= False:
friend_request_sent_by_user = find_friend_request(user_sender=user,
user_receiver=profile_user)
pending_to_them_id = friend_request_sent_by_user.id
context['pending_to_them_id'] = pending_to_them_id
context['sent_request'] = 'sent_to_them'
# there are no requests
else:
context['sent_request'] = 'no_request'

elif user.is_authenticated and user == profile_user:


is_logged_user = True
friend_requests_received = FriendRequest.objects.filter(receiver=user,
is_active=True)
friend_requests_sent = FriendRequest.objects.filter(sender=user,
is_active=True)
context['friend_requests_received'] = friend_requests_received
context['friend_requests_sent'] = friend_requests_sent

context['user'] = user
context['profile'] = profile
context['post_list'] = profile_posts
context['is_logged_user'] = is_logged_user
context['is_friend'] = is_friend
context['friends'] = profile_friends

return render(request, 'socialnetwork/profile.html', context)

class EditProfile(View):
def get(self, request, *args, **kwargs):
user = request.user
profile = get_object_or_404(UserProfile, user=user)

user_form = UserSignupForm(request.POST or None,


request.FILES or None,
instance=user)
profile_form = UserProfileForm(request.POST or None,
request.FILES or None,
instance=profile)
context = {
'user_form': user_form,
'profile_form': profile_form,
'profile': profile
}

return render(request, 'socialnetwork/profile_edit.html', context)

def post(self, request, *args, **kwargs):

user = request.user
profile = get_object_or_404(UserProfile, user=user)

user_form = UserForm(request.POST, instance=user)


profile_form = UserProfileForm(request.POST, request.FILES, instance=profile)

if user_form.is_valid() and profile_form.is_valid():


user_form.save()
profile_form.save()

messages.success(request, 'Profile edited.')

return redirect('/profile/'+user.username)

else:
print(user_form.errors, profile_form.errors)

context = {
'user_form': user_form,
'profile_form': profile_form,
}

return render(request, 'socialnetwork/profile_edit.html', context)

class UserSearch(View):

def get(self, request, *args, **kwargs):


user_searched = self.request.GET.get('query')
users = User.objects.annotate(full_name=Concat('first_name', V(' '),
'last_name')).filter(
Q(username__icontains=user_searched)|
Q(first_name__icontains=user_searched)|
Q(last_name__icontains=user_searched)|
Q(full_name__icontains=user_searched)
)

context = {
'users': users,
}

return render(request, 'socialnetwork/user_search.html', context)

Asgi.py
"""
ASGI config for socialnetwork_project 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/3.2/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'socialnetwork_project.settings')

application = get_asgi_application()

Routing.py
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from django.urls import path
from chat.consumers import ChatConsumer
application = ProtocolTypeRouter({
'websocket':AuthMiddlewareStack(
URLRouter([
path('ws/chat/chat_room/<str:username>/', ChatConsumer.as_asgi())
])
)
})

Settings.py
"""
Django settings for socialnetwork_project project.

Generated by 'django-admin startproject' using Django 3.2.5.

For more information on this file, see


https://docs.djangoproject.com/en/3.2/topics/settings/

For the full list of settings and their values, see


https://docs.djangoproject.com/en/3.2/ref/settings/
"""

from pathlib import Path


import os

# Build paths inside the project like this: BASE_DIR / 'subdir'.


BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Quick-start development settings - unsuitable for production


# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!


SECRET_KEY = 'django-insecure-3rsyk&3xt1v=-l^vnqxhu2cjn!3@l9%tzb60bj(w)@ny^!=rg$'

# SECURITY WARNING: don't run with debug turned on in production!


DEBUG = True

ALLOWED_HOSTS = [
'localhost',
'127.0.0.1'
]
# Authentication backends
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', # default
# any other authentication backends
)

# Application definition

INSTALLED_APPS = [
'socialnetwork.apps.SocialnetworkConfig',
'api.apps.ApiConfig',
'friend.apps.FriendConfig',
'chat.apps.ChatConfig',

'channels',
'rest_framework',
'django_bootstrap5',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
]

# allow any access to any user


REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
]
}

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'socialnetwork_project.urls'

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

ASGI_APPLICATION = 'socialnetwork_project.routing.application'
WSGI_APPLICATION = 'socialnetwork_project.wsgi.application'

# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': BASE_DIR / 'db.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}

# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
{
'NAME':
'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]

# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

USE_I18N = True

USE_L10N = True

USE_TZ = True

# Static files (CSS, JavaScript, Images)


# https://docs.djangoproject.com/en/3.2/howto/static-files/

STATIC_URL = '/static/'

STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')
]

# Default primary key field type


# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

SITE_ID = 1

LOGIN_URL = '/login'
LOGIN_REDIRECT_URL = '/home'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

# CHANNEL_LAYERS = {
# "default": {
# "BACKEND": "channels_redis.core.RedisChannelLayer",
# "CONFIG": {
# "hosts": [("127.0.0.1", 6379)],
# },
# },
# }

CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}

Urls.py
"""socialnetwork_project URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.conf.urls.static import static
from django.conf import settings
urlpatterns = [
path('', include('socialnetwork.urls')),
path('admin/', admin.site.urls),
path('friend', include('friend.urls', namespace='friend')),
path('chat/', include('chat.urls', namespace='chat')),
path('api/', include('api.urls', namespace='api'))
]

# extend url patterns to include static files and images


urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Wsgi.py
"""
WSGI config for socialnetwork_project 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/3.2/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'socialnetwork_project.settings')

application = get_wsgi_application()

Styles.css
.chat-message-date {
font-size: small;
}

Manage.py
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys

def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'socialnetwork_project.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)

if __name__ == '__main__':
main()

Pipfile
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
django = "*"
django-bootstrap5 = "*"
django-allauth = "*"
pillow = "*"
channels-redis = "*"
redis = "*"
asgiref = "*"
djangorestframework = "*"
requests = "*"
factory-boy = "*"
[dev-packages]

[requires]
python_version = "3.9"

pipfile.lock
{
"_meta": {
"hash": {
"sha256":
"9071f233c19248277df2234dd604d37e08c38e343052ad89d0a240da2dbee9af"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.9"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"aioredis": {
"hashes": [

"sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a",

"sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"
],
"version": "==1.3.1"
},
"asgiref": {
"hashes": [

"sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9",

"sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"
],
"index": "pypi",
"version": "==3.4.1"
},
"async-timeout": {
"hashes": [

"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",

"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
],
"markers": "python_full_version >= '3.5.3'",
"version": "==3.0.1"
},
"attrs": {
"hashes": [

"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",

"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1,
3.2, 3.3, 3.4'",
"version": "==21.2.0"
},
"autobahn": {
"hashes": [

"sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac",

"sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03"
],
"markers": "python_version >= '3.7'",
"version": "==21.3.1"
},
"automat": {
"hashes": [

"sha256:7979803c74610e11ef0c0d68a2942b152df52da55336e0c9d58daf1831cbdf33",

"sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111"
],
"version": "==20.2.0"
},
"beautifulsoup4": {
"hashes": [

"sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf",

"sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"
],
"markers": "python_version >= '3.1'",
"version": "==4.10.0"
},
"certifi": {
"hashes": [

"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",

"sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
],
"version": "==2021.5.30"
},
"cffi": {
"hashes": [

"sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d",

"sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771",

"sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872",

"sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c",

"sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc",

"sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762",

"sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202",

"sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5",

"sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548",
"sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a",

"sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f",

"sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20",

"sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218",

"sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c",

"sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e",

"sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56",

"sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224",

"sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a",

"sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2",

"sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a",

"sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819",

"sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346",

"sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b",

"sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e",

"sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534",

"sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb",

"sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0",

"sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156",

"sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd",

"sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87",
"sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc",

"sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195",

"sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33",

"sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f",

"sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d",

"sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd",

"sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728",

"sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7",

"sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca",

"sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99",

"sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf",

"sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e",

"sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c",

"sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5",

"sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"
],
"version": "==1.14.6"
},
"channels": {
"hashes": [

"sha256:0ff0422b4224d10efac76e451575517f155fe7c97d369b5973b116f22eeaf86c",

"sha256:fdd9a94987a23d8d7ebd97498ed8b8cc83163f37e53fc6c85098aba7a3bb8b75"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.4"
},
"channels-redis": {
"hashes": [

"sha256:0a18ce279c15ba79b7985bb12b2d6dd0ac8a14e4ad6952681f4422a4cc4a5ea9",

"sha256:1abd5820ff1ed4ac627f8a219ad389e4c87e52e47a230929a7a474e95dd2c6c2"
],
"index": "pypi",
"version": "==3.3.0"
},
"charset-normalizer": {
"hashes": [

"sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b",

"sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
],
"markers": "python_version >= '3'",
"version": "==2.0.4"
},
"constantly": {
"hashes": [

"sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35",

"sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d"
],
"version": "==15.1.0"
},
"cryptography": {
"hashes": [

"sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e",

"sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b",

"sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7",

"sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085",

"sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc",
"sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a",

"sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498",

"sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9",

"sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c",

"sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7",

"sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb",

"sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14",

"sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af",

"sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e",

"sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5",

"sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06",

"sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7"
],
"version": "==3.4.8"
},
"daphne": {
"hashes": [

"sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f",

"sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.2"
},
"defusedxml": {
"hashes": [

"sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
"sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1,
3.2, 3.3, 3.4'",
"version": "==0.7.1"
},
"django": {
"hashes": [

"sha256:95b318319d6997bac3595517101ad9cc83fe5672ac498ba48d1a410f47afecd2",

"sha256:e93c93565005b37ddebf2396b4dc4b6913c1838baa82efdfb79acedd5816c240"
],
"index": "pypi",
"version": "==3.2.7"
},
"django-allauth": {
"hashes": [

"sha256:6d46be0e1480316ccd45476db3aefb39db70e038d2a543112d314b76bb999a4e"
],
"index": "pypi",
"version": "==0.45.0"
},
"django-bootstrap5": {
"hashes": [

"sha256:0021355df9212c511876e3f5336ba470416a6dd52f61bf5acec5bb783ab41ff5",

"sha256:ac05ca69b990e62657c0bf40db14946c22fd64c7fb9dfe094a9f22ecc58f86c5"
],
"index": "pypi",
"version": "==2.1.2"
},
"djangorestframework": {
"hashes": [

"sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf",

"sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2"
],
"index": "pypi",
"version": "==3.12.4"
},
"factory-boy": {
"hashes": [

"sha256:1d3db4b44b8c8c54cdd8b83ae4bdb9aeb121e464400035f1f03ae0e1eade56a4",

"sha256:401cc00ff339a022f84d64a4339503d1689e8263a4478d876e58a3295b155c5b"
],
"index": "pypi",
"version": "==3.2.0"
},
"faker": {
"hashes": [

"sha256:6714c153433086681b26e5c95ee314ee0fcd45ec05f2426097543dd4c70789a6",

"sha256:810859626d19e62a2a13aa4a08d59ada131f0522431eec163b09b6df147a25b9"
],
"markers": "python_version >= '3.6'",
"version": "==8.12.1"
},
"hiredis": {
"hashes": [

"sha256:04026461eae67fdefa1949b7332e488224eac9e8f2b5c58c98b54d29af22093e",

"sha256:04927a4c651a0e9ec11c68e4427d917e44ff101f761cd3b5bc76f86aaa431d27",

"sha256:07bbf9bdcb82239f319b1f09e8ef4bdfaec50ed7d7ea51a56438f39193271163",

"sha256:09004096e953d7ebd508cded79f6b21e05dff5d7361771f59269425108e703bc",

"sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26",

"sha256:0b39ec237459922c6544d071cdcf92cbb5bc6685a30e7c6d985d8a3e3a75326e",

"sha256:0d5109337e1db373a892fdcf78eb145ffb6bbd66bb51989ec36117b9f7f9b579",

"sha256:0f41827028901814c709e744060843c77e78a3aca1e0d6875d2562372fcb405a",
"sha256:11d119507bb54e81f375e638225a2c057dda748f2b1deef05c2b1a5d42686048",

"sha256:1233e303645f468e399ec906b6b48ab7cd8391aae2d08daadbb5cad6ace4bd87",

"sha256:139705ce59d94eef2ceae9fd2ad58710b02aee91e7fa0ccb485665ca0ecbec63",

"sha256:1f03d4dadd595f7a69a75709bc81902673fa31964c75f93af74feac2f134cc54",

"sha256:240ce6dc19835971f38caf94b5738092cb1e641f8150a9ef9251b7825506cb05",

"sha256:294a6697dfa41a8cba4c365dd3715abc54d29a86a40ec6405d677ca853307cfb",

"sha256:3d55e36715ff06cdc0ab62f9591607c4324297b6b6ce5b58cb9928b3defe30ea",

"sha256:3dddf681284fe16d047d3ad37415b2e9ccdc6c8986c8062dbe51ab9a358b50a5",

"sha256:3f5f7e3a4ab824e3de1e1700f05ad76ee465f5f11f5db61c4b297ec29e692b2e",

"sha256:508999bec4422e646b05c95c598b64bdbef1edf0d2b715450a078ba21b385bcc",

"sha256:5d2a48c80cf5a338d58aae3c16872f4d452345e18350143b3bf7216d33ba7b99",

"sha256:5dc7a94bb11096bc4bffd41a3c4f2b958257085c01522aa81140c68b8bf1630a",

"sha256:65d653df249a2f95673976e4e9dd7ce10de61cfc6e64fa7eeaa6891a9559c581",

"sha256:7492af15f71f75ee93d2a618ca53fea8be85e7b625e323315169977fae752426",

"sha256:7f0055f1809b911ab347a25d786deff5e10e9cf083c3c3fd2dd04e8612e8d9db",

"sha256:807b3096205c7cec861c8803a6738e33ed86c9aae76cac0e19454245a6bbbc0a",

"sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a",

"sha256:87c7c10d186f1743a8fd6a971ab6525d60abd5d5d200f31e073cd5e94d7e7a9d",

"sha256:8b42c0dc927b8d7c0eb59f97e6e34408e53bc489f9f90e66e568f329bff3e443",

"sha256:a00514362df15af041cc06e97aebabf2895e0a7c42c83c21894be12b84402d79",

"sha256:a39efc3ade8c1fb27c097fd112baf09d7fd70b8cb10ef1de4da6efbe066d381d",
"sha256:a4ee8000454ad4486fb9f28b0cab7fa1cd796fc36d639882d0b34109b5b3aec9",

"sha256:a7928283143a401e72a4fad43ecc85b35c27ae699cf5d54d39e1e72d97460e1d",

"sha256:adf4dd19d8875ac147bf926c727215a0faf21490b22c053db464e0bf0deb0485",

"sha256:ae8427a5e9062ba66fc2c62fb19a72276cf12c780e8db2b0956ea909c48acff5",

"sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048",

"sha256:b84f29971f0ad4adaee391c6364e6f780d5aae7e9226d41964b26b49376071d0",

"sha256:c39c46d9e44447181cd502a35aad2bb178dbf1b1f86cf4db639d7b9614f837c6",

"sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41",

"sha256:dcef843f8de4e2ff5e35e96ec2a4abbdf403bd0f732ead127bd27e51f38ac298",

"sha256:e3447d9e074abf0e3cd85aef8131e01ab93f9f0e86654db7ac8a3f73c63706ce",

"sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0",

"sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"
],
"markers": "python_version >= '3.6'",
"version": "==2.0.0"
},
"hyperlink": {
"hashes": [

"sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b",

"sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"
],
"version": "==21.0.0"
},
"idna": {
"hashes": [

"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
],
"markers": "python_version >= '3'",
"version": "==3.2"
},
"incremental": {
"hashes": [

"sha256:02f5de5aff48f6b9f665d99d48bfc7ec03b6e3943210de7cfc88856d755d6f57",

"sha256:92014aebc6a20b78a8084cdd5645eeaa7f74b8933f70fa3ada2cfbd1e3b54321"
],
"version": "==21.3.0"
},
"msgpack": {
"hashes": [

"sha256:0cb94ee48675a45d3b86e61d13c1e6f1696f0183f0715544976356ff86f741d9",

"sha256:1026dcc10537d27dd2d26c327e552f05ce148977e9d7b9f1718748281b38c841",

"sha256:26a1759f1a88df5f1d0b393eb582ec022326994e311ba9c5818adc5374736439",

"sha256:2a5866bdc88d77f6e1370f82f2371c9bc6fc92fe898fa2dec0c5d4f5435a2694",

"sha256:31c17bbf2ae5e29e48d794c693b7ca7a0c73bd4280976d408c53df421e838d2a",

"sha256:497d2c12426adcd27ab83144057a705efb6acc7e85957a51d43cdcf7f258900f",

"sha256:5a9ee2540c78659a1dd0b110f73773533ee3108d4e1219b5a15a8d635b7aca0e",

"sha256:8521e5be9e3b93d4d5e07cb80b7e32353264d143c1f072309e1863174c6aadb1",

"sha256:87869ba567fe371c4555d2e11e4948778ab6b59d6cc9d8460d543e4cfbbddd1c",

"sha256:8ffb24a3b7518e843cd83538cf859e026d24ec41ac5721c18ed0c55101f9775b",

"sha256:92be4b12de4806d3c36810b0fe2aeedd8d493db39e2eb90742b9c09299eb5759",

"sha256:9ea52fff0473f9f3000987f313310208c879493491ef3ccf66268eff8d5a0326",
"sha256:a4355d2193106c7aa77c98fc955252a737d8550320ecdb2e9ac701e15e2943bc",

"sha256:a99b144475230982aee16b3d249170f1cccebf27fb0a08e9f603b69637a62192",

"sha256:ac25f3e0513f6673e8b405c3a80500eb7be1cf8f57584be524c4fa78fe8e0c83",

"sha256:b28c0876cce1466d7c2195d7658cf50e4730667196e2f1355c4209444717ee06",

"sha256:b55f7db883530b74c857e50e149126b91bb75d35c08b28db12dcb0346f15e46e",

"sha256:b6d9e2dae081aa35c44af9c4298de4ee72991305503442a5c74656d82b581fe9",

"sha256:c747c0cc08bd6d72a586310bda6ea72eeb28e7505990f342552315b229a19b33",

"sha256:d6c64601af8f3893d17ec233237030e3110f11b8a962cb66720bf70c0141aa54",

"sha256:d8167b84af26654c1124857d71650404336f4eb5cc06900667a493fc619ddd9f",

"sha256:de6bd7990a2c2dabe926b7e62a92886ccbf809425c347ae7de277067f97c2887",

"sha256:e36a812ef4705a291cdb4a2fd352f013134f26c6ff63477f20235138d1d21009",

"sha256:e89ec55871ed5473a041c0495b7b4e6099f6263438e0bd04ccd8418f92d5d7f2",

"sha256:f3e6aaf217ac1c7ce1563cf52a2f4f5d5b1f64e8729d794165db71da57257f0c",

"sha256:f484cd2dca68502de3704f056fa9b318c94b1539ed17a4c784266df5d6978c87",

"sha256:fae04496f5bc150eefad4e9571d1a76c55d021325dcd484ce45065ebbdd00984",

"sha256:fe07bc6735d08e492a327f496b7850e98cb4d112c56df69b0c844dbebcbb47f6"
],
"version": "==1.0.2"
},
"oauthlib": {
"hashes": [

"sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc",

"sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"
],
"markers": "python_version >= '3.6'",
"version": "==3.1.1"
},
"pillow": {
"hashes": [

"sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30",

"sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9",

"sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71",

"sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9",

"sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b",

"sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630",

"sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875",

"sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2",

"sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1",

"sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7",

"sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3",

"sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b",

"sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6",

"sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba",

"sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4",

"sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864",

"sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056",

"sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228",
"sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8",

"sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb",

"sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d",

"sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da",

"sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073",

"sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3",

"sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616",

"sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa",

"sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979",

"sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a",

"sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b",

"sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6",

"sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441",

"sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624",

"sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd",

"sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550",

"sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09",

"sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196",

"sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b",

"sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1",

"sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6",
"sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83",

"sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f",

"sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4",

"sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19",

"sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341",

"sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96",

"sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355",

"sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c",

"sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c",

"sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629",

"sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2",

"sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87",

"sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5",

"sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"
],
"index": "pypi",
"version": "==8.3.2"
},
"pyasn1": {
"hashes": [

"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",

"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",

"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",

"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",

"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",

"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",

"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",

"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",

"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",

"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",

"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",

"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
],
"version": "==0.4.8"
},
"pyasn1-modules": {
"hashes": [

"sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8",

"sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199",

"sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811",

"sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed",

"sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4",

"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",

"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74",

"sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb",

"sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45",
"sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd",

"sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0",

"sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d",

"sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"
],
"version": "==0.2.8"
},
"pycparser": {
"hashes": [

"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",

"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1,
3.2, 3.3'",
"version": "==2.20"
},
"pyjwt": {
"extras": [
"crypto"
],
"hashes": [

"sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1",

"sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130"
],
"markers": "python_version >= '3.6'",
"version": "==2.1.0"
},
"pyopenssl": {
"hashes": [

"sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51",

"sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b"
],
"version": "==20.0.1"
},
"python-dateutil": {
"hashes": [

"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",

"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1,
3.2, 3.3'",
"version": "==2.8.2"
},
"python3-openid": {
"hashes": [

"sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf",

"sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"
],
"version": "==3.2.0"
},
"pytz": {
"hashes": [

"sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",

"sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"
],
"version": "==2021.1"
},
"redis": {
"hashes": [

"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",

"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
],
"index": "pypi",
"version": "==3.5.3"
},
"requests": {
"hashes": [

"sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",

"sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
],
"index": "pypi",
"version": "==2.26.0"
},
"requests-oauthlib": {
"hashes": [

"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",

"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a",

"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
],
"version": "==1.3.0"
},
"service-identity": {
"hashes": [

"sha256:6e6c6086ca271dc11b033d17c3a8bea9f24ebff920c587da090afc9519419d34",

"sha256:f0b0caac3d40627c3c04d7a51b6e06721857a0e10a8775f2d1d7e72901b3a7db"
],
"version": "==21.1.0"
},
"six": {
"hashes": [

"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",

"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1,
3.2, 3.3'",
"version": "==1.16.0"
},
"soupsieve": {
"hashes": [
"sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc",

"sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"
],
"markers": "python_version >= '3.6'",
"version": "==2.2.1"
},
"sqlparse": {
"hashes": [

"sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae",

"sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"
],
"markers": "python_version >= '3.5'",
"version": "==0.4.2"
},
"text-unidecode": {
"hashes": [

"sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8",

"sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"
],
"version": "==1.3"
},
"twisted": {
"extras": [
"tls"
],
"hashes": [

"sha256:13c1d1d2421ae556d91e81e66cf0d4f4e4e1e4a36a0486933bee4305c6a4fb9b",

"sha256:2cd652542463277378b0d349f47c62f20d9306e57d1247baabd6d1d38a109006"
],
"markers": "python_full_version >= '3.6.7'",
"version": "==21.7.0"
},
"twisted-iocpsupport": {
"hashes": [
"sha256:306becd6e22ab6e8e4f36b6bdafd9c92e867c98a5ce517b27fdd27760ee7ae41",

"sha256:3c61742cb0bc6c1ac117a7e5f422c129832f0c295af49e01d8a6066df8cfc04d",

"sha256:72068b206ee809c9c596b57b5287259ea41ddb4774d86725b19f35bf56aa32a9",

"sha256:7d972cfa8439bdcb35a7be78b7ef86d73b34b808c74be56dfa785c8a93b851bf",

"sha256:81b3abe3527b367da0220482820cb12a16c661672b7bcfcde328902890d63323",

"sha256:851b3735ca7e8102e661872390e3bce88f8901bece95c25a0c8bb9ecb8a23d32",

"sha256:985c06a33f5c0dae92c71a036d1ea63872ee86a21dd9b01e1f287486f15524b4",

"sha256:9dbb8823b49f06d4de52721b47de4d3b3026064ef4788ce62b1a21c57c3fff6f",

"sha256:b435857b9efcbfc12f8c326ef0383f26416272260455bbca2cd8d8eca470c546",

"sha256:b76b4eed9b27fd63ddb0877efdd2d15835fdcb6baa745cb85b66e5d016ac2878",

"sha256:b9fed67cf0f951573f06d560ac2f10f2a4bbdc6697770113a2fc396ea2cb2565",

"sha256:bf4133139d77fc706d8f572e6b7d82871d82ec7ef25d685c2351bdacfb701415"
],
"markers": "platform_system == 'Windows'",
"version": "==1.0.2"
},
"txaio": {
"hashes": [

"sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8",

"sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb"
],
"markers": "python_version >= '3.6'",
"version": "==21.2.1"
},
"typing-extensions": {
"hashes": [

"sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e",
"sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7",

"sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"
],
"version": "==3.10.0.2"
},
"urllib3": {
"hashes": [

"sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",

"sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1,
3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.6"
},
"zope.interface": {
"hashes": [

"sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192",

"sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702",

"sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09",

"sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4",

"sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a",

"sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3",

"sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf",

"sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c",

"sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d",

"sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78",

"sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83",
"sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531",

"sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46",

"sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021",

"sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94",

"sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc",

"sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63",

"sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54",

"sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117",

"sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25",

"sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05",

"sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e",

"sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1",

"sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004",

"sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2",

"sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e",

"sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f",

"sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f",

"sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120",

"sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f",

"sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1",

"sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9",
"sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e",

"sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7",

"sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8",

"sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b",

"sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155",

"sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7",

"sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c",

"sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325",

"sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d",

"sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb",

"sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e",

"sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959",

"sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7",

"sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920",

"sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e",

"sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48",

"sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8",

"sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4",

"sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1,
3.2, 3.3, 3.4'",
"version": "==5.4.0"
}
},
"develop": {}
}

You might also like