Permalink
Browse files

Admin area, configurable badges and general workflow, better settings…

…. Badges are now awarded automaticaly instead of waiting for a cron job. Several bug fixes.
  • Loading branch information...
1 parent 5aecabc commit 526540bef31137779e4d0fb73cddd07a09464674 @hrcerqueira hrcerqueira committed Mar 15, 2010
Showing with 1,532 additions and 385 deletions.
  1. 0 .gitignore
  2. +161 −178 .idea/workspace.xml
  3. 0 .project
  4. 0 .pydevproject
  5. 0 HOW_TO_DEBUG
  6. 0 INSTALL
  7. 0 INSTALL.pip
  8. 0 INSTALL.webfaction
  9. 0 LICENSE
  10. 0 PENDING
  11. 0 ROADMAP.rst
  12. 0 WISH_LIST
  13. 0 __init__.py
  14. 0 cron/README
  15. 0 cron/send_email_alerts
  16. 0 cron/send_email_alerts_virtualenv
  17. 0 dos2unix.sh
  18. +48 −90 forum/auth.py
  19. +6 −6 forum/badges/__init__.py
  20. +99 −5 forum/badges/base.py
  21. +13 −3 forum/models/__init__.py
  22. +3 −2 forum/models/base.py
  23. +2 −2 forum/models/question.py
  24. +2 −5 forum/models/repute.py
  25. +5 −1 forum/models/tag.py
  26. +79 −0 forum/models/utils.py
  27. +18 −3 forum/modules.py
  28. +287 −0 forum/settings/__init__.py
  29. +106 −0 forum/settings/base.py
  30. +29 −0 forum/settings/forms.py
  31. +28 −0 forum/settings/pages.py
  32. +33 −0 forum/skins/default/media/style/admin.css
  33. +4 −24 forum/skins/default/templates/about.html
  34. +9 −1 forum/skins/default/templates/auth/signin.html
  35. +3 −0 forum/skins/default/templates/header.html
  36. +45 −0 forum/skins/default/templates/osqaadmin/base.html
  37. +10 −0 forum/skins/default/templates/osqaadmin/index.html
  38. +5 −0 forum/startup.py
  39. +5 −0 forum/urls.py
  40. +1 −0 forum/views/__init__.py
  41. +42 −0 forum/views/admin.py
  42. +8 −3 forum/views/auth.py
  43. +7 −6 forum/views/commands.py
  44. +2 −1 forum/views/meta.py
  45. +1 −1 forum/views/readers.py
  46. +1 −1 forum/views/users.py
  47. +16 −11 forum/views/writers.py
  48. 0 forum_modules/default_badges/__init__.py
  49. +228 −0 forum_modules/default_badges/badges.py
  50. +140 −0 forum_modules/default_badges/settings.py
  51. +3 −1 forum_modules/facebookauth/authentication.py
  52. +17 −3 forum_modules/facebookauth/settings.py
  53. +18 −2 forum_modules/oauthauth/settings.py
  54. +3 −2 forum_modules/pgfulltext/__init__.py
  55. +1 −1 forum_modules/pgfulltext/pg_fts_install.sql
  56. +16 −0 forum_modules/robotstxt/settings.py
  57. +3 −2 forum_modules/robotstxt/urls.py
  58. +5 −0 forum_modules/sphinxfulltext/__init__.py
  59. BIN locale/en/LC_MESSAGES/django.mo
  60. 0 locale/en/LC_MESSAGES/django.po
  61. BIN locale/es/LC_MESSAGES/django.mo
  62. 0 locale/es/LC_MESSAGES/django.po
  63. BIN locale/zh_CN/LC_MESSAGES/django.mo
  64. 0 locale/zh_CN/LC_MESSAGES/django.po
  65. +3 −1 manage.py
  66. 0 osqa-requirements.txt
  67. 0 osqa.wsgi.dist
  68. +3 −4 settings.py
  69. +12 −21 settings_local.py.dist
  70. 0 sphinx/sphinx.conf
  71. 0 sql_scripts/091111_upgrade_evgeny.sql
  72. 0 sql_scripts/091208_upgrade_evgeny.sql
  73. 0 sql_scripts/091208_upgrade_evgeny_1.sql
  74. 0 sql_scripts/100108_upgrade_ef.sql
  75. +1 −1 sql_scripts/badges.sql
  76. 0 sql_scripts/cnprog.xml
  77. 0 sql_scripts/cnprog_new_install.sql
  78. 0 sql_scripts/cnprog_new_install_2009_02_28.sql
  79. 0 sql_scripts/cnprog_new_install_2009_03_31.sql
  80. 0 sql_scripts/cnprog_new_install_2009_04_07.sql
  81. 0 sql_scripts/cnprog_new_install_2009_04_09.sql
  82. 0 sql_scripts/drop-all-tables.sh
  83. 0 sql_scripts/drop-auth.sql
  84. 0 sql_scripts/pg_fts_install.sql
  85. 0 sql_scripts/update_2009_01_13_001.sql
  86. 0 sql_scripts/update_2009_01_13_002.sql
  87. 0 sql_scripts/update_2009_01_18_001.sql
  88. 0 sql_scripts/update_2009_01_24.sql
  89. 0 sql_scripts/update_2009_01_25_001.sql
  90. 0 sql_scripts/update_2009_02_26_001.sql
  91. 0 sql_scripts/update_2009_04_10_001.sql
  92. 0 sql_scripts/update_2009_07_05_EF.sql
  93. 0 sql_scripts/update_2009_12_24_001.sql
  94. 0 sql_scripts/update_2009_12_27_001.sql
  95. 0 sql_scripts/update_2009_12_27_002.sql
  96. 0 sql_scripts/update_2010_02_22.sql
  97. +1 −4 urls.py
View
0 .gitignore 100644 → 100755
No changes.
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
0 .project 100644 → 100755
No changes.
View
0 .pydevproject 100644 → 100755
No changes.
View
0 HOW_TO_DEBUG 100644 → 100755
No changes.
View
0 INSTALL 100644 → 100755
No changes.
View
0 INSTALL.pip 100644 → 100755
No changes.
View
0 INSTALL.webfaction 100644 → 100755
No changes.
View
0 LICENSE 100644 → 100755
No changes.
View
0 PENDING 100644 → 100755
No changes.
View
0 ROADMAP.rst 100644 → 100755
No changes.
View
0 WISH_LIST 100644 → 100755
No changes.
View
0 __init__.py 100644 → 100755
No changes.
View
0 cron/README 100644 → 100755
No changes.
View
0 cron/send_email_alerts 100644 → 100755
No changes.
View
0 cron/send_email_alerts_virtualenv 100644 → 100755
No changes.
View
0 dos2unix.sh 100644 → 100755
No changes.
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -1,10 +1,10 @@
import re
-from forum.badges.base import BadgeImplementation
+from forum.badges.base import AbstractBadge
from forum.modules import get_modules_script_classes
-ALL_BADGES = dict([
- (re.sub('BadgeImpl', '', name).lower(), cls) for name, cls
- in get_modules_script_classes('badges', BadgeImplementation).items()
- if not re.search('AbstractBadgeImpl$', name)
- ])
+ALL_BADGES = [
+ cls() for name, cls
+ in get_modules_script_classes('badges', AbstractBadge).items()
+ if not re.search('AbstractBadge$', name)
+ ]
View
@@ -1,11 +1,105 @@
+import re
+from string import lower
+from django.contrib.contenttypes.models import ContentType
-class BadgeImplementation(object):
- name = ""
- description = ""
+from forum.models.utils import countable_update
+from forum.models.user import activity_record
+from forum.models import Badge, Award, Activity
+
+import logging
+
+class AbstractBadge(object):
+
+ _instance = None
+
+ @property
+ def name(self):
+ return " ".join(re.findall(r'([A-Z][a-z1-9]+)', re.sub('Badge', '', self.__class__.__name__)))
+
+ @property
+ def description(self):
+ raise NotImplementedError
+
+ def __new__(cls, *args, **kwargs):
+ if cls._instance is None:
+ cls.badge = "-".join(map(lower, re.findall(r'([A-Z][a-z1-9]+)', re.sub('Badge', '', cls.__name__))))
+ cls._instance = super(AbstractBadge, cls).__new__(cls, *args, **kwargs)
+
+ return cls._instance
def install(self):
pass
- def process_job(self):
- raise NotImplementedError
+ def award_badge(self, user, obj=None, award_once=False):
+ try:
+ badge = Badge.objects.get(slug=self.badge)
+ except:
+ logging.log('Trying to award a badge not installed in the database.')
+ return
+
+ content_type = ContentType.objects.get_for_model(obj.__class__)
+
+ awarded = user.awards.filter(badge=badge)
+
+ if not award_once:
+ awarded = awarded.filter(content_type=content_type, object_id=obj.id)
+
+ if len(awarded):
+ logging.log(1, 'Trying to award badged already awarded.')
+ return
+
+ award = Award(user=user, badge=badge, content_type=content_type, object_id=obj.id)
+ award.save()
+
+class CountableAbstractBadge(AbstractBadge):
+
+ def __init__(self, model, countable, expected_value, handler):
+ sender = getattr(model, "%s_sender" % countable)
+
+ def wrapper(sender, **kwargs):
+ if kwargs['new_value'] == expected_value:
+ handler(instance=kwargs['instance'])
+
+ countable_update.connect(wrapper, sender=sender, weak=False)
+
+class PostCountableAbstractBadge(CountableAbstractBadge):
+ def __init__(self, model, countable, expected_value):
+
+ def handler(instance):
+ self.award_badge(instance.author, instance)
+
+ super(PostCountableAbstractBadge, self).__init__(model, countable, expected_value, handler)
+
+
+class ActivityAbstractBadge(AbstractBadge):
+
+ def __init__(self, activity_type, handler):
+
+ def wrapper(sender, **kwargs):
+ handler(instance=kwargs['instance'])
+
+ activity_record.connect(wrapper, sender=activity_type, weak=False)
+
+
+class ActivityCountAbstractBadge(AbstractBadge):
+
+ def __init__(self, activity_type, count):
+
+ def handler(sender, **kwargs):
+ instance = kwargs['instance']
+ if Activity.objects.filter(user=instance.user, activity_type__in=activity_type).count() == count:
+ self.award_badge(instance.user, instance.content_object)
+
+ if not isinstance(activity_type, (tuple, list)):
+ activity_type = (activity_type, )
+
+ for type in activity_type:
+ activity_record.connect(handler, sender=type, weak=False)
+
+class FirstActivityAbstractBadge(ActivityCountAbstractBadge):
+
+ def __init__(self, activity_type):
+ super(FirstActivityAbstractBadge, self).__init__(activity_type, 1)
+
+
View
@@ -4,6 +4,7 @@
from meta import Vote, Comment, FlaggedItem
from user import Activity, ValidationHash, EmailFeedSetting, AuthKeyUserAssociation
from repute import Badge, Award, Repute
+from utils import KeyValue
import re
from base import *
@@ -248,7 +249,7 @@ def record_delete_question(instance, delete_by, **kwargs):
"""
when user deleted the question
"""
- if instance.__class__ == "Question":
+ if instance.__class__ is Question:
activity_type = TYPE_ACTIVITY_DELETE_QUESTION
else:
activity_type = TYPE_ACTIVITY_DELETE_ANSWER
@@ -344,6 +345,8 @@ def post_stored_anonymous_content(sender,user,session_key,signal,*args,**kwargs)
ValidationHash = ValidationHash
AuthKeyUserAssociation = AuthKeyUserAssociation
+KeyValue = KeyValue
+
__all__ = [
'Question',
'QuestionRevision',
@@ -369,8 +372,15 @@ def post_stored_anonymous_content(sender,user,session_key,signal,*args,**kwargs)
'EmailFeedSetting',
'ValidationHash',
'AuthKeyUserAssociation',
-
- 'User'
+ 'KeyValue',
+
+ 'User',
+ 'tags_updated',
+ 'edit_question_or_answer',
+ 'delete_post_or_answer',
+ 'mark_offensive',
+ 'user_updated',
+ 'user_logged_in',
]
View
@@ -9,7 +9,7 @@
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
from django.template.defaultfilters import slugify
-from django.db.models.signals import post_delete, post_save, pre_save
+from django.db.models.signals import post_delete, post_save, pre_save, pre_delete
from django.utils.translation import ugettext as _
from django.utils.safestring import mark_safe
from django.contrib.sitemaps import ping_google
@@ -18,6 +18,7 @@
import logging
from forum.const import *
+from utils import CountableField
class UserContent(models.Model):
user = models.ForeignKey(User, related_name='%(class)ss')
@@ -98,7 +99,7 @@ class Content(models.Model):
locked_at = models.DateTimeField(null=True, blank=True)
score = models.IntegerField(default=0)
- vote_up_count = models.IntegerField(default=0)
+ vote_up_count = CountableField(default=0)
vote_down_count = models.IntegerField(default=0)
comment_count = models.PositiveIntegerField(default=0)
View
@@ -132,8 +132,8 @@ class Question(Content, DeletableContent):
# Denormalised data
answer_count = models.PositiveIntegerField(default=0)
- view_count = models.PositiveIntegerField(default=0)
- favourite_count = models.PositiveIntegerField(default=0)
+ view_count = CountableField(default=0)
+ favourite_count = CountableField(default=0)
last_activity_at = models.DateTimeField(default=datetime.datetime.now)
last_activity_by = models.ForeignKey(User, related_name='last_active_in_questions')
tagnames = models.CharField(max_length=125)
View
@@ -55,13 +55,9 @@ def get_recent_awards(self):
).values('badge_id', 'badge_name', 'badge_description', 'badge_type', 'user_id', 'user_name')
return awards
-class Award(models.Model):
+class Award(MetaContent, UserContent):
"""The awarding of a Badge to a User."""
- user = models.ForeignKey(User, related_name='award_user')
badge = models.ForeignKey('Badge', related_name='award_badge')
- content_type = models.ForeignKey(ContentType)
- object_id = models.PositiveIntegerField()
- content_object = generic.GenericForeignKey('content_type', 'object_id')
awarded_at = models.DateTimeField(default=datetime.datetime.now)
notified = models.BooleanField(default=False)
@@ -71,6 +67,7 @@ def __unicode__(self):
return u'[%s] is awarded a badge [%s] at %s' % (self.user.username, self.badge.name, self.awarded_at)
class Meta:
+ unique_together = ('content_type', 'object_id', 'user', 'badge')
app_label = 'forum'
db_table = u'award'
View
@@ -1,6 +1,7 @@
from base import *
from django.utils.translation import ugettext as _
+import django.dispatch
class TagManager(models.Manager):
UPDATE_USED_COUNTS_QUERY = (
@@ -46,6 +47,7 @@ def update_use_counts(self, tags):
query = self.UPDATE_USED_COUNTS_QUERY % ','.join(['%s'] * len(tags))
cursor.execute(query, [tag.id for tag in tags])
transaction.commit_unless_managed()
+ tags_update_use_count.send(sender=Tag, tags=tags)
def get_tags_by_questions(self, questions):
question_ids = []
@@ -82,4 +84,6 @@ class MarkedTag(models.Model):
reason = models.CharField(max_length=16, choices=TAG_MARK_REASONS)
class Meta:
- app_label = 'forum'
+ app_label = 'forum'
+
+tags_update_use_count = django.dispatch.Signal(providing_args=['tags'])
View
@@ -0,0 +1,79 @@
+from django.db import models
+from django.core.cache import cache
+from django.conf import settings
+import django.dispatch
+
+class CountableField(models.IntegerField):
+
+ __metaclass__ = models.SubfieldBase
+
+ def contribute_to_class(self, cls, name):
+
+ signal_sender = object()
+
+ def increment(instance):
+ old_value = instance.__dict__[name]
+ new_value = old_value + 1
+
+ instance.__dict__[name] = new_value
+ countable_update.send(sender=signal_sender, instance=instance, new_value=new_value, old_value=old_value)
+
+ def decrement(instance):
+ old_value = instance.__dict__[name]
+ new_value = old_value - 1
+
+ instance.__dict__[name] = new_value
+ countable_update.send(sender=signal_sender, instance=instance, new_value=new_value, old_value=old_value)
+
+ cls.add_to_class("increment_%s" % name, increment)
+ cls.add_to_class("decrement_%s" % name, decrement)
+ cls.add_to_class("%s_sender" % name, signal_sender)
+
+ super(CountableField, self).contribute_to_class(cls, name)
+
+countable_update = django.dispatch.Signal(providing_args=['instance', 'old_value', 'new_value'])
+
+class KeyValueManager(models.Manager):
+
+ def create_cache_key(self, key):
+ return "%s:key_value:%s" % (settings.APP_URL, key)
+
+ def save_to_cache(self, instance):
+ cache.set(self.create_cache_key(instance.key), instance, 2592000)
+
+ def remove_from_cache(self, instance):
+ cache.delete(self.create_cache_key(instance.key))
+
+ def get(self, **kwargs):
+ if 'key' in kwargs:
+ instance = cache.get(self.create_cache_key(kwargs['key']))
+
+ if instance is None:
+ instance = super(KeyValueManager, self).get(**kwargs)
+ self.save_to_cache(instance)
+
+ return instance
+
+ else:
+ return super(KeyValueManager, self).get(**kwargs)
+
+class KeyValue(models.Model):
+ key = models.CharField(max_length=255, unique=True)
+ value = models.TextField()
+
+ objects = KeyValueManager()
+
+ class Meta:
+ app_label = 'forum'
+
+ def save(self):
+ super(KeyValue, self).save()
+ KeyValue.objects.save_to_cache(self)
+
+ def delete(self):
+ KeyValue.objects.remove_from_cache(self)
+ super(KeyValue, self).delete()
+
+
+
+
View
@@ -3,18 +3,21 @@
import re
from django.template import Template, TemplateDoesNotExist
+from django.conf import settings
MODULES_PACKAGE = 'forum_modules'
MODULES_FOLDER = os.path.join(os.path.dirname(__file__), '../' + MODULES_PACKAGE)
-MODULE_LIST = [
+DISABLED_MODULES = getattr(settings, 'ENABLE_MODULES', [])
+
+MODULE_LIST = filter(lambda m: getattr(m, 'CAN_USE', True), [
__import__('forum_modules.%s' % f, globals(), locals(), ['forum_modules'])
for f in os.listdir(MODULES_FOLDER)
if os.path.isdir(os.path.join(MODULES_FOLDER, f)) and
os.path.exists(os.path.join(MODULES_FOLDER, "%s/__init__.py" % f)) and
- not os.path.exists(os.path.join(MODULES_FOLDER, "%s/DISABLED" % f))
-]
+ not f in DISABLED_MODULES
+])
def get_modules_script(script_name):
all = []
@@ -28,6 +31,18 @@ def get_modules_script(script_name):
return all
+def get_modules_scipt_implementations(script_name, impl_class):
+ scripts = get_modules_script(script_name)
+ all_impls = {}
+
+ for script in scripts:
+ all_impls.update(dict([
+ (n, i) for (n, i) in [(n, getattr(script, n)) for n in dir(script)]
+ if isinstance(i, impl_class)
+ ]))
+
+ return all_impls
+
def get_modules_script_classes(script_name, base_class):
scripts = get_modules_script(script_name)
all_classes = {}
Oops, something went wrong.

0 comments on commit 526540b

Please sign in to comment.