Files
reddit/r2/r2/models/link.py
2011-11-21 14:32:17 -08:00

1311 lines
46 KiB
Python

# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is Reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of the
# Original Code is CondeNet, Inc.
#
# All portions of the code written by CondeNet are Copyright (c) 2006-2010
# CondeNet, Inc. All Rights Reserved.
################################################################################
from r2.lib.db.thing import Thing, Relation, NotFound, MultiRelation, \
CreationError
from r2.lib.db.operators import desc
from r2.lib.utils import base_url, tup, domain, title_to_url, UrlParser
from r2.lib.utils.trial_utils import trial_info
from account import Account, DeletedUser
from subreddit import Subreddit
from printable import Printable
from r2.config import cache
from r2.lib.memoize import memoize
from r2.lib.filters import _force_utf8
from r2.lib import utils
from r2.lib.log import log_text
from mako.filters import url_escape
from r2.lib.strings import strings, Score
from r2.lib.db import tdb_cassandra
from pylons import c, g, request
from pylons.i18n import ungettext, _
from datetime import datetime
from hashlib import md5
import random, re
class LinkExists(Exception): pass
# defining types
class Link(Thing, Printable):
_data_int_props = Thing._data_int_props + ('num_comments', 'reported')
_defaults = dict(is_self = False,
over_18 = False,
nsfw_str = False,
reported = 0, num_comments = 0,
moderator_banned = False,
banned_before_moderator = False,
media_object = None,
promoted = None,
pending = False,
disable_comments = False,
selftext = '',
noselfreply = False,
ip = '0.0.0.0')
_essentials = ('sr_id', 'author_id')
_nsfw = re.compile(r"\bnsfw\b", re.I)
def __init__(self, *a, **kw):
Thing.__init__(self, *a, **kw)
@property
def has_thumbnail(self):
return self._t.get('has_thumbnail', hasattr(self, 'thumbnail_url'))
@classmethod
def _by_url(cls, url, sr):
from subreddit import FakeSubreddit
if isinstance(sr, FakeSubreddit):
sr = None
try:
lbu = LinksByUrl._byID(LinksByUrl._key_from_url(url))
except tdb_cassandra.NotFound:
# translate the tdb_cassandra.NotFound into the NotFound
# the caller is expecting
raise NotFound('Link "%s"' % url)
link_id36s = lbu._values()
links = Link._byID36(link_id36s, data = True, return_dict = False)
links = [l for l in links if not l._deleted]
if links and sr:
for link in links:
if sr._id == link.sr_id:
# n.b. returns the first one if there are multiple
return link
elif links:
return links
raise NotFound('Link "%s"' % url)
def set_url_cache(self):
if self.url != 'self':
LinksByUrl._set_values(LinksByUrl._key_from_url(self.url),
{self._id36: self._id36})
def update_url_cache(self, old_url):
"""Remove the old url from the by_url cache then update the
cache with the new url."""
if old_url != 'self':
try:
lbu = LinksByUrl._key_from_url(old_url)
del lbu[self._id36]
lbu._commit()
except tdb_cassandra.NotFound:
pass
self.set_url_cache()
@property
def already_submitted_link(self):
return self.make_permalink_slow() + '?already_submitted=true'
def resubmit_link(self, sr_url = False):
submit_url = self.subreddit_slow.path if sr_url else '/'
submit_url += 'submit?resubmit=true&url=' + url_escape(self.url)
return submit_url
@classmethod
def _submit(cls, title, url, author, sr, ip, spam=False):
from r2.models import admintools
l = cls(_ups = 1,
title = title,
url = url,
_spam = spam,
author_id = author._id,
sr_id = sr._id,
lang = sr.lang,
ip = ip)
l._commit()
l.set_url_cache()
if author._spam:
admintools.spam(l, banner='banned user')
return l
@classmethod
def _somethinged(cls, rel, user, link, name):
return rel._fast_query(tup(user), tup(link), name = name,
thing_data=True, timestamp_optimize = True)
def _something(self, rel, user, somethinged, name):
try:
saved = rel(user, self, name=name)
saved._commit()
except CreationError, e:
return somethinged(user, self)[(user, self, name)]
rel._fast_query_timestamp_touch(user)
return saved
def _unsomething(self, user, somethinged, name):
saved = somethinged(user, self)[(user, self, name)]
if saved:
saved._delete()
return saved
@classmethod
def _saved(cls, user, link):
return cls._somethinged(SaveHide, user, link, 'save')
def _save(self, user):
# dual-write CassandraSaves
CassandraSave._save(user, self)
return self._something(SaveHide, user, self._saved, 'save')
def _unsave(self, user):
# dual-write CassandraSaves
CassandraSave._unsave(user, self)
return self._unsomething(user, self._saved, 'save')
@classmethod
def _clicked(cls, user, link):
return cls._somethinged(Click, user, link, 'click')
def _click(self, user):
return self._something(Click, user, self._clicked, 'click')
@classmethod
def _hidden(cls, user, link):
return cls._somethinged(SaveHide, user, link, 'hide')
def _hide(self, user):
CassandraHide._hide(user, self)
return self._something(SaveHide, user, self._hidden, 'hide')
def _unhide(self, user):
CassandraHide._unhide(user, self)
return self._unsomething(user, self._hidden, 'hide')
def link_domain(self):
if self.is_self:
return 'self'
else:
return domain(self.url)
def keep_item(self, wrapped):
user = c.user if c.user_is_loggedin else None
if not c.user_is_admin:
if self._spam and (not user or
(user and self.author_id != user._id)):
return False
#author_karma = wrapped.author.link_karma
#if author_karma <= 0 and random.randint(author_karma, 0) != 0:
#return False
if user and not c.ignore_hide_rules:
if user.pref_hide_ups and wrapped.likes == True and self.author_id != user._id:
return False
if user.pref_hide_downs and wrapped.likes == False and self.author_id != user._id:
return False
if wrapped._score < user.pref_min_link_score:
return False
if wrapped.hidden:
return False
# hide NSFW links from non-logged users and under 18 logged users
# if they're not explicitly visiting an NSFW subreddit
if ((not c.user_is_loggedin and c.site != wrapped.subreddit)
or (c.user_is_loggedin and not c.over18)):
is_nsfw = bool(wrapped.over_18)
is_from_nsfw_sr = bool(wrapped.subreddit.over_18)
if is_nsfw or is_from_nsfw_sr:
return False
return True
# none of these things will change over a link's lifetime
cache_ignore = set(['subreddit', 'num_comments', 'link_child']
).union(Printable.cache_ignore)
@staticmethod
def wrapped_cache_key(wrapped, style):
s = Printable.wrapped_cache_key(wrapped, style)
if wrapped.promoted is not None:
s.extend([getattr(wrapped, "promote_status", -1),
getattr(wrapped, "disable_comments", False),
getattr(wrapped, "media_override", False),
wrapped._date,
c.user_is_sponsor,
wrapped.url, repr(wrapped.title)])
if style == "htmllite":
s.extend([request.get.has_key('twocolumn'),
c.link_target])
elif style == "xml":
s.append(request.GET.has_key("nothumbs"))
elif style == "compact":
s.append(c.permalink_page)
s.append(getattr(wrapped, 'media_object', {}))
return s
def make_permalink(self, sr, force_domain = False):
from r2.lib.template_helpers import get_domain
p = "comments/%s/%s/" % (self._id36, title_to_url(self.title))
# promoted links belong to a separate subreddit and shouldn't
# include that in the path
if self.promoted is not None:
if force_domain:
res = "http://%s/%s" % (get_domain(cname = False,
subreddit = False), p)
else:
res = "/%s" % p
elif not c.cname and not force_domain:
res = "/r/%s/%s" % (sr.name, p)
elif sr != c.site or force_domain:
if(c.cname and sr == c.site):
res = "http://%s/%s" % (get_domain(cname = True,
subreddit = False),p)
else:
res = "http://%s/r/%s/%s" % (get_domain(cname = False,
subreddit = False),sr.name,p)
else:
res = "/%s" % p
# WARNING: If we ever decide to add any ?foo=bar&blah parameters
# here, Comment.make_permalink will need to be updated or else
# it will fail.
return res
def make_permalink_slow(self, force_domain = False):
return self.make_permalink(self.subreddit_slow,
force_domain = force_domain)
@staticmethod
def _should_expunge_selftext(link):
verdict = getattr(link, "verdict", "")
if verdict not in ("admin-removed", "mod-removed"):
return False
if not c.user_is_loggedin:
return True
if c.user_is_admin:
return False
if c.user == link.author:
return False
if link.can_ban:
return False
return True
@classmethod
def add_props(cls, user, wrapped):
from r2.lib.pages import make_link_child
from r2.lib.count import incr_counts
from r2.lib import media
from r2.lib.utils import timeago
from r2.lib.template_helpers import get_domain
from r2.models.subreddit import FakeSubreddit
from r2.lib.wrapped import CachedVariable
# referencing c's getattr is cheap, but not as cheap when it
# is in a loop that calls it 30 times on 25-200 things.
user_is_admin = c.user_is_admin
user_is_loggedin = c.user_is_loggedin
pref_media = user.pref_media
pref_frame = user.pref_frame
pref_newwindow = user.pref_newwindow
cname = c.cname
site = c.site
if user_is_loggedin:
saved_lu = []
for item in wrapped:
if not SaveHide._can_skip_lookup(user, item):
saved_lu.append(item._id36)
saved = CassandraSave._fast_query(user._id36, saved_lu)
hidden = CassandraHide._fast_query(user._id36, saved_lu)
clicked = {}
else:
saved = hidden = clicked = {}
trials = trial_info(wrapped)
for item in wrapped:
show_media = False
if not hasattr(item, "score_fmt"):
item.score_fmt = Score.number_only
if c.render_style == 'compact':
item.score_fmt = Score.points
item.pref_compress = user.pref_compress
if user.pref_compress and item.promoted is None:
item.render_css_class = "compressed link"
item.score_fmt = Score.points
elif pref_media == 'on' and not user.pref_compress:
show_media = True
elif pref_media == 'subreddit' and item.subreddit.show_media:
show_media = True
elif item.promoted and item.has_thumbnail:
if user_is_loggedin and item.author_id == user._id:
show_media = True
elif pref_media != 'off' and not user.pref_compress:
show_media = True
item.nsfw_str = item._nsfw.findall(item.title)
item.over_18 = bool(item.over_18 or item.subreddit.over_18 or
item.nsfw_str)
item.nsfw = item.over_18 and user.pref_label_nsfw
item.is_author = (user == item.author)
item.thumbnail_sprited = False
# always show a promo author their own thumbnail
if item.promoted and (user_is_admin or item.is_author) and item.has_thumbnail:
item.thumbnail = media.thumbnail_url(item)
elif user.pref_no_profanity and item.over_18 and not c.site.over_18:
if show_media:
item.thumbnail = "nsfw"
item.thumbnail_sprited = True
else:
item.thumbnail = ""
elif not show_media:
item.thumbnail = ""
elif item.has_thumbnail:
item.thumbnail = media.thumbnail_url(item)
elif item.is_self:
item.thumbnail = "self"
item.thumbnail_sprited = True
else:
item.thumbnail = "default"
item.thumbnail_sprited = True
item.score = max(0, item.score)
if getattr(item, "domain_override", None):
item.domain = item.domain_override
else:
item.domain = (domain(item.url) if not item.is_self
else 'self.' + item.subreddit.name)
item.urlprefix = ''
if user_is_loggedin:
item.saved = (user._id36, item._id36) in saved
item.hidden = (user._id36, item._id36) in hidden
item.clicked = bool(clicked.get((user, item, 'click')))
else:
item.saved = item.hidden = item.clicked = False
item.num = None
item.permalink = item.make_permalink(item.subreddit)
if item.is_self:
item.url = item.make_permalink(item.subreddit,
force_domain = True)
if g.shortdomain:
item.shortlink = g.shortdomain + '/' + item._id36
# do we hide the score?
if user_is_admin:
item.hide_score = False
elif item.promoted and item.score <= 0:
item.hide_score = True
elif user == item.author:
item.hide_score = False
# TODO: uncomment to let gold users see the score of upcoming links
# elif user.gold:
# item.hide_score = False
elif item._date > timeago("2 hours"):
item.hide_score = True
else:
item.hide_score = False
# store user preferences locally for caching
item.pref_frame = pref_frame
item.newwindow = pref_newwindow
# is this link a member of a different (non-c.site) subreddit?
item.different_sr = (isinstance(site, FakeSubreddit) or
site.name != item.subreddit.name)
if user_is_loggedin and item.author_id == user._id:
item.nofollow = False
elif item.score <= 1 or item._spam or item.author._spam:
item.nofollow = True
else:
item.nofollow = False
item.subreddit_path = item.subreddit.path
if cname:
item.subreddit_path = ("http://" +
get_domain(cname = (site == item.subreddit),
subreddit = False))
if site != item.subreddit:
item.subreddit_path += item.subreddit.path
item.domain_path = "/domain/%s/" % item.domain
if item.is_self:
item.domain_path = item.subreddit_path
# attach video or selftext as needed
item.link_child, item.editable = make_link_child(item)
item.tblink = "http://%s/tb/%s" % (
get_domain(cname = cname, subreddit=False),
item._id36)
if item.is_self:
item.href_url = item.permalink
else:
item.href_url = item.url
# show the toolbar if the preference is set and the link
# is neither a promoted link nor a self post
if pref_frame and not item.is_self and not item.promoted:
item.mousedown_url = item.tblink
else:
item.mousedown_url = None
item.fresh = not any((item.likes != None,
item.saved,
item.clicked,
item.hidden,
item._deleted,
item._spam))
# bits that we will render stubs (to make the cached
# version more flexible)
item.num = CachedVariable("num")
item.numcolmargin = CachedVariable("numcolmargin")
item.commentcls = CachedVariable("commentcls")
item.midcolmargin = CachedVariable("midcolmargin")
item.comment_label = CachedVariable("numcomments")
item.as_deleted = False
if item.deleted and not c.user_is_admin:
item.author = DeletedUser()
item.as_deleted = True
item.trial_info = trials.get(item._fullname, None)
item.approval_checkmark = None
item_age = datetime.now(g.tz) - item._date
if item_age.days > g.VOTE_AGE_LIMIT and item.promoted is None:
item.votable = False
else:
item.votable = True
if item.can_ban:
verdict = getattr(item, "verdict", None)
if verdict in ('admin-approved', 'mod-approved'):
approver = None
if getattr(item, "ban_info", None):
approver = item.ban_info.get("unbanner", None)
if approver:
item.approval_checkmark = _("approved by %s") % approver
else:
item.approval_checkmark = _("approved by a moderator")
if item.trial_info is not None:
item.reveal_trial_info = True
item.use_big_modbuttons = True
item.expunged = False
if item.is_self:
item.expunged = Link._should_expunge_selftext(item)
if user_is_loggedin:
incr_counts(wrapped)
# Run this last
Printable.add_props(user, wrapped)
@property
def subreddit_slow(self):
from subreddit import Subreddit
"""return's a link's subreddit. in most case the subreddit is already
on the wrapped link (as .subreddit), and that should be used
when possible. """
return Subreddit._byID(self.sr_id, True, return_dict = False)
class LinksByUrl(tdb_cassandra.View):
_use_db = True
@classmethod
def _key_from_url(cls, url):
if not utils.domain(url) in g.case_sensitive_domains:
keyurl = _force_utf8(UrlParser.base_url(url.lower()))
else:
# Convert only hostname to lowercase
up = UrlParser(url)
up.hostname = up.hostname.lower()
keyurl = _force_utf8(UrlParser.base_url(up.unparse()))
return keyurl
# Note that there are no instances of PromotedLink or LinkCompressed,
# so overriding their methods here will not change their behaviour
# (except for add_props). These classes are used to override the
# render_class on a Wrapped to change the template used for rendering
class PromotedLink(Link):
_nodb = True
@classmethod
def add_props(cls, user, wrapped):
# prevents cyclic dependencies
from r2.lib import promote
Link.add_props(user, wrapped)
user_is_sponsor = c.user_is_sponsor
status_dict = dict((v, k) for k, v in promote.STATUS.iteritems())
for item in wrapped:
# these are potentially paid for placement
item.nofollow = True
item.user_is_sponsor = user_is_sponsor
status = getattr(item, "promote_status", -1)
if item.is_author or c.user_is_sponsor:
item.rowstyle = "link " + promote.STATUS.name[status].lower()
else:
item.rowstyle = "link promoted"
# Run this last
Printable.add_props(user, wrapped)
class Comment(Thing, Printable):
_data_int_props = Thing._data_int_props + ('reported',)
_defaults = dict(reported = 0, parent_id = None,
moderator_banned = False, new = False,
banned_before_moderator = False)
_essentials = ('link_id', 'author_id')
def _markdown(self):
pass
@classmethod
def _new(cls, author, link, parent, body, ip):
from r2.lib.db.queries import changed
c = Comment(_ups = 1,
body = body,
link_id = link._id,
sr_id = link.sr_id,
author_id = author._id,
ip = ip)
c._spam = author._spam
#these props aren't relations
if parent:
c.parent_id = parent._id
link._incr('num_comments', 1)
to = None
name = 'inbox'
if parent:
to = Account._byID(parent.author_id, True)
elif link.is_self and not link.noselfreply:
to = Account._byID(link.author_id, True)
name = 'selfreply'
c._commit()
changed(link, True) # link's number of comments changed
inbox_rel = None
# only global admins can be message spammed.
# Don't send the message if the recipient has blocked
# the author
if to and ((not c._spam and author._id not in to.enemies)
or to.name in g.admins):
# When replying to your own comment, record the inbox
# relation, but don't give yourself an orangered
orangered = (to.name != author.name)
inbox_rel = Inbox._add(to, c, name, orangered=orangered)
return (c, inbox_rel)
@property
def subreddit_slow(self):
from subreddit import Subreddit
"""return's a comments's subreddit. in most case the subreddit is already
on the wrapped link (as .subreddit), and that should be used
when possible. if sr_id does not exist, then use the parent link's"""
self._safe_load()
if hasattr(self, 'sr_id'):
sr_id = self.sr_id
else:
l = Link._byID(self.link_id, True)
sr_id = l.sr_id
return Subreddit._byID(sr_id, True, return_dict = False)
def keep_item(self, wrapped):
return True
cache_ignore = set(["subreddit", "link", "to"]
).union(Printable.cache_ignore)
@staticmethod
def wrapped_cache_key(wrapped, style):
s = Printable.wrapped_cache_key(wrapped, style)
s.extend([wrapped.body])
return s
def make_permalink(self, link, sr=None, context=None, anchor=False):
url = link.make_permalink(sr) + self._id36
if context:
url += "?context=%d" % context
if anchor:
url += "#%s" % self._id36
return url
def make_permalink_slow(self, context=None, anchor=False):
l = Link._byID(self.link_id, data=True)
return self.make_permalink(l, l.subreddit_slow,
context=context, anchor=anchor)
@classmethod
def add_props(cls, user, wrapped):
from r2.lib.template_helpers import add_attr, get_domain
from r2.lib import promote
from r2.lib.wrapped import CachedVariable
from r2.lib.pages import WrappedUser
#fetch parent links
links = Link._byID(set(l.link_id for l in wrapped), data = True,
return_dict = True, stale=True)
# fetch authors
authors = Account._byID(set(l.author_id for l in links.values()), data=True,
return_dict=True, stale=True)
#get srs for comments that don't have them (old comments)
for cm in wrapped:
if not hasattr(cm, 'sr_id'):
cm.sr_id = links[cm.link_id].sr_id
subreddits = Subreddit._byID(set(cm.sr_id for cm in wrapped),
data=True, return_dict=False, stale=True)
cids = dict((w._id, w) for w in wrapped)
parent_ids = set(cm.parent_id for cm in wrapped
if getattr(cm, 'parent_id', None)
and cm.parent_id not in cids)
parents = {}
if parent_ids:
parents = Comment._byID(parent_ids, data=True, stale=True)
can_reply_srs = set(s._id for s in subreddits if s.can_comment(user)) \
if c.user_is_loggedin else set()
can_reply_srs.add(promote.get_promote_srid())
min_score = user.pref_min_comment_score
profilepage = c.profilepage
user_is_admin = c.user_is_admin
user_is_loggedin = c.user_is_loggedin
focal_comment = c.focal_comment
cname = c.cname
site = c.site
for item in wrapped:
# for caching:
item.profilepage = c.profilepage
item.link = links.get(item.link_id)
if (item.link._score <= 1 or item.score < 3 or
item.link._spam or item._spam or item.author._spam):
item.nofollow = True
else:
item.nofollow = False
if not hasattr(item, 'subreddit'):
item.subreddit = item.subreddit_slow
if item.author_id == item.link.author_id and not item.link._deleted:
add_attr(item.attribs, 'S',
link = item.link.make_permalink(item.subreddit))
if not hasattr(item, 'target'):
item.target = None
if item.parent_id:
if item.parent_id in cids:
item.parent_permalink = '#' + utils.to36(item.parent_id)
else:
parent = parents[item.parent_id]
item.parent_permalink = parent.make_permalink(item.link, item.subreddit)
else:
item.parent_permalink = None
item.can_reply = False
if c.can_reply or (item.sr_id in can_reply_srs):
age = datetime.now(g.tz) - item._date
if age.days < g.REPLY_AGE_LIMIT:
item.can_reply = True
# not deleted on profile pages,
# deleted if spam and not author or admin
item.deleted = (not profilepage and
(item._deleted or
(item._spam and
item.author != user and
not item.show_spam)))
extra_css = ''
if item.deleted:
extra_css += "grayed"
if not user_is_admin:
item.author = DeletedUser()
item.body = '[deleted]'
if focal_comment == item._id36:
extra_css += " border"
if profilepage:
item.link_author = WrappedUser(authors[item.link.author_id])
item.subreddit_path = item.subreddit.path
if cname:
item.subreddit_path = ("http://" +
get_domain(cname = (site == item.subreddit),
subreddit = False))
if site != item.subreddit:
item.subreddit_path += item.subreddit.path
item.full_comment_path = item.link.make_permalink(item.subreddit)
# don't collapse for admins, on profile pages, or if deleted
item.collapsed = False
if ((item.score < min_score) and not (profilepage or
item.deleted or user_is_admin)):
item.collapsed = True
item.collapsed_reason = _("comment score below threshold")
if user_is_loggedin and item.author_id in c.user.enemies:
if "grayed" not in extra_css:
extra_css += " grayed"
item.collapsed = True
item.collapsed_reason = _("blocked user")
item.editted = getattr(item, "editted", False)
item.render_css_class = "comment %s" % CachedVariable("time_period")
#will get updated in builder
item.num_children = 0
item.score_fmt = Score.points
item.permalink = item.make_permalink(item.link, item.subreddit)
item.is_author = (user == item.author)
item.is_focal = (focal_comment == item._id36)
item_age = c.start_time - item._date
if item_age.days > g.VOTE_AGE_LIMIT:
item.votable = False
else:
item.votable = True
#will seem less horrible when add_props is in pages.py
from r2.lib.pages import UserText
item.usertext = UserText(item, item.body,
editable = item.is_author,
nofollow = item.nofollow,
target = item.target,
extra_css = extra_css)
# Run this last
Printable.add_props(user, wrapped)
class CommentSortsCache(tdb_cassandra.View):
"""A cache of the sort-values of comments to avoid looking up all
of the comments in a big tree at render-time just to determine
the candidate order"""
_use_db = True
_value_type = 'float'
class StarkComment(Comment):
"""Render class for the comments in the top-comments display in
the reddit toolbar"""
_nodb = True
class MoreMessages(Printable):
cachable = False
display = ""
new = False
was_comment = False
is_collapsed = True
def __init__(self, parent, child):
self.parent = parent
self.child = child
@staticmethod
def wrapped_cache_key(item, style):
return False
@property
def _fullname(self):
return self.parent._fullname
@property
def _id36(self):
return self.parent._id36
@property
def subject(self):
return self.parent.subject
@property
def childlisting(self):
return self.child
@property
def to(self):
return self.parent.to
@property
def author(self):
return self.parent.author
@property
def recipient(self):
return self.parent.recipient
@property
def sr_id(self):
return self.parent.sr_id
@property
def subreddit(self):
return self.parent.subreddit
class MoreComments(Printable):
cachable = False
display = ""
@staticmethod
def wrapped_cache_key(item, style):
return False
def __init__(self, link, depth, parent_id = None):
from r2.lib.wrapped import CachedVariable
if parent_id is not None:
id36 = utils.to36(parent_id)
self.parent_id = parent_id
self.parent_name = "t%s_%s" % (utils.to36(Comment._type_id), id36)
self.parent_permalink = link.make_permalink_slow() + id36
self.link_name = link._fullname
self.link_id = link._id
self.depth = depth
self.children = []
self.count = 0
self.previous_visits_hex = CachedVariable("previous_visits_hex")
@property
def _fullname(self):
return "t%s_%s" % (utils.to36(Comment._type_id), self._id36)
@property
def _id36(self):
return utils.to36(self.children[0]) if self.children else '_'
class MoreRecursion(MoreComments):
pass
class MoreChildren(MoreComments):
pass
class Message(Thing, Printable):
_defaults = dict(reported = 0, was_comment = False, parent_id = None,
new = False, first_message = None, to_id = None,
sr_id = None, to_collapse = None, author_collapse = None)
_data_int_props = Thing._data_int_props + ('reported', )
_essentials = ('author_id',)
cache_ignore = set(["to", "subreddit"]).union(Printable.cache_ignore)
@classmethod
def _new(cls, author, to, subject, body, ip, parent = None, sr = None):
m = Message(subject = subject,
body = body,
author_id = author._id,
new = True,
ip = ip)
m._spam = author._spam
sr_id = None
# check to see if the recipient is a subreddit and swap args accordingly
if to and isinstance(to, Subreddit):
to_subreddit = True
to, sr = None, to
else:
to_subreddit = False
if sr:
sr_id = sr._id
if parent:
m.parent_id = parent._id
if parent.first_message:
m.first_message = parent.first_message
else:
m.first_message = parent._id
if parent.sr_id:
sr_id = parent.sr_id
if not to and not sr_id:
raise CreationError, "Message created with neither to nor sr_id"
m.to_id = to._id if to else None
if sr_id is not None:
m.sr_id = sr_id
m._commit()
inbox_rel = None
if sr_id and not sr:
sr = Subreddit._byID(sr_id)
inbox_rel = []
if sr_id:
# if there is a subreddit id, and it's either a reply or
# an initial message to an SR, add to the moderator inbox
# (i.e., don't do it for automated messages from the SR)
if parent or to_subreddit:
inbox_rel.append(ModeratorInbox._add(sr, m, 'inbox'))
if author.name in g.admins:
m.distinguished = 'admin'
m._commit()
elif sr.is_moderator(author):
m.distinguished = 'yes'
m._commit()
# if there is a "to" we may have to create an inbox relation as well
# also, only global admins can be message spammed.
if to and (not m._spam or to.name in g.admins):
# if the current "to" is not a sr moderator,
# they need to be notified
if not sr_id or not sr.is_moderator(to):
# Record the inbox relation, but don't give the user
# an orangered, if they PM themselves.
# Don't notify on PMs from blocked users, either
orangered = (to.name != author.name and
author._id not in to.enemies)
inbox_rel.append(Inbox._add(to, m, 'inbox',
orangered=orangered))
# find the message originator
elif sr_id and m.first_message:
first = Message._byID(m.first_message, True)
orig = Account._byID(first.author_id, True)
# if the originator is not a moderator...
if not sr.is_moderator(orig) and orig._id != author._id:
inbox_rel.append(Inbox._add(orig, m, 'inbox'))
return (m, inbox_rel)
@property
def permalink(self):
return "/message/messages/%s" % self._id36
def can_view_slow(self):
if c.user_is_loggedin:
# simple case from before:
if (c.user_is_admin or
c.user._id in (self.author_id, self.to_id)):
return True
elif self.sr_id:
sr = Subreddit._byID(self.sr_id)
is_moderator = sr.is_moderator(c.user)
# moderators can view messages on subreddits they moderate
if is_moderator:
return True
elif self.first_message:
first = Message._byID(self.first_message, True)
return (first.author_id == c.user._id)
@classmethod
def add_props(cls, user, wrapped):
from r2.lib.db import queries
#TODO global-ish functions that shouldn't be here?
#reset msgtime after this request
msgtime = c.have_messages
# make sure there is a sr_id set:
for w in wrapped:
if not hasattr(w, "sr_id"):
w.sr_id = None
# load the to fields if one exists
to_ids = set(w.to_id for w in wrapped if w.to_id is not None)
tos = Account._byID(to_ids, True) if to_ids else {}
# load the subreddit field if one exists:
sr_ids = set(w.sr_id for w in wrapped if w.sr_id is not None)
m_subreddits = Subreddit._byID(sr_ids, data = True, return_dict = True)
# load the links and their subreddits (if comment-as-message)
links = Link._byID(set(l.link_id for l in wrapped if l.was_comment),
data = True,
return_dict = True)
# subreddits of the links (for comment-as-message)
l_subreddits = Subreddit._byID(set(l.sr_id for l in links.values()),
data = True, return_dict = True)
parents = Comment._byID(set(l.parent_id for l in wrapped
if l.parent_id and l.was_comment),
data = True, return_dict = True)
# load the unread list to determine message newness
unread = set(queries.get_unread_inbox(user))
msg_srs = set(m_subreddits[x.sr_id]
for x in wrapped if x.sr_id is not None
and isinstance(x.lookups[0], Message))
# load the unread mod list for the same reason
mod_unread = set(queries.merge_results(
*[queries.get_unread_subreddit_messages(sr) for sr in msg_srs]))
for item in wrapped:
item.to = tos.get(item.to_id)
if item.sr_id:
item.recipient = (item.author_id != c.user._id)
else:
item.recipient = (item.to_id == c.user._id)
# new-ness is stored on the relation
if item.author_id == c.user._id:
item.new = False
elif item._fullname in unread:
item.new = True
# wipe new messages if preferences say so, and this isn't a feed
# and it is in the user's personal inbox
if (item.new and c.user.pref_mark_messages_read
and c.extension not in ("rss", "xml", "api", "json")):
queries.set_unread(item.lookups[0],
c.user, False)
else:
item.new = (item._fullname in mod_unread)
item.score_fmt = Score.none
item.message_style = ""
# comment as message:
if item.was_comment:
link = links[item.link_id]
sr = l_subreddits[link.sr_id]
item.to_collapse = False
item.author_collapse = False
item.link_title = link.title
item.permalink = item.lookups[0].make_permalink(link, sr=sr)
item.link_permalink = link.make_permalink(sr)
if item.parent_id:
item.subject = _('comment reply')
item.message_style = "comment-reply"
parent = parents[item.parent_id]
item.parent = parent._fullname
item.parent_permalink = parent.make_permalink(link, sr)
else:
item.subject = _('post reply')
item.message_style = "post-reply"
elif item.sr_id is not None:
item.subreddit = m_subreddits[item.sr_id]
item.is_collapsed = None
if not item.new:
if item.recipient:
item.is_collapsed = item.to_collapse
if item.author_id == c.user._id:
item.is_collapsed = item.author_collapse
if c.user.pref_collapse_read_messages:
item.is_collapsed = (item.is_collapsed is not False)
if item.author_id in c.user.enemies and not item.was_comment:
item.is_collapsed = True
if not c.user_is_admin:
item.subject = _('[message from blocked user]')
item.body = _('[unblock user to see this message]')
# Run this last
Printable.add_props(user, wrapped)
@property
def subreddit_slow(self):
from subreddit import Subreddit
if self.sr_id:
return Subreddit._byID(self.sr_id)
@staticmethod
def wrapped_cache_key(wrapped, style):
s = Printable.wrapped_cache_key(wrapped, style)
s.extend([wrapped.new, wrapped.collapsed])
return s
def keep_item(self, wrapped):
return True
class SaveHide(Relation(Account, Link)): pass
class Click(Relation(Account, Link)): pass
class SimpleRelation(tdb_cassandra.Relation):
_use_db = False
@classmethod
def _create(cls, user, link, write_consistency_level = None):
n = cls(thing1_id = user._id36,
thing2_id = link._id36)
n._commit(write_consistency_level=write_consistency_level)
return n
@classmethod
def _uncreate(cls, user, link):
try:
cls._fast_query(user._id36, link._id36)._destroy()
except tdb_cassandra.NotFound:
pass
class CassandraSave(SimpleRelation):
_use_db = True
_cf_name = 'Save'
# thing1_cls = Account
# thing2_cls = Link
@classmethod
def _save(cls, *a, **kw):
return cls._create(*a, **kw)
@classmethod
def _unsave(cls, *a, **kw):
return cls._uncreate(*a, **kw)
def _on_create(self):
# it's okay if these indices get lost
wcl = tdb_cassandra.CL.ONE
SavesByAccount._set_values(self.thing1_id,
{self._id: self._id},
write_consistency_level=wcl)
return SimpleRelation._on_create(self)
def _on_destroy(self):
sba = SavesByAccount._byID(self.thing1_id)
del sba[self._id]
sba._commit()
return SimpleRelation._on_destroy(self)
class CassandraHide(SimpleRelation):
_use_db = True
_cf_name = 'Hide'
_ttl = 7*24*60*60
@classmethod
def _hide(cls, *a, **kw):
return cls._create(*a, **kw)
@classmethod
def _unhide(cls, *a, **kw):
return cls._uncreate(*a, **kw)
class CassandraClick(SimpleRelation):
_use_db = True
_cf_name = 'Click'
class SavesByAccount(tdb_cassandra.View):
_use_db = True
_cf_name = 'SavesByAccount'
class Inbox(MultiRelation('inbox',
Relation(Account, Comment),
Relation(Account, Message))):
_defaults = dict(new = False)
@classmethod
def _add(cls, to, obj, *a, **kw):
orangered = kw.pop("orangered", True)
i = Inbox(to, obj, *a, **kw)
i.new = True
i._commit()
if not to._loaded:
to._load()
#if there is not msgtime, or it's false, set it
if orangered and (not hasattr(to, 'msgtime') or not to.msgtime):
to.msgtime = obj._date
to._commit()
return i
@classmethod
def set_unread(cls, thing, unread, to = None):
inbox_rel = cls.rel(Account, thing.__class__)
if to:
inbox = inbox_rel._query(inbox_rel.c._thing2_id == thing._id,
eager_load = True)
else:
inbox = inbox_rel._query(inbox_rel.c._thing2_id == thing._id,
inbox_rel.c._thing1_id == to._id,
eager_load = True)
res = []
for i in inbox:
if i:
i.new = unread
i._commit()
res.append(i)
return res
class LinkOnTrial(Printable):
@classmethod
def add_props(cls, user, wrapped):
Link.add_props(user, wrapped)
for item in wrapped:
item.rowstyle = "link ontrial"
# Run this last
Printable.add_props(user, wrapped)
class ModeratorInbox(Relation(Subreddit, Message)):
#TODO: shouldn't dupe this
@classmethod
def _add(cls, sr, obj, *a, **kw):
i = ModeratorInbox(sr, obj, *a, **kw)
i.new = True
i._commit()
if not sr._loaded:
sr._load()
moderators = Account._byID(sr.moderator_ids(), data=True,
return_dict = False)
for m in moderators:
if obj.author_id != m._id and not getattr(m, 'modmsgtime', None):
m.modmsgtime = obj._date
m._commit()
return i
@classmethod
def set_unread(cls, thing, unread):
inbox = cls._query(cls.c._thing2_id == thing._id,
eager_load = True)
res = []
for i in inbox:
if i:
i.new = unread
i._commit()
res.append(i)
return res