models.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. from six.moves.urllib.parse import urlparse
  2. from mptt.managers import TreeManager
  3. from mptt.models import MPTTModel, TreeForeignKey
  4. from django.core.urlresolvers import reverse
  5. from django.db import models
  6. from django.utils.translation import ugettext_lazy as _
  7. from misago.acl import version as acl_version
  8. from misago.acl.models import BaseRole
  9. from misago.conf import settings
  10. from misago.core.cache import cache
  11. from misago.core.utils import slugify
  12. from misago.threads.threadtypes import trees_map
  13. CACHE_NAME = 'misago_categories_tree'
  14. THREADS_ROOT_NAME = 'root_category'
  15. class CategoryManager(TreeManager):
  16. def private_threads(self):
  17. return self.get_special('private_threads')
  18. def root_category(self):
  19. return self.get_special('root_category')
  20. def get_special(self, special_role):
  21. cache_name = '%s_%s' % (CACHE_NAME, special_role)
  22. special_category = cache.get(cache_name, 'nada')
  23. if special_category == 'nada':
  24. special_category = self.get(special_role=special_role)
  25. cache.set(cache_name, special_category)
  26. return special_category
  27. def all_categories(self, include_root=False):
  28. tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
  29. queryset = self.filter(tree_id=tree_id)
  30. if not include_root:
  31. queryset = queryset.filter(level__gt=0)
  32. return queryset.order_by('lft')
  33. def get_cached_categories_dict(self):
  34. categories_dict = cache.get(CACHE_NAME, 'nada')
  35. if categories_dict == 'nada':
  36. categories_dict = self.get_categories_dict_from_db()
  37. cache.set(CACHE_NAME, categories_dict)
  38. return categories_dict
  39. def get_categories_dict_from_db(self):
  40. categories_dict = {}
  41. for category in self.all_categories(include_root=True):
  42. categories_dict[category.pk] = category
  43. return categories_dict
  44. def clear_cache(self):
  45. cache.delete(CACHE_NAME)
  46. class Category(MPTTModel):
  47. parent = TreeForeignKey(
  48. 'self',
  49. null=True,
  50. blank=True,
  51. related_name='children'
  52. )
  53. special_role = models.CharField(max_length=255, null=True, blank=True)
  54. name = models.CharField(max_length=255)
  55. slug = models.CharField(max_length=255)
  56. description = models.TextField(null=True, blank=True)
  57. is_closed = models.BooleanField(default=False)
  58. threads = models.PositiveIntegerField(default=0)
  59. posts = models.PositiveIntegerField(default=0)
  60. last_post_on = models.DateTimeField(null=True, blank=True)
  61. last_thread = models.ForeignKey(
  62. 'misago_threads.Thread',
  63. related_name='+',
  64. null=True,
  65. blank=True,
  66. on_delete=models.SET_NULL
  67. )
  68. last_thread_title = models.CharField(max_length=255, null=True, blank=True)
  69. last_thread_slug = models.CharField(max_length=255, null=True, blank=True)
  70. last_poster = models.ForeignKey(
  71. settings.AUTH_USER_MODEL,
  72. related_name='+',
  73. null=True,
  74. blank=True,
  75. on_delete=models.SET_NULL
  76. )
  77. last_poster_name = models.CharField(max_length=255, null=True, blank=True)
  78. last_poster_slug = models.CharField(max_length=255, null=True, blank=True)
  79. prune_started_after = models.PositiveIntegerField(default=0)
  80. prune_replied_after = models.PositiveIntegerField(default=0)
  81. archive_pruned_in = models.ForeignKey(
  82. 'self',
  83. related_name='pruned_archive',
  84. null=True,
  85. blank=True,
  86. on_delete=models.SET_NULL
  87. )
  88. css_class = models.CharField(max_length=255, null=True, blank=True)
  89. objects = CategoryManager()
  90. @property
  91. def thread_type(self):
  92. return trees_map.get_type_for_tree_id(self.tree_id)
  93. def __unicode__(self):
  94. return unicode(self.thread_type.get_category_name(self))
  95. def lock(self):
  96. return Category.objects.select_for_update().get(id=self.id)
  97. def delete(self, *args, **kwargs):
  98. Category.objects.clear_cache()
  99. acl_version.invalidate()
  100. return super(Category, self).delete(*args, **kwargs)
  101. def synchronize(self):
  102. self.threads = self.thread_set.filter(is_unapproved=False).count()
  103. if self.threads:
  104. replies_sum = self.thread_set.aggregate(models.Sum('replies'))
  105. self.posts = self.threads + replies_sum['replies__sum']
  106. else:
  107. self.posts = 0
  108. if self.threads:
  109. last_thread_qs = self.thread_set.filter(is_unapproved=False)
  110. last_thread = last_thread_qs.order_by('-last_post_on')[:1][0]
  111. self.set_last_thread(last_thread)
  112. else:
  113. self.empty_last_thread()
  114. def delete_content(self):
  115. from .signals import delete_category_content
  116. delete_category_content.send(sender=self)
  117. def move_content(self, new_category):
  118. from .signals import move_category_content
  119. move_category_content.send(sender=self, new_category=new_category)
  120. def get_absolute_url(self):
  121. return self.thread_type.get_category_absolute_url(self)
  122. def get_last_thread_url(self):
  123. return self.thread_type.get_category_last_thread_url(self)
  124. def get_last_post_url(self):
  125. return self.thread_type.get_category_last_post_url(self)
  126. def get_api_read_url(self):
  127. return self.thread_type.get_category_api_read_url(self)
  128. def set_name(self, name):
  129. self.name = name
  130. self.slug = slugify(name)
  131. def set_last_thread(self, thread):
  132. self.last_post_on = thread.last_post_on
  133. self.last_thread = thread
  134. self.last_thread_title = thread.title
  135. self.last_thread_slug = thread.slug
  136. self.last_poster = thread.last_poster
  137. self.last_poster_name = thread.last_poster_name
  138. self.last_poster_slug = thread.last_poster_slug
  139. def empty_last_thread(self):
  140. self.last_post_on = None
  141. self.last_thread = None
  142. self.last_thread_title = None
  143. self.last_thread_slug = None
  144. self.last_poster = None
  145. self.last_poster_name = None
  146. self.last_poster_slug = None
  147. def has_child(self, child):
  148. return child.lft > self.lft and child.rght < self.rght
  149. class CategoryRole(BaseRole):
  150. pass
  151. class RoleCategoryACL(models.Model):
  152. role = models.ForeignKey('misago_acl.Role', related_name='categories_acls')
  153. category = models.ForeignKey('Category', related_name='category_role_set')
  154. category_role = models.ForeignKey(CategoryRole)