├── .env ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── auth ├── __init__.py ├── apps.py ├── templates │ └── auth │ │ ├── login.html │ │ └── register.html ├── urls.py └── views.py ├── chat ├── __init__.py ├── admin.py ├── apps.py ├── consumer.py ├── models.py ├── routing.py ├── serializers.py ├── templates │ └── chat │ │ ├── about.html │ │ ├── create-group.html │ │ ├── group-list.html │ │ ├── index.html │ │ └── room.html ├── test_consumer.py ├── tests_models.py ├── urls.py └── views.py ├── core ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── requirements.txt ├── static ├── assets │ ├── IRANSansWeb.woff2 │ └── favicon.ico ├── base.css ├── css │ ├── about.css │ ├── auth.css │ ├── chat.css │ └── style.css └── js │ ├── bootstrap.js │ └── reconnecting_ws.js └── templates ├── admin ├── app_index.html ├── base.html ├── base_login.html ├── base_site.html ├── change_form.html ├── change_list.html ├── change_list_results.html ├── includes │ └── fieldset.html ├── index.html ├── lib │ ├── _main_header.html │ └── _main_sidebar.html ├── login.html ├── object_history.html └── search_form.html ├── adminlte ├── base.html ├── components │ └── info_box.html ├── example.html ├── index.html ├── lib │ ├── _main_footer.html │ ├── _main_header.html │ ├── _main_sidebar.html │ ├── _messages.html │ ├── _pagination.html │ ├── _scripts.html │ └── _styles.html ├── login.html └── pages │ ├── edit.html │ └── index.html └── base.html /.env: -------------------------------------------------------------------------------- 1 | SECRET_KEY=abc 2 | DEBUG=1 3 | ALLOWED_HOSTS=127.0.0.1,localhost 4 | DB_NAME=chat_data 5 | DB_USER=postgres 6 | DB_PASSWORD=password 7 | DB_HOST=localhost 8 | DB_POST=5432 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css linguist-detectable=false 2 | *.js linguist-detectable=false 3 | *.html linguist-detectable=false 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | __pycache__ 3 | migrations/ 4 | test/ 5 | staticfiles/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mohammad Dori 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Chat 2 | 3 | Create advance messenger web application with Django. 4 | 5 | # 6 | 7 | ## Tools 8 | 9 | - [Django](https://www.djangoproject.com/) 10 | - [Django Channels](https://channels.readthedocs.io/en/stable/) 11 | - [Python](https://www.python.org/) 12 | - [Bootstrap](https://getbootstrap.com/) 13 | - [PostgreSQL](https://www.postgresql.org/) 14 | - [Redis](https://redis.io/) 15 | - [Docker](https://www.docker.com/) 16 | 17 | # 18 | 19 | # Run Project 20 | 21 | ## Download Codes 22 | 23 | ``` 24 | git clone https://github.com/dori-dev/django-chat.git 25 | cd django-chat 26 | ``` 27 | 28 | ## Install Postgresql 29 | 30 | Install postgresql from [here](https://www.postgresql.org/download/). 31 | 32 | ## Create DataBase 33 | 34 | Create a postgres database and set your database name, user, password, host and port in `.env` file with change `DB_NAME` `DB_USER` `DB_PASSWORD` `DB_HOST` `DB_POST` variables! 35 | 36 | ## Install Docker 37 | 38 | Install docker from [here](https://docs.docker.com/engine/install/). 39 | 40 | ## Setup Redis 41 | 42 | ``` 43 | sudo docker run -p 6379:6379 -d redis:5 44 | ``` 45 | 46 | ## Build Virtual Environment 47 | 48 | ``` 49 | python3 -m venv env 50 | source env/bin/activate 51 | ``` 52 | 53 | ## Install Project Requirements 54 | 55 | ``` 56 | pip install -r requirements.txt 57 | ``` 58 | 59 | ## Migrate Models 60 | 61 | ``` 62 | python3 manage.py makemigrations chat 63 | python3 manage.py migrate 64 | ``` 65 | 66 | ## Add Super User 67 | 68 | ``` 69 | python3 manage.py createsuperuser 70 | ``` 71 | 72 | ## Collect Static 73 | 74 | ``` 75 | python3 manage.py collectstatic 76 | ``` 77 | 78 | ## Test Project 79 | 80 | At first, install chrome [webdriver](https://chromedriver.chromium.org/) for test with selenium. 81 | then copy `chromedriver` binary file in `env/bin/` path 82 | 83 | ## Run Test 84 | 85 | ``` 86 | python3 manage.py test 87 | ``` 88 | 89 | ## Run Server 90 | 91 | ``` 92 | python3 manage.py runserver 93 | ``` 94 | 95 | ## Open On Browser 96 | 97 | Home Page 98 | [127.0.0.1:8000](http://127.0.0.1:8000/) 99 | 100 | Admin Page 101 | [127.0.0.1:8000/admin](http://127.0.0.1:8000/admin) 102 | 103 | # 104 | 105 | # Links 106 | 107 | Download Source Code: [Click Here](https://github.com/dori-dev/django-chat/archive/refs/heads/master.zip) 108 | 109 | My Github Account: [Click Here](https://github.com/dori-dev/) 110 | -------------------------------------------------------------------------------- /auth/__init__.py: -------------------------------------------------------------------------------- 1 | from email.policy import default 2 | -------------------------------------------------------------------------------- /auth/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AuthConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'auth.apps' 7 | label = 'authentication' 8 | -------------------------------------------------------------------------------- /auth/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block style %} 6 | 7 | {% endblock style %} 8 | 9 | {% block title %} 10 | نیوچت - ورود 11 | {% endblock title %} 12 | 13 | 14 | {% block content %} 15 | 16 | 17 |

ورود

18 | {% if request.GET.next %} 19 |

20 | برای ادامه ابتدا 21 | وارد 22 | شوید یا 23 | 24 | ثبت نام 25 | 26 | کنید 27 |

28 | {% endif %} 29 |
30 | {% if form.errors %} 31 |
32 | 42 |
43 | {% else %} 44 |
45 | {% endif %} 46 | 47 |
48 | 49 | 51 |
52 |
53 | 54 | 56 |
57 | {% if request.GET.next %} 58 | 59 | {% endif %} {% csrf_token %} 60 | 63 |
64 |

65 | اگر حساب کاربری ندارید در سایت ثبت نام کنید 66 |

67 | 68 | 71 | 72 |
73 |
74 | {% endblock content %} -------------------------------------------------------------------------------- /auth/templates/auth/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block style %} 6 | 7 | 8 | {% endblock style %} 9 | 10 | {% block title %} 11 | نیوچت - ثبت نام 12 | {% endblock title %} 13 | 14 | 15 | {% block content %} 16 | 17 |

ثبت نام

18 | 19 | {% if request.GET.next %} 20 |

21 | برای ادامه ابتدا 22 | 23 | وارد 24 | 25 | شوید یا 26 | ثبت نام 27 | کنید 28 |

29 | {% endif %} 30 | 31 |
32 |
{{ form.errors }}
33 | {% csrf_token %} 34 |
35 | 36 | 38 |
39 |
40 | 41 | 43 |
44 |
45 | 46 | 48 |
49 | {% if request.GET.next %} 50 | 51 | {% endif %} 52 | 55 |
56 |

57 | اگر قبلا ثبت نام کردید وارد حساب تان شوید 58 |

59 | 60 | 63 | 64 |
65 |
66 | 67 | {% endblock content %} -------------------------------------------------------------------------------- /auth/urls.py: -------------------------------------------------------------------------------- 1 | """auth urls 2 | """ 3 | from django.urls import path 4 | from . import views 5 | 6 | app_name = "auth" 7 | urlpatterns = [ 8 | path("register/", views.register_view, name="register"), 9 | path("login/", views.login_view, name="login"), 10 | path("logout/", views.logout_view, name="logout"), 11 | ] 12 | -------------------------------------------------------------------------------- /auth/views.py: -------------------------------------------------------------------------------- 1 | """chat authentication 2 | """ 3 | from django.shortcuts import render, redirect 4 | from django.contrib.auth.forms import UserCreationForm, AuthenticationForm 5 | from django.contrib.auth import login, logout 6 | 7 | 8 | def register_view(request): 9 | """register view""" 10 | if request.method == "POST": 11 | form = UserCreationForm(request.POST) 12 | if form.is_valid(): 13 | user = form.save() 14 | login(request, user) 15 | if "next" in request.POST: 16 | return redirect(request.POST.get('next')) 17 | return redirect("index") 18 | 19 | else: 20 | form = UserCreationForm() 21 | arg = { 22 | "form": form, 23 | } 24 | return render(request, "auth/register.html", arg) 25 | 26 | 27 | def login_view(request): 28 | """login view""" 29 | if request.method == "POST": 30 | form = AuthenticationForm(data=request.POST) 31 | if form.is_valid(): 32 | user = form.get_user() 33 | login(request, user) 34 | if "next" in request.POST: 35 | return redirect(request.POST.get('next')) 36 | return redirect("index") 37 | else: 38 | form = AuthenticationForm() 39 | arg = { 40 | "form": form, 41 | } 42 | return render(request, "auth/login.html", arg) 43 | 44 | 45 | def logout_view(request): 46 | """logout view""" 47 | if request.method == "POST": 48 | logout(request) 49 | return redirect("index") 50 | -------------------------------------------------------------------------------- /chat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dori-dev/django-chat/e1570e9915e0547e03cd1f5d26f1a54e2b24fef8/chat/__init__.py -------------------------------------------------------------------------------- /chat/admin.py: -------------------------------------------------------------------------------- 1 | """chat admin 2 | """ 3 | from csv import list_dialects 4 | from datetime import datetime 5 | from django.contrib import admin 6 | from django.utils.html import format_html 7 | from .models import Message, Chat, Customize 8 | 9 | 10 | class MessageAdmin(admin.ModelAdmin): 11 | list_display = ("author", "message", "time", "room") 12 | list_filter = ("author", "room", "timestamp") 13 | search_fields = ("content",) 14 | 15 | def time(self, model: Message): 16 | date: datetime = model.timestamp.astimezone() 17 | format_date = datetime.strftime(date, "%b %d, %H:%M:%S") 18 | return format_date 19 | 20 | def message(self, model: Message): 21 | content = model.content 22 | if model.type == "image": 23 | return format_html(f'تصویر') 24 | return content 25 | 26 | time.short_description = "زمان" 27 | message.short_description = "پیام" 28 | 29 | 30 | class ChatAdmin(admin.ModelAdmin): 31 | list_display = ("name", "members_count", "messages", "time") 32 | list_filter = ("members", "timestamp") 33 | search_fields = ("name",) 34 | 35 | def members_count(self, model: Chat): 36 | return model.members.count() 37 | 38 | def messages(self, model: Chat): 39 | messages = Message.objects.filter(room=model) 40 | return len(messages) 41 | 42 | def time(self, model: Chat): 43 | date: datetime = model.timestamp.astimezone() 44 | format_date = datetime.strftime(date, "%b %d, %H:%M:%S") 45 | return format_date 46 | time.short_description = "زمان ایجاد" 47 | members_count.short_description = "تعداد اعضا" 48 | messages.short_description = "تعداد پیام ها" 49 | 50 | 51 | class CustomizeAdmin(admin.ModelAdmin): 52 | list_display = ("name", "describe", "show_color") 53 | 54 | def show_color(self, model: Customize): 55 | color = model.color 56 | html = f'

{color}

' 57 | return format_html(html) 58 | 59 | show_color.short_description = "رنگ" 60 | 61 | 62 | admin.site.register(Message, MessageAdmin) 63 | admin.site.register(Chat, ChatAdmin) 64 | admin.site.register(Customize, CustomizeAdmin) 65 | -------------------------------------------------------------------------------- /chat/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ChatConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'chat' 7 | verbose_name = "گفتگو" 8 | 9 | def ready(self): 10 | try: 11 | from .models import Customize 12 | if len(Customize.objects.all()) == 0: 13 | Customize.create_default_fields() 14 | except Exception: 15 | pass 16 | -------------------------------------------------------------------------------- /chat/consumer.py: -------------------------------------------------------------------------------- 1 | """chat consumer 2 | """ 3 | import json 4 | from django.db.models.query import QuerySet 5 | from asgiref.sync import async_to_sync 6 | from channels.generic.websocket import WebsocketConsumer 7 | from rest_framework.renderers import JSONRenderer 8 | from .serializers import MessageSerializer 9 | from .models import Chat, Message, User 10 | 11 | 12 | class ChatConsumer(WebsocketConsumer): 13 | """chat consumer 14 | """ 15 | 16 | def __init__(self, *args, **kwargs): 17 | super().__init__(*args, **kwargs) 18 | self.commands: dict = { 19 | "new_message": "message", 20 | "send_image": "image", 21 | "message": "new_message", 22 | "image": "send_image", 23 | } 24 | 25 | def new_message(self, data: dict): 26 | self.notification(data) 27 | author = User.objects.get(username=data['username']) 28 | room = Chat.objects.get(room_id=data['room_name']) 29 | type = self.commands[data['command']] 30 | content: str = data['message'] 31 | # use `objects.create` because I just want `insert` message! 32 | message: Message = Message.objects.create( 33 | author=author, 34 | content=content.strip(), 35 | room=room, 36 | type=type) 37 | return message.get_time() 38 | 39 | def fetch_message(self, room_name: str): 40 | query_set: QuerySet[Message] = Message.last_messages(room_name) 41 | content: bytes = self.message_serializer(query_set) 42 | messages: list = reversed(json.loads(content)) 43 | for message in messages: 44 | self.chat_message( 45 | { 46 | "message": message['content'], 47 | "author": message['author_username'], 48 | "command": self.commands[message['type']], 49 | "time": message['time'], 50 | } 51 | ) 52 | 53 | def notification(self, data: dict): 54 | room: str = data['room_name'] 55 | members: list = Chat.get_members_list(room) 56 | listener: Chat = Chat.objects.get(name="listener") 57 | async_to_sync(self.channel_layer.group_send)( 58 | f"chat_{listener.room_id}", 59 | { 60 | 'type': 'chat_message', 61 | 'message': data["message"], 62 | 'author': data["username"], 63 | 'room_id': room, 64 | 'members_list': members, 65 | 'room_name': Chat.objects.get(room_id=room).name 66 | } 67 | ) 68 | 69 | def message_serializer(self, query_set: QuerySet[Message]): 70 | serialized: list = MessageSerializer(query_set, many=True) 71 | content: bytes = JSONRenderer().render(serialized.data) 72 | return content 73 | 74 | def connect(self): 75 | self.room_id: str = self.scope['url_route']['kwargs']['room_id'] 76 | self.room_group_name = f'chat_{self.room_id}' 77 | # join room group 78 | async_to_sync(self.channel_layer.group_add)( 79 | self.room_group_name, 80 | self.channel_name) 81 | self.accept() 82 | 83 | def disconnect(self, close_code): 84 | # leave room group 85 | async_to_sync(self.channel_layer.group_discard)( 86 | self.room_group_name, 87 | self.channel_name) 88 | 89 | def receive(self, text_data: str): 90 | """receive data from WebSocket 91 | 92 | Args: 93 | text_data (str): text data 94 | """ 95 | data_dict: dict = json.loads(text_data) 96 | command: str = data_dict['command'] 97 | # execute the function according to the given `command` 98 | if command == 'new_message': 99 | message: str = data_dict['message'] 100 | if not message.strip(): 101 | return 102 | time = self.new_message(data_dict) 103 | data_dict['time'] = time 104 | self.send_to_room(data_dict) 105 | elif command == 'fetch_message': 106 | room_name: str = data_dict['room_name'] 107 | self.fetch_message(room_name) 108 | elif command == 'send_image': 109 | time = self.new_message(data_dict) 110 | data_dict['time'] = time 111 | self.send_to_room(data_dict) 112 | else: 113 | print(f'Invalid Command: "{command}"') 114 | 115 | def send_to_room(self, data: dict): 116 | """send message to room group 117 | 118 | Args: 119 | message (str): message will send 120 | """ 121 | message: str = data['message'] 122 | author: str = data['username'] 123 | command: str = data.get("command", "new_message") 124 | time: str = data['time'] 125 | async_to_sync(self.channel_layer.group_send)( 126 | self.room_group_name, 127 | { 128 | 'type': 'chat_message', 129 | 'message': message, 130 | 'author': author, 131 | 'command': command, 132 | 'time': time, 133 | } 134 | ) 135 | 136 | def chat_message(self, event: dict): 137 | """receive message from room group 138 | 139 | Args: 140 | event (dict): the event 141 | """ 142 | # send message to WebSocket 143 | self.send(text_data=json.dumps(event)) 144 | -------------------------------------------------------------------------------- /chat/models.py: -------------------------------------------------------------------------------- 1 | """models of chat app 2 | """ 3 | from datetime import datetime 4 | from typing import List 5 | from django.db.models.query import QuerySet 6 | from django.db import models 7 | from django.contrib.auth.models import User 8 | from django.utils.crypto import get_random_string 9 | from colorfield.fields import ColorField 10 | 11 | 12 | def unique_string(): 13 | random_string: str = get_random_string(8) 14 | all_room_id: QuerySet[Chat] = Chat.objects.all() 15 | rooms_id = list( 16 | map(lambda chat: chat.room_id, all_room_id)) 17 | while random_string in rooms_id: 18 | random_string: str = get_random_string(8) 19 | print(random_string) 20 | return random_string 21 | 22 | 23 | def remove_listener(list_: list) -> list: 24 | if "listener" in list_: 25 | list_.remove("listener") 26 | return list_ 27 | 28 | 29 | class Chat(models.Model): 30 | name = models.CharField( 31 | default='welcome', max_length=512, 32 | null=False, blank=False, 33 | verbose_name="گروه") 34 | members = models.ManyToManyField( 35 | User, 36 | blank=False, 37 | verbose_name="اعضا") 38 | timestamp = models.DateTimeField( 39 | auto_now_add=True, 40 | verbose_name="زمان") 41 | room_id = models.CharField( 42 | default=unique_string, 43 | unique=True, 44 | editable=False, 45 | max_length=8) 46 | 47 | @staticmethod 48 | def best_group() -> list: 49 | all_room: QuerySet[Chat] = Chat.objects.all().order_by('timestamp') 50 | rooms: List[tuple] = list( 51 | map(lambda room: (room.name, 52 | len(Message.objects.filter(room=room)), 53 | room.members.count()), all_room) 54 | ) 55 | busy_rooms = sorted( 56 | rooms, key=lambda room: room[1], reverse=True) 57 | sorted_rooms = sorted( 58 | busy_rooms, key=lambda room: room[2], reverse=True) 59 | best_room = list( 60 | map(lambda room: room[0], sorted_rooms[:8])) 61 | return remove_listener(best_room) 62 | 63 | @staticmethod 64 | def last_group() -> list: 65 | all_room: QuerySet[Chat] = Chat.objects.all().order_by( 66 | "-timestamp")[:8] 67 | last_room = list( 68 | map(lambda room: room.name, all_room)) 69 | return remove_listener(last_room) 70 | 71 | @staticmethod 72 | def your_group(user) -> list: 73 | chats: QuerySet[Chat] = Chat.objects.filter( 74 | members__username=user) 75 | group_messages: List[tuple] = [] 76 | for chat in chats: 77 | chat_name = chat.name 78 | try: 79 | last_message: Message = Message.objects.filter( 80 | room=chat).order_by("-timestamp")[0] 81 | except IndexError: 82 | continue 83 | message_time: datetime = last_message.timestamp 84 | group_messages.append((chat_name, message_time)) 85 | sorted_chats = sorted(group_messages, reverse=True, 86 | key=lambda data: data[1]) 87 | chat_names = list( 88 | map(lambda chat: chat[0], sorted_chats)) 89 | return remove_listener(chat_names) 90 | 91 | @staticmethod 92 | def all_groups() -> list: 93 | all_group = Chat.objects.all().order_by("-timestamp") 94 | groups = list( 95 | map(lambda group: group.name, all_group) 96 | ) 97 | return remove_listener(groups) 98 | 99 | @staticmethod 100 | def get_members_list(room_id: str): 101 | room: Chat = Chat.objects.get(room_id=room_id) 102 | members = list( 103 | map(lambda user: user.username, room.members.all()) 104 | ) 105 | return members 106 | 107 | class Meta: 108 | verbose_name_plural = "چت ها" 109 | verbose_name = "چت" 110 | 111 | def __str__(self): 112 | return self.name 113 | 114 | 115 | class Message(models.Model): 116 | TYPES = ( 117 | ('message', 'پیام'), 118 | ('image', 'عکس'), 119 | ) 120 | author = models.ForeignKey( 121 | User, on_delete=models.CASCADE, 122 | verbose_name="کاربر") 123 | content = models.TextField(verbose_name="پیام") 124 | timestamp = models.DateTimeField( 125 | auto_now_add=True, 126 | verbose_name="زمان") 127 | room = models.ForeignKey( 128 | Chat, on_delete=models.CASCADE, 129 | null=False, blank=False, 130 | verbose_name="گروه") 131 | type = models.CharField( 132 | max_length=32, 133 | choices=TYPES, 134 | default="message", 135 | verbose_name="نوع") 136 | 137 | @staticmethod 138 | def last_messages(room_id: str): 139 | room = Chat.objects.get(room_id=room_id) 140 | return Message.objects.filter( 141 | room=room).order_by("-timestamp")[:22] 142 | 143 | def author_username(self): 144 | return self.author.username 145 | 146 | def get_time(self): 147 | time = datetime.strftime( 148 | self.timestamp.astimezone(), 149 | "%I:%M %p") 150 | return time 151 | 152 | def __str__(self): 153 | return f'پیام "{self.author}" در گروه "{self.room}"' 154 | 155 | class Meta: 156 | verbose_name_plural = "پیام ها" 157 | verbose_name = "پیام" 158 | 159 | 160 | class Customize(models.Model): 161 | default_fields = { 162 | "your_group_color": ("#198754", "گروه های شما"), 163 | "hot_group_color": ("#dc3545", "داغ ترین گروه ها"), 164 | "last_group_color": ("#0a58ca", "آخرین گروه ها"), 165 | "group_list_color": ("#6c757d", "لیست گروه ها"), 166 | "reply": ("#16a085", "پیام فرستنده"), 167 | "send": ("#2980b9", "پیام شما"), 168 | "input": ("#bdc3c7", "رنگ ورودی پیام"), 169 | "input_button": ("#7f8c8d", "رنگ دکمه های چت"), 170 | "input_button_hover": ("#95a5a6", "هاور شدن دکمه های چت"), 171 | "placeholder": ("#565f62", "رنگ placeholder"), 172 | "time": ("#333333", "رنگ زمان"), 173 | } 174 | 175 | name = models.CharField( 176 | max_length=64, 177 | default="title", 178 | null=False, blank=False, 179 | verbose_name="اسم") 180 | describe = models.CharField( 181 | max_length=128, 182 | default="عنوان صفحه اصلی", 183 | null=False, blank=False, 184 | verbose_name="درباره") 185 | color = ColorField(default="#27ae60", verbose_name="رنگ") 186 | 187 | @classmethod 188 | def create_default_fields(cls): 189 | for name, value in Customize.default_fields.items(): 190 | color, description = value 191 | Customize.objects.create( 192 | name=name, 193 | describe=description, 194 | color=color) 195 | 196 | @classmethod 197 | def get_all_fields(cls): 198 | fields: QuerySet[Customize] = Customize.objects.all() 199 | return {field.name: field.color for field in fields} 200 | 201 | def __str__(self): 202 | return self.name 203 | 204 | class Meta: 205 | verbose_name_plural = "شخصی سازی" 206 | verbose_name = "رنگ" 207 | -------------------------------------------------------------------------------- /chat/routing.py: -------------------------------------------------------------------------------- 1 | """chat routing 2 | """ 3 | from django.urls import re_path 4 | from . import consumer 5 | 6 | websocket_urlpatterns = [ 7 | re_path(r'ws/id/(?P\w+)/$', consumer.ChatConsumer.as_asgi()), 8 | ] 9 | -------------------------------------------------------------------------------- /chat/serializers.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from rest_framework import serializers 3 | from .models import Message 4 | 5 | 6 | class MessageSerializer(serializers.ModelSerializer): 7 | time = serializers.SerializerMethodField() 8 | 9 | class Meta: 10 | model = Message 11 | fields = ['author_username', 'content', 'type', 'time'] 12 | 13 | def get_time(self, message: Message): 14 | time = datetime.strftime( 15 | message.timestamp.astimezone(), 16 | "%I:%M %p") 17 | return time 18 | -------------------------------------------------------------------------------- /chat/templates/chat/about.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | 5 | {% block style %} 6 | 7 | {% endblock style %} 8 | 9 | 10 | {% block title %} 11 | درباره من 12 | {% endblock title %} 13 | 14 | 15 | {% block content %} 16 |

درباره من

17 |
18 |

سلام، من محمد دری ام سازنده نیوچت

19 |

به نیوچت خوش اومدی :)

20 |

امیدوارم که از نیوچت لذت ببری!

21 |

آشنایی بیشتر با پیامرسان نیوچت

22 |
23 |
24 |
25 |

محمد دری 2022

26 |

تلفن 09013128010

27 |

ایمیل mr.dori.dev@gmail.com

28 |

گیت هاب github.com/dori-dev

29 | 30 |
31 | {% endblock content %} -------------------------------------------------------------------------------- /chat/templates/chat/create-group.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block style %} 5 | 6 | {% endblock style %} 7 | 8 | 9 | {% block title %} 10 | نیوچت - ساخت گروه 11 | {% endblock title %} 12 | {% block content %} 13 | 14 | 20 | 37 | {% endblock content %} -------------------------------------------------------------------------------- /chat/templates/chat/group-list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block style %} 5 | 6 | {% endblock style %} 7 | 8 | {% block title %} 9 | نیوچت - لیست گروه ها 10 | {% endblock title %} 11 | 12 | {% block content %} 13 |

لیست تمام گروه ها

14 | 22 | 23 | 24 | 25 | 99 | 100 | {% endblock content %} -------------------------------------------------------------------------------- /chat/templates/chat/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block style %} 5 | 6 | {% endblock style %} 7 | 8 | {% block title %} 9 | نیوچت 10 | {% endblock title %} 11 | {% block content %} 12 | 18 | 19 | {% if user.is_authenticated %} {% if your_groups_len %} 20 |

گروه های شما

21 | 29 | {% endif %} {% endif %} 30 | 31 |

داغ ترین گروه ها 🔥

32 | 40 | 41 |

آخرین گروه ها 🆕

42 | 50 | 67 | {% endblock content %} -------------------------------------------------------------------------------- /chat/templates/chat/room.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{ room }} - نیوچت 13 | 14 | 15 | 16 | 17 |
18 |
19 | 21 | 23 | بازگشت 24 |

گروه "{{ room }}"

25 |
26 |
27 |
28 |
29 |
30 | 31 | 39 | 46 |
47 |
48 |
49 | 50 | 51 | 52 |

53 | 54 | {{ room_id|json_script:"room-id" }} 55 | 56 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /chat/test_consumer.py: -------------------------------------------------------------------------------- 1 | from channels.testing import ChannelsLiveServerTestCase 2 | from selenium import webdriver 3 | from selenium.webdriver.common.action_chains import ActionChains 4 | from selenium.webdriver.support.wait import WebDriverWait 5 | 6 | 7 | class ConsumerTests(ChannelsLiveServerTestCase): 8 | """Consumer test class 9 | """ 10 | serve_static = True # emulate StaticLiveServerTestCase 11 | 12 | @classmethod 13 | def setUpClass(cls): 14 | super().setUpClass() 15 | try: 16 | # Requires "chromedriver" binary to be installed in $PATH 17 | cls.driver = webdriver.Chrome() 18 | except Exception: 19 | super().tearDownClass() 20 | raise 21 | 22 | @classmethod 23 | def tearDownClass(cls): 24 | cls.driver.quit() 25 | super().tearDownClass() 26 | 27 | def test_chat_same_room(self): 28 | """test when chat message posted then 29 | seen by everyone in same room 30 | """ 31 | try: 32 | self._register("ali", "1234") 33 | self._enter_chat_room("room_1") 34 | self._open_new_window() 35 | self._enter_chat_room("room_1") 36 | self._switch_to_window(0) 37 | self._post_message("hello") 38 | WebDriverWait(self.driver, 2).until( 39 | lambda _: 40 | "hello" in self._chat_message_value, 41 | "Message was not received by window 1 from window 1") 42 | self._switch_to_window(1) 43 | WebDriverWait(self.driver, 2).until( 44 | lambda _: 45 | "hello" in self._chat_message_value, 46 | "Message was not received by window 2 from window 1") 47 | finally: 48 | self._close_all_new_windows() 49 | 50 | def test_chat_different_room(self): 51 | """test when chat message posted then 52 | not seen by anyone in different room 53 | """ 54 | try: 55 | self._register("mohammad", "12345678") 56 | self._enter_chat_room("room_1") 57 | self._open_new_window() 58 | self._enter_chat_room("room_2") 59 | self._switch_to_window(0) 60 | self._post_message("hello") 61 | WebDriverWait(self.driver, 2).until( 62 | lambda _: 63 | "hello" in self._chat_message_value, 64 | "Message was not received by window 1 from window 1") 65 | self._switch_to_window(1) 66 | self._post_message("world") 67 | WebDriverWait(self.driver, 2).until( 68 | lambda _: 69 | "world" in self._chat_message_value, 70 | "Message was not received by window 2 from window 2") 71 | self.assertTrue( 72 | "hello" not in self._chat_message_value, 73 | "Message was improperly received by window 2 from window 1") 74 | finally: 75 | self._close_all_new_windows() 76 | 77 | def _enter_chat_room(self, room_name): 78 | self.driver.get(self.live_server_url + "/chat/") 79 | ActionChains(self.driver).send_keys(room_name + "\n").perform() 80 | 81 | def _open_new_window(self): 82 | self.driver.execute_script('window.open("about: blank", "_blank");') 83 | self.driver.switch_to.window(self.driver.window_handles[-1]) 84 | 85 | def _register(self, username: str, password: str): 86 | self.driver.get(self.live_server_url + "/auth/register/") 87 | username_input = self.driver.find_element_by_id("id_username") 88 | password1_input = self.driver.find_element_by_id("id_password1") 89 | password2_input = self.driver.find_element_by_id("id_password2") 90 | register_button = self.driver.find_element_by_id("register") 91 | username_input.send_keys(username) 92 | password1_input.send_keys(password) 93 | password2_input.send_keys(password) 94 | register_button.click() 95 | self.driver.get(self.live_server_url) 96 | 97 | def _close_all_new_windows(self): 98 | while len(self.driver.window_handles) > 1: 99 | self.driver.switch_to.window(self.driver.window_handles[-1]) 100 | self.driver.execute_script("window.close();") 101 | if len(self.driver.window_handles) == 1: 102 | self.driver.switch_to.window(self.driver.window_handles[0]) 103 | 104 | def _switch_to_window(self, window_index): 105 | self.driver.switch_to.window(self.driver.window_handles[window_index]) 106 | 107 | def _post_message(self, message): 108 | ActionChains(self.driver).send_keys(message + "\n").perform() 109 | 110 | @property 111 | def _chat_message_value(self): 112 | chat_messages = self.driver.find_elements_by_class_name( 113 | "text") 114 | values = [] 115 | for message in chat_messages: 116 | values.append(message.text) 117 | return values 118 | -------------------------------------------------------------------------------- /chat/tests_models.py: -------------------------------------------------------------------------------- 1 | """chat test 2 | """ 3 | from datetime import datetime 4 | from django.db import IntegrityError 5 | from django.test import TestCase 6 | from .models import Message, Chat, User 7 | 8 | 9 | class MessageTestCase(TestCase): 10 | """test Message model 11 | """ 12 | 13 | def setUp(self): 14 | self.test_user1 = User.objects.create(username="john") 15 | self.test_user2 = User.objects.create(username="alice") 16 | chat: Chat = Chat.objects.create(name="chat_room1") 17 | chat.members.add(self.test_user1) 18 | self.test_room1 = chat 19 | chat: Chat = Chat.objects.create(name="chat_room2") 20 | chat.members.add(self.test_user1) 21 | chat.members.add(self.test_user2) 22 | self.test_room2 = chat 23 | 24 | def test_message_user(self): 25 | message: Message = Message.objects.create( 26 | author=self.test_user2, 27 | content="test", 28 | room=self.test_room2) 29 | self.assertEqual(message.author.username, "alice") 30 | 31 | def test_message_content(self): 32 | message: Message = Message.objects.create( 33 | author=self.test_user1, 34 | content="12345678", 35 | room=self.test_room2) 36 | self.assertEqual(message.content, "12345678") 37 | 38 | def test_message_length(self): 39 | message: Message = Message.objects.create( 40 | author=self.test_user1, 41 | content="ali", 42 | room=self.test_room1) 43 | self.assertEqual(len(message.content), 3) 44 | 45 | def test_message_no_user(self): 46 | with self.assertRaises(IntegrityError): 47 | Message.objects.create( 48 | author=None, content="test", room=self.test_room2) 49 | 50 | def test_message_create_retrieve(self): 51 | message_id = Message.objects.create( 52 | author=self.test_user1, 53 | content="the content", 54 | room=self.test_room2).id 55 | message: Message = Message.objects.get(id=message_id) 56 | # Asserts 57 | self.assertEqual(message.content, "the content") 58 | self.assertEqual(message.author, self.test_user1) 59 | self.assertEqual(message.room, self.test_room2) 60 | self.assertEqual(message.room.name, "chat_room2") 61 | 62 | def test_author_username(self): 63 | message: Message = Message.objects.create( 64 | author=self.test_user2, 65 | content="hello there", 66 | room=self.test_room2) 67 | self.assertEqual(message.author_username(), "alice") 68 | 69 | def test_get_time(self): 70 | current_time = datetime.now() 71 | formated_time = datetime.strftime( 72 | current_time, 73 | "%I:%M %p") 74 | message: Message = Message.objects.create( 75 | author=self.test_user1, 76 | content="this is for test time :)", 77 | room=self.test_room1) 78 | self.assertEqual(message.get_time(), formated_time) 79 | 80 | 81 | class ChatTestCase(TestCase): 82 | """test Chat model 83 | """ 84 | 85 | def setUp(self): 86 | self.test_user1 = User.objects.create(username="John") 87 | self.test_user2 = User.objects.create(username="Alice") 88 | self.test_user3 = User.objects.create(username="Bill") 89 | self.test_user4 = User.objects.create(username="Rose") 90 | 91 | def test_chat_member(self): 92 | chat: Chat = Chat.objects.create(name="greeting") 93 | chat.members.add(self.test_user1) 94 | self.assertEqual(chat.members.count(), 1) 95 | 96 | def test_chat_members(self): 97 | chat: Chat = Chat.objects.create(name="greeting") 98 | chat.members.add(self.test_user1) 99 | chat.members.add(self.test_user2) 100 | self.assertEqual(chat.members.count(), 2) 101 | 102 | def test_twice_chat_members(self): 103 | chat: Chat = Chat.objects.create(name="greeting") 104 | chat.members.add(self.test_user1) 105 | chat.members.add(self.test_user1) 106 | self.assertEqual(chat.members.count(), 1) 107 | 108 | def test_chat_empty_member(self): 109 | chat: Chat = Chat.objects.create(name="greeting") 110 | self.assertEqual(chat.members.count(), 0) 111 | 112 | def test_chat_name(self): 113 | chat: Chat = Chat.objects.create(name="Hi-There") 114 | self.assertEqual(chat.name, "Hi-There") 115 | 116 | def test_chat_empty_name(self): 117 | chat: Chat = Chat.objects.create() 118 | self.assertEqual(chat.name, "welcome") 119 | 120 | def test_all_group(self): 121 | Chat.objects.create(name="group-1") 122 | Chat.objects.create(name="listener") 123 | Chat.objects.create(name="group-3") 124 | Chat.objects.create(name="group-2") 125 | self.assertEqual( 126 | Chat.all_groups(), ["group-2", "group-3", "group-1"]) 127 | self.assertEqual(len(Chat.all_groups()), 3) 128 | 129 | def test_members_list(self): 130 | chat: Chat = Chat.objects.create(name="family") 131 | chat.members.add(self.test_user1) 132 | chat.members.add(self.test_user2) 133 | chat.members.add(self.test_user4) 134 | chat.members.add(self.test_user3) 135 | members = Chat.get_members_list(chat.room_id) 136 | self.assertEqual(len(members), 4) 137 | 138 | def test_user_empty_group(self): 139 | me = self.test_user3 140 | chat1: Chat = Chat.objects.create(name="saved") 141 | chat1.members.add(me) 142 | chat2: Chat = Chat.objects.create(name="test") 143 | chat2.members.add(me) 144 | chat3: Chat = Chat.objects.create(name="hello") 145 | chat3.members.add(me) 146 | groups = Chat.your_group(me) 147 | self.assertEqual(groups, []) 148 | 149 | def test_user_groups(self): 150 | me = self.test_user2 151 | chat1: Chat = Chat.objects.create(name="welcome") 152 | chat1.members.add(me) 153 | chat2: Chat = Chat.objects.create(name="nice") 154 | chat2.members.add(me) 155 | chat3: Chat = Chat.objects.create(name="hello") 156 | chat3.members.add(me) 157 | Message.objects.create( 158 | author=me, 159 | content="hello there", 160 | room=chat2 161 | ) 162 | Message.objects.create( 163 | author=me, 164 | content="i am join you!!", 165 | room=chat1 166 | ) 167 | groups = Chat.your_group(me) 168 | self.assertEqual(groups, ["welcome", "nice"]) 169 | -------------------------------------------------------------------------------- /chat/urls.py: -------------------------------------------------------------------------------- 1 | """chat urls 2 | """ 3 | from django.urls import path 4 | from . import views 5 | 6 | urlpatterns = [ 7 | path('', views.index, name='index'), 8 | path('chat/', views.index, name='index'), 9 | path('groups/', views.group_list, name="groups"), 10 | path('create/', views.create_group, name='create'), 11 | path('about/', views.about, name='about'), 12 | path('chat//', views.room, name='room'), 13 | path('id//', views.group_view, name='room'), 14 | ] 15 | -------------------------------------------------------------------------------- /chat/views.py: -------------------------------------------------------------------------------- 1 | """chat views 2 | """ 3 | import json 4 | from django.core.paginator import Paginator 5 | from django.shortcuts import redirect, render 6 | from django.utils.safestring import mark_safe 7 | from django.contrib.auth.decorators import login_required 8 | from django.utils.text import slugify 9 | from .models import Chat, Customize 10 | 11 | 12 | def index(request: object): 13 | user = request.user 14 | color_fields = Customize.get_all_fields() 15 | context = { 16 | 'best_groups': Chat.best_group(), 17 | 'last_groups': Chat.last_group(), 18 | } 19 | if user.is_authenticated: 20 | your_groups = Chat.your_group(user) 21 | context['your_groups'] = your_groups 22 | context['your_groups_len'] = len(your_groups) 23 | context = {**context, **color_fields} 24 | return render(request, "chat/index.html", context) 25 | 26 | 27 | @login_required(login_url="auth:register") 28 | def room(request: object, room_name: str): 29 | room_name = slugify(room_name, allow_unicode=True) 30 | if room_name == "listener": 31 | return redirect("/chat/listener2") 32 | user = request.user 33 | chat_model = Chat.objects.filter(name=room_name) 34 | if chat_model.exists(): 35 | chat_model[0].members.add(user) 36 | else: 37 | chat = Chat.objects.create(name=room_name) 38 | chat.members.add(user) 39 | chat = Chat.objects.get(name=room_name) 40 | room_id = chat.room_id 41 | return redirect(f'/id/{room_id}') 42 | 43 | 44 | def group_list(request: object): 45 | color_fields = Customize.get_all_fields() 46 | all_groups = Chat.all_groups() 47 | paginator = Paginator(all_groups, 24) 48 | page_number = request.GET.get('page') 49 | page_obj = paginator.get_page(page_number) 50 | context = { 51 | 'page_obj': page_obj 52 | } 53 | context = {**context, **color_fields} 54 | return render(request, "chat/group-list.html", context) 55 | 56 | 57 | def create_group(request: object): 58 | return render(request, "chat/create-group.html") 59 | 60 | 61 | @login_required(login_url="auth:register") 62 | def group_view(request: object, room_id: str): 63 | color_fields = Customize.get_all_fields() 64 | listener_room = Chat.objects.filter(name="listener") 65 | if not listener_room.exists(): 66 | Chat.objects.create(name="listener") 67 | listener: Chat = Chat.objects.get(name="listener") 68 | if room_id == listener.room_id: 69 | return redirect("/chat/listener2") 70 | user = request.user 71 | chat_model = Chat.objects.filter(room_id=room_id) 72 | if chat_model.exists(): 73 | chat_model[0].members.add(user) 74 | else: 75 | return redirect("index") 76 | username = request.user.username 77 | context = { 78 | "room_id": room_id, 79 | "username": mark_safe(json.dumps(username)), 80 | "name": user, 81 | "room": chat_model[0].name, 82 | "listener_id": listener.room_id 83 | } 84 | context = {**context, **color_fields} 85 | return render(request, "chat/room.html", context) 86 | 87 | 88 | def about(request: object): 89 | return render(request, "chat/about.html") 90 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dori-dev/django-chat/e1570e9915e0547e03cd1f5d26f1a54e2b24fef8/core/__init__.py -------------------------------------------------------------------------------- /core/asgi.py: -------------------------------------------------------------------------------- 1 | """ASGI config for core project. 2 | """ 3 | 4 | import os 5 | from channels.auth import AuthMiddlewareStack 6 | from channels.routing import ProtocolTypeRouter, URLRouter 7 | from django.core.asgi import get_asgi_application 8 | import chat.routing 9 | 10 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') 11 | 12 | application = ProtocolTypeRouter({ 13 | "http": get_asgi_application(), 14 | "websocket": AuthMiddlewareStack( 15 | URLRouter( 16 | chat.routing.websocket_urlpatterns 17 | ) 18 | ), 19 | }) 20 | -------------------------------------------------------------------------------- /core/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for core project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.9. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | from pathlib import Path 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-secret-key') 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | the_debug = os.environ.get('DEBUG', '1') == '1' 28 | if the_debug: 29 | DEBUG = True 30 | ALLOWED_HOSTS = [] 31 | else: 32 | DEBUG = False 33 | default_host = "127.0.0.1,localhost" 34 | ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', default_host).split(',') 35 | 36 | 37 | # Application definition 38 | 39 | INSTALLED_APPS = [ 40 | 'adminlte3', 41 | 'adminlte3_theme', 42 | 'django.contrib.admin', 43 | 'django.contrib.auth', 44 | 'django.contrib.contenttypes', 45 | 'django.contrib.sessions', 46 | 'django.contrib.messages', 47 | 'django.contrib.staticfiles', 48 | 'chat', 49 | 'auth', 50 | 'channels', 51 | 'rest_framework', 52 | 'colorfield', 53 | ] 54 | 55 | MIDDLEWARE = [ 56 | 'django.middleware.security.SecurityMiddleware', 57 | 'django.contrib.sessions.middleware.SessionMiddleware', 58 | 'django.middleware.common.CommonMiddleware', 59 | 'django.middleware.csrf.CsrfViewMiddleware', 60 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 61 | 'django.contrib.messages.middleware.MessageMiddleware', 62 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 63 | ] 64 | 65 | ROOT_URLCONF = 'core.urls' 66 | 67 | TEMPLATES = [ 68 | { 69 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 70 | 'DIRS': ['templates'], 71 | 'APP_DIRS': True, 72 | 'OPTIONS': { 73 | 'context_processors': [ 74 | 'django.template.context_processors.debug', 75 | 'django.template.context_processors.request', 76 | 'django.contrib.auth.context_processors.auth', 77 | 'django.contrib.messages.context_processors.messages', 78 | ], 79 | }, 80 | }, 81 | ] 82 | 83 | WSGI_APPLICATION = 'core.wsgi.application' 84 | 85 | # Channels 86 | ASGI_APPLICATION = 'core.asgi.application' 87 | CHANNEL_LAYERS = { 88 | 'default': { 89 | 'BACKEND': 'channels_redis.core.RedisChannelLayer', 90 | 'CONFIG': { 91 | "hosts": [('localhost', 6379)], 92 | }, 93 | }, 94 | } 95 | 96 | # Database 97 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 98 | 99 | DATABASES = { 100 | 'default': { 101 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 102 | 'NAME': os.environ.get('DB_NAME', 'chat_data'), 103 | 'USER': os.environ.get('DB_USER', 'postgres'), 104 | 'PASSWORD': os.environ.get('DB_PASSWORD', 'password'), 105 | 'HOST': os.environ.get('DB_HOST', 'localhost'), 106 | 'POST': int(os.environ.get('DB_POST', '5432')), 107 | 'TEST': { 108 | 'NAME': os.path.join(BASE_DIR, 'db_test.sqlite3') 109 | } 110 | } 111 | } 112 | 113 | 114 | # Internationalization 115 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 116 | 117 | LANGUAGE_CODE = 'fa-IR' 118 | 119 | TIME_ZONE = 'Asia/Tehran' 120 | 121 | USE_I18N = True 122 | 123 | USE_L10N = True 124 | 125 | USE_TZ = True 126 | 127 | 128 | # Static files (CSS, JavaScript, Images) 129 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 130 | 131 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 132 | STATIC_URL = '/static/' 133 | 134 | STATICFILES_DIRS = [ 135 | os.path.join(BASE_DIR, 'static') 136 | ] 137 | 138 | 139 | # Default primary key field type 140 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 141 | -------------------------------------------------------------------------------- /core/urls.py: -------------------------------------------------------------------------------- 1 | """core URL Configuration 2 | """ 3 | from django.contrib import admin 4 | from django.urls import path, include 5 | 6 | urlpatterns = [ 7 | path('admin/', admin.site.urls), 8 | path('', include('chat.urls')), 9 | path('auth/', include('auth.urls')), 10 | ] 11 | -------------------------------------------------------------------------------- /core/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for core project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | import dotenv 6 | import pathlib 7 | 8 | 9 | def main(): 10 | """Run administrative tasks.""" 11 | DOT_ENV_PATH = pathlib.Path() / '.env' 12 | if DOT_ENV_PATH.exists(): 13 | dotenv.read_dotenv(DOT_ENV_PATH) 14 | else: 15 | print("No .env found, be sure to make it!") 16 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') 17 | try: 18 | from django.core.management import execute_from_command_line 19 | except ImportError as exc: 20 | raise ImportError( 21 | "Couldn't import Django. Are you sure it's installed and " 22 | "available on your PYTHONPATH environment variable? Did you " 23 | "forget to activate a virtual environment?" 24 | ) from exc 25 | execute_from_command_line(sys.argv) 26 | 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | channels==3.0.4 2 | Django==4.0.3 3 | channels-redis==3.4.0 4 | selenium==4.1.3 5 | djangorestframework==3.13.1 6 | Markdown==3.3.6 7 | django-filter==21.1 8 | psycopg2==2.9.3 9 | django-adminlte-3==0.1.6 10 | django-dotenv==1.4.2 11 | django-colorfield==0.6.3 12 | -------------------------------------------------------------------------------- /static/assets/IRANSansWeb.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dori-dev/django-chat/e1570e9915e0547e03cd1f5d26f1a54e2b24fef8/static/assets/IRANSansWeb.woff2 -------------------------------------------------------------------------------- /static/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dori-dev/django-chat/e1570e9915e0547e03cd1f5d26f1a54e2b24fef8/static/assets/favicon.ico -------------------------------------------------------------------------------- /static/base.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:"IRAN Sans";src:url(assets/IRANSansWeb.woff2) format("woff2")}*,::after,::before{box-sizing:border-box}body{margin:0;font-family:"IRAN Sans",sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}.custom-nav{margin-left:3px}@media (prefers-reduced-motion: no-preference){:root{scroll-behavior:smooth}}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width: 1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + 0.9vw)}@media (min-width: 1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + 0.6vw)}@media (min-width: 1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + 0.3vw)}@media (min-width: 1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}img,svg{vertical-align:middle}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input{margin:0;font-family:"IRAN Sans",sans-serif;font-size:inherit;line-height:inherit}button{text-transform:none}[role="button"]{cursor:pointer}[type="button"],[type="reset"],[type="submit"],button{-webkit-appearance:button}[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}::-webkit-file-upload-button{font:inherit}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}.container,.container-fluid{width:100%;padding-right:var(--bs-gutter-x,0.75rem);padding-left:var(--bs-gutter-x,0.75rem);margin-right:auto;margin-left:auto}@media (min-width: 576px){.container{max-width:540px}}@media (min-width: 768px){.container{max-width:720px}}@media (min-width: 992px){.container{max-width:960px}}@media (min-width: 1200px){.container{max-width:1140px}}@media (min-width: 1400px){.container{max-width:1320px}}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar > .container,.navbar > .container-fluid{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width: 576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width: 768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width: 1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width: 1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,0.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,0.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,0.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,0.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,0.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show > .nav-link{color:rgba(0,0,0,0.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,0.55);border-color:rgba(0,0,0,0.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280,0,0,0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,0.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,0.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,0.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,0.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,0.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show > .nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,0.55);border-color:rgba(255,255,255,0.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255,255,255,0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,0.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus + .btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,0.25)}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus + .btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,0.5)}.btn-check:active + .btn-primary,.btn-check:checked + .btn-primary,.btn-primary.active,.btn-primary:active,.show > .btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active + .btn-primary:focus,.btn-check:checked + .btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show > .btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,0.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus + .btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,0.5)}.btn-check:active + .btn-secondary,.btn-check:checked + .btn-secondary,.btn-secondary.active,.btn-secondary:active,.show > .btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active + .btn-secondary:focus,.btn-check:checked + .btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show > .btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,0.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus + .btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,0.5)}.btn-check:active + .btn-success,.btn-check:checked + .btn-success,.btn-success.active,.btn-success:active,.show > .btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active + .btn-success:focus,.btn-check:checked + .btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show > .btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,0.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus + .btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,0.5)}.btn-check:active + .btn-outline-primary,.btn-check:checked + .btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active + .btn-outline-primary:focus,.btn-check:checked + .btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,0.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus + .btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,0.5)}.btn-check:active + .btn-outline-secondary,.btn-check:checked + .btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active + .btn-outline-secondary:focus,.btn-check:checked + .btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,0.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus + .btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,0.5)}.btn-check:active + .btn-outline-success,.btn-check:checked + .btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active + .btn-outline-success:focus,.btn-check:checked + .btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,0.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus + .btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,0.5)}.btn-check:active + .btn-outline-danger,.btn-check:checked + .btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active + .btn-outline-danger:focus,.btn-check:checked + .btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,0.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control[type="file"]{overflow:hidden}.form-control[type="file"]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,0.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}@media (min-width: 992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion: reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show > .nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill > .nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified > .nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,0.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion: reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion: reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,0.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width: 576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width: 768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width: 992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width: 1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width: 1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,0.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,0.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,0.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,0.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical > .btn,.btn-group > .btn{position:relative;flex:1 1 auto}.btn-group-vertical > .btn-check:checked + .btn,.btn-group-vertical > .btn-check:focus + .btn,.btn-group-vertical > .btn.active,.btn-group-vertical > .btn:active,.btn-group-vertical > .btn:focus,.btn-group-vertical > .btn:hover,.btn-group > .btn-check:checked + .btn,.btn-group > .btn-check:focus + .btn,.btn-group > .btn.active,.btn-group > .btn:active,.btn-group > .btn:focus,.btn-group > .btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group > .btn-group:not(:first-child),.btn-group > .btn:not(:first-child){margin-left:-1px}.btn-group > .btn-group:not(:last-child) > .btn,.btn-group > .btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group > .btn-group:not(:first-child) > .btn,.btn-group > .btn:nth-child(n + 3),.btn-group > :not(.btn-check) + .btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm > .btn + .dropdown-toggle-split,.btn-sm + .dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg > .btn + .dropdown-toggle-split,.btn-lg + .dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical > .btn,.btn-group-vertical > .btn-group{width:100%}.btn-group-vertical > .btn-group:not(:first-child),.btn-group-vertical > .btn:not(:first-child){margin-top:-1px}.btn-group-vertical > .btn-group:not(:last-child) > .btn,.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical > .btn-group:not(:first-child) > .btn,.btn-group-vertical > .btn ~ .btn{border-top-left-radius:0;border-top-right-radius:0} -------------------------------------------------------------------------------- /static/css/about.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 19px; 3 | } 4 | 5 | h1 { 6 | margin: 30px 10px 40px 0; 7 | } 8 | 9 | a { 10 | color: #007bff; 11 | text-decoration: none; 12 | background-color: transparent; 13 | -webkit-text-decoration-skip: objects; 14 | text-decoration: none; 15 | } 16 | 17 | a:visited, 18 | a:hover { 19 | color: #0056b3; 20 | text-decoration: underline; 21 | } 22 | 23 | .about p { 24 | margin: 7px 25px 0 0; 25 | } 26 | .social p { 27 | margin: 10px 25px 0 0; 28 | } 29 | -------------------------------------------------------------------------------- /static/css/auth.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: white; 3 | color: black; 4 | font-family: "IRAN Sans", sans-serif; 5 | font-weight: normal; 6 | } 7 | 8 | /* Base Style */ 9 | 10 | label { 11 | display: inline-block; 12 | } 13 | button { 14 | border-radius: 0; 15 | } 16 | button:focus:not(:focus-visible) { 17 | outline: 0; 18 | } 19 | button, 20 | input { 21 | margin: 0; 22 | font-family: inherit; 23 | font-size: inherit; 24 | line-height: inherit; 25 | } 26 | button { 27 | text-transform: none; 28 | } 29 | 30 | .btn { 31 | display: inline-block; 32 | font-weight: 400; 33 | line-height: 1.5; 34 | color: #212529; 35 | text-align: center; 36 | text-decoration: none; 37 | vertical-align: middle; 38 | cursor: pointer; 39 | -webkit-user-select: none; 40 | -moz-user-select: none; 41 | user-select: none; 42 | background-color: transparent; 43 | border: 1px solid transparent; 44 | padding: 0.375rem 0.75rem; 45 | font-size: 1rem; 46 | border-radius: 0.3rem; 47 | transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, 48 | border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 49 | } 50 | @media (prefers-reduced-motion: reduce) { 51 | .btn { 52 | transition: none; 53 | } 54 | } 55 | 56 | .btn-primary { 57 | color: #fff; 58 | background-color: #0d6efd; 59 | border-color: #0d6efd; 60 | } 61 | .btn-primary:hover { 62 | color: #fff; 63 | background-color: #0b5ed7; 64 | border-color: #0a58ca; 65 | } 66 | .btn-check:focus + .btn-primary, 67 | .btn-primary:focus { 68 | color: #fff; 69 | background-color: #0b5ed7; 70 | border-color: #0a58ca; 71 | box-shadow: 0 0 0 0.25rem rgba(49, 132, 253, 0.5); 72 | } 73 | 74 | .btn-success { 75 | color: #fff; 76 | background-color: #198754; 77 | border-color: #198754; 78 | } 79 | .btn-success:hover { 80 | color: #fff; 81 | background-color: #157347; 82 | border-color: #146c43; 83 | } 84 | .btn-check:focus + .btn-success, 85 | .btn-success:focus { 86 | color: #fff; 87 | background-color: #157347; 88 | border-color: #146c43; 89 | box-shadow: 0 0 0 0.25rem rgba(60, 153, 110, 0.5); 90 | } 91 | 92 | ul { 93 | padding-left: 2rem; 94 | margin-top: 0; 95 | margin-bottom: 1rem; 96 | } 97 | 98 | .form-control { 99 | display: block; 100 | width: 100%; 101 | padding: 0.375rem 0.75rem; 102 | font-size: 1rem; 103 | font-weight: 400; 104 | line-height: 1.5; 105 | color: #212529; 106 | background-color: #fff; 107 | background-clip: padding-box; 108 | border: 1px solid #ced4da; 109 | -webkit-appearance: none; 110 | -moz-appearance: none; 111 | appearance: none; 112 | border-radius: 0.25rem; 113 | transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 114 | } 115 | @media (prefers-reduced-motion: reduce) { 116 | .form-control { 117 | transition: none; 118 | } 119 | } 120 | 121 | .form-control:focus { 122 | color: #212529; 123 | background-color: #fff; 124 | border-color: #86b7fe; 125 | outline: 0; 126 | box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); 127 | } 128 | .form-control::-webkit-date-and-time-value { 129 | height: 1.5em; 130 | } 131 | .form-control::-moz-placeholder { 132 | color: #6c757d; 133 | opacity: 1; 134 | } 135 | .form-control::placeholder { 136 | color: #6c757d; 137 | opacity: 1; 138 | } 139 | 140 | h1, 141 | h2, 142 | h3 { 143 | margin-top: 0; 144 | margin-bottom: 0.5rem; 145 | font-weight: 500; 146 | line-height: 1.2; 147 | } 148 | h1 { 149 | font-size: calc(1.375rem + 1.5vw); 150 | } 151 | @media (min-width: 1200px) { 152 | h1 { 153 | font-size: 2.5rem; 154 | } 155 | } 156 | h2 { 157 | font-size: calc(1.325rem + 0.9vw); 158 | } 159 | @media (min-width: 1200px) { 160 | h2 { 161 | font-size: 2rem; 162 | } 163 | } 164 | h3 { 165 | font-size: calc(1.3rem + 0.6vw); 166 | } 167 | @media (min-width: 1200px) { 168 | h3 { 169 | font-size: 1.75rem; 170 | } 171 | } 172 | 173 | /* Add Todo */ 174 | 175 | .input-container { 176 | display: flex; 177 | flex-direction: column; 178 | width: 100%; 179 | margin-bottom: 15px; 180 | } 181 | 182 | .input-container label { 183 | margin-bottom: 4px; 184 | font-size: 22px; 185 | } 186 | 187 | .neighbor-btn { 188 | margin-left: 10px; 189 | } 190 | 191 | .errorlist li { 192 | font-size: 0px; 193 | } 194 | 195 | .error { 196 | margin-bottom: 30px; 197 | } 198 | 199 | .errorlist li ul li { 200 | color: #e24139; 201 | margin-top: 5px; 202 | font-size: 18px; 203 | display: block; 204 | list-style-type: none; 205 | } 206 | 207 | .auth-btn { 208 | margin-top: 20px; 209 | font-size: 20px; 210 | } 211 | 212 | .page-label { 213 | font-size: 45px; 214 | margin-bottom: 30px; 215 | margin-top: 15px; 216 | } 217 | 218 | .auth-form { 219 | margin-bottom: 100px; 220 | } 221 | 222 | .large-btn { 223 | font-size: 22px; 224 | } 225 | 226 | .warning { 227 | padding-bottom: 50px; 228 | color: #d0342c; 229 | } 230 | 231 | .about { 232 | margin-top: 10px; 233 | margin-bottom: 10px; 234 | font-size: 18px; 235 | } 236 | 237 | .copyright-about { 238 | margin-top: 10px; 239 | font-size: 20px; 240 | } 241 | 242 | .footer-basic { 243 | padding: 30px 0; 244 | } 245 | 246 | .link, 247 | .how-to-use a { 248 | color: #007bff; 249 | text-decoration: none; 250 | background-color: transparent; 251 | -webkit-text-decoration-skip: objects; 252 | } 253 | 254 | .link:hover, 255 | .link:visited, 256 | .how-to-use a:hover, 257 | .how-to-use a:visited { 258 | color: #0056b3; 259 | text-decoration: underline; 260 | } 261 | -------------------------------------------------------------------------------- /static/css/chat.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "IRAN Sans"; 3 | src: url("../assets/IRANSansWeb.woff2") format("woff2"); 4 | } 5 | 6 | body { 7 | background: #eee; 8 | font-family: "IRAN Sans", sans-serif; 9 | margin: 0; 10 | } 11 | 12 | :root { 13 | --reply: #16a085; 14 | --send: #2980b9; 15 | --input: #bdc3c7; 16 | --input-button: #7f8c8d; 17 | --input-button-hover: #95a5a6; 18 | --placeholder: #565f62; 19 | --time: #333333; 20 | } 21 | 22 | ::placeholder { 23 | color: var(--placeholder); 24 | } 25 | 26 | .chat-label { 27 | overflow: hidden; 28 | background-color: #2d3436; 29 | position: fixed; 30 | top: 0; 31 | width: 100%; 32 | z-index: 10; 33 | } 34 | 35 | .chat-label a { 36 | float: right; 37 | display: block; 38 | color: #f2f2f2; 39 | text-align: center; 40 | padding: 14px 16px; 41 | text-decoration: none; 42 | font-size: 17px; 43 | } 44 | 45 | .chat-label p { 46 | margin: 0; 47 | margin-top: 5px; 48 | float: right; 49 | display: block; 50 | color: #f2f2f2; 51 | text-align: center; 52 | padding: 14px 16px; 53 | text-decoration: none; 54 | font-size: 17px; 55 | } 56 | 57 | .chat-label a:hover { 58 | background: #5c5c5c; 59 | } 60 | 61 | .chat-body .answer.left { 62 | padding: 0 0 0 25px; 63 | text-align: left; 64 | float: left; 65 | } 66 | 67 | .chat-body .answer { 68 | position: relative; 69 | max-width: 600px; 70 | overflow: hidden; 71 | clear: both; 72 | } 73 | 74 | .chat-body .answer:first-child { 75 | margin-top: 70px; 76 | } 77 | .chat-body .answer:last-child { 78 | margin-bottom: 100px; 79 | } 80 | 81 | .chat-body .answer .name { 82 | margin-top: 10px; 83 | font-size: 15px; 84 | line-height: 36px; 85 | } 86 | 87 | .chat-body .answer.left .text { 88 | background: var(--reply); 89 | color: #fff; 90 | border-radius: 12px 12px 12px 0; 91 | } 92 | 93 | .chat-body .answer.left .text:before { 94 | left: -24px; 95 | border-right-color: var(--reply); 96 | border-right-width: 12px; 97 | } 98 | 99 | .chat-body .answer .text { 100 | padding: 12px; 101 | font-size: 16px; 102 | line-height: 26px; 103 | position: relative; 104 | } 105 | 106 | .chat-body .answer .text:before { 107 | content: ""; 108 | display: block; 109 | position: absolute; 110 | bottom: 0; 111 | border: 18px solid transparent; 112 | border-bottom-width: 0; 113 | } 114 | 115 | .chat-body .answer.left .time { 116 | padding-right: 10px; 117 | color: var(--time); 118 | direction: ltr; 119 | } 120 | 121 | .chat-body .answer .time { 122 | font-size: 16px; 123 | line-height: 36px; 124 | position: relative; 125 | padding-bottom: 1px; 126 | } 127 | 128 | .chat-body .answer.right { 129 | padding: 0 25px 0 0; 130 | text-align: right; 131 | float: right; 132 | } 133 | 134 | .chat-body .answer.right .text { 135 | background: var(--send); 136 | color: #fff; 137 | border-radius: 12px 12px 0 12px; 138 | } 139 | 140 | .chat-body .answer.right .text:before { 141 | right: -24px; 142 | border-left-color: var(--send); 143 | border-left-width: 12px; 144 | } 145 | 146 | .chat-body .answer.right .time { 147 | padding-left: 10px; 148 | color: var(--time); 149 | direction: ltr; 150 | } 151 | 152 | .message-input { 153 | bottom: 0; 154 | width: 100%; 155 | z-index: 99; 156 | display: flex; 157 | flex-direction: row-reverse; 158 | justify-content: flex-end; 159 | margin-bottom: 5px; 160 | } 161 | 162 | .message-input input { 163 | direction: rtl; 164 | font-family: "IRAN Sans", sans-serif; 165 | border: none; 166 | width: calc(100% - 165px); 167 | max-width: 700px; 168 | height: 40px; 169 | padding: 5px 16px 5px 4px; 170 | font-size: 1.3em; 171 | border-top-left-radius: 20px; 172 | border-bottom-left-radius: 20px; 173 | color: #000; 174 | background-color: var(--input); 175 | } 176 | 177 | .message-input input:focus { 178 | outline: none; 179 | } 180 | 181 | .message-input button { 182 | padding-top: 5px; 183 | border: none; 184 | width: 60px; 185 | height: 50px; 186 | cursor: pointer; 187 | font-size: 1.8em; 188 | background: var(--input-button); 189 | color: #000; 190 | } 191 | 192 | .message-input .send-msg { 193 | border-top-right-radius: 20px; 194 | border-bottom-right-radius: 20px; 195 | } 196 | 197 | .message-input button:hover { 198 | background: var(--input-button-hover); 199 | } 200 | 201 | .message-input button:focus { 202 | outline: none; 203 | } 204 | 205 | .image { 206 | max-width: 50%; 207 | border-radius: 5px; 208 | cursor: pointer; 209 | transition: 0.3s; 210 | } 211 | 212 | .image:hover { 213 | opacity: 0.7; 214 | } 215 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | ul li { 2 | display: block; 3 | } 4 | 5 | ul a { 6 | text-decoration: none; 7 | } 8 | 9 | .search { 10 | margin-top: 20px; 11 | margin-right: 10px; 12 | } 13 | 14 | .title { 15 | margin-top: 60px; 16 | margin-right: 10px; 17 | } 18 | 19 | .hint { 20 | margin-top: 30px; 21 | } 22 | 23 | .submit-btn { 24 | font-size: larger; 25 | margin-top: -10px; 26 | } 27 | 28 | .group-input { 29 | max-width: 600px; 30 | font-size: large; 31 | } 32 | 33 | .custom-btn { 34 | margin: 10px; 35 | } 36 | 37 | .all-group-btn { 38 | font-size: large; 39 | margin: 10px; 40 | } -------------------------------------------------------------------------------- /static/js/reconnecting_ws.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | if (typeof define === "function" && define.amd) { 3 | define([], factory); 4 | } else if (typeof module !== "undefined" && module.exports) { 5 | module.exports = factory(); 6 | } else { 7 | global.ReconnectingWebSocket = factory(); 8 | } 9 | })(this, function () { 10 | if (!("WebSocket" in window)) { 11 | return; 12 | } 13 | 14 | function ReconnectingWebSocket(url, protocols, options) { 15 | // Default settings 16 | var settings = { 17 | /** Whether this instance should log debug messages. */ 18 | debug: false, 19 | 20 | /** Whether or not the websocket should attempt to connect immediately upon instantiation. */ 21 | automaticOpen: true, 22 | 23 | /** The number of milliseconds to delay before attempting to reconnect. */ 24 | reconnectInterval: 1000, 25 | /** The maximum number of milliseconds to delay a reconnection attempt. */ 26 | maxReconnectInterval: 30000, 27 | /** The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. */ 28 | reconnectDecay: 1.5, 29 | 30 | /** The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. */ 31 | timeoutInterval: 2000, 32 | 33 | /** The maximum number of reconnection attempts to make. Unlimited if null. */ 34 | maxReconnectAttempts: null, 35 | 36 | /** The binary type, possible values 'blob' or 'arraybuffer', default 'blob'. */ 37 | binaryType: "blob", 38 | }; 39 | if (!options) { 40 | options = {}; 41 | } 42 | 43 | // Overwrite and define settings with options if they exist. 44 | for (var key in settings) { 45 | if (typeof options[key] !== "undefined") { 46 | this[key] = options[key]; 47 | } else { 48 | this[key] = settings[key]; 49 | } 50 | } 51 | 52 | // These should be treated as read-only properties 53 | 54 | /** The URL as resolved by the constructor. This is always an absolute URL. Read only. */ 55 | this.url = url; 56 | 57 | /** The number of attempted reconnects since starting, or the last successful connection. Read only. */ 58 | this.reconnectAttempts = 0; 59 | 60 | /** 61 | * The current state of the connection. 62 | * Can be one of: WebSocket.CONNECTING, WebSocket.OPEN, WebSocket.CLOSING, WebSocket.CLOSED 63 | * Read only. 64 | */ 65 | this.readyState = WebSocket.CONNECTING; 66 | 67 | /** 68 | * A string indicating the name of the sub-protocol the server selected; this will be one of 69 | * the strings specified in the protocols parameter when creating the WebSocket object. 70 | * Read only. 71 | */ 72 | this.protocol = null; 73 | 74 | // Private state variables 75 | 76 | var self = this; 77 | var ws; 78 | var forcedClose = false; 79 | var timedOut = false; 80 | var eventTarget = document.createElement("div"); 81 | 82 | // Wire up "on*" properties as event handlers 83 | 84 | eventTarget.addEventListener("open", function (event) { 85 | self.onopen(event); 86 | }); 87 | eventTarget.addEventListener("close", function (event) { 88 | self.onclose(event); 89 | }); 90 | eventTarget.addEventListener("connecting", function (event) { 91 | self.onconnecting(event); 92 | }); 93 | eventTarget.addEventListener("message", function (event) { 94 | self.onmessage(event); 95 | }); 96 | eventTarget.addEventListener("error", function (event) { 97 | self.onerror(event); 98 | }); 99 | 100 | // Expose the API required by EventTarget 101 | 102 | this.addEventListener = eventTarget.addEventListener.bind(eventTarget); 103 | this.removeEventListener = 104 | eventTarget.removeEventListener.bind(eventTarget); 105 | this.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget); 106 | 107 | /** 108 | * This function generates an event that is compatible with standard 109 | * compliant browsers and IE9 - IE11 110 | * 111 | * This will prevent the error: 112 | * Object doesn't support this action 113 | * 114 | * http://stackoverflow.com/questions/19345392/why-arent-my-parameters-getting-passed-through-to-a-dispatched-event/19345563#19345563 115 | * @param s String The name that the event should use 116 | * @param args Object an optional object that the event will use 117 | */ 118 | function generateEvent(s, args) { 119 | var evt = document.createEvent("CustomEvent"); 120 | evt.initCustomEvent(s, false, false, args); 121 | return evt; 122 | } 123 | 124 | this.open = function (reconnectAttempt) { 125 | ws = new WebSocket(self.url, protocols || []); 126 | ws.binaryType = this.binaryType; 127 | 128 | if (reconnectAttempt) { 129 | if ( 130 | this.maxReconnectAttempts && 131 | this.reconnectAttempts > this.maxReconnectAttempts 132 | ) { 133 | return; 134 | } 135 | } else { 136 | eventTarget.dispatchEvent(generateEvent("connecting")); 137 | this.reconnectAttempts = 0; 138 | } 139 | 140 | if (self.debug || ReconnectingWebSocket.debugAll) { 141 | console.debug("ReconnectingWebSocket", "attempt-connect", self.url); 142 | } 143 | 144 | var localWs = ws; 145 | var timeout = setTimeout(function () { 146 | if (self.debug || ReconnectingWebSocket.debugAll) { 147 | console.debug( 148 | "ReconnectingWebSocket", 149 | "connection-timeout", 150 | self.url 151 | ); 152 | } 153 | timedOut = true; 154 | localWs.close(); 155 | timedOut = false; 156 | }, self.timeoutInterval); 157 | 158 | ws.onopen = function (event) { 159 | clearTimeout(timeout); 160 | if (self.debug || ReconnectingWebSocket.debugAll) { 161 | console.debug("ReconnectingWebSocket", "onopen", self.url); 162 | } 163 | self.protocol = ws.protocol; 164 | self.readyState = WebSocket.OPEN; 165 | self.reconnectAttempts = 0; 166 | var e = generateEvent("open"); 167 | e.isReconnect = reconnectAttempt; 168 | reconnectAttempt = false; 169 | eventTarget.dispatchEvent(e); 170 | }; 171 | 172 | ws.onclose = function (event) { 173 | clearTimeout(timeout); 174 | ws = null; 175 | if (forcedClose) { 176 | self.readyState = WebSocket.CLOSED; 177 | eventTarget.dispatchEvent(generateEvent("close")); 178 | } else { 179 | self.readyState = WebSocket.CONNECTING; 180 | var e = generateEvent("connecting"); 181 | e.code = event.code; 182 | e.reason = event.reason; 183 | e.wasClean = event.wasClean; 184 | eventTarget.dispatchEvent(e); 185 | if (!reconnectAttempt && !timedOut) { 186 | if (self.debug || ReconnectingWebSocket.debugAll) { 187 | console.debug("ReconnectingWebSocket", "onclose", self.url); 188 | } 189 | eventTarget.dispatchEvent(generateEvent("close")); 190 | } 191 | 192 | var timeout = 193 | self.reconnectInterval * 194 | Math.pow(self.reconnectDecay, self.reconnectAttempts); 195 | setTimeout( 196 | function () { 197 | self.reconnectAttempts++; 198 | self.open(true); 199 | }, 200 | timeout > self.maxReconnectInterval 201 | ? self.maxReconnectInterval 202 | : timeout 203 | ); 204 | } 205 | }; 206 | ws.onmessage = function (event) { 207 | if (self.debug || ReconnectingWebSocket.debugAll) { 208 | console.debug( 209 | "ReconnectingWebSocket", 210 | "onmessage", 211 | self.url, 212 | event.data 213 | ); 214 | } 215 | var e = generateEvent("message"); 216 | e.data = event.data; 217 | eventTarget.dispatchEvent(e); 218 | }; 219 | ws.onerror = function (event) { 220 | if (self.debug || ReconnectingWebSocket.debugAll) { 221 | console.debug("ReconnectingWebSocket", "onerror", self.url, event); 222 | } 223 | eventTarget.dispatchEvent(generateEvent("error")); 224 | }; 225 | }; 226 | 227 | // Whether or not to create a websocket upon instantiation 228 | if (this.automaticOpen == true) { 229 | this.open(false); 230 | } 231 | 232 | /** 233 | * Transmits data to the server over the WebSocket connection. 234 | * 235 | * @param data a text string, ArrayBuffer or Blob to send to the server. 236 | */ 237 | this.send = function (data) { 238 | if (ws) { 239 | if (self.debug || ReconnectingWebSocket.debugAll) { 240 | console.debug("ReconnectingWebSocket", "send", self.url, data); 241 | } 242 | return ws.send(data); 243 | } else { 244 | throw "INVALID_STATE_ERR : Pausing to reconnect websocket"; 245 | } 246 | }; 247 | 248 | /** 249 | * Closes the WebSocket connection or connection attempt, if any. 250 | * If the connection is already CLOSED, this method does nothing. 251 | */ 252 | this.close = function (code, reason) { 253 | // Default CLOSE_NORMAL code 254 | if (typeof code == "undefined") { 255 | code = 1000; 256 | } 257 | forcedClose = true; 258 | if (ws) { 259 | ws.close(code, reason); 260 | } 261 | }; 262 | 263 | /** 264 | * Additional public API method to refresh the connection if still open (close, re-open). 265 | * For example, if the app suspects bad data / missed heart beats, it can try to refresh. 266 | */ 267 | this.refresh = function () { 268 | if (ws) { 269 | ws.close(); 270 | } 271 | }; 272 | } 273 | 274 | /** 275 | * An event listener to be called when the WebSocket connection's readyState changes to OPEN; 276 | * this indicates that the connection is ready to send and receive data. 277 | */ 278 | ReconnectingWebSocket.prototype.onopen = function (event) {}; 279 | /** An event listener to be called when the WebSocket connection's readyState changes to CLOSED. */ 280 | ReconnectingWebSocket.prototype.onclose = function (event) {}; 281 | /** An event listener to be called when a connection begins being attempted. */ 282 | ReconnectingWebSocket.prototype.onconnecting = function (event) {}; 283 | /** An event listener to be called when a message is received from the server. */ 284 | ReconnectingWebSocket.prototype.onmessage = function (event) {}; 285 | /** An event listener to be called when an error occurs. */ 286 | ReconnectingWebSocket.prototype.onerror = function (event) {}; 287 | 288 | /** 289 | * Whether all instances of ReconnectingWebSocket should log debug messages. 290 | * Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true. 291 | */ 292 | ReconnectingWebSocket.debugAll = false; 293 | 294 | ReconnectingWebSocket.CONNECTING = WebSocket.CONNECTING; 295 | ReconnectingWebSocket.OPEN = WebSocket.OPEN; 296 | ReconnectingWebSocket.CLOSING = WebSocket.CLOSING; 297 | ReconnectingWebSocket.CLOSED = WebSocket.CLOSED; 298 | 299 | return ReconnectingWebSocket; 300 | }); 301 | -------------------------------------------------------------------------------- /templates/admin/app_index.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/index.html" %} 2 | {% load i18n %} 3 | 4 | {% block bodyclass %}{{ block.super }} app-{{ app_label }}{% endblock %} 5 | 6 | {% if not is_popup %} 7 | {% block breadcrumbs %} 8 |
9 |
10 |

{{app_label}}

11 |
12 |
13 | 19 |
20 |
21 | {% endblock %} 22 | {% endif %} 23 | 24 | 25 | -------------------------------------------------------------------------------- /templates/admin/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'adminlte/base.html' %} 2 | {% load static i18n %} 3 | 4 | {% block extra_head %} 5 | {{ block.super }} 6 | 7 | {% if LANGUAGE_BIDI %} 8 | 10 | {% endif %} 11 | 13 | {% block extrastyle %}{% endblock %} 14 | {% block extrahead %}{% endblock %} 15 | {% block blockbots %} 16 | {% endblock %} 17 | {% endblock %} 18 | 19 | {% block nav_header %} 20 | {% include 'admin/lib/_main_header.html' %} 21 | {% endblock %} 22 | 23 | {% block nav_sidebar %} 24 | {% include 'admin/lib/_main_sidebar.html' %} 25 | {% endblock %} 26 | 27 | {% block content_header %} 28 |
29 |
30 | {% block breadcrumbs %} 31 | 35 | {% endblock %} 36 |
37 |
38 | 39 | {% endblock %} 40 | 41 | {% block content_block_wrap %} 42 | 43 | {% block content %} 44 | {% block object-tools %}{% endblock %} 45 | {{ content }} 46 | {% block sidebar %}{% endblock %} 47 | {% endblock %} 48 | 49 | 50 | {% endblock %} -------------------------------------------------------------------------------- /templates/admin/base_login.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | {% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %} 3 | 4 | 5 | 6 | 7 | {% block title %}{% endblock %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 |