Skip to content

Commit

Permalink
adds conversations app for n-party communications
Browse files Browse the repository at this point in the history
  • Loading branch information
jontsai committed May 8, 2024
1 parent 2b5aac2 commit 1475ec1
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 37 deletions.
Empty file added apps/conversations/__init__.py
Empty file.
92 changes: 92 additions & 0 deletions apps/conversations/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Django Imports
from django.contrib import admin

# HTK Imports
from htk.utils import htk_setting
from htk.utils.general import resolve_model_dynamically


Conversation = resolve_model_dynamically(htk_setting('HTK_CONVERSATION_MODEL'))
ConversationMessage = resolve_model_dynamically(
htk_setting('HTK_CONVERSATION_MESSAGE_MODEL')
)
ConversationParticipant = resolve_model_dynamically(
htk_setting('HTK_CONVERSATION_PARTICIPANT_MODEL')
)


class ConversationParticipantInline(admin.TabularInline):
model = ConversationParticipant
extra = 0


class ConversationMessageInline(admin.TabularInline):
model = ConversationMessage
extra = 0


class BaseConversationAdmin(admin.ModelAdmin):
model = Conversation

list_display = (
'id',
'topic',
'description',
'num_participants',
'num_messages',
'created_by',
'created_at',
'updated_at',
)

inlines = (
ConversationParticipantInline,
# NOTE: This will crash if the there are too many messages in the conversation
ConversationMessageInline,
)

search_fields = (
'topic',
'description',
'created_by__username',
'created_by__first_name',
'created_by__last_name',
)


class BaseConversationParticipantAdmin(admin.ModelAdmin):
model = ConversationParticipant

list_display = (
'id',
'conversation',
'user',
'joined_at',
)

search_fields = (
'conversation__topic',
'user__username',
'user__first_name',
'user__last_name',
)

list_filters = (
'conversation',
'user',
)


class BaseConversationMessageAdmin(admin.ModelAdmin):
model = ConversationMessage

list_display = (
'id',
'conversation',
'author',
'reply_to',
'content',
'posted_at',
'edited_at',
'deleted_at',
)
Empty file.
4 changes: 4 additions & 0 deletions apps/conversations/constants/defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
HTK_CONVERSATION_MODEL = None
HTK_CONVERSATION_PARTICIPANT_MODEL = None
HTK_CONVERSATION_MESSAGE_MODEL = None
HTK_CONVERSATION_MESSAGE_MAX_LENGTH = 2048
19 changes: 19 additions & 0 deletions apps/conversations/fk_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Django Imports
from django.db import models

# HTK Imports
from htk.models.fk_fields import build_kwargs
from htk.utils import htk_setting


def fk_conversation(
related_name: str,
required: bool = False,
**kwargs,
) -> models.ForeignKey:
field = models.ForeignKey(
htk_setting('HTK_CONVERSATION_MODEL'),
related_name=related_name,
**build_kwargs(required=required, **kwargs),
)
return field
126 changes: 126 additions & 0 deletions apps/conversations/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Django Imports
from django.db import models

# HTK Imports
from htk.apps.conversations.fk_fields import fk_conversation
from htk.models.fk_fields import fk_user
from htk.utils import htk_setting


# isort: off


class BaseConversation(models.Model):
"""A base conversation class which is extensible
A conversation is a collection of messages between `n` participants, where `n >= 2`.
When `n` is:
- `2`, it is a private conversation, or a direct message ("DM")
- `>2`, it is a group conversation
"""

topic = models.CharField(max_length=256, blank=True)
description = models.TextField(max_length=1024, blank=True)
created_by = fk_user(related_name='created_conversations', required=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
abstract = True

def __str__(self):
value = '%s (#%s)' % (
self.topic or '<No Topic>',
self.id,
)
return value

@property
def num_participants(self):
num = self.participants.count()
return num

@property
def num_messages(self):
num = self.messages.count()
return num


class BaseConversationParticipant(models.Model):
"""A participant in a conversation
This is a many-to-many relationship between a User and a Conversation
For n-party conversations, there will be n-1 ConversationParticipants
"""

conversation = fk_conversation(related_name='participants', required=True)
user = fk_user(related_name='conversation_participants', required=True)
joined_at = models.DateTimeField(auto_now_add=True)

class Meta:
abstract = True

def __str__(self):
value = '%s - %s' % (
self.conversation,
self.user,
)
return value


class BaseConversationMessage(models.Model):
"""A message in a conversation
A conversation message is a message that belongs to a conversation.
A conversation message is visible to all participants in the conversation.
"""

conversation = fk_conversation(related_name='messages', required=True)
# `author` is not required when the message is system-generated
author = fk_user(
related_name='authored_conversation_messages', required=False
)
reply_to = models.ForeignKey(
'self',
related_name='replies',
blank=True,
null=True,
on_delete=models.CASCADE,
)
content = models.TextField(
max_length=htk_setting('HTK_CONVERSATION_MESSAGE_MAX_LENGTH')
)
posted_at = models.DateTimeField(auto_now_add=True)
edited_at = models.DateTimeField(blank=True, null=True)
# soft-deletion: app will hide messages that are "deleted"; but also allow for messages to be "undeleted"
deleted_at = models.DateTimeField(blank=True, null=True)

class Meta:
abstract = True

def __str__(self):
value = '%s - %s' % (
self.conversation,
self.author,
)
return value

@property
def was_edited(self):
return self.edited_at is not None and self.edited_at > self.posted_at

@property
def is_deleted(self):
return self.deleted_at is not None

def save(self, **kwargs):
"""Saves this message.
Side effect: also performs any customizations, like updating cache, etc
"""
super().save(**kwargs)

# force update on `Conversation.updated_at`
self.conversation.save()
80 changes: 43 additions & 37 deletions apps/forums/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,43 @@


class Forum(models.Model):
"""Forum represents a message forum
"""
name = models.CharField(max_length=64)
description = models.CharField(max_length=128, blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
"""Forum represents a message forum"""

name = models.CharField(max_length=128)
description = models.CharField(max_length=256, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
app_label = 'htk'

def __str__(self):
value = '%s' % (
self.name,
)
value = '%s' % (self.name,)
return value

@property
def recent_thread(self):
"""Retrieves the most recent ForumThread
"""
ordered_threads = self.threads.order_by('-updated')
if len(ordered_threads):
thread = ordered_threads[0]
else:
thread = None
"""Retrieves the most recent ForumThread"""
thread = self.threads.order_by('-updated').first()
return thread

def num_threads(self):
@property
def num_threads(self) -> int:
num = self.threads.count()
return num

def num_messages(self):
num = 0
for thread in self.threads.all():
num += thread.num_messages()
return num
@property
def num_messages(self) -> int:
total = sum(thread.num_messages for thread in self.threads.all())
return total


class ForumThread(models.Model):
forum = models.ForeignKey(Forum, related_name='threads')
subject = models.CharField(max_length=128)
author = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='authored_threads')
author = models.ForeignKey(
settings.AUTH_USER_MODEL, related_name='authored_threads'
)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
# status
Expand All @@ -66,25 +62,25 @@ def __str__(self):
)
return value

@property
def num_messages(self):
num = self.messages.count()
return num

@property
def recent_message(self):
"""Retrieves the most recent message in ForumThread
Requires all ForumThreads to have at least one message
"""
ordered_messages = self.messages.order_by('-timestamp')
if len(ordered_messages):
message = ordered_messages[0]
else:
message = None
message = self.messages.order_by('-timestamp').first()
return message


class ForumMessage(models.Model):
thread = models.ForeignKey(ForumThread, related_name='messages')
author = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='messages')
author = models.ForeignKey(
settings.AUTH_USER_MODEL, related_name='messages'
)
reply_to = models.ForeignKey(
'self',
related_name='replies',
Expand All @@ -93,7 +89,8 @@ class ForumMessage(models.Model):
on_delete=models.CASCADE,
)
text = models.TextField(max_length=3000)
timestamp = models.DateTimeField(auto_now=True)
posted_at = models.DateTimeField(auto_now_add=True)
edited_at = models.DateTimeField(auto_now=True)
tags = models.ManyToManyField('ForumTag', blank=True)

class Meta:
Expand All @@ -103,21 +100,30 @@ class Meta:
def __str__(self):
return 'ForumMessage %s' % (self.id,)

@property
def was_edited(self):
return self.edited_at > self.posted_at

@property
def snippet(self):
snippet = (self.text[:FORUM_SNIPPET_LENGTH] + '...') if len(self.text) > FORUM_SNIPPET_LENGTH else self.text
snippet = (
(self.text[:FORUM_SNIPPET_LENGTH] + '...')
if len(self.text) > FORUM_SNIPPET_LENGTH
else self.text
)
return snippet

def save(self, **kwargs):
"""Any customizations, like updating cache, etc
"""
"""Any customizations, like updating cache, etc"""
super(ForumMessage, self).save(**kwargs)
self.thread.save() # update ForumThread.updated timestamp
# force update on `ForumThread.updated_at`
self.thread.save()


class ForumTag(models.Model):
"""ForumTag can either apply to ForumThread or ForumMessage
"""
name = models.CharField(max_length=32)
"""ForumTag can either apply to ForumThread or ForumMessage"""

name = models.CharField(max_length=64)

class Meta:
app_label = 'htk'
Expand Down
1 change: 1 addition & 0 deletions constants/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
from htk.apps.accounts.constants.defaults import *
from htk.apps.bible.constants.defaults import *
from htk.apps.changelog.constants.defaults import *
from htk.apps.conversations.constants.defaults import *
from htk.apps.cpq.constants.defaults import *
from htk.apps.file_storage.constants.defaults import *
from htk.apps.geolocations.constants.defaults import *
Expand Down

0 comments on commit 1475ec1

Please sign in to comment.