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
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):
),
)
+ 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):
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")
# 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)
}
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) {
}
);
}
- foreach_arr(
+ foreach_obj(
callbacks,
function (event_name, callback) {
request.addEventListener(event_name, callback);
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)
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(),
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,
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,
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,
},
)
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)
"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,
],
},
)
+ 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()
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],
+ )
+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 (
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
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:
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 "
-[ ] 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
[ ] ws and chat tests?
[ ] media uploads: images, gifs, even video files?
[ ] moderation functions: report messages to MANAGERS, including private ones
-[ ] write email to user