mirror of
				https://gitlab.com/animath/si/plateforme.git
				synced 2025-10-30 03:39:52 +01:00 
			
		
		
		
	Compare commits
	
		
			9 Commits
		
	
	
		
			af9dcebca9
			...
			b820306e2e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | b820306e2e | ||
|  | 756f2074b3 | ||
|  | 6385e53425 | ||
|  | 32b816a4de | ||
|  | 41ccbcb277 | ||
|  | 2438bb9bcc | ||
|  | ddd2280ae4 | ||
|  | 4958628e40 | ||
|  | 731d309305 | 
| @@ -8,8 +8,8 @@ from .models import Channel, Message | ||||
|  | ||||
| @admin.register(Channel) | ||||
| class ChannelAdmin(admin.ModelAdmin): | ||||
|     list_display = ('name', 'read_access', 'write_access', 'tournament', 'pool', 'team', 'private',) | ||||
|     list_filter = ('read_access', 'write_access', 'tournament', 'private',) | ||||
|     list_display = ('name', 'category', 'read_access', 'write_access', 'tournament', 'private',) | ||||
|     list_filter = ('category', 'read_access', 'write_access', 'tournament', 'private',) | ||||
|     search_fields = ('name', 'tournament__name', 'team__name', 'team__trigram',) | ||||
|     autocomplete_fields = ('tournament', 'pool', 'team', 'invited', ) | ||||
|  | ||||
|   | ||||
| @@ -2,8 +2,15 @@ | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.apps import AppConfig | ||||
| from django.db.models.signals import post_save | ||||
|  | ||||
|  | ||||
| class ChatConfig(AppConfig): | ||||
|     default_auto_field = "django.db.models.BigAutoField" | ||||
|     name = "chat" | ||||
|  | ||||
|     def ready(self): | ||||
|         from chat import signals | ||||
|         post_save.connect(signals.create_tournament_channels, "participation.Tournament") | ||||
|         post_save.connect(signals.create_pool_channels, "participation.Pool") | ||||
|         post_save.connect(signals.create_team_channel, "participation.Participation") | ||||
|   | ||||
| @@ -3,7 +3,6 @@ | ||||
|  | ||||
| from channels.generic.websocket import AsyncJsonWebsocketConsumer | ||||
| from django.contrib.auth.models import User | ||||
| from participation.models import Team, Pool, Tournament | ||||
| from registration.models import Registration | ||||
|  | ||||
| from .models import Channel, Message | ||||
| @@ -38,6 +37,7 @@ class ChatConsumer(AsyncJsonWebsocketConsumer): | ||||
|         channels = await Channel.get_accessible_channels(user, 'read') | ||||
|         async for channel in channels.all(): | ||||
|             await self.channel_layer.group_add(f"chat-{channel.id}", self.channel_name) | ||||
|         await self.channel_layer.group_add(f"user-{user.id}", self.channel_name) | ||||
|  | ||||
|     async def disconnect(self, close_code) -> None: | ||||
|         """ | ||||
| @@ -51,6 +51,7 @@ class ChatConsumer(AsyncJsonWebsocketConsumer): | ||||
|         channels = await Channel.get_accessible_channels(self.scope['user'], 'read') | ||||
|         async for channel in channels.all(): | ||||
|             await self.channel_layer.group_discard(f"chat-{channel.id}", self.channel_name) | ||||
|         await self.channel_layer.group_discard(f"user-{self.scope['user'].id}", self.channel_name) | ||||
|  | ||||
|     async def receive_json(self, content, **kwargs): | ||||
|         """ | ||||
| @@ -61,9 +62,15 @@ class ChatConsumer(AsyncJsonWebsocketConsumer): | ||||
|             case 'fetch_channels': | ||||
|                 await self.fetch_channels() | ||||
|             case 'send_message': | ||||
|                 await self.receive_message(content) | ||||
|                 await self.receive_message(**content) | ||||
|             case 'edit_message': | ||||
|                 await self.edit_message(**content) | ||||
|             case 'delete_message': | ||||
|                 await self.delete_message(**content) | ||||
|             case 'fetch_messages': | ||||
|                 await self.fetch_messages(**content) | ||||
|             case 'start_private_chat': | ||||
|                 await self.start_private_chat(**content) | ||||
|             case unknown: | ||||
|                 print("Unknown message type:", unknown) | ||||
|  | ||||
| @@ -77,19 +84,20 @@ class ChatConsumer(AsyncJsonWebsocketConsumer): | ||||
|             'channels': [ | ||||
|                 { | ||||
|                     'id': channel.id, | ||||
|                     'name': channel.name, | ||||
|                     'name': channel.get_visible_name(user), | ||||
|                     'category': channel.category, | ||||
|                     'read_access': True, | ||||
|                     'write_access': await write_channels.acontains(channel), | ||||
|                 } | ||||
|                 async for channel in read_channels.all() | ||||
|                 async for channel in read_channels.prefetch_related('invited').all() | ||||
|             ] | ||||
|         } | ||||
|         await self.send_json(message) | ||||
|  | ||||
|     async def receive_message(self, message: dict) -> None: | ||||
|     async def receive_message(self, channel_id: int, content: str, **kwargs) -> None: | ||||
|         user = self.scope['user'] | ||||
|         channel = await Channel.objects.prefetch_related('tournament__pools__juries', 'pool', 'team', 'invited') \ | ||||
|             .aget(id=message['channel_id']) | ||||
|             .aget(id=channel_id) | ||||
|         write_channels = await Channel.get_accessible_channels(user, 'write') | ||||
|         if not await write_channels.acontains(channel): | ||||
|             return | ||||
| @@ -97,7 +105,7 @@ class ChatConsumer(AsyncJsonWebsocketConsumer): | ||||
|         message = await Message.objects.acreate( | ||||
|             author=user, | ||||
|             channel=channel, | ||||
|             content=message['content'], | ||||
|             content=content, | ||||
|         ) | ||||
|  | ||||
|         await self.channel_layer.group_send(f'chat-{channel.id}', { | ||||
| @@ -105,10 +113,41 @@ class ChatConsumer(AsyncJsonWebsocketConsumer): | ||||
|             'id': message.id, | ||||
|             'channel_id': channel.id, | ||||
|             'timestamp': message.created_at.isoformat(), | ||||
|             'author_id': message.author_id, | ||||
|             'author': await message.aget_author_name(), | ||||
|             'content': message.content, | ||||
|         }) | ||||
|  | ||||
|     async def edit_message(self, message_id: int, content: str, **kwargs) -> None: | ||||
|         message = await Message.objects.aget(id=message_id) | ||||
|         user = self.scope['user'] | ||||
|         if user.id != message.author_id and not user.is_superuser: | ||||
|             return | ||||
|  | ||||
|         message.content = content | ||||
|         await message.asave() | ||||
|  | ||||
|         await self.channel_layer.group_send(f'chat-{message.channel_id}', { | ||||
|             'type': 'chat.edit_message', | ||||
|             'id': message_id, | ||||
|             'channel_id': message.channel_id, | ||||
|             'content': content, | ||||
|         }) | ||||
|  | ||||
|     async def delete_message(self, message_id: int, **kwargs) -> None: | ||||
|         message = await Message.objects.aget(id=message_id) | ||||
|         user = self.scope['user'] | ||||
|         if user.id != message.author_id and not user.is_superuser: | ||||
|             return | ||||
|  | ||||
|         await message.adelete() | ||||
|  | ||||
|         await self.channel_layer.group_send(f'chat-{message.channel_id}', { | ||||
|             'type': 'chat.delete_message', | ||||
|             'id': message_id, | ||||
|             'channel_id': message.channel_id, | ||||
|         }) | ||||
|  | ||||
|     async def fetch_messages(self, channel_id: int, offset: int = 0, limit: int = 50, **_kwargs) -> None: | ||||
|         channel = await Channel.objects.aget(id=channel_id) | ||||
|         read_channels = await Channel.get_accessible_channels(self.scope['user'], 'read') | ||||
| @@ -125,6 +164,7 @@ class ChatConsumer(AsyncJsonWebsocketConsumer): | ||||
|                 { | ||||
|                     'id': message.id, | ||||
|                     'timestamp': message.created_at.isoformat(), | ||||
|                     'author_id': message.author_id, | ||||
|                     'author': await message.aget_author_name(), | ||||
|                     'content': message.content, | ||||
|                 } | ||||
| @@ -132,7 +172,59 @@ class ChatConsumer(AsyncJsonWebsocketConsumer): | ||||
|             ])) | ||||
|         }) | ||||
|  | ||||
|     async def start_private_chat(self, user_id: int, **kwargs) -> None: | ||||
|         user = self.scope['user'] | ||||
|         other_user = await User.objects.aget(id=user_id) | ||||
|         channel_qs = Channel.objects.filter(private=True).filter(invited=user).filter(invited=other_user) | ||||
|         if not await channel_qs.aexists(): | ||||
|             channel = await Channel.objects.acreate( | ||||
|                 name=f"{user.first_name} {user.last_name}, {other_user.first_name} {other_user.last_name}", | ||||
|                 category=Channel.ChannelCategory.PRIVATE, | ||||
|                 private=True, | ||||
|             ) | ||||
|             await channel.invited.aset([user, other_user]) | ||||
|             await channel.asave() | ||||
|  | ||||
|             await self.channel_layer.group_add(f"chat-{channel.id}", self.channel_name) | ||||
|  | ||||
|             if user != other_user: | ||||
|                 await self.channel_layer.group_send(f"user-{other_user.id}", { | ||||
|                     'type': 'chat.start_private_chat', | ||||
|                     'channel': { | ||||
|                         'id': channel.id, | ||||
|                         'name': f"{user.first_name} {user.last_name}", | ||||
|                         'category': channel.category, | ||||
|                         'read_access': True, | ||||
|                         'write_access': True, | ||||
|                     } | ||||
|                 }) | ||||
|         else: | ||||
|             channel = await channel_qs.afirst() | ||||
|  | ||||
|         await self.channel_layer.group_send(f"user-{user.id}", { | ||||
|             'type': 'chat.start_private_chat', | ||||
|             'channel': { | ||||
|                 'id': channel.id, | ||||
|                 'name': f"{other_user.first_name} {other_user.last_name}", | ||||
|                 'category': channel.category, | ||||
|                 'read_access': True, | ||||
|                 'write_access': True, | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|     async def chat_send_message(self, message) -> None: | ||||
|         await self.send_json({'type': 'send_message', 'id': message['id'], 'channel_id': message['channel_id'], | ||||
|                               'timestamp': message['timestamp'], 'author': message['author'], | ||||
|                               'content': message['content']}) | ||||
|  | ||||
|     async def chat_edit_message(self, message) -> None: | ||||
|         print(message) | ||||
|         await self.send_json({'type': 'edit_message', 'id': message['id'], 'channel_id': message['channel_id'], | ||||
|                               'content': message['content']}) | ||||
|  | ||||
|     async def chat_delete_message(self, message) -> None: | ||||
|         await self.send_json({'type': 'delete_message', 'id': message['id'], 'channel_id': message['channel_id']}) | ||||
|  | ||||
|     async def chat_start_private_chat(self, message) -> None: | ||||
|         await self.channel_layer.group_add(f"chat-{message['channel']['id']}", self.channel_name) | ||||
|         await self.send_json({'type': 'start_private_chat', 'channel': message['channel']}) | ||||
|   | ||||
							
								
								
									
										0
									
								
								chat/management/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								chat/management/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								chat/management/commands/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								chat/management/commands/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										143
									
								
								chat/management/commands/create_chat_channels.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								chat/management/commands/create_chat_channels.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | ||||
| # Copyright (C) 2024 by Animath | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.core.management import BaseCommand | ||||
| from django.utils.translation import activate | ||||
| from participation.models import Team, Tournament | ||||
| from tfjm.permissions import PermissionType | ||||
|  | ||||
| from ...models import Channel | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     def handle(self, *args, **kwargs): | ||||
|         activate('fr') | ||||
|  | ||||
|         Channel.objects.update_or_create( | ||||
|             name="Annonces", | ||||
|             defaults=dict( | ||||
|                 category=Channel.ChannelCategory.GENERAL, | ||||
|                 read_access=PermissionType.AUTHENTICATED, | ||||
|                 write_access=PermissionType.ADMIN, | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|         Channel.objects.update_or_create( | ||||
|             name="Aide jurys et orgas", | ||||
|             defaults=dict( | ||||
|                 category=Channel.ChannelCategory.GENERAL, | ||||
|                 read_access=PermissionType.VOLUNTEER, | ||||
|                 write_access=PermissionType.VOLUNTEER, | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|         Channel.objects.update_or_create( | ||||
|             name="Général", | ||||
|             defaults=dict( | ||||
|                 category=Channel.ChannelCategory.GENERAL, | ||||
|                 read_access=PermissionType.AUTHENTICATED, | ||||
|                 write_access=PermissionType.AUTHENTICATED, | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|         Channel.objects.update_or_create( | ||||
|             name="Je cherche une équipe", | ||||
|             defaults=dict( | ||||
|                 category=Channel.ChannelCategory.GENERAL, | ||||
|                 read_access=PermissionType.AUTHENTICATED, | ||||
|                 write_access=PermissionType.AUTHENTICATED, | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|         Channel.objects.update_or_create( | ||||
|             name="Détente", | ||||
|             defaults=dict( | ||||
|                 category=Channel.ChannelCategory.GENERAL, | ||||
|                 read_access=PermissionType.AUTHENTICATED, | ||||
|                 write_access=PermissionType.AUTHENTICATED, | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|         for tournament in Tournament.objects.all(): | ||||
|             Channel.objects.update_or_create( | ||||
|                 name=f"{tournament.name} - Annonces", | ||||
|                 defaults=dict( | ||||
|                     category=Channel.ChannelCategory.TOURNAMENT, | ||||
|                     read_access=PermissionType.TOURNAMENT_MEMBER, | ||||
|                     write_access=PermissionType.TOURNAMENT_ORGANIZER, | ||||
|                     tournament=tournament, | ||||
|                 ), | ||||
|             ) | ||||
|  | ||||
|             Channel.objects.update_or_create( | ||||
|                 name=f"{tournament.name} - Général", | ||||
|                 defaults=dict( | ||||
|                     category=Channel.ChannelCategory.TOURNAMENT, | ||||
|                     read_access=PermissionType.TOURNAMENT_MEMBER, | ||||
|                     write_access=PermissionType.TOURNAMENT_MEMBER, | ||||
|                     tournament=tournament, | ||||
|                 ), | ||||
|             ) | ||||
|  | ||||
|             Channel.objects.update_or_create( | ||||
|                 name=f"{tournament.name} - Détente", | ||||
|                 defaults=dict( | ||||
|                     category=Channel.ChannelCategory.TOURNAMENT, | ||||
|                     read_access=PermissionType.TOURNAMENT_MEMBER, | ||||
|                     write_access=PermissionType.TOURNAMENT_MEMBER, | ||||
|                     tournament=tournament, | ||||
|                 ), | ||||
|             ) | ||||
|  | ||||
|             Channel.objects.update_or_create( | ||||
|                 name=f"{tournament.name} - Juré⋅es", | ||||
|                 defaults=dict( | ||||
|                     category=Channel.ChannelCategory.TOURNAMENT, | ||||
|                     read_access=PermissionType.JURY_MEMBER, | ||||
|                     write_access=PermissionType.JURY_MEMBER, | ||||
|                     tournament=tournament, | ||||
|                 ), | ||||
|             ) | ||||
|  | ||||
|             if tournament.remote: | ||||
|                 Channel.objects.update_or_create( | ||||
|                     name=f"{tournament.name} - Président⋅es de jury", | ||||
|                     defaults=dict( | ||||
|                         category=Channel.ChannelCategory.TOURNAMENT, | ||||
|                         read_access=PermissionType.TOURNAMENT_JURY_PRESIDENT, | ||||
|                         write_access=PermissionType.TOURNAMENT_JURY_PRESIDENT, | ||||
|                         tournament=tournament, | ||||
|                     ), | ||||
|                 ) | ||||
|  | ||||
|                 for pool in tournament.pools.all(): | ||||
|                     Channel.objects.update_or_create( | ||||
|                         name=f"{tournament.name} - Poule {pool.short_name}", | ||||
|                         defaults=dict( | ||||
|                             category=Channel.ChannelCategory.TOURNAMENT, | ||||
|                             read_access=PermissionType.POOL_MEMBER, | ||||
|                             write_access=PermissionType.POOL_MEMBER, | ||||
|                             pool=pool, | ||||
|                         ), | ||||
|                     ) | ||||
|  | ||||
|                     Channel.objects.update_or_create( | ||||
|                         name=f"{tournament.name} - Poule {pool.short_name} - Jury", | ||||
|                         defaults=dict( | ||||
|                             category=Channel.ChannelCategory.TOURNAMENT, | ||||
|                             read_access=PermissionType.JURY_MEMBER, | ||||
|                             write_access=PermissionType.JURY_MEMBER, | ||||
|                             pool=pool, | ||||
|                         ), | ||||
|                     ) | ||||
|  | ||||
|         for team in Team.objects.filter(participation__valid=True).all(): | ||||
|             Channel.objects.update_or_create( | ||||
|                 name=f"Équipe {team.trigram}", | ||||
|                 defaults=dict( | ||||
|                     category=Channel.ChannelCategory.TEAM, | ||||
|                     read_access=PermissionType.TEAM_MEMBER, | ||||
|                     write_access=PermissionType.TEAM_MEMBER, | ||||
|                     team=team, | ||||
|                 ), | ||||
|             ) | ||||
| @@ -0,0 +1,36 @@ | ||||
| # Generated by Django 5.0.3 on 2024-04-28 11:08 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("chat", "0001_initial"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterModelOptions( | ||||
|             name="channel", | ||||
|             options={ | ||||
|                 "ordering": ("category", "name"), | ||||
|                 "verbose_name": "channel", | ||||
|                 "verbose_name_plural": "channels", | ||||
|             }, | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="channel", | ||||
|             name="category", | ||||
|             field=models.CharField( | ||||
|                 choices=[ | ||||
|                     ("general", "General channels"), | ||||
|                     ("tournament", "Tournament channels"), | ||||
|                     ("team", "Team channels"), | ||||
|                     ("private", "Private channels"), | ||||
|                 ], | ||||
|                 default="general", | ||||
|                 max_length=255, | ||||
|                 verbose_name="category", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @@ -13,11 +13,24 @@ from tfjm.permissions import PermissionType | ||||
|  | ||||
|  | ||||
| class Channel(models.Model): | ||||
|     class ChannelCategory(models.TextChoices): | ||||
|         GENERAL = 'general', _("General channels") | ||||
|         TOURNAMENT = 'tournament', _("Tournament channels") | ||||
|         TEAM = 'team', _("Team channels") | ||||
|         PRIVATE = 'private', _("Private channels") | ||||
|  | ||||
|     name = models.CharField( | ||||
|         max_length=255, | ||||
|         verbose_name=_("name"), | ||||
|     ) | ||||
|  | ||||
|     category = models.CharField( | ||||
|         max_length=255, | ||||
|         verbose_name=_("category"), | ||||
|         choices=ChannelCategory, | ||||
|         default=ChannelCategory.GENERAL, | ||||
|     ) | ||||
|  | ||||
|     read_access = models.CharField( | ||||
|         max_length=16, | ||||
|         verbose_name=_("read permission"), | ||||
| @@ -78,6 +91,13 @@ class Channel(models.Model): | ||||
|                     "in addition to the permitted group of the channel."), | ||||
|     ) | ||||
|  | ||||
|     def get_visible_name(self, user: User) -> str: | ||||
|         if self.private: | ||||
|             users = [f"{u.first_name} {u.last_name}" for u in self.invited.all() if u != user] \ | ||||
|                     or [f"{user.first_name} {user.last_name}"] | ||||
|             return ", ".join(users) | ||||
|         return self.name | ||||
|  | ||||
|     def __str__(self): | ||||
|         return str(format_lazy(_("Channel {name}"), name=self.name)) | ||||
|  | ||||
| @@ -90,10 +110,10 @@ class Channel(models.Model): | ||||
|             return Channel.objects.filter(**{permission_type: PermissionType.ANONYMOUS}) | ||||
|  | ||||
|         qs |= Channel.objects.filter(**{permission_type: PermissionType.AUTHENTICATED}) | ||||
|         registration = await Registration.objects.aget(user_id=user.id) | ||||
|         registration = await Registration.objects.prefetch_related('user').aget(user_id=user.id) | ||||
|  | ||||
|         if registration.is_admin: | ||||
|             return Channel.objects.all() | ||||
|             return Channel.objects.prefetch_related('invited').exclude(~Q(invited=user) & Q(private=True)).all() | ||||
|  | ||||
|         if registration.is_volunteer: | ||||
|             registration = await VolunteerRegistration.objects \ | ||||
| @@ -140,14 +160,14 @@ class Channel(models.Model): | ||||
|             qs |= Channel.objects.filter(Q(team=team), | ||||
|                                          **{permission_type: PermissionType.TEAM_MEMBER}) | ||||
|  | ||||
|         qs |= Channel.objects.filter(invited=user) | ||||
|         qs |= Channel.objects.filter(invited=user).prefetch_related('invited') | ||||
|  | ||||
|         return qs | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("channel") | ||||
|         verbose_name_plural = _("channels") | ||||
|         ordering = ('name',) | ||||
|         ordering = ('category', 'name',) | ||||
|  | ||||
|  | ||||
| class Message(models.Model): | ||||
|   | ||||
							
								
								
									
										100
									
								
								chat/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								chat/signals.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| # Copyright (C) 2024 by Animath | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from chat.models import Channel | ||||
| from participation.models import Participation, Pool, Tournament | ||||
| from tfjm.permissions import PermissionType | ||||
|  | ||||
|  | ||||
| def create_tournament_channels(instance: Tournament, **_kwargs): | ||||
|     tournament = instance | ||||
|  | ||||
|     Channel.objects.update_or_create( | ||||
|         name=f"{tournament.name} - Annonces", | ||||
|         defaults=dict( | ||||
|             category=Channel.ChannelCategory.TOURNAMENT, | ||||
|             read_access=PermissionType.TOURNAMENT_MEMBER, | ||||
|             write_access=PermissionType.TOURNAMENT_ORGANIZER, | ||||
|             tournament=tournament, | ||||
|         ), | ||||
|     ) | ||||
|  | ||||
|     Channel.objects.update_or_create( | ||||
|         name=f"{tournament.name} - Général", | ||||
|         defaults=dict( | ||||
|             category=Channel.ChannelCategory.TOURNAMENT, | ||||
|             read_access=PermissionType.TOURNAMENT_MEMBER, | ||||
|             write_access=PermissionType.TOURNAMENT_MEMBER, | ||||
|             tournament=tournament, | ||||
|         ), | ||||
|     ) | ||||
|  | ||||
|     Channel.objects.update_or_create( | ||||
|         name=f"{tournament.name} - Détente", | ||||
|         defaults=dict( | ||||
|             category=Channel.ChannelCategory.TOURNAMENT, | ||||
|             read_access=PermissionType.TOURNAMENT_MEMBER, | ||||
|             write_access=PermissionType.TOURNAMENT_MEMBER, | ||||
|             tournament=tournament, | ||||
|         ), | ||||
|     ) | ||||
|  | ||||
|     Channel.objects.update_or_create( | ||||
|         name=f"{tournament.name} - Juré⋅es", | ||||
|         defaults=dict( | ||||
|             category=Channel.ChannelCategory.TOURNAMENT, | ||||
|             read_access=PermissionType.JURY_MEMBER, | ||||
|             write_access=PermissionType.JURY_MEMBER, | ||||
|             tournament=tournament, | ||||
|         ), | ||||
|     ) | ||||
|  | ||||
|     if tournament.remote: | ||||
|         Channel.objects.update_or_create( | ||||
|             name=f"{tournament.name} - Président⋅es de jury", | ||||
|             defaults=dict( | ||||
|                 category=Channel.ChannelCategory.TOURNAMENT, | ||||
|                 read_access=PermissionType.TOURNAMENT_JURY_PRESIDENT, | ||||
|                 write_access=PermissionType.TOURNAMENT_JURY_PRESIDENT, | ||||
|                 tournament=tournament, | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|  | ||||
| def create_pool_channels(instance: Pool, **_kwargs): | ||||
|     pool = instance | ||||
|     tournament = pool.tournament | ||||
|  | ||||
|     if tournament.remote: | ||||
|         Channel.objects.update_or_create( | ||||
|             name=f"{tournament.name} - Poule {pool.short_name}", | ||||
|             defaults=dict( | ||||
|                 category=Channel.ChannelCategory.TOURNAMENT, | ||||
|                 read_access=PermissionType.POOL_MEMBER, | ||||
|                 write_access=PermissionType.POOL_MEMBER, | ||||
|                 pool=pool, | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|         Channel.objects.update_or_create( | ||||
|             name=f"{tournament.name} - Poule {pool.short_name} - Jury", | ||||
|             defaults=dict( | ||||
|                 category=Channel.ChannelCategory.TOURNAMENT, | ||||
|                 read_access=PermissionType.JURY_MEMBER, | ||||
|                 write_access=PermissionType.JURY_MEMBER, | ||||
|                 pool=pool, | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|  | ||||
| def create_team_channel(instance: Participation, **_kwargs): | ||||
|     if instance.valid: | ||||
|         Channel.objects.update_or_create( | ||||
|             name=f"Équipe {instance.team.trigram}", | ||||
|             defaults=dict( | ||||
|                 category=Channel.ChannelCategory.TEAM, | ||||
|                 read_access=PermissionType.TEAM_MEMBER, | ||||
|                 write_access=PermissionType.TEAM_MEMBER, | ||||
|                 team=instance.team, | ||||
|             ), | ||||
|         ) | ||||
| @@ -6,6 +6,7 @@ | ||||
|  | ||||
| const MAX_MESSAGES = 50 | ||||
|  | ||||
| const channel_categories = ['general', 'tournament', 'team', 'private'] | ||||
| let channels = {} | ||||
| let messages = {} | ||||
| let selected_channel_id = null | ||||
| @@ -62,20 +63,27 @@ function sendMessage() { | ||||
|  | ||||
| function setChannels(new_channels) { | ||||
|     channels = {} | ||||
|     let navTab = document.getElementById('nav-channels-tab') | ||||
|     navTab.innerHTML = '' | ||||
|     let categoryLists = {} | ||||
|     for (let category of channel_categories) { | ||||
|         categoryLists[category] = document.getElementById(`nav-${category}-channels-tab`) | ||||
|         categoryLists[category].innerHTML = '' | ||||
|         categoryLists[category].parentElement.classList.add('d-none') | ||||
|     } | ||||
|  | ||||
|     for (let channel of new_channels) { | ||||
|         channels[channel['id']] = channel | ||||
|         if (!messages[channel['id']]) | ||||
|             messages[channel['id']] = new Map() | ||||
|  | ||||
|         let categoryList = categoryLists[channel['category']] | ||||
|         categoryList.parentElement.classList.remove('d-none') | ||||
|  | ||||
|         let navItem = document.createElement('li') | ||||
|         navItem.classList.add('list-group-item') | ||||
|         navItem.id = `tab-channel-${channel['id']}` | ||||
|         navItem.setAttribute('data-bs-dismiss', 'offcanvas') | ||||
|         navItem.onclick = () => selectChannel(channel['id']) | ||||
|         navTab.appendChild(navItem) | ||||
|         categoryList.appendChild(navItem) | ||||
|  | ||||
|         let channelButton = document.createElement('button') | ||||
|         channelButton.classList.add('nav-link') | ||||
| @@ -87,8 +95,13 @@ function setChannels(new_channels) { | ||||
|     } | ||||
|  | ||||
|     if (new_channels && (!selected_channel_id || !channels[selected_channel_id])) { | ||||
|         if (window.location.hash) | ||||
|             selectChannel(window.location.hash.substring(9)) | ||||
|         if (window.location.hash) { | ||||
|             let channel_id = parseInt(window.location.hash.substring(9)) | ||||
|             if (channels[channel_id]) | ||||
|                 selectChannel(channel_id) | ||||
|             else | ||||
|                 selectChannel(Object.keys(channels)[0]) | ||||
|         } | ||||
|         else | ||||
|             selectChannel(Object.keys(channels)[0]) | ||||
|     } | ||||
| @@ -109,6 +122,16 @@ function receiveMessage(message) { | ||||
|         showNotification(channels[message['channel_id']]['name'], `${message['author']} : ${message['content']}`) | ||||
| } | ||||
|  | ||||
| function editMessage(data) { | ||||
|     messages[data['channel_id']].get(data['id'])['content'] = data['content'] | ||||
|     redrawMessages() | ||||
| } | ||||
|  | ||||
| function deleteMessage(data) { | ||||
|     messages[data['channel_id']].delete(data['id']) | ||||
|     redrawMessages() | ||||
| } | ||||
|  | ||||
| function fetchMessages(channel_id, offset = 0, limit = MAX_MESSAGES) { | ||||
|     socket.send(JSON.stringify({ | ||||
|         'type': 'fetch_messages', | ||||
| @@ -142,6 +165,22 @@ function receiveFetchedMessages(data) { | ||||
|     redrawMessages() | ||||
| } | ||||
|  | ||||
| function startPrivateChat(data) { | ||||
|     let channel = data['channel'] | ||||
|     if (!channel) { | ||||
|         console.error('Private chat not found:', data) | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     if (!channels[channel['id']]) { | ||||
|         channels[channel['id']] = channel | ||||
|         messages[channel['id']] = new Map() | ||||
|         setChannels(Object.values(channels)) | ||||
|     } | ||||
|  | ||||
|     selectChannel(channel['id']) | ||||
| } | ||||
|  | ||||
| function redrawMessages() { | ||||
|     let messageList = document.getElementById('message-list') | ||||
|     messageList.innerHTML = '' | ||||
| @@ -155,8 +194,12 @@ function redrawMessages() { | ||||
|             let newTimestamp = new Date(message['timestamp']) | ||||
|             if ((newTimestamp - lastTimestamp) / 1000 < 60 * 10) { | ||||
|                 let messageContentDiv = document.createElement('div') | ||||
|                 messageContentDiv.innerText = message['content'] | ||||
|                 lastContentDiv.appendChild(messageContentDiv) | ||||
|                 let messageContentSpan = document.createElement('span') | ||||
|                 messageContentSpan.innerText = message['content'] | ||||
|                 messageContentDiv.appendChild(messageContentSpan) | ||||
|  | ||||
|                 registerMessageContextMenu(message, messageContentSpan) | ||||
|                 continue | ||||
|             } | ||||
|         } | ||||
| @@ -173,6 +216,8 @@ function redrawMessages() { | ||||
|         authorSpan.innerText = message['author'] | ||||
|         authorDiv.appendChild(authorSpan) | ||||
|  | ||||
|         registerSendPrivateMessageContextMenu(message, authorSpan) | ||||
|  | ||||
|         let dateSpan = document.createElement('span') | ||||
|         dateSpan.classList.add('text-muted', 'float-end') | ||||
|         dateSpan.innerText = new Date(message['timestamp']).toLocaleString() | ||||
| @@ -182,8 +227,12 @@ function redrawMessages() { | ||||
|         messageElement.appendChild(contentDiv) | ||||
|  | ||||
|         let messageContentDiv = document.createElement('div') | ||||
|         messageContentDiv.innerText = message['content'] | ||||
|         contentDiv.appendChild(messageContentDiv) | ||||
|         let messageContentSpan = document.createElement('span') | ||||
|         messageContentSpan.innerText = message['content'] | ||||
|         messageContentDiv.appendChild(messageContentSpan) | ||||
|  | ||||
|         registerMessageContextMenu(message, messageContentSpan) | ||||
|  | ||||
|         lastMessage = message | ||||
|         lastContentDiv = contentDiv | ||||
| @@ -196,6 +245,93 @@ function redrawMessages() { | ||||
|         fetchMoreButton.classList.remove('d-none') | ||||
| } | ||||
|  | ||||
| function removeAllPopovers() { | ||||
|     for (let popover of document.querySelectorAll('*[aria-describedby*="popover"]')) { | ||||
|         let instance = bootstrap.Popover.getInstance(popover) | ||||
|         if (instance) | ||||
|             instance.dispose() | ||||
|     } | ||||
| } | ||||
|  | ||||
| function registerSendPrivateMessageContextMenu(message, element) { | ||||
|     element.addEventListener('contextmenu', (menu_event) => { | ||||
|         menu_event.preventDefault() | ||||
|         removeAllPopovers() | ||||
|         const popover = bootstrap.Popover.getOrCreateInstance(element, { | ||||
|             'title': message['author'], | ||||
|             'content': `<a id="send-private-message-link-${message['id']}" class="nav-link" href="#" tabindex="0">Envoyer un message privé</a>`, | ||||
|             'html': true, | ||||
|         }) | ||||
|         popover.show() | ||||
|  | ||||
|         document.getElementById('send-private-message-link-' + message['id']).addEventListener('click', event => { | ||||
|             event.preventDefault() | ||||
|             popover.dispose() | ||||
|             socket.send(JSON.stringify({ | ||||
|                 'type': 'start_private_chat', | ||||
|                 'user_id': message['author_id'], | ||||
|             })) | ||||
|         }) | ||||
|     }) | ||||
| } | ||||
|  | ||||
| function registerMessageContextMenu(message, element) { | ||||
|     element.addEventListener('contextmenu', (menu_event) => { | ||||
|         menu_event.preventDefault() | ||||
|         removeAllPopovers() | ||||
|         let content = `<a id="send-private-message-link-msg-${message['id']}" class="nav-link" href="#" tabindex="0">Envoyer un message privé</a>` | ||||
|  | ||||
|         let has_right_to_edit = message['author_id'] === USER_ID || IS_ADMIN | ||||
|         if (has_right_to_edit) { | ||||
|             content += `<hr class="my-1">` | ||||
|             content += `<a id="edit-message-${message['id']}" class="nav-link" href="#" tabindex="0">Modifier</a>` | ||||
|             content += `<a id="delete-message-${message['id']}" class="nav-link" href="#" tabindex="0">Supprimer</a>` | ||||
|         } | ||||
|  | ||||
|         const popover = bootstrap.Popover.getOrCreateInstance(element, { | ||||
|             'content': content, | ||||
|             'html': true, | ||||
|             'placement': 'bottom', | ||||
|         }) | ||||
|         popover.show() | ||||
|  | ||||
|         document.getElementById('send-private-message-link-msg-' + message['id']).addEventListener('click', event => { | ||||
|             event.preventDefault() | ||||
|             popover.dispose() | ||||
|             socket.send(JSON.stringify({ | ||||
|                 'type': 'start_private_chat', | ||||
|                 'user_id': message['author_id'], | ||||
|             })) | ||||
|         }) | ||||
|  | ||||
|         if (has_right_to_edit) { | ||||
|             document.getElementById('edit-message-' + message['id']).addEventListener('click', event => { | ||||
|                 event.preventDefault() | ||||
|                 popover.dispose() | ||||
|                 let new_message = prompt("Modifier le message", message['content']) | ||||
|                 if (new_message) { | ||||
|                     socket.send(JSON.stringify({ | ||||
|                         'type': 'edit_message', | ||||
|                         'message_id': message['id'], | ||||
|                         'content': new_message, | ||||
|                     })) | ||||
|                 } | ||||
|             }) | ||||
|  | ||||
|             document.getElementById('delete-message-' + message['id']).addEventListener('click', event => { | ||||
|                 event.preventDefault() | ||||
|                 popover.dispose() | ||||
|                 if (confirm("Supprimer le message ?")) { | ||||
|                     socket.send(JSON.stringify({ | ||||
|                         'type': 'delete_message', | ||||
|                         'message_id': message['id'], | ||||
|                     })) | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
|     }) | ||||
| } | ||||
|  | ||||
| function toggleFullscreen() { | ||||
|     let chatContainer = document.getElementById('chat-container') | ||||
|     if (!chatContainer.getAttribute('data-fullscreen')) { | ||||
| @@ -211,6 +347,8 @@ function toggleFullscreen() { | ||||
| } | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     document.addEventListener('click', removeAllPopovers) | ||||
|  | ||||
|     /** | ||||
|      * Process the received data from the server. | ||||
|      * @param data The received message | ||||
| @@ -223,9 +361,18 @@ document.addEventListener('DOMContentLoaded', () => { | ||||
|             case 'send_message': | ||||
|                 receiveMessage(data) | ||||
|                 break | ||||
|             case 'edit_message': | ||||
|                 editMessage(data) | ||||
|                 break | ||||
|             case 'delete_message': | ||||
|                 deleteMessage(data) | ||||
|                 break | ||||
|             case 'fetch_messages': | ||||
|                 receiveFetchedMessages(data) | ||||
|                 break | ||||
|             case 'start_private_chat': | ||||
|                 startPrivateChat(data) | ||||
|                 break | ||||
|             default: | ||||
|                 console.log(data) | ||||
|                 console.error('Unknown message type:', data['type']) | ||||
| @@ -238,6 +385,7 @@ document.addEventListener('DOMContentLoaded', () => { | ||||
|         socket = new WebSocket( | ||||
|             (document.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/ws/chat/' | ||||
|         ) | ||||
|         let socketOpen = false | ||||
|  | ||||
|         // Listen on websockets and process messages from the server | ||||
|         socket.addEventListener('message', e => { | ||||
| @@ -250,10 +398,11 @@ document.addEventListener('DOMContentLoaded', () => { | ||||
|         // Manage errors | ||||
|         socket.addEventListener('close', e => { | ||||
|             console.error('Chat socket closed unexpectedly, restarting…') | ||||
|             setTimeout(() => setupSocket(2 * nextDelay), nextDelay) | ||||
|             setTimeout(() => setupSocket(socketOpen ? 1000 : 2 * nextDelay), nextDelay) | ||||
|         }) | ||||
|  | ||||
|         socket.addEventListener('open', e => { | ||||
|             socketOpen = true | ||||
|             socket.send(JSON.stringify({ | ||||
|                 'type': 'fetch_channels', | ||||
|             })) | ||||
|   | ||||
| @@ -3,13 +3,30 @@ | ||||
| <noscript> | ||||
|     {% trans "JavaScript must be enabled on your browser to access chat." %} | ||||
| </noscript> | ||||
| <div class="offcanvas offcanvas-start" tabindex="-1" id="channelSelector" aria-labelledby="offcanvasExampleLabel"> | ||||
| <div class="offcanvas offcanvas-start" tabindex="-1" id="channelSelector" aria-labelledby="offcanvasTitle"> | ||||
|     <div class="offcanvas-header"> | ||||
|         <h4 class="offcanvas-title" id="offcanvasExampleLabel">{% trans "Chat channels" %}</h4> | ||||
|         <h3 class="offcanvas-title" id="offcanvasTitle">{% trans "Chat channels" %}</h3> | ||||
|         <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button> | ||||
|     </div> | ||||
|     <div class="offcanvas-body"> | ||||
|         <ul class="list-group list-group-flush" id="nav-channels-tab"></ul> | ||||
|         <ul class="list-group list-group-flush" id="nav-channels-tab"> | ||||
|             <li class="list-group-item d-none"> | ||||
|                 <h4>{% trans "General channels" %}</h4> | ||||
|                 <ul class="list-group list-group-flush" id="nav-general-channels-tab"></ul> | ||||
|             </li> | ||||
|             <li class="list-group-item d-none"> | ||||
|                 <h4>{% trans "Tournament channels" %}</h4> | ||||
|                 <ul class="list-group list-group-flush" id="nav-tournament-channels-tab"></ul> | ||||
|             </li> | ||||
|             <li class="list-group-item d-none"> | ||||
|                 <h4>{% trans "Team channels" %}</h4> | ||||
|                 <ul class="list-group list-group-flush" id="nav-team-channels-tab"></ul> | ||||
|             </li> | ||||
|             <li class="list-group-item d-none"> | ||||
|                 <h4>{% trans "Private channels" %}</h4> | ||||
|                 <ul class="list-group list-group-flush" id="nav-private-channels-tab"></ul> | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| @@ -70,4 +87,9 @@ | ||||
|             </div> | ||||
|         </form> | ||||
|     </div> | ||||
| </div> | ||||
| </div> | ||||
|  | ||||
| <script> | ||||
|     const USER_ID = {{ request.user.id }} | ||||
|     const IS_ADMIN = {{ request.user.registration.is_admin|yesno:"true,false" }} | ||||
| </script> | ||||
| @@ -7,7 +7,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: TFJM\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2024-04-28 11:56+0200\n" | ||||
| "POT-Creation-Date: 2024-04-28 13:08+0200\n" | ||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||
| "Last-Translator: Emmy D'Anello <emmy.danello@animath.fr>\n" | ||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | ||||
| @@ -21,20 +21,40 @@ msgstr "" | ||||
| msgid "API" | ||||
| msgstr "API" | ||||
|  | ||||
| #: chat/models.py:18 participation/models.py:35 participation/models.py:263 | ||||
| #: chat/models.py:17 | ||||
| msgid "General channels" | ||||
| msgstr "Canaux généraux" | ||||
|  | ||||
| #: chat/models.py:18 | ||||
| msgid "Tournament channels" | ||||
| msgstr "Canaux de tournois" | ||||
|  | ||||
| #: chat/models.py:19 | ||||
| msgid "Team channels" | ||||
| msgstr "Canaux d'équipes" | ||||
|  | ||||
| #: chat/models.py:20 | ||||
| msgid "Private channels" | ||||
| msgstr "Messages privés" | ||||
|  | ||||
| #: chat/models.py:24 participation/models.py:35 participation/models.py:263 | ||||
| #: participation/tables.py:18 participation/tables.py:34 | ||||
| msgid "name" | ||||
| msgstr "nom" | ||||
|  | ||||
| #: chat/models.py:23 | ||||
| #: chat/models.py:29 | ||||
| msgid "category" | ||||
| msgstr "catégorie" | ||||
|  | ||||
| #: chat/models.py:36 | ||||
| msgid "read permission" | ||||
| msgstr "permission de lecture" | ||||
|  | ||||
| #: chat/models.py:29 | ||||
| #: chat/models.py:42 | ||||
| msgid "write permission" | ||||
| msgstr "permission d'écriture" | ||||
|  | ||||
| #: chat/models.py:39 draw/admin.py:53 draw/admin.py:71 draw/admin.py:88 | ||||
| #: chat/models.py:52 draw/admin.py:53 draw/admin.py:71 draw/admin.py:88 | ||||
| #: draw/models.py:26 participation/admin.py:79 participation/admin.py:140 | ||||
| #: participation/admin.py:171 participation/models.py:693 | ||||
| #: participation/models.py:717 participation/models.py:935 | ||||
| @@ -43,7 +63,7 @@ msgstr "permission d'écriture" | ||||
| msgid "tournament" | ||||
| msgstr "tournoi" | ||||
|  | ||||
| #: chat/models.py:41 | ||||
| #: chat/models.py:54 | ||||
| msgid "" | ||||
| "For a permission that concerns a tournament, indicates what is the concerned " | ||||
| "tournament." | ||||
| @@ -51,21 +71,21 @@ msgstr "" | ||||
| "Pour une permission qui concerne un tournoi, indique quel est le tournoi " | ||||
| "concerné." | ||||
|  | ||||
| #: chat/models.py:50 draw/models.py:429 draw/models.py:456 | ||||
| #: chat/models.py:63 draw/models.py:429 draw/models.py:456 | ||||
| #: participation/admin.py:136 participation/admin.py:155 | ||||
| #: participation/models.py:1434 participation/models.py:1443 | ||||
| #: participation/tables.py:84 | ||||
| msgid "pool" | ||||
| msgstr "poule" | ||||
|  | ||||
| #: chat/models.py:52 | ||||
| #: chat/models.py:65 | ||||
| msgid "" | ||||
| "For a permission that concerns a pool, indicates what is the concerned pool." | ||||
| msgstr "" | ||||
| "Pour une permission qui concerne une poule, indique quelle est la poule " | ||||
| "concernée." | ||||
|  | ||||
| #: chat/models.py:61 draw/templates/draw/tournament_content.html:277 | ||||
| #: chat/models.py:74 draw/templates/draw/tournament_content.html:277 | ||||
| #: participation/admin.py:167 participation/models.py:252 | ||||
| #: participation/models.py:708 | ||||
| #: participation/templates/participation/tournament_harmonize.html:15 | ||||
| @@ -75,18 +95,18 @@ msgstr "" | ||||
| msgid "team" | ||||
| msgstr "équipe" | ||||
|  | ||||
| #: chat/models.py:63 | ||||
| #: chat/models.py:76 | ||||
| msgid "" | ||||
| "For a permission that concerns a team, indicates what is the concerned team." | ||||
| msgstr "" | ||||
| "Pour une permission qui concerne une équipe, indique quelle est l'équipe " | ||||
| "concernée." | ||||
|  | ||||
| #: chat/models.py:67 | ||||
| #: chat/models.py:80 | ||||
| msgid "private" | ||||
| msgstr "privé" | ||||
|  | ||||
| #: chat/models.py:69 | ||||
| #: chat/models.py:82 | ||||
| msgid "" | ||||
| "If checked, only users who have been explicitly added to the channel will be " | ||||
| "able to access it." | ||||
| @@ -94,11 +114,11 @@ msgstr "" | ||||
| "Si sélectionné, seul⋅es les utilisateur⋅rices qui ont été explicitement " | ||||
| "ajouté⋅es au canal pourront y accéder." | ||||
|  | ||||
| #: chat/models.py:74 | ||||
| #: chat/models.py:87 | ||||
| msgid "invited users" | ||||
| msgstr "Utilisateur⋅rices invité" | ||||
|  | ||||
| #: chat/models.py:77 | ||||
| #: chat/models.py:90 | ||||
| msgid "" | ||||
| "Extra users who have been invited to the channel, in addition to the " | ||||
| "permitted group of the channel." | ||||
| @@ -106,40 +126,40 @@ msgstr "" | ||||
| "Utilisateur⋅rices supplémentaires qui ont été invité⋅es au canal, en plus du " | ||||
| "groupe autorisé du canal." | ||||
|  | ||||
| #: chat/models.py:82 | ||||
| #: chat/models.py:95 | ||||
| #, python-brace-format | ||||
| msgid "Channel {name}" | ||||
| msgstr "Canal {name}" | ||||
|  | ||||
| #: chat/models.py:148 chat/models.py:157 | ||||
| #: chat/models.py:161 chat/models.py:170 | ||||
| msgid "channel" | ||||
| msgstr "canal" | ||||
|  | ||||
| #: chat/models.py:149 | ||||
| #: chat/models.py:162 | ||||
| msgid "channels" | ||||
| msgstr "canaux" | ||||
|  | ||||
| #: chat/models.py:163 | ||||
| #: chat/models.py:176 | ||||
| msgid "author" | ||||
| msgstr "auteur⋅rice" | ||||
|  | ||||
| #: chat/models.py:170 | ||||
| #: chat/models.py:183 | ||||
| msgid "created at" | ||||
| msgstr "créé le" | ||||
|  | ||||
| #: chat/models.py:175 | ||||
| #: chat/models.py:188 | ||||
| msgid "updated at" | ||||
| msgstr "modifié le" | ||||
|  | ||||
| #: chat/models.py:180 | ||||
| #: chat/models.py:193 | ||||
| msgid "content" | ||||
| msgstr "contenu" | ||||
|  | ||||
| #: chat/models.py:243 | ||||
| #: chat/models.py:256 | ||||
| msgid "message" | ||||
| msgstr "message" | ||||
|  | ||||
| #: chat/models.py:244 | ||||
| #: chat/models.py:257 | ||||
| msgid "messages" | ||||
| msgstr "messages" | ||||
|  | ||||
| @@ -171,7 +191,7 @@ msgstr "Déconnexion" | ||||
| msgid "Install app on home screen" | ||||
| msgstr "Installer l'application sur l'écran d'accueil" | ||||
|  | ||||
| #: chat/templates/chat/content.html:54 | ||||
| #: chat/templates/chat/content.html:55 | ||||
| msgid "Fetch previous messages…" | ||||
| msgstr "Récupérer les messages précédents…" | ||||
|  | ||||
|   | ||||
| @@ -27,7 +27,7 @@ SESSION_COOKIE_SECURE = False | ||||
| CSRF_COOKIE_SECURE = False | ||||
| CSRF_COOKIE_HTTPONLY = False | ||||
| X_FRAME_OPTIONS = 'DENY' | ||||
| SESSION_COOKIE_AGE = 60 * 60 * 3 | ||||
| SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2  # 2 weeks | ||||
|  | ||||
| CHANNEL_LAYERS = { | ||||
|     "default": { | ||||
|   | ||||
							
								
								
									
										4
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -13,7 +13,7 @@ deps = coverage | ||||
|  | ||||
| commands = | ||||
|     python manage.py compilemessages -i .tox -i venv | ||||
|     coverage run --source=api,draw,logs,participation,registration,tfjm ./manage.py test api/ draw/ logs/ participation/ registration/ tfjm/ | ||||
|     coverage run --source=api,draw,logs,participation,registration,tfjm ./manage.py test api/ chat/ draw/ logs/ participation/ registration/ tfjm/ | ||||
|     coverage report -m | ||||
|  | ||||
| [testenv:linters] | ||||
| @@ -26,7 +26,7 @@ deps = | ||||
|     pep8-naming | ||||
|     pyflakes | ||||
| commands = | ||||
|     flake8 api/ draw/ logs/ participation/ registration/ tfjm/ | ||||
|     flake8 api/ chat/ draw/ logs/ participation/ registration/ tfjm/ | ||||
|  | ||||
| [flake8] | ||||
| exclude = | ||||
|   | ||||
		Reference in New Issue
	
	Block a user