PageRenderTime 58ms CodeModel.GetById 6ms RepoModel.GetById 1ms app.codeStats 0ms

/r2/r2/models/poll.py

https://github.com/wangmxf/lesswrong
Python | 395 lines | 391 code | 4 blank | 0 comment | 0 complexity | ba73216e9bc7d061e743e346fb77bf3f MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-2.1
  1. from __future__ import with_statement
  2. import re
  3. import datetime
  4. from pylons import c, g, request
  5. from r2.lib.db.thing import Thing, Relation, NotFound, MultiRelation, CreationError
  6. from account import Account
  7. from r2.lib.utils import to36, median
  8. from r2.lib.filters import safemarkdown, _force_unicode
  9. pages = None # r2.lib.pages imported dynamically further down
  10. class PollError(Exception):
  11. def __init__(self, message):
  12. Exception.__init__(self)
  13. self.message = message
  14. poll_re = re.compile(r"""
  15. \[\s*poll\s* # [poll] or [polltype]
  16. (?::\s* ([^\]]*?) )?
  17. \s*\]
  18. ((?:\s* {\s*[^}]+\s*} )*) # Poll options enclosed in curly braces
  19. """, re.VERBOSE)
  20. poll_options_re = re.compile(r"""
  21. {\s*([^}]+)\s*}
  22. """, re.VERBOSE)
  23. pollid_re = re.compile(r"""
  24. \[\s*pollid\s*:\s*([a-zA-Z0-9]+)\s*\]
  25. """, re.VERBOSE)
  26. scalepoll_re = re.compile(r"""^
  27. \s*([^.]+)\s*(\.{2,})\s*([^.]+)\s*
  28. $""", re.VERBOSE)
  29. def parsepolls(text, thing, dry_run = False):
  30. """
  31. Look for poll markup syntax, ie "[poll:polltype]{options}". Parse it,
  32. create a poll object, and replace the raw syntax with "[pollid:123]".
  33. `PollError` is raised if there are any errors in the syntax.
  34. :param dry_run: If true, the syntax is still checked, but no database objects are created.
  35. """
  36. def checkmatch(match):
  37. optionsText = match.group(2)
  38. options = poll_options_re.findall(optionsText)
  39. poll = createpoll(thing, match.group(1), options, dry_run = dry_run)
  40. pollid = "" if dry_run else str(poll._id)
  41. return "[pollid:" + pollid + "]"
  42. return re.sub(poll_re, checkmatch, text)
  43. def getpolls(text):
  44. polls = []
  45. matches = re.findall(pollid_re, text)
  46. for match in matches:
  47. try:
  48. pollid = int(str(match))
  49. polls.append(pollid)
  50. except: pass
  51. return polls
  52. def containspolls(text):
  53. return bool(re.match(poll_re, text) or re.match(pollid_re, text))
  54. # Look for poll IDs in a comment/article, like "[pollid:123]", find the
  55. # matching poll in the database, and convert it into an HTML implementation
  56. # of that poll. If there was at least one poll, puts poll options ('[]Vote
  57. # Anonymously [Submit]/[View Results] [Raw Data]') at the bottom
  58. def renderpolls(text, thing):
  59. polls_not_voted = []
  60. polls_voted = []
  61. oldballots = []
  62. def checkmatch(match):
  63. pollid = match.group(1)
  64. try:
  65. poll = Poll._byID(pollid, True)
  66. if poll.thingid != thing._id:
  67. return "Error: Poll belongs to a different comment"
  68. if poll.user_has_voted(c.user):
  69. polls_voted.append(pollid)
  70. return poll.render_results()
  71. else:
  72. polls_not_voted.append(pollid)
  73. return poll.render()
  74. except NotFound:
  75. return "Error: Poll not found!"
  76. text = re.sub(pollid_re, checkmatch, _force_unicode(text))
  77. if polls_voted or polls_not_voted:
  78. voted_on_all = not polls_not_voted
  79. page = _get_pageclass('PollWrapper')(thing, text, voted_on_all)
  80. text = page.render('html')
  81. return text
  82. def pollsandmarkdown(text, thing):
  83. ret = renderpolls(safemarkdown(text), thing)
  84. return ret
  85. def createpoll(thing, polltype, args, dry_run = False):
  86. poll = Poll.createpoll(thing, polltype, args, dry_run = dry_run)
  87. if g.write_query_queue:
  88. queries.new_poll(poll)
  89. return poll
  90. def exportvotes(pollids):
  91. csv_rows = []
  92. aliases = {'next_alias': 1}
  93. for pollid in pollids:
  94. poll = Poll._byID(pollid)
  95. ballots = poll.get_ballots()
  96. for ballot in ballots:
  97. row = ballot.export_row(aliases)
  98. csv_rows.append(row)
  99. return exportheader() + '\n'.join(csv_rows)
  100. def exportheader():
  101. return """#
  102. # Exported poll results from Less Wrong
  103. # Columns: user, pollid, response, date
  104. # user is either a username or a number (if the 'voted anonymously' button was
  105. # checked). Anonymous user numbers are shared between poll questions asked in a
  106. # single comment, but not between comments.
  107. # pollid is a site-wide unique identifier of the poll.
  108. # response is the user's answer to the poll. For multiple-choice polls, this is
  109. # the index of their choice, starting at zero. For scale polls, this is the
  110. # distance of their choice from the left, starting at zero. For probability and
  111. # numeric polls, this is a number.
  112. #
  113. """
  114. def _get_pageclass(name):
  115. # sidestep circular import issues
  116. global pages
  117. if not pages:
  118. from r2.lib import pages
  119. return getattr(pages, name)
  120. class PollType:
  121. ballot_class = None
  122. results_class = None
  123. def render(self, poll):
  124. return _get_pageclass(self.ballot_class)(poll).render('html')
  125. def render_results(self, poll):
  126. return _get_pageclass(self.results_class)(poll).render('html')
  127. def _check_num_choices(self, num):
  128. if num < 2:
  129. raise PollError('Polls must have at least two choices')
  130. if num > g.poll_max_choices:
  131. raise PollError('Polls cannot have more than {0} choices'.format(g.poll_max_choices))
  132. def _check_range(self, num, func, min, max, message):
  133. try:
  134. num = func(num)
  135. if min <= num <= max:
  136. return str(num)
  137. except:
  138. pass
  139. raise PollError(message)
  140. class MultipleChoicePoll(PollType):
  141. ballot_class = 'MultipleChoicePollBallot'
  142. results_class = 'MultipleChoicePollResults'
  143. def init_blank(self, poll):
  144. self._check_num_choices(len(poll.choices))
  145. poll.votes_for_choice = [0 for _ in poll.choices]
  146. def add_response(self, poll, response):
  147. poll.votes_for_choice[int(response)] = poll.votes_for_choice[int(response)] + 1
  148. def validate_response(self, poll, response):
  149. return self._check_range(response, int, 0, len(poll.choices) - 1, 'Invalid choice')
  150. class ScalePoll(PollType):
  151. ballot_class = 'ScalePollBallot'
  152. results_class = 'ScalePollResults'
  153. def init_blank(self, poll):
  154. parsed_poll = re.match(scalepoll_re, poll.polltypestring)
  155. poll.scalesize = len(parsed_poll.group(2))
  156. poll.leftlabel = parsed_poll.group(1)
  157. poll.rightlabel = parsed_poll.group(3)
  158. self._check_num_choices(poll.scalesize)
  159. poll.votes_for_choice = [0 for _ in range(poll.scalesize)]
  160. def add_response(self, poll, response):
  161. poll.votes_for_choice[int(response)] = poll.votes_for_choice[int(response)] + 1
  162. def validate_response(self, poll, response):
  163. return self._check_range(response, int, 0, poll.scalesize - 1, 'Invalid choice')
  164. class NumberPoll(PollType):
  165. ballot_class = 'NumberPollBallot'
  166. results_class = 'NumberPollResults'
  167. def init_blank(self, poll):
  168. poll.sum = 0
  169. poll.median = 0
  170. def add_response(self, poll, response):
  171. responsenum = float(response)
  172. poll.sum += responsenum
  173. responses = [float(ballot.response) for ballot in poll.get_ballots()]
  174. responses.sort()
  175. poll.median = median(responses)
  176. def validate_response(self, poll, response):
  177. return self._check_range(response, float, -2**64, 2**64, 'Invalid number')
  178. class ProbabilityPoll(NumberPoll):
  179. ballot_class = 'ProbabilityPollBallot'
  180. results_class = 'ProbabilityPollResults'
  181. def validate_response(self, poll, response):
  182. return self._check_range(response, float, 0, 1, 'Probability must be between 0 and 1')
  183. class Poll(Thing):
  184. @classmethod
  185. def createpoll(cls, thing, polltypestring, options, dry_run = False):
  186. assert dry_run == (thing is None)
  187. polltype = cls.normalize_polltype(polltypestring)
  188. poll = cls(thingid = thing and thing._id,
  189. polltype = polltype,
  190. polltypestring = polltypestring,
  191. choices = options)
  192. polltype_class = poll.polltype_class()
  193. if not polltype_class:
  194. raise PollError(u"Invalid poll type '{0}'".format(polltypestring))
  195. poll.init_blank()
  196. if not dry_run:
  197. thing.has_polls = True
  198. poll._commit()
  199. return poll
  200. @classmethod
  201. def normalize_polltype(self, polltype):
  202. #If not specified, default to multiplechoice
  203. if not polltype:
  204. return 'multiplechoice'
  205. polltype = polltype.lower()
  206. #If the poll type has a dot in it, then it's a scale, like 'agree.....disagree'
  207. if re.match(scalepoll_re, polltype):
  208. return 'scale'
  209. #Check against lists of synonyms
  210. if polltype in {'choice':1, 'c':1, 'list':1}:
  211. return 'multiplechoice'
  212. elif polltype in {'probability':1, 'prob':1, 'p':1, 'chance':1, 'likelihood':1}:
  213. return 'probability'
  214. elif polltype in {'number':1, 'numeric':1, 'num':1, 'n':1, 'float':1, 'double':1}:
  215. return 'number'
  216. else:
  217. return 'invalid'
  218. def polltype_class(self):
  219. if self.polltype == 'multiplechoice':
  220. return MultipleChoicePoll()
  221. elif self.polltype == 'scale' :
  222. return ScalePoll()
  223. elif self.polltype == 'probability' :
  224. return ProbabilityPoll()
  225. elif self.polltype == 'number':
  226. return NumberPoll()
  227. else:
  228. return None
  229. def init_blank(self):
  230. self.num_votes = 0
  231. self.polltype_class().init_blank(self)
  232. def add_response(self, response):
  233. self.num_votes = self.num_votes + 1
  234. self.polltype_class().add_response(self, response)
  235. # Mark the votes_for_choice list as dirty to ensure it gets persisted
  236. if hasattr(self, 'votes_for_choice'):
  237. self._dirties['votes_for_choice'] = self.votes_for_choice
  238. self._commit()
  239. def validate_response(self, response):
  240. return self.polltype_class().validate_response(self, response)
  241. def render(self):
  242. return self.polltype_class().render(self)
  243. def render_results(self):
  244. return self.polltype_class().render_results(self)
  245. def user_has_voted(self, user):
  246. if not c.user_is_loggedin:
  247. return False
  248. oldballots = self.get_user_ballot(user)
  249. return (len(oldballots) > 0)
  250. def get_user_ballot(poll, user):
  251. return list(Ballot._query(Ballot.c._thing1_id == user._id,
  252. Ballot.c._thing2_id == poll._id,
  253. data = True))
  254. def get_ballots(self):
  255. return list(Ballot._query(Ballot.c._thing2_id == self._id,
  256. data = True))
  257. def num_votes_for(self, choice):
  258. if self.votes_for_choice:
  259. return self.votes_for_choice[choice]
  260. else:
  261. raise TypeError
  262. def bar_length(self, choice, max_length):
  263. max_votes = max(self.votes_for_choice)
  264. if max_votes == 0:
  265. return 0
  266. return int(float(self.num_votes_for(choice)) / max_votes * max_length)
  267. def fraction_for(self, choice):
  268. return float(self.num_votes_for(choice)) / self.num_votes * 100
  269. def rendered_percentage_for(self, choice):
  270. return str(int(round(self.fraction_for(choice)))) + "%"
  271. #Get the total number of votes on this poll as a correctly-pluralized noun phrase, ie "123 votes" or "1 vote"
  272. def num_votes_string(self):
  273. if self.num_votes == 1:
  274. return "1 vote"
  275. else:
  276. return str(self.num_votes) + " votes"
  277. def get_property(self, property):
  278. if property == 'mean':
  279. return self.sum / self.num_votes
  280. elif property == 'median':
  281. return self.median
  282. class Ballot(Relation(Account, Poll)):
  283. @classmethod
  284. def submitballot(cls, user, comment, pollobj, response, anonymous, ip, spam):
  285. with g.make_lock('voting_on_%s' % pollobj._id):
  286. pollid = pollobj._id
  287. oldballot = list(cls._query(cls.c._thing1_id == user._id,
  288. cls.c._thing2_id == pollid))
  289. if len(oldballot):
  290. raise PollError('You already voted on this poll')
  291. ballot = Ballot(user, pollobj, response)
  292. ballot.ip = ip
  293. ballot.anonymous = anonymous
  294. ballot.date = datetime.datetime.now().isoformat()
  295. ballot.response = response
  296. ballot._commit()
  297. pollobj.add_response(response)
  298. return ballot
  299. def export_row(self, aliases):
  300. userid = self._thing1_id
  301. pollid = self._thing2_id
  302. if hasattr(self, 'anonymous') and self.anonymous:
  303. if not userid in aliases:
  304. aliases[userid] = aliases['next_alias']
  305. aliases['next_alias'] = aliases['next_alias'] + 1
  306. username = aliases[userid]
  307. else:
  308. username = Account._byID(userid, data = True).name
  309. return "\"{0}\",\"{1}\",\"{2}\",\"{3}\"".format(username, pollid, self.response, self.date)