Personal wiki notebook (not under development)

Forums.py 9.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import os.path
  2. import cherrypy
  3. from model.User import User
  4. from model.Notebook import Notebook
  5. from model.Note import Note
  6. from model.Tag import Tag
  7. from Expose import expose
  8. from Expire import strongly_expire
  9. from Validate import validate, Valid_string, Valid_int, Valid_friendly_id
  10. from Database import Valid_id, end_transaction
  11. from Users import grab_user_id
  12. from Notebooks import Notebooks
  13. from Users import Access_error
  14. from view.Forums_page import Forums_page
  15. from view.Forum_page import Forum_page
  16. from view.Forum_rss import Forum_rss
  17. from view.Main_page import Main_page
  18. class Forums( object ):
  19. """
  20. Controller for dealing with discussion forums, corresponding to the "/forums" URL.
  21. """
  22. def __init__( self, database, notebooks, users ):
  23. """
  24. Create a new Forums object, representing a collection of forums.
  25. @type database: controller.Database
  26. @param database: database that forums are stored in
  27. @type notebooks: controller.Users
  28. @param notebooks: controller for all notebooks
  29. @type users: controller.Users
  30. @param users: controller for all users
  31. @rtype: Forums
  32. @return: newly constructed Forums
  33. """
  34. self.__database = database
  35. self.__notebooks = notebooks
  36. self.__users = users
  37. self.__general = Forum( database, notebooks, users, u"general" )
  38. self.__support = Forum( database, notebooks, users, u"support" )
  39. @expose( view = Forums_page )
  40. @strongly_expire
  41. @end_transaction
  42. @grab_user_id
  43. @validate(
  44. user_id = Valid_id( none_okay = True ),
  45. )
  46. def index( self, user_id ):
  47. """
  48. Provide the information necessary to display the listing of available forums (currently hard-coded).
  49. @type user_id: unicode or NoneType
  50. @param user_id: id of the current user
  51. """
  52. result = self.__users.current( user_id )
  53. parents = [ notebook for notebook in result[ u"notebooks" ] if notebook.trash_id and not notebook.deleted ]
  54. if len( parents ) > 0:
  55. result[ "first_notebook" ] = parents[ 0 ]
  56. else:
  57. result[ "first_notebook" ] = None
  58. return result
  59. general = property( lambda self: self.__general )
  60. support = property( lambda self: self.__support )
  61. class Forum( object ):
  62. DEFAULT_THREAD_NAME = u"new discussion"
  63. def __init__( self, database, notebooks, users, name ):
  64. """
  65. Create a new Forum object, representing a single forum.
  66. @type database: controller.Database
  67. @param database: database that forums are stored in
  68. @type notebooks: controller.Users
  69. @param notebooks: controller for all notebooks
  70. @type users: controller.Users
  71. @param users: controller for all users
  72. @type name: unicode
  73. @param name: one-word name of this forum
  74. @rtype: Forums
  75. @return: newly constructed Forums
  76. """
  77. self.__database = database
  78. self.__notebooks = notebooks
  79. self.__users = users
  80. self.__name = name
  81. @expose( view = Forum_page, rss = Forum_rss )
  82. @strongly_expire
  83. @end_transaction
  84. @grab_user_id
  85. @validate(
  86. start = Valid_int( min = 0 ),
  87. count = Valid_int( min = 1, max = 50 ),
  88. user_id = Valid_id( none_okay = True ),
  89. note_id = Valid_id( none_okay = True ),
  90. )
  91. def index( self, start = 0, count = 50, note_id = None, user_id = None ):
  92. """
  93. Provide the information necessary to display the current threads within a forum.
  94. @type start: integer or NoneType
  95. @param start: index of first forum thread to display (optional, defaults to 0)
  96. @type count: integer or NoneType
  97. @param count: how many forum threads to display (optional, defaults to quite a few)
  98. @type note_id: unicode or NoneType
  99. @param note_id: id of thread to redirect to (optional, legacy support for old URLs)
  100. @type user_id: unicode or NoneType
  101. @param user_id: id of the current user
  102. @rtype: unicode
  103. @return: rendered HTML page
  104. """
  105. if note_id:
  106. return dict( redirect = os.path.join( cherrypy.request.path, note_id ) )
  107. result = self.__users.current( user_id )
  108. parents = [ notebook for notebook in result[ u"notebooks" ] if notebook.trash_id and not notebook.deleted ]
  109. if len( parents ) > 0:
  110. result[ "first_notebook" ] = parents[ 0 ]
  111. else:
  112. result[ "first_notebook" ] = None
  113. anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ), use_cache = True )
  114. if anonymous is None:
  115. raise Access_error()
  116. # load a slice of the list of the threads in this forum, excluding those with a default name
  117. threads = self.__database.select_many(
  118. Notebook,
  119. anonymous.sql_load_notebooks(
  120. parents_only = False, undeleted_only = True, tag_name = u"forum", tag_value = self.__name,
  121. exclude_notebook_name = self.DEFAULT_THREAD_NAME, reverse = True,
  122. start = start, count = count,
  123. )
  124. )
  125. # if there are no matching threads, then this forum doesn't exist
  126. if len( threads ) == 0:
  127. raise cherrypy.NotFound
  128. # count the total number of threads in this forum, excluding those with a default name
  129. total_thread_count = self.__database.select_one(
  130. int,
  131. anonymous.sql_count_notebooks(
  132. parents_only = False, undeleted_only = True, tag_name = u"forum", tag_value = self.__name,
  133. exclude_notebook_name = self.DEFAULT_THREAD_NAME,
  134. )
  135. )
  136. result[ "forum_name" ] = self.__name
  137. result[ "threads" ] = threads
  138. result[ "start" ] = start
  139. result[ "count" ] = count
  140. result[ "total_thread_count" ] = total_thread_count
  141. return result
  142. @expose( view = Main_page )
  143. @strongly_expire
  144. @end_transaction
  145. @grab_user_id
  146. @validate(
  147. thread_id = unicode,
  148. start = Valid_int( min = 0 ),
  149. count = Valid_int( min = 1, max = 50 ),
  150. note_id = Valid_id( none_okay = True ),
  151. posts = Valid_int(),
  152. user_id = Valid_id( none_okay = True ),
  153. )
  154. def default( self, thread_id, start = 0, count = 10, note_id = None, posts = None, user_id = None ):
  155. """
  156. Provide the information necessary to display a forum thread.
  157. @type thread_id: unicode
  158. @param thread_id: id or "friendly id" of thread notebook to display
  159. @type start: unicode or NoneType
  160. @param start: index of recent note to start with (defaults to 0, the most recent note)
  161. @type count: int or NoneType
  162. @param count: number of recent notes to display (defaults to 10 notes)
  163. @type note_id: unicode or NoneType
  164. @param note_id: id of single note to load (optional)
  165. @type posts: integer or NoneType
  166. @param posts: ignored. used for link-visitedness purposes on the client side
  167. @type user_id: unicode or NoneType
  168. @param user_id: id of the current user
  169. @rtype: unicode
  170. @return: rendered HTML page
  171. @raise Validation_error: one of the arguments is invalid
  172. """
  173. # first try loading the thread by id, and then if not found, try loading by "friendly id"
  174. try:
  175. Valid_id()( thread_id )
  176. if not self.__database.load( Notebook, thread_id ):
  177. raise ValueError()
  178. except ValueError:
  179. try:
  180. Valid_friendly_id()( thread_id )
  181. except ValueError:
  182. raise cherrypy.NotFound
  183. try:
  184. thread = self.__database.select_one( Notebook, Notebook.sql_load_by_friendly_id( thread_id ) )
  185. except:
  186. raise cherrypy.NotFound
  187. if not thread:
  188. raise cherrypy.NotFound
  189. thread_id = thread.object_id
  190. result = self.__users.current( user_id )
  191. result.update( self.__notebooks.old_notes( thread_id, start, count, user_id ) )
  192. # if a single note was requested, just return that one note
  193. if note_id:
  194. note = self.__database.load( Note, note_id )
  195. if note:
  196. result[ "notes" ] = [ note ]
  197. else:
  198. result[ "notes" ] = []
  199. return result
  200. @expose()
  201. @end_transaction
  202. @grab_user_id
  203. @validate(
  204. user_id = Valid_id( none_okay = True ),
  205. )
  206. def create_thread( self, user_id ):
  207. """
  208. Create a new forum thread with a blank post, and give the thread a default name. Then redirect
  209. to that new thread.
  210. @type user_id: unicode or NoneType
  211. @param user_id: id of current logged-in user (if any)
  212. @rtype dict
  213. @return { 'redirect': new_notebook_url }
  214. @raise Access_error: the current user doesn't have access to create a post
  215. @raise Validation_error: one of the arguments is invalid
  216. """
  217. if user_id is None:
  218. raise Access_error()
  219. user = self.__database.load( User, user_id )
  220. if user is None or not user.username or user.username == "anonymous":
  221. raise Access_error()
  222. anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ), use_cache = True )
  223. if anonymous is None:
  224. raise Access_error()
  225. # for now, crappy hard-coding to prevent just anyone from creating a blog thread
  226. if self.__name == u"blog" and user.username != u"witten":
  227. raise Access_error()
  228. # create the new notebook thread
  229. thread_id = self.__database.next_id( Notebook, commit = False )
  230. thread = Notebook.create( thread_id, self.DEFAULT_THREAD_NAME, user_id = user.object_id )
  231. self.__database.save( thread, commit = False )
  232. # associate the forum tag with the new notebook thread
  233. tag = self.__database.select_one( Tag, Tag.sql_load_by_name( u"forum", user_id = anonymous.object_id ) )
  234. self.__database.execute(
  235. anonymous.sql_save_notebook_tag( thread_id, tag.object_id, value = self.__name ),
  236. commit = False,
  237. )
  238. # give the anonymous user access to the new notebook thread
  239. self.__database.execute(
  240. anonymous.sql_save_notebook( thread_id, read_write = True, owner = False, own_notes_only = True ),
  241. commit = False,
  242. )
  243. # create a blank post in which the user can start off the thread
  244. note_id = self.__database.next_id( Notebook, commit = False )
  245. note = Note.create( note_id, u"<h3>", notebook_id = thread_id, startup = True, rank = 0, user_id = user_id )
  246. self.__database.save( note, commit = False )
  247. self.__database.commit()
  248. if self.__name == "blog":
  249. return dict(
  250. redirect = u"/blog/%s" % thread_id,
  251. )
  252. return dict(
  253. redirect = u"/forums/%s/%s" % ( self.__name, thread_id ),
  254. )