]> git.mar77i.info Git - chat/commitdiff
improve tests. update todo
authormar77i <mar77i@protonmail.ch>
Sun, 20 Oct 2024 17:08:44 +0000 (19:08 +0200)
committermar77i <mar77i@protonmail.ch>
Sun, 20 Oct 2024 17:08:44 +0000 (19:08 +0200)
chat/rest_views.py
chat/serializers.py
chat/static/chat/chat.js
chat/static/chat/underscore.js
chat/tests.py
chat/views.py
manage.py
todo.txt

index fc1f0cde6a6bbbd05a52199e4de8e63798a5b1ef..c2c547487898f07ebdf67a12bbe469ca4ea9f1f3 100644 (file)
@@ -33,20 +33,25 @@ class ModelRestView(LoginRequiredMixin, View):
     def get_queryset(self):
         return QuerySet(self.serializer.model).all()
 
+    safe_chars = "/#%[]=:;$&()+,!?*@'~"
+
     def paginate(self, queryset, to_json):
         query = self.request.GET.copy()
-        full_path = self.request.get_full_path()
-        pos = full_path.find("?")
-        if pos >= 0:
-            full_path = full_path[:pos]
         if "before" in query:
             queryset = queryset.filter(pk__lt=int(query["before"]))
         count = queryset.count()
-        queryset = queryset[max(count - settings.DEFAULT_PAGE_SIZE, 0) :]
-        if count > settings.DEFAULT_PAGE_SIZE:
+        if "since" in query:
+            queryset = queryset.filter(pk__gte=int(query["since"]))
+            page_size = queryset.count()
+        else:
+            page_size = settings.DEFAULT_PAGE_SIZE
+            queryset = queryset[max(count - page_size, 0) :]
+        if count > page_size:
             query["before"] = queryset[0].pk
             prev_url = self.request.build_absolute_uri(
-                urlunsplit(("", "", full_path, query.urlencode(), "")),
+                urlunsplit(
+                    ("", "", self.request.path, query.urlencode(self.safe_chars), "")
+                ),
             )
         else:
             prev_url = None
@@ -112,7 +117,14 @@ class ModelRestView(LoginRequiredMixin, View):
     def dispatch(self, request, *args, **kwargs):
         if not request.user.is_authenticated:
             return self.handle_no_permission()
-        return getattr(self, self.method_map[request.method.lower()])()
+        try:
+            return getattr(self, self.method_map[request.method.lower()])()
+        except Exception as e:
+            return HttpResponse(
+                json.dumps({type(e).__name__: e.args}),
+                content_type="application/json",
+                status=500,
+            )
 
     @classmethod
     def get_urls(cls, name):
@@ -129,6 +141,13 @@ class ModelRestView(LoginRequiredMixin, View):
             ),
         )
 
+    def handle_no_permission(self):
+        return HttpResponse(
+            json.dumps({"Location": settings.LOGIN_URL}),
+            content_type="application/json",
+            status=401,
+        )
+
 
 class ListAllMixin:
     def paginate(self, queryset, to_json):
@@ -213,6 +232,10 @@ class ChannelMessageRestView(ModelRestView):
 
     def get_queryset(self):
         queryset = super().get_queryset().filter(channel__users=self.request.user.pk)
-        if "channel_id" in self.request.GET:
+        if (
+            self.request.method == "GET"
+            and self.request.resolver_match.url_name == "chat-channelmessage-list"
+            and "channel_id" in self.request.GET
+        ):
             queryset = queryset.filter(channel_id=self.request.GET["channel_id"])
         return queryset.order_by("ts")
index 11dddd5690247b7715cf6ddd4f244da600aaf49b..f542b315d9bd663d16aeb8da67a13b2803094420 100644 (file)
@@ -55,8 +55,10 @@ class ModelSerializer:
                 # disallow reassigning FKs on update
                 if instance.pk is not None:
                     raise ValueError(f"Not allowed to update: {field_name}")
-                if not isinstance(value, int):
-                    raise ValueError(f"Please just add the _id field: {field_name}")
+                if not field_name.endswith("_id") or not isinstance(value, int):
+                    raise ValueError(
+                        f"Please use fk_id fields with an id: {field_name}"
+                    )
                 setattr(instance, field_name, value)
             else:
                 setattr(instance, field_name, value)
index 737ee08c2ee2b515d8445d8e75a9fadbe8bb08f3..267f888e53553d14d4ffff217dfe3767b6fff488 100644 (file)
     }
     bottom_callback = no_bottom_callback;
 
+    function maybe_redirect(data) {
+        if (data.hasOwnProperty("Location")) {
+            location.href = data.Location;
+        }
+    }
+
     function xhr(method, url, callback, data) {
         return _.xhr(
             method,
                 "error": function (msg) {
                     add_msg(null, "xhr_error", msg);
                 },
-                "load": (
-                    callback
-                    ? function () {
-                        callback(JSON.parse(this.responseText));
-                    }
-                    : null
-                ),
-                "readystatechange": function () {
-                    if (this.readyState === this.DONE) {
-                        if (this.readyState === this.DONE) {
-                            console.log("DONE url?", this.responseURL, url);
-                        } else {
-                            console.log("url?", this.responseURL, url);
-                        }
+                "load": function () {
+                    data = JSON.parse(this.responseText);
+                    if (this.status === 401) {
+                        maybe_redirect(data);
+                    } else if (callback) {
+                        callback(data);
                     }
                 }
             },
 
     function ws_receive(msg) {
         var data = JSON.parse(msg.data);
-        if (data.hasOwnProperty("Location")) {
-            location.href = data.Location;
-        }
+        maybe_redirect(data);
         // here we could add indication for the pm or channel being modified
         if (
             data.table === "chat_channel" || data.table === "chat_channeluser"
     }
 
     function send_msg() {
-        var data = {"text": ta.value};
         var ta = _.dgEBCN0("messages_footer").children[0];
+        var data = {"text": ta.value};
         _.foreach_obj(
             current_channel.msg_data,
             function (name, value) {
index 26d410f311fc3c3f17fafea76fdc652b091b4b68..3c074aa4ac22320442afeedc7a583e3f23355a01 100644 (file)
@@ -58,7 +58,7 @@
                 }
             );
         }
-        foreach_arr(
+        foreach_obj(
             callbacks,
             function (event_name, callback) {
                 request.addEventListener(event_name, callback);
index 5d1ea205261e528f0e75b7284ce2954d0fdac699..632031baed9e9d93f772e9c787d6d498d0e6b3a0 100644 (file)
@@ -458,8 +458,8 @@ class ChatTest(ChatTestMixin, TestCase):
         user2_url = reverse("chat-user-detail", args=[user2.pk])
 
         response = self.client.get(user2_url)
-        self.assertEqual(response.status_code, 302)
-        self.assertEqual(response.headers["Location"], f"/login/?next={user2_url}")
+        self.assertEqual(response.status_code, 401)
+        self.assertEqual(json.loads(response.content), {"Location": "/login/"})
         cookies = self.login_user(user2, user2_password)
         response = self.client.get(user2_url, headers={"cookie": cookies})
         data = json.loads(response.content)
@@ -596,13 +596,27 @@ class ChatTest(ChatTestMixin, TestCase):
         user3.is_staff = False
         user3.save(update_fields=["is_staff"])
         self.assertFalse(user3.is_privileged())
+        response = self.client.post(
+            reverse("chat-user-list"),
+            json.dumps({"username": "user5"}),
+            content_type="application/json",
+            headers={"cookie": cookies},
+        )
+        self.assertEqual(response.status_code, 401)
+        response = self.client.delete(
+            user4_url,
+            json.dumps({"username": "user5"}),
+            content_type="application/json",
+            headers={"cookie": cookies},
+        )
+        self.assertEqual(response.status_code, 401)
         response = self.client.put(
             user4_data["url"],
             json.dumps({"username": "user4_modified"}).encode(),
             content_type="application/json",
             headers={"cookie": cookies},
         )
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 401)
         response = self.client.put(
             reverse("chat-user-detail", args=[user3.pk]),
             json.dumps({"username": "user3"}).encode(),
@@ -656,8 +670,9 @@ class ChatTest(ChatTestMixin, TestCase):
     def test_chat_privatemessage_api(self):
         user1, user2, user2_password, channel = self.setup_users()
         cookies = self.login_user(user2, user2_password)
+        privatemessage_list_url = reverse("chat-privatemessage-list")
         response = self.client.post(
-            reverse("chat-privatemessage-list"),
+            privatemessage_list_url,
             json.dumps(
                 {
                     "id": None,
@@ -669,12 +684,14 @@ class ChatTest(ChatTestMixin, TestCase):
             headers={"cookie": cookies},
         )
         data = json.loads(response.content)
-        privatemessage_url = reverse("chat-privatemessage-detail", args=[data["id"]])
+        privatemessage_detail_url = reverse(
+            "chat-privatemessage-detail", args=[data["id"]]
+        )
         self.assertEqual(
             data,
             {
                 "id": data["id"],
-                "url": f"http://testserver{privatemessage_url}",
+                "url": f"http://testserver{privatemessage_detail_url}",
                 "ts": data["ts"],
                 "sender_id": user2.pk,
                 "recipient_id": user1.pk,
@@ -705,7 +722,7 @@ class ChatTest(ChatTestMixin, TestCase):
             data_new,
             {
                 "id": data["id"],
-                "url": f"http://testserver{privatemessage_url}",
+                "url": f"http://testserver{privatemessage_detail_url}",
                 "ts": data["ts"],
                 "sender_id": user2.pk,
                 "recipient_id": user1.pk,
@@ -713,7 +730,7 @@ class ChatTest(ChatTestMixin, TestCase):
             },
         )
         response = self.client.get(
-            f"{reverse('chat-privatemessage-list')}?other={user1.pk}",
+            f"{reverse('chat-privatemessage-list')}?recipient_id={user1.pk}",
             headers={"cookie": cookies},
         )
         data = json.loads(response.content)
@@ -733,7 +750,7 @@ class ChatTest(ChatTestMixin, TestCase):
                 "result": [
                     {
                         "id": data["result"][0]["id"],
-                        "url": f"http://testserver{privatemessage_url}",
+                        "url": f"http://testserver{privatemessage_detail_url}",
                         "ts": data["result"][0]["ts"],
                         "sender_id": user2.pk,
                         "recipient_id": user1.pk,
@@ -742,6 +759,24 @@ class ChatTest(ChatTestMixin, TestCase):
                 ],
             },
         )
+        response = self.client.put(
+            privatemessage_detail_url,
+            json.dumps({"recipient_id": user2.pk}),
+            content_type="application/json",
+        )
+        self.assertEqual(
+            json.loads(response.content),
+            {"ValueError": ["Not allowed to update: recipient_id"]},
+        )
+        response = self.client.post(
+            privatemessage_list_url,
+            json.dumps({"recipient_id": reverse("chat-user-detail", args=[user2.pk])}),
+            content_type="application/json",
+        )
+        self.assertEqual(
+            json.loads(response.content),
+            {"ValueError": ["Please use fk_id fields with an id: recipient_id"]},
+        )
 
     def test_chat_channelmessage_api(self):
         user1, user2, user2_password, channel = self.setup_users()
@@ -776,12 +811,20 @@ class ChatTest(ChatTestMixin, TestCase):
                 user=user2,
                 text=token_urlsafe(),
             )
-        list_url = f"{reverse('chat-channelmessage-list')}?channel={channel.pk}"
+        list_url = f"{reverse('chat-channelmessage-list')}?channel_id={channel.pk}"
         response = self.client.get(list_url)
         data = json.loads(response.content)
+        first_page_ids = [item["id"] for item in data["result"]]
         first_id = data["result"][0]["id"]
-        list_url = f"{list_url}&before={first_id}"
-        self.assertEqual(data["previous"], f"http://testserver{list_url}")
+        before = f"&before={first_id}"
+        self.assertEqual(data["previous"], f"http://testserver{list_url}{before}")
+
         response = self.client.get(data["previous"])
         data = json.loads(response.content)
         self.assertTrue(all(item["id"] < first_id for item in data["result"]))
+
+        response = self.client.get(f"{list_url}&since={data['result'][0]['id']}")
+        self.assertEqual(
+            [item["id"] for item in json.loads(response.content)["result"]],
+            [*(item["id"] for item in data["result"]), *first_page_ids],
+        )
index ee67f036589009876e2357128e6f2ce611e70b49..f366f4f3e7df1530befbcdb570319a0658371560 100644 (file)
@@ -1,5 +1,7 @@
+from asgiref.sync import sync_to_async
 from django.conf import settings
 from django.contrib.auth.forms import SetPasswordForm
+from django.contrib.auth.mixins import LoginRequiredMixin
 from django.contrib.auth.models import update_last_login
 from django.contrib.auth.tokens import default_token_generator
 from django.contrib.auth.views import (
@@ -11,7 +13,7 @@ from django.contrib.auth.views import (
 from django.contrib.staticfiles import finders
 from django.core.mail import mail_admins
 from django.core.signing import Signer
-from django.http import FileResponse, Http404
+from django.http import FileResponse, Http404, HttpResponseBase
 from django.urls import reverse, reverse_lazy
 from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
 from django.utils.safestring import mark_safe
@@ -23,7 +25,13 @@ from .forms import AuthenticationForm, PasswordResetForm, RegisterForm
 from .models import User
 
 
-class ChatView(View):
+class ChatView(LoginRequiredMixin, View):
+    async def dispatch(self, request, *args, **kwargs):
+        result = sync_to_async(super().dispatch)(request, *args, **kwargs)
+        while not isinstance(result, HttpResponseBase):
+            result = await result
+        return result
+
     async def get(self, request, *args, **kwargs):
         path = finders.find("chat/chat.html")
         with open(path) as fh:
index ecd372e10d26feebf451dd39e16bde9036fa1359..3cec53ad551caa577862ad02d61a3a1d66b1c5b6 100755 (executable)
--- a/manage.py
+++ b/manage.py
@@ -19,12 +19,12 @@ def setup_virtual_env():  # pragma: no cover
 
 def main():
     """Run administrative tasks."""
-    if "VIRTUAL_ENV" not in os.environ:
+    if "VIRTUAL_ENV" not in os.environ:  # pragma: no cover
         setup_virtual_env()
     os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chat.settings")
     try:
         from django.core.management import execute_from_command_line
-    except ImportError as exc:
+    except ImportError as exc:  # pragma: no cover
         raise ImportError(
             "Couldn't import Django. Are you sure it's installed and "
             "available on your PYTHONPATH environment variable? Did you "
index 55cfb8bed100623c6889abac4aa09974e7961166..f24a7858a71d16d32b945ceb4204fbf086a32bb0 100644 (file)
--- a/todo.txt
+++ b/todo.txt
@@ -1,10 +1,12 @@
-[ ] make it easy to see what users are in the current channel
+[ ] logout button
+[ ] show message timestamps and message edit/delete menus
+   - privileged users can edit / delete any message
+[ ] edit existing messages
 [ ] "load more" button
 [ ] new message(s) indicators in left panel
-[ ] edit existing messages
-[ ] channel urls, privatemessage urls
-[ ] in <a href="#">, use javascript:void(0) instead
+[ ] update address bar for different views
 [ ] channel management: channel admin
+[ ] write email to user
 
 [ ] ws client infrastructure: auto-reconnect, notify-throttle
 [ ] server-side throttling
@@ -14,4 +16,3 @@
 [ ] ws and chat tests?
 [ ] media uploads: images, gifs, even video files?
 [ ] moderation functions: report messages to MANAGERS, including private ones
-[ ] write email to user