diff --git a/README.md b/README.md index 8337502..d5d3491 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Demo: ## Requirements: - python 3.10 -- django 4.1 +- django 5 - channels - htmx > 1.8.5 @@ -36,10 +36,11 @@ python -m venv venv source venv/bin/activate ``` -3. Install the required dependencies: +3. Install the required dependencies, we use pip-tools for managing dependencies ``` -pip install -r requirements.txt +pip install pip-tools +pip-sync ``` 4. Make Migrations: @@ -54,3 +55,10 @@ python manage.py migrate python manage.py runserver ``` +6. Create an admin user: + +``` +python manage.py createsuperuser +``` + +7. Visit the admin panel at http://localhost:8000/admin diff --git a/accounts/admin.py b/accounts/admin.py index 8c38f3f..8220d5c 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -1,3 +1,30 @@ from django.contrib import admin -# Register your models here. +from accounts.models import User + + +@admin.register(User) +class UserAdmin(admin.ModelAdmin): + list_display = ("username", "email", "is_superuser", "is_staff") + search_fields = ( + "username", + "email", + ) + list_filter = ("is_superuser", "is_staff") + + fieldsets = ( + ( + None, + { + "fields": ( + "username", + "email", + "is_superuser", + "is_staff", + "is_active", + "last_login", + "date_joined", + ) + }, + ), + ) diff --git a/accounts/forms.py b/accounts/forms.py index 8840927..8647390 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -1,8 +1,8 @@ from accounts.models import User from django.contrib.auth.forms import UserCreationForm -class UserRegisterForm(UserCreationForm): +class UserRegisterForm(UserCreationForm): class Meta: - model= User - fields = ['username', 'password1', 'password2'] + model = User + fields = ["username", "password1", "password2"] diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py index e1aacd0..a688e92 100644 --- a/accounts/migrations/0001_initial.py +++ b/accounts/migrations/0001_initial.py @@ -7,7 +7,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/accounts/models.py b/accounts/models.py index 4c9c171..3d30525 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,4 +1,5 @@ from django.contrib.auth.models import AbstractUser + class User(AbstractUser): pass diff --git a/accounts/views.py b/accounts/views.py index e0338f8..32ccde8 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -14,10 +14,10 @@ def register(request): user = authenticate(username=username, password=raw_password) login(request, user) messages.success( - request, f"Your account has been created and you are now logged in as {username}" + request, + f"Your account has been created and you are now logged in as {username}", ) return redirect("index") else: form = UserRegisterForm() return render(request, "accounts/register.html", {"form": form}) - diff --git a/backchat/asgi.py b/backchat/asgi.py index 2b65ac2..628b989 100644 --- a/backchat/asgi.py +++ b/backchat/asgi.py @@ -3,16 +3,14 @@ from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backchat.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backchat.settings") from chat import routing -application = ProtocolTypeRouter({ - 'http': get_asgi_application(), - "websocket":AuthMiddlewareStack( - URLRouter( - routing.websocket_urlpatterns - ) - ) -}) +application = ProtocolTypeRouter( + { + "http": get_asgi_application(), + "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)), + } +) diff --git a/backchat/settings.py b/backchat/settings.py index e9dd718..45e4815 100644 --- a/backchat/settings.py +++ b/backchat/settings.py @@ -16,9 +16,7 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", - "channels", - "accounts", "chat", ] @@ -38,7 +36,9 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": ["templates",], + "DIRS": [ + "templates", + ], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -94,7 +94,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" CHANNEL_LAYERS = { - 'default': { + "default": { "BACKEND": "channels.layers.InMemoryChannelLayer", } } diff --git a/backchat/urls.py b/backchat/urls.py index 6624991..8833b10 100644 --- a/backchat/urls.py +++ b/backchat/urls.py @@ -3,6 +3,6 @@ urlpatterns = [ path("admin/", admin.site.urls), - path("auth/", include('accounts.urls')), - path("", include('chat.urls')), + path("auth/", include("accounts.urls")), + path("", include("chat.urls")), ] diff --git a/chat/admin.py b/chat/admin.py index 8c38f3f..849b72d 100644 --- a/chat/admin.py +++ b/chat/admin.py @@ -1,3 +1,52 @@ from django.contrib import admin -# Register your models here. +from chat.models import Message +from chat.models import Room + + +@admin.register(Room) +class RoomAdmin(admin.ModelAdmin): + list_display = ( + "name", + "slug", + ) + search_fields = ( + "name", + "slug", + ) + filter_horizontal = ("users",) + + +@admin.register(Message) +class MessageAdmin(admin.ModelAdmin): + readonly_fields = ["created_at"] + + list_display = ( + "room", + "user", + "message", + + ) + search_fields = ( + "room", + "user", + "message", + ) + list_filter = ( + "room", + "user", + ) + + fieldsets = ( + ( + None, + { + "fields": ( + "room", + "user", + "message", + "created_at", + ) + }, + ), + ) diff --git a/chat/consumers.py b/chat/consumers.py index 9854277..491548e 100644 --- a/chat/consumers.py +++ b/chat/consumers.py @@ -12,19 +12,22 @@ async def connect(self): self.room_group_name = "chat_%s" % self.room_name self.user = self.scope["user"] - await self.channel_layer.group_add( - self.room_group_name, self.channel_name - ) + await self.channel_layer.group_add(self.room_group_name, self.channel_name) await self.add_user(self.room_name, self.user) - await self.accept() + messages = await self.get_room_messages(self.room_name) + for message in messages: + message_html = ( + f"
" + f"

{message['username']}: {message['text']}

" + ) + await self.send(text_data=json.dumps({"history": message_html})) + async def disconnect(self, close_code): await self.remove_user(self.room_name, self.user) - await self.channel_layer.group_discard( - self.room_group_name, self.channel_name - ) + await self.channel_layer.group_discard(self.room_group_name, self.channel_name) async def receive(self, text_data): text_data_json = json.loads(text_data) @@ -33,35 +36,44 @@ async def receive(self, text_data): username = user.username room = self.room_name - #await self.save_message(room, user, message) + await self.save_message(room, user, message) await self.channel_layer.group_send( - self.room_group_name, + self.room_group_name, { "type": "chat_message", "message": message, - #"room": room, + "room": room, "username": username, - } + }, ) async def chat_message(self, event): message = event["message"] - #room = event["room"] + room = event["room"] username = event["username"] - - message_html = f"

{username}: {message}

" + message_html = ( + f"
" + f"

{username}: {message}

" + ) await self.send( text_data=json.dumps( - { - "message": message_html, - #"room": room, - "username": username - } + {"message": message_html, "room": room, "username": username} ) ) + @sync_to_async + def get_room_messages(self, room_slug): + room = Room.objects.get(slug=room_slug) + messages = Message.objects.filter(room=room).order_by("-created_at")[ + :50 + ] # Adjust the number of messages as needed + return [ + {"text": message.message, "username": message.user.username} + for message in messages + ] + @sync_to_async def save_message(self, room, user, message): room = Room.objects.get(slug=room) diff --git a/chat/migrations/0001_initial.py b/chat/migrations/0001_initial.py index c0a7d55..c4931c2 100644 --- a/chat/migrations/0001_initial.py +++ b/chat/migrations/0001_initial.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [] diff --git a/chat/migrations/0002_room_users_message.py b/chat/migrations/0002_room_users_message.py index 01152de..7b31f53 100644 --- a/chat/migrations/0002_room_users_message.py +++ b/chat/migrations/0002_room_users_message.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("chat", "0001_initial"), diff --git a/chat/models.py b/chat/models.py index da87adc..0fd40f7 100644 --- a/chat/models.py +++ b/chat/models.py @@ -1,6 +1,7 @@ from django.db import models from accounts.models import User + class Room(models.Model): name = models.CharField(max_length=128) slug = models.SlugField(unique=True) @@ -17,4 +18,4 @@ class Message(models.Model): created_at = models.DateTimeField(auto_now_add=True) def __str__(self): - return (self.room.name + " - " + str(self.user.username) + " : " + str(self.message)) + return f"{self.room.name} - {str(self.user.username)} : {str(self.message)}" diff --git a/chat/routing.py b/chat/routing.py index 605437e..53db9a4 100644 --- a/chat/routing.py +++ b/chat/routing.py @@ -3,5 +3,5 @@ from chat import consumers websocket_urlpatterns = [ - path('chat//', consumers.ChatConsumer.as_asgi()), + path("chat//", consumers.ChatConsumer.as_asgi()), ] diff --git a/chat/urls.py b/chat/urls.py index 18e60d1..93422ad 100644 --- a/chat/urls.py +++ b/chat/urls.py @@ -4,8 +4,9 @@ urlpatterns = [ - path("", TemplateView.as_view(template_name="base.html"), name='index'), - path("chat/room//", views.index, name='chat'), - path("create/", views.room_create, name='room-create'), - path("join/", views.room_join, name='room-join'), + path("", TemplateView.as_view(template_name="base.html"), name="index"), + + path("chat/room//", views.index, name="chat"), + path("create/", views.room_create, name="room-create"), + path("join/", views.room_join, name="room-join"), ] diff --git a/chat/views.py b/chat/views.py index 1e10c30..fe58cd9 100644 --- a/chat/views.py +++ b/chat/views.py @@ -1,32 +1,61 @@ -import string import random +import string + from django.contrib.auth.decorators import login_required from django.shortcuts import render, reverse, redirect from django.utils.text import slugify + from chat.models import Room @login_required def index(request, slug): - room = Room.objects.get(slug=slug) - return render(request, 'chat/room.html', {'name': room.name, 'slug': room.slug}) + try: + room = Room.objects.get(slug=slug) + except Room.DoesNotExist: + return render( + request, + "chat/join.html", + {"error": "This room does not exist. Please check the name and try again."}, + ) + return render(request, "chat/room.html", {"name": room.name, "slug": room.slug}) + @login_required def room_create(request): - if request.method == "POST": - room_name = request.POST["room_name"] - uid = str(''.join(random.choices(string.ascii_letters + string.digits, k=4))) - room_slug = slugify(room_name + "_" + uid) - room = Room.objects.create(name=room_name, slug=room_slug) - return redirect(reverse('chat', kwargs={'slug': room.slug})) - else: - return render(request, 'chat/create.html') + if request.method != "POST": + return render(request, "chat/create.html") + + room_name = request.POST["room_name"] + + try: + Room.objects.get(name=room_name) + return render( + request, + "chat/create.html", + {"error": "This room already exists. Please try again."}, + ) + except Room.DoesNotExist: + pass + + uid = str("".join(random.choices(string.ascii_letters + string.digits, k=4))) + room_slug = slugify(f"{room_name}_{uid}") + room = Room.objects.create(name=room_name, slug=room_slug) + return redirect(reverse("chat", kwargs={"slug": room.slug})) + @login_required def room_join(request): - if request.method == "POST": - room_name = request.POST["room_name"] + if request.method != "POST": + return render(request, "chat/join.html") + + room_name = request.POST["room_name"] + try: room = Room.objects.get(slug=room_name) - return redirect(reverse('chat', kwargs={'slug': room.slug})) - else: - return render(request, 'chat/join.html') + except Room.DoesNotExist: + return render( + request, + "chat/join.html", + {"error": "This room does not exist. Please check the name and try again."}, + ) + return redirect(reverse("chat", kwargs={"slug": room.slug})) diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000..e09fc78 --- /dev/null +++ b/requirements.in @@ -0,0 +1,3 @@ +channels +daphne +django==5.0 diff --git a/requirements.txt b/requirements.txt index 85cacfa..19c8739 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1,76 @@ -asgiref==3.6.0 -attrs==22.2.0 -autobahn==23.1.1 -Automat==22.10.0 -cffi==1.15.1 +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile +# +asgiref==3.7.2 + # via + # channels + # daphne + # django +attrs==23.2.0 + # via + # automat + # service-identity + # twisted +autobahn==23.6.2 + # via daphne +automat==22.10.0 + # via twisted +cffi==1.16.0 + # via cryptography channels==4.0.0 -constantly==15.1.0 -cryptography==39.0.0 + # via -r requirements.in +constantly==23.10.4 + # via twisted +cryptography==41.0.7 + # via + # autobahn + # pyopenssl + # service-identity daphne==4.0.0 -Django==4.1.6 + # via -r requirements.in +django==5.0 + # via + # -r requirements.in + # channels hyperlink==21.0.0 -idna==3.4 + # via + # autobahn + # twisted +idna==3.6 + # via + # hyperlink + # twisted incremental==22.10.0 -pyasn1==0.4.8 -pyasn1-modules==0.2.8 + # via twisted +pyasn1==0.5.1 + # via + # pyasn1-modules + # service-identity +pyasn1-modules==0.3.0 + # via service-identity pycparser==2.21 -pyOpenSSL==23.0.0 -service-identity==21.1.0 + # via cffi +pyopenssl==23.3.0 + # via twisted +service-identity==23.1.0 + # via twisted six==1.16.0 + # via automat sqlparse==0.4.3 -Twisted==22.10.0 + # via django +twisted[tls]==23.10.0 + # via + # daphne + # twisted txaio==23.1.1 -typing_extensions==4.4.0 -zope.interface==5.5.2 + # via autobahn +typing-extensions==4.9.0 + # via twisted +zope-interface==6.1 + # via twisted + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/templates/chat/join.html b/templates/chat/join.html index b847a06..886243b 100644 --- a/templates/chat/join.html +++ b/templates/chat/join.html @@ -6,4 +6,7 @@ + {% if error %} +

{{ error }}

+ {% endif %} {% endblock %}