├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── chat ├── chat │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── chats │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── routing.py │ ├── urls.py │ └── views.py ├── core │ ├── __init__.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ └── widgets.py ├── groups │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_group_slug.py │ │ ├── 0003_alter_group_image.py │ │ └── __init__.py │ ├── models.py │ ├── routing.py │ ├── urls.py │ ├── validators.py │ └── views.py ├── homepage │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── urls.py │ └── views.py ├── info │ ├── __init__.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── urls.py │ └── views.py ├── manage.py ├── media │ └── uploads │ │ ├── groups_images │ │ ├── 8db47865498ca8604502ffbc03bfe6c8.jpg │ │ ├── Big_Floppa-2.jpg │ │ ├── Big_Floppa.jpg │ │ ├── avatar1.jpg │ │ ├── logo.png │ │ └── уровень-балдежа.jpg │ │ └── users_images │ │ ├── 1626191441_2-funart-pro-p-khomyak-v-skaipe-zhivotnie-krasivo-foto-2.jpg │ │ ├── Big_Floppa-2.jpg │ │ ├── Big_Floppa-2_7TLn3NN.jpg │ │ ├── Big_Floppa.jpg │ │ ├── Big_Floppa_ZElZuAl.jpg │ │ ├── Ittpostyourbestmostfunnyanimerelatedpicturesgifs_52440fe62b8a2ef805_5HPXhzP.jpg │ │ ├── bg.png │ │ ├── hamster-768x432.jpg │ │ ├── playlist.jpg │ │ └── уровень-балдежа.jpg ├── static_dev │ ├── fav │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-150x150.png │ │ ├── safari-pinned-tab.svg │ │ └── site.webmanifest │ ├── img │ │ ├── bg.png │ │ ├── grid.svg │ │ ├── logo.png │ │ ├── text_logo.svg │ │ └── user.png │ ├── js │ │ ├── auto_textarea_resizing.js │ │ ├── delete_group_confirmation_modal.js │ │ ├── disable_file_input_on_clear.js │ │ ├── menu.js │ │ ├── messages_confirmation_modal.js │ │ ├── page_reload_listener.js │ │ ├── search.js │ │ ├── search_bar.js │ │ └── sockets.js │ └── scss │ │ ├── _reset.scss │ │ ├── _variables.scss │ │ ├── style.css │ │ └── style.scss ├── templates │ ├── base.html │ ├── chats │ │ ├── chat.html │ │ ├── chats_list.html │ │ └── clear_chats_messages.html │ ├── groups │ │ ├── clear_group_messages.html │ │ ├── create_group.html │ │ ├── delete_group_messages.html │ │ ├── edit_group.html │ │ ├── group.html │ │ └── groups_list.html │ ├── homepage │ │ └── home.html │ ├── includes │ │ ├── checkbox_select.html │ │ ├── clear_chat_messages_confirm.html │ │ ├── clear_group_messages_confirm.html │ │ ├── clearable_file_input.html │ │ ├── confirmation_top.html │ │ ├── delete_group_confirm.html │ │ ├── form.html │ │ ├── homepage_content.html │ │ ├── sidebar.html │ │ └── websocket_data.html │ ├── info │ │ ├── about.html │ │ └── instruction.html │ ├── users │ │ ├── logged_out.html │ │ ├── login.html │ │ ├── password_change.html │ │ ├── password_change_done.html │ │ ├── password_reset.html │ │ ├── password_reset_complete.html │ │ ├── password_reset_confirm.html │ │ ├── password_reset_done.html │ │ ├── profile.html │ │ ├── signup.html │ │ ├── user_detail.html │ │ └── users_list.html │ └── users_channels │ │ └── users_channels.html ├── users │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── urls.py │ ├── validators.py │ └── views.py ├── users_channels │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_userschannel_slug.py │ │ └── __init__.py │ ├── models.py │ ├── routing.py │ ├── urls.py │ └── views.py ├── users_messages │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20220503_2338.py │ │ ├── 0003_auto_20220504_2136.py │ │ ├── 0004_auto_20220504_2256.py │ │ ├── 0005_auto_20220505_1848.py │ │ ├── 0006_auto_20220505_1900.py │ │ ├── 0007_auto_20220515_1205.py │ │ ├── 0008_chatmessage_dailychatmessages.py │ │ ├── 0009_rename_chatmessage_userchatmessage.py │ │ ├── 0010_alter_dailychatmessages_chat.py │ │ └── __init__.py │ └── models.py └── websockets │ ├── connection_types.py │ ├── consumers.py │ ├── errors.py │ ├── events.py │ ├── helpers.py │ ├── queries.py │ └── types.py ├── docs ├── ideas.md └── technical_requirements.md └── requirements.txt /.env.example: -------------------------------------------------------------------------------- 1 | DEBUG= 2 | SECRET_KEY= 3 | REDIS_HOST= 4 | REDIS_PORT= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .vscode 132 | .flake8 133 | 134 | *.css.map 135 | 136 | cache 137 | 138 | static -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sergey Yaksanov 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 | # Soft - django websocket chat 2 | 3 | # Установка и запуск 4 | 5 | Клонируйте репозиторий 6 | ```bash 7 | git clone https://github.com/Yakser/django-websocket-chat 8 | ``` 9 | Перед установкой зависимостей необходимо скачать [C++ Build Tools](https://stackoverflow.com/questions/40504552/how-to-install-visual-c-build-tools) Они нужны для сборки некоторых библиотек. 10 | 11 | # Установка зависимостей 12 | 13 | В папке проекта (`/django-websocket-chat/`) выполните команду: 14 | ``` 15 | pip install -r requirements.txt 16 | ``` 17 | 18 | # Redis 19 | 20 | ## Linux/Mac OS 21 | 22 | Скачайте [Redis](https://redis.io) и запустите его 23 | 24 | ## Windows 25 | - Установите [WSL2](https://docs.microsoft.com/ru-ru/windows/wsl/install) 26 | - В WSL установите `redis-server`: 27 | 28 | ```bash 29 | sudo apt-add-repository ppa:redislabs/redis 30 | sudo apt-get update 31 | sudo apt-get upgrade 32 | sudo apt-get install redis-server 33 | ``` 34 | 35 | Запустите `redis-sever`: 36 | ```bash 37 | sudo service redis-server start 38 | ``` 39 | 40 | # Запуск 41 | Сделайте и заполните файл `.env` в соответствии с `.env.example` 42 | 43 | Перейдите в директорию django-проекта: 44 | ```shell 45 | cd chat 46 | ``` 47 | 48 | Запустите приложение командой 49 | 50 | ```shell 51 | python manage.py runserver 52 | ``` 53 | 54 | # Технические детали 55 | - Python 3.10.4 56 | - Django 3.2.13 57 | - БД: SQLite 58 | - Библиотека для работы с вебсокетами: channels 3.0.4 59 | - CSS препроцессор: SCSS 60 | -------------------------------------------------------------------------------- /chat/chat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/chat/__init__.py -------------------------------------------------------------------------------- /chat/chat/asgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import groups.routing 4 | from channels.auth import AuthMiddlewareStack 5 | from channels.routing import ProtocolTypeRouter, URLRouter 6 | from django.core.asgi import get_asgi_application 7 | 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chat.settings") 9 | 10 | application = ProtocolTypeRouter({ 11 | "http": get_asgi_application(), 12 | "websocket": AuthMiddlewareStack( 13 | URLRouter( 14 | groups.routing.websocket_urlpatterns 15 | ) 16 | ), 17 | }) 18 | -------------------------------------------------------------------------------- /chat/chat/settings.py: -------------------------------------------------------------------------------- 1 | import mimetypes 2 | from pathlib import Path 3 | 4 | from decouple import config 5 | 6 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 7 | BASE_DIR = Path(__file__).resolve().parent.parent 8 | 9 | 10 | SECRET_KEY = config('SECRET_KEY', 11 | default="django-default-secret-key", 12 | cast=str) 13 | 14 | DEBUG = config('DEBUG', 15 | default=False, 16 | cast=bool) 17 | 18 | # Application definition 19 | 20 | INSTALLED_APPS = [ 21 | 'django.contrib.admin', 22 | 'django.contrib.auth', 23 | 'django.contrib.contenttypes', 24 | 'django.contrib.sessions', 25 | 'django.contrib.messages', 26 | 'django.contrib.staticfiles', 27 | 'django.forms', 28 | 'debug_toolbar', 29 | 'sass_processor', 30 | 'channels', 31 | 'core.apps.CoreConfig', 32 | 'groups.apps.GroupsConfig', 33 | 'homepage.apps.HomepageConfig', 34 | 'info.apps.InfoConfig', 35 | 'users.apps.UsersConfig', 36 | 'users_channels.apps.UsersChannelsConfig', 37 | 'users_messages.apps.UsersMessagesConfig', 38 | 'chats.apps.ChatsConfig', 39 | 'sorl.thumbnail', 40 | 'django_cleanup.apps.CleanupConfig', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | 'debug_toolbar.middleware.DebugToolbarMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'chat.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [BASE_DIR / 'templates'], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'chat.wsgi.application' 73 | 74 | ASGI_APPLICATION = "chat.asgi.application" 75 | 76 | REDIS_HOST = config('REDIS_HOST', 77 | default="127.0.0.1", 78 | cast=str) 79 | 80 | REDIS_PORT = config('REDIS_PORT', 81 | default=6379, 82 | cast=int) 83 | 84 | CHANNEL_LAYERS = { 85 | "default": { 86 | "BACKEND": "channels_redis.core.RedisChannelLayer", 87 | "CONFIG": { 88 | "hosts": [(REDIS_HOST, REDIS_PORT)], 89 | }, 90 | }, 91 | } 92 | 93 | 94 | # Database 95 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 96 | 97 | DATABASES = { 98 | 'default': { 99 | 'ENGINE': 'django.db.backends.sqlite3', 100 | 'NAME': BASE_DIR / 'db.sqlite3', 101 | } 102 | } 103 | 104 | 105 | # Password validation 106 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 107 | 108 | AUTH_PASSWORD_VALIDATORS = [ 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 114 | }, 115 | { 116 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 117 | }, 118 | { 119 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 120 | }, 121 | ] 122 | 123 | INTERNAL_IPS = [ 124 | '127.0.0.1', 125 | ] 126 | 127 | 128 | # Internationalization 129 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 130 | 131 | LANGUAGE_CODE = 'ru' 132 | 133 | TIME_ZONE = 'UTC' 134 | 135 | USE_I18N = True 136 | 137 | USE_L10N = True 138 | 139 | USE_TZ = True 140 | 141 | 142 | # Static files (CSS, JavaScript, Images) 143 | 144 | STATIC_ROOT = BASE_DIR / 'static' 145 | 146 | STATIC_URL = '/static_dev/' 147 | 148 | 149 | STATICFILES_DIRS = [ 150 | BASE_DIR / 'static_dev', 151 | ] 152 | 153 | STATICFILES_FINDERS = [ 154 | 'django.contrib.staticfiles.finders.FileSystemFinder', 155 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 156 | 'sass_processor.finders.CssFinder', 157 | ] 158 | 159 | SASS_PROCESSOR_ROOT = BASE_DIR / 'static_dev' 160 | 161 | # Default primary key field type 162 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 163 | 164 | 165 | # Set up debug-toolbar 166 | SHOW_TOOLBAR_CALLBACK = True 167 | mimetypes.add_type("application/javascript", ".js", True) 168 | 169 | LOGIN_URL = '/auth/login/' 170 | LOGIN_REDIRECT_URL = '/' 171 | LOGOUT_REDIRECT_URL = '/auth/login/' 172 | 173 | MEDIA_ROOT = BASE_DIR / 'media' 174 | MEDIA_URL = '/media/' 175 | 176 | FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' 177 | -------------------------------------------------------------------------------- /chat/chat/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.urls import include, path 5 | 6 | urlpatterns = [ 7 | path('admin/', admin.site.urls), 8 | path('groups/', include('groups.urls', namespace="groups")), 9 | path('chats/', include('chats.urls', namespace="chats")), 10 | path('channels/', include('users_channels.urls', namespace="channels")), 11 | path('auth/', include('users.urls', namespace="users")), 12 | path('info/', include('info.urls', namespace="info")), 13 | path('', include('homepage.urls', namespace="homepage")), 14 | ] 15 | 16 | if settings.DEBUG: 17 | import debug_toolbar 18 | 19 | urlpatterns += [path('__debug__/', include(debug_toolbar.urls))] 20 | urlpatterns += static(settings.MEDIA_URL, 21 | document_root=settings.MEDIA_ROOT) 22 | -------------------------------------------------------------------------------- /chat/chat/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for chat 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', 'chat.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /chat/chats/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/chats/__init__.py -------------------------------------------------------------------------------- /chat/chats/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from chats.models import Chat 4 | 5 | 6 | @admin.register(Chat) 7 | class ChatAdmin(admin.ModelAdmin): 8 | list_display = ('id', 'first_user', 'second_user', ) 9 | list_display_links = ('id', 'first_user', 'second_user', ) 10 | -------------------------------------------------------------------------------- /chat/chats/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ChatsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'chats' 7 | verbose_name = 'Чаты' 8 | -------------------------------------------------------------------------------- /chat/chats/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-15 08:05 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Chat', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('first_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='first_user_chats', to=settings.AUTH_USER_MODEL, verbose_name='Первый пользователь чата')), 22 | ('second_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='second_user_chats', to=settings.AUTH_USER_MODEL, verbose_name='Второй пользователь чата')), 23 | ], 24 | options={ 25 | 'verbose_name': 'Чат', 26 | 'verbose_name_plural': 'Чаты', 27 | }, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /chat/chats/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/chats/migrations/__init__.py -------------------------------------------------------------------------------- /chat/chats/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.db import models 3 | 4 | from django.db.models import Q 5 | from django.db.models.query import QuerySet 6 | 7 | 8 | User = get_user_model() 9 | 10 | 11 | class ChatManager(models.Manager): 12 | def get_chat_by_users(self, first_user: User, second_user: User) -> QuerySet: 13 | return Chat.objects.filter(Q(first_user=first_user) & Q(second_user=second_user) | 14 | Q(first_user=second_user) & Q(second_user=first_user)) 15 | 16 | 17 | class Chat(models.Model): 18 | """ 19 | Модель чат между двумя пользователями. 20 | 21 | Attributes: 22 | first_user (User): первый участник чата 23 | second_user (User): второй участник чата 24 | 25 | """ 26 | first_user = models.ForeignKey(User, 27 | verbose_name='Первый пользователь чата', 28 | related_name='first_user_chats', 29 | on_delete=models.CASCADE) 30 | second_user = models.ForeignKey(User, 31 | verbose_name='Второй пользователь чата', 32 | related_name='second_user_chats', 33 | on_delete=models.CASCADE) 34 | 35 | def check_if_user_is_member(self, user: User) -> bool: 36 | """ 37 | Проверяет, является ли пользователь участником чата 38 | 39 | Args: 40 | user (User): пользователь 41 | 42 | Returns: 43 | bool 44 | """ 45 | return self.first_user.id == user.id or self.second_user.id == user.id 46 | 47 | objects = ChatManager() 48 | 49 | class Meta: 50 | verbose_name = 'Чат' 51 | verbose_name_plural = 'Чаты' 52 | 53 | def __str__(self): 54 | return f"Chat<{self.id}>" 55 | -------------------------------------------------------------------------------- /chat/chats/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | from websockets.consumers import ChatsConsumer 3 | 4 | websocket_urlpatterns = [ 5 | re_path(r'ws/chat/(?P\w+)/$', 6 | ChatsConsumer.as_asgi()), 7 | ] 8 | -------------------------------------------------------------------------------- /chat/chats/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from chats.views import (ChatRedirectOrCreateView, ChatsListView, ChatView, 4 | ClearChatMessagesConfirmView, ClearChatMessagesView) 5 | 6 | app_name = 'chats' 7 | 8 | urlpatterns = [ 9 | path('', 10 | ChatsListView.as_view(), 11 | name='chats_list'), 12 | path('/', 13 | ChatView.as_view(), 14 | name='chat'), 15 | path('clear_messages_confirm//', 16 | ClearChatMessagesConfirmView.as_view(), 17 | name='clear_messages_confirm'), 18 | path('clear_messages//', 19 | ClearChatMessagesView.as_view(), 20 | name='clear_messages'), 21 | path('redirect_or_create/', 22 | ChatRedirectOrCreateView.as_view(), 23 | name='redirect_or_create'), 24 | ] 25 | -------------------------------------------------------------------------------- /chat/chats/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.contrib.auth.decorators import login_required 3 | from django.core.handlers.asgi import ASGIRequest 4 | from django.db.models.query import QuerySet 5 | from django.shortcuts import get_object_or_404, render 6 | from django.utils.decorators import method_decorator 7 | from django.views.generic.base import RedirectView, TemplateView 8 | from users_messages.models import DailyChatMessages 9 | from websockets.connection_types import CHATS_CONNECTION 10 | 11 | from chats.models import Chat 12 | 13 | User = get_user_model() 14 | 15 | 16 | @method_decorator(login_required, name='dispatch') 17 | class ChatView(TemplateView): 18 | """ 19 | Отображает модель Chat - список сообщений, панель редактирования и удаления чата, 20 | поле отправки сообщения 21 | 22 | Context: 23 | connection_type (str): тип подключения к вебсокетам 24 | is_member (bool): флаг, указывающий является ли пользователь участником чата 25 | user (User): экземпляр класса User 26 | chat (Chat): экземпляр класса Chat 27 | messages (UserChatMessage[]): сообщения чата 28 | 29 | Template: 30 | template_name: 'chats/chat.html' 31 | 32 | """ 33 | 34 | template_name = 'chats/chat.html' 35 | 36 | def get(self, request: ASGIRequest, chat_id: int, *args, **kwargs): 37 | return render(request, 38 | self.template_name, 39 | self.get_context_data(request.user, chat_id)) 40 | 41 | def get_context_data(self, user: User, chat_id: int, **kwargs): 42 | context = super().get_context_data(**kwargs) 43 | 44 | chat: Chat = get_object_or_404(Chat, pk=chat_id) 45 | 46 | context['connection_type'] = CHATS_CONNECTION 47 | context['chat'] = chat 48 | context['user'] = user 49 | context['is_member'] = False 50 | 51 | if chat.check_if_user_is_member(user): 52 | container, is_created = DailyChatMessages.objects.get_or_create(chat=chat) 53 | 54 | messages = container.get_messages() 55 | 56 | context['is_member'] = True 57 | context['chat'] = chat 58 | context['messages'] = messages 59 | 60 | return context 61 | 62 | 63 | class ChatsListView(TemplateView): 64 | """ 65 | Отображает список чатов (Chat) 66 | 67 | Context: 68 | chats (Chat[]): QuerySet содержащий экземпляры класса Chat 69 | 70 | Template: 71 | template_name: 'chats/chats_list.html' 72 | """ 73 | 74 | template_name = 'chats/chats_list.html' 75 | 76 | def get(self, request: ASGIRequest, *args, **kwargs): 77 | return render(request, 78 | self.template_name, 79 | self.get_context_data(request.user)) 80 | 81 | def get_context_data(self, user: User, **kwargs): 82 | context = super().get_context_data(**kwargs) 83 | 84 | # получаем чаты пользователя 85 | chats: QuerySet = user.first_user_chats.all() | user.second_user_chats.all() 86 | 87 | context['chats'] = chats 88 | return context 89 | 90 | 91 | class ChatRedirectOrCreateView(RedirectView): 92 | """ 93 | Перенаправляет пользователя на страницу чата. 94 | Если чата не существует, то он создается и пользователь также перенаправляется в чат. 95 | 96 | Pattern: 97 | pattern_name: 'chats:chat' 98 | 99 | """ 100 | permanent = False 101 | query_string = True 102 | pattern_name = 'chats:chat' 103 | 104 | def get_redirect_url(self, *args, **kwargs): 105 | first_username = self.request.user.username 106 | second_username = self.request.GET.get('username') 107 | 108 | first_user = get_object_or_404(User, username=first_username) 109 | second_user = get_object_or_404(User, username=second_username) 110 | 111 | chats: QuerySet = Chat.objects.get_chat_by_users(first_user, second_user) 112 | if chats: 113 | chat = chats.first() 114 | else: 115 | chat = Chat(first_user=first_user, second_user=second_user) 116 | chat.save() 117 | 118 | return super().get_redirect_url(chat_id=chat.id) 119 | 120 | 121 | @method_decorator(login_required, name='dispatch') 122 | class ClearChatMessagesConfirmView(TemplateView): 123 | """ 124 | Отображает модальное окно с подтверждением удаления истории сообщений 125 | 126 | Context: 127 | chat (Chat): экземпляр класса Chat 128 | 129 | Template: 130 | template_name: 'chats/clear_chat_messages_confirm.html' 131 | """ 132 | 133 | template_name = 'chats/clear_chat_messages_confirm.html' 134 | 135 | def get(self, request: ASGIRequest, chat_id: int, *args, **kwargs): 136 | return render(request, 137 | self.template_name, 138 | self.get_context_data(chat_id)) 139 | 140 | def get_context_data(self, chat_id: int, **kwargs): 141 | context = super().get_context_data(**kwargs) 142 | chat = get_object_or_404(Chat, pk=chat_id) 143 | context['chat'] = chat 144 | return context 145 | 146 | 147 | @method_decorator(login_required, name='dispatch') 148 | class ClearChatMessagesView(TemplateView): 149 | """ 150 | Отображает страницу с сообщением об успешном удалении истории сообщений чата 151 | 152 | Context: 153 | chat (Chat): экземпляр класса Chat 154 | 155 | Template: 156 | template_name: 'chats/clear_chats_messages.html' 157 | """ 158 | 159 | template_name = 'chats/clear_chats_messages.html' 160 | 161 | def get(self, request: ASGIRequest, chat_id: int, *args, **kwargs): 162 | chat: Chat = get_object_or_404(Chat, pk=chat_id) 163 | 164 | chat_container: DailyChatMessages = DailyChatMessages.objects.get(chat=chat) 165 | chat_container.chat_messages.all().delete() 166 | 167 | return render(request, 168 | self.template_name, 169 | self.get_context_data(chat_id)) 170 | 171 | def get_context_data(self, chat_id: int, **kwargs): 172 | context = super().get_context_data(**kwargs) 173 | chat = get_object_or_404(Chat, pk=chat_id) 174 | context['chat'] = chat 175 | return context 176 | -------------------------------------------------------------------------------- /chat/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/core/__init__.py -------------------------------------------------------------------------------- /chat/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'core' 7 | -------------------------------------------------------------------------------- /chat/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/core/migrations/__init__.py -------------------------------------------------------------------------------- /chat/core/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.db import models 3 | from django.utils.html import mark_safe 4 | from sorl.thumbnail import get_thumbnail 5 | 6 | User = get_user_model() 7 | 8 | 9 | class BaseUserMessage(models.Model): 10 | """ 11 | Абстрактная модель сообщения пользователя 12 | 13 | Attributes: 14 | text (TextField): текст сообщения 15 | time (TimeField): время отправки сообщения 16 | 17 | """ 18 | 19 | text = models.TextField(max_length=1024, 20 | verbose_name='Текст') 21 | time = models.TimeField(verbose_name='Время отправки', 22 | auto_now_add=True, 23 | null=True) 24 | 25 | class Meta: 26 | abstract = True 27 | 28 | def __str__(self): 29 | return f"Message<{self.id}>" 30 | 31 | 32 | class BaseWebsocketGroup(models.Model): 33 | """ 34 | Абстрактная модель websocket группы, которая создает свой channel layer 35 | 36 | Attributes: 37 | slug (SlugField): уникальный идентификатор 38 | name (CharField): имя 39 | image (ImageField): изображение 40 | owner (User): владелец 41 | group_members (User[]): участники 42 | 43 | """ 44 | 45 | slug = models.SlugField(verbose_name='Идентификатор', 46 | help_text='Используйте буквы, цифры или @/./+/-/_ ', 47 | max_length=100, 48 | unique=True, 49 | primary_key=True) 50 | 51 | name = models.CharField(verbose_name='Название', 52 | max_length=100, 53 | help_text='Введите название, длина до 100 символов', 54 | default='Default group name') 55 | 56 | image = models.ImageField(verbose_name='Изображение группы', 57 | upload_to='uploads/groups_images', 58 | null=True, 59 | blank=True, 60 | help_text='Выберите изображение группы') 61 | 62 | owner = models.ForeignKey(User, 63 | verbose_name='Владелец', 64 | related_name='owner_groups', 65 | on_delete=models.SET_NULL, 66 | null=True) 67 | 68 | group_members = models.ManyToManyField(User, 69 | verbose_name='Участники', 70 | related_name='users_groups') 71 | 72 | def get_image_x256(self): 73 | return get_thumbnail(self.image, 74 | '256', 75 | quality=51) 76 | 77 | def image_tmb(self): 78 | if self.image: 79 | return mark_safe(f'" 90 | 91 | 92 | class BaseDailyMessages(models.Model): 93 | """ 94 | Абстрактная модель контейнера сообщений 95 | 96 | Attributes: 97 | date (DateField): дата создания контейнера 98 | 99 | """ 100 | # TODO task с созданием нового контейнера раз в сутки с помощью django-rq 101 | 102 | date = models.DateField(verbose_name='Дата создания', 103 | auto_now_add=True, 104 | null=True) 105 | 106 | class Meta: 107 | abstract = True 108 | 109 | def __str__(self): 110 | return f"DailyMessages<{self.date.strftime('%Y-%m-%d %H:%M')}>" 111 | -------------------------------------------------------------------------------- /chat/core/widgets.py: -------------------------------------------------------------------------------- 1 | from django.forms.widgets import ClearableFileInput, CheckboxSelectMultiple 2 | 3 | 4 | class CustomClearableFileInput(ClearableFileInput): 5 | template_name = 'includes/clearable_file_input.html' 6 | 7 | 8 | class CustomCheckboxSelectMultiple(CheckboxSelectMultiple): 9 | template_name = 'includes/checkbox_select.html' 10 | -------------------------------------------------------------------------------- /chat/groups/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/groups/__init__.py -------------------------------------------------------------------------------- /chat/groups/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from groups.models import Group 4 | 5 | 6 | @admin.register(Group) 7 | class GroupAdmin(admin.ModelAdmin): 8 | list_display = ('name', 'image_tmb', ) 9 | -------------------------------------------------------------------------------- /chat/groups/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class GroupsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'groups' 7 | verbose_name = 'Группы' 8 | -------------------------------------------------------------------------------- /chat/groups/forms.py: -------------------------------------------------------------------------------- 1 | from core.widgets import CustomCheckboxSelectMultiple, CustomClearableFileInput 2 | from django import forms 3 | from django.contrib.auth import get_user_model 4 | from django.db.models import Q 5 | 6 | from groups.validators import validate_slug 7 | 8 | User = get_user_model() 9 | 10 | 11 | class CreateGroupForm(forms.Form): 12 | """ 13 | Форма создания группы 14 | 15 | Fields: 16 | slug (SlugField): идентификатор 17 | name (CharField): имя 18 | group_members(ModelMultipleChoiceField): участники 19 | 20 | """ 21 | 22 | def __init__(self, *args, **kwargs): 23 | self.user: User = kwargs.pop('user') 24 | super(CreateGroupForm, self).__init__(*args, **kwargs) 25 | self.fields['group_members'].queryset = \ 26 | User.objects.filter(~Q(id=self.user.id)).only('id', 'username') 27 | 28 | slug = forms.SlugField(label='Идентификатор', 29 | help_text='Используйте буквы, цифры или @/./+/-/_ ', 30 | max_length=100, 31 | validators=[validate_slug]) 32 | 33 | name = forms.CharField(max_length=100, 34 | label='Имя группы', 35 | help_text='Максимум 100 символов', 36 | required=True) 37 | 38 | group_members = forms.ModelMultipleChoiceField(label='Участники', 39 | queryset=User.objects.all(), 40 | widget=CustomCheckboxSelectMultiple(), 41 | required=False) 42 | 43 | 44 | class EditGroupForm(forms.Form): 45 | """ 46 | Форма редактирования группы 47 | 48 | Fields: 49 | name (CharField): имя 50 | image (ImageField): изображение 51 | group_members(ModelMultipleChoiceField): участники 52 | 53 | """ 54 | 55 | def __init__(self, *args, **kwargs): 56 | self.user: User = kwargs.pop('user') 57 | super(EditGroupForm, self).__init__(*args, **kwargs) 58 | self.fields['group_members'].queryset = \ 59 | User.objects.filter(~Q(id=self.user.id)).only('id', 'username') 60 | 61 | name = forms.CharField(max_length=100, 62 | label='Имя группы', 63 | help_text='Максимум 100 символов', 64 | required=True) 65 | 66 | image = forms.ImageField(label='Аватар группы', 67 | required=False, 68 | allow_empty_file=False, 69 | widget=CustomClearableFileInput) 70 | 71 | group_members = forms.ModelMultipleChoiceField(label='Участники', 72 | queryset=User.objects.all(), 73 | widget=CustomCheckboxSelectMultiple(), 74 | required=False) 75 | -------------------------------------------------------------------------------- /chat/groups/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-03 10:47 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Group', 19 | fields=[ 20 | ('slug', models.SlugField(default='default_group', help_text='Используйте буквы, цифры или @/./+/-/_ ', max_length=100, primary_key=True, serialize=False, unique=True, verbose_name='Идентификатор')), 21 | ('image', models.ImageField(help_text='Выберите изображение группы', null=True, upload_to='uploads/groups_images', verbose_name='Изображение группы')), 22 | ('name', models.CharField(default='Default group name', help_text='Введите название, длина до 100 символов', max_length=100, verbose_name='Название')), 23 | ('group_members', models.ManyToManyField(related_name='users_groups', to=settings.AUTH_USER_MODEL, verbose_name='Участники')), 24 | ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner_groups', to=settings.AUTH_USER_MODEL, verbose_name='Владелец')), 25 | ], 26 | options={ 27 | 'verbose_name': 'Группа', 28 | 'verbose_name_plural': 'Группы', 29 | }, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /chat/groups/migrations/0002_alter_group_slug.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-03 19:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('groups', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='group', 15 | name='slug', 16 | field=models.SlugField(help_text='Используйте буквы, цифры или @/./+/-/_ ', max_length=100, primary_key=True, serialize=False, unique=True, verbose_name='Идентификатор'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /chat/groups/migrations/0003_alter_group_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-05 14:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('groups', '0002_alter_group_slug'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='group', 15 | name='image', 16 | field=models.ImageField(blank=True, help_text='Выберите изображение группы', null=True, upload_to='uploads/groups_images', verbose_name='Изображение группы'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /chat/groups/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/groups/migrations/__init__.py -------------------------------------------------------------------------------- /chat/groups/models.py: -------------------------------------------------------------------------------- 1 | from core.models import BaseWebsocketGroup 2 | 3 | 4 | class Group(BaseWebsocketGroup): 5 | """ 6 | Модель Группы. Наследуется от абстрактной модели BaseWebsocketGroup 7 | """ 8 | class Meta: 9 | verbose_name = 'Группа' 10 | verbose_name_plural = 'Группы' 11 | -------------------------------------------------------------------------------- /chat/groups/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | from websockets.consumers import GroupsConsumer 3 | 4 | websocket_urlpatterns = [ 5 | re_path(r'ws/chat/(?P\w+)/$', 6 | GroupsConsumer.as_asgi()), 7 | ] 8 | -------------------------------------------------------------------------------- /chat/groups/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from groups.views import (ClearGroupMessagesConfirmView, 4 | ClearGroupMessagesView, CreateGroupView, 5 | DeleteGroupView, EditGroupView, GroupsListView, 6 | GroupView) 7 | 8 | app_name = 'groups' 9 | 10 | urlpatterns = [ 11 | path('to//', 12 | GroupView.as_view(), 13 | name='group'), 14 | path('create/', 15 | CreateGroupView.as_view(), 16 | name='create'), 17 | path('edit//', 18 | EditGroupView.as_view(), 19 | name='edit'), 20 | path('delete//', 21 | DeleteGroupView.as_view(), 22 | name='delete_group'), 23 | path('clear_messages_confirm//', 24 | ClearGroupMessagesConfirmView.as_view(), 25 | name='clear_messages_confirm'), 26 | path('clear_messages//', 27 | ClearGroupMessagesView.as_view(), 28 | name='clear_messages'), 29 | path('', 30 | GroupsListView.as_view(), 31 | name='groups_list'), 32 | ] 33 | -------------------------------------------------------------------------------- /chat/groups/validators.py: -------------------------------------------------------------------------------- 1 | from django.forms import ValidationError 2 | 3 | from groups.models import Group 4 | 5 | 6 | def validate_slug(value: str): 7 | if len(value.strip()) < 2: 8 | raise ValidationError('Длина не менее 2 символов!') 9 | if Group.objects.filter(slug=value): 10 | raise ValidationError('Группа с таким идентификатором уже существует!') 11 | -------------------------------------------------------------------------------- /chat/groups/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.contrib.auth.decorators import login_required 3 | from django.core.handlers.asgi import ASGIRequest 4 | from django.db.models import Prefetch 5 | from django.db.models.query import QuerySet 6 | from django.shortcuts import get_object_or_404, redirect, render 7 | from django.utils.decorators import method_decorator 8 | from django.views.generic.base import TemplateView 9 | from users_messages.models import DailyGroupMessages 10 | from websockets.connection_types import GROUPS_CONNECTION 11 | 12 | from groups.forms import CreateGroupForm, EditGroupForm 13 | from groups.models import Group 14 | 15 | User = get_user_model() 16 | 17 | 18 | @method_decorator(login_required, name='dispatch') 19 | class GroupView(TemplateView): 20 | """ 21 | Отображает модель Group - список сообщений, панель редактирования и удаления группы, 22 | поле отправки сообщения 23 | 24 | Context: 25 | connection_type (str): тип подключения к вебсокетам 26 | is_member (bool): флаг, указывающий является ли пользователь участником группы 27 | user (User): экземпляр класса User 28 | group (Group): экземпляр класса Group 29 | messages (UserGroupMessage[]): сообщения группы 30 | members_count (int): количество участников групы 31 | 32 | Template: 33 | template_name: 'groups/group.html' 34 | 35 | """ 36 | 37 | template_name = 'groups/group.html' 38 | 39 | def get(self, request: ASGIRequest, group_slug: str, *args, **kwargs): 40 | return render(request, 41 | self.template_name, 42 | self.get_context_data(request.user, group_slug)) 43 | 44 | def get_context_data(self, user: User, group_slug: str, **kwargs): 45 | context = super().get_context_data(**kwargs) 46 | group: Group = get_object_or_404(Group, slug=group_slug) 47 | 48 | context['connection_type'] = GROUPS_CONNECTION 49 | context['user'] = user 50 | context['members_count'] = group.group_members.count() 51 | context['is_member'] = False 52 | 53 | if self._check_if_user_is_member(group, user): 54 | container, created = DailyGroupMessages.objects.get_or_create(group=group) 55 | 56 | prefetch_users = Prefetch('user', queryset=User.objects.only('username')) 57 | messages = container.group_messages.prefetch_related(prefetch_users) 58 | 59 | context['is_member'] = True 60 | context['group'] = group 61 | context['messages'] = messages 62 | 63 | return context 64 | 65 | def _check_if_user_is_member(self, group: Group, user: User) -> bool: 66 | return group.group_members.filter(id=user.id) 67 | 68 | 69 | @method_decorator(login_required, name='dispatch') 70 | class CreateGroupView(TemplateView): 71 | """ 72 | Отображает форму создания группы 73 | 74 | Context: 75 | user (User): экземпляр класса User 76 | form (CreateGroupForm): форма создания группы 77 | 78 | Template: 79 | template_name: 'groups/create_group.html' 80 | 81 | Form: 82 | form_class: CreateGroupForm 83 | 84 | """ 85 | 86 | template_name = 'groups/create_group.html' 87 | form_class = CreateGroupForm 88 | 89 | def get(self, request: ASGIRequest, *args, **kwargs): 90 | return render(request, 91 | self.template_name, 92 | self.get_context_data(request)) 93 | 94 | def post(self, request: ASGIRequest, *args, **kwargs): 95 | user: User = get_object_or_404(User.objects.all(), 96 | pk=request.user.id) 97 | 98 | form = self.form_class(request.POST, user=user) 99 | 100 | if form.is_valid(): 101 | members: QuerySet = form.cleaned_data['group_members'] 102 | 103 | group: Group = Group(slug=form.cleaned_data['slug'], 104 | name=form.cleaned_data['name'], 105 | owner=user) 106 | group.save() 107 | group.group_members.set(members) 108 | group.group_members.add(user) 109 | group.save() 110 | 111 | return redirect('groups:group', form.cleaned_data['slug']) 112 | 113 | return self.get(request) 114 | 115 | def get_context_data(self, request: ASGIRequest, **kwargs): 116 | context = super().get_context_data(**kwargs) 117 | 118 | user: User = get_object_or_404(User.objects.only('email', 'username'), 119 | pk=request.user.id) 120 | 121 | form = self.form_class(request.POST or None, user=user) 122 | form.is_valid() 123 | 124 | context['user'] = user 125 | context['form'] = form 126 | 127 | return context 128 | 129 | 130 | @method_decorator(login_required, name='dispatch') 131 | class GroupsListView(TemplateView): 132 | """ 133 | Отображает список групп (Group) пользователя 134 | 135 | Context: 136 | groups (Group[]): QuerySet содержащий экземпляры класса Group 137 | 138 | Template: 139 | template_name: 'groups/groups_list.html' 140 | 141 | """ 142 | 143 | template_name = 'groups/groups_list.html' 144 | 145 | def get(self, request: ASGIRequest, *args, **kwargs): 146 | return render(request, 147 | self.template_name, 148 | self.get_context_data(request.user)) 149 | 150 | def get_context_data(self, user: User, **kwargs): 151 | context = super().get_context_data(**kwargs) 152 | groups: QuerySet = user.users_groups.all() 153 | context['groups'] = groups 154 | return context 155 | 156 | 157 | @method_decorator(login_required, name='dispatch') 158 | class ClearGroupMessagesConfirmView(TemplateView): 159 | """ 160 | Отображает модальное окно с подтверждением удаления истории сообщений группы 161 | 162 | Context: 163 | group (Group): экземпляр класса Group 164 | 165 | Template: 166 | template_name: 'groups/clear_group_messages_confirm.html' 167 | 168 | """ 169 | 170 | template_name = 'groups/clear_group_messages_confirm.html' 171 | 172 | def get(self, request: ASGIRequest, group_slug: str, *args, **kwargs): 173 | return render(request, 174 | self.template_name, 175 | self.get_context_data(group_slug)) 176 | 177 | def get_context_data(self, group_slug: str, **kwargs): 178 | context = super().get_context_data(**kwargs) 179 | group: Group = get_object_or_404(Group, pk=group_slug) 180 | context['group'] = group 181 | return context 182 | 183 | 184 | @method_decorator(login_required, name='dispatch') 185 | class ClearGroupMessagesView(TemplateView): 186 | """ 187 | Отображает страницу с сообщением об успешном удалении истории сообщений группы 188 | 189 | Context: 190 | group (Group): экземпляр класса Group 191 | 192 | Template: 193 | template_name: 'groups/clear_group_messages.html' 194 | 195 | """ 196 | 197 | template_name = 'groups/clear_group_messages.html' 198 | 199 | def get(self, request: ASGIRequest, group_slug: str, *args, **kwargs): 200 | group: Group = get_object_or_404(Group, pk=group_slug) 201 | group_container: DailyGroupMessages = DailyGroupMessages.objects.get(group=group) 202 | group_container.group_messages.all().delete() 203 | 204 | return render(request, 205 | self.template_name, 206 | self.get_context_data(group_slug)) 207 | 208 | def get_context_data(self, group_slug: str, **kwargs): 209 | context = super().get_context_data(**kwargs) 210 | group: Group = get_object_or_404(Group, pk=group_slug) 211 | context['group'] = group 212 | return context 213 | 214 | 215 | @method_decorator(login_required, name='dispatch') 216 | class DeleteGroupView(TemplateView): 217 | template_name = 'groups/delete_group_messages.html' 218 | 219 | def get(self, request: ASGIRequest, group_slug: str, *args, **kwargs): 220 | group: Group = get_object_or_404(Group, pk=group_slug) 221 | group_container: DailyGroupMessages = DailyGroupMessages.objects.get(group=group) 222 | group_container.group_messages.all().delete() 223 | group.delete() 224 | 225 | return render(request, 226 | self.template_name, 227 | self.get_context_data()) 228 | 229 | def get_context_data(self, **kwargs): 230 | context = super().get_context_data(**kwargs) 231 | return context 232 | 233 | 234 | @method_decorator(login_required, name='dispatch') 235 | class EditGroupView(TemplateView): 236 | """ 237 | Отображает страницу с сообщением об успешном удалении истории сообщений группы 238 | 239 | Context: 240 | group (Group): экземпляр класса Group 241 | form (EditGroupForm): форма редактирования группы 242 | 243 | Template: 244 | template_name: 'groups/edit_group.html' 245 | 246 | Form: 247 | form_class (EditGroupForm): форма редактирования группы 248 | 249 | """ 250 | 251 | template_name = 'groups/edit_group.html' 252 | form_class = EditGroupForm 253 | 254 | def get(self, request: ASGIRequest, group_slug: str, *args, **kwargs): 255 | return render(request, 256 | self.template_name, 257 | self.get_context_data(request, group_slug)) 258 | 259 | def post(self, request, group_slug: str, *args, **kwargs): 260 | group: Group = get_object_or_404(Group.objects.all(), 261 | pk=group_slug) 262 | 263 | user: User = get_object_or_404(User.objects.only('id'), 264 | pk=request.user.id) 265 | 266 | form = self.form_class(request.POST, 267 | request.FILES, 268 | user=user) 269 | 270 | if form.is_valid() and group.owner == user: 271 | group.name = form.cleaned_data['name'] 272 | 273 | members: QuerySet = form.cleaned_data['group_members'] 274 | 275 | if members: 276 | group.group_members.set(members) 277 | group.group_members.add(user) 278 | 279 | group.save() 280 | 281 | # если нажата кнопка удаления изображения 282 | if form.cleaned_data['image'] is False: 283 | group.image = None 284 | else: 285 | group.image = form.cleaned_data['image'] or group.image 286 | 287 | group.save() 288 | 289 | return redirect('groups:group', group.slug) 290 | 291 | return render(request, 292 | self.template_name, 293 | self.get_context_data(request, group_slug)) 294 | 295 | def get_context_data(self, request: ASGIRequest, group_slug: str, **kwargs): 296 | context = super().get_context_data(**kwargs) 297 | 298 | group: Group = get_object_or_404(Group.objects.all(), 299 | pk=group_slug) 300 | user: User = get_object_or_404(User.objects.only('id'), 301 | pk=request.user.id) 302 | 303 | initial_form_data = { 304 | 'name': group.name, 305 | 'group_members': group.group_members.all, 306 | 'image': group.image, 307 | } 308 | 309 | form = self.form_class(request.POST or None, 310 | request.FILES or None, 311 | user=user, 312 | initial=initial_form_data) 313 | 314 | # небоходимо, чтобы показать ошибки валидации формы 315 | if form.is_valid(): 316 | form.validate_all(group) 317 | 318 | context['group'] = group 319 | context['form'] = form 320 | context['edit'] = True 321 | 322 | return context 323 | -------------------------------------------------------------------------------- /chat/homepage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/homepage/__init__.py -------------------------------------------------------------------------------- /chat/homepage/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /chat/homepage/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class HomepageConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'homepage' 7 | -------------------------------------------------------------------------------- /chat/homepage/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/homepage/migrations/__init__.py -------------------------------------------------------------------------------- /chat/homepage/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /chat/homepage/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from homepage.views import HomeView 4 | 5 | app_name = 'homepage' 6 | 7 | urlpatterns = [ 8 | path('', HomeView.as_view(), name='home'), 9 | ] 10 | -------------------------------------------------------------------------------- /chat/homepage/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic.base import TemplateView 2 | 3 | 4 | class HomeView(TemplateView): 5 | """ 6 | Отображает главную страницу 7 | 8 | Template: 9 | template_name: 'homepage/home.html' 10 | 11 | """ 12 | template_name = 'homepage/home.html' 13 | 14 | def get_context_data(self, **kwargs): 15 | context = super().get_context_data(**kwargs) 16 | return context 17 | -------------------------------------------------------------------------------- /chat/info/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/info/__init__.py -------------------------------------------------------------------------------- /chat/info/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class InfoConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'info' 7 | verbose_name = 'Информация о проекте' 8 | -------------------------------------------------------------------------------- /chat/info/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/info/migrations/__init__.py -------------------------------------------------------------------------------- /chat/info/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from info.views import AboutView, InstructionView 4 | 5 | app_name = 'info' 6 | 7 | urlpatterns = [ 8 | path('about', 9 | AboutView.as_view(), 10 | name='about'), 11 | path('instruction', 12 | InstructionView.as_view(), 13 | name='instruction'), 14 | ] 15 | -------------------------------------------------------------------------------- /chat/info/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic.base import TemplateView 2 | 3 | 4 | class AboutView(TemplateView): 5 | """ 6 | Отображает страницу информации о проекте 7 | 8 | Template: 9 | template_name: 'info/about.html' 10 | 11 | """ 12 | template_name = 'info/about.html' 13 | 14 | def get_context_data(self, **kwargs): 15 | context = super().get_context_data(**kwargs) 16 | return context 17 | 18 | 19 | class InstructionView(TemplateView): 20 | """ 21 | Отображает страницу с инструкцией по использованию 22 | 23 | Template: 24 | template_name: 'info/instruction.html' 25 | 26 | """ 27 | 28 | template_name = 'info/instruction.html' 29 | 30 | def get_context_data(self, **kwargs): 31 | context = super().get_context_data(**kwargs) 32 | return context -------------------------------------------------------------------------------- /chat/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chat.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /chat/media/uploads/groups_images/8db47865498ca8604502ffbc03bfe6c8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/groups_images/8db47865498ca8604502ffbc03bfe6c8.jpg -------------------------------------------------------------------------------- /chat/media/uploads/groups_images/Big_Floppa-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/groups_images/Big_Floppa-2.jpg -------------------------------------------------------------------------------- /chat/media/uploads/groups_images/Big_Floppa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/groups_images/Big_Floppa.jpg -------------------------------------------------------------------------------- /chat/media/uploads/groups_images/avatar1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/groups_images/avatar1.jpg -------------------------------------------------------------------------------- /chat/media/uploads/groups_images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/groups_images/logo.png -------------------------------------------------------------------------------- /chat/media/uploads/groups_images/уровень-балдежа.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/groups_images/уровень-балдежа.jpg -------------------------------------------------------------------------------- /chat/media/uploads/users_images/1626191441_2-funart-pro-p-khomyak-v-skaipe-zhivotnie-krasivo-foto-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/users_images/1626191441_2-funart-pro-p-khomyak-v-skaipe-zhivotnie-krasivo-foto-2.jpg -------------------------------------------------------------------------------- /chat/media/uploads/users_images/Big_Floppa-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/users_images/Big_Floppa-2.jpg -------------------------------------------------------------------------------- /chat/media/uploads/users_images/Big_Floppa-2_7TLn3NN.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/users_images/Big_Floppa-2_7TLn3NN.jpg -------------------------------------------------------------------------------- /chat/media/uploads/users_images/Big_Floppa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/users_images/Big_Floppa.jpg -------------------------------------------------------------------------------- /chat/media/uploads/users_images/Big_Floppa_ZElZuAl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/users_images/Big_Floppa_ZElZuAl.jpg -------------------------------------------------------------------------------- /chat/media/uploads/users_images/Ittpostyourbestmostfunnyanimerelatedpicturesgifs_52440fe62b8a2ef805_5HPXhzP.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/users_images/Ittpostyourbestmostfunnyanimerelatedpicturesgifs_52440fe62b8a2ef805_5HPXhzP.jpg -------------------------------------------------------------------------------- /chat/media/uploads/users_images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/users_images/bg.png -------------------------------------------------------------------------------- /chat/media/uploads/users_images/hamster-768x432.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/users_images/hamster-768x432.jpg -------------------------------------------------------------------------------- /chat/media/uploads/users_images/playlist.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/users_images/playlist.jpg -------------------------------------------------------------------------------- /chat/media/uploads/users_images/уровень-балдежа.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/media/uploads/users_images/уровень-балдежа.jpg -------------------------------------------------------------------------------- /chat/static_dev/fav/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/static_dev/fav/android-chrome-192x192.png -------------------------------------------------------------------------------- /chat/static_dev/fav/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/static_dev/fav/android-chrome-512x512.png -------------------------------------------------------------------------------- /chat/static_dev/fav/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/static_dev/fav/apple-touch-icon.png -------------------------------------------------------------------------------- /chat/static_dev/fav/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /chat/static_dev/fav/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/static_dev/fav/favicon-16x16.png -------------------------------------------------------------------------------- /chat/static_dev/fav/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/static_dev/fav/favicon-32x32.png -------------------------------------------------------------------------------- /chat/static_dev/fav/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/static_dev/fav/favicon.ico -------------------------------------------------------------------------------- /chat/static_dev/fav/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/static_dev/fav/mstile-150x150.png -------------------------------------------------------------------------------- /chat/static_dev/fav/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /chat/static_dev/fav/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/fav/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/fav/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /chat/static_dev/img/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/static_dev/img/bg.png -------------------------------------------------------------------------------- /chat/static_dev/img/grid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chat/static_dev/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/static_dev/img/logo.png -------------------------------------------------------------------------------- /chat/static_dev/img/text_logo.svg: -------------------------------------------------------------------------------- 1 | text_logo -------------------------------------------------------------------------------- /chat/static_dev/img/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/static_dev/img/user.png -------------------------------------------------------------------------------- /chat/static_dev/js/auto_textarea_resizing.js: -------------------------------------------------------------------------------- 1 | const autoTextareaResizing = () => { 2 | const textarea = document.getElementById('chat-message-input'); 3 | const borderWidth = textarea.style.borderWidth || 1; 4 | 5 | textarea.style.height = textarea.scrollHeight + 2 * borderWidth + 'px'; 6 | textarea.style.overflowY = 'hidden'; 7 | 8 | textarea.addEventListener('input', (event) => { 9 | textarea.style.height = 'auto'; 10 | textarea.style.height = textarea.scrollHeight + 2 * borderWidth + 'px'; 11 | }) 12 | } 13 | 14 | autoTextareaResizing(); -------------------------------------------------------------------------------- /chat/static_dev/js/delete_group_confirmation_modal.js: -------------------------------------------------------------------------------- 1 | const confirmationModal = () => { 2 | const confirmation = document.querySelector('.confirmation'); 3 | 4 | document.querySelector('.delete-btn').addEventListener("click", function () { 5 | confirmation.style.display = 'block'; 6 | }); 7 | 8 | document.querySelector('.close-btn').addEventListener("click", function () { 9 | confirmation.style.display = 'none'; 10 | }); 11 | 12 | document.querySelector('.close-confirm').addEventListener("click", function () { 13 | confirmation.style.display = 'none'; 14 | }); 15 | } 16 | 17 | confirmationModal(); -------------------------------------------------------------------------------- /chat/static_dev/js/disable_file_input_on_clear.js: -------------------------------------------------------------------------------- 1 | const disableFileInputOnClear = () => { 2 | const clearCheckbox = document.getElementById('image-clear_id'); 3 | const imageInput = document.getElementById('id_image'); 4 | 5 | clearCheckbox.addEventListener('click', () => { 6 | console.log(clearCheckbox.checked) 7 | if (clearCheckbox.checked) { 8 | imageInput.disabled = true; 9 | } else { 10 | imageInput.disabled = false; 11 | } 12 | }) 13 | } 14 | 15 | disableFileInputOnClear(); -------------------------------------------------------------------------------- /chat/static_dev/js/menu.js: -------------------------------------------------------------------------------- 1 | const Menu = () => { 2 | const nav = document.getElementById("js-menu-nav"); 3 | nav.style.height = `${ nav.scrollHeight }px`; 4 | window.getComputedStyle(nav, null).getPropertyValue("height"); 5 | nav.style.height = "0px"; 6 | 7 | nav.addEventListener("transitionend", () => { 8 | if (nav.style.height != "0px") { 9 | nav.style.height = "auto" 10 | } 11 | }); 12 | 13 | const menuBtn = document.getElementById('js-menu-btn') 14 | menuBtn.addEventListener('click', () => { 15 | nav.classList.toggle('active'); 16 | menuBtn.classList.toggle('active'); 17 | if (nav.classList.contains('active')) { 18 | nav.style.height = `${ nav.scrollHeight }px`; 19 | nav.style.marginBlockStart = '20px'; 20 | nav.style.opacity = '1'; 21 | 22 | } else { 23 | nav.style.height = `${ nav.scrollHeight }px`; 24 | window.getComputedStyle(nav, null).getPropertyValue("height"); 25 | nav.style.height = "0px"; 26 | nav.style.marginBlockStart = '0px'; 27 | nav.style.opacity = '0'; 28 | chats.style.height = 'auto'; 29 | } 30 | }); 31 | } 32 | 33 | Menu(); -------------------------------------------------------------------------------- /chat/static_dev/js/messages_confirmation_modal.js: -------------------------------------------------------------------------------- 1 | const confirmationModal = () => { 2 | const confirmation = document.querySelector('.confirmation'); 3 | 4 | document.querySelector('.chat__clear-messages').addEventListener("click", function () { 5 | confirmation.style.display = 'block'; 6 | }); 7 | 8 | document.querySelector('.close-btn').addEventListener("click", function () { 9 | confirmation.style.display = 'none'; 10 | }); 11 | 12 | document.querySelector('.close-confirm').addEventListener("click", function () { 13 | confirmation.style.display = 'none'; 14 | }); 15 | } 16 | 17 | confirmationModal(); -------------------------------------------------------------------------------- /chat/static_dev/js/page_reload_listener.js: -------------------------------------------------------------------------------- 1 | const pageReloadListener = () => { 2 | const chat = document.getElementById('chat-log'); 3 | const chatsList = document.querySelector('.chats-list'); 4 | 5 | document.addEventListener("DOMContentLoaded", function (event) { 6 | let scrollpos = localStorage.getItem('chatScrollpos'); 7 | if (scrollpos) chat.scrollTo(0, scrollpos); 8 | 9 | scrollpos = localStorage.getItem('chatsScrollpos'); 10 | if (scrollpos) chatsList.scrollTo(0, scrollpos); 11 | }); 12 | 13 | window.onbeforeunload = function (e) { 14 | if (chat) { 15 | let pos = chat.scrollTop; 16 | if (pos == 0) { 17 | pos = 1; 18 | } 19 | localStorage.setItem('chatScrollpos', pos); 20 | } 21 | if (chatsList) localStorage.setItem('chatsScrollpos', chatsList.scrollTop); 22 | } 23 | } 24 | 25 | pageReloadListener(); -------------------------------------------------------------------------------- /chat/static_dev/js/search.js: -------------------------------------------------------------------------------- 1 | const clearString = (value) => { 2 | return value.trim().toLowerCase(); 3 | } 4 | const listSearch = () => { 5 | const input = document.getElementById('js-search'); 6 | const listContainer = document.getElementById('js-checkboxes-list-container').firstElementChild 7 | const elements = listContainer.children; 8 | 9 | input.addEventListener('input', () => { 10 | const clearedValue = clearString(input.value); 11 | 12 | if (clearedValue.length > 0) { 13 | for (let element of elements) { 14 | if (clearString(element.textContent).includes(clearedValue)) { 15 | element.style.display = 'block'; 16 | } else { 17 | element.style.display = 'none'; 18 | } 19 | } 20 | } else { 21 | for (let element of elements) { 22 | element.style.display = 'block'; 23 | } 24 | } 25 | }) 26 | } 27 | 28 | listSearch(); 29 | -------------------------------------------------------------------------------- /chat/static_dev/js/search_bar.js: -------------------------------------------------------------------------------- 1 | const clearString = (value) => { 2 | return value.trim().toLowerCase(); 3 | } 4 | const barSearch = () => { 5 | const input = document.getElementById('js-search'); 6 | const listContainer = document.getElementById('js-list-container') 7 | const elements = listContainer.children; 8 | input.addEventListener('input', () => { 9 | const clearedValue = clearString(input.value); 10 | if (clearedValue.length > 0) { 11 | for (let element of elements) { 12 | if (clearString(element.textContent).includes(clearedValue)) { 13 | element.style.display = 'flex'; 14 | } else { 15 | element.style.display = 'none'; 16 | } 17 | } 18 | } else { 19 | for (let element of elements) { 20 | element.style.display = 'flex'; 21 | } 22 | } 23 | }) 24 | } 25 | 26 | barSearch(); 27 | -------------------------------------------------------------------------------- /chat/static_dev/js/sockets.js: -------------------------------------------------------------------------------- 1 | moment.locale('ru'); 2 | 3 | const connectionType= JSON.parse(document.getElementById('connection_type').textContent); 4 | const connectionName = JSON.parse(document.getElementById('connection_name').textContent); 5 | const currentUsername = JSON.parse(document.getElementById('username').textContent); 6 | 7 | const chatSocket = new WebSocket(`ws://${window.location.host}/ws/chat/${connectionType}_${connectionName}/`); 8 | 9 | const chatLog = document.querySelector('#chat-log'); 10 | chatLog.scrollTop = chatLog.scrollHeight; 11 | 12 | const chatInput = document.querySelector('#chat-message-input'); 13 | chatInput.focus(); 14 | 15 | const chatSubmit = document.querySelector('#chat-message-submit'); 16 | 17 | 18 | chatSocket.onmessage = function (event) { 19 | const { username, message } = JSON.parse(event.data); 20 | const cleanedMessage = cleanMessage(message); 21 | 22 | const currentUsername = document.querySelector('.user__username').textContent.trim(); 23 | console.log(currentUsername); 24 | if (validateMessage(message)) { 25 | const messageMarkup = ` 26 |
27 | ${username || 'Анонимный пользователь'} 28 |

${cleanedMessage}

29 | ${moment().format('LT')} 30 |
` 31 | 32 | chatLog.insertAdjacentHTML('beforeend', messageMarkup); 33 | chatLog.scrollTop = chatLog.scrollHeight; 34 | } 35 | }; 36 | 37 | chatSocket.onclose = function (e) { 38 | console.error('Chat socket closed unexpectedly'); 39 | // alert('Произошла ошибка! Сервер недоступен.') 40 | }; 41 | 42 | 43 | chatInput.onkeydown = function (e) { 44 | if (e.keyCode === 13) { // enter, return 45 | e.preventDefault(); 46 | 47 | chatInput.style.height = 'auto'; 48 | chatSubmit.click(); 49 | } 50 | }; 51 | 52 | chatSubmit.onclick = function (event) { 53 | const message = cleanMessage(chatInput.value); 54 | if (validateMessage(message)) { 55 | try { 56 | chatSocket.send(JSON.stringify({ 57 | 'message': message, 58 | })); 59 | chatInput.value = ''; 60 | } catch (e) { 61 | consol.error(e); 62 | } 63 | } 64 | }; 65 | 66 | 67 | const cleanMessage = (message) => { 68 | return message.trim(); 69 | } 70 | 71 | const validateMessage = (message) => { 72 | return message.length > 0; 73 | } -------------------------------------------------------------------------------- /chat/static_dev/scss/_reset.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | *, 3 | html, 4 | ::before, 5 | ::after { 6 | margin: 0; 7 | padding: 0; 8 | font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 9 | box-sizing: border-box; 10 | list-style: none; 11 | text-decoration: none; 12 | list-style: none; 13 | scroll-behavior: smooth; 14 | } 15 | 16 | a, 17 | a:visited, a:hover { 18 | color: $white; 19 | text-decoration: none; 20 | } 21 | ul { 22 | margin-bottom: 0; 23 | } 24 | fieldset { 25 | border: none; 26 | } 27 | 28 | input, 29 | textarea, 30 | button { 31 | font-family: 'Istok Web', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 32 | border: none; 33 | } 34 | 35 | p { 36 | margin-bottom: 0; 37 | } -------------------------------------------------------------------------------- /chat/static_dev/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | $bg: #0b1120; 2 | $sidebar: #0F172A; 3 | $gray: #1e293b; 4 | $light-gray: #353f4f; 5 | $lighter-gray: #475569; 6 | $transparented-light-gray: rgba(53, 63, 79, .5); 7 | $white: #f5f5f5; 8 | $blue: #0eaef6; 9 | $red: #BE185D; 10 | $transition: 0.15s ease-in; 11 | -------------------------------------------------------------------------------- /chat/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load sass_tags %} 2 | {% load static %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% comment %} Connect bootstrap {% endcomment %} 13 | 14 | 15 | 16 | 17 | 18 | {% comment %} Connect google fonts {% endcomment %} 19 | 20 | 21 | 22 | 23 | {% comment %} Connect SCSS styles {% endcomment %} 24 | 25 | 26 | {% comment %} Connect favicons and manifest {% endcomment %} 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% comment %} Connect MomentJS {% endcomment %} 40 | 41 | 42 | {% block title %} Главная страница {% endblock %} | Soft 43 | 44 | 45 | 46 |
47 |
48 |
49 |
50 |
51 | {% block content %} Пустая страничка {% endblock %} 52 |
53 |
54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /chat/templates/chats/chat.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} Чат {% endblock %} 6 | 7 | {% block content %} 8 | {% include '../includes/sidebar.html' %} 9 |
10 | {% if is_member %} 11 | {% include '../includes/clear_chat_messages_confirm.html' with chat=chat %} 12 | {% if request.user.id == chat.first_user.id %} 13 |

Чат с {{ chat.second_user.username }}

14 | {% else %} 15 |

Чат с {{ chat.first_user.username }}

16 | {% endif %} 17 |
18 |
19 | {% if request.user.id == chat.first_user.id %} 20 | {{ chat.second_user.username|truncatechars:50 }} 21 | {% else %} 22 | {{ chat.first_user.username|truncatechars:50 }} 23 | {% endif %} 24 | 25 | 26 | 27 |
28 |
29 | 30 | 31 | ✨ Напишите первое сообщение! ✨ 32 | 33 | 34 | {% for message in messages.all %} 35 | {% if message.user == user %} 36 |
37 | 38 | {{ message.user.username }} 39 | 40 |

{{ message.text|safe }}

41 | {{ message.time }} 42 |
43 | {% else %} 44 |
45 | 46 | {{ message.user.username }} 47 | 48 |

{{message.text|safe}}

49 | {{ message.time }} 50 |
51 | {% endif %} 52 | {% endfor %} 53 |
54 |
55 | 61 | 64 |
65 |
66 | 67 | 68 | {% include '../includes/websocket_data.html' with connection_type=connection_type connection_name=chat.id username=request.user.username %} 69 | {% else %} 70 |

Вы не состоите в этом чате 😔

71 | {% endif %} 72 |
73 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/chats/chats_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} Чаты {% endblock %} 6 | 7 | {% block content %} 8 | {% include '../includes/sidebar.html' %} 9 | 45 | 46 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/chats/clear_chats_messages.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} Чаты {% endblock %} 6 | 7 | {% block content %} 8 | {% include '../includes/sidebar.html' %} 9 |
10 |

История сообщений удалена!

11 | 12 |
13 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/groups/clear_group_messages.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} Группы {% endblock %} 6 | 7 | {% block content %} 8 | {% include '../includes/sidebar.html' %} 9 |
10 |

История сообщений удалена!

11 | 12 |
13 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/groups/create_group.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} Создание группы {% endblock %} 6 | 7 | {% block content %} 8 | {% include '../includes/sidebar.html' %} 9 |
10 |

Создание группы

11 | {% include 'includes/form.html' with form=form submit_text="Создать группу" %} 12 |
13 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/groups/delete_group_messages.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} Группы {% endblock %} 6 | 7 | {% block content %} 8 | {% include '../includes/sidebar.html' %} 9 |
10 |

Группа удалена!

11 | 12 |
13 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/groups/edit_group.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} 6 | Редактирование группы 7 | {% endblock %} 8 | 9 | {% block content %} 10 | {% include '../includes/sidebar.html' %} 11 |
12 | {% include '../includes/delete_group_confirm.html' with group=group %} 13 |

Редактирование группы

14 | {% include 'includes/form.html' with form=form submit_text="Изменить" %} 15 | 16 |
17 | {% endblock %} 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /chat/templates/groups/group.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} Группа {{ group.name|safe }} {% endblock %} 6 | 7 | {% block content %} 8 | {% include '../includes/sidebar.html' %} 9 |
10 | {% if is_member %} 11 | {% include '../includes/clear_group_messages_confirm.html' with group=group %} 12 |

Группа: {{ group.name|safe }}

13 |
14 |
15 | 16 | {{ group.slug|safe }} 17 | 18 |
19 |
20 | 21 | 22 | {{ members_count }} 23 | 24 |
25 | {% if user.id == group.owner.id %} 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% endif %} 33 | 34 |
35 |
36 | 37 | 38 | ✨ {{ group.owner.username|capfirst }} создал группу {{ group.name }}! ✨ 39 | 40 | 41 | {% for message in messages.all %} 42 | {% if message.user == user %} 43 |
44 | 45 | {{ message.user.username }} 46 | 47 |

{{ message.text|safe }}

48 | {{ message.time }} 49 |
50 | {% else %} 51 |
52 | 53 | {{ message.user.username }} 54 | 55 |

{{message.text|safe}}

56 | {{ message.time }} 57 |
58 | {% endif %} 59 | {% endfor %} 60 |
61 |
62 | 68 | 71 |
72 |
73 | 74 | 75 | {% include '../includes/websocket_data.html' with connection_type=connection_type connection_name=group.slug username=request.user.username %} 76 | {% else %} 77 |

Вы не состоите в этой группе 😔

78 | {% endif %} 79 |
80 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/groups/groups_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} Группы {% endblock %} 6 | 7 | {% block content %} 8 | {% include '../includes/sidebar.html' %} 9 | 32 | 33 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/homepage/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% block title %} Главная {% endblock %} 4 | {% block content %} 5 | {% if request.user.is_authenticated %} 6 | {% include '../includes/sidebar.html' %} 7 |
8 | {% include '../includes/homepage_content.html' %} 9 |
10 | {% else %} 11 |
12 | {% include '../includes/homepage_content.html' %} 13 | 17 |
18 | {% endif %} 19 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/includes/checkbox_select.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | {% include "django/forms/widgets/multiple_input.html" %} 4 |
5 | -------------------------------------------------------------------------------- /chat/templates/includes/clear_chat_messages_confirm.html: -------------------------------------------------------------------------------- 1 |
2 | {% include '../includes/confirmation_top.html'%} 3 |
4 |
5 |

Удалить историю сообщений чата?

6 | 14 |
15 |
-------------------------------------------------------------------------------- /chat/templates/includes/clear_group_messages_confirm.html: -------------------------------------------------------------------------------- 1 |
2 | {% include '../includes/confirmation_top.html'%} 3 |
4 |
5 |

Удалить историю сообщений группы {{ group.name }}?

6 | 14 |
15 |
-------------------------------------------------------------------------------- /chat/templates/includes/clearable_file_input.html: -------------------------------------------------------------------------------- 1 | {% if widget.is_initial %} 2 | Посмотреть изображение 3 | {% if not widget.required %} 4 |
5 | 6 | 7 |
8 | {% endif %} 9 | {% endif %} 10 | 11 | -------------------------------------------------------------------------------- /chat/templates/includes/confirmation_top.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Подтверждение 4 | 5 | 6 | 7 | 8 |
-------------------------------------------------------------------------------- /chat/templates/includes/delete_group_confirm.html: -------------------------------------------------------------------------------- 1 |
2 | {% include '../includes/confirmation_top.html'%} 3 |
4 |
5 |

Удалить группу?

6 | 14 |
15 |
-------------------------------------------------------------------------------- /chat/templates/includes/form.html: -------------------------------------------------------------------------------- 1 |
2 | {% load static %} 3 |
4 | {% if form.errors %} 5 | {% for error in form.non_field_errors %} 6 | {{ error|escape }} 7 | {% endfor %} 8 | {% endif %} 9 |
10 | 11 |
12 | {% csrf_token %} 13 | 14 | {% for field in form %} 15 |
16 | 22 |
23 | {% for error in field.errors %} 24 | {{ error|escape }} 25 | {% endfor %} 26 |
27 | {{ field }} 28 | {% if field.help_text %} 29 | 30 | {{ field.help_text|safe}} 31 | 32 | {% endif %} 33 |
34 | {% endfor %} 35 | 36 |
37 | 38 | {% if edit %} 39 | 40 | {% endif %} 41 |
42 | {% if show_forgot_password %} 43 | 44 | Забыли пароль? 45 | 46 | {% endif %} 47 | {% if show_register_link %} 48 | 49 | Первый раз в Soft? Создать аккаунт! 50 | 51 | {% endif %} 52 | {% if show_already_have_account %} 53 | 54 | Уже есть аккаунт? Войти 55 | 56 | {% endif %} 57 |
58 | 59 | 60 |
-------------------------------------------------------------------------------- /chat/templates/includes/homepage_content.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 |

4 | Добро пожаловать в Soft Logo 5 |

6 |

7 | Это текстовый чат, основанный на веб-сокетах. Пользователи могут общаться друг с другом, а также создавать группы. 8 |

9 | 10 | {% if request.user.is_authenticated %} 11 |

12 | Откройте меню, чтобы создать группу или пообщаться с кем-нибудь! 13 |

14 | {% else %} 15 |

16 | О проекте 17 |

18 |

19 | Инструкция 20 |

21 |

22 | Авторизуйтесь, чтобы попробовать! 23 |

24 | {% endif %} -------------------------------------------------------------------------------- /chat/templates/includes/sidebar.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | -------------------------------------------------------------------------------- /chat/templates/includes/websocket_data.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | {{ connection_name|json_script:"connection_name" }} 4 | {{ connection_type|json_script:"connection_type" }} 5 | {{ username|json_script:"username" }} 6 | 7 | -------------------------------------------------------------------------------- /chat/templates/info/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} Про проект {% endblock %} 6 | 7 | {% block content %} 8 | {% include '../includes/sidebar.html' %} 9 |
10 |

О проекте

11 |
12 |

Soft - текстовый чат, основанный на веб-сокетах, где пользователи могут общаться друг с другом и создавать группы

13 | 17 |

18 | Приложение написано на языке Python с использованием фреймворка Django 19 |

20 |

Технические детали:

21 |
    22 |
  • Python 3.10.4
  • 23 |
  • Django 3.2.13
  • 24 |
  • БД: SQLite
  • 25 |
  • Библиотека для работы с вебсокетами: channels 3.0.4
  • 26 |
  • CSS препроцессор: SCSS
  • 27 |
28 |
29 |
30 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/info/instruction.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} Инструкция {% endblock %} 6 | 7 | {% block content %} 8 | {% include '../includes/sidebar.html' %} 9 |
10 |

Инструкция

11 |
12 |

13 | README 14 | также можно почитать на нашем GitHub 😇 15 | 16 |

17 | И не забудьте поставить звездочку 🌟 18 |

19 |

Инструкция по установке и запуску

20 |

21 | Клонируйте репозиторий 22 | git clone https://github.com/Yakser/django-websocket-chat 23 |

24 |

25 | Перед установкой зависимостей проекта необходимо скачать 26 | 27 | C++ Build Tools 28 | 29 |

30 |

31 | Установить зависимости можно командой pip install -r requirements.txt 32 |

33 |

34 | Затем необходимо скачать и запустить 35 | 36 | Redis 37 | 38 |

39 |
Если у Вас Windows
40 |

41 | Нужно установить 42 | 43 | WSL2 44 | 45 | и в ней запустить 46 | 47 | redis-server 48 | 49 |

50 |
Запуск
51 |

Сделайте и заполните файл .env в соответствии с .env.example

52 |

Перейдите в директорию django-проекта cd chat

53 |

54 | Запустите приложение командой python manage.py runserver 55 |

56 |
57 |
58 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/users/logged_out.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} 3 | Выход 4 | {% endblock %} 5 | 6 | {% block content %} 7 |

Вы вышли из аккаунта 😔

8 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/users/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %} 4 | Вход 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |

10 | Вход в Soft Logo 11 |

12 | {% include 'includes/form.html' with form=form submit_text="Войти" show_forgot_password=True show_register_link=True %} 13 |
14 | 15 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/users/password_change.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} 3 | Смена пароля 4 | {% endblock %} 5 | 6 | {% block content %} 7 |

Сменить пароль

8 | {% include 'includes/form.html' with form=form submit_text="Изменить пароль" %} 9 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/users/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} 3 | Смена пароля 4 | {% endblock %} 5 | 6 | {% block content %} 7 |

Вы успешно сменили пароль!

8 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/users/password_reset.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} 3 | Восстановление пароля 4 | {% endblock %} 5 | 6 | {% block content %} 7 |

Восстановление пароля

8 | {% include 'includes/form.html' with form=form submit_text="Восстановить пароль" %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /chat/templates/users/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} 3 | Восстановление пароля завершено 4 | {% endblock %} 5 | 6 | {% block content %} 7 |

Восстановление пароля завершено

8 |

9 | Ваш пароль был сохранен. Теперь вы можете войти. 10 |

11 |

12 | Войти 13 |

14 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/users/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} 3 | Подтверждение 4 | {% endblock %} 5 | 6 | {% block content %} 7 | {% if form %} 8 |

Подтверждение

9 | {% include 'includes/form.html' with form=form submit_text="Восстановить пароль" %} 10 | {% else %} 11 |

Ошибка восстановления пароля

12 |

13 | Неверная ссылка для восстановления пароля. Возможно, ей уже воспользовались. Пожалуйста, попробуйте восстановить пароль еще раз. 14 |

15 | {% endif %} 16 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/users/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} 3 | Восстановление пароля 4 | {% endblock %} 5 | 6 | {% block content %} 7 |

Письмо с инструкциями по восстановлению пароля отправлено

8 |

9 | Мы отправили вам инструкцию по установке нового пароля на указанный адрес электронной почты (если в нашей базе данных есть такой адрес). Вы должны получить ее в ближайшее время. 10 | 11 | Если вы не получили письмо, пожалуйста, убедитесь, что вы ввели адрес с которым Вы зарегистрировались, и проверьте папку со спамом. 12 |

13 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/users/profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | Мой профиль 5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% include '../includes/sidebar.html' %} 9 |
10 |

Мой профиль

11 | {% include 'includes/form.html' with form=form submit_text="Изменить" %} 12 |
13 | {% endblock %} 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /chat/templates/users/signup.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | Регистрация 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |

Регистрация

10 | {% include 'includes/form.html' with form=form submit_text="Зарегистрироваться" show_already_have_account=True %} 11 |
12 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/users/user_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | Профиль {{ user.username|safe }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% include '../includes/sidebar.html' %} 9 |
10 |

Профиль пользователя {{ user.username|safe }}

11 |
12 | 45 |
46 | 47 |
48 | {% endblock %} -------------------------------------------------------------------------------- /chat/templates/users/users_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} 6 | Список пользователей 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 | {% include '../includes/sidebar.html' %} 12 | 33 | 34 | 35 | {% endblock %} 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /chat/templates/users_channels/users_channels.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} Главная {% endblock %} 6 | 7 | {% block content %} 8 | {% include '../includes/sidebar.html' %} 9 |
10 |

Канал: {{ channel_name|safe }}

11 |
12 |
13 |
14 | 15 | 16 |
17 | {% include '../includes/websocket_data.html' with connection_type=connection_type connection_name=channel_name %} 18 |
19 | {% endblock %} -------------------------------------------------------------------------------- /chat/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/users/__init__.py -------------------------------------------------------------------------------- /chat/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.auth.admin import UserAdmin 4 | 5 | from users.models import Profile 6 | 7 | User = get_user_model() 8 | 9 | 10 | class ProfileInlined(admin.StackedInline): 11 | model = Profile 12 | can_delete = False 13 | 14 | 15 | class UserAdmin(UserAdmin): 16 | inlines = (ProfileInlined, ) 17 | list_display = ('username', 'email', 'is_staff', ) 18 | 19 | 20 | admin.site.unregister(User) 21 | admin.site.register(User, UserAdmin) 22 | -------------------------------------------------------------------------------- /chat/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'users' 7 | -------------------------------------------------------------------------------- /chat/users/forms.py: -------------------------------------------------------------------------------- 1 | from core.widgets import CustomClearableFileInput 2 | from django import forms 3 | from django.contrib.auth import get_user_model 4 | from django.contrib.auth.password_validation import validate_password 5 | from users.validators import validate_email, validate_login 6 | 7 | User = get_user_model() 8 | 9 | 10 | class EditProfileForm(forms.Form): 11 | """ 12 | Форма редактирования профиля пользователя 13 | 14 | Fields: 15 | login (CharField): имя пользователя 16 | email (EmailField): электронная почта 17 | image (ImageField): изображение 18 | biography (CharField): краткая информация о пользователе 19 | 20 | """ 21 | 22 | login = forms.CharField(max_length=150, 23 | label='Имя пользователя', 24 | help_text='Максимум 150 символов', 25 | required=True) 26 | 27 | email = forms.EmailField(label='Почта', 28 | required=False) 29 | 30 | image = forms.ImageField(label='Аватар', 31 | required=False, 32 | allow_empty_file=False, 33 | widget=CustomClearableFileInput) 34 | 35 | biography = forms.CharField(label='Расскажите немного о себе', 36 | widget=forms.Textarea(), 37 | max_length=500, 38 | required=False) 39 | 40 | def validate_edit_login(self, current_user): 41 | login = self.cleaned_data['login'] 42 | 43 | if len(login.strip()) < 2: 44 | self.add_error('login', 'Длина имени не менее 2 символов!') 45 | return 46 | 47 | if login != current_user.username and User.objects.filter(username=login): 48 | self.add_error('login', 49 | 'Пользователь с таким именем уже существует!') 50 | return 51 | 52 | return True 53 | 54 | def validate_edit_email(self, current_user): 55 | email = self.cleaned_data['email'] 56 | 57 | if email != current_user.email and User.objects.filter(email=email): 58 | self.add_error('email', 59 | 'Пользователь с такой почтой уже существует!') 60 | return 61 | 62 | return True 63 | 64 | def validate_all(self, current_user): 65 | return self.validate_edit_email(current_user) and self.validate_edit_login(current_user) 66 | 67 | 68 | class SignupForm(forms.Form): 69 | """ 70 | Форма регистрации 71 | 72 | Fields: 73 | login (CharField): имя пользователя 74 | email (EmailField): электронная почта 75 | password (CharField): пароль 76 | password_repeat (CharField): еще раз пароль 77 | 78 | """ 79 | email = forms.CharField(max_length=200, 80 | label='Адрес электронной почты', 81 | widget=forms.EmailInput, 82 | required=True, 83 | validators=[validate_email]) 84 | login = forms.CharField(max_length=150, 85 | label='Имя пользователя', 86 | help_text='Максимум 150 символов', 87 | required=True, 88 | validators=[validate_login]) 89 | password = forms.CharField(max_length=255, label='Пароль', 90 | help_text='Максимум 255 символов', 91 | widget=forms.PasswordInput, 92 | required=True, 93 | min_length=8, 94 | validators=[validate_password]) 95 | password_repeat = forms.CharField(max_length=255, 96 | label='Повторите пароль', 97 | widget=forms.PasswordInput) 98 | 99 | def check_passwords_match(self): 100 | password = self.cleaned_data['password'] 101 | password_repeat = self.cleaned_data['password_repeat'] 102 | 103 | if password != password_repeat: 104 | self.add_error('password', 'Пароли не совпадают!') 105 | return 106 | return password 107 | -------------------------------------------------------------------------------- /chat/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-03 10:47 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Profile', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('biography', models.TextField(help_text='Немного расскажите о себе', max_length=500, null=True, verbose_name='О себе')), 22 | ('image', models.ImageField(help_text='Выберите изображение', null=True, upload_to='uploads/users_images', verbose_name='Изображение пользователя')), 23 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 24 | ], 25 | options={ 26 | 'verbose_name': 'Дополнительное поле', 27 | 'verbose_name_plural': 'Дополнительные поля', 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /chat/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/users/migrations/__init__.py -------------------------------------------------------------------------------- /chat/users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.db import models 3 | from django.db.models.signals import post_save 4 | from django.dispatch import receiver 5 | from django.utils.html import mark_safe 6 | from sorl.thumbnail import get_thumbnail 7 | 8 | User = get_user_model() 9 | 10 | 11 | class Profile(models.Model): 12 | """ 13 | Модель профиля пользователя. Расширяет модель User. 14 | 15 | Attributes: 16 | user (User): пользователь 17 | image (ImageField): изображение 18 | biography (TextField): краткая информация о пользователе 19 | 20 | """ 21 | 22 | user = models.OneToOneField(User, 23 | on_delete=models.CASCADE) 24 | 25 | biography = models.TextField(verbose_name='О себе', 26 | max_length=500, 27 | unique=False, 28 | null=True, 29 | help_text='Немного расскажите о себе') 30 | 31 | image = models.ImageField(verbose_name='Изображение пользователя', 32 | upload_to='uploads/users_images', 33 | null=True, 34 | help_text='Выберите изображение') 35 | 36 | def get_image_x256(self): 37 | return get_thumbnail(self.image, 38 | '256', 39 | quality=51) 40 | 41 | def image_tmb(self): 42 | if self.image: 43 | return mark_safe(f'//', 37 | PasswordResetConfirmView.as_view(template_name='users/password_reset_confirm.html'), 38 | name='password_reset_confirm'), 39 | path('reset/done/', 40 | PasswordResetCompleteView.as_view(template_name='users/password_reset_complete.html'), 41 | name='password_reset_complete'), 42 | 43 | path('users/', 44 | UsersListView.as_view(), 45 | name='users_list'), 46 | path('users//', 47 | UserDetailView.as_view(), 48 | name='user_detail'), 49 | 50 | path('signup/', 51 | SignupView.as_view(), 52 | name='signup'), 53 | path('profile/', 54 | ProfileView.as_view(), 55 | name='profile'), 56 | ] 57 | -------------------------------------------------------------------------------- /chat/users/validators.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.forms import ValidationError 3 | 4 | User = get_user_model() 5 | 6 | 7 | def validate_login(value): 8 | if len(value.strip()) < 2: 9 | raise ValidationError('Длина не менее 2 символов!') 10 | if User.objects.filter(username=value): 11 | raise ValidationError('Пользователь с таким именем уже существует!') 12 | 13 | 14 | def validate_email(value): 15 | if User.objects.filter(email=value): 16 | raise ValidationError('Пользователь с такой почтой уже существует!') 17 | 18 | 19 | def validate_edit_login(current_user): 20 | def wrapper(login): 21 | if login != current_user.username and User.objects.filter(username=login): 22 | raise ValidationError( 23 | 'Пользователь с таким именем уже существует!') 24 | 25 | return wrapper 26 | 27 | 28 | def validate_edit_email(current_user): 29 | def wrapper(email): 30 | if email != current_user.email and User.objects.filter(email=email): 31 | raise ValidationError( 32 | 'Пользователь с такой почтой уже существует!') 33 | return wrapper 34 | -------------------------------------------------------------------------------- /chat/users/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model, login 2 | from django.contrib.auth.decorators import login_required 3 | from django.shortcuts import get_object_or_404, redirect, render 4 | from django.utils.decorators import method_decorator 5 | from django.views.generic.base import TemplateView 6 | 7 | from users.forms import EditProfileForm, SignupForm 8 | 9 | User = get_user_model() 10 | 11 | 12 | class UsersListView(TemplateView): 13 | """ 14 | Отображает список пользователей 15 | 16 | Context: 17 | users (User[]): QuerySet, содержащий экземпляры класса User 18 | 19 | Template: 20 | template_name: 'users/users_list.html' 21 | 22 | """ 23 | 24 | template_name = 'users/users_list.html' 25 | 26 | def get_context_data(self, **kwargs): 27 | context = super().get_context_data(**kwargs) 28 | users = User.objects.only('id', 'username') 29 | context['users'] = users 30 | return context 31 | 32 | 33 | @method_decorator(login_required, name='dispatch') 34 | class UserDetailView(TemplateView): 35 | """ 36 | Отображает страницу пользователя (User) 37 | 38 | Context: 39 | user (User): экземпляр класса User 40 | 41 | Template: 42 | template_name: 'users/users_list.html' 43 | 44 | """ 45 | template_name = 'users/user_detail.html' 46 | 47 | def get(self, request, id: int, *args, **kwargs): 48 | if request.user.id == id: 49 | return redirect('users:profile') 50 | else: 51 | return render(request, 52 | self.template_name, 53 | self.get_context_data(id)) 54 | 55 | def get_context_data(self, id: int, **kwargs): 56 | context = super().get_context_data(**kwargs) 57 | user: User = get_object_or_404(User.objects.only('email', 'username'), pk=id) 58 | context['user'] = user 59 | return context 60 | 61 | 62 | @method_decorator(login_required, name='dispatch') 63 | class ProfileView(TemplateView): 64 | """ 65 | Отображает страницу редактирования профиля пользователя. 66 | 67 | Context: 68 | user (User): экземпляр класса User 69 | form (EditProfileForm): форма редактирования профиля 70 | 71 | Template: 72 | template_name: 'users/profile.html' 73 | 74 | Form: 75 | form_class (EditProfileForm): форма редактирования профиля 76 | 77 | """ 78 | 79 | template_name = 'users/profile.html' 80 | form_class = EditProfileForm 81 | 82 | def get(self, request, *args, **kwargs): 83 | return render(request, 84 | self.template_name, 85 | self.get_context_data(request)) 86 | 87 | def post(self, request, *args, **kwargs): 88 | user: User = get_object_or_404(User.objects.only('email', 'username'), 89 | pk=request.user.id) 90 | 91 | form = self.form_class(request.POST, request.FILES) 92 | 93 | if form.is_valid() and form.validate_all(request.user): 94 | user.email = form.cleaned_data['email'] 95 | user.username = form.cleaned_data['login'] 96 | user.profile.biography = form.cleaned_data['biography'] or user.profile.biography 97 | 98 | # если нажат чекбокс очистки изображения 99 | if form.cleaned_data['image'] is False: 100 | user.profile.image = None 101 | else: 102 | user.profile.image = form.cleaned_data['image'] or user.profile.image 103 | 104 | user.save() 105 | 106 | return redirect('users:profile') 107 | 108 | return render(request, 109 | self.template_name, 110 | self.get_context_data(request)) 111 | 112 | def get_context_data(self, request, **kwargs): 113 | context = super().get_context_data(**kwargs) 114 | 115 | user: User = get_object_or_404(User.objects.only('email', 'username'), 116 | pk=request.user.id) 117 | 118 | initial_form_data = { 119 | 'email': user.email, 120 | 'login': user.username, 121 | 'biography': user.profile.biography, 122 | 'image': user.profile.image 123 | } 124 | form = self.form_class(request.POST or None, 125 | request.FILES or None, 126 | initial=initial_form_data) 127 | # небоходимо, чтобы показать ошибки валидации формы 128 | if form.is_valid(): 129 | form.validate_all(user) 130 | 131 | context['user'] = user 132 | context['form'] = form 133 | 134 | return context 135 | 136 | 137 | class SignupView(TemplateView): 138 | """ 139 | Отображает страницу регистрации 140 | 141 | Context: 142 | form (SignupForm): форма регистрации 143 | 144 | Template: 145 | template_name: 'users/signup.html' 146 | 147 | Form: 148 | form_class (SignupForm): форма регистрации 149 | 150 | """ 151 | template_name = 'users/signup.html' 152 | form_class = SignupForm 153 | 154 | def get(self, request, *args, **kwargs): 155 | return render(request, 156 | self.template_name, 157 | self.get_context_data(request)) 158 | 159 | def post(self, request, *args, **kwargs): 160 | context = self.get_context_data(request) 161 | 162 | form = context['form'] 163 | 164 | if form.is_valid() and form.check_passwords_match(): 165 | user: User = User.objects.create_user( 166 | username=form.cleaned_data['login'], 167 | password=form.cleaned_data['password'], 168 | email=form.cleaned_data['email'] 169 | ) 170 | 171 | login(request, user) 172 | 173 | return redirect('homepage:home') 174 | 175 | return render(request, 176 | self.template_name, 177 | self.get_context_data(request)) 178 | 179 | def get_context_data(self, request, **kwargs): 180 | context = super().get_context_data(**kwargs) 181 | form = self.form_class(request.POST or None) 182 | context['form'] = form 183 | # небоходимо, чтобы показать ошибки валидации формы 184 | form.is_valid() and form.check_passwords_match() 185 | return context 186 | -------------------------------------------------------------------------------- /chat/users_channels/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/users_channels/__init__.py -------------------------------------------------------------------------------- /chat/users_channels/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from users_channels.models import UsersChannel 4 | 5 | 6 | @admin.register(UsersChannel) 7 | class UsersChannelAdmin(admin.ModelAdmin): 8 | list_display = ('name', 'image_tmb', ) 9 | -------------------------------------------------------------------------------- /chat/users_channels/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersChannelsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'users_channels' 7 | verbose_name = 'Каналы' 8 | -------------------------------------------------------------------------------- /chat/users_channels/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-03 10:47 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='UsersChannel', 19 | fields=[ 20 | ('slug', models.SlugField(default='default_group', help_text='Используйте буквы, цифры или @/./+/-/_ ', max_length=100, primary_key=True, serialize=False, unique=True, verbose_name='Идентификатор')), 21 | ('name', models.CharField(default='Default group name', help_text='Введите название, длина до 100 символов', max_length=100, verbose_name='Название')), 22 | ('image', models.ImageField(help_text='Выберите изображение канала', null=True, upload_to='uploads/groups_images', verbose_name='Изображение канала')), 23 | ('group_members', models.ManyToManyField(related_name='users_channels', to=settings.AUTH_USER_MODEL, verbose_name='Участники')), 24 | ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner_users_channels', to=settings.AUTH_USER_MODEL, verbose_name='Владелец')), 25 | ], 26 | options={ 27 | 'verbose_name': 'Канал', 28 | 'verbose_name_plural': 'Каналы', 29 | }, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /chat/users_channels/migrations/0002_alter_userschannel_slug.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-03 19:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users_channels', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='userschannel', 15 | name='slug', 16 | field=models.SlugField(help_text='Используйте буквы, цифры или @/./+/-/_ ', max_length=100, primary_key=True, serialize=False, unique=True, verbose_name='Идентификатор'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /chat/users_channels/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/users_channels/migrations/__init__.py -------------------------------------------------------------------------------- /chat/users_channels/models.py: -------------------------------------------------------------------------------- 1 | from core.models import BaseWebsocketGroup 2 | from django.contrib.auth import get_user_model 3 | from django.db import models 4 | 5 | User = get_user_model() 6 | 7 | 8 | class UsersChannel(BaseWebsocketGroup): 9 | """ 10 | Модель Канала. Наследуется от абстрактной модели BaseWebsocketGroup. 11 | 12 | """ 13 | 14 | image = models.ImageField(verbose_name='Изображение канала', 15 | upload_to='uploads/groups_images', 16 | null=True, 17 | help_text='Выберите изображение канала') 18 | 19 | owner = models.ForeignKey(User, 20 | verbose_name='Владелец', 21 | related_name='owner_users_channels', 22 | on_delete=models.SET_NULL, 23 | null=True) 24 | 25 | group_members = models.ManyToManyField(User, 26 | verbose_name='Участники', 27 | related_name='users_channels') 28 | 29 | class Meta: 30 | verbose_name = 'Канал' 31 | verbose_name_plural = 'Каналы' 32 | -------------------------------------------------------------------------------- /chat/users_channels/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | from websockets.consumers import UsersChannelsConsumer 3 | 4 | websocket_urlpatterns = [ 5 | re_path(r'ws/chat/(?P\w+)/$', 6 | UsersChannelsConsumer.as_asgi()), 7 | ] 8 | -------------------------------------------------------------------------------- /chat/users_channels/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from users_channels.views import UsersChannelsView 4 | 5 | app_name = 'users_channels' 6 | 7 | urlpatterns = [ 8 | path('/', 9 | UsersChannelsView.as_view(), 10 | name='users_channels'), 11 | ] 12 | -------------------------------------------------------------------------------- /chat/users_channels/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic.base import TemplateView 2 | from websockets.connection_types import USERS_CHANNELS_CONNECTION 3 | 4 | 5 | class UsersChannelsView(TemplateView): 6 | template_name = 'users_channels/users_channels.html' 7 | 8 | def get_context_data(self, **kwargs): 9 | context = super().get_context_data(**kwargs) 10 | context['connection_type'] = USERS_CHANNELS_CONNECTION 11 | return context 12 | -------------------------------------------------------------------------------- /chat/users_messages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/users_messages/__init__.py -------------------------------------------------------------------------------- /chat/users_messages/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from users_messages.models import (DailyChannelMessages, DailyChatMessages, 4 | DailyGroupMessages, UserChannelMessage, 5 | UserChatMessage, UserGroupMessage) 6 | 7 | 8 | @admin.register(DailyChatMessages) 9 | class DailyChatMessagesAdmin(admin.ModelAdmin): 10 | list_display = ('id', 'date', ) 11 | 12 | 13 | @admin.register(DailyGroupMessages) 14 | class DailyGroupMessagesAdmin(admin.ModelAdmin): 15 | list_display = ('id', 'date', ) 16 | 17 | 18 | @admin.register(DailyChannelMessages) 19 | class DailyChannelMessagesAdmin(admin.ModelAdmin): 20 | list_display = ('id', 'date', ) 21 | 22 | 23 | @admin.register(UserGroupMessage) 24 | class UserGroupMessageAdmin(admin.ModelAdmin): 25 | list_display = ('id', 'user_id', 'time', ) 26 | 27 | 28 | @admin.register(UserChannelMessage) 29 | class UserChannelMessageAdmin(admin.ModelAdmin): 30 | list_display = ('id', 'user_id', 'time', ) 31 | 32 | 33 | @admin.register(UserChatMessage) 34 | class UserChatMessageAdmin(admin.ModelAdmin): 35 | list_display = ('id', 'user_id', 'time', ) 36 | -------------------------------------------------------------------------------- /chat/users_messages/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersMessagesConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'users_messages' 7 | verbose_name = 'Сообщения' 8 | -------------------------------------------------------------------------------- /chat/users_messages/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-03 10:47 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='UserMessage', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('text', models.TextField(max_length=1024, verbose_name='Текст')), 22 | ('datetime', models.DateTimeField()), 23 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='messages', to=settings.AUTH_USER_MODEL, verbose_name='Отправитель')), 24 | ], 25 | options={ 26 | 'verbose_name': 'Сообщение', 27 | 'verbose_name_plural': 'Сообщения', 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /chat/users_messages/migrations/0002_auto_20220503_2338.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-03 19:38 2 | 3 | import datetime 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('groups', '0002_alter_group_slug'), 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('users_channels', '0002_alter_userschannel_slug'), 15 | ('users_messages', '0001_initial'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='DailyMessagesChannel', 21 | fields=[ 22 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('date', models.DateField(null=True, verbose_name='Дата создания')), 24 | ('channel', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='channel_containers', to='users_channels.userschannel', verbose_name='Канал')), 25 | ], 26 | options={ 27 | 'verbose_name': 'Контейнер сообщений канала', 28 | 'verbose_name_plural': 'Контейнеры сообщений канала', 29 | }, 30 | ), 31 | migrations.CreateModel( 32 | name='DailyMessagesGroup', 33 | fields=[ 34 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 35 | ('date', models.DateField(null=True, verbose_name='Дата создания')), 36 | ('group', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='group_containers', to='groups.group', verbose_name='Группа')), 37 | ], 38 | options={ 39 | 'verbose_name': 'Контейнер сообщений группы', 40 | 'verbose_name_plural': 'Контейнеры сообщений группы', 41 | }, 42 | ), 43 | migrations.CreateModel( 44 | name='UserChannelMessage', 45 | fields=[ 46 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 47 | ('text', models.TextField(max_length=1024, verbose_name='Текст')), 48 | ('time', models.TimeField(default=datetime.datetime(2022, 5, 3, 23, 38, 28, 110076), null=True, verbose_name='Время отправки')), 49 | ('container', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='channel_messages', to='users_messages.dailymessageschannel', verbose_name='Контейнер')), 50 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='channel_messages', to=settings.AUTH_USER_MODEL, verbose_name='Отправитель')), 51 | ], 52 | options={ 53 | 'verbose_name': 'Сообщение канала', 54 | 'verbose_name_plural': 'Сообщения канала', 55 | }, 56 | ), 57 | migrations.CreateModel( 58 | name='UserGroupMessage', 59 | fields=[ 60 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 61 | ('text', models.TextField(max_length=1024, verbose_name='Текст')), 62 | ('time', models.TimeField(default=datetime.datetime(2022, 5, 3, 23, 38, 28, 110076), null=True, verbose_name='Время отправки')), 63 | ('container', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='group_messages', to='users_messages.dailymessagesgroup', verbose_name='Контейнер')), 64 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='group_messages', to=settings.AUTH_USER_MODEL, verbose_name='Отправитель')), 65 | ], 66 | options={ 67 | 'verbose_name': 'Сообщение группы', 68 | 'verbose_name_plural': 'Сообщения группы', 69 | }, 70 | ), 71 | migrations.DeleteModel( 72 | name='UserMessage', 73 | ), 74 | ] 75 | -------------------------------------------------------------------------------- /chat/users_messages/migrations/0003_auto_20220504_2136.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-04 17:36 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('users_messages', '0002_auto_20220503_2338'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='userchannelmessage', 16 | name='time', 17 | field=models.TimeField(default=datetime.datetime(2022, 5, 4, 21, 36, 16, 997528), null=True, verbose_name='Время отправки'), 18 | ), 19 | migrations.AlterField( 20 | model_name='usergroupmessage', 21 | name='time', 22 | field=models.TimeField(default=datetime.datetime(2022, 5, 4, 21, 36, 16, 997528), null=True, verbose_name='Время отправки'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /chat/users_messages/migrations/0004_auto_20220504_2256.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-04 18:56 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('users_messages', '0003_auto_20220504_2136'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='userchannelmessage', 16 | name='time', 17 | field=models.TimeField(default=datetime.datetime(2022, 5, 4, 22, 56, 28, 527462), null=True, verbose_name='Время отправки'), 18 | ), 19 | migrations.AlterField( 20 | model_name='usergroupmessage', 21 | name='time', 22 | field=models.TimeField(default=datetime.datetime(2022, 5, 4, 22, 56, 28, 527462), null=True, verbose_name='Время отправки'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /chat/users_messages/migrations/0005_auto_20220505_1848.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-05 14:48 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | atomic = False 9 | dependencies = [ 10 | ('groups', '0003_alter_group_image'), 11 | ('users_channels', '0002_alter_userschannel_slug'), 12 | ('users_messages', '0004_auto_20220504_2256'), 13 | ] 14 | 15 | operations = [ 16 | migrations.RenameModel( 17 | old_name='DailyMessagesChannel', 18 | new_name='DailyChannelMessages', 19 | ), 20 | migrations.RenameModel( 21 | old_name='DailyMessagesGroup', 22 | new_name='DailyGroupMessages', 23 | ), 24 | migrations.AlterField( 25 | model_name='userchannelmessage', 26 | name='time', 27 | field=models.TimeField(default=datetime.datetime(2022, 5, 5, 18, 48, 21, 560786), null=True, verbose_name='Время отправки'), 28 | ), 29 | migrations.AlterField( 30 | model_name='usergroupmessage', 31 | name='time', 32 | field=models.TimeField(default=datetime.datetime(2022, 5, 5, 18, 48, 21, 560786), null=True, verbose_name='Время отправки'), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /chat/users_messages/migrations/0006_auto_20220505_1900.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-05 15:00 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('users_messages', '0005_auto_20220505_1848'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='dailychannelmessages', 16 | name='date', 17 | field=models.DateField(auto_now_add=True, null=True, verbose_name='Дата создания'), 18 | ), 19 | migrations.AlterField( 20 | model_name='dailygroupmessages', 21 | name='date', 22 | field=models.DateField(auto_now_add=True, null=True, verbose_name='Дата создания'), 23 | ), 24 | migrations.AlterField( 25 | model_name='userchannelmessage', 26 | name='time', 27 | field=models.TimeField(default=datetime.datetime(2022, 5, 5, 19, 0, 35, 152147), null=True, verbose_name='Время отправки'), 28 | ), 29 | migrations.AlterField( 30 | model_name='usergroupmessage', 31 | name='time', 32 | field=models.TimeField(default=datetime.datetime(2022, 5, 5, 19, 0, 35, 152147), null=True, verbose_name='Время отправки'), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /chat/users_messages/migrations/0007_auto_20220515_1205.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-15 08:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users_messages', '0006_auto_20220505_1900'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='userchannelmessage', 15 | name='time', 16 | field=models.TimeField(auto_now_add=True, null=True, verbose_name='Время отправки'), 17 | ), 18 | migrations.AlterField( 19 | model_name='usergroupmessage', 20 | name='time', 21 | field=models.TimeField(auto_now_add=True, null=True, verbose_name='Время отправки'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /chat/users_messages/migrations/0008_chatmessage_dailychatmessages.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-15 11:46 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('groups', '0003_alter_group_image'), 13 | ('users_messages', '0007_auto_20220515_1205'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='DailyChatMessages', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('date', models.DateField(auto_now_add=True, null=True, verbose_name='Дата создания')), 22 | ('chat', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='chat_containers', to='groups.group', verbose_name='Чат')), 23 | ], 24 | options={ 25 | 'verbose_name': 'Контейнер сообщений чата', 26 | 'verbose_name_plural': 'Контейнеры сообщений чатов', 27 | }, 28 | ), 29 | migrations.CreateModel( 30 | name='ChatMessage', 31 | fields=[ 32 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 33 | ('text', models.TextField(max_length=1024, verbose_name='Текст')), 34 | ('time', models.TimeField(auto_now_add=True, null=True, verbose_name='Время отправки')), 35 | ('container', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='chat_messages', to='users_messages.dailychatmessages', verbose_name='Контейнер')), 36 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='chat_messages', to=settings.AUTH_USER_MODEL, verbose_name='Отправитель')), 37 | ], 38 | options={ 39 | 'verbose_name': 'Сообщение чата', 40 | 'verbose_name_plural': 'Сообщения чата', 41 | }, 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /chat/users_messages/migrations/0009_rename_chatmessage_userchatmessage.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-15 11:49 2 | 3 | from django.conf import settings 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 11 | ('users_messages', '0008_chatmessage_dailychatmessages'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameModel( 16 | old_name='ChatMessage', 17 | new_name='UserChatMessage', 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /chat/users_messages/migrations/0010_alter_dailychatmessages_chat.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-05-15 12:32 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('chats', '0001_initial'), 11 | ('users_messages', '0009_rename_chatmessage_userchatmessage'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='dailychatmessages', 17 | name='chat', 18 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='chat_containers', to='chats.chat', verbose_name='Чат'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /chat/users_messages/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yakser/django-websocket-chat/666d5217cb60d8f9d0f077c25f4d94ab616977c3/chat/users_messages/migrations/__init__.py -------------------------------------------------------------------------------- /chat/users_messages/models.py: -------------------------------------------------------------------------------- 1 | from chats.models import Chat 2 | from core.models import BaseDailyMessages, BaseUserMessage 3 | from django.contrib.auth import get_user_model 4 | from django.db import models 5 | from django.db.models import Prefetch 6 | from django.db.models.query import QuerySet 7 | from groups.models import Group 8 | from users_channels.models import UsersChannel 9 | 10 | User = get_user_model() 11 | 12 | 13 | class DailyChatMessages(BaseDailyMessages): 14 | chat = models.ForeignKey(Chat, 15 | on_delete=models.SET_NULL, 16 | verbose_name='Чат', 17 | related_name='chat_containers', 18 | null=True 19 | ) 20 | 21 | def get_messages(self) -> QuerySet: 22 | """ 23 | Возвращает сообщения контейнера 24 | 25 | Returns: 26 | QuerySet: сообщения контейнера 27 | 28 | """ 29 | prefetch_users = Prefetch('user', queryset=User.objects.only('username')) 30 | return self.chat_messages.prefetch_related(prefetch_users) 31 | 32 | class Meta: 33 | verbose_name = 'Контейнер сообщений чата' 34 | verbose_name_plural = 'Контейнеры сообщений чатов' 35 | 36 | 37 | class DailyGroupMessages(BaseDailyMessages): 38 | group = models.ForeignKey(Group, 39 | on_delete=models.SET_NULL, 40 | verbose_name='Группа', 41 | related_name='group_containers', 42 | null=True 43 | ) 44 | 45 | class Meta: 46 | verbose_name = 'Контейнер сообщений группы' 47 | verbose_name_plural = 'Контейнеры сообщений группы' 48 | 49 | 50 | class DailyChannelMessages(BaseDailyMessages): 51 | channel = models.ForeignKey(UsersChannel, 52 | on_delete=models.SET_NULL, 53 | verbose_name='Канал', 54 | related_name='channel_containers', 55 | null=True 56 | ) 57 | 58 | class Meta: 59 | verbose_name = 'Контейнер сообщений канала' 60 | verbose_name_plural = 'Контейнеры сообщений канала' 61 | 62 | 63 | class UserChatMessage(BaseUserMessage): 64 | user = models.ForeignKey(User, 65 | on_delete=models.SET_NULL, 66 | verbose_name='Отправитель', 67 | related_name='chat_messages', 68 | null=True 69 | ) 70 | container = models.ForeignKey(DailyChatMessages, 71 | on_delete=models.SET_NULL, 72 | verbose_name='Контейнер', 73 | related_name='chat_messages', 74 | null=True 75 | ) 76 | 77 | class Meta: 78 | verbose_name = 'Сообщение чата' 79 | verbose_name_plural = 'Сообщения чата' 80 | 81 | 82 | class UserGroupMessage(BaseUserMessage): 83 | user = models.ForeignKey(User, 84 | on_delete=models.SET_NULL, 85 | verbose_name='Отправитель', 86 | related_name='group_messages', 87 | null=True 88 | ) 89 | container = models.ForeignKey(DailyGroupMessages, 90 | on_delete=models.SET_NULL, 91 | verbose_name='Контейнер', 92 | related_name='group_messages', 93 | null=True 94 | ) 95 | 96 | class Meta: 97 | verbose_name = 'Сообщение группы' 98 | verbose_name_plural = 'Сообщения группы' 99 | 100 | 101 | class UserChannelMessage(BaseUserMessage): 102 | user = models.ForeignKey(User, 103 | on_delete=models.SET_NULL, 104 | verbose_name='Отправитель', 105 | related_name='channel_messages', 106 | null=True 107 | ) 108 | container = models.ForeignKey(DailyChannelMessages, 109 | on_delete=models.SET_NULL, 110 | verbose_name='Контейнер', 111 | related_name='channel_messages', 112 | null=True 113 | ) 114 | 115 | class Meta: 116 | verbose_name = 'Сообщение канала' 117 | verbose_name_plural = 'Сообщения канала' 118 | -------------------------------------------------------------------------------- /chat/websockets/connection_types.py: -------------------------------------------------------------------------------- 1 | GROUPS_CONNECTION = 'groups' 2 | USERS_CHANNELS_CONNECTION = 'users_channels' 3 | CHATS_CONNECTION = 'chats' 4 | -------------------------------------------------------------------------------- /chat/websockets/consumers.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from channels.generic.websocket import AsyncWebsocketConsumer 4 | from django.contrib.auth import get_user_model 5 | 6 | from websockets.events import ChatMessageEventType 7 | from websockets.queries import add_message_to_db 8 | from websockets.types import (ChatMessageEvent, Message, WebsocketData, 9 | WebsocketEvent) 10 | 11 | User = get_user_model() 12 | 13 | 14 | class BaseConsumer(AsyncWebsocketConsumer): 15 | """ 16 | Асинхронный вебсокет consumer 17 | """ 18 | async def connect(self): 19 | """ 20 | Подключение к websocket группе 21 | """ 22 | self.connection_name = self.scope['url_route']['kwargs']['group_name'] 23 | self.connection_group_name = f'websocket_group_{self.connection_name}' 24 | 25 | # Вход в группу 26 | await self.channel_layer.group_add( 27 | self.connection_group_name, 28 | self.channel_name 29 | ) 30 | 31 | await self.accept() 32 | 33 | async def disconnect(self, close_code: int): 34 | """ 35 | Осуществляет выход из группы 36 | 37 | Args: 38 | close_code (int): код выхода 39 | """ 40 | 41 | await self.channel_layer.group_discard( 42 | self.connection_group_name, 43 | self.channel_name 44 | ) 45 | 46 | async def receive(self, text_data: str): 47 | """ 48 | Получает сообщения от вебсокета 49 | 50 | Args: 51 | text_data (str): строка с json объектом Message. Например '{"message": "message-text"}' 52 | """ 53 | 54 | text_data_json: Message = json.loads(text_data) 55 | user: User = self.scope['user'] 56 | 57 | message: str = text_data_json['message'] 58 | username: str = user.username 59 | 60 | group_send_data: ChatMessageEvent = { 61 | 'type': ChatMessageEventType.type, 62 | 'message': message, 63 | 'username': username 64 | } 65 | 66 | await self.channel_layer.group_send( 67 | self.connection_group_name, 68 | group_send_data 69 | ) 70 | 71 | await add_message_to_db(user, message, self.connection_name) 72 | 73 | async def chat_message(self, event: WebsocketEvent): 74 | """ 75 | Получает сообщения от websocket группы 76 | 77 | Args: 78 | event (WebsocketEvent): событие WebsocketEvent 79 | """ 80 | 81 | message = event['message'] 82 | username = event['username'] 83 | 84 | data: WebsocketData = { 85 | 'message': message, 86 | 'username': username 87 | } 88 | 89 | await self.send(text_data=json.dumps(data)) 90 | 91 | 92 | class ChatsConsumer(BaseConsumer): 93 | pass 94 | 95 | 96 | class GroupsConsumer(BaseConsumer): 97 | pass 98 | 99 | 100 | class UsersChannelsConsumer(BaseConsumer): 101 | pass 102 | -------------------------------------------------------------------------------- /chat/websockets/errors.py: -------------------------------------------------------------------------------- 1 | class ConnectionTypeDoesNotExist(Exception): 2 | pass -------------------------------------------------------------------------------- /chat/websockets/events.py: -------------------------------------------------------------------------------- 1 | class WebsocketEventType: 2 | type: str = 'base-websocket-event' 3 | 4 | def __str__(self): 5 | return self.type 6 | 7 | def __repr__(self): 8 | return f"WebsocketEvent<{self.type}>" 9 | 10 | 11 | class ChatMessageEventType(WebsocketEventType): 12 | type: str = 'chat_message' 13 | -------------------------------------------------------------------------------- /chat/websockets/helpers.py: -------------------------------------------------------------------------------- 1 | from chats.models import Chat 2 | from django.shortcuts import get_object_or_404 3 | from groups.models import Group 4 | from users_messages.models import DailyChatMessages, DailyGroupMessages 5 | 6 | from websockets.connection_types import CHATS_CONNECTION, GROUPS_CONNECTION 7 | from websockets.errors import ConnectionTypeDoesNotExist 8 | 9 | 10 | def get_group_slug(connection_name: str) -> str: 11 | """ 12 | Убирает GROUPS_CONNECTION из имени подключения и возвращает slug группы. 13 | Если имя подключения не соответствует подключению группы, то возвращается None 14 | 15 | Args: 16 | connection_name (str): имя подключения, для групп - GROUPS_CONNECTION_ 17 | 18 | Returns: 19 | str: slug группы 20 | 21 | Raises: 22 | ConnectionTypeDoesNotExist: несуществующий тип подключения 23 | """ 24 | if connection_name.startswith(GROUPS_CONNECTION): 25 | return connection_name[len(GROUPS_CONNECTION) + 1:] 26 | raise ConnectionTypeDoesNotExist("Данный тип подключения не существует!") 27 | 28 | 29 | def get_group_daily_messages(group_slug: str) -> DailyGroupMessages: 30 | group: Group = get_object_or_404(Group, pk=group_slug) 31 | 32 | return DailyGroupMessages.objects.get(group=group) 33 | 34 | 35 | def get_chat_id(connection_name: str) -> str: 36 | """ 37 | Убирает CHATS_CONNECTION из имени подключения и возвращает id чата. 38 | Если имя подключения не соответствует подключению чата, то возвращается None 39 | 40 | Args: 41 | connection_name (str): имя подключения, для чатов - CHATS_CONNECTION_ 42 | 43 | Returns: 44 | int: id чата 45 | 46 | Raises: 47 | ConnectionTypeDoesNotExist: несуществующий тип подключения 48 | """ 49 | if connection_name.startswith(CHATS_CONNECTION): 50 | return int(connection_name[len(CHATS_CONNECTION) + 1:]) 51 | 52 | raise ConnectionTypeDoesNotExist("Данный тип подключения не существует!") 53 | 54 | 55 | def get_chat_daily_messages(chat_id: int) -> DailyChatMessages: 56 | chat: Chat = get_object_or_404(Chat, pk=chat_id) 57 | 58 | return DailyChatMessages.objects.get(chat=chat) 59 | -------------------------------------------------------------------------------- /chat/websockets/queries.py: -------------------------------------------------------------------------------- 1 | from channels.db import database_sync_to_async 2 | from django.contrib.auth import get_user_model 3 | from users_messages.models import UserChatMessage, UserGroupMessage 4 | 5 | from websockets.connection_types import CHATS_CONNECTION, GROUPS_CONNECTION 6 | from websockets.errors import ConnectionTypeDoesNotExist 7 | from websockets.helpers import (get_chat_daily_messages, get_chat_id, 8 | get_group_daily_messages, get_group_slug) 9 | 10 | User = get_user_model() 11 | 12 | 13 | async def add_message_to_db(user: User, message_text: str, connection_name: str) -> None: 14 | """ 15 | Добавляет сообщение в контейнер соответствующий типу подключения 16 | 17 | Args: 18 | user (User): отправитель 19 | message_text (str): текст сообщения 20 | connection_name (str): тип подключения 21 | 22 | Raises: 23 | ConnectionTypeDoesNotExist: несуществующий тип подключения 24 | """ 25 | if connection_name.startswith(GROUPS_CONNECTION): 26 | await add_message_to_group(user, message_text, connection_name) 27 | elif connection_name.startswith(CHATS_CONNECTION): 28 | await add_message_to_chat(user, message_text, connection_name) 29 | else: 30 | raise ConnectionTypeDoesNotExist("Данный тип подключения не существует!") 31 | 32 | 33 | @database_sync_to_async 34 | def add_message_to_group(user: User, message_text: str, connection_name: str) -> None: 35 | """ 36 | Создает сообщение в контейнере группы DailyGroupMessages 37 | 38 | Args: 39 | user (User): отправитель 40 | message_text (str): текст сообщения 41 | connection_name (str): имя подключения, для групп - GROUPS_CONNECTION_ 42 | """ 43 | group_slug = get_group_slug(connection_name) 44 | group_daily_messages = get_group_daily_messages(group_slug) 45 | 46 | message = UserGroupMessage(user=user, text=message_text, container=group_daily_messages) 47 | message.save() 48 | 49 | 50 | @database_sync_to_async 51 | def add_message_to_chat(user: User, message_text: str, connection_name: str) -> None: 52 | """ 53 | Создает сообщение в контейнере группы DailyChatMessages 54 | 55 | Args: 56 | user (User): отправитель 57 | message_text (str): текст сообщения 58 | connection_name (str): имя подключения, для чатов - CHATS_CONNECTION_ 59 | """ 60 | 61 | chat_id = get_chat_id(connection_name) 62 | chat_daily_messages = get_chat_daily_messages(chat_id) 63 | 64 | message = UserChatMessage(user=user, text=message_text, container=chat_daily_messages) 65 | message.save() 66 | -------------------------------------------------------------------------------- /chat/websockets/types.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict 2 | 3 | 4 | class Message(TypedDict): 5 | message: str 6 | 7 | 8 | class WebsocketData(TypedDict): 9 | username: str 10 | message: str 11 | 12 | 13 | class WebsocketEvent(WebsocketData): 14 | type: str 15 | 16 | 17 | class ChatMessageEvent(WebsocketEvent): 18 | type: str 19 | -------------------------------------------------------------------------------- /docs/ideas.md: -------------------------------------------------------------------------------- 1 | # ИДЕИ ДЛЯ ДОРАБОТКИ 2 | 3 | ### Создание контейнера сообщений раз в сутки 4 | - Redis 5 | ### Сделать отправку сообщения и запись его в БД независимыми 6 | - Создание тасок для отправки сообщений (Redis, django-rq) 7 | -------------------------------------------------------------------------------- /docs/technical_requirements.md: -------------------------------------------------------------------------------- 1 | # Django Websocket Chat 2 | # 1. Цель проекта 3 | 4 | Цель проекта — разработать текстовый чат, основанный на веб-сокетах (далее Система). Пользователи смогут общаться друг с другом, создавать группы и каналы. 5 | 6 | # 2. Описание системы 7 | 8 | Система состоит из следующих основных функциональных блоков: 9 | 10 | 1. Регистрация, аутентификация и авторизация 11 | 2. Функционал для пользователя 12 | 3. Функционал для администратора 13 | 14 | ## 2.1. Типы пользователей 15 | 16 | Система предусматривает два типа пользователей системы: пользователь и администратор. 17 | 18 | ## 2.2. Регистрация 19 | 20 | * username — обязательное поле 21 | * email — обязательное поле 22 | * пароль — обязательное поле 23 | 24 | ## 2.3. Функционал для администратора 25 | 26 | - Админ-панель с возможностью добавления, удаления и изменения пользователей, чатов, групп и каналов. 27 | 28 | ## 2.4. Функционал для пользователя 29 | 30 | После аутентификации пользователь может написать другим пользователям, создать канал или группу. 31 | Основной функционал состоит из следующих блоков: 32 | 33 | - Редактирование данных профиля 34 | - Заведение и редактирование каналов 35 | - Заведение и редактирование групп 36 | - Чаты с другими пользователями 37 | 38 | ### 2.4.1. Редактирование профиля 39 | 40 | В этом разделе у пользователя есть возможность редактирования данных своего профиля — email, имя, фамилия, соц. сети, аватар и краткая биография. 41 | Пользователь может сменить пароль, подтвердив свой старый пароль, или сбросить пароль с помощью почты. 42 | 43 | ### 2.4.2. Заведение и редактирование каналов 44 | 45 | Канал — инструмент, позволяющий доставлять информацию участникам канала. Только создатель канала (или администраторы канала) могут писать сообщения. 46 | 47 | ### 2.4.3. Заведение и редактирование групп 48 | 49 | Группа - инструмент, позволяющий пользователям общаться вместе в одном чате. Создатель группы имеет право добавлять и удалять других участников. 50 | 51 | # 3. Предлагаемый стек технологий 52 | 53 | * Бэкенд: 54 | - Язык Python 55 | - Фреймворк Django 56 | - БД sqlite3 57 | - Django ORM 58 | - django-channels + java-script для работы с сокетами 59 | * Фронтенд: 60 | - JS 61 | - SCSS 62 | - Bootstrap 4 63 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aioredis==1.3.1 2 | asgiref==3.5.0 3 | async-timeout==4.0.2 4 | attrs==21.4.0 5 | autobahn==22.3.2 6 | Automat==20.2.0 7 | autopep8==1.6.0 8 | cffi==1.15.0 9 | channels==3.0.4 10 | channels-redis==3.4.0 11 | constantly==15.1.0 12 | cryptography==37.0.1 13 | daphne==3.0.2 14 | Django==3.2.15 15 | django-appconf==1.0.5 16 | django-cleanup==6.0.0 17 | django-compressor==4.0 18 | django-debug-toolbar==3.2.4 19 | django-libsass==0.9 20 | django-sass-processor==1.1 21 | hiredis==2.0.0 22 | hyperlink==21.0.0 23 | idna==3.3 24 | incremental==21.3.0 25 | libsass==0.21.0 26 | msgpack==1.0.3 27 | Pillow==9.1.1 28 | pyasn1==0.4.8 29 | pyasn1-modules==0.2.8 30 | pycodestyle==2.8.0 31 | pycparser==2.21 32 | pyOpenSSL==22.0.0 33 | python-decouple==3.6 34 | pytz==2022.1 35 | rcssmin==1.1.0 36 | rjsmin==1.2.0 37 | service-identity==21.1.0 38 | six==1.16.0 39 | sorl-thumbnail==12.8.0 40 | sqlparse==0.4.2 41 | toml==0.10.2 42 | Twisted==22.4.0 43 | txaio==22.2.1 44 | typing-extensions==4.2.0 45 | zope.interface==5.4.0 46 | --------------------------------------------------------------------------------