From 22236fb67da2f27ceb35ad138f4d534a4fd08251 Mon Sep 17 00:00:00 2001 From: jordan Date: Sat, 2 Apr 2011 12:20:13 +0000 Subject: [PATCH] Reintegrate merge cacheimp -> trunk. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@924 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- forum/actions/user.py | 18 +-- forum/badges/base.py | 20 +-- forum/markdownext/mdx_auto_linker.py | 105 ++++++++++++++++ forum/markdownext/mdx_urlize.py | 44 ------- forum/models/action.py | 3 +- forum/models/base.py | 119 +++++++++++++++++- forum/models/meta.py | 29 +---- forum/models/node.py | 30 +++-- forum/models/question.py | 16 +-- forum/models/tag.py | 16 ++- forum/models/user.py | 4 - forum/models/utils.py | 4 + forum/modules/ui.py | 10 ++ forum/modules/ui_objects.py | 36 +++--- forum/registry.py | 42 ++++--- forum/settings/sidebar.py | 6 +- forum/skins/default/media/js/osqa.ask.js | 10 +- forum/skins/default/media/style/user.css | 1 + .../templates/paginator/page_numbers.html | 2 +- forum/skins/default/templates/user.html | 6 +- .../default/templates/users/questions.html | 7 +- forum/startup.py | 1 + forum/templatetags/user_tags.py | 9 +- forum/utils/pagination.py | 4 +- forum/utils/userlinking.py | 20 +-- forum/views/commands.py | 5 +- forum/views/meta.py | 2 +- forum_modules/exporter/exporter.py | 9 +- forum_modules/mysqlfulltext/__init__.py | 10 ++ forum_modules/mysqlfulltext/fts_install.sql | 31 +++++ forum_modules/mysqlfulltext/models.py | 9 ++ forum_modules/mysqlfulltext/settings.py | 3 + forum_modules/mysqlfulltext/startup.py | 34 +++++ forum_modules/openidauth/consumer.py | 3 +- forum_modules/sximporter/importer.py | 17 ++- 35 files changed, 490 insertions(+), 195 deletions(-) create mode 100644 forum/markdownext/mdx_auto_linker.py delete mode 100644 forum/markdownext/mdx_urlize.py create mode 100644 forum_modules/mysqlfulltext/__init__.py create mode 100644 forum_modules/mysqlfulltext/fts_install.sql create mode 100644 forum_modules/mysqlfulltext/models.py create mode 100644 forum_modules/mysqlfulltext/settings.py create mode 100644 forum_modules/mysqlfulltext/startup.py diff --git a/forum/actions/user.py b/forum/actions/user.py index f9a9913..15ad5ee 100644 --- a/forum/actions/user.py +++ b/forum/actions/user.py @@ -1,4 +1,4 @@ -from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext, ugettext as _ from django.db.models import F from forum.models.action import ActionProxy from forum.models import Award, Badge, ValidationHash, User @@ -98,15 +98,15 @@ class AwardPointsAction(ActionProxy): def repute_users(self): self.repute(self._affected, self._value) + self.repute(self.user, -self._value) - if self._value > 0: - self._affected.message_set.create( - message=_("Congratulations, you have been awarded an extra %s reputation points.") % self._value + - '
%s' % self.extra.get('message', _('Thank you'))) - else: - self._affected.message_set.create( - message=_("You gave %s reputation points.") % self._value + - '
%s' % self.extra.get('message', '')) + + self._affected.message_set.create( + message=_("Congratulations, you have been awarded an extra %(points)s reputation %(points_label)s on this answer.") % { + 'points': self._value, + 'points_label': ungettext('point', 'points', self._value), + 'answer_url': self.node.get_absolute_url() + }) def describe(self, viewer=None): value = self.extra.get('value', _('unknown')) diff --git a/forum/badges/base.py b/forum/badges/base.py index 79fcb52..c78a925 100644 --- a/forum/badges/base.py +++ b/forum/badges/base.py @@ -20,10 +20,12 @@ class BadgesMeta(type): if not dic.get('abstract', False): if not name in installed: - badge.ondb = Badge(cls=name, type=dic.get('type', Badge.BRONZE)) - badge.ondb.save() + ondb = Badge(cls=name, type=dic.get('type', Badge.BRONZE)) + ondb.save() else: - badge.ondb = installed[name] + ondb = installed[name] + + badge.ondb = ondb.id inst = badge() @@ -36,9 +38,8 @@ class BadgesMeta(type): for action in badge.listen_to: action.hook(hook) - BadgesMeta.by_class[name] = badge - badge.ondb.__dict__['_class'] = inst - BadgesMeta.by_id[badge.ondb.id] = badge + BadgesMeta.by_class[name] = inst + BadgesMeta.by_id[ondb.id] = inst return badge @@ -58,18 +59,19 @@ class AbstractBadge(object): @classmethod def award(cls, user, action, once=False): + db_object = Badge.objects.get(id=cls.ondb) try: if once: node = None - awarded = AwardAction.get_for(user, cls.ondb) + awarded = AwardAction.get_for(user, db_object) else: node = action.node - awarded = AwardAction.get_for(user, cls.ondb, node) + awarded = AwardAction.get_for(user, db_object, node) trigger = isinstance(action, Action) and action or None if not awarded: - AwardAction(user=user, node=node).save(data=dict(badge=cls.ondb, trigger=trigger)) + AwardAction(user=user, node=node).save(data=dict(badge=db_object, trigger=trigger)) except MultipleObjectsReturned: if node: logging.error('Found multiple %s badges awarded for user %s (%s)' % (self.name, user.username, user.id)) diff --git a/forum/markdownext/mdx_auto_linker.py b/forum/markdownext/mdx_auto_linker.py new file mode 100644 index 0000000..9d77c38 --- /dev/null +++ b/forum/markdownext/mdx_auto_linker.py @@ -0,0 +1,105 @@ +import markdown +import re, socket + +TLDS = ('gw', 'gu', 'gt', 'gs', 'gr', 'gq', 'gp', 'gy', 'gg', 'gf', 'ge', 'gd', 'ga', 'edu', 'va', 'gn', 'gl', 'gi', + 'gh', 'iq', 'lb', 'lc', 'la', 'tv', 'tw', 'tt', 'arpa', 'lk', 'li', 'lv', 'to', 'lt', 'lr', 'ls', 'th', 'tf', + 'su', 'td', 'aspx', 'tc', 'ly', 'do', 'coop', 'dj', 'dk', 'de', 'vc', 'me', 'dz', 'uy', 'yu', 'vg', 'ro', + 'vu', 'qa', 'ml', 'us', 'zm', 'cfm', 'tel', 'ee', 'htm', 'za', 'ec', 'bg', 'uk', 'eu', 'et', 'zw', + 'es', 'er', 'ru', 'rw', 'rs', 'asia', 're', 'it', 'net', 'gov', 'tz', 'bd', 'be', 'bf', 'asp', 'jobs', 'ba', + 'bb', 'bm', 'bn', 'bo', 'bh', 'bi', 'bj', 'bt', 'jm', 'sb', 'bw', 'ws', 'br', 'bs', 'je', 'tg', 'by', 'bz', + 'tn', 'om', 'ua', 'jo', 'pdf', 'mz', 'com', 'ck', 'ci', 'ch', 'co', 'cn', 'cm', 'cl', 'cc', 'tr', 'ca', 'cg', + 'cf', 'cd', 'cz', 'cy', 'cx', 'org', 'cr', 'txt', 'cv', 'cu', 've', 'pr', 'ps', 'fk', 'pw', 'pt', 'museum', + 'py', 'tl', 'int', 'pa', 'pf', 'pg', 'pe', 'pk', 'ph', 'pn', 'eg', 'pl', 'tk', 'hr', 'aero', 'ht', 'hu', 'hk', + 'hn', 'vn', 'hm', 'jp', 'info', 'md', 'mg', 'ma', 'mc', 'uz', 'mm', 'local', 'mo', 'mn', 'mh', 'mk', 'cat', + 'mu', 'mt', 'mw', 'mv', 'mq', 'ms', 'mr', 'im', 'ug', 'my', 'mx', 'il', 'pro', 'ac', 'sa', 'ae', 'ad', 'ag', + 'af', 'ai', 'vi', 'is', 'ir', 'am', 'al', 'ao', 'an', 'aq', 'as', 'ar', 'au', 'at', 'aw', 'in', 'ax', 'az', + 'ie', 'id', 'sr', 'nl', 'mil', 'no', 'na', 'travel', 'nc', 'ne', 'nf', 'ng', 'nz', 'dm', 'np', + 'so', 'nr', 'nu', 'fr', 'io', 'ni', 'ye', 'sv', 'jsp', 'kz', 'fi', 'fj', 'php', 'fm', 'fo', 'tj', 'sz', 'sy', + 'mobi', 'kg', 'ke', 'doc', 'ki', 'kh', 'kn', 'km', 'st', 'sk', 'kr', 'si', 'kp', 'kw', 'sn', 'sm', 'sl', 'sc', + 'biz', 'ky', 'sg', 'se', 'sd') + +AUTO_LINK_RE = re.compile(r""" + (?P.?\s*) + (?P + (?P + ((?P[a-z][a-z]+)://)? + (?P\w(?:[\w-]*\w)?\.\w(?:[\w-]*\w)?(?:\.\w(?:[\w-]*\w)?)*) + ) | (?P + ((?P[a-z][a-z]+)://) + (?P\w(?:[\w-]*\w)?(?:\.\w(?:[\w-]*\w)?)*) + ) + (?P:\d+)? + (?P/[^\s<]*)? + ) + +""", re.X | re.I) + +def is_ip(addr): + try: + socket.inet_aton(addr) + return True + except: + return False + +def replacer(m): + + ws = m.group('ws') + + if ws and ws[0] in ("'", '"'): + return m.group(0) + + elif not ws: + ws = '' + + if m.group('format1'): + fn = 1 + else: + fn = 2 + + protocol = m.group('protocol%s' % fn) + domain = m.group('domain%s' % fn) + + if not protocol: + domain_chunks = domain.split('.') + + if not ((len(domain_chunks) == 1 and domain_chunks[0].lower() == 'localhost') or (domain_chunks[-1].lower() in TLDS)): + return m.group(0) + + if (not protocol) and is_ip(domain): + return m.group(0) + + + port = m.group('port') + uri = m.group('uri') + + if not ws: + ws = '' + + if not port: + port = '' + + if not protocol: + protocol = 'http' + + if not uri: + uri = '' + + url = "%s://%s%s%s" % (protocol, domain, port, uri) + + return "%s%s" % (ws, url, m.group('url')) + + +class AutoLinker(markdown.postprocessors.Postprocessor): + + def run(self, text): + return AUTO_LINK_RE.sub(replacer, text) + +class AutoLinkerExtension(markdown.Extension): + + def extendMarkdown(self, md, md_globals): + md.postprocessors['autolinker'] = AutoLinker() + +def makeExtension(configs=None): + return AutoLinkerExtension(configs=configs) + + diff --git a/forum/markdownext/mdx_urlize.py b/forum/markdownext/mdx_urlize.py deleted file mode 100644 index b323531..0000000 --- a/forum/markdownext/mdx_urlize.py +++ /dev/null @@ -1,44 +0,0 @@ -import markdown - -# Global Vars -URLIZE_RE = '(%s)' % '|'.join([ - r'<(?:f|ht)tps?://[^>]*>', - r'\b(?:f|ht)tps?://[^)<>\s]+[^.,)<>\s]', - r'\bwww\.[^)<>\s]+[^.,)<>\s]', - r'[^*(<\s]+\.(?:com|net|org)\b', -]) - -class UrlizePattern(markdown.inlinepatterns.Pattern): - """ Return a link Element given an autolink (`http://example/com`). """ - def handleMatch(self, m): - url = m.group(2) - - if url.startswith('<'): - url = url[1:-1] - - text = url - - if not url.split('://')[0] in ('http','https','ftp'): - if '@' in url and not '/' in url: - url = 'mailto:' + url - else: - url = 'http://' + url - - el = markdown.etree.Element("a") - el.set('href', url) - el.text = markdown.AtomicString(text) - return el - -class UrlizeExtension(markdown.Extension): - """ Urlize Extension for Python-Markdown. """ - - def extendMarkdown(self, md, md_globals): - """ Replace autolink with UrlizePattern """ - md.inlinePatterns['autolink'] = UrlizePattern(URLIZE_RE, md) - -def makeExtension(configs=None): - return UrlizeExtension(configs=configs) - -if __name__ == "__main__": - import doctest - doctest.testmod() diff --git a/forum/models/action.py b/forum/models/action.py index 7fd2e6f..dc29a02 100644 --- a/forum/models/action.py +++ b/forum/models/action.py @@ -16,7 +16,7 @@ class ActionQuerySet(CachedQuerySet): return super(ActionQuerySet, self).obj_from_datadict(datadict) def get(self, *args, **kwargs): - action = super(ActionQuerySet, self).get(*args, **kwargs).leaf() + action = super(ActionQuerySet, self).get(*args, **kwargs).leaf if not isinstance(action, self.model): raise self.model.DoesNotExist() @@ -101,6 +101,7 @@ class Action(BaseModel): cancel = ActionRepute(action=self, user=repute.user, value=(-repute.value), by_canceled=True) cancel.save() + @property def leaf(self): leaf_cls = ActionProxyMetaClass.types.get(self.action_type, None) diff --git a/forum/models/base.py b/forum/models/base.py index c5a80f9..bf132c4 100644 --- a/forum/models/base.py +++ b/forum/models/base.py @@ -1,5 +1,9 @@ import datetime import re +try: + from hashlib import md5 +except: + from md5 import new as md5 from urllib import quote_plus, urlencode from django.db import models, IntegrityError, connection, transaction from django.utils.http import urlquote as django_urlquote @@ -18,6 +22,10 @@ from forum import settings import logging +if not hasattr(cache, 'get_many'): + #put django 1.2 code here + pass + class LazyQueryList(object): def __init__(self, model, items): self.items = items @@ -33,6 +41,9 @@ class LazyQueryList(object): def __len__(self): return len(self.items) +class ToFetch(str): + pass + class CachedQuerySet(models.query.QuerySet): def lazy(self): @@ -45,15 +56,20 @@ class CachedQuerySet(models.query.QuerySet): return LazyQueryList(self.model, list(self.values_list(*values_list))) else: - if len(self.query.extra): - print self.query.extra return self def obj_from_datadict(self, datadict): obj = self.model() obj.__dict__.update(datadict) + + if hasattr(obj, '_state'): + obj._state.db = 'default' + return obj + def _base_clone(self): + return self._clone(klass=models.query.QuerySet) + def get(self, *args, **kwargs): key = self.model.infer_cache_key(kwargs) @@ -61,7 +77,7 @@ class CachedQuerySet(models.query.QuerySet): obj = cache.get(key) if obj is None: - obj = super(CachedQuerySet, self).get(*args, **kwargs) + obj = self._base_clone().get(*args, **kwargs) obj.cache() else: obj = self.obj_from_datadict(obj) @@ -69,7 +85,85 @@ class CachedQuerySet(models.query.QuerySet): return obj - return super(CachedQuerySet, self).get(*args, **kwargs) + return self._base_clone().get(*args, **kwargs) + + def _fetch_from_query_cache(self, key): + invalidation_key = self.model._get_cache_query_invalidation_key() + cached_result = cache.get_many([invalidation_key, key]) + + if not invalidation_key in cached_result: + self.model._set_query_cache_invalidation_timestamp() + return None + + if (key in cached_result) and(cached_result[invalidation_key] < cached_result[key][0]): + return cached_result[key][1] + + return None + + def count(self): + cache_key = self.model._generate_cache_key("CNT:%s" % self._get_query_hash()) + result = self._fetch_from_query_cache(cache_key) + + if result is not None: + return result + + result = super(CachedQuerySet, self).count() + cache.set(cache_key, (datetime.datetime.now(), result), 60 * 60) + return result + + def iterator(self): + cache_key = self.model._generate_cache_key("QUERY:%s" % self._get_query_hash()) + on_cache_query_attr = self.model.value_to_list_on_cache_query() + + to_return = None + to_cache = {} + + key_list = self._fetch_from_query_cache(cache_key) + + if key_list is None: + if not len(self.query.aggregates): + values_list = [on_cache_query_attr] + + if len(self.query.extra): + values_list += self.query.extra.keys() + + key_list = [v[0] for v in self.values_list(*values_list)] + to_cache[cache_key] = (datetime.datetime.now(), key_list) + else: + to_return = list(super(CachedQuerySet, self).iterator()) + to_cache[cache_key] = (datetime.datetime.now(), [row.__dict__[on_cache_query_attr] for row in to_return]) + + if (not to_return) and key_list: + row_keys = [self.model.infer_cache_key({on_cache_query_attr: attr}) for attr in key_list] + cached = cache.get_many(row_keys) + + to_return = [ + (ck in cached) and self.obj_from_datadict(cached[ck]) or ToFetch(key_list[i]) for i, ck in enumerate(row_keys) + ] + + if len(cached) != len(row_keys): + to_fetch = [str(tr) for tr in to_return if isinstance(tr, ToFetch)] + + fetched = dict([(str(r.__dict__[on_cache_query_attr]), r) for r in + models.query.QuerySet(self.model).filter(**{"%s__in" % on_cache_query_attr: to_fetch})]) + + to_return = [(isinstance(tr, ToFetch) and fetched[str(tr)] or tr) for tr in to_return] + to_cache.update(dict([(self.model.infer_cache_key({on_cache_query_attr: attr}), r._as_dict()) for attr, r in fetched.items()])) + + if len(to_cache): + cache.set_many(to_cache, 60 * 60) + + if to_return: + for row in to_return: + if hasattr(row, 'leaf'): + yield row.leaf + else: + yield row + + def _get_query_hash(self): + return md5(str(self.query)).hexdigest() + + class CachedManager(models.Manager): use_for_related_fields = True @@ -178,8 +272,20 @@ class BaseModel(models.Model): self.uncache() self.reset_original_state() + self._set_query_cache_invalidation_timestamp() self.cache() + @classmethod + def _get_cache_query_invalidation_key(cls): + return cls._generate_cache_key("INV_TS") + + @classmethod + def _set_query_cache_invalidation_timestamp(cls): + cache.set(cls._get_cache_query_invalidation_key(), datetime.datetime.now(), 60 * 60 * 24) + + for base in filter(lambda c: issubclass(c, BaseModel) and (not c is BaseModel), cls.__bases__): + base._set_query_cache_invalidation_timestamp() + @classmethod def _generate_cache_key(cls, key, group=None): if group is None: @@ -190,6 +296,10 @@ class BaseModel(models.Model): def cache_key(self): return self._generate_cache_key(self.id) + @classmethod + def value_to_list_on_cache_query(cls): + return 'id' + @classmethod def infer_cache_key(cls, querydict): try: @@ -208,6 +318,7 @@ class BaseModel(models.Model): def delete(self): self.uncache() + self._set_query_cache_invalidation_timestamp() super(BaseModel, self).delete() diff --git a/forum/models/meta.py b/forum/models/meta.py index 41dc5d6..2b790d8 100644 --- a/forum/models/meta.py +++ b/forum/models/meta.py @@ -24,27 +24,8 @@ class Flag(models.Model): app_label = 'forum' unique_together = ('user', 'node') -class BadgesQuerySet(models.query.QuerySet): - def get(self, *args, **kwargs): - try: - pk = [v for (k,v) in kwargs.items() if k in ('pk', 'pk__exact', 'id', 'id__exact')][0] - except: - return super(BadgesQuerySet, self).get(*args, **kwargs) - from forum.badges.base import BadgesMeta - badge = BadgesMeta.by_id.get(int(pk), None) - if not badge: - return super(BadgesQuerySet, self).get(*args, **kwargs) - return badge.ondb - - -class BadgeManager(models.Manager): - use_for_related_fields = True - - def get_query_set(self): - return BadgesQuerySet(self.model) - -class Badge(models.Model): +class Badge(BaseModel): GOLD = 1 SILVER = 2 BRONZE = 3 @@ -55,16 +36,18 @@ class Badge(models.Model): awarded_to = models.ManyToManyField(User, through='Award', related_name='badges') - objects = BadgeManager() + def get_handler(self): + from forum.badges import BadgesMeta + return BadgesMeta.by_id.get(self.id, None) @property def name(self): - cls = self.__dict__.get('_class', None) + cls = self.get_handler() return cls and cls.name or _("Unknown") @property def description(self): - cls = self.__dict__.get('_class', None) + cls = self.get_handler() return cls and cls.description or _("No description available") @models.permalink diff --git a/forum/models/node.py b/forum/models/node.py index 553b6c2..0ebcd21 100644 --- a/forum/models/node.py +++ b/forum/models/node.py @@ -7,6 +7,7 @@ from django.utils.translation import ugettext as _ from django.utils.safestring import mark_safe from django.utils.html import strip_tags from forum.utils.html import sanitize_html +from forum.utils.userlinking import auto_user_link from forum.settings import SUMMARY_LENGTH from utils import PickledObjectField @@ -24,6 +25,9 @@ class NodeContent(models.Model): def html(self): return self.body + def rendered(self, content): + return auto_user_link(self, self._as_markdown(content, *['auto_linker'])) + @classmethod def _as_markdown(cls, content, *extensions): try: @@ -43,7 +47,6 @@ class NodeContent(models.Model): def tagname_list(self): if self.tagnames: - t = [name.strip() for name in self.tagnames.split(u' ') if name] return [name.strip() for name in self.tagnames.split(u' ') if name] else: return [] @@ -333,22 +336,32 @@ class Node(BaseModel, NodeContent): def create_revision(self, user, **kwargs): number = self.revisions.aggregate(last=models.Max('revision'))['last'] + 1 revision = self._create_revision(user, number, **kwargs) - self.activate_revision(user, revision, extensions=['urlize']) + self.activate_revision(user, revision) return revision - def activate_revision(self, user, revision, extensions=['urlize']): + def activate_revision(self, user, revision): self.title = revision.title self.tagnames = revision.tagnames - - from forum.utils.userlinking import auto_user_link - - self.body = auto_user_link(self, self._as_markdown(revision.body, *extensions)) + + self.body = self.rendered(revision.body) self.active_revision = revision self.update_last_activity(user) self.save() + def get_active_users(self, active_users = None): + if not active_users: + active_users = set() + + active_users.add(self.author) + + for node in self.children.all(): + if not node.nis.deleted: + node.get_active_users(active_users) + + return active_users + def _list_changes_in_tags(self): dirty = self.get_dirty_fields() @@ -377,7 +390,7 @@ class Node(BaseModel, NodeContent): for name in tag_changes['added']: try: tag = Tag.objects.get(name=name) - except: + except Tag.DoesNotExist: tag = Tag.objects.create(name=name, created_by=self._last_active_user()) if not self.nis.deleted: @@ -437,7 +450,6 @@ class Node(BaseModel, NodeContent): tags_changed = self._process_changes_in_tags() super(Node, self).save(*args, **kwargs) - if tags_changed: self.tags = list(Tag.objects.filter(name__in=self.tagname_list())) class Meta: diff --git a/forum/models/question.py b/forum/models/question.py index ef4a37d..136d3de 100644 --- a/forum/models/question.py +++ b/forum/models/question.py @@ -60,21 +60,7 @@ class Question(Node): return [Question.objects.get(id=r['id']) for r in related_list] - def get_active_users(self): - active_users = set() - - active_users.add(self.author) - - for answer in self.answers: - active_users.add(answer.author) - - for comment in answer.comments: - active_users.add(comment.author) - - for comment in self.comments: - active_users.add(comment.author) - - return active_users + class QuestionSubscription(models.Model): diff --git a/forum/models/tag.py b/forum/models/tag.py index dd628c8..c84a922 100644 --- a/forum/models/tag.py +++ b/forum/models/tag.py @@ -5,7 +5,7 @@ from django.utils.translation import ugettext as _ from forum import modules -class ActiveTagManager(models.Manager): +class ActiveTagManager(CachedManager): use_for_related_fields = True def get_query_set(self): @@ -34,6 +34,20 @@ class Tag(BaseModel): else: self.used_count = models.F('used_count') + value + def cache_key(self): + return self._generate_cache_key(self.name) + + @classmethod + def infer_cache_key(cls, querydict): + if 'name' in querydict: + return cls._generate_cache_key(querydict['name']) + + return BaseModel.infer_cache_key(querydict) + + @classmethod + def value_to_list_on_cache_query(cls): + return 'name' + @models.permalink def get_absolute_url(self): return ('tag_questions', (), {'tag': self.name}) diff --git a/forum/models/user.py b/forum/models/user.py index f8c91d6..d3b2c6f 100644 --- a/forum/models/user.py +++ b/forum/models/user.py @@ -4,10 +4,6 @@ from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User as DjangoUser, AnonymousUser as DjangoAnonymousUser from django.db.models import Q -try: - from hashlib import md5 -except: - from md5 import new as md5 import string from random import Random diff --git a/forum/models/utils.py b/forum/models/utils.py index ecbe038..1fbda58 100644 --- a/forum/models/utils.py +++ b/forum/models/utils.py @@ -122,3 +122,7 @@ class KeyValue(BaseModel): except: return None + @classmethod + def value_to_list_on_cache_query(cls): + return 'key' + diff --git a/forum/modules/ui.py b/forum/modules/ui.py index 48c0246..2694110 100644 --- a/forum/modules/ui.py +++ b/forum/modules/ui.py @@ -9,6 +9,16 @@ class Registry(list): self.append(item) + def find_by_name(self, name): + for i in self: + if i.name and (i.name == name): + return i + + def remove_by_name(self, name): + for i, r in enumerate(self): + if r.name and (r.name == name): + return self.pop(i) + HEAD_CONTENT = 'HEAD_CONTENT' HEADER_LINKS = 'HEADER_LINKS' diff --git a/forum/modules/ui_objects.py b/forum/modules/ui_objects.py index e42c940..a51044a 100644 --- a/forum/modules/ui_objects.py +++ b/forum/modules/ui_objects.py @@ -7,7 +7,7 @@ from ui import Registry from copy import copy class Visibility(object): - def __init__(self, level='public'): + def __init__(self, level='public', negated=False): if level not in ['public', 'authenticated', 'staff', 'superuser', 'owner']: try: int(level) @@ -18,7 +18,7 @@ class Visibility(object): self.by_reputation = False self.level = level - self.negated = False + self.negated = negated def show_to(self, user): if self.by_reputation: @@ -36,8 +36,7 @@ class Visibility(object): return res def __invert__(self): - inverted = copy(self) - inverted.negated = True + return Visibility(self.level, not self.negated) Visibility.PUBLIC = Visibility('public') @@ -68,9 +67,10 @@ class ObjectBase(object): else: return self.argument - def __init__(self, visibility=None, weight=500): + def __init__(self, visibility=None, weight=500, name=''): self.visibility = visibility self.weight = weight + self.name = name def _visible_to(self, user): return (not self.visibility) or (self.visibility and self.visibility.show_to(user)) @@ -94,8 +94,8 @@ class LoopBase(ObjectBase): class Link(ObjectBase): - def __init__(self, text, url, attrs=None, pre_code='', post_code='', visibility=None, weight=500): - super(Link, self).__init__(visibility, weight) + def __init__(self, text, url, attrs=None, pre_code='', post_code='', visibility=None, weight=500, name=''): + super(Link, self).__init__(visibility, weight, name) self.text = self.Argument(text) self.url = self.Argument(url) self.attrs = self.Argument(attrs or {}) @@ -108,8 +108,8 @@ class Link(ObjectBase): self.post_code(context)) class Include(ObjectBase): - def __init__(self, tpl, visibility=None, weight=500): - super(Include, self).__init__(visibility, weight) + def __init__(self, tpl, visibility=None, weight=500, name=''): + super(Include, self).__init__(visibility, weight, name) self.template = template.loader.get_template(tpl) def render(self, context): @@ -119,8 +119,8 @@ class Include(ObjectBase): class LoopContext(LoopBase): - def __init__(self, loop_context, visibility=None, weight=500): - super(LoopContext, self).__init__(visibility, weight) + def __init__(self, loop_context, visibility=None, weight=500, name=''): + super(LoopContext, self).__init__(visibility, weight, name) self.loop_context = self.Argument(loop_context) def update_context(self, context): @@ -128,8 +128,8 @@ class LoopContext(LoopBase): class PageTab(LoopBase): - def __init__(self, tab_name, tab_title, url_getter, weight): - super(PageTab, self).__init__(weight=weight) + def __init__(self, tab_name, tab_title, url_getter, weight, name=''): + super(PageTab, self).__init__(weight=weight, name=name) self.tab_name = tab_name self.tab_title = tab_title self.url_getter = url_getter @@ -144,7 +144,7 @@ class PageTab(LoopBase): class ProfileTab(LoopBase): def __init__(self, name, title, description, url_getter, private=False, render_to=None, weight=500): - super(ProfileTab, self).__init__(weight=weight) + super(ProfileTab, self).__init__(weight=weight, name=name) self.name = name self.title = title self.description = description @@ -167,8 +167,8 @@ class ProfileTab(LoopBase): class AjaxMenuItem(ObjectBase): - def __init__(self, label, url, a_attrs=None, span_label='', span_attrs=None, visibility=None, weight=500): - super(AjaxMenuItem, self).__init__(visibility, weight) + def __init__(self, label, url, a_attrs=None, span_label='', span_attrs=None, visibility=None, weight=500, name=''): + super(AjaxMenuItem, self).__init__(visibility, weight, name) self.label = self.Argument(label) self.url = self.Argument(url) self.a_attrs = self.Argument(a_attrs or {}) @@ -182,8 +182,8 @@ class AjaxMenuItem(ObjectBase): **{'class': 'item'}) class AjaxMenuGroup(ObjectBase, Registry): - def __init__(self, label, items, visibility=None, weight=500): - super(AjaxMenuGroup, self).__init__(visibility, weight) + def __init__(self, label, items, visibility=None, weight=500, name=''): + super(AjaxMenuGroup, self).__init__(visibility, weight, name) self.label = label for item in items: diff --git a/forum/registry.py b/forum/registry.py index aedf5b0..43f11a7 100644 --- a/forum/registry.py +++ b/forum/registry.py @@ -2,39 +2,43 @@ from forum.modules import ui, get_modules_script from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse from django.template.defaultfilters import slugify -from django.template import get_templatetags_modules + from forum.templatetags.extra_tags import get_score_badge from forum.utils.html import cleanup_urls from forum import settings -modules_template_tags = get_modules_script('templatetags') -django_template_tags = get_templatetags_modules() +try: + from django.template import get_templatetags_modules + modules_template_tags = get_modules_script('templatetags') + django_template_tags = get_templatetags_modules() -for m in modules_template_tags: - django_template_tags.append(m.__name__) + for m in modules_template_tags: + django_template_tags.append(m.__name__) +except: + pass ui.register(ui.HEADER_LINKS, - ui.Link(_('faq'), ui.Url('faq'), weight=400), - ui.Link(_('about'), ui.Url('about'), weight=300), + ui.Link(_('faq'), ui.Url('faq'), weight=400, name='FAQ'), + ui.Link(_('about'), ui.Url('about'), weight=300, name='ABOUT'), ui.Link( text=lambda u, c: u.is_authenticated() and _('logout') or _('login'), url=lambda u, c: u.is_authenticated() and reverse('logout') or reverse('auth_signin'), - weight=200), + weight=200, name='LOGIN/OUT'), ui.Link( visibility=ui.Visibility.AUTHENTICATED, text=lambda u, c: u.username, url=lambda u, c: u.get_profile_url(), post_code=lambda u, c: get_score_badge(u), - weight=100), + weight=100, name='ACCOUNT'), ui.Link( visibility=ui.Visibility.SUPERUSER, text=_('administration'), url=lambda u, c: reverse('admin_index'), - weight=0) + weight=0, name='ADMINISTRATION') ) @@ -67,24 +71,28 @@ ui.register(ui.USER_MENU, label=_("edit profile"), url=lambda u, c: reverse('edit_user', kwargs={'id': c['user'].id}), span_attrs={'class': 'user-edit'}, - weight=0 + weight=0, + name='EDIT_PROFILE' ), ui.UserMenuItem( label=_("authentication settings"), url=lambda u, c: reverse('user_authsettings', kwargs={'id': c['user'].id}), span_attrs={'class': 'user-auth'}, - weight=100 + weight=100, + name='AUTH_SETTINGS' ), ui.UserMenuItem( label=_("email notification settings"), url=lambda u, c: reverse('user_subscriptions', kwargs={'id': c['user'].id, 'slug': slugify(c['user'].username)}), span_attrs={'class': 'user-subscriptions'}, - weight=200 + weight=200, + name='EMAIL_SETTINGS' ), ui.UserMenuItem( label=_("other preferences"), url=lambda u, c: reverse('user_preferences', kwargs={'id': c['user'].id, 'slug': slugify(c['user'].username)}), - weight=200 + weight=200, + name='OTHER_PREFS' ), ModerationMenuGroup(_("Moderation tools"), items=( ui.UserMenuItem( @@ -92,6 +100,7 @@ ui.register(ui.USER_MENU, url=lambda u, c: reverse('user_suspend', kwargs={'id': c['user'].id}), a_attrs=lambda u, c: {'class': c['user'].is_suspended() and 'ajax-command confirm' or 'ajax-command withprompt'}, render_to=lambda u: not u.is_superuser, + name='SUSPENSION' ), ui.UserMenuItem( label=lambda u, c: _("give/take karma"), @@ -99,18 +108,21 @@ ui.register(ui.USER_MENU, a_attrs=lambda u, c: {'id': 'award-rep-points', 'class': 'ajax-command withprompt'}, span_attrs={'class': 'user-award_rep'}, render_to=lambda u: not u.is_suspended(), + name='KARMA' ), ui.UserMenuItem( label=lambda u, c: c['user'].is_staff and _("remove moderator status") or _("grant moderator status"), url=lambda u, c: reverse('user_powers', kwargs={'id': c['user'].id, 'action':c['user'].is_staff and 'remove' or 'grant', 'status': 'staff'}), a_attrs=lambda u, c: {'class': 'ajax-command confirm'}, span_attrs={'class': 'user-moderator'}, + name='MODERATOR' ), SuperUserSwitchMenuItem( label=lambda u, c: c['user'].is_superuser and _("remove super user status") or _("grant super user status"), url=lambda u, c: reverse('user_powers', kwargs={'id': c['user'].id, 'action':c['user'].is_superuser and 'remove' or 'grant', 'status': 'super'}), a_attrs=lambda u, c: {'class': 'ajax-command confirm'}, span_attrs={'class': 'user-superuser'}, + name='SUPERUSER' ), - ), visibility=ui.Visibility.SUPERUSER, weight=500) + ), visibility=ui.Visibility.SUPERUSER, weight=500, name='MOD_TOOLS') ) diff --git a/forum/settings/sidebar.py b/forum/settings/sidebar.py index 2242538..7814594 100644 --- a/forum/settings/sidebar.py +++ b/forum/settings/sidebar.py @@ -26,7 +26,8 @@ u""" , SIDEBAR_SET, dict( label = "Question title tips", help_text = "Tips visible on the ask or edit questions page about the question title.", -required=False)) +required=False, +widget=Textarea(attrs={'rows': '10'}))) QUESTION_TAG_TIPS = Setting('QUESTION_TAG_TIPS', u""" @@ -37,7 +38,8 @@ u""" , SIDEBAR_SET, dict( label = "Tagging tips", help_text = "Tips visible on the ask or edit questions page about good tagging.", -required=False)) +required=False, +widget=Textarea(attrs={'rows': '10'}))) SIDEBAR_UPPER_SHOW = Setting('SIDEBAR_UPPER_SHOW', True, SIDEBAR_SET, dict( diff --git a/forum/skins/default/media/js/osqa.ask.js b/forum/skins/default/media/js/osqa.ask.js index 4bf05be..fab1c0a 100644 --- a/forum/skins/default/media/js/osqa.ask.js +++ b/forum/skins/default/media/js/osqa.ask.js @@ -19,6 +19,7 @@ $(function() { var $input = $('#id_title'); var $box = $('#ask-related-questions'); var template = $('#question-summary-template').html(); + var $editor = $('#editor'); var results_cache = {}; @@ -80,7 +81,14 @@ $(function() { $input.keyup(reload_suggestions_box); $input.focus(reload_suggestions_box); - $input.blur(close_suggestions_box); + + $editor.change(function() { + if ($editor.html().length > 10) { + close_suggestions_box(); + } + }); + + // for chrome $input.keydown(focus_on_question); diff --git a/forum/skins/default/media/style/user.css b/forum/skins/default/media/style/user.css index f4fff84..7c8bcbb 100644 --- a/forum/skins/default/media/style/user.css +++ b/forum/skins/default/media/style/user.css @@ -79,3 +79,4 @@ div.dialog.award-rep-points table input, div.dialog.award-rep-points table texta .user-moderator { background: url('/m/default/media/images/user-sprite.png') no-repeat 0 -51px; } .user-subscriptions { background: url('/m/default/media/images/user-sprite.png') no-repeat 0 -68px; } .user-superuser { background: url('/m/default/media/images/user-sprite.png') no-repeat 0 -85px; } + diff --git a/forum/skins/default/templates/paginator/page_numbers.html b/forum/skins/default/templates/paginator/page_numbers.html index abe021d..9d4fab7 100644 --- a/forum/skins/default/templates/paginator/page_numbers.html +++ b/forum/skins/default/templates/paginator/page_numbers.html @@ -19,7 +19,7 @@ {% endif %} {% endfor %} {% if has_next %} - {% trans "next page" %} » + {% trans "next" %} » {% endif %}

{% endspaceless %} \ No newline at end of file diff --git a/forum/skins/default/templates/user.html b/forum/skins/default/templates/user.html index ec852cc..61c63a0 100644 --- a/forum/skins/default/templates/user.html +++ b/forum/skins/default/templates/user.html @@ -22,11 +22,15 @@ $('#user-reputation').animate({ backgroundColor: "transparent" }, 1000); } - {% endif %} + {% block userjs %}{% endblock %} {% endblock %} {% block content %} diff --git a/forum/skins/default/templates/users/questions.html b/forum/skins/default/templates/users/questions.html index 21c193c..92de7cf 100644 --- a/forum/skins/default/templates/users/questions.html +++ b/forum/skins/default/templates/users/questions.html @@ -1,11 +1,14 @@ {% extends "user.html" %} -{% load extra_tags %} -{% load question_list_tags %} +{% load extra_tags question_list_tags i18n %} {% block usercontent %}
+{% if favorites %} {% for favorite in favorites %} {% question_list_item favorite.node favorite_count=yes signature_type=badges %} {% endfor %} +{% else %} + {% trans "No favorite questions to display." %} +{% endif %}
{% endblock %} diff --git a/forum/startup.py b/forum/startup.py index 9582a25..7f06bfb 100644 --- a/forum/startup.py +++ b/forum/startup.py @@ -11,6 +11,7 @@ get_modules_script('startup') import forum.badges import forum.subscriptions import forum.registry +get_modules_script('registry') diff --git a/forum/templatetags/user_tags.py b/forum/templatetags/user_tags.py index b0f2b61..2bc4385 100644 --- a/forum/templatetags/user_tags.py +++ b/forum/templatetags/user_tags.py @@ -37,13 +37,16 @@ class ActivityNode(template.Node): def render(self, context): try: - action = self.activity.resolve(context).leaf() + action = self.activity.resolve(context).leaf viewer = self.viewer.resolve(context) describe = mark_safe(action.describe(viewer)) return self.template.render(template.Context(dict(action=action, describe=describe))) except Exception, e: - #return action.action_type + ":" + str(e) - logging.error("Error in %s action describe: %s" % (action.action_type, str(e))) + import traceback + msg = "Error in action describe: \n %s" % ( + traceback.format_exc() + ) + logging.error(msg) @register.tag def activity_item(parser, token): diff --git a/forum/utils/pagination.py b/forum/utils/pagination.py index fb99244..382e59e 100644 --- a/forum/utils/pagination.py +++ b/forum/utils/pagination.py @@ -210,8 +210,8 @@ def _paginated(request, objects, context): def get_page(): object_list = page_obj.object_list - if hasattr(object_list, 'lazy'): - return object_list.lazy() + #if hasattr(object_list, 'lazy'): + # return object_list.lazy() return object_list paginator.page = get_page() diff --git a/forum/utils/userlinking.py b/forum/utils/userlinking.py index 53a49dc..6693883 100644 --- a/forum/utils/userlinking.py +++ b/forum/utils/userlinking.py @@ -1,6 +1,6 @@ import re -from forum.models import User, Question, Answer, Comment +from forum.models.user import User def find_best_match_in_name(content, uname, fullname, start_index): end_index = start_index + len(fullname) @@ -20,22 +20,8 @@ def find_best_match_in_name(content, uname, fullname, start_index): APPEAL_PATTERN = re.compile(r'(?