diff --git a/openslides/core/apps.py b/openslides/core/apps.py index d180b7109..08f80bcc2 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -20,6 +20,7 @@ class CoreAppConfig(AppConfig): from openslides.utils.rest_api import router from .signals import setup_general_config from .views import ( + ChatMessageViewSet, ConfigViewSet, CustomSlideViewSet, ProjectorViewSet, @@ -31,6 +32,7 @@ class CoreAppConfig(AppConfig): # Register viewsets. router.register('core/projector', ProjectorViewSet) + router.register('core/chatmessage', ChatMessageViewSet) router.register('core/customslide', CustomSlideViewSet) router.register('core/tag', TagViewSet) router.register('core/config', ConfigViewSet, 'config') diff --git a/openslides/core/chatbox.py b/openslides/core/chatbox.py deleted file mode 100644 index a84314e9c..000000000 --- a/openslides/core/chatbox.py +++ /dev/null @@ -1,110 +0,0 @@ -from datetime import datetime - -from django.conf import settings -from django.utils.html import urlize -from django.utils.importlib import import_module -from sockjs.tornado import SockJSConnection - - -class ChatboxSocketHandler(SockJSConnection): - """ - Websocket handler for the chatbox. - """ - clients = set() - - def on_open(self, info): - """ - Checks connecting user and adds his client to the clients list. - """ - from openslides.users.models import User - - # get the session (compatible with other auth-backends) - engine = import_module(settings.SESSION_ENGINE) - try: - session_key = info.get_cookie(settings.SESSION_COOKIE_NAME).value - session_store = engine.SessionStore(session_key) - pk = session_store.get('_auth_user_id') - except AttributeError: - return False - - try: - self.user = User.objects.get(pk=pk) - except User.DoesNotExist: - return False - - if self.user.has_perm('core.can_use_chat'): - self.clients.add(self) - return True - else: - return False - - def on_message(self, message): - """ - Sends the given message to all clients. - - Also appends the message to the cache and removes old messages if there - are more than 100. - """ - if self.user.has_perm('core.can_use_chat') and message: - message_object = ChatMessage(person=self.user, message=message) - chat_messages.append(message_object) - if len(chat_messages) > 100: - chat_messages.pop(0) - self.broadcast( - self.clients, - '%s %s %s' % (message_object.html_time(), - message_object.html_person(), - urlize(message_object.message))) - - def on_close(self): - """ - Removes client from the clients list. - """ - self.clients.remove(self) - - -class ChatMessage(object): - """ - Class for all chat messages. They are stored in the chat_messages object. - - The argument person has to be a Person object, the argument message has to - be the message as string. The argument color can be a three-tuple of RGB - color values. Default is black (0, 0, 0). - """ - def __init__(self, person, message, color=None): - self.person = person - self.message = message - self.color = color or (0, 0, 0) - self.time = datetime.now() - - def html_time(self): - """ - Returns the message time in html style. - """ - return '%s' % self.time.strftime('%H:%M') - - def html_person(self): - """ - Returns the message sender name in html style. - """ - return "%(person)s:" % { - 'color': 'rgb(%d,%d,%d)' % self.color, - 'person': self.person.clean_name} - - -chat_messages = [] -""" -Cache with all messages during livetime of the server. -""" - - -def chat_messages_context_processor(request): - """ - Adds all chat messages to the request context as template context processor. - Returns None if the request user has no permission to use the chat. - """ - if request.user.has_perm('core.can_use_chat'): - value = chat_messages - else: - value = None - return {'chat_messages': value} diff --git a/openslides/core/migrations/0005_add_chat_message_model.py b/openslides/core/migrations/0005_add_chat_message_model.py new file mode 100644 index 000000000..6fec317ef --- /dev/null +++ b/openslides/core/migrations/0005_add_chat_message_model.py @@ -0,0 +1,35 @@ +from django.conf import settings +from django.db import migrations, models + +import openslides.utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('core', '0004_clear_all_and_make_it_new'), + ] + + operations = [ + migrations.CreateModel( + name='ChatMessage', + fields=[ + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), + ('message', models.TextField(verbose_name='Message')), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'permissions': (('can_use_chat', 'Can use the chat'),), + }, + bases=(openslides.utils.models.RESTModelMixin, models.Model), + ), + migrations.AlterModelOptions( + name='projector', + options={'permissions': ( + ('can_see_projector', 'Can see the projector'), + ('can_manage_projector', 'Can manage the projector'), + ('can_see_dashboard', 'Can see the dashboard'))}, + ), + ] diff --git a/openslides/core/models.py b/openslides/core/models.py index ac4fe7ac1..09bbade37 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.db import models from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy, ugettext_noop @@ -50,8 +51,7 @@ class Projector(RESTModelMixin, models.Model): permissions = ( ('can_see_projector', ugettext_noop('Can see the projector')), ('can_manage_projector', ugettext_noop('Can manage the projector')), - ('can_see_dashboard', ugettext_noop('Can see the dashboard')), - ('can_use_chat', ugettext_noop('Can use the chat'))) + ('can_see_dashboard', ugettext_noop('Can see the dashboard'))) @property def elements(self): @@ -154,3 +154,26 @@ class ConfigStore(models.Model): class Meta: permissions = (('can_manage_config', ugettext_noop('Can manage configuration')),) + + +class ChatMessage(RESTModelMixin, models.Model): + """ + Model for chat messages. + + At the moment we only have one global chat room for managers. + """ + message = models.TextField( + verbose_name=ugettext_lazy('Message')) + + timestamp = models.DateTimeField(auto_now_add=True) + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + verbose_name=ugettext_lazy('User')) + + class Meta: + permissions = ( + ('can_use_chat', ugettext_noop('Can use the chat')),) + + def __str__(self): + return 'Message {}'.format(self.timestamp) diff --git a/openslides/core/serializers.py b/openslides/core/serializers.py index 622e9655b..7fb6db583 100644 --- a/openslides/core/serializers.py +++ b/openslides/core/serializers.py @@ -1,6 +1,6 @@ from openslides.utils.rest_api import Field, ModelSerializer, ValidationError -from .models import CustomSlide, Projector, Tag +from .models import ChatMessage, CustomSlide, Projector, Tag class JSONSerializerField(Field): @@ -51,3 +51,13 @@ class TagSerializer(ModelSerializer): class Meta: model = Tag fields = ('id', 'name', ) + + +class ChatMessageSerializer(ModelSerializer): + """ + Serializer for core.models.ChatMessage objects. + """ + class Meta: + model = ChatMessage + fields = ('id', 'message', 'timestamp', 'user', ) + read_only_fields = ('user', ) diff --git a/openslides/core/views.py b/openslides/core/views.py index e36728528..6ec3572e9 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -28,8 +28,9 @@ from openslides.utils.rest_api import ( from .config import config from .exceptions import ConfigError, ConfigNotFound -from .models import CustomSlide, Projector, Tag +from .models import ChatMessage, CustomSlide, Projector, Tag from .serializers import ( + ChatMessageSerializer, CustomSlideSerializer, ProjectorSerializer, TagSerializer, @@ -414,6 +415,34 @@ class ConfigViewSet(ViewSet): return Response({'key': key, 'value': value}) +class ChatMessageViewSet(ModelViewSet): + """ + API endpoint for chat messages. + + There are the following views: metadata, list, retrieve and create. + The views partial_update, update and destroy are disabled. + """ + queryset = ChatMessage.objects.all() + serializer_class = ChatMessageSerializer + + def check_view_permissions(self): + """ + Returns True if the user has required permissions. + """ + # We do not want anonymous users to use the chat even the anonymous + # group has the permission core.can_use_chat. + return (self.action in ('metadata', 'list', 'retrieve', 'create') and + self.request.user.is_authenticated() and + self.request.user.has_perm('core.can_use_chat')) + + def perform_create(self, serializer): + """ + Customized method to inject the request.user into serializer's save + method so that the request.user can be saved into the model field. + """ + serializer.save(user=self.request.user) + + # Special API views class UrlPatternsView(utils_views.APIView): diff --git a/openslides/global_settings.py b/openslides/global_settings.py index 7b87d62d8..62b15d902 100644 --- a/openslides/global_settings.py +++ b/openslides/global_settings.py @@ -97,7 +97,6 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.request', 'django.core.context_processors.i18n', 'django.core.context_processors.static', - 'openslides.core.chatbox.chat_messages_context_processor', ) CACHES = { diff --git a/openslides/utils/autoupdate.py b/openslides/utils/autoupdate.py index f9c093fb2..3bb33c2b5 100644 --- a/openslides/utils/autoupdate.py +++ b/openslides/utils/autoupdate.py @@ -134,8 +134,6 @@ def run_tornado(addr, port, *args, **kwargs): app = WSGIContainer(get_wsgi_application()) # Collect urls - from openslides.core.chatbox import ChatboxSocketHandler - chatbox_socket_js_router = SockJSRouter(ChatboxSocketHandler, '/core/chatbox') sock_js_router = SockJSRouter(OpenSlidesSockJSConnection, '/sockjs') other_urls = [ (r"%s(.*)" % settings.STATIC_URL, DjangoStaticFileHandler), @@ -144,7 +142,7 @@ def run_tornado(addr, port, *args, **kwargs): # Start the application debug = settings.DEBUG - tornado_app = Application(sock_js_router.urls + chatbox_socket_js_router.urls + other_urls, autoreload=debug, debug=debug) + tornado_app = Application(sock_js_router.urls + other_urls, autoreload=debug, debug=debug) server = HTTPServer(tornado_app) server.listen(port=port, address=addr) IOLoop.instance().start()