Skip to content

Commit 5f9e242

Browse files
authored
Merge branch 'master' into feature/docs
2 parents a984e5d + 6cb2c17 commit 5f9e242

22 files changed

Lines changed: 404 additions & 64 deletions

.github/workflows/release-ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ jobs:
9898
password: ${{ secrets.SERVER_PASSWORD }}
9999
command: |
100100
cd /home/app/procollab-backend &&
101+
docker container prune -f &&
102+
docker image prune -a -f &&
101103
docker-compose -f docker-compose.prod-ci.yml -p prod pull &&
102104
103105
rm -f .env &&

chats/consumers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ async def connect(self):
4848
"""User connected to websocket"""
4949

5050
if self.scope["user"].is_anonymous:
51+
# not authenticated
5152
return await self.close(403)
5253

5354
self.user = self.scope["user"]

chats/middleware.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from urllib.parse import parse_qs
2+
13
import jwt
24
from channels.db import database_sync_to_async
35
from django.conf import settings
@@ -103,24 +105,23 @@ def __init__(self, app):
103105
self.app = app
104106

105107
async def __call__(self, scope, receive, send):
106-
# Look up user from query string (you should also do things like
107-
# checking if it is a valid user ID, or if scope["user"] is already
108-
# populated).
109-
headers = scope["headers"]
110-
try:
111-
token = None
112-
for name, value in headers:
113-
if name == b"authorization":
114-
token = value.decode()
115-
break
108+
# Look up user from query string
116109

110+
# TODO: (you should also do things like
111+
# checking if it is a valid user ID, or if scope["user" ] is already
112+
# populated).
113+
114+
query_string = scope["query_string"].decode()
115+
query_dict = parse_qs(query_string)
116+
try:
117+
token = query_dict["token"][0]
117118
if token is None:
118119
raise ValueError("Token is missing from headers")
119120

120121
scope["token"] = token
121122
scope["user"] = await get_user(scope)
122-
except ValueError:
123-
# Token is missing from headers
123+
except (ValueError, KeyError, IndexError):
124+
# Token is missing from query string
124125
from django.contrib.auth.models import AnonymousUser
125126

126127
scope["user"] = AnonymousUser()

chats/websockets_settings.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from dataclasses import dataclass
22
from enum import Enum
3-
from typing import Optional, Union
43

54

65
class ChatType(str, Enum):
@@ -25,15 +24,7 @@ class EventGroupType(str, Enum):
2524
GENERAL_EVENTS = "GENERAL_EVENTS"
2625

2726

28-
@dataclass(slots=True, frozen=True)
29-
class NewMessageEventContent:
30-
chat_id: Optional[str]
31-
chat_type: Optional[Union[ChatType.DIRECT, ChatType.PROJECT]]
32-
message: Optional[str]
33-
reply_to: Optional[int]
34-
35-
36-
@dataclass(slots=True, frozen=True)
27+
@dataclass(frozen=True)
3728
class Event:
3829
type: EventType
3930
content: dict

docs/chats.md

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
## Общая инфа
44
URL для всего вебсокет-релейтед - `/ws/`
55

6-
В данный момент есть только 1 Consumer (т.е. View, но для вебсокетов). Это ChatsConsumer, живет на `/ws/chats/`.
6+
В данный момент есть только 1 Consumer (т.е. View, но для вебсокетов). Это ChatConsumer, живет на `/ws/chat/`.
77

8-
## ChatsConsumer
9-
`/ws/chats/`
8+
`/ws/chat/`
109

1110
### Подключение
1211
Чтобы законнектиться, укажите в хедерах авторизацию по Bearer токену (как и для всех других запросов в REST API).
@@ -20,12 +19,12 @@ class Event:
2019
type: EventType
2120
content: dict
2221
```
23-
И соответсвенно EventType вот такой:
22+
И соответственно EventType вот такой:
2423
```py
2524
# эти строки указывать в {"type": event_type}
2625

2726
class EventType(str, Enum):
28-
# CHATS RELATED EVENTS
27+
# CHAT RELATED EVENTS
2928
NEW_MESSAGE = "new_message"
3029
DELETE_MESSAGE = "delete_message"
3130
READ_MESSAGE = "message_read"
@@ -35,6 +34,40 @@ class EventType(str, Enum):
3534
SET_ONLINE = "set_online"
3635
SET_OFFLINE = "set_offline"
3736
```
37+
Пример того, как выглядит Event на новое сообщение
38+
```json
39+
{
40+
"type": "new_message",
41+
"content": {
42+
"chat_type": "direct",
43+
"chat_id": "12_23",
44+
"message": "hello world",
45+
"reply_to": 54
46+
}
47+
}
48+
```
49+
50+
## Методы e.g. Ивенты
51+
52+
### SET_ONLINE/SET_OFFLINE
53+
Без параметров.
54+
55+
### NEW_MESSAGE
56+
- `chat_type: str`\
57+
`"direct"` или `"project"`, зависит от типа чата
58+
- `chat_id: int/str`\
59+
Если тип `"project"`, то тип будет `int` и это айди проекта, которому принадлежит чат. Если тип `"direct"`, то это `str`. Выглядит как `{user1_id}_{user2_id}`, **где первое число всегда меньше второго**.
60+
- `message: str` текст сообщения
61+
- `reply_to: Optional[int]` айди сообщения, на которое кидается ответ. Если его нет, то обязательно кидать `None`
62+
63+
### TYPING
64+
- `chat_type` см выше
65+
- `chat_id` см выше
66+
67+
### READ_MESSAGE
68+
- `chat_type` см выше
69+
- `chat_id` см выше
70+
- `message_id: int` айди сообщение, которое прочитали
3871

3972
#### General events
4073

procollab/asgi.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import chats.routing
33

44
from channels.routing import ProtocolTypeRouter, URLRouter
5-
from channels.security.websocket import AllowedHostsOriginValidator
65
from django.core.asgi import get_asgi_application
76

87
from chats.middleware import TokenAuthMiddleware
@@ -12,8 +11,6 @@
1211
application = ProtocolTypeRouter(
1312
{
1413
"http": get_asgi_application(),
15-
"websocket": AllowedHostsOriginValidator(
16-
TokenAuthMiddleware(URLRouter(chats.routing.websocket_urlpatterns))
17-
),
14+
"websocket": TokenAuthMiddleware(URLRouter(chats.routing.websocket_urlpatterns)),
1815
}
1916
)

procollab/settings.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727
]
2828

2929
ALLOWED_HOSTS = [
30+
"127.0.0.1:8000",
3031
"127.0.0.1",
3132
"localhost",
3233
"0.0.0.0",
3334
"api.procollab.ru",
34-
"127.0.0.1:8000",
35+
"app.procollab.ru",
36+
"procollab.ru",
3537
]
3638

3739
PASSWORD_HASHERS = [
@@ -80,7 +82,7 @@
8082
"rest_framework_simplejwt",
8183
"rest_framework_simplejwt.token_blacklist",
8284
"django_cleanup.apps.CleanupConfig",
83-
"rest_framework.authtoken",
85+
# "rest_framework.authtoken",
8486
# Plugins
8587
"corsheaders",
8688
"django_filters",
@@ -169,23 +171,31 @@
169171

170172
CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}
171173
else:
172-
CHANNEL_LAYERS = {
173-
"default": {
174-
"BACKEND": "channels_redis.core.RedisChannelLayer",
175-
"CONFIG": {
176-
"hosts": [("127.0.0.1", 6379)],
177-
},
178-
},
179-
}
180-
181-
REDIS_HOST = config("REDIS_HOST", cast=str, default="127.0.0.1")
174+
# fixme
182175
CACHES = {
183176
"default": {
184-
"BACKEND": "django.core.cache.backends.redis.RedisCache",
185-
"LOCATION": f"redis://{REDIS_HOST}:6379",
177+
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
186178
}
187179
}
188180

181+
CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}
182+
# CHANNEL_LAYERS = {
183+
# "default": {
184+
# "BACKEND": "channels_redis.core.RedisChannelLayer",
185+
# "CONFIG": {
186+
# "hosts": [("127.0.0.1", 6379)],
187+
# },
188+
# },
189+
# }
190+
#
191+
# REDIS_HOST = config("REDIS_HOST", cast=str, default="127.0.0.1")
192+
# CACHES = {
193+
# "default": {
194+
# "BACKEND": "django.core.cache.backends.redis.RedisCache",
195+
# "LOCATION": f"redis://{REDIS_HOST}:6379",
196+
# }
197+
# }
198+
189199
REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"] = [
190200
"rest_framework.renderers.JSONRenderer",
191201
]
@@ -252,7 +262,7 @@
252262
"UPDATE_LAST_LOGIN": False,
253263
"ALGORITHM": "HS256",
254264
"SIGNING_KEY": SECRET_KEY,
255-
"VERIFYING_KEY": None,
265+
"VERIFYING_KEY": True,
256266
"AUDIENCE": None,
257267
"ISSUER": None,
258268
"JWK_URL": None,
@@ -273,8 +283,7 @@
273283
}
274284

275285
if DEBUG:
276-
SIMPLE_JWT["ACCESS_TOKEN_LIFETIME"] = timedelta(weeks=1)
277-
286+
SIMPLE_JWT["ACCESS_TOKEN_LIFETIME"] = timedelta(seconds=30)
278287

279288
SESSION_COOKIE_SECURE = False
280289

projects/constants.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
VERBOSE_STEPS = (
2+
(0, "Идея"),
3+
(1, "Прототип"),
4+
(2, "MVP(Минимально жизнеспособный продукт)"),
5+
(3, "Первые продажи"),
6+
(4, "Масштабирование"),
7+
)
8+
9+
RECOMMENDATIONS_COUNT = 5

projects/helpers.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,35 @@
1-
VERBOSE_STEPS = (
2-
(0, "Идея"),
3-
(1, "Прототип"),
4-
(2, "MVP(Минимально жизнеспособный продукт)"),
5-
(3, "Первые продажи"),
6-
(4, "Масштабирование"),
7-
)
1+
from random import sample
2+
3+
from django.contrib.auth import get_user_model
4+
5+
from projects.constants import RECOMMENDATIONS_COUNT
6+
from projects.models import Project
7+
8+
User = get_user_model()
9+
10+
11+
def get_recommended_users(project: Project) -> list[User]:
12+
"""
13+
Searches for users by matching their key_skills and vacancies required_skills
14+
"""
15+
16+
# fixme: store key_skills and required_skills more convenient, not just as a string
17+
all_needed_skills = set()
18+
for vacancy in project.vacancies.all():
19+
all_needed_skills.update(set(vacancy.required_skills.lower().split(",")))
20+
21+
recommended_users = []
22+
for user in User.objects.get_members():
23+
if user == project.leader or not user.key_skills:
24+
continue
25+
26+
skills = set(user.key_skills.lower().split(","))
27+
if skills.intersection(all_needed_skills):
28+
recommended_users.append(user)
29+
30+
# get some random users
31+
sampled_recommended_users = sample(
32+
recommended_users, min(RECOMMENDATIONS_COUNT, len(recommended_users))
33+
)
34+
35+
return sampled_recommended_users

projects/managers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ def check_if_owns_any_projects(self, user) -> bool:
7070
# I don't think this should work but the function has no usages, so I'll let it be
7171
return user.leader_projects.exists()
7272

73+
def get_projects_from_list_of_ids(self, ids):
74+
return self.get_queryset().filter(id__in=ids)
75+
7376

7477
class AchievementManager(Manager):
7578
def get_achievements_for_list_view(self):

0 commit comments

Comments
 (0)