Skip to content

Commit

Permalink
- Added attributes initiator_last_update_at and, 'respondent_last_u…
Browse files Browse the repository at this point in the history
…pdate_model' and validation functionality to Contact model

- Added `id` attribute to EventOut
- `ReportReason` was changed to `ReportType`
- Library versions updated
  • Loading branch information
onstabb committed Dec 15, 2023
1 parent 31dd930 commit a7280d2
Show file tree
Hide file tree
Showing 15 changed files with 183 additions and 93 deletions.
7 changes: 4 additions & 3 deletions requirements-base.txt
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
fastapi~=0.104.1
fastapi~=0.105.0
pydantic~=2.5.2
python-jose~=3.3.0
mongoengine~=0.27.0
Pillow~=10.1.0
passlib~=1.7.4
python-dotenv~=1.0.0
phonenumbers~=8.13.25
phonenumbers~=8.13.26
telesign~=2.2.2
bcrypt~=4.0.1
apscheduler~=3.10.4
pymongo~=4.6.0
pymongo~=4.6.1
python-multipart~=0.0.6
pytz~=2023.3
vincenty~=0.1.4
sse-starlette~=1.8.2
boto3~=1.29.6
starlette-admin~=0.12.2
itsdangerous~=2.1.2

2 changes: 1 addition & 1 deletion src/admin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class ReportView(ModelView):
fields = [
"initiator",
"respondent",
"reason",
"type",
"additional_info",
"closed",
]
Expand Down
8 changes: 8 additions & 0 deletions src/contacts/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@ class ContactState(enum.StrEnum):
ESTABLISHED = "established"
REFUSED = "refused"
BLOCKED = "blocked"


@enum.unique
class ContactUpdateAction(enum.StrEnum):
ESTABLISH = "establish"
REFUSE = "refuse"
BLOCK = "block"
SEEN = "seen"
70 changes: 65 additions & 5 deletions src/contacts/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime
from typing import TYPE_CHECKING, Self

from mongoengine import (
ReferenceField,
Expand All @@ -11,10 +12,14 @@
)
from mongoengine.base import EmbeddedDocumentList


from contacts.enums import ContactState
from datehelpers import get_aware_datetime_now
from models import BaseDocument

if TYPE_CHECKING:
from users.models import User


# None # established # refused # blocked
CONTACT_RESULT_TABLE = (
Expand All @@ -32,17 +37,72 @@ class Message(EmbeddedDocument):


class Contact(BaseDocument):
initiator = ReferenceField('User', required=True)
respondent = ReferenceField('User', required=True)
initiator_state = EnumField(ContactState, null=True) # type: ContactState | None
respondent_state = EnumField(ContactState, null=True) # type: ContactState | None
status = EnumField(ContactState, null=True) # type: ContactState | None
initiator = ReferenceField('User', required=True) # type: User
respondent = ReferenceField('User', required=True) # type: User
initiator_state = EnumField(ContactState) # type: ContactState
respondent_state = EnumField(ContactState, null=True, required=False) # type: ContactState | None
initiator_last_update_at = DateTimeField(default=get_aware_datetime_now) # type: datetime
respondent_last_update_at = DateTimeField(required=False, null=True) # type: datetime | None
status = EnumField(ContactState, null=True, required=False) # type: ContactState | None
messages = EmbeddedDocumentListField(Message) # type: EmbeddedDocumentList[Message]

meta = {
"indexes": [{"fields": ["initiator", "respondent"], "unique": True}]
}

def __init__(self, *args, **values) -> None:
super().__init__(*args, **values)
self._initial_initiator_state = self.initiator_state
self._initial_respondent_state = self.respondent_state

@staticmethod
def get_contact_status(state_1: ContactState | None, state_2: ContactState | None) -> ContactState | None:
enum_list: list[ContactState | None] = [None] + list(ContactState)
index_1: int = enum_list.index(state_1)
index_2: int = enum_list.index(state_2)
return CONTACT_RESULT_TABLE[index_1][index_2]

def clean(self) -> None:
if self.respondent_last_update_at is None and self.respondent_state:
self.respondent_last_update_at = get_aware_datetime_now()

if self._initial_initiator_state != self.initiator_state:
self._initial_initiator_state = self.initiator_state
self.initiator_last_update_at = get_aware_datetime_now()
elif self._initial_respondent_state != self.respondent_state:
self._initial_respondent_state = self.respondent_state
self.respondent_last_update_at = get_aware_datetime_now()

self.status = self.get_contact_status(self.initiator_state, self.respondent_state)
return

@property
def established(self) -> bool:
return self.status == ContactState.ESTABLISHED

def reset_field_to_dbref(self, field_name: str) -> Self:
document = getattr(self, field_name)
setattr(self, field_name, document.to_dbref())
return self

def reset_current_user(self, current_user: 'User') -> Self:
"""
This function updates the self instance by setting the appropriate user ID based on the role of the provided 'user' in the contact.
If 'user' is the respondent, the 'respondent' attribute in the self instance is set to the user's ID.
If 'user' is the initiator, the 'initiator' attribute is set to the user's ID.
The purpose of this function is a resolving who is the opposite user for current user.
:param current_user: The user for which we want to define the opposite user.
:return: self instance
:raise ValueError: Contact does not contain this user
"""

if current_user == self.initiator:
self.initiator = current_user.id
elif current_user == self.respondent:
self.respondent = current_user.id
else:
ValueError(f"Contact does not contain user {current_user.id}")

return self

42 changes: 24 additions & 18 deletions src/contacts/routers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from fastapi import APIRouter, HTTPException, status, Query

from contacts import config, service
from contacts.enums import ContactState
from contacts.enums import ContactState, ContactUpdateAction
from contacts.models import Contact
from contacts.schemas import ContactStateIn, ContactCreateDataIn, ContactOut, MessageIn, MessageOut
from contacts.schemas import ContactUpdateIn, ContactCreateDataIn, ContactOut, MessageIn, MessageOut
from notifications.enums import NotificationType
from notifications.manager import notification_manager
from users.dependencies import CurrentActiveCompletedUser, TargetActiveCompletedUser, get_active_completed_user
Expand All @@ -19,12 +19,14 @@ def get_contacts(
contact_status: ContactState = Query(alias="status", default=ContactState.ESTABLISHED),
limit: int = 0,
):
return service.get_contacts_for_user(current_user, limit=limit, status=contact_status)
return service.get_contacts_by_user(current_user, limit=limit, status=contact_status)


@router.get("/{user_id}", response_model=ContactOut)
def get_contact(current_user: CurrentActiveCompletedUser, target_user: TargetActiveCompletedUser):
return service.get_contact_by_users_pair(current_user, target_user)
contact = service.get_contact_by_users_pair(current_user, target_user)
contact.reset_current_user(current_user)
return contact


@router.post("", response_model=ContactOut, status_code=status.HTTP_201_CREATED)
Expand All @@ -36,7 +38,6 @@ def create_contact(data_in: ContactCreateDataIn, current_user: CurrentActiveComp

target_user = get_active_completed_user(data_in.user_id)
contact: Contact | None = service.get_contact_by_users_pair(current_user, target_user)

if contact:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail=f"Contact with profile `{target_user.id}` already exists"
Expand All @@ -49,31 +50,34 @@ def create_contact(data_in: ContactCreateDataIn, current_user: CurrentActiveComp
target_user.id,
NotificationType.LIKE
)

contact.reset_current_user(current_user)
return contact


@router.patch("/{user_id}", response_model=ContactOut)
def update_contact_state(
def update_contact(
target_user: TargetActiveCompletedUser,
current_user: CurrentActiveCompletedUser,
state_data: ContactStateIn,
update_data: ContactUpdateIn,
):
contact = service.get_contact_by_users_pair(current_user, target_user, use_id_for_current_user=False)
contact = service.get_contact_by_users_pair(current_user, target_user)
if not contact:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Contact doesn't exists")

if contact.status == ContactState.BLOCKED or (contact.status and state_data.action != ContactState.BLOCKED):
if contact.status == ContactState.BLOCKED:
return contact

service.update_contact_status(state_data, current_user, contact)
if contact.established and current_user == contact.respondent:
if contact.status and update_data.action not in (ContactUpdateAction.BLOCK, ContactUpdateAction.SEEN):
return contact

service.update_contact(update_data.action, current_user, contact)
if update_data.action == ContactUpdateAction.ESTABLISH and contact.established and current_user == contact.respondent:
notification_manager.put_notification(
UserPublicOut.model_validate(current_user, from_attributes=True),
contact.initiator,
NotificationType.CONTACT_ESTABLISHED
recipient_id=contact.initiator,
notification_type=NotificationType.CONTACT_ESTABLISHED
)

contact.reset_current_user(current_user)
return contact


Expand All @@ -83,14 +87,16 @@ def send_message(
current_user: CurrentActiveCompletedUser,
target_user: TargetActiveCompletedUser,
):
contact = service.get_contact_by_users_pair(current_user, target_user, use_id_for_current_user=False)
contact = service.get_contact_by_users_pair(current_user, target_user)
if not contact or not contact.established:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Contact must be established")

if service.get_messages_count_from_sender(contact, current_user) >= config.MAX_SENT_MESSAGES:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Limit of the messages has been exceeded")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Limit of the messages is exceeded")

message = service.create_message(contact, sender=current_user, message_in=message_in)
message_out = MessageOut.model_validate(message, from_attributes=True)
notification_manager.put_notification(message_out, recipient_id=target_user.id, notification_type=NotificationType.MESSAGE)
notification_manager.put_notification(
message_out, recipient_id=target_user.id, notification_type=NotificationType.MESSAGE
)
return message_out
36 changes: 23 additions & 13 deletions src/contacts/schemas.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from datetime import datetime
from functools import cached_property
from typing import Any, Self

from pydantic import AliasChoices, BaseModel, Field, constr, computed_field
from bson import ObjectId
from pydantic import AliasChoices, BaseModel, Field, constr, computed_field, model_validator

from contacts import config
from contacts.enums import ContactState
from contacts.enums import ContactState, ContactUpdateAction
from models import PydanticObjectId, DateTimeFromObjectId
from users.schemas import UserPublicOut

Expand All @@ -25,33 +27,41 @@ class MessageOut(MessageBase):
class ContactBaseOut(BaseModel):
initiator: UserPublicOut | PydanticObjectId = Field(exclude=True)
respondent: UserPublicOut | PydanticObjectId = Field(exclude=True)
opposite_user: UserPublicOut | None = Field(exclude=True, default=None)
initiator_last_update_at: datetime = Field(exclude=True)
respondent_last_update_at: datetime | None = Field(exclude=True)

opposite_user: UserPublicOut | None = None
opposite_user_last_update_at: datetime | None = None
created_at: DateTimeFromObjectId = Field(validation_alias=AliasChoices("_id", "id"), )

@computed_field
@cached_property
def user(self) -> UserPublicOut | None:
@model_validator(mode="after")
def assign_opposite_user(self) -> Self:
""" This is the opposite user resolver for particular user """
if self.opposite_user:
return self.opposite_user
return self
if isinstance(self.initiator, UserPublicOut) and isinstance(self.respondent, PydanticObjectId):
return self.initiator
self.opposite_user = self.initiator
self.opposite_user_last_update_at = self.initiator_last_update_at
elif isinstance(self.respondent, UserPublicOut) and isinstance(self.initiator, PydanticObjectId):
return self.respondent
self.opposite_user = self.respondent
self.opposite_user_last_update_at = self.respondent_last_update_at
else:
ValueError("Incorrect input")

return None
return self


class ContactOut(ContactBaseOut):
status: ContactState | None
messages: list[MessageOut]


class ContactStateIn(BaseModel):
action: ContactState
class ContactUpdateIn(BaseModel):
action: ContactUpdateAction


class ContactCreateDataIn(ContactStateIn):
class ContactCreateDataIn(BaseModel):
state: ContactState
user_id: PydanticObjectId


Loading

0 comments on commit a7280d2

Please sign in to comment.