PageRenderTime 53ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/r2/r2/models/link.py

https://github.com/stevewilber/reddit
Python | 1562 lines | 1478 code | 43 blank | 41 comment | 88 complexity | a5f79d00643cf43dbe81cf2739be0c0b MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, Apache-2.0

Large files files are truncated, but you can click here to view the full file

  1. # The contents of this file are subject to the Common Public Attribution
  2. # License Version 1.0. (the "License"); you may not use this file except in
  3. # compliance with the License. You may obtain a copy of the License at
  4. # http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
  5. # License Version 1.1, but Sections 14 and 15 have been added to cover use of
  6. # software over a computer network and provide for limited attribution for the
  7. # Original Developer. In addition, Exhibit A has been modified to be consistent
  8. # with Exhibit B.
  9. #
  10. # Software distributed under the License is distributed on an "AS IS" basis,
  11. # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
  12. # the specific language governing rights and limitations under the License.
  13. #
  14. # The Original Code is reddit.
  15. #
  16. # The Original Developer is the Initial Developer. The Initial Developer of
  17. # the Original Code is reddit Inc.
  18. #
  19. # All portions of the code written by reddit are Copyright (c) 2006-2012 reddit
  20. # Inc. All Rights Reserved.
  21. ###############################################################################
  22. from r2.lib.db.thing import Thing, Relation, NotFound, MultiRelation, \
  23. CreationError
  24. from r2.lib.db.operators import desc
  25. from r2.lib.utils import base_url, tup, domain, title_to_url, UrlParser
  26. from account import Account, DeletedUser
  27. from subreddit import Subreddit, DomainSR
  28. from printable import Printable
  29. from r2.config import cache, extensions
  30. from r2.lib.memoize import memoize
  31. from r2.lib.filters import _force_utf8
  32. from r2.lib import utils
  33. from r2.lib.log import log_text
  34. from mako.filters import url_escape
  35. from r2.lib.strings import strings, Score
  36. from r2.lib.db import tdb_cassandra
  37. from r2.lib.db.tdb_cassandra import NotFoundException, view_of
  38. from r2.models.subreddit import MultiReddit
  39. from r2.models.promo import PROMOTE_STATUS, get_promote_srid
  40. from r2.models.query_cache import CachedQueryMutator
  41. from pylons import c, g, request
  42. from pylons.i18n import ungettext, _
  43. from datetime import datetime, timedelta
  44. from hashlib import md5
  45. import random, re
  46. class LinkExists(Exception): pass
  47. # defining types
  48. class Link(Thing, Printable):
  49. _data_int_props = Thing._data_int_props + ('num_comments', 'reported')
  50. _defaults = dict(is_self=False,
  51. over_18=False,
  52. nsfw_str=False,
  53. reported=0, num_comments=0,
  54. moderator_banned=False,
  55. banned_before_moderator=False,
  56. media_object=None,
  57. promoted=None,
  58. pending=False,
  59. disable_comments=False,
  60. selftext='',
  61. noselfreply=False,
  62. ip='0.0.0.0',
  63. flair_text=None,
  64. flair_css_class=None)
  65. _essentials = ('sr_id', 'author_id')
  66. _nsfw = re.compile(r"\bnsfw\b", re.I)
  67. def __init__(self, *a, **kw):
  68. Thing.__init__(self, *a, **kw)
  69. @property
  70. def has_thumbnail(self):
  71. return self._t.get('has_thumbnail', hasattr(self, 'thumbnail_url'))
  72. @classmethod
  73. def _by_url(cls, url, sr):
  74. from subreddit import FakeSubreddit
  75. if isinstance(sr, FakeSubreddit):
  76. sr = None
  77. try:
  78. lbu = LinksByUrl._byID(LinksByUrl._key_from_url(url))
  79. except tdb_cassandra.NotFound:
  80. # translate the tdb_cassandra.NotFound into the NotFound
  81. # the caller is expecting
  82. raise NotFound('Link "%s"' % url)
  83. link_id36s = lbu._values()
  84. links = Link._byID36(link_id36s, data=True, return_dict=False)
  85. links = [l for l in links if not l._deleted]
  86. if links and sr:
  87. for link in links:
  88. if sr._id == link.sr_id:
  89. # n.b. returns the first one if there are multiple
  90. return link
  91. elif links:
  92. return links
  93. raise NotFound('Link "%s"' % url)
  94. def set_url_cache(self):
  95. if self.url != 'self':
  96. LinksByUrl._set_values(LinksByUrl._key_from_url(self.url),
  97. {self._id36: ''})
  98. @property
  99. def already_submitted_link(self):
  100. return self.make_permalink_slow() + '?already_submitted=true'
  101. def resubmit_link(self, sr_url=False):
  102. submit_url = self.subreddit_slow.path if sr_url else '/'
  103. submit_url += 'submit?resubmit=true&url=' + url_escape(self.url)
  104. return submit_url
  105. @classmethod
  106. def _submit(cls, title, url, author, sr, ip, spam=False):
  107. from r2.models import admintools
  108. l = cls(_ups=1,
  109. title=title,
  110. url=url,
  111. _spam=spam,
  112. author_id=author._id,
  113. sr_id=sr._id,
  114. lang=sr.lang,
  115. ip=ip)
  116. l._commit()
  117. l.set_url_cache()
  118. if author._spam:
  119. g.stats.simple_event('spam.autoremove.link')
  120. admintools.spam(l, banner='banned user')
  121. return l
  122. @classmethod
  123. def _somethinged(cls, rel, user, link, name):
  124. return rel._fast_query(tup(user), tup(link), name=name,
  125. thing_data=True, timestamp_optimize=True)
  126. def _something(self, rel, user, somethinged, name):
  127. try:
  128. saved = rel(user, self, name=name)
  129. saved._commit()
  130. except CreationError, e:
  131. return somethinged(user, self)[(user, self, name)]
  132. return saved
  133. def _unsomething(self, user, somethinged, name):
  134. saved = somethinged(user, self)[(user, self, name)]
  135. if saved:
  136. saved._delete()
  137. return saved
  138. @classmethod
  139. def _saved(cls, user, link):
  140. return cls._somethinged(SaveHide, user, link, 'save')
  141. def _save(self, user):
  142. LinkSavesByAccount._save(user, self)
  143. return self._something(SaveHide, user, self._saved, 'save')
  144. def _unsave(self, user):
  145. LinkSavesByAccount._unsave(user, self)
  146. return self._unsomething(user, self._saved, 'save')
  147. @classmethod
  148. def _clicked(cls, user, link):
  149. return cls._somethinged(Click, user, link, 'click')
  150. def _click(self, user):
  151. return self._something(Click, user, self._clicked, 'click')
  152. @classmethod
  153. def _hidden(cls, user, link):
  154. return cls._somethinged(SaveHide, user, link, 'hide')
  155. def _hide(self, user):
  156. LinkHidesByAccount._hide(user, self)
  157. return self._something(SaveHide, user, self._hidden, 'hide')
  158. def _unhide(self, user):
  159. LinkHidesByAccount._unhide(user, self)
  160. return self._unsomething(user, self._hidden, 'hide')
  161. def link_domain(self):
  162. if self.is_self:
  163. return 'self'
  164. else:
  165. return domain(self.url)
  166. def keep_item(self, wrapped):
  167. user = c.user if c.user_is_loggedin else None
  168. if not (c.user_is_admin or (isinstance(c.site, DomainSR) and
  169. wrapped.subreddit.is_moderator(user))):
  170. if self._spam and (not user or
  171. (user and self.author_id != user._id)):
  172. return False
  173. #author_karma = wrapped.author.link_karma
  174. #if author_karma <= 0 and random.randint(author_karma, 0) != 0:
  175. #return False
  176. if user and not c.ignore_hide_rules:
  177. if user.pref_hide_ups and wrapped.likes == True and self.author_id != user._id:
  178. return False
  179. if user.pref_hide_downs and wrapped.likes == False and self.author_id != user._id:
  180. return False
  181. if wrapped._score < user.pref_min_link_score:
  182. return False
  183. if wrapped.hidden:
  184. return False
  185. # Always show NSFW to API users unless obey_over18=true in querystring
  186. is_api = c.render_style in extensions.API_TYPES
  187. if is_api and not c.obey_over18:
  188. return True
  189. # hide NSFW links from non-logged users and under 18 logged users
  190. # if they're not explicitly visiting an NSFW subreddit or a multireddit
  191. if (((not c.user_is_loggedin and c.site != wrapped.subreddit)
  192. or (c.user_is_loggedin and not c.over18))
  193. and not (isinstance(c.site, MultiReddit) and c.over18)):
  194. is_nsfw = bool(wrapped.over_18)
  195. is_from_nsfw_sr = bool(wrapped.subreddit.over_18)
  196. if is_nsfw or is_from_nsfw_sr:
  197. return False
  198. return True
  199. # none of these things will change over a link's lifetime
  200. cache_ignore = set(['subreddit', 'num_comments', 'link_child']
  201. ).union(Printable.cache_ignore)
  202. @staticmethod
  203. def wrapped_cache_key(wrapped, style):
  204. s = Printable.wrapped_cache_key(wrapped, style)
  205. if wrapped.promoted is not None:
  206. s.extend([getattr(wrapped, "promote_status", -1),
  207. getattr(wrapped, "disable_comments", False),
  208. getattr(wrapped, "media_override", False),
  209. wrapped._date,
  210. c.user_is_sponsor,
  211. wrapped.url, repr(wrapped.title)])
  212. if style == "htmllite":
  213. s.extend([request.get.has_key('twocolumn'),
  214. c.link_target])
  215. elif style == "xml":
  216. s.append(request.GET.has_key("nothumbs"))
  217. elif style == "compact":
  218. s.append(c.permalink_page)
  219. s.append(getattr(wrapped, 'media_object', {}))
  220. s.append(wrapped.flair_text)
  221. s.append(wrapped.flair_css_class)
  222. # if browsing a single subreddit, incorporate link flair position
  223. # in the key so 'flair' buttons show up appropriately for mods
  224. if hasattr(c.site, '_id'):
  225. s.append(c.site.link_flair_position)
  226. return s
  227. def make_permalink(self, sr, force_domain=False):
  228. from r2.lib.template_helpers import get_domain
  229. p = "comments/%s/%s/" % (self._id36, title_to_url(self.title))
  230. # promoted links belong to a separate subreddit and shouldn't
  231. # include that in the path
  232. if self.promoted is not None:
  233. if force_domain:
  234. res = "http://%s/%s" % (get_domain(cname=False,
  235. subreddit=False), p)
  236. else:
  237. res = "/%s" % p
  238. elif not c.cname and not force_domain:
  239. res = "/r/%s/%s" % (sr.name, p)
  240. elif sr != c.site or force_domain:
  241. if(c.cname and sr == c.site):
  242. res = "http://%s/%s" % (get_domain(cname=True,
  243. subreddit=False), p)
  244. else:
  245. res = "http://%s/r/%s/%s" % (get_domain(cname=False,
  246. subreddit=False), sr.name, p)
  247. else:
  248. res = "/%s" % p
  249. # WARNING: If we ever decide to add any ?foo=bar&blah parameters
  250. # here, Comment.make_permalink will need to be updated or else
  251. # it will fail.
  252. return res
  253. def make_permalink_slow(self, force_domain=False):
  254. return self.make_permalink(self.subreddit_slow,
  255. force_domain=force_domain)
  256. @staticmethod
  257. def _should_expunge_selftext(link):
  258. verdict = getattr(link, "verdict", "")
  259. if verdict not in ("admin-removed", "mod-removed"):
  260. return False
  261. if not c.user_is_loggedin:
  262. return True
  263. if c.user_is_admin:
  264. return False
  265. if c.user == link.author:
  266. return False
  267. if link.can_ban:
  268. return False
  269. return True
  270. @classmethod
  271. def add_props(cls, user, wrapped):
  272. from r2.lib.pages import make_link_child
  273. from r2.lib.count import incr_counts
  274. from r2.lib import media
  275. from r2.lib.utils import timeago
  276. from r2.lib.template_helpers import get_domain
  277. from r2.models.subreddit import FakeSubreddit
  278. from r2.lib.wrapped import CachedVariable
  279. # referencing c's getattr is cheap, but not as cheap when it
  280. # is in a loop that calls it 30 times on 25-200 things.
  281. user_is_admin = c.user_is_admin
  282. user_is_loggedin = c.user_is_loggedin
  283. pref_media = user.pref_media
  284. pref_frame = user.pref_frame
  285. pref_newwindow = user.pref_newwindow
  286. cname = c.cname
  287. site = c.site
  288. if user_is_loggedin:
  289. try:
  290. saved = LinkSavesByAccount.fast_query(user, wrapped)
  291. hidden = LinkHidesByAccount.fast_query(user, wrapped)
  292. except tdb_cassandra.TRANSIENT_EXCEPTIONS as e:
  293. g.log.warning("Cassandra save/hide lookup failed: %r", e)
  294. saved = hidden = {}
  295. clicked = {}
  296. else:
  297. saved = hidden = clicked = {}
  298. for item in wrapped:
  299. show_media = False
  300. if not hasattr(item, "score_fmt"):
  301. item.score_fmt = Score.number_only
  302. if c.render_style == 'compact':
  303. item.score_fmt = Score.points
  304. item.pref_compress = user.pref_compress
  305. if user.pref_compress and item.promoted is None:
  306. item.render_css_class = "compressed link"
  307. item.score_fmt = Score.points
  308. elif pref_media == 'on' and not user.pref_compress:
  309. show_media = True
  310. elif pref_media == 'subreddit' and item.subreddit.show_media:
  311. show_media = True
  312. elif item.promoted and item.has_thumbnail:
  313. if user_is_loggedin and item.author_id == user._id:
  314. show_media = True
  315. elif pref_media != 'off' and not user.pref_compress:
  316. show_media = True
  317. item.nsfw_str = item._nsfw.findall(item.title)
  318. item.over_18 = bool(item.over_18 or item.subreddit.over_18 or
  319. item.nsfw_str)
  320. item.nsfw = item.over_18 and user.pref_label_nsfw
  321. item.is_author = (user == item.author)
  322. item.thumbnail_sprited = False
  323. # always show a promo author their own thumbnail
  324. if item.promoted and (user_is_admin or item.is_author) and item.has_thumbnail:
  325. item.thumbnail = media.thumbnail_url(item)
  326. elif user.pref_no_profanity and item.over_18 and not c.site.over_18:
  327. if show_media:
  328. item.thumbnail = "nsfw"
  329. item.thumbnail_sprited = True
  330. else:
  331. item.thumbnail = ""
  332. elif not show_media:
  333. item.thumbnail = ""
  334. elif item.has_thumbnail:
  335. item.thumbnail = media.thumbnail_url(item)
  336. elif item.is_self:
  337. item.thumbnail = "self"
  338. item.thumbnail_sprited = True
  339. else:
  340. item.thumbnail = "default"
  341. item.thumbnail_sprited = True
  342. item.score = max(0, item.score)
  343. if getattr(item, "domain_override", None):
  344. item.domain = item.domain_override
  345. else:
  346. item.domain = (domain(item.url) if not item.is_self
  347. else 'self.' + item.subreddit.name)
  348. item.urlprefix = ''
  349. if user_is_loggedin:
  350. item.saved = (user, item) in saved
  351. item.hidden = (user, item) in hidden
  352. item.clicked = bool(clicked.get((user, item, 'click')))
  353. else:
  354. item.saved = item.hidden = item.clicked = False
  355. item.num = None
  356. item.permalink = item.make_permalink(item.subreddit)
  357. if item.is_self:
  358. item.url = item.make_permalink(item.subreddit,
  359. force_domain=True)
  360. if g.shortdomain:
  361. item.shortlink = g.shortdomain + '/' + item._id36
  362. # do we hide the score?
  363. if user_is_admin:
  364. item.hide_score = False
  365. elif item.promoted and item.score <= 0:
  366. item.hide_score = True
  367. elif user == item.author:
  368. item.hide_score = False
  369. # TODO: uncomment to let gold users see the score of upcoming links
  370. # elif user.gold:
  371. # item.hide_score = False
  372. elif item._date > timeago("2 hours"):
  373. item.hide_score = True
  374. else:
  375. item.hide_score = False
  376. # store user preferences locally for caching
  377. item.pref_frame = pref_frame
  378. item.newwindow = pref_newwindow
  379. # is this link a member of a different (non-c.site) subreddit?
  380. item.different_sr = (isinstance(site, FakeSubreddit) or
  381. site.name != item.subreddit.name)
  382. if user_is_loggedin and item.author_id == user._id:
  383. item.nofollow = False
  384. elif item.score <= 1 or item._spam or item.author._spam:
  385. item.nofollow = True
  386. else:
  387. item.nofollow = False
  388. item.subreddit_path = item.subreddit.path
  389. if cname:
  390. item.subreddit_path = ("http://" +
  391. get_domain(cname=(site == item.subreddit),
  392. subreddit=False))
  393. if site != item.subreddit:
  394. item.subreddit_path += item.subreddit.path
  395. item.domain_path = "/domain/%s/" % item.domain
  396. if item.is_self:
  397. item.domain_path = item.subreddit_path
  398. # attach video or selftext as needed
  399. item.link_child, item.editable = make_link_child(item)
  400. item.tblink = "http://%s/tb/%s" % (
  401. get_domain(cname=cname, subreddit=False),
  402. item._id36)
  403. if item.is_self:
  404. item.href_url = item.permalink
  405. else:
  406. item.href_url = item.url
  407. # show the toolbar if the preference is set and the link
  408. # is neither a promoted link nor a self post
  409. if pref_frame and not item.is_self and not item.promoted:
  410. item.mousedown_url = item.tblink
  411. else:
  412. item.mousedown_url = None
  413. item.fresh = not any((item.likes != None,
  414. item.saved,
  415. item.clicked,
  416. item.hidden,
  417. item._deleted,
  418. item._spam))
  419. # bits that we will render stubs (to make the cached
  420. # version more flexible)
  421. item.num = CachedVariable("num")
  422. item.numcolmargin = CachedVariable("numcolmargin")
  423. item.commentcls = CachedVariable("commentcls")
  424. item.midcolmargin = CachedVariable("midcolmargin")
  425. item.comment_label = CachedVariable("numcomments")
  426. item.lastedited = CachedVariable("lastedited")
  427. item.as_deleted = False
  428. if item.deleted and not c.user_is_admin:
  429. item.author = DeletedUser()
  430. item.as_deleted = True
  431. item.approval_checkmark = None
  432. item_age = datetime.now(g.tz) - item._date
  433. if item_age.days > g.VOTE_AGE_LIMIT and item.promoted is None:
  434. item.votable = False
  435. else:
  436. item.votable = True
  437. if item.can_ban:
  438. verdict = getattr(item, "verdict", None)
  439. if verdict in ('admin-approved', 'mod-approved'):
  440. approver = None
  441. if getattr(item, "ban_info", None):
  442. approver = item.ban_info.get("unbanner", None)
  443. if approver:
  444. item.approval_checkmark = _("approved by %s") % approver
  445. else:
  446. item.approval_checkmark = _("approved by a moderator")
  447. item.expunged = False
  448. if item.is_self:
  449. item.expunged = Link._should_expunge_selftext(item)
  450. item.editted = getattr(item, "editted", False)
  451. taglinetext = ''
  452. if item.different_sr:
  453. author_text = (" <span>" + _("by %(author)s to %(reddit)s") +
  454. "</span>")
  455. else:
  456. author_text = " <span>" + _("by %(author)s") + "</span>"
  457. if item.editted:
  458. if item.score_fmt == Score.points:
  459. taglinetext = ("<span>" +
  460. _("%(score)s submitted %(when)s "
  461. "ago%(lastedited)s") +
  462. "</span>")
  463. taglinetext += author_text
  464. elif item.different_sr:
  465. taglinetext = _("submitted %(when)s ago%(lastedited)s "
  466. "by %(author)s to %(reddit)s")
  467. else:
  468. taglinetext = _("submitted %(when)s ago%(lastedited)s "
  469. "by %(author)s")
  470. else:
  471. if item.score_fmt == Score.points:
  472. taglinetext = ("<span>" +
  473. _("%(score)s submitted %(when)s ago") +
  474. "</span>")
  475. taglinetext += author_text
  476. elif item.different_sr:
  477. taglinetext = _("submitted %(when)s ago by %(author)s "
  478. "to %(reddit)s")
  479. else:
  480. taglinetext = _("submitted %(when)s ago by %(author)s")
  481. item.taglinetext = taglinetext
  482. if user_is_loggedin:
  483. incr_counts(wrapped)
  484. # Run this last
  485. Printable.add_props(user, wrapped)
  486. @property
  487. def subreddit_slow(self):
  488. from subreddit import Subreddit
  489. """return's a link's subreddit. in most case the subreddit is already
  490. on the wrapped link (as .subreddit), and that should be used
  491. when possible. """
  492. return Subreddit._byID(self.sr_id, True, return_dict=False)
  493. class LinksByUrl(tdb_cassandra.View):
  494. _use_db = True
  495. _connection_pool = 'main'
  496. _read_consistency_level = tdb_cassandra.CL.ONE
  497. @classmethod
  498. def _key_from_url(cls, url):
  499. if not utils.domain(url) in g.case_sensitive_domains:
  500. keyurl = _force_utf8(UrlParser.base_url(url.lower()))
  501. else:
  502. # Convert only hostname to lowercase
  503. up = UrlParser(url)
  504. up.hostname = up.hostname.lower()
  505. keyurl = _force_utf8(UrlParser.base_url(up.unparse()))
  506. return keyurl
  507. # Note that there are no instances of PromotedLink or LinkCompressed,
  508. # so overriding their methods here will not change their behaviour
  509. # (except for add_props). These classes are used to override the
  510. # render_class on a Wrapped to change the template used for rendering
  511. class PromotedLink(Link):
  512. _nodb = True
  513. @classmethod
  514. def add_props(cls, user, wrapped):
  515. Link.add_props(user, wrapped)
  516. user_is_sponsor = c.user_is_sponsor
  517. status_dict = dict((v, k) for k, v in PROMOTE_STATUS.iteritems())
  518. for item in wrapped:
  519. # these are potentially paid for placement
  520. item.nofollow = True
  521. item.user_is_sponsor = user_is_sponsor
  522. status = getattr(item, "promote_status", -1)
  523. if item.is_author or c.user_is_sponsor:
  524. item.rowstyle = "link " + PROMOTE_STATUS.name[status].lower()
  525. else:
  526. item.rowstyle = "link promoted"
  527. # Run this last
  528. Printable.add_props(user, wrapped)
  529. def make_comment_gold_message(comment, user_gilded):
  530. author = Account._byID(comment.author_id, data=True)
  531. if not comment._deleted and not author._deleted:
  532. author_name = author.name
  533. else:
  534. author_name = _("[deleted]")
  535. if comment.gildings == 0:
  536. return None
  537. if c.user_is_loggedin and comment.author_id == c.user._id:
  538. gilded_message = ungettext(
  539. "a redditor gifted you a month of reddit gold for this comment.",
  540. "redditors have gifted you %(months)d months of reddit gold for "
  541. "this comment.",
  542. comment.gildings
  543. )
  544. elif user_gilded:
  545. gilded_message = ungettext(
  546. "you have gifted reddit gold to %(recipient)s for this comment.",
  547. "you and other redditors have gifted %(months)d months of "
  548. "reddit gold to %(recipient)s for this comment.",
  549. comment.gildings
  550. )
  551. else:
  552. gilded_message = ungettext(
  553. "a redditor has gifted reddit gold to %(recipient)s for this "
  554. "comment.",
  555. "redditors have gifted %(months)d months of reddit gold to "
  556. "%(recipient)s for this comment.",
  557. comment.gildings
  558. )
  559. return gilded_message % dict(
  560. recipient=author_name,
  561. months=comment.gildings,
  562. )
  563. class Comment(Thing, Printable):
  564. _data_int_props = Thing._data_int_props + ('reported', 'gildings')
  565. _defaults = dict(reported=0,
  566. parent_id=None,
  567. moderator_banned=False,
  568. new=False,
  569. gildings=0,
  570. banned_before_moderator=False)
  571. _essentials = ('link_id', 'author_id')
  572. def _markdown(self):
  573. pass
  574. @classmethod
  575. def _new(cls, author, link, parent, body, ip):
  576. from r2.lib.db.queries import changed
  577. c = Comment(_ups=1,
  578. body=body,
  579. link_id=link._id,
  580. sr_id=link.sr_id,
  581. author_id=author._id,
  582. ip=ip)
  583. c._spam = author._spam
  584. if author._spam:
  585. g.stats.simple_event('spam.autoremove.comment')
  586. #these props aren't relations
  587. if parent:
  588. c.parent_id = parent._id
  589. link._incr('num_comments', 1)
  590. to = None
  591. name = 'inbox'
  592. if parent:
  593. to = Account._byID(parent.author_id, True)
  594. elif link.is_self and not link.noselfreply:
  595. to = Account._byID(link.author_id, True)
  596. name = 'selfreply'
  597. c._commit()
  598. changed(link, True) # link's number of comments changed
  599. inbox_rel = None
  600. # only global admins can be message spammed.
  601. # Don't send the message if the recipient has blocked
  602. # the author
  603. if to and ((not c._spam and author._id not in to.enemies)
  604. or to.name in g.admins):
  605. # When replying to your own comment, record the inbox
  606. # relation, but don't give yourself an orangered
  607. orangered = (to.name != author.name)
  608. inbox_rel = Inbox._add(to, c, name, orangered=orangered)
  609. return (c, inbox_rel)
  610. def _save(self, user):
  611. CommentSavesByAccount._save(user, self)
  612. def _unsave(self, user):
  613. CommentSavesByAccount._unsave(user, self)
  614. @property
  615. def subreddit_slow(self):
  616. from subreddit import Subreddit
  617. """return's a comments's subreddit. in most case the subreddit is already
  618. on the wrapped link (as .subreddit), and that should be used
  619. when possible. if sr_id does not exist, then use the parent link's"""
  620. self._safe_load()
  621. if hasattr(self, 'sr_id'):
  622. sr_id = self.sr_id
  623. else:
  624. l = Link._byID(self.link_id, True)
  625. sr_id = l.sr_id
  626. return Subreddit._byID(sr_id, True, return_dict=False)
  627. def keep_item(self, wrapped):
  628. return True
  629. cache_ignore = set(["subreddit", "link", "to"]
  630. ).union(Printable.cache_ignore)
  631. @staticmethod
  632. def wrapped_cache_key(wrapped, style):
  633. s = Printable.wrapped_cache_key(wrapped, style)
  634. s.extend([wrapped.body])
  635. return s
  636. def make_permalink(self, link, sr=None, context=None, anchor=False):
  637. url = link.make_permalink(sr) + self._id36
  638. if context:
  639. url += "?context=%d" % context
  640. if anchor:
  641. url += "#%s" % self._id36
  642. return url
  643. def make_permalink_slow(self, context=None, anchor=False):
  644. l = Link._byID(self.link_id, data=True)
  645. return self.make_permalink(l, l.subreddit_slow,
  646. context=context, anchor=anchor)
  647. def _gild(self, user):
  648. self._incr("gildings")
  649. GildedCommentsByAccount.gild_comment(user, self)
  650. @classmethod
  651. def add_props(cls, user, wrapped):
  652. from r2.lib.template_helpers import add_attr, get_domain
  653. from r2.lib.wrapped import CachedVariable
  654. from r2.lib.pages import WrappedUser
  655. #fetch parent links
  656. links = Link._byID(set(l.link_id for l in wrapped), data=True,
  657. return_dict=True, stale=True)
  658. # fetch authors
  659. authors = Account._byID(set(l.author_id for l in links.values()), data=True,
  660. return_dict=True, stale=True)
  661. #get srs for comments that don't have them (old comments)
  662. for cm in wrapped:
  663. if not hasattr(cm, 'sr_id'):
  664. cm.sr_id = links[cm.link_id].sr_id
  665. subreddits = Subreddit._byID(set(cm.sr_id for cm in wrapped),
  666. data=True, return_dict=False, stale=True)
  667. cids = dict((w._id, w) for w in wrapped)
  668. parent_ids = set(cm.parent_id for cm in wrapped
  669. if getattr(cm, 'parent_id', None)
  670. and cm.parent_id not in cids)
  671. parents = {}
  672. if parent_ids:
  673. parents = Comment._byID(parent_ids, data=True, stale=True)
  674. can_reply_srs = set(s._id for s in subreddits if s.can_comment(user)) \
  675. if c.user_is_loggedin else set()
  676. can_reply_srs.add(get_promote_srid())
  677. min_score = user.pref_min_comment_score
  678. profilepage = c.profilepage
  679. user_is_admin = c.user_is_admin
  680. user_is_loggedin = c.user_is_loggedin
  681. focal_comment = c.focal_comment
  682. cname = c.cname
  683. site = c.site
  684. if user_is_loggedin:
  685. gilded = [comment for comment in wrapped if comment.gildings > 0]
  686. try:
  687. user_gildings = GildedCommentsByAccount.fast_query(user,
  688. gilded)
  689. except tdb_cassandra.TRANSIENT_EXCEPTIONS as e:
  690. g.log.warning("Cassandra gilding lookup failed: %r", e)
  691. user_gildings = {}
  692. try:
  693. saved = CommentSavesByAccount.fast_query(user, wrapped)
  694. except tdb_cassandra.TRANSIENT_EXCEPTIONS as e:
  695. g.log.warning("Cassandra comment save lookup failed: %r", e)
  696. saved = {}
  697. else:
  698. user_gildings = {}
  699. saved = {}
  700. for item in wrapped:
  701. # for caching:
  702. item.profilepage = c.profilepage
  703. item.link = links.get(item.link_id)
  704. if (item.link._score <= 1 or item.score < 3 or
  705. item.link._spam or item._spam or item.author._spam):
  706. item.nofollow = True
  707. else:
  708. item.nofollow = False
  709. if not hasattr(item, 'subreddit'):
  710. item.subreddit = item.subreddit_slow
  711. if item.author_id == item.link.author_id and not item.link._deleted:
  712. add_attr(item.attribs, 'S',
  713. link=item.link.make_permalink(item.subreddit))
  714. if not hasattr(item, 'target'):
  715. item.target = "_top" if cname else None
  716. if item.parent_id:
  717. if item.parent_id in cids:
  718. item.parent_permalink = '#' + utils.to36(item.parent_id)
  719. else:
  720. parent = parents[item.parent_id]
  721. item.parent_permalink = parent.make_permalink(item.link, item.subreddit)
  722. else:
  723. item.parent_permalink = None
  724. item.can_reply = False
  725. if c.can_reply or (item.sr_id in can_reply_srs):
  726. age = datetime.now(g.tz) - item._date
  727. if item.link.promoted or age.days < g.REPLY_AGE_LIMIT:
  728. item.can_reply = True
  729. if user_is_loggedin:
  730. item.user_gilded = (user, item) in user_gildings
  731. item.saved = (user, item) in saved
  732. else:
  733. item.user_gilded = False
  734. item.saved = False
  735. item.gilded_message = make_comment_gold_message(item,
  736. item.user_gilded)
  737. # not deleted on profile pages,
  738. # deleted if spam and not author or admin
  739. item.deleted = (not profilepage and
  740. (item._deleted or
  741. (item._spam and
  742. item.author != user and
  743. not item.show_spam)))
  744. extra_css = ''
  745. if item.deleted:
  746. extra_css += "grayed"
  747. if not user_is_admin:
  748. item.author = DeletedUser()
  749. item.body = '[deleted]'
  750. if focal_comment == item._id36:
  751. extra_css += " border"
  752. if profilepage:
  753. item.link_author = WrappedUser(authors[item.link.author_id])
  754. item.subreddit_path = item.subreddit.path
  755. if cname:
  756. item.subreddit_path = ("http://" +
  757. get_domain(cname=(site == item.subreddit),
  758. subreddit=False))
  759. if site != item.subreddit:
  760. item.subreddit_path += item.subreddit.path
  761. item.full_comment_path = item.link.make_permalink(item.subreddit)
  762. # don't collapse for admins, on profile pages, or if deleted
  763. item.collapsed = False
  764. if ((item.score < min_score) and not (profilepage or
  765. item.deleted or user_is_admin)):
  766. item.collapsed = True
  767. item.collapsed_reason = _("comment score below threshold")
  768. if user_is_loggedin and item.author_id in c.user.enemies:
  769. if "grayed" not in extra_css:
  770. extra_css += " grayed"
  771. item.collapsed = True
  772. item.collapsed_reason = _("blocked user")
  773. item.editted = getattr(item, "editted", False)
  774. item.render_css_class = "comment %s" % CachedVariable("time_period")
  775. #will get updated in builder
  776. item.num_children = 0
  777. item.score_fmt = Score.points
  778. item.permalink = item.make_permalink(item.link, item.subreddit)
  779. item.is_author = (user == item.author)
  780. item.is_focal = (focal_comment == item._id36)
  781. item_age = c.start_time - item._date
  782. if item_age.days > g.VOTE_AGE_LIMIT:
  783. item.votable = False
  784. else:
  785. item.votable = True
  786. #will seem less horrible when add_props is in pages.py
  787. from r2.lib.pages import UserText
  788. item.usertext = UserText(item, item.body,
  789. editable=item.is_author,
  790. nofollow=item.nofollow,
  791. target=item.target,
  792. extra_css=extra_css)
  793. item.lastedited = CachedVariable("lastedited")
  794. # Run this last
  795. Printable.add_props(user, wrapped)
  796. class CommentSortsCache(tdb_cassandra.View):
  797. """A cache of the sort-values of comments to avoid looking up all
  798. of the comments in a big tree at render-time just to determine
  799. the candidate order"""
  800. _use_db = True
  801. _value_type = 'float'
  802. _connection_pool = 'main'
  803. _read_consistency_level = tdb_cassandra.CL.ONE
  804. _fetch_all_columns = True
  805. class StarkComment(Comment):
  806. """Render class for the comments in the top-comments display in
  807. the reddit toolbar"""
  808. _nodb = True
  809. class MoreMessages(Printable):
  810. cachable = False
  811. display = ""
  812. new = False
  813. was_comment = False
  814. is_collapsed = True
  815. def __init__(self, parent, child):
  816. self.parent = parent
  817. self.child = child
  818. @staticmethod
  819. def wrapped_cache_key(item, style):
  820. return False
  821. @property
  822. def _fullname(self):
  823. return self.parent._fullname
  824. @property
  825. def _id36(self):
  826. return self.parent._id36
  827. @property
  828. def subject(self):
  829. return self.parent.subject
  830. @property
  831. def childlisting(self):
  832. return self.child
  833. @property
  834. def to(self):
  835. return self.parent.to
  836. @property
  837. def author(self):
  838. return self.parent.author
  839. @property
  840. def recipient(self):
  841. return self.parent.recipient
  842. @property
  843. def sr_id(self):
  844. return self.parent.sr_id
  845. @property
  846. def subreddit(self):
  847. return self.parent.subreddit
  848. class MoreComments(Printable):
  849. cachable = False
  850. display = ""
  851. @staticmethod
  852. def wrapped_cache_key(item, style):
  853. return False
  854. def __init__(self, link, depth, parent_id=None):
  855. from r2.lib.wrapped import CachedVariable
  856. if parent_id is not None:
  857. id36 = utils.to36(parent_id)
  858. self.parent_id = parent_id
  859. self.parent_name = "t%s_%s" % (utils.to36(Comment._type_id), id36)
  860. self.parent_permalink = link.make_permalink_slow() + id36
  861. self.link_name = link._fullname
  862. self.link_id = link._id
  863. self.depth = depth
  864. self.children = []
  865. self.count = 0
  866. self.previous_visits_hex = CachedVariable("previous_visits_hex")
  867. @property
  868. def _fullname(self):
  869. return "t%s_%s" % (utils.to36(Comment._type_id), self._id36)
  870. @property
  871. def _id36(self):
  872. return utils.to36(self.children[0]) if self.children else '_'
  873. class MoreRecursion(MoreComments):
  874. pass
  875. class MoreChildren(MoreComments):
  876. pass
  877. class Message(Thing, Printable):
  878. _defaults = dict(reported=0,
  879. was_comment=False,
  880. parent_id=None,
  881. new=False,
  882. first_message=None,
  883. to_id=None,
  884. sr_id=None,
  885. to_collapse=None,
  886. author_collapse=None,
  887. from_sr=False)
  888. _data_int_props = Thing._data_int_props + ('reported',)
  889. _essentials = ('author_id',)
  890. cache_ignore = set(["to", "subreddit"]).union(Printable.cache_ignore)
  891. @classmethod
  892. def _new(cls, author, to, subject, body, ip, parent=None, sr=None,
  893. from_sr=False):
  894. m = Message(subject=subject, body=body, author_id=author._id, new=True,
  895. ip=ip, from_sr=from_sr)
  896. m._spam = author._spam
  897. if author._spam:
  898. g.stats.simple_event('spam.autoremove.message')
  899. sr_id = None
  900. # check to see if the recipient is a subreddit and swap args accordingly
  901. if to and isinstance(to, Subreddit):
  902. if from_sr:
  903. raise CreationError("Cannot send from SR to SR")
  904. to_subreddit = True
  905. to, sr = None, to
  906. else:
  907. to_subreddit = False
  908. if sr:
  909. sr_id = sr._id
  910. if parent:
  911. m.parent_id = parent._id
  912. if parent.first_message:
  913. m.first_message = parent.first_message
  914. else:
  915. m.first_message = parent._id
  916. if parent.sr_id:
  917. sr_id = parent.sr_id
  918. if not to and not sr_id:
  919. raise CreationError("Message created with neither to nor sr_id")
  920. if from_sr and not sr_id:
  921. raise CreationError("Message sent from_sr without setting sr")
  922. m.to_id = to._id if to else None
  923. if sr_id is not None:
  924. m.sr_id = sr_id
  925. m._commit()
  926. if sr_id and not sr:
  927. sr = Subreddit._byID(sr_id)
  928. inbox_rel = []
  929. if sr_id:
  930. # if there is a subreddit id, and it's either a reply or
  931. # an initial message to an SR, add to the moderator inbox
  932. # (i.e., don't do it for automated messages from the SR)
  933. if parent or to_subreddit and not from_sr:
  934. inbox_rel.append(ModeratorInbox._add(sr, m, 'inbox'))
  935. if author.name in g.admins:
  936. m.distinguished = 'admin'
  937. m._commit()
  938. elif sr.is_moderator(author):
  939. m.distinguished = 'yes'
  940. m._commit()
  941. # if there is a "to" we may have to create an inbox relation as well
  942. # also, only global admins can be message spammed.
  943. if to and (not m._spam or to.name in g.admins):
  944. # if the current "to" is not a sr moderator,
  945. # they need to be notified
  946. if not sr_id or not sr.is_moderator(to):
  947. # Record the inbox relation, but don't give the user
  948. # an orangered, if they PM themselves.
  949. # Don't notify on PMs from blocked users, either
  950. orangered = (to.name != author.name and
  951. author._id not in to.enemies)
  952. inbox_rel.append(Inbox._add(to, m, 'inbox',
  953. orangered=orangered))
  954. # find the message originator
  955. elif sr_id and m.first_message:
  956. first = Message._byID(m.first_message, True)
  957. orig = Account._byID(first.author_id, True)
  958. # if the originator is not a moderator...
  959. if not sr.is_moderator(orig) and orig._id != author._id:
  960. inbox_rel.append(Inbox._add(orig, m, 'inbox'))
  961. return (m, inbox_rel)
  962. @property
  963. def permalink(self):
  964. return "/message/messages/%s" % self._id36
  965. def can_view_slow(self):
  966. if c.user_is_loggedin:
  967. # simple case from before:
  968. if (c.user_is_admin or
  969. c.user._id in (self.author_id, self.to_id)):
  970. return True
  971. elif self.sr_id:
  972. sr = Subreddit._byID(self.sr_id)
  973. is_moderator = sr.is_moderator(c.user)
  974. # moderators can view messages on subreddits they moderate
  975. if is_moderator:
  976. return True
  977. elif self.first_message:
  978. first = Message._byID(self.first_message, True)
  979. return (first.author_id == c.user._id)
  980. @classmethod
  981. def add_props(cls, user, wrapped):
  982. from r2.lib.db import queries
  983. #TODO global-ish functions that shouldn't be here?
  984. #reset msgtime after this request
  985. msgtime = c.have_messages
  986. # make sure there is a sr_id set:
  987. for w in wrapped:
  988. if not hasattr(w, "sr_id"):
  989. w.sr_id = None
  990. # load the to fields if one exists
  991. to_ids = set(w.to_id for w in wrapped if w.to_id is not None)
  992. tos = Account._byID(to_ids, True) if to_ids else {}
  993. # load the subreddit field if one exists:
  994. sr_ids = set(w.sr_id for w in wrapped if w.sr_id is not None)
  995. m_subreddits = Subreddit._byID(sr_ids, data=True, return_dict=True)
  996. # load the links and their subreddits (if comment-as-message)
  997. links = Link._byID(set(l.link_id for l in wrapped if l.was_comment),
  998. data=True,
  999. return_dict=True)
  1000. # subreddits of the links (for comment-as-message)
  1001. l_subreddits = Subreddit._byID(set(l.sr_id for l in links.values()),
  1002. data=True, return_dict=True)
  1003. parents = Comment._byID(set(l.parent_id for l in wrapped
  1004. if l.parent_id and l.was_comment),
  1005. data=True, return_dict=True)
  1006. # load the unread list to determine message newness
  1007. unread = set(queries.get_unread_inbox(user))
  1008. msg_srs = set(m_subreddits[x.sr_id]
  1009. for x in wrapped if x.sr_id is not None
  1010. and isinstance(x.lookups[0], Message))
  1011. # load the unread mod list for the same reason
  1012. mod_unread = set(queries.get_unread_subreddit_messages_multi(msg_srs))
  1013. for item in wrapped:
  1014. item.to = tos.get(item.to_id)
  1015. if item.sr_id:
  1016. item.recipient = (item.author_id != c.user._id)
  1017. else:
  1018. item.recipient = (item.to_id == c.user._id)
  1019. # new-ness is stored on the relation
  1020. if item.author_id == c.user._id:
  1021. item.new = False
  1022. elif item._fullname in unread:
  1023. item.new = True
  1024. # wipe new messages if preferences say so, and this isn't a feed
  1025. # and it is in the user's personal inbox
  1026. if (item.new and c.user.pref_mark_messages_read
  1027. and c.extension not in ("rss", "xml", "api", "json")):
  1028. queries.set_unread(item.lookups[0],
  1029. c.user, False)
  1030. else:
  1031. item.new = (item._fullname in mod_unread and not item.to_id)
  1032. item.score_fmt = Score.none
  1033. item.message_style = ""
  1034. # comment as message:
  1035. if item.was_comment:
  1036. link = links[item.link_id]
  1037. sr = l_subreddits[link.sr_id]
  1038. item.to_collapse = False
  1039. item.author_collapse = False
  1040. item.link_title = link.title
  1041. item.permalink = item.lookups[0].make_permalink(link, sr=sr)
  1042. item.link_permalink = link.make_permalink(sr)
  1043. if item.parent_id:
  1044. item.subject = _('comment reply')
  1045. item.message_style = "comment-reply"
  1046. parent = parents[item.parent_id]
  1047. item.parent = parent._fullname
  1048. item.parent_permalink = parent.make_permalink(link, sr)
  1049. else:
  1050. item.subject = _('post reply')
  1051. item.message_style = "post-reply"
  1052. elif item.sr_id is not None:
  1053. item.subreddit = m_subreddits[item.sr_id]
  1054. item.hide_author = False
  1055. if getattr(item, "from_sr", False):
  1056. if not (item.subreddit.is_moderator(c.user) or
  1057. c.user_is_admin):
  1058. item.author = item.subreddit
  1059. item.hide_author = True
  1060. item.is_collapsed = None
  1061. if not item.new:
  1062. if item.recipient:
  1063. item.is_collapsed = item.to_collapse
  1064. if item.author_id == c.user._id:
  1065. item.is_collapsed = item.author_collapse
  1066. if c.user.pref_collapse_read_messages:
  1067. item.is_collapsed = (item.is_collapsed is not False)
  1068. if item.author_id in c.user.enemies and not item.was_comment:
  1069. item.is_collapsed = True
  1070. if not c.user_is_admin:
  1071. item.subject = _('[message from blocked user]')
  1072. item.body = _('[unblock user to see this message]')
  1073. taglinetext = ''
  1074. if item.hide_author:
  1075. taglinetext = _("subreddit message %(author)s sent %(when)s ago")
  1076. elif item.author_id == c.user._id:
  1077. taglinetext = _("to %(dest)s sent %(when)s ago")
  1078. elif item.to_id == c.user._id or item.to_id is None:
  1079. taglinetext = _("from %(author)s sent %(when)s ago")
  1080. else:
  1081. taglinetext = _("to %(dest)s from %(author)s sent %(when)s ago")
  1082. item.taglinetext = taglinetext
  1083. item.dest = item.to.name if item.to else ""
  1084. if item.sr_id:
  1085. if item.hide_author:
  1086. item.updated_author = _("via %(subreddit)s")
  1087. else:
  1088. item.updated_author = _("%(author)s via %(subreddit)s")
  1089. else:
  1090. item.updated_author = ''
  1091. # Run this last
  1092. Printable.add_props(user, wrapped)
  1093. @property
  1094. def subreddit_slow(self):
  1095. from subreddit import Subreddit
  1096. if self.sr_id:
  1097. return Subreddit._byID(self.sr_id)
  1098. @staticmethod
  1099. def wrapped_cache_key(wrapped, style):
  1100. s = Printable.wrapped_cache_key(wrapped, style)
  1101. s.extend([wrapped.new, wrapped.collapsed])
  1102. return s
  1103. def keep_item(self, wrapped):
  1104. return True
  1105. class SaveHide(Relation(Account, Link)): pass
  1106. class Click(Relation(Account, Link)): pass
  1107. class GildedCommentsByAccount(tdb_cassandra.DenormalizedRelation):
  1108. _use_db = True
  1109. _last_modified_name = 'Gilding'
  1110. _views = []
  1111. @classmethod
  1112. def value_for(cls, thing1, thing2, opaque):
  1113. return ''
  1114. @classmethod
  1115. def gild_comment(cls, user, comment):
  1116. cls.create(user, [comment])
  1117. class _SaveHideByAccount(tdb_cassandra.DenormalizedRelation):
  1118. @classmethod
  1119. def value_for(cls, thing1, thing2, opaque):
  1120. return ''
  1121. @classmethod
  1122. def _cached_queries(cls, user, thing):
  1123. return []
  1124. @classmethod
  1125. def _savehide(cls, user, things):
  1126. things = tup(things)
  1127. now = datetime.now(g.tz)
  1128. with CachedQueryMutator() as m:

Large files files are truncated, but you can click here to view the full file