project/threads/models.py
import datetime
import json
import math
import os
from calendar import month_name
from categories.models import Category
from common.utils import PathAndRename
from core.constants import CIVI_TYPES
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.files.storage import default_storage
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from taggit.managers import TaggableManager
class Fact(models.Model):
body = models.CharField(max_length=511)
created = models.DateTimeField(auto_now_add=True, blank=True, null=True)
last_modified = models.DateTimeField(auto_now=True, blank=True, null=True)
class ThreadManager(models.Manager):
def summarize(self, thread):
# Number of characters after which to truncate thread
thread_truncate_length = 320
# If thread length is longer than truncate length... add elipsis (truncate)
ellipsis_if_too_long = (
"" if len(thread.summary) <= thread_truncate_length else "..."
)
thread_data = {
"id": thread.id,
"title": thread.title,
"summary": thread.summary[:thread_truncate_length] + (ellipsis_if_too_long),
"created": f"""{month_name[thread.created.month]}
{thread.created.day},
{thread.created.year}""",
"category_id": thread.category.id,
"image": thread.image_url,
}
author_data = {
"username": thread.author.username,
"full_name": thread.author.profile.full_name,
"profile_image": thread.author.profile.profile_image_url,
}
stats_data = {
"num_views": thread.num_views,
"num_civis": Civi.objects.all()
.filter(thread_id=thread.id)
.count(), # thread.num_civis,
"num_solutions": thread.num_solutions,
}
data = {"thread": thread_data, "author": author_data, "stats": stats_data}
return data
def filter_by_category(self, categories):
return self.all().filter(category__in=categories)
class Thread(models.Model):
author = models.ForeignKey(
get_user_model(),
default=None,
null=True,
on_delete=models.PROTECT,
related_name="threads",
)
category = models.ForeignKey(
Category,
default=None,
null=True,
on_delete=models.PROTECT,
related_name="threads",
)
facts = models.ManyToManyField(Fact)
tags = TaggableManager()
title = models.CharField(max_length=127, blank=False, null=False)
summary = models.CharField(max_length=4095, blank=False, null=False)
image = models.ImageField(
upload_to=PathAndRename("thread_uploads"), blank=True, null=True
)
def __str__(self):
return self.title
def __unicode__(self):
return self.title
@property
def image_url(self): # TODO: move this to utils
if self.image and default_storage.exists(
os.path.join(settings.MEDIA_ROOT, self.image.name)
):
return self.image.url
else:
# NOTE: This default url will probably be changed later
return "/static/img/no_image_md.png"
created = models.DateTimeField(auto_now_add=True, blank=True, null=True)
last_modified = models.DateTimeField(auto_now=True, blank=True, null=True)
# Allow draft stage threads (default to True)
is_draft = models.BooleanField(default=True)
num_views = models.IntegerField(default=0)
num_civis = models.IntegerField(default=0) # TODO To be dropped
num_solutions = models.IntegerField(default=0) # TODO To be dropped
objects = ThreadManager()
@property
def created_date_str(self):
d = self.created
return f"{month_name[d.month]} {d.day}, {d.year}"
@property
def contributors(self):
return get_user_model().objects.filter(
pk__in=self.civis.order_by("-created")
.values_list("author", flat=True)
.order_by("author")
.distinct()
)
@property
def problem_civis(self):
return self.civis.filter(c_type="problem")
@property
def cause_civis(self):
return self.civis.filter(c_type="cause")
@property
def solution_civis(self):
return self.civis.filter(c_type="solution")
class CiviManager(models.Manager):
def summarize(self, civi):
return {
"id": civi.id,
"type": civi.c_type,
"title": civi.title,
"body": civi.body[:150],
}
def serialize(self, civi, filter=None):
data = {
"type": civi.c_type,
"title": civi.title,
"body": civi.body,
"author": {
"username": civi.author.username,
"profile_image": civi.author.profile.profile_image_url,
"first_name": civi.author.first_name,
"last_name": civi.author.last_name,
},
"tags": [tag.title for tag in civi.tags.all()],
"created": f"""{month_name[civi.created.month]}
{civi.created.day},
{civi.created.year}""",
"attachments": [],
"votes": civi.votes,
"id": civi.id,
"thread_id": civi.thread.id,
}
if filter and filter in data:
return json.dumps({filter: data[filter]})
return json.dumps(data, cls=DjangoJSONEncoder)
def serialize_s(self, civi, filter=None):
data = {
"type": civi.c_type,
"title": civi.title,
"body": civi.body,
"author": dict(
username=civi.author.username,
profile_image=civi.author.profile.profile_image_url,
first_name=civi.author.first_name,
last_name=civi.author.last_name,
),
"tags": [h.title for h in civi.tags.all()],
"created": f"""{month_name[civi.created.month]}
{civi.created.day},
{civi.created.year}""",
"attachments": [],
"votes": civi.votes,
"id": civi.id,
"thread_id": civi.thread.id,
"links": [
civi for civi in civi.linked_civis.all().values_list("id", flat=True)
],
}
if filter and filter in data:
return json.dumps({filter: data[filter]})
return data
def thread_sorted_by_score(self, civis_queryset, requested_user_id):
queryset = civis_queryset.order_by("-created")
return sorted(
queryset.all(), key=lambda c: c.score(requested_user_id), reverse=True
)
class Civi(models.Model):
objects = CiviManager()
author = models.ForeignKey(
get_user_model(),
related_name="civis",
default=None,
null=True,
on_delete=models.PROTECT,
)
thread = models.ForeignKey(
Thread,
related_name="civis",
default=None,
null=True,
on_delete=models.PROTECT,
)
tags = TaggableManager()
linked_civis = models.ManyToManyField("self", related_name="links", symmetrical=False, blank=True)
title = models.CharField(max_length=255, blank=False, null=False)
body = models.CharField(max_length=1023, blank=False, null=False)
c_type = models.CharField(max_length=31, default="problem", choices=CIVI_TYPES)
votes_vneg = models.IntegerField(default=0)
votes_neg = models.IntegerField(default=0)
votes_neutral = models.IntegerField(default=0)
votes_pos = models.IntegerField(default=0)
votes_vpos = models.IntegerField(default=0)
def __str__(self):
return self.title
def __unicode__(self):
return self.title
def _get_votes(self):
activity_votes = Activity.objects.filter(civi=self)
votes = {
"total": activity_votes.count()
- activity_votes.filter(activity_type="vote_neutral").count(),
"votes_vneg": activity_votes.filter(activity_type="vote_vneg").count(),
"votes_neg": activity_votes.filter(activity_type="vote_neg").count(),
"votes_neutral": activity_votes.filter(
activity_type="vote_neutral"
).count(),
"votes_pos": activity_votes.filter(activity_type="vote_pos").count(),
"votes_vpos": activity_votes.filter(activity_type="vote_vpos").count(),
}
return votes
votes = property(_get_votes)
created = models.DateTimeField(auto_now_add=True, blank=True, null=True)
last_modified = models.DateTimeField(auto_now=True, blank=True, null=True)
@property
def created_date_str(self):
d = self.created
return f"{month_name[d.month]} {d.day}, {d.year}"
def score(self, requested_user_id=None):
# TODO: add docstring comment describing this score function
# in relatively plain English
# include descriptions of all variables
# Weights for different vote types
vneg_weight = -2
neg_weight = -1
pos_weight = 1
vpos_weight = 2
post_time = self.created
current_time = datetime.datetime.now()
# Get all votes
votes = self.votes
# Score each each type of vote, based on count for that type
vneg_score = votes["votes_vneg"] * vneg_weight
neg_score = votes["votes_neg"] * neg_weight
pos_score = votes["votes_pos"] * pos_weight
vpos_score = votes["votes_vpos"] * vpos_weight
# Sum up all of the scores
scores_sum = vneg_score + neg_score + pos_score + vpos_score
if requested_user_id:
profile = get_user_model().objects.get(id=requested_user_id).profile
scores_sum = (
1
if self.author.profile
in profile.following.all().values_list("id", flat=True)
else 0
)
else:
scores_sum = 0
favorite = 0
# Calculate how long ago the post was created
time_ago = (current_time - post_time.replace(tzinfo=None)).total_seconds() / 300
gravity = 1 # TODO: determine what the variable 'g' does
amp = math.pow(10, 0)
# Calculate rank based on positive, zero, or negative scores sum
if scores_sum > 0:
# TODO: determine why we set votes total to two when votes['total'] is <= 1
# set votes total to 2 when votes['total'] is <= 1
votes_total = votes["total"] if votes["total"] > 1 else 2
# step3 - A X*Log10V+Y + F + (##/T) = Rank Value
rank = (
scores_sum * math.log10(votes_total) * amp
+ scores_sum
+ favorite
+ gravity / time_ago
)
elif scores_sum == 0:
# Get count of total votes
votes_total = votes["total"]
# step3 - B V^2+Y + F + (##/T) = Rank Value
rank = votes_total**2 + scores_sum + favorite + gravity / time_ago
elif scores_sum < 0:
# TODO: determine why we set votes total to two when votes['tota'] is <= 1
# set votes total to 2 when votes['total'] is <= 1
votes_total = votes["total"] if votes["total"] > 1 else 2
# step3 - C
if abs(scores_sum) / votes_total <= 5:
rank = (
abs(scores_sum) * math.log10(votes_total) * amp
+ scores_sum
+ favorite
+ gravity / time_ago
)
else:
rank = (
scores_sum * math.log10(votes_total) * amp
+ scores_sum
+ favorite
+ gravity / time_ago
)
return rank
def dict_with_score(self, requested_user_id=None):
data = {
"id": self.id,
"thread_id": self.thread.id,
"type": self.c_type,
"title": self.title,
"body": self.body,
"author": {
"username": self.author.username,
"profile_image": self.author.profile.profile_image_url,
"profile_image_thumb_url": self.author.profile.profile_image_thumb_url,
"first_name": self.author.first_name,
"last_name": self.author.last_name,
},
"votes": self.votes,
"links": [
civi for civi in self.linked_civis.all().values_list("id", flat=True)
],
"created": self.created_date_str,
# Not Implemented Yet
"tags": [],
"attachments": [
{"id": img.id, "url": img.image_url} for img in self.images.all()
],
}
if requested_user_id:
data["score"] = self.score(requested_user_id)
return data
class Response(models.Model):
author = models.ForeignKey(
get_user_model(),
default=None,
null=True,
on_delete=models.PROTECT,
related_name="responses",
)
civi = models.ForeignKey(
Civi,
default=None,
null=True,
on_delete=models.PROTECT,
related_name="responses",
)
title = models.CharField(max_length=127)
body = models.TextField(max_length=2047)
votes_vneg = models.IntegerField(default=0)
votes_neg = models.IntegerField(default=0)
votes_neutral = models.IntegerField(default=0)
votes_pos = models.IntegerField(default=0)
votes_vpos = models.IntegerField(default=0)
created = models.DateTimeField(auto_now_add=True, blank=True, null=True)
last_modified = models.DateTimeField(auto_now=True, blank=True, null=True)
class CiviImageManager(models.Manager):
def get_images(self):
return
class CiviImage(models.Model):
objects = CiviImageManager()
civi = models.ForeignKey(
Civi,
related_name="images",
on_delete=models.PROTECT,
)
title = models.CharField(max_length=255, null=True, blank=True)
image = models.ImageField(
upload_to=PathAndRename("civi_uploads"), null=True, blank=True
)
created = models.DateTimeField(auto_now_add=True, blank=True, null=True)
@property
def image_url(self):
if self.image and default_storage.exists(
os.path.join(settings.MEDIA_ROOT, self.image.name)
):
return self.image.url
else:
# NOTE: This default url will probably be changed later
return "/static/img/no_image_md.png"
class ActivityManager(models.Manager):
def votes(self, civi_id):
civi = Civi.objects.get(id=civi_id)
votes = dict(
votes_vneg=civi.votes_vneg,
votes_neg=civi.votes_neg,
votes_neutral=civi.votes_neutral,
votes_pos=civi.votes_pos,
votes_vpos=civi.votes_vpos,
)
return votes
class Activity(models.Model):
user = models.ForeignKey(
get_user_model(),
default=None,
null=True,
on_delete=models.PROTECT,
related_name="activities",
)
thread = models.ForeignKey(
Thread,
default=None,
null=True,
on_delete=models.PROTECT,
related_name="activities",
)
civi = models.ForeignKey(
Civi,
default=None,
null=True,
on_delete=models.PROTECT,
related_name="activities",
)
activity_CHOICES = (
("vote_vneg", "Vote Strongly Disagree"),
("vote_neg", "Vote Disagree"),
("vote_neutral", "Vote Neutral"),
("vote_pos", "Vote Agree"),
("vote_vpos", "Vote Strongly Agree"),
("favorite", "Favor a Civi"),
)
activity_type = models.CharField(max_length=255, choices=activity_CHOICES)
read = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True, blank=True, null=True)
last_modified = models.DateTimeField(auto_now=True, blank=True, null=True)
@property
def is_positive_vote(self):
return self.activity_type.endswith("pos")
@property
def is_negative_vote(self):
return self.activity_type.endswith("neg")
class Meta:
verbose_name_plural = "Activities"
class Rebuttal(models.Model):
author = models.ForeignKey(
get_user_model(),
default=None,
null=True,
on_delete=models.PROTECT,
related_name="rebuttals",
)
response = models.ForeignKey(
Response,
default=None,
null=True,
on_delete=models.PROTECT,
related_name="rebuttals",
)
body = models.TextField(max_length=1023)
votes_vneg = models.IntegerField(default=0)
votes_neg = models.IntegerField(default=0)
votes_neutral = models.IntegerField(default=0)
votes_pos = models.IntegerField(default=0)
votes_vpos = models.IntegerField(default=0)
created = models.DateTimeField(auto_now_add=True, blank=True, null=True)
last_modified = models.DateTimeField(auto_now=True, blank=True, null=True)
class Rationale(models.Model):
title = models.CharField(max_length=127)
body = models.TextField(max_length=4095)
votes_vneg = models.IntegerField(default=0)
votes_neg = models.IntegerField(default=0)
votes_neutral = models.IntegerField(default=0)
votes_pos = models.IntegerField(default=0)
votes_vpos = models.IntegerField(default=0)
created = models.DateTimeField(auto_now_add=True, blank=True, null=True)
last_modified = models.DateTimeField(auto_now=True, blank=True, null=True)